diff --git a/CHANGELOG.md b/CHANGELOG.md index cddd6faab..065f5d7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,6 +211,108 @@ hashes are given where a change is easiest to understand by reading the commit. through the ppmem window, so the second node's processor no longer takes the bus path for every cached access and runs as fast as the first. +### InfiniteReality front end + +- `src/dev/kona/` is the InfiniteReality (KONA) pipe's host interface: widget + id and identity registers, the one-wire board record of an Onyx2 deskside's + InfiniteReality2 (GE14-4, one or two RM7), the flash EEPROM and the + operation the graphics install tool programs it with, the command FIFO on + `GFifo` (a full ring answers the bus busy), command framing for the + textport, kernel and GL grammars, DMA and display-list memory. The + Onyx2 fits it with `[graphics] board`. +- The back end runs a display thread: `KonaBackend::attach` hands it a + `DisplayHandle` to complete retraces, raise the video interrupt and mark + activity for the driver's watchdog. A reset ends the epoch tickets were made + in, so stale calls are dropped. `stop`, `quiesce` and `resume` let it join and + park its threads. +- `KonaBackend` is the InfiniteReality drawing back end: it takes the jobs the + front end frames, carries the pipe's configuration (raster + managers, texture memory per manager, board set, managed area) and answers + `kona status` and `kona counters`. Words the command framers drop because + no command is open for them reach the back end as `Job::Dropped`. Nothing + draws yet. +- The KONA back end holds the pixel store (one array per word position of a + pixel's record, allocated when first written and published through fixed + pointers), lays a buffer configuration out in a pixel's words within the host's + budget (`kona status` shows the result and the refusals), and has packers for + the host and bus pixel formats. `kona pix ` shows a pixel's words. +- The replay reads recordings record by record, so the 71-million-record GL demo + recording can be censused per marked span (`KONA_GL_RECORDING`); the results + are in `clanker/kona/token-census.md`. +- The KONA back end draws the PROM's textport (colour, raster position, boxes, + points, bitmap blits) into the pixel store, boxes by a fill rule that is a + parameter (`FillRule`). Replaying the PROM recording to its end draws the + graphics menu; `kona hash` shows the store's hash. +- The X server's graphics context, position, clip list and scissor are decoded and held by the KONA back end (`kona status` shows what was understood). X boxes, rectangles, spans and points (and the data tokens that feed them) draw under that state into the pixel store, clipped to the screen, the clip list and the scissor, and masked by the stipple the X server stores and loads. Replaying the IRIX recording over its exactly framed window (about 20,700 records of graphics traffic) draws the greeter's start, 154 fills; `kona hash` pins it. +- The KONA back end loads the textport's colour map and composes the frame the PROM's console draws (the default descriptor: the front colour word as an index into that map); `kona shot ` writes it. Replaying the PROM recording gives a frame with no pixel differing from the recorded capture. Other colour-index and direct-colour windows are composed from the display tables (see below); overlays are not. +- The KONA back end runs a display thread (`kona-display`) at 60 Hz: it completes waiting retrace requests at frame boundaries (the retrace tag changes then) and raises the video interrupt once a frame, never marking activity for the driver's watchdog. A reset cancels waiting requests; stop joins the thread and quiesce holds it. +- The KONA back end owns a screen (`KonaBackend::display`, a `GfxDisplay`): at each retrace its display thread composes the frame and presents it through the renderer slot, every frame, as VPro's does; a screenshot or frame request is answered by the first frame composed after it. A machine that holds the `Arc` can return it from `get_display`. +- The KONA back end keeps the X server's state (graphics context, position, clip list, scissor, stipple cache) once per rendering context, as the front end names the current one with every job; commands with no context current use a copy of their own, as before. A fresh activation starts a context from power-on, a switch-out keeps its state. `kona status` shows the copy the last job ran under and how many contexts hold any. +- The KONA back end counts the kernel's memory-region commands (place, copy down, swap out and in, with the bytes each swap names) and `kona status` shows them. The front end already answers them and moves the bytes (zeros out, dropped in); no pool lives in the back end yet, so there is nothing for a swap to encode. +- The KONA back end takes the X server's pixel commands: setup, write (rows as a data run or, with the mode word's DMA bit, in the DMA packet that follows), copy, read and finish. Writes and copies draw under the clip list, scissor, stipple, mask and logic operation; reads answer on the return page or, inside a select-feed bracket, write the host buffer at the bracket's finish and post the four-word completion. Refused and counted: the window-copy pair, overlay, popup and display-ID targets, the two-buffer RGB form, and frame-buffer word reads (they need a per-pixel tag). `kona status` shows the counts and the refusals. +- The X server's fills into the display-ID, overlay and popup planes now draw, into planes of their own beside the pixel record; nothing shows the overlays or the popup yet. The two-buffer RGB form and overlay-both stay refused and counted. +- The KONA display composes index and direct-colour windows. It keeps the X colour map, the display-ID table and mode words, the gamma RAM's first page and the cursor's glyph memory and registers; each pixel's display ID (bits 7:2 of the ID plane) says whether its word is an 8-bit or a 12-bit index into the colour map, or direct colour, the map's entries go through the gamma RAM, and a glyph cursor is drawn over the frame. The textport's frame is unchanged, and replaying the IRIX recording now gives a desktop frame with no pixel differing from its capture by more than eight levels. Not composed yet: overlays, the popup, the crosshair cursor, the DAC cursor's channels. +- The KONA back end takes the GL client's state and vertices. Each context keeps its matrix stacks, viewport, scissor, depth range, lights, material and current colour, normal and texture coordinate; a begin and its vertices are transformed and lit on the shared GL core (`src/dev/gl`), clipped to the view volume (near and far geometrically, x and y at a guard band), divided, cut to the window or scissor in window space, quantised to 1/256 pixel and handed over as typed vertices with the vertex bus's completion kinds and four texture words (`s/w, t/w, r/w, q/w`). There is no rasteriser behind it yet, so nothing is drawn; `kona status` counts what was handed over, clipped, culled and not taken (double forms, texture generation, user clip planes, fog, polygon modes, vertex arrays and the IRIS GL compatibility tokens). +- The KONA back end has its own tile scheduler (`back/sched`): a ring of command slots, a queue of slot indices for every screen tile a command touches, a lock-free ready queue and `kona-tile-N` workers that own a tile until its queue is empty, slots retired in order, and a work budget. Nothing draws through it yet; its tests run box fills (xor and and-not, overlapping) tiled and inline and require the same store hash. +- The KONA back end draws the GL client's triangles. Set-up (the half-pixel offset, the 1/8-pixel grid, integer edge functions with a left-and-bottom-out fill rule, `f32` attribute planes) runs on the front-end thread and each triangle becomes a job on the tile scheduler, `[graphics] threads` workers on `[graphics] tile`-pixel tiles (`BackendConfig::with_pool`); the per-pixel module has no `f64`. Colour is Gouraud or flat, depth is tested under the GL functions, glClear clears colour and depth, the scissor and the window the kernel placed bound every draw, and a double-buffered window draws into the buffer its swaps are not showing. A window with no colour-map offset that is not colour index shows as direct colour, the buffer its swaps last named. Points, lines, stencil, blending, masks and textures are not drawn yet. Replaying the GL demo recording draws atlantis's sea and ivview's windmill (with `KONA_SHOT_DIR`, frames are kept). +- The KONA back end draws the X server's background points (one, six or twenty-four to a token) in the context's background colour. They take a waiting data run and the clip, scissor, write mask and logic operation, and not the stipple; that last rule is inferred. +- The KONA back end draws the X server's relative points (one, six or twenty-four to a token). 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, starting again at the origin with the next token. They draw like absolute points, stipple included. The anchor rules are inferred. +- The KONA back end draws the X server's line segments and polylines, absolute and relative, under the context's clip, scissor, stipple, write mask and logic operation. A polyline of n points makes n - 1 segments, arriving as n - 1 words of data preload and a one-word token, and a waiting data run joins it. The drawable's position is added to every point and line vertex, absolute and relative (inferred; the X server's position loads are mostly nonzero). Which pixels the pipe lights is unknown: the model lights both ends, a segment of no length as one pixel and a shared vertex once, a labelled approximation. +- The KONA back end takes the double-precision forms of the GL client's colour, normal, texture-coordinate and vertex tokens, which were counted as not modelled and drew nothing. A double is two words, high word first, narrowed to single precision as the command arrives and run as the float form; the word counts are established for the texture coordinates and inferred for the others. The colour-index double stays unmodelled. +- The KONA back end clips against the GL client's user clip planes: the set-plane token and the enables for planes 0 to 5 were not taken and the query answered zeros. A plane is kept in eye space, the coefficients times the inverse of the model-view matrix at the call (inferred), and cuts lines, points and polygons beside the view volume. A model-view matrix with no inverse leaves the plane as it was; what the pipe does there is unknown. +- The KONA back end generates texture coordinates (glTexGen): object and eye linear, and sphere map for s and t, per coordinate, with the planes and modes kept in the context's state and snapshot and the eye planes held in eye space. Generation runs while texturing is on, and the generated coordinate goes through the texture matrix. The pipe's distance planes and modes, and sphere map on r or q, are counted and change nothing. +- The KONA back end applies fog: the setter's float and integer forms and the enable fill a fog state per context (in the snapshot and answered by the state query), and each fragment's colour is blended with the fog colour by OpenGL's linear, exp or exp2 factor of the eye-space distance, after the texture environment and before the alpha test. The pipe computes the factor from the window depth through a 12-bit table; the model approximates that table by evaluating the function at the fragment's eye distance and rounding to 12 bits, so how close it lands is unmeasured. Colour-index fog, the function-table mode and the fog offset are accepted and not modelled. +- The KONA back end draws polygons as lines or points under the polygon fill mode, which was counted and ignored. A face in the line mode is drawn as lines between the vertices whose edge flag is set, and one in the point mode as points at them, through the paths GL lines and points take: line width and stipple, point size, the face's own colours, and not the polygon stipple. A strip, fan or quad strip draws every edge of each piece. +- The KONA back end loads and draws volume textures, which were counted as not modelled. A volume arrives as one texel draw per slice and is bound by the 3D records; the 3D enable turns on texture coordinates, with generated coordinates running under it as under the 2D enable, and a fragment samples nearest or trilinearly with r choosing the slice. Where the texels sit inside a level's address pair and the single-level form's level of detail are unknown and approximated. +- The KONA back end keeps the detail bind and the filter-coefficient tables per context and combines a magnified base texture with its detail image (add or modulate, by the filter curve) or sharpens it against the next coarser level, which showed only plain linear magnification before. A base bind whose format row differs from the detail bind's, or that has no detail bind, magnifies linearly. The detail's position is the base's texel coordinate times 2^|L| (inferred); the arithmetic's widths and rounding, and the sharpen arithmetic, are unknown and approximated. A minified fragment is unchanged, and a plain 2D draw runs the loop it ran before. +- The KONA texture memory is addressed the way the pipe addresses it: a level at address a of a bank is the words from 16 a of that bank's plane, so a level reloaded at the same place overwrites it, one a later load overlaps is retired, and nothing is allocated. Before, the texture arena only grew, filled at the product's size and refused every later load and bind (a TyrQuake replay hit it). A load is now refused only when its address and extent reach past the product's bank, or when its texels would take texture memory past the product's pool. Planes are sparse 64 KiB pages made on first write, so a texel at the top of a bank costs a page, and the pages held across all planes are capped at the product's texture memory. Each context has planes of its own, given back when its slot is activated afresh; the owner's block offset is not modelled, so contexts neither contend for memory nor alias, so a texture loaded through one member of a share group (contexts the kernel lets see the same physical block) is not visible to a bind from another member. Snapshots keep each plane's pages (a snapshot with the older texture layout is refused), and `kona status` reports texture memory held, its peak and the ceiling. +- The KONA back end loads textures sent in the packed pixel types (3-3-2, 4-4-4-4, 5-5-5-1, 8-8-8-8 and 10-10-10-2). It refused them as an unknown type, so the level never loaded and every bind of it was refused: a replay of an Onyx2 Quake session lost 478 binds to 4-4-4-4 textures. The bit order and scaling are OpenGL's (inferred). +- The KONA back end smooths points, lines and polygons, whose enables were counted and ignored: with one on, the primitive is weighed by the share of each pixel it covers, and that share scales the fragment's alpha after fog and before the alpha test. A line is its rectangle, a point the share of an 8 x 8 grid inside its disc, and a polygon of up to eight edges after clipping its clipped outline. In a multisampled window lines and points still smooth and polygons multisample. The coverage rule is unknown, so these are the model's own approximations, kept in `back/raster/smooth.rs`. +- The KONA board was reworked to a fixed shape: one concrete back end (no + `Backend` trait or null back end), one `Mutex` over the stream state and + the back end, processor accesses that only queue or `try_lock`, one fake + machine for the tests, named host tokens and register fields in `defs.rs`, and + a tile scheduler with a preallocated queue per tile and a draw-state table. + GL triangles reach set-up as plain window-relative vertices under a draw state; + nothing clips in window space, so a large triangle keeps its colour slopes + where it crosses the screen, and the ivview windmill's tower, which drew 54 + red pixels, now draws about 10,600 to 13,200 (the capture shows about 11,700). + Every store access drains the tiles by going through `raster::surface`. +- A recorded command stream (the `KONAREC` file QEMU's KONA model writes) can be + replayed into the front end: stores and loads in order, the board's DMA reads + served from the recording and its writes compared. With no back end the replay + is exact only up to the first data run one must size; past it the stream + misframes. With the drawing back end behind it (`replay_into`) the IRIX + stream frames whole and every byte the board writes to guest memory matches + the recording. Set `KONA_RECORDINGS` to the + directory of recordings (and `KONA_SHOT_DIR` to keep the frame the desktop test composes) and run `cargo test --lib kona::back::replay -- + --nocapture`; without it the tests skip. + +- The KONA back end keeps the display generator's eight video channels: the + formats, input rectangles, placement, cursor priority, gamma map and enables the + X server sets, and answers its video Gets (board and channel configuration, + resize, blanking, enables, Vampire) on the return page. When the firmware is + released it runs the stored video combination, or the built-in one when the + stored image is not whole. The state is in the snapshot. Blanking is kept and + logged and `kona status` lists the blanked channels, but nothing is drawn + differently; a managed size other than the configured one is ignored, and + Vampire and X-window binding are not modelled. The video command table now gives + the data-word count of all 93 requests, and the monitor command has no reply. + +- The KONA texture read-back (token 0x052) returns a level's raw 16-bit words + when it is read as luminance in unsigned shorts under the one-component storage + code, as the library does before it reloads and rebinds a level, and a bind + samples whatever words a level holds under its own format. A read bigger than + the model builds is refused and counted before anything is allocated, including + one made large by the pack skips, and a bracketed read-back is delivered to the + range its start token describes. A level updated at its edge is no longer taken + for a small level's copy, and a snapshot keeps the detail images. +- The KONA display shows black while channel 0 is blanked, by its own blanking or by a temporary blank of all channels. The pixel store is untouched. The choice is made once per frame. +- The KONA display draws channel 0's DAC cursor over the composed frame, past the gamma RAM, in X11 mode when the control register enables it. The DAC block's index and data writes are followed for the control value and the position, and the state is saved in the snapshot. The XGA mode, the interlace bit's effect and the load for channel 0xde are not drawn. +- The KONA display narrows the gamma RAM's 12-bit output to the DAC's 8 bits by dropping the low four, where it rounded: the colour map's output chips drive the video-output chips from bits 11:4 alone, with nothing rounding or dithering on that link (established there, inferred end to end). Colours that came out one level higher now match. +- The KONA front end counts the kernel's write of 1 to the activity register once per video interrupt, instead of logging an unimplemented-write warning for each (about 85 a second while a guest ran). +- The KONA back end answers every push of the GL attribute stack with a completion word, as the pipe does (inferred), so a client that pushes only the list bit no longer waits on its return page forever (the cycles demo printed "GL:Wait Rdata timed out"). Nothing is read back for the list bit, and the data for the other attribute bits is left empty and counted. A push onto a full 16-entry stack sets the stack-overflow error (inferred) and still completes. + ### Onyx2 - `machine.profile = "onyx2_ip27"` runs the stock IP27 PROM on one or two Hub @@ -226,6 +328,16 @@ hashes are given where a change is easiest to understand by reading the commit. BaseIO card's boot flash. The node PROM loads it at "Loading BASEIO prom"; without it the PROM runs its own reduced copy, which has no graphics console. +- `[graphics] board = "ir2e"` (or `"ir2e:2rm"`) fits an InfiniteReality2E pipe in + the Onyx2 deskside: GE16-4 and RM9 board records, board set 4, 64 MB of + texture per raster manager. The pipe's product table also holds the + InfiniteReality3 (board set 5, 256 MB of texture per raster manager, which + the texture store sizes itself for); no machine fits it, so it has no board + name. +- `[graphics] board = "ir2"` (or `"ir2:2rm"`) fits the InfiniteReality2 pipe + in the Onyx2's graphics slot. Its interrupt packets and DMA go to the Hub at + the widget the guest programs into it, its window is the machine's display, + and `[graphics] flash` keeps its flash EEPROM in a file. ### Origin 2000 diff --git a/FEATURES.md b/FEATURES.md index ab61a8c09..f52466d40 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -244,11 +244,12 @@ config only when one of these keys is set. See the | Key | Default | Values and behavior | |---|---|---| -| `graphics.board` | `newport`; `solidimpact` on the Octane | newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12, none. Extreme is Indigo2 only. The Octane takes only `solidimpact` (SI), `highimpact` (SI with texture option), `maximpact` (MXI), `v6`, `v8`, `v10`, `v12` (the VPro boards, also `vpro:v12` and so on; need an IRIX install with the VPro driver, IRIX 6.5.10 or later and PROM 4.0, or 4.5 for the V10 and V12, which shipped only in the Octane2 and so give the machine the Octane2's Crossbow 1.4 and Bridge revision) or `none` (no board: the machine runs on its serial console, with no window). The Fuel takes `v12` or `v10` in its graphics slot, or `none`, its default. The Tezro takes `v12` at widget 12, or `none`, its default. | +| `graphics.board` | `newport`; `solidimpact` on the Octane | newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12, ir2, ir2:2rm, ir2e, ir2e:2rm, none. `ir2` and `ir2:2rm` are the Onyx2 deskside's InfiniteReality2 pipe with one or two raster managers; `ir2e` and `ir2e:2rm` are the InfiniteReality2E (GE16-4, RM9, 64 MB texture per raster manager), which also fits the deskside. The Onyx2 takes only the first four or `none`; the default is `none`. Extreme is Indigo2 only. The Octane takes only `solidimpact` (SI), `highimpact` (SI with texture option), `maximpact` (MXI), `v6`, `v8`, `v10`, `v12` (the VPro boards, also `vpro:v12` and so on; need an IRIX install with the VPro driver, IRIX 6.5.10 or later and PROM 4.0, or 4.5 for the V10 and V12, which shipped only in the Octane2 and so give the machine the Octane2's Crossbow 1.4 and Bridge revision) or `none` (no board: the machine runs on its serial console, with no window). The Fuel takes `v12` or `v10` in its graphics slot, or `none`, its default. The Tezro takes `v12` at widget 12, or `none`, its default. | +| `graphics.flash` | unset | InfiniteReality2: the file that keeps the pipe's 512 KiB flash EEPROM between runs; created when absent. Unset keeps it in memory. | | `graphics.heads` | `1` | 1 or 2; dual-head requires Newport. | | `graphics.resolution` | `guest` | guest, 1024x768, 1280x960, 1280x1024; presets require Newport. | -| `graphics.threads` | `2` | Worker threads the board's fragment pipeline draws on, at least 1. VPro: the screen tiles' owners, besides its front-end and raster threads; more help on a host with cores to spare. | -| `graphics.tile` | `32` | VPro: the fragment pipeline's screen tile size in pixels, a power of two from 8 to 256. Each tile's work runs in order on one worker at a time; with many threads, 64 can be faster. | +| `graphics.threads` | `2` | Worker threads the board's fragment pipeline draws on, at least 1. VPro and InfiniteReality: the screen tiles' owners, besides its front-end and raster threads; more help on a host with cores to spare. | +| `graphics.tile` | `32` | VPro and InfiniteReality: the fragment pipeline's screen tile size in pixels, a power of two from 8 to 256. Each tile's work runs in order on one worker at a time; with many threads, 64 can be faster. | | `graphics.raster` | `cpu` | VPro and IMPACT: where the raster unit runs. `cpu` draws on the threads above (IMPACT: its raster engine threads); `wgpu` draws on the host GPU through wgpu compute (needs a build with the `wgpu` feature; work in progress, see `clanker/plans/wgpu-raster.md`). An IMPACT board then has one raster engine, whatever `IRIS_MGRAS_ENGINES` says. | | `graphics.gpu_backend` | `auto` | The wgpu backend for `raster = "wgpu"`: auto, vulkan, metal, dx12 or gl. | diff --git a/HACKING.md b/HACKING.md index cea249f7f..9d898d8c7 100644 --- a/HACKING.md +++ b/HACKING.md @@ -164,7 +164,12 @@ pub trait Resettable { fn power_on(&self); } `register_commands`/`execute_command` give a device its own debug commands; [debug.md](debug.md) documents them. - **Saveable** is snapshot state. A device that implements it gets a - save → load → save round-trip test. + save → load → save round-trip test. State too big for a TOML table goes to the + chunk store instead: the machine calls the device's own `save_chunks` and + `load_chunks` (as for the framebuffers), keeps the hash lists in `ChunksManifest` + (`kona_chunks` for the KONA pipe's pixel store and cursor memory) and lists them in + `referenced_hashes`, so `gc` keeps them. A new field in `ChunksManifest` changes + `chunks.bin`, so it bumps `SCHEMA_VERSION`. - **Resettable** is power-on state. Devices connect to each other through device-specific `connect()` / @@ -323,6 +328,7 @@ Every device that has real work to do runs on its own thread: | `bcm5701-rx`, `bcm5701-net` | Tezro BCM5701 receive pump and network backend | | `ioc3-eth-rx`, `ioc3-eth-net` | IOC3 ethernet receive pump (every machine with an IOC3); the network backend | | `odyssey-work`, `odyssey-raster`, `odyssey-pool-N`, `odyssey-display` | VPro: the front end decodes the queued command-FIFO stores (the processor's stores only queue); the raster unit sets each draw up and walks its primitives into 2x2 quads; a pool of workers (`[graphics] threads`, default 2) walks, shades and stores filled triangles and stores every other fragment, one screen tile at a time; and the display composes each whole frame from board memory (64-bit words, as main memory is: a field is a bit range of its word, read by shift and mask with no byte order, `vram.rs`) as the scan-out registers describe it, counts the retrace and presents the window at 60 Hz. No board-wide lock: the front end owns its state, an access that needs the drawing is answered busy until it is done, the display FIFO (PB&J's register file) has its own lock and never waits for the drawing (`display_fifo.rs`), and the display reads board memory while it is drawn, as the board's own scan-out does; nobody tracks which rows changed. The raster thread sets primitives up and writes each as one command in a ring of 64-byte slots (`pipe/ring.rs`): a triangle's record and bounding box, a quad it shaded itself (lines, points), or a clear's rectangle; the slot's index goes on the queue of every screen tile (`[graphics] tile`, default 32 pixels) it touches. A worker takes a ready tile and owns it until its queue is empty, walking each triangle within the tile and shading and storing quad by quad, so every pixel sees its fragments in command order with no other ordering, and the depth pre-test reads depth its own tile stored. A slot is retired once all its tiles have run it. The raster thread drains the ring before any other work on board memory (a texture upload waits for the shader alone). A completion (a fence's token, a query's words) does not drain: it rides the ring and runs once the ring retires past the quads queued before it. A quad's four pixels are lanes: the walk's depth, the texture unit's positions and the colour weights run on the host's SIMD unit (`fearless_simd`, `pipe/lanes.rs`, the level detected at start-up), bit for bit the scalar arithmetic. When its queue runs dry the raster thread does not drain: it keeps one count of the raster unit's pending jobs for the quads still in flight, and a front-end wait or a refused processor read asks it to store them. `IRIS_ODSY_SHADER=inline` makes the raster thread shade every quad itself, and `IRIS_ODSY_BACKEND=inline` stores on the raster thread with no pool. The texture registers a draw reads live in an array of draw state slots (`pipe/slot.rs`): the front end writes the current slot, a draw latches it and holds it until its job is given back, and a write while a queued draw holds the current slot first copies it to a free one, so the stages read their draw's registers with no lock | +| `kona-hip`, `kona-tile-N`, `kona-display` | InfiniteReality (KONA): the processor's stores to the pipe only queue in a ring (`src/dev/kona/mod.rs`); `kona-hip` takes them in order under the pipe lock, frames the commands and runs the back end inline, so the front end's state needs no lock of its own. A draw is set up on `kona-hip` and its job goes on the index queue of every screen tile it touches (`[graphics] tile`); `kona-tile-N` workers (`[graphics] threads`; none means `kona-hip` runs the tiles itself) each take a ready tile and own it until its queue is empty, so every pixel sees its work in command order. `kona-hip` drains the tiles before any other use of the pixel store. `kona-display` composes the whole frame from the store and the display tables at 60 Hz and, at each frame boundary, completes the retrace requests made since the last one (through the display handle, not the pipe lock) | Threads that have nothing to do park instead of spinning: the graphics consumers when their FIFO is empty, and the CPU thread in `idle-pause` builds diff --git a/clanker/INDEX.md b/clanker/INDEX.md index e218b3f4b..b64856b60 100644 --- a/clanker/INDEX.md +++ b/clanker/INDEX.md @@ -298,6 +298,16 @@ Odyssey (VPro V6/V8/V10/V12): the Octane2/Fuel/Tezro graphics board. Modelled on | [a-refused-command-store-answers-busy-never-spins.md](odyssey/a-refused-command-store-answers-busy-never-spins.md) | rule | RAD "thread held off" under Quake: the room check counted retired stores while the GFifo head is published lazily, so `write_command` spun on the host with no interrupts; check the ring's own view and answer busy | | [sharegroup-warning-at-gl-exit-is-normal.md](odyssey/sharegroup-warning-at-gl-exit-is-normal.md) | rule | `odsy: attempted sharegroup operation from unbound gfxp` at GL program exit is normal: a real V10 logs it 57 times when tyrquake quits; only a sign of trouble mid-run or after a teardown | + +## kona/ + +The InfiniteReality (KONA) graphics pipe's host interface. + +| Note | Kind | Title | +|---|---|---| +| [front-end.md](kona/front-end.md) | design | The Host Interface Processor: attach interface, the command queue on `GFifo`, which accesses wait, the job seam to a back end, approximations | +| [token-census.md](kona/token-census.md) | measurement | The token families two recorded streams (the PROM to its graphics menu, IRIX to the 4Dwm desktop) send, how far a replay into `NullBackend` stays exact, and the selectors QEMU's decoder leaves unresolved | + ## hostgl/ Host GL pass-through for guest GL programs (`iris-hostgl`). @@ -491,3 +501,5 @@ Plans, task briefs and reviews written for agents. Each states its own status; m | [smp-octane.md](plans/smp-octane.md) | plan | Two CPUs on the Octane: HEART MP registers and PROM/kernel slave flow, CPU core → N instances (park-request bit, CpuShared, debugger extraction), jitv2 shared pool audit and fixes, milestones and foreseen problems | | [vpro-lines-on-the-triangle-arithmetic.md](plans/vpro-lines-on-the-triangle-arithmetic.md) | plan | VPro lines: integer pixel walk kept, attributes on the triangle's f32/0.15/12.12 arithmetic instead of f64, major-axis t; then a ring primitive for the threaded CPU unit and a REC_LINE GPU kernel; validate on octopus | | [vnc-server-plan.md](plans/vnc-server-plan.md) | plan | Built-in VNC server: board-agnostic frame tap, damage from frame diffs, pointer steered by the HW cursor, monitor/MCP screenshot + click for agents | +| [onyx2-infinitereality.md](plans/onyx2-infinitereality.md) | plan | Plan: the Onyx2, Onyx 3000 and InfiniteReality in Iris: order of work, decisions, finish line and per-PR rules +| [kona-back-end.md](plans/kona-back-end.md) | design | Design: the InfiniteReality (KONA) board: concrete back end, pipe lock, draw-state table on a tile scheduler, geometry hand-off, ownership, steps and approximations diff --git a/clanker/kona/front-end.md b/clanker/kona/front-end.md new file mode 100644 index 000000000..98f0bd599 --- /dev/null +++ b/clanker/kona/front-end.md @@ -0,0 +1,157 @@ +# KONA front end: the Host Interface Processor and its queue + +Status: reference (iris `src/dev/kona/`, 2026-11-08). The InfiniteReality +pipe's host interface and the back end it drives. The Onyx2 machine fits it +with `[graphics] board`. + +**What it is.** The processor stores commands to a command page in the pipe's +widget aperture. A Host Interface Processor on the board takes the words, +answers the commands that are about the board itself, and hands every command +that draws or changes display state to the Geometry Distributor and the engines +behind it. This module is the Host Interface Processor and the widget around it. +The engines are `KonaBackend` (`back/`), which keeps the pipe's drawing state, +the tile scheduler, the pixel store and the display. + +## Attaching it + +A machine builds `Kona::new(cfg, irq, mem, status, flash)` and calls the +`BusDevice` methods with offsets inside the 16 MiB aperture. The board names no +Hub, HEART or Bedrock type; it uses Iris's own traits: + +- `irq: Arc`: a crosstalk interrupt packet is one + `set_line(vector, true)` that sets a pending bit; the board never clears one. + The packet's destination is decoded when the driver programs the destination + registers; a packet for another widget than the host's is counted and + logged once, not delivered. Called with no board lock held. +- `mem: Arc`: host physical addresses, as the driver's packets + and pages name them. The packets name no destination widget; the machine's + handle chooses one. +- `status`: the machine's status-bar inputs, passed to the board's display. +- `flash`: an optional file the EEPROM is held in. + +The graphics credits themselves (the Hub's per-store credit counter) are not +modelled; the qtest cases for them are not ported. + +## The thread and the queue + +One thread, `kona-hip`, stands for the Host Interface Processor. A store to the +command page, the DMA push port, the bracket register, the firmware load port +or the map RAM goes into a `GFifo` and the thread runs it in order. A full ring +answers the bus `BUS_BUSY`; the CPU thread never spins (see +`a-refused-command-store-answers-busy-never-spins.md` in `odyssey/` for why). + +`regs::classify` says how an access meets the stream: + +| Class | Registers | Rule | +|---|---|---| +| Queues (stores) | command page, DMA push, bracket, load port, map RAM | joins the ring | +| Now | interrupt cause, enable, mailbox, credit (read), retrace tag, widget id, low-window word, activity latch | served from the register file at once, whatever the thread does | +| Waits | everything else | busy until the ring is empty and the thread idle | + +The register at +0x800208 is the graphics FIFO's fill level: the words framers +hold plus the ring's backlog, rounded up to two (bit 0 never sets), and full +(0x7fe) while a display list runs. Users wait for it to fall; the graphics +install tool reads it before every command word and goes on below 0x5dd. + +Without the thread running (before `start`) a stream store waits in the ring. +`stop` joins the thread and runs what it had not taken; call it with the +processors stopped. + +Locks: `Host::front` (register file, a few instructions, never across a call +out). The Host Interface Processor's state and the back end sit in one +`Mutex` (`Shared::pipe`): the thread holds it for a batch of stores, and +a processor's access that needs the stream only `try_lock`s it, answering busy +if the thread holds it. So the machine's DMA, interrupt and FIFO handles are +never called with a processor holding a board lock. The activity latch, +held-word count, "reasons wait" and "list runs" flags are atomics because the +thread touches them per word. Stores that wait for the stream (a widget +register, the credit or the tag) ride the ring and the thread applies them in +stream order, so a processor thread never runs the back end. + +A word that opens no command (no table has it) is not silently lost: it +reaches the back end as `Job::Dropped`, so a token whose length the tables lack +is visible as its word and the words behind it. + +Re-entry and failure limits: + +- A machine's DMA or interrupt handle is called from the kona thread with the + pipe held. It must not call back into the board's monitor accessors + (`failure`, `flash_image`, `load_record`, `with_backend`) or reset: they take + the pipe and would wait on the caller's own lock. +- A back-end job that never returns keeps the pipe, so it blocks those + monitors and reset (they sleep, they do not spin) and every waiting register. +- A panic on the kona thread, or in a tile worker, marks the board dead: the + bus then fails visibly (`failure()` says why). + +The published held-word count (and so the credit register) is updated at the +end of each run of up to 32 stores, so during a long run it lags the framers. +An 8-byte access is all or nothing: a store refused busy has done none of its +words (reads have no side effects), so the processor's retry repeats nothing. + +## The command stream + +`pipe.rs` ports the textport/kernel/GL framers: a word is an argument of the +open command, else it opens one. The grammars share the page; how the pipe tells +them apart is unknown, and `Core::textport_push` documents the rule the model +infers. Variable runs the front end can size (colour map, gamma, cursor, video, +EEPROM, list-call pairs, texture table) it takes itself; a run the back end must +size is asked for by answering `Reply::Data(n)` to the command. + +## What a back end's threads may do + +`BackPort` works only inside `KonaBackend::job`, on `kona-hip`. The back end's +display thread and tile scheduler get a `DisplayHandle` instead, from +`KonaBackend::attach`, which `Device::start` calls before the thread runs and +again after each stop: + +- `retrace_done(epoch, tag)` and `video_frame(epoch)` set the retrace tag and + the video cause and send the general-vector packet once, with the register + lock dropped, so they cannot deadlock the Host Interface Processor's thread. + Both are for the display thread. `video_frame` is the tagless form. +- `activity()` sets the watchdog latch. The tile scheduler calls it whenever the + producer waits (a full ring, a spent budget, no free draw-state entry, a + drain); the display thread must not, or the watchdog could + never fire. +- `epoch()` is what a ticket is made in. A reset begins a new epoch, and a + call carrying an older one is dropped, so a thread that was mid-call during + a reset cannot raise a cause the reset cleared. + +The handle holds the register file (`Host`, its own `Arc`) weakly: the pipe +slot owns the back end, so a strong reference would be a cycle. With the board +gone every call is a no-op. With no interrupt destination programmed the video +cause still sets, and the missing destination is logged once, not per frame. + +`Device::stop` calls `KonaBackend::stop` after the thread joined and the jobs it +had not taken ran. `Kona::quiesce` lets the ring run out and calls +`KonaBackend::quiesce`; `Kona::resume` undoes it. + +## Approximations and open questions + +Grades: established (a published manual or measurement), inferred (from +software's behaviour or a public driver), approximated (a model choice), unknown. +Statements about what host software does are software expectations taken from +QEMU's model (jamesbraid/qemu-sgi-origami), not hardware facts. + +| Item | Grade | +|---|---| +| A DMA packet's high word with bit 28 clear is `io >> 12` of a device address the map RAM translates (entry `word >> 2`); with bit 28 set it is an absolute host frame | inferred; the entry decode is unknown | +| A mapped packet that hits an invalid entry is refused and completes nothing | unknown (the error register's bit 13 is the likely report) | +| The widget control register resets to the host widget (`KonaConfig::with_host_widget`) in bits 25:22, so DMA works before software programs it | approximated: the model's choice; the real power-on value is unknown (QEMU does the same with its hub widget) | +| The widget DMA goes to is bits 25:22 of the widget control register (`Kona::dma_widget`); the machine's handle may use it | inferred | +| GL texture table framed as a slot word and 33 words | inferred | +| Video commands of unknown length framed by searching for a checksum equal to the command plus its data words | inferred | +| Textport, kernel and GL grammars share the command page; the rule that tells them apart (`Core::textport_push`) | inferred; the pipe's own rule is unknown | +| EEPROM identify answers 0x1fc40000 every time (codes from a published example) | software assumes it is stable (established); a part in ID mode returns fixed bytes (inferred); the codes are documentation, not a measurement; bytes 2 and 3 unknown | +| EEPROM check always answers 1 | approximated | +| Flash is 512 KiB with the layout the install tool writes; a new flash is zero with header state 2 | inferred; factory contents unknown | +| Fill-level register: a level in 32-bit words at two-word resolution | inferred; the FIFO's depth is unknown (at least 0x70c); the graphics flow-control credits are a different counter, doublewords, not modelled; "full while a list runs" is approximated | +| Interrupt-enable register: stores its twelve bits, reads them back, reset 0 | software never reads it (established); reset value unknown | +| Error register (+0x6c) reads 0 and ignores writes; no widget errors are raised | the register is write-one-to-clear and a healthy pipe reads 0 (inferred); power-on value unknown | +| A command-page store at +0xc is a plain word, like offset 0 | unknown; software only uses it when its CPU inventory is unrecognised, which an Onyx2 guest's is not (established) | +| The Onyx2 pipe's name is KONAL; the kernel supplies it, the board supplies nothing | established | +| Reasons post one at a time; a waiting one is looked for when the cause register is accessed and at the end of each run | approximated (QEMU polls on a timer) | +| Replies post at once instead of after queued work | approximated | +| A forced bracket with no context current suspends a user command with no owner to resume it | approximated (as QEMU) | +| The indirect data port does not advance its address on read | unknown | +| With no display thread running, a retrace request completes at once | approximated | +| Identify and config part numbers, names, revisions and chip words | inferred from QEMU's model of a deskside pipe | diff --git a/clanker/kona/token-census.md b/clanker/kona/token-census.md new file mode 100644 index 000000000..469dcb403 --- /dev/null +++ b/clanker/kona/token-census.md @@ -0,0 +1,311 @@ +# KONA token census of the two recorded streams + +Status: measurement (2026-11-07). What the front end saw when QEMU's recordings +of an Onyx2 deskside's InfiniteReality2 (two raster managers) were replayed +into it with `NullBackend` behind a counting tap +(`src/dev/kona/back/census.rs`, `replay.rs`). The recorder is another model, +not a pipe, so the streams are what that model's guest sent. + +Reproduce: `KONA_RECORDINGS= cargo test --lib +kona::back::replay -- --nocapture --test-threads=1`. Without the variable the +tests skip. + +## How far the counts can be trusted + +The front end frames only the commands in its selector tables, and answers +`Reply::Data(n)` only when a back end asks. Data a command carries that the +tables cannot size reaches the back end as `Job::Dropped` words. The record +where the 64th dropped word in a row came marks where a replay into +`NullBackend` stops being a count of the stream (the data run itself began a +little before): a data word that happens to equal a selector can open a +command that was never sent, and the register answers drift (a bogus +`context_deactivate` posts a completion reason the recording lacks). + +- PROM recording: no such run. The whole stream is exact. +- IRIX recording: the graphics command stream starts at record 1,394,816 of + 1,727,527 (the earlier records are boot) and the 64th dropped word in a row + came at record 1,415,538, about 20,700 records in. The data run it belongs to + began a little before; the pixel write that carries the misframed word + below is at 1,417,227, after it. Deterministic register answers and every + byte the front end wrote to guest memory match the recording up to there. + The first load that differs is at 1,419,153: the geometry-engine cause bit + is set where the recording's is not, from a misframed word (record 1,417,231, + the word 0x1fc inside the data of the pixel write that began at 1,417,227). + Counts for the whole run are therefore upper-level estimates; the counts + "up to the long run" are exact but cover little (1,642 jobs of 75,126). + +## PROM recording (63,750 records) + +Stores 52,362, loads 11,381, resets 6. Textport grammar only, no kernel or GL +command, no DMA, no interrupt, no dropped word, 9,499 jobs: + +| Textport command | Count | +|---|---| +| 0x2 | 3,232 | +| 0x5 | 4,766 | +| 0x4 | 534 | +| 0x3 | 201 | +| 0x1 | 2 | +| 0x7, 0x8 | 1 each | +| bitmap blits | 762 (2,732 payload words) | + +The replay is exact: no load differs, nothing dropped, the board does not fail. + +## IRIX recording to the 4Dwm desktop (1,727,528 records) + +Stores 1,103,572, loads 591,438, resets 2, two geometry-engine interrupts +(vector 12 both times, matched), 31,860 general interrupts (not reproduced: +the recorded model raises them, and no retrace request arrives in this stream). 75,126 jobs over the whole run. + +Kernel and X commands, 44 selectors, 9,206 commands (whole run; an estimate +past the long run). The X path dominates: x_position 1,485, x_gc 1,195, x_clip +1,126, x_box 730, x_pixel_write 692, x_bdata4 514, x_pixel_setup 448, +x_point 411, x_rect 319, x_pixel_finish 267, x_spans6 248, x_load_stipple 230, +x_point6 176, x_data1 115, x_pixel_copy 113, x_store_stipple 113, x_data2 96, +x_spans2 84, x_rotate_stipple 84, x_data3 82, x_pixel_read 63, x_data4 58, +x_data6 50, x_spans 40, x_point24 23, x_box6 7, x_rect6 4. Also video 66, +display_mode 96, did_map 96, color_map 73, gamma 3, cursor 6, x_scissor 74, +six DMA packets (454,800 bytes of image data, all addressed through the map +RAM), select_feed start and finish 2 each, and one each of x_init, get_config, +reason_page, context_map and context_records, return_page 2 and +context_deactivate 2 (one real; the other is a misframe inside pixel data). +One context activation. + +GL commands, 10 selectors, 210 commands (whole run): light parameter (0x7) 3, +light model (0xa) 36, texture env (0x12) 18, clear colour (0x1d) 36, clear +depth (0x1f) 3, pixel store (0x33) 2, unsigned pixel map header (0x35) 54, +colour4s (0x6d) 36, end (0x71) 21, texture coordinate1f (0x7c) 1. The tokens are lighting, texture +environment, pixel maps and clears, and 21 `end` with no `begin` (the front end +has no table entry for the begin forms). + +Up to the long run (exact): 24 kernel selectors, 822 commands, one GL command +(light parameter), video 66, display_mode 96, did_map 96, color_map 34, gamma 3, +cursor 3, pixel setup 23, pixel write 23, pixel copy 17. + +Data runs the front end sized itself: color map 73 runs (331 words), gamma 3 +(12,288), cursor 6 (1,560). + +## Words the front end could not size + +65,085 dropped words in the whole run, by the command before them: + +| After | Words | +|---|---| +| x_pixel_write (0x139) | 58,442 | +| gl_pixel_store (0x33) | 5,355 | +| gl_pixel_map_ui (0x35) | 486 | +| x_store_stipple (0x13d) | 454 | +| gl_tex_envi (0x12) | 256 | +| context_deactivate (0x1fc) | 68 (a misframe inside pixel data) | +| gl_clear_depth (0x1f) | 13 | +| gl_lightf (0x7) | 8 | + +These are the pixel, stipple, pixel-map and texture-table data a back end must +size by answering `Reply::Data(n)`, from the token layouts of the setup and +header commands before them. The rule that sizes each is the command-grammar +dependency for the X pixel path (pixel setup and write), stipples, GL pixel +store and pixel maps, and the texture environment and table. + +## Against the layouts QEMU's decoder marks unresolved + +The decoder's table (33 selectors) marks five entries unresolved: + +| Selector | Call | In the IRIX stream | +|---|---|---| +| 0x1f | clear depth | 3 commands framed with two words, as the layout below has it: the argument is a double, high word first (established); QEMU's table leaves it unresolved | +| 0x0 | begin | not framed: the front end has no entry, the zero is a dropped word (6,735 dropped zeros, which also cover data and arguments) | +| 0x500 | begin | not framed; 0x500 is also the managed width in the position and clip commands, so the 4 dropped 0x500 words say nothing | +| 0x119 | context resolve | absent | +| 0x1d6 | context resolve | absent | + +So the intersection with QEMU's table is two entries: clear depth (0x1f), whose +layout that is established, and begin (0x0 or 0x500; 21 `end` tokens imply +about 21 begins). The two context-resolve layouts +are not used by either stream. + +Six of the ten GL selectors seen have no entry in QEMU's decoder table: light +parameter 0x7, light model 0xa, texture env 0x12, pixel store 0x33, colour4s +0x6d, texture coordinate1f 0x7c. Their meaning is not in the sources the +design names; the front end's table names are labels only. + +Not in either stream: textport commands 0x6 and 0x9..0xc, GL vertex, array, +matrix, texture bind and query tokens, retrace requests, swap markers, +overlay buffer configuration, window loss. A GL demo recording is needed to +census those. + +## GL demo recording (atlantis, ivview, tex_cube) + +One recording, 71,292,888 records, of the same deskside booting to the 4Dwm +desktop and running three GL clients, with user marks around each. Counted +over the marked spans, replayed into `NullBackend` behind the tap (loads not +compared): `KONA_GL_RECORDING= cargo test --lib +kona::back::replay::tests::the_gl -- --nocapture` (about 100 s in a debug +build). A span starts at its mark and runs to the next mark. + +| Span | Records | Swap markers | Retrace markers | Retrace requests framed | +|---|---|---|---|---| +| atlantis (mark 210) | 31,264,285 | 956 | 956 | 957 | +| ivview, windmill with textures and display lists (220) | 5,623,179 | 1,377 | 1,377 | 1,377 | +| tex_cube (230) | 20,241 | 2 | 2 | 2 | + +The retrace requests match the recorded retrace completions, so the retrace +and swap path now has a stream (swap_pair 967, 1,383 and 5 framed). + +### How far each span can be trusted + +- **atlantis:** clean. Only 136 words were dropped in 31 million records (all + after x_pixel_write), no run of 64 in a row; the 11.08 million GL commands + are as sent. +- **ivview:** mostly clean, with two large dropped runs that are padding, not + data. 1,526,000 dropped zeros follow gl_pop_matrix (986,532) and + gl_translatef (539,000), one distinct value each: the zero that pads a + command out to a doubleword store (a back end ignores it). 176,320 words + follow gl_polygon_stipple, exactly 32 words for each of its 5,510 commands: + the stipple pattern. The first run of 64 dropped words in a row came at record + 65,523,929, inside the 1.5 million zeros. +- **tex_cube:** not reliable. 7,240 dropped words in 7,671 jobs, most behind + gl_buffer_config (3,605 words, 3,574 distinct values) and gl_tex_load (343): + texture and buffer data the tables cannot size, and bogus commands opened by + data words equal to selectors (the gl_histogram family, 4 + 2 + 2 + 1 + commands, is almost certainly that). Only the large-scale shape is usable. + +### GL token families (commands framed) + +| Family | atlantis | ivview | tex_cube | +|---|---|---|---| +| vertex (begin, end, colour, normal, texcoord, vertex) | 10,548,064 | 2,325,683 | 2 | +| array | 0 | 0 | 0 | +| matrix (mode, load, mult, push, pop, rotate, scale, translate, viewport) | 128,268 | 343,031 | 45 | +| texture (env, gen, bind, load, table, scale and bias, active texture) | 8,726 | 34 | 80 | +| query (get, get current, get error, finish, flush, histogram gets) | 10,538 | 1,381 | 19 | +| state and other | 385,419 | 157,067 | 70 | + +Vertex data is all vertex3f and normal3f (4,152,423 and 3,847,140 in atlantis; +1,421,908 and 462,956 in ivview) between begin and end (1,247,928 pairs in +atlantis). The begin forms are the table's `gl_begin_first + N` and +`gl_begin_last` entries, not the zero and 0x500 forms QEMU's decoder leaves +unresolved: of those, 0 commands were framed. No vertex-array token was sent by +any of the three. Textures: atlantis binds one texture 958 times (bind_id, +bind_2d, bind_linear, post_scale, prefix each 958 times); ivview sends texture +tables and scale and bias only; tex_cube loads (gl_tex_load 46, load_id 7). + +### Against QEMU's decoder + +| | atlantis | ivview | tex_cube | +|---|---|---|---| +| GL commands framed | 11,081,015 | 2,827,196 | 216 | +| of them with no entry in QEMU's decoder table | 5,674,910 | 1,180,740 | 211 | +| of them in its unresolved set | 957 | 0 | 0 | +| selectors used / of them outside the table | 59 / 49 | 39 / 32 | 39 / 36 | + +The unresolved set is hit only by 0x1f (clear depth): 957 commands in atlantis, +one per frame. The other four entries (two begin forms, two context-resolve +forms) are never framed. The dropped zeros with value 0 are padding in ivview +(1,527,006 of 1,706,206 dropped words) and 63 of 136 in atlantis; they are not +evidence of a begin token. + +The selectors outside the decoder table that carry most commands: gl_normal3f +(3.8 million), gl_begin_last (1.2 million), gl_color4f, gl_lightf, gl_light_modeli +and gl_light_modelf, gl_enable and gl_disable, gl_material, the matrix ops and +the texture bind family. The decoder is a subset of the grammar; the hardware +owner is asked for the meaning of the words of each of these. + +### Dropped words by preceding command + +| After | atlantis | ivview | tex_cube | +|---|---|---|---| +| x_pixel_write | 136 | 3,134 | 221 | +| gl_pop_matrix | | 986,532 | | +| gl_translatef | | 539,000 | | +| gl_polygon_stipple | | 176,320 | | +| gl_get | | 1,110 | 769 | +| gl_feedback_type | | 110 | | +| gl_buffer_config | | | 3,605 | +| gl_reset_histogram | | | 1,080 | +| list_call | | | 645 | +| gl_tex_load | | | 343 | +| gl_histogram | | | 281 | +| gl_front_face | | | 124 | +| gl_get_histogram, gl_get_histogram_parameter | | | 62, 61 | +| gl_bitmap_inline | | | 49 | + +### Grammar the back end still needs, from this stream + +1. **Data runs to size** (so the back end can answer `Reply::Data`): the + pixel write payload (x_pixel_write); gl_polygon_stipple (32 words, measured + here, to be confirmed); gl_tex_load and gl_buffer_config data; gl_bitmap_inline; + gl_feedback_type's results. The words behind gl_get and list_call were one + repeated value each (padding). +2. **Meaning of the words** of the commands outside QEMU's decoder table, by + volume: normal3f, begin_last and the begin_first forms, color4f, the light and + light-model setters, enable and disable, material, the matrix ops, the texture + bind family, pixel_store. +3. **Clear depth (0x1f)**: used every frame. Its layout is established (a + double, high word first, converted to a 32-bit fraction and then a floating + depth word); only QEMU's table still marks it unresolved, so no new grammar + is needed beyond what is already established. +4. A GL array stream: no recording has sent one yet. + +## Vertex census through the GL path + +The same GL recording replayed into the drawing back end with the GL path's +census on, which measures every vertex against the window as it is +transformed: `KONA_GL_RECORDING= cargo test --lib +kona::back::replay::tests::the_gl_demo_recording_is_censused_through -- +--nocapture` (about two minutes in a debug build), or `kona gl census` then +`kona gl` on a running board. The window is the managed area, 1280 x 1024 in +window coordinates from 0; the window's own origin and size are not decoded, so +"outside" means outside the screen's rectangle, and the glViewport box is not +used as a clip. A vertex is classed once: behind the eye (w <= 0), outside the +view volume (the guard band is eight clip widths on x and y, so only z can put +a vertex here), outside the window, or inside. + +| | atlantis | ivview | tex_cube | +|---|---|---|---| +| vertices (all begin_last, i.e. polygons, in atlantis) | 4,152,423 | 1,421,908 | 0 | +| inside the window | 2,897,206 (69.8%) | 1,421,908 | | +| outside the window, inside the volume | 1,255,217 (30.2%) | 0 | | +| outside the view volume | 0 | 0 | | +| behind the eye | 0 | 0 | | +| polygons culled (back faces: cull face 0x405, 23,484 times; the front face is left counter-clockwise bar one glFrontFace of each sense in the whole recording) | 627,715 | 0 | | +| polygons cut by the window | 2,757 | 0 | | +| polygons wholly outside the window | 188,490 | 0 | | + +The guard band means nothing in either demo reached the volume clip: every +clip the path made was the window cut or a cull. About 15% of atlantis's +primitives (188,490 of 1,247,928) are wholly off screen, so the window-space +clip is not a corner case for GL demos and has to be there before the raster +stage's integer edge functions. ivview's windmill stays inside the window +throughout. + +Texture-coordinate token forms: none. Neither demo sends a texture-coordinate +command (gl_texcoord1f, 2f, 2s, 3f or 4f): atlantis texturing comes through +texture generation (gl_tex_gen, 4 commands) and ivview through texture tables. +The only texture coordinate in any recording is one gl_texcoord1f in the 4Dwm +stream. The perspective word (a fourth texture word per textured vertex, q/w) has therefore no recorded vertex to be checked against; it is +covered by a unit test only, and texture generation, which the GL path does not +take, is what the next texture step needs. + +Not taken by the path in these spans, counted: glEnable and glDisable of 0xc60 +and 0xc61 (the texture-generation capabilities, 1,914 each, atlantis), 0xb42 +(11,021), 0xb10, 0xb20 and 0xbe2 (ivview, one each), and one gl_mult_matrixd in +tex_cube. The ivview and atlantis zero words after push, pop and translate are +padding, as above. + +### Hand-off measurement + +The token layer's rate through the GL path with the vertices handed over as the +typed struct and read, against the same vertices made into bus words and +unpacked by the consumer (7 words a vertex), 30,000 independent triangles, ship +profile, functions aligned to 64 bytes, four runs, median, in Mvertices/s (a +vertex is a vertex command and its share of the begin, end and colour +commands): nothing read 19.5; struct 18.7, bus words 18.2; textured struct +18.0, textured bus words 17.5; struct with the raster snapshot rebuilt on every +draw (a state command between draws) 16.7. The struct is cheaper by about 3% +(the ranges do not overlap), so it is kept; the margin is small, and the +taking the cheaper form picks it. A snapshot rebuilt every draw +costs 11% over a shared one, with the extra state command included; a shared +snapshot costs a reference count. The first version of the path made 10.6 +Mvertices/s and spent most of it zeroing workspace for the largest polygon a +clip can make; a triangle or quad inside the volume now uses a workspace of +four. diff --git a/clanker/plans/kona-back-end.md b/clanker/plans/kona-back-end.md new file mode 100644 index 000000000..bc9d02a84 --- /dev/null +++ b/clanker/plans/kona-back-end.md @@ -0,0 +1,204 @@ +# Design: the InfiniteReality (KONA) board in Iris + +Status: 2026-11-10. Built: the front end, the PROM console and the 4Dwm +desktop (within one level of their captures), GL triangles, lines and points +on the tile scheduler, texture, GL pixel operations, queries, select and +feedback, multisample, fog, texture generation, volume textures, the DAC +cursor and snapshots, with the GL demo frames pinned by replay. The Onyx2 +deskside fits the board. Not done: stereo, the crosshair cursor, the DAC +cursor on channels past the first, and any check against hardware pixels. The +plan is +[onyx2-infinitereality.md](onyx2-infinitereality.md). + +KONA is built on one branch and lands as one PR. Nothing here adds an +abstraction that isn't named: anything not named is private to its module. +Nothing I hold gives hardware pixels, so the whole model is an approximation +and no test claims pixel accuracy. + +## Shape + +```mermaid +flowchart TB + G[Guest stores] --> F[Command ring on GFifo] + F --> H[kona-hip: front end, then KonaBackend::job] + H -->|typed jobs| S[Tile scheduler and draw-state table] + S --> W[kona-tile-N workers] + W --> P[(Pixel store, one array)] + T[(Texture store, one copy)] --> W + P --> D[kona-display: composed every retrace] + D --> V[GfxDisplay screen] + D -.->|video interrupt| H + S -.->|planned| X[wgpu raster path] + X -.-> P +``` + +The stages, with the board each one stands in for: + +```mermaid +flowchart TB + A[Vertices, state] --> B[Transform and lighting via src/dev/gl
Geometry Engine] + B --> C[Clip: near and far geometrically, guard band in x and y
Geometry Engine] + C --> E[Vertex bus hand-off: typed vertices
Geometry Engine to Raster Manager] + E --> F[Set-up, cut to the viewport and clip rectangle
Raster Manager] + F --> G[Raster: 2x2 quads, plane equations
Raster Manager] + G --> H[Texture fetch and filter
Raster Manager] + H --> I[Fragment operations
Raster Manager] + I --> J[Pixel store
Raster Manager] + J --> K[Scan-out at retrace
Display Generator] +``` + +## Front end and attach + +- **A concrete back end.** `KonaBackend` is a plain struct. The `Backend` trait + and its null implementation go: there is one real implementation, and a trait + needs two. +- **Attach.** `Kona` implements upstream's `BusDevice`, `Device` and + `Resettable`, and takes an `InterruptController` and a `BusDevice` for host + memory, the way VPro does. Each interrupt packet is one `set_line(vector, + true)`; the board never clears, and the controller treats each `true` as + "set pending bit". A test on the machine side holds the controller to that. +- **One lock.** `Mutex` guards the front end's stream state and the back + end together. `kona-hip` takes it per batch of ring entries. CPU threads only + `try_lock` it, for a waiting read, and answer busy if they can't get it or if + the ring isn't empty. It is the one board lock held across memory and + interrupt calls, and it can't deadlock because nothing that takes it is + called back by the machine. +- **`defs`.** Every command selector, register offset, word index, bit field, + GL enumerant and hardware count is a named constant in one `defs` module, + transcribed from the board's published command table. No bare numbers in the + rest of the code. +- **Reuse.** `src/dev/gl/` is used the way VPro uses it: KONA implements + `ClipVertex` for its own vertex type and its own `Geometry`. Nothing in + `src/dev/gl/`, MGRAS or VPro is edited. + +## Back end + +- **Token layer.** Plain data on `kona-hip`, in stream order. A table of + contexts holds each one's X state, GL state and texture state. GL tokens + dispatch through a table indexed by selector. The X integer path and the + textport draw inline after a drain, onto the pixel store. +- **Geometry.** The board's projection puts clip z at (z-w)/2, so near and far + are two plane distances, both clipped geometrically. x and y clip only at a + guard band. Triangles then go to the raster with no window clip: set-up cuts + each one to the viewport and the clip rectangle. +- **One snap.** The token layer truncates to the 1/256 grid once, set-up takes + that to 1/8 exactly, and edges are exact integers with the fill rule decided + by ties, so shared edges are watertight. +- **Typed hand-off.** A vertex carries window position, a depth fraction, + colour, two sets of four texture words (s/w, t/w, r/w, q/w; zero if + untextured) and a line distance. Colour is linear in screen space. +- **Draw-state table.** `DrawState` is a flat `Copy` record with no pointers: + window, clip, viewport, ownership, targets, depth, stencil, alpha, blend, + logic op, masks, stipples, fog and texture. State tokens mark it dirty; the + next draw builds one record and puts it in the scheduler's table, and each + job names it by index. A table entry is reused only after the last job + naming it has retired. +- **Concrete tile scheduler.** Square tiles, one owner each, in KONA's own + module, with a slot ring and per-tile queues. Every job goes to every tile its + rectangle touches; workers keep the screen's 2x2 quad grid, so a tiled run is + bit-exact with an inline one. +- **Per pixel.** Integer or reproducible `f32` arithmetic, with `f64` only in + set-up, because WGSL has no `f64` and the stage must be portable to a GPU + kernel. `WordArray::store` is the single write point. There is one + per-fragment path that reads the draw state and branches on the enabled + operations. Fast modes with const generics, as `ng1/rex3_generic.rs` does, + wait for a fill-rate benchmark on a recorded stream to justify them. +- **Texture.** The texture arena is a member of the pixel store: one byte arena + of one RM's capacity, zero-allocated and published once, with a bump + allocator; an allocation past the end fails and is counted. Each context + keeps a level map by bank and address, and a bind builds a flat `TexDesc` + with no pointers. 1D and 2D sample; 3D and volume binds are counted, not + drawn. The fourth texture word is q/w, so texture is perspective-correct. + Only triangles sample. +- **Multisample.** 4 or 8 samples, chosen by the buffer layout. A pattern token + picks one of seven patterns per count, each a pair of location words on the + 1/8 grid, and a sample mask indexes the coverage words. The first pattern is + the default. +- **Window-ID ownership.** When the window-ID test is enabled, a fragment whose + ID plane value fails the compare against the draw's reference and mask is + dropped. A group-range ID passes on its group bits only, and a draw with the + all-ones flag writes nothing. This is the rule between overlapping windows. +- **One pixel store.** Plane-major: one `u32` array per word of the pixel + record, allocated whole at draw set-up and published through a fixed + `AtomicPtr`, so workers and the display never check for allocation. Two + 8-bit ID planes sit beside it. Texture is one zero-allocated copy. +- **Display at retrace.** The display thread composes the whole frame every + retrace from the pixel store and the loaded tables, with no change tracking. + A swap takes effect when its command is reached in the stream: it loads the + display's bank table, and the next composed frame shows it. A retrace request + waits for the next frame boundary, tagged with the reset epoch it was made in + so a reset drops it. + +## Threads + +| Thread | Does | +|---|---| +| CPU threads | `BusDevice` accesses: push to the ring, or answer from the register file | +| `kona-hip` | front end, token layer, replies, set-up, display-table loads | +| `kona-tile-N` | shades and stores the quads of the tiles it owns | +| `kona-display` | composes at retrace, latches swaps, drives the video interrupt | + +Anything that must see drawing drains the scheduler first. The display waits for +nothing, and a guest sync fault shows as a flash of wrong pixels, as on the +board. Reset stops `kona-hip`, drains twice and clears the stores. A worker panic +poisons the scheduler and the board reports dead. Worker count and tile size +are the existing `[graphics] threads` and `tile` settings. + +## Replay and goldens + +I replay recordings from my QEMU model: the PROM, the 4Dwm desktop and the GL +demos. The driver performs the recorded stores and loads, feeds the recorded +DMA, and compares replies, interrupts and swap markers in order. The PROM and +4Dwm frames are held to no differing pixels against their captures; the GL demo +frames are pinned hashes. Recordings stay out of git. `KONA_RECORDINGS` and +`KONA_GL_RECORDING` name them, and the tests skip with a printed reason when +they're absent, so upstream CI doesn't run them. The golden scenes follow +VPro's: a small window per scene, planes hashed, bit-exact. Those hashes are +Iris's own, so they catch regressions and don't show correctness. + +Benchmarks run with every function 64-byte aligned in both builds: fill rate per +fast mode against the generic kernel at 1, 2 and 4 workers, and a timed replay. +A CPU-benchmark regression from the board's presence fails the PR. + +## Steps + +All steps are on the one branch, each with its tests. + +1. `defs` from the published command table. +2. Front end, pipe lock and attach through `InterruptController` and `BusDevice`. +3. Concrete back end; the drain rule; the PROM and 4Dwm replays. +4. Tile scheduler and draw-state table; tiled equals inline at 0, 1, 2 and 4 workers. +5. The geometry hand-off: near and far clipping, no window clip; GL goldens. +6. The Onyx2 machine instantiates the board (after the Onyx2 machine PR). +7. ID planes, overlays and cursors. +8. The per-pixel module and fragment operations, with the ownership test. +9. Lines, points and stipple. +10. Texture: arena, loads, binds, sampling. +11. GL pixel operations, queries, select and feedback; multisample. +12. Snapshots through the chunk store: a `Stream` visitor with a writer and a + reader, chunks for the stores, the manifest change, and an atomic load that + parses everything before it changes the live board. +13. IRIX on the Onyx2: 4Dwm and the GL demos without a pipe kill. + +## Approximations + +| Item | Grade | Test | +|---|---|---| +| Native token layer in place of the geometry engines and controller firmware | approximated | replay, then IRIX | +| Near and far clipped geometrically; x and y at a guard band | inferred | golden with a z-straddling triangle | +| Window position truncates toward minus infinity; one snap to 1/256, then 1/8 | unknown | golden, an edge on a grid line | +| Texture word carries q/w; colour linear in screen space | inferred, form unknown | goldens | +| Sample rule and positions; fill rule | approximated | goldens | +| Depth as an integer fraction | unknown | goldens | +| Fragment operation order, blend and dither arithmetic | unknown | goldens | +| Lines, points, stipple, texture filtering, format rounding | unknown | goldens | +| Retrace completes at the next frame boundary; swap applies in stream order | approximated | replay | +| One pixel store, one texture store, ID planes | unknown layout | allocation test | +| No credit count | decided | a large GL stream never trips a pipe kill | +| IR3 renders as IR2; IR4 identity only | approximated | `hinv`, visuals | + +## Open + +The set-up arithmetic can only be settled by measuring hardware. The display-ID +mode byte's fields are unknown, so some overlay and colour-index windows wait. +I don't know how wide a managed area can be past `GfxDisplay`'s stride. diff --git a/clanker/plans/onyx2-infinitereality.md b/clanker/plans/onyx2-infinitereality.md new file mode 100644 index 000000000..8a3a97487 --- /dev/null +++ b/clanker/plans/onyx2-infinitereality.md @@ -0,0 +1,105 @@ +# Plan: the Onyx2, Onyx 3000 and InfiniteReality in Iris + +Status: 2026-11-10. The Origin 200, Origin 2000 and Onyx2 (one and two +nodes) boot IRIX 6.5.30 to a login. The Onyx2 takes the InfiniteReality +pipe with `[graphics] board = "ir2"`, `"ir2:2rm"`, `"ir2e"` or `"ir2e:2rm"`, +and reaches the 4Dwm desktop. The PROM console and the desktop are within one +level of their captures, and the GL demo frames are pinned by replay. The +back end draws triangles, lines and points, textures (including volume +textures and detail), fog, pixel operations, queries, select and feedback, +multisample and the DAC cursor; its limits are in +[kona-back-end.md](kona-back-end.md) and `docs/graphics/kona.md`. The Origin +3200 and Onyx 3000 are not started. Snapshots of the IP27 machines are +refused, since the Hubs and node memory have no saved state. + +## Goal + +I want Iris to run the Origin 200, Origin 2000 and Onyx2 (IP27), and then the +Origin 3200 and Onyx 3000 (IP35), with the InfiniteReality pipe (KONA) on the +Onyx2 and Onyx 3400. The code is written the way current Iris is written, so +it can merge upstream without a restructure, and no CPU benchmark gets slower. + +I port the IP27 platform and KONA's front end from QEMU's `sgi-origami` +branch. The drawing back end is new, built for Iris, because QEMU's is the same +shared-draw-path design that upstream already replaced in VPro. + +## Order + +```mermaid +flowchart TB + A[Origin 200: one Hub, no graphics] --> B[Origin 2000 and Onyx2 desksides] + B --> C[KONA joined on the Onyx2 deskside] + B --> D[Origin 3200 and Onyx 3000 on the Fuel's Bedrock code] + C --> D + K[KONA back end on the test board, against recorded streams] -.-> C +``` + +1. **Origin 200.** One node, one Hub straight to the Bridge, no graphics. + The Hub is ported from `hw/misc/sgi-hub.c`. The Bridge, IOC3 and Crossbow + get small changes first (interrupt and DMA targets, a per-platform CPU + limit), each with the Octane as its consumer, so the Hub PR stays small. +2. **Origin 2000 and Onyx2 desksides.** Two nodes over the Null Router + (007-3453-002 pp. 21-22; Onyx2 007-3454-005 p. 19), four CPUs. The PROM + brings a node's CPUs together by timing polls in retired instructions, so I + reuse the Tezro starvation holds (the `tezro-mp-starvation` branch) at the + Hub's registers. +3. **The InfiniteReality board**, joined to the Onyx2 deskside: PROM and + `hinv`, console, 4Dwm, then the OpenGL demos QEMU runs today. +4. **Origin 3200 and Onyx 3000** on the Fuel's Bedrock code: two C-bricks + (007-4311-002 p. 87), then the N-brick crossbar to a G-brick, then KONA. + The rack and the Onyx 3400's four C-bricks come last. + +Every machine gets a serial login first, on one CPU, from an IRIX disk made +under QEMU, unmodified (a copy-on-write overlay on a copy, never the +original). Graphics never waits for a booting machine: the back end is built +on the test board against streams recorded from QEMU's KONA, and joins a +machine at the Onyx2 join. + +I compare the console and device access logs against QEMU on the same PROM and +disk and fix the first divergence. Unmodelled accesses stay visible through +`devlog`; nothing is answered with a made-up success. + +## Decisions + +- KONA is one implementation branch and lands as one PR, stacked on the SN0 + machine PRs it joins. Iris leads the board. I build the back end here and port it back to QEMU + afterwards. The platform parts still port from QEMU. +- Graphics credits are not counted. Iris keeps the register values IRIX writes; + the FIFO level, the FIFO-low interrupt and `GFifo`'s busy answer are the only + throttle. VPro already works this way. When `PiGfx` leaves + `src/dev/odyssey/` for the Hub, the test-only credit paths go with it. +- KONA's board state saves and restores through the chunk store. The Octane + refuses a save with IMPACT, VPro or two CPUs, and the IP27 machines refuse + with a reason too, until multi-CPU IP27 and IP35 state round-trip. +- The KONA model is an approximation. Success is stock software behaving and + the model agreeing with itself. Nothing I hold gives hardware pixels. +- Out of scope: the Mesa provider, more than one pipe. + +## Done when + +1. The Origin 200, the Origin 2000 deskside (two nodes, four CPUs) and the + Onyx2 deskside (two nodes) boot IRIX 6.5.30 to a serial login. +2. The Origin 3200 with two C-bricks boots IRIX to a serial login. +3. IRIX 6.5.30 installs in Iris on the Onyx2 deskside and the installed disk + boots to a serial login. +4. The Onyx2 deskside with InfiniteReality2 on one RM reaches 4Dwm, and + atlantis, ivview and the textured-cube demo run without a pipe kill or FIFO + fault. Their recorded streams replay cleanly. +5. KONA's state and the multi-CPU IP27 machine round-trip through a snapshot. +6. Every branch passes `cargo test --workspace` and the performance rule. + +## Rules for every PR + +- No new branch, lookup or lock on a per-access, per-instruction or per-pixel + path. Measure with every function 64-byte aligned in both builds. A dip that + survives equal alignment is a bug. CPU and platform PRs run `iris-bench`; + KONA PRs replay a recorded 4Dwm-plus-GL stream and a fill-rate benchmark. +- Tests run under plain `cargo test` on `TestBoard`, free their device maps + (`MAX_MAPS` stays 64), retry on `BUS_BUSY` and never wait on the wall clock. +- A file ported from QEMU keeps `Ported from hw/... in jamesbraid/qemu-sgi-origami`. + Files that aren't ports say "Designed for Iris". +- Use published names only: SGI's manuals by number and page, the 1997 + SIGGRAPH InfiniteReality paper, Kilgard's 1997 paper and SGI's 1998 Onyx2 + technical report. Describe registers by role. No names or numbers taken from + IRIX or unpublished sources. +- One commit per logical change, fixups folded in. diff --git a/debug.md b/debug.md index 9c009a751..2732b99d6 100644 --- a/debug.md +++ b/debug.md @@ -502,6 +502,17 @@ guest. `y = 0` is the top display line throughout. | `vpro trace [fifo,draw,cpu,dma\|all]` / `vpro trace flush\|off` / `vpro trace` | Annotated trace (`gputrace` builds; also `IRIS_ODSY_TRACE=` from power-on): `FIFO` lines are command words as the parser completes them, named, with their operands as words and as floats where the word carries floats, plus copy rects, pattern and matrix loads, image boxes and bursts, queries and unknown words; `DRAW` lines each assembled primitive sent to the raster unit (`draw TRIANGLES n=3 ctx=1 textured depth queued cost 96`) and each clear; `CPU` lines every bus access that is not a command store, repeats collapsed; `DMA` lines the page-table descriptors. `flush` writes the buffer out (it is written at most twice a second otherwise); alone, the trace's state | | `vpro rec ` / `vpro rec mark\|off` / `vpro rec` | Replayable recording (record.rs; also `IRIS_ODSY_REC=` from power-on): every bus store and load and the DMA reads, gzip. A start, a `mark` and `off` wait for the board to go idle and write the memory hash (`vpro hash`) into the file, so a replay can check board memory at those points; a recording started mid-session cannot reproduce the live memory and says so when replayed, but still replays deterministically. Alone, the recording's state. Replay: `VPRO_REPLAY= cargo test --release --lib odyssey::golden::replay -- --nocapture` (see `src/dev/odyssey/testdata/README.md`) | +**InfiniteReality (KONA)** + +| Command | Description | +|---|---| +| `kona status` | The pipe the back end draws for: raster managers, texture memory per manager, board set, managed area, the most words a pixel's record may take, whether the display handle is attached, the pixel store's allocated word arrays, buffer configurations laid out and refused by reason, and the last layout, the textport's colour, raster position and draw count, the X state of the context the last job ran under (graphics context, position, clip list, scissor, stipple, fills drawn and refused) with how many contexts have loaded any, the X pixel commands taken (setups, writes by data run and by DMA, copies, reads, finishes) with what was refused and why (context or setup not understood, target with no store, destination too wide, window copies, reads answered with zeros, frame-buffer word reads, rows that arrived short or with no write waiting, read brackets refused or left with no completion), the kernel's memory-region commands counted with the bytes the swaps name, the raster stage (triangles set up, with no area or off the target, clears, workers and tile size), the GL path (commands taken, vertices and primitives, draws handed over, clipped, culled and cut by the window, and the tokens and enable caps it did not take), the display tables (display-ID entries and how many the model does not read, mode words, whether gamma is loaded, the cursor's mode and address, entries dropped), and the fill rule | +| `kona pix ` | The words of one pixel's record that have an array in the pixel store, as the planes hold them. Reads the store without waiting for the command stream, so it can show a pixel mid-draw | +| `kona shot ` | The frame the display would compose from the store and the display tables now (the textport colour map for a window with no table entry; the X colour map, gamma and cursor otherwise), written as a PNG | +| `kona hash` | The hash of the pixel store, to compare a live board with a replay. Like `pix` and `shot` it does not wait for the command stream: a hash taken from a running board can be taken mid-draw. `status` and `counters` read the back end's state and wait for it at most half a second, then answer `kona: board busy` | +| `kona gl [census\|clear]` | What the GL path counted: commands, begins by mode, vertices and primitives, the texture-coordinate forms, clipping, culling and window cuts, what was handed over, and the tokens and enable caps not taken. `census` makes every vertex also be measured against the window (an extra divide each), counted as inside, outside the window, outside the volume or behind the eye; `clear` zeroes the counts | +| `kona counters` | Jobs the back end has taken, by kind, with the words the command framers dropped | + `iris-ci screenshot` captures the composed screen from any board, also headless. `iris-ci screen-wait GOLDEN.png` takes screenshots until one matches a PNG and, on a timeout, leaves the last one and a diff image behind. diff --git a/docs/graphics/README.md b/docs/graphics/README.md index 7f0ef47a0..6ea37197e 100644 --- a/docs/graphics/README.md +++ b/docs/graphics/README.md @@ -15,7 +15,7 @@ implement, compares them, and describes the structure they share. | IMPACT (MGRAS) | Indigo2 IMPACT, Octane | [mgras.md](mgras.md) | | O2 graphics (CRIME, GBE) | O2 | [o2.md](o2.md) | | VPro (Odyssey) | Octane | [vpro.md](vpro.md) | -| InfiniteReality (KONA), in progress | Onyx, Onyx2, Onyx 300 and Onyx 350 (device only; no machine profile yet) | [kona.md](kona.md) | +| InfiniteReality (KONA) | Onyx, Onyx2, Onyx 300 and Onyx 350 (Onyx2 only, with `[graphics] board`; see the [Onyx2 guide](../onyx2-ip27.md)) | [kona.md](kona.md) | The Machines column lists the machines each board shipped in. Iris accepts some boards on more machine profiles than the column shows; `GraphicsBoard::supports` in `src/config.rs` lists them. @@ -84,13 +84,11 @@ nothing: the guest's own library code runs as ordinary guest code. | Stage | Express | Newport | IMPACT | O2 | VPro | InfiniteReality | |---|---|---|---|---|---|---| -| Geometry (stages 1-3) | shared GL core | guest | shared GL core | guest | shared GL core | shared GL core (planned) | -| Rasterisation and shading | span interpreter, optional span JIT | shader table chosen per draw, optional JIT | interpreter or JIT, one thread per raster engine | plain Rust | tile workers, or the host GPU | integer 2D loops; tile workers for GL (planned) | +| Geometry (stages 1-3) | shared GL core | guest | shared GL core | guest | shared GL core | shared GL core | +| Rasterisation and shading | span interpreter, optional span JIT | shader table chosen per draw, optional JIT | interpreter or JIT, one thread per raster engine | plain Rust | tile workers, or the host GPU | integer 2D loops on the front end; tile workers for GL | | Frame buffer | word arrays | word arrays | per-engine pages | guest RAM | one word array, or a GPU buffer | one pixel store | | Scan-out | display thread | display thread | display thread | display thread | display thread, or the host GPU | display thread | -Entries marked planned are not built yet. - ### 2D drawing in Iris Each board's 2D commands run on its command thread, or on the front-end thread @@ -100,7 +98,7 @@ and not publicly documented for Express, VPro and InfiniteReality. ## Threads -Boards split the work into one to three threads after the CPU. The CPU thread writes drawing commands to the FIFO, and a few registers act on the CPU thread directly (O2 GBE, Newport CONFIG and DCB). Newport and O2 have one command-and-draw thread. Express and IMPACT split decode from raster, and VPro splits it twice. InfiniteReality's tile workers are planned. A display thread reads the frame buffer at each refresh and presents it. +Boards split the work into one to three threads after the CPU. The CPU thread writes drawing commands to the FIFO, and a few registers act on the CPU thread directly (O2 GBE, Newport CONFIG and DCB). Newport and O2 have one command-and-draw thread. Express and IMPACT split decode from raster, and VPro splits it twice. InfiniteReality splits it once, into tile workers. A display thread reads the frame buffer at each refresh and presents it. ```mermaid flowchart LR @@ -125,13 +123,11 @@ flowchart LR direction TB vp0["CPU"] -->|FIFO| vp1["front end"] -->|draw jobs| vpr["raster"] -->|tile queues| vp2["tile workers"] -.->|memory| vp3["display"] end - subgraph KN["InfiniteReality (in progress)"] + subgraph KN["InfiniteReality"] direction TB - kn0["CPU"] -->|FIFO| kn1["front end and 2D"] -.->|memory| kn2["display"] - kn3["tile workers (planned)"] + kn0["CPU"] -->|FIFO| kn1["front end, 2D and set-up"] -->|tile queues| kn3["tile workers"] -.->|memory| kn2["display"] end EX ~~~ NP ~~~ IM ~~~ O2 ~~~ VP ~~~ KN - style kn3 stroke-dasharray: 4 3 ``` Solid arrows are queues. Dotted arrows are reads of board memory, mostly unlocked. diff --git a/docs/graphics/kona.md b/docs/graphics/kona.md index c0f5172e8..45c76f0c4 100644 --- a/docs/graphics/kona.md +++ b/docs/graphics/kona.md @@ -1,32 +1,43 @@ # InfiniteReality (KONA) -Source: `src/dev/kona/` on the `kona` branch. Part of [Graphics boards](README.md). +Source: `src/dev/kona/`. Part of [Graphics boards](README.md). -The InfiniteReality model is on the `kona` branch and not yet in main. +InfiniteReality is the graphics pipe of Onyx and Onyx2 (later Onyx 300 and Onyx 350 as InfiniteReality3/4). Iris models an InfiniteReality2 or InfiniteReality2E pipe with one or two Raster Managers, fitted in the graphics slot of the [Onyx2 deskside](../onyx2-ip27.md). The pipe's window is the machine's display. No other machine takes the board. The product table also holds an InfiniteReality3, which shipped in other chassis than the deskside, so no machine fits it and it has no board name yet. -InfiniteReality is the graphics pipe of Onyx and Onyx2 (later Onyx 300 and Onyx 350 as InfiniteReality3/4). This board is in progress: the -back end draws the console, the X server's fills, pixel moves and stipples, and composes the desktop display. GL drawing is planned, not built. The first part of this page is -what exists. The second part, marked as planned, is the design for the rest. +The pipe is a crosstalk widget. The processor stores commands to one command page, in one of three grammars that share the page: the PROM's textport, the kernel and X server's commands, and the GL client's. A Host Interface Processor takes the words, answers the commands about the board itself (identity, configuration, flash, DMA, display lists) and passes everything that draws or changes display state on. The Geometry Distributor, Geometry Engines, Raster Managers and Display Generator behind it are the back end. -The plan is written down in `clanker/plans/kona-back-end.md` and -`clanker/plans/onyx2-infinitereality.md` on the `clanker-onyx2-plan` branch. They -are not on this branch. +The back end draws the PROM's console, the X server's fills, pixel moves and stipples, and the GL client's triangles, lines, points, pixel operations and textures, and it composes the desktop display. Nothing available gives hardware pixels, so the whole back end is an approximation and no test claims pixel accuracy. Input the back end does not take is counted per kind, shown by the `kona` monitor commands and logged. Nothing is answered with a made-up success. -## Built +## The pipeline + +On the real pipe the Geometry Engines transform, light and clip. Each Raster Manager (one, two or four per pipe) has one Fragment Generator, which sets triangles up from screen-space vertices, scan-converts, computes texture addresses, filters texels, applies fog and does the alpha test. Its Image Engines do the per-fragment operations and hold the frame buffer. The Display Generator reads the pixels out. + +Iris runs transform, lighting, clipping and set-up on the front-end thread, the per-pixel stages on tile workers and scan-out on a display thread. Fog and smoothing are applied by the model's own rules, not the board's. + +```mermaid +flowchart LR + A["Transform and lighting
Hardware: Geometry Engines
Iris: shared GL core, on kona-hip"] + B["Clipping
Hardware: Geometry Engines
Iris: shared GL core; near and far planes,
user clip planes,
x and y only at a guard band"] + C["Set-up
Hardware: Fragment Generator, from screen-space vertices
Iris: on kona-hip; 1/8 pixel grid, exact integer edges,
cut to the scissor and viewport"] + D["Rasterisation
Hardware: Fragment Generator, scan conversion
Iris: kona-tile-N workers, in 2x2 quads"] + E["Shading and texture
Hardware: Fragment Generator, texture
addressing, filtering, fog, alpha test
Iris: tile workers; fog and smoothing by the model's rules"] + G["Per-fragment operations
Hardware: Image Engines
Iris: tile workers"] + H[("Frame buffer
Hardware: Image Engines, one 256K x 32 SDRAM each
Iris: one u32 array per pixel word")] + I["Scan-out
Hardware: Display Generator
Iris: kona-display composes at retrace"] + A --> B --> C --> D --> E --> G --> H --> I +``` + +### 2D drawing + +The textport and the X server's integer fills, pixel moves and stipples do not go through the transform, clip or set-up stages. They run on the `kona-hip` front-end thread, after the tile workers have drained, straight into the pixel store. What the board does with these commands is not publicly documented. + +- **The textport** sets a colour index, plots points, fills boxes, draws bitmaps and loads the textport's colour map. A pixel is a colour index, resolved at scan-out. The placement of a bitmap is the model's reading of the glyph routines that send it. +- **The X server** loads a graphics context, a drawable position, a clip list, a scissor and a stipple cache for each rendering context, and each context keeps its own copy. Boxes, rectangles, spans and points draw under that state into the main planes, the overlay and popup planes or the display-ID plane, clipped and masked by the stipple. Background points draw in the context's background colour and are not stippled. Pixel writes, reads and copies are taken too. Relative points step from the previous point, the first from the drawable's origin, and every other point and line vertex, absolute or relative, is added to the drawable's position: the senders do not zero it (the X server loads the drawable's screen position, and 386 of the 1192 position loads in the desktop recording are nonzero), the recorded loads are established and the add is inferred. A polyline arrives as n - 1 words of data preload and a one-word token (the sender counts, established; the pipe's consumption, inferred). Line segments and polylines are approximated: the segment token's drawing is unknown and the polylines' is inferred (a polyline of n points makes n - 1 segments with no implicit close), and the engine's line drawer is not decoded to the pixels it lights. The model draws both ends, a segment of no length as one pixel, and a vertex shared by two segments once, under the same state and clip, with the line through pixel centres and a tie going to the pixel nearer the end point. None of those four choices is read from a source, including the one-pixel segment of no length, whose cited evidence is a zero-width line. Whether a polyline's last vertex or a closing vertex is drawn once or twice is not known. +- **Boxes** light the columns and rows the fill rule gives: for corners `x1 < x2`, columns `x1 + 1` to `x2`. The X server's rows run down from the top of the screen and the textport's up from the bottom. The store is bottom-origin and the display flips it once. -The pipe is a crosstalk widget. The processor stores commands to one command -page, in one of three grammars (the PROM's textport, the kernel and X server's -commands, and the GL client's) that share the page. A Host Interface Processor -takes the words, answers the commands about the board itself (identity, -configuration, flash, DMA, display lists) and passes everything that draws or -changes display state on. The Geometry Distributor, Geometry Engines, Raster -Managers and Display Generator behind it are the back end. +## Data flow -`kona-hip` runs the front end (`pipe.rs`): it frames each word into a command, -handles what is the board's own, and hands the rest to a `Backend` as a typed -`Job` (`job.rs`), in stream order, on the same thread. The back end (`back/`) owns -what the job changes. It handles the textport, the X server's integer fills and -the display. Most GL commands are counted and dropped; buffer configuration is handled. +The processor's stores go into a ring and the processor goes on. `kona-hip` takes the stores in order, frames each word into a command, handles what is the board's own and hands the rest to the back end as a typed job. Registers the driver polls while the pipe works are served at once from a register file. ```mermaid flowchart LR @@ -34,173 +45,149 @@ flowchart LR CPU -. "answered at once" .-> HOST["host registers
one short lock"] RING --> HIP["kona-hip
frame, answer, DMA, lists"] HIP -. "DMA" .-> RAM[("guest RAM")] - HIP -- "Job" --> BE["back end
token layer: textport, X fills"] + HIP -- "Job" --> BE["back end
token layer, X and textport drawing,
GL transform, clip and set-up"] BE -- "integer fills" --> STORE[("pixel store
u32 array per word")] + BE -- "triangle jobs" --> TILES["tile scheduler
slot ring, tile queues"] + TILES --> WORK["kona-tile-N
shade, test, store"] + WORK --> STORE + TEX[("texture store")] --> WORK STORE --> DISP["kona-display
compose at retrace"] DISP --> SCR["GfxDisplay screen"] DISP -- "retrace tag, video irq" --> HOST ``` -The board is given three handles instead of a bus and an interrupt controller -(`attach.rs`): one to send interrupt packets, one for host DMA, one called when -the FIFO has drained. The attach interface names no Hub or HEART type; the machine's glue -implements the three handles. - -The state of the front end sits in a slot (`Shared::pipe`). The thread, or a -processor that finds the ring empty, takes it out for an instant, works with no -lock and puts it back. A taken slot is busy to the bus. The machine's DMA, -interrupt and FIFO handles are therefore never called with a board lock held. - -Pixel memory is one `u32` array per word of a pixel's record, each the size of -the managed area, allocated whole on first write and published through a fixed -atomic pointer (`back/store/`). The record can hold many words, laid out by the -window's buffer configuration. A fill that touches one word writes contiguous -memory, and a window that uses 13 words allocates 13 arrays. The elements are -relaxed atomics, so a display read that races a write is defined and costs a plain -load. Which physical memory holds a pixel is unknown, and the function from -(pixel, word) to storage is behind one method (`word_slot`). - -A retrace request carries a tag. The display thread completes the requests -waiting at each frame boundary, sets the retrace tag register and raises the -video interrupt through a `DisplayHandle` (`handle.rs`), which holds the -front end weakly and drops calls made in a previous reset epoch. - -Not modelled: which Raster Manager holds a pixel, raster-bus words and -flow-control credits. +The machine gives the board an `InterruptController`, a `BusDevice` for host memory, the status-bar source and, optionally, a flash file. The board names no Hub or Bridge type. Each interrupt packet is one `set_line(vector, true)`: it sets the vector's pending bit, and the board never clears one. A full ring answers the bus busy and the processor retries the store. Graphics credits are not counted. -## The pipeline - -On the real pipe the Geometry Engines transform, light and clip. Each Raster -Manager (one, two or four per pipe) has one Fragment Generator, which sets triangles -up from screen-space vertices, scan-converts, computes texture addresses, filters -texels, applies fog and does the alpha test. Its Image Engines do the per-fragment -operations and hold the frame buffer. The Display Generator reads the pixels out. Dashed boxes are planned. +`Shared::pipe`, a mutex over the front end's state and the back end, is held by `kona-hip` for each batch of stores, across calls to the machine's memory and interrupt controller. That cannot deadlock, because the machine never calls back into the board. A processor access only `try_lock`s it, for a read that has to wait for the stream, and answers busy when it is out. The register file has its own short lock, never held across a call out of the board. -```mermaid -flowchart LR - A["Transform and lighting
Hardware: Geometry Engines
Iris, planned: shared GL core in the token layer,
on kona-hip"] - B["Clipping
Hardware: Geometry Engines
Iris, planned: shared GL core, then a window-space
clip before the hand-off"] - C["Set-up
Hardware: Fragment Generator, from screen-space vertices
Iris, planned: kona-raster, exact integer edges"] - D["Rasterisation
Hardware: Fragment Generator, scan conversion
Iris, planned: kona-tile-N workers in 2x2 quads"] - E["Shading and texture
Hardware: Fragment Generator, texture
addressing, filtering, fog, alpha test
Iris, planned: tile workers"] - G["Per-fragment operations
Hardware: Image Engines
Iris, planned: tile workers.
Built today: integer fills, pixel moves, stipples"] - H[("Frame buffer
Hardware: Image Engines, one 256K x 32 SDRAM each
Iris, built: one u32 array per pixel word")] - I["Scan-out
Hardware: Display Generator
Iris, built: kona-display composes at retrace"] - A --> B --> C --> D --> E --> G --> H --> I - classDef planned stroke-dasharray: 4 3 - class A,B,C,D,E,G planned -``` +The front end frames words with tables of the command selectors and drops words the tables lack. Every selector, register offset, word index and bit field is a named constant in `defs.rs`. The back end keeps each rendering context's X state, GL state and texture state together, and the front end names the current context with every job. -## 2D drawing +Pixel memory is one `u32` array per word of a pixel's record. The record holds up to 64 words, laid out by the window's buffer configuration (`back/store/layout.rs`): colour buffers, depth, 8-bit stencil, multisample colour and accumulation, in that order. Each array is the size of the managed area, allocated whole at a draw's set-up and published through a fixed atomic pointer, so a worker or the display never checks for allocation. A fill that touches one word writes contiguous memory, and a window that uses 13 words allocates 13 arrays. The elements are relaxed atomics, so a display read that races a write is defined and costs a plain load. Four planes sit beside the record: the display-ID plane, overlay 0, overlay 1 and the popup. Texture memory is a separate store of one 32-bit word per texel, sized as one Raster Manager's texture memory. Which physical memory holds a pixel is unknown, and the function from (pixel, word) to storage is behind one method (`word_slot`). -The textport and the X server's integer fills, pixel moves and stipples run on -the kona-hip front-end thread. What the board does with these commands is not -publicly documented. +Not modelled: which Raster Manager holds a pixel, raster-bus words and flow-control credits. ## Threads ```mermaid flowchart LR - cpu["CPU thread"] -->|ring| hip["kona-hip
front end and integer fills"] - hip -.->|pixel store, read unlocked| disp["kona-display
compose at retrace"] - raster["kona-raster, kona-tile-N
planned, not built"] - style raster stroke-dasharray: 4 3 + cpu["CPU thread"] -->|ring| hip["kona-hip
front end, token layer, set-up, integer fills"] + hip -->|tile queues| tile["kona-tile-N
shade and store"] + tile -.->|pixel store, read unlocked| disp["kona-display
compose at retrace"] + hip -.->|pixel store| disp ``` -The front end runs the ring and the integer fills on one thread. The raster and -tile threads belong to the planned back end and are not built yet. +`kona-hip` runs the front end, the token layer and set-up. A draw's set-up goes into the next slot of a ring, and the slot's index goes on the queue of every screen tile the draw can touch. A `kona-tile-N` worker takes a ready tile and owns it until its queue is empty, so every pixel sees its work in command order. `[graphics] threads` sets the worker count (default 2) and `[graphics] tile` the tile size in pixels (default 32). A work budget in estimated pixels stops `kona-hip` running far ahead of the workers. A panic in a worker poisons the scheduler, and the board reports itself dead. -## Planned: the drawing back end +Anything that reads or writes pixels outside the workers, such as an integer fill, a pixel read, a swap or a retrace request, drains the tiles first. The display thread waits for nothing and reads the store without a lock. It composes the whole frame at 60 Hz with no change tracking. At each frame boundary it completes the retrace requests made since the last one and raises the video interrupt, through a handle to the register file. A reset begins a new epoch, so a call from a previous one is dropped. -None of this is built. Nothing available gives hardware pixels, so the whole back -end is an approximation, and no test will claim pixel accuracy. Unexpected input -is counted per kind, shown by the `kona` monitor commands and logged. Nothing is -answered with a made-up success. +## The display generator's channels -### From a GL token to pixels +The X server describes the screen to the display generator with video commands: it loads a video format into each of eight channels, says which rectangle of the managed area a channel shows, and enables or blanks it. The back end keeps what those setters leave behind (`back/display/channels.rs`), answers the Gets from it on the return page, and, when the loader releases the firmware, runs the video combination stored in the flash or the built-in one. The state is part of the snapshot. Gets answer from the model's state; no reply was measured from a pipe. -Front-end tokens become typed jobs. The token layer runs on the front end's -thread, in stream order. It tracks GL contexts and uses the shared GL core -(`src/dev/gl/`) for transform, lighting, clipping and window mapping, the way VPro -does: the board's vertex type implements `ClipVertex`, and clipping and -projection run under the board's own `Geometry`, so the core needs no change. -Primitive assembly, cull, polygon modes and the window-space clip are the board's -own. +Blanking blacks the frame when it blanks channel 0 (the one the board shows) or, as a temporary blank, every channel; the pixel store is untouched and the picture returns when the blank ends. Other channels' blanking is kept (`kona status` lists the channels blanked, and each change is logged). Kept and not acted on: a managed size other than the configured one (a combination that names another is logged and the configured size is what the board reports), a channel taking another's timing (Vampire), a channel bound to an X window, and the timing words of a loaded format. The board shows one channel's worth of pixels at the managed size, 1280 x 1024 to begin with. -```mermaid -flowchart LR - CMD["Job::Command, grammar Gl
selector, args, packed"] --> TL["token layer
match on selector"] - TL -- "state tokens" --> CTX["context slot
GL state from the shared core,
plus raster state in groups"] - TL -- "vertex tokens" --> XF["transform and light
math::xform, Lighting::light_vertex"] - CTX --> XF - XF --> ASM["primitive assembly
strips, fans, quads, polygons
to triangles, lines, points"] - ASM --> CLIP["clip_polygon_by with the board's Geometry,
window mapping, quantising for the hand-off"] - CLIP --> JOB["RasterJob: typed vertices
plus an Arc of each state group"] - CTX -- "snapshot per draw" --> JOB - JOB --> SCHED["tile scheduler
one owner per tile"] - SCHED --> SET["set-up in the raster stage
exact integer edges on a 1/8 pixel grid"] - SET --> QUAD["2x2 quads: plane equations at each pixel,
integer or reproducible f32"] - QUAD --> FOP["texture, fog, then
stencil, depth, blend, logic op"] - FOP --> STORE[("pixel store")] - classDef planned stroke-dasharray: 4 3 - class CMD,TL,CTX,XF,ASM,CLIP,JOB,SCHED,SET,QUAD,FOP planned +## From a GL token to pixels + +The pipe, not the guest's graphics library, keeps the matrices, lights and material. The GL client's state commands update the rendering context's state. A begin opens a primitive, vertex commands transform, light and assemble it, and an end closes it. Commands the model does not take (the colour-index double form and vertex arrays among them) are counted by selector and shown by `kona status`. + +### Geometry + +- **Assembly.** Triangles, strips, fans, quads, quad strips and polygons are broken into independent triangles, a polygon of more than three vertices into one fan from its first. That the engines split polygons this way is inferred. Flat shading takes the colour of the vertex GL names. Two-sided lighting and culling use the polygon's projected orientation. +- **Polygon modes.** The polygon fill mode command carries two words, the face then the mode (established). The pipe checks neither: a face other than front, back or both is ignored without an error, and a mode word is kept as sent. A triangle reads its face's mode as line when it equals GL's line mode, as point when below it and as fill when above (established), and the model does the same. The pipe also writes a line-mode flag from each call's mode alone; the model keeps no such flag. A face in the line mode is drawn, after clipping and culling, as lines between the vertices whose edge flag is set, and one in the point mode as points at them. They take the line width and stipple or the point size, and the face's own colours, not the polygon stipple, and they are smoothed by the polygon-smooth switch and not the line or point one (inferred). A triangle's and a polygon's edges come from the per-vertex edge flags (inferred). A quad strip's pieces get a constant flag word whatever the flags, so the user's edge flags do not reach them (the constant is established, its meaning inferred); the model draws a quad's four sides. Which edges of a triangle strip or fan draw is unknown, and the model draws every edge of each piece. The pipe applies polygon offset to a polygon drawn as lines or points, under an enable of its own for each mode, and never to a line or point primitive (established); the model keeps no enables for the two modes and applies no offset to them, a limit. +- **Clipping.** The shared GL core clips in clip space (`src/dev/gl/`, with `KonaGeometry` in `back/gl/vertex.rs`). Near and far are clipped geometrically against two planes. x and y are cut only at a guard band eight clip widths out from the centre, which keeps window positions inside an `i32` grid. Which exact depth range the engines cut is unconfirmed. Nothing clips in window space. +- **User clip planes.** `glClipPlane` arrives as a plane name and four doubles (nine words) and the plane's enable through the generic enable token, which takes only the six names 0x3000 to 0x3005 and refuses the rest of 0x3000 to 0x3fff as an invalid enumerant; the framing, the names and the query that reads a plane back are established. The double narrowing to single precision first is inferred. The model keeps the plane in eye space, the coefficients times the full 4x4 inverse of the model-view matrix at the call. That the pipe does that is inferred; a matrix with no inverse is an unknown, and the model leaves the plane unchanged. A vertex is tested against the six view-volume planes and the enabled user planes together, as the core's planes 0 to 11, for triangles, lines and points. A primitive's begin copies the planes and enables into the clipper once, so a vertex takes no new branch. +- **Texture-coordinate generation.** The one token (nine words: a coordinate, a parameter name, seven value words) and the per-coordinate enables are established. Its modes are object linear, eye linear (the start-up mode, established) and, for s and t only, sphere map. The equations are OpenGL's, in `src/dev/gl/texgen.rs`, and are inferred: no source states the arithmetic. Eye planes are stored in eye space like clip planes. A generated coordinate replaces the current one before the texture matrix, and only while texturing is on. A begin decides once whether anything is generated and whether that needs the normal. The pipe accepts the coordinates 0x2000 to 0x2003, the names 0x2500 to 0x2502 and 0x81f4 to 0x81f7 and the modes 0x2400 to 0x2402 and 0x81f0 to 0x81f3, and refuses any other value (established). The point and line distance planes and modes are accepted, counted and change nothing. Sphere map on r or q is not refused, and the engine appears to ignore it (inferred): the model keeps the mode and leaves the coordinate. The eye plane is stored times the inverse model-view, the object plane as sent; start-up is OpenGL's (s = (1,0,0,0), t = (0,1,0,0), r and q zero, eye linear). +- **Double forms.** The double colour, normal, texture-coordinate and vertex tokens are narrowed to single precision as the command arrives, high word first, and run the float forms' path. The texture-coordinate forms are established and the others inferred. The colour-index double form stays unmodelled; the model has no colour-index mode. +- **The hand-off.** A vertex leaves the geometry stage as a window position, a 32-bit depth fraction, four 13-bit colour components and two sets of four texture words (`s/w`, `t/w`, `r/w`, `q/w`; zero when untextured). That a textured vertex carries a fourth word is inferred, and so is its form; the model uses `q/w`. +- **One snap.** A window position is rounded to the nearest 1/256 pixel, ties to even, once. This stands in for the geometry engine's subtract of 49152.0 putting the position on that grid. The rounding mode is inferred. + +### Set-up and the sample rule + +Set-up runs once for each triangle, on `kona-hip`. It takes the GL half-pixel offset off x and y, rounds to the nearest 1/8 pixel with ties up, and adds the window's origin in whole pixels. The 8 x 8 sub-pixel accuracy is published; the rounding rule is the model's. Edges are exact integers on that grid. + +A sample sits at the pixel's integer corner (inferred). A sample exactly on a left or bottom edge is out, and one on a right or top edge is in (inferred, from the whole-pixel rectangle the textport sends). Every sample falls on one side of an edge two triangles share, so shared edges are watertight. A triangle of no area draws nothing. + +There is no window-space clip. A triangle is cut to the box of its sample points, the scissor and clip rectangle, and the viewport, and that rectangle is the job's. The attribute planes are valued at the rectangle's corner, so a triangle far outside the screen keeps its slopes and colour where it crosses the screen's edge. `f64` is used only here, to solve the attribute planes. + +### Per pixel + +A tile worker walks a triangle in 2x2 quads aligned to even screen coordinates, clipped to its tile. The walk keeps the window's own pixel grid, so a tiled run is bit-exact with an inline one. The per-pixel arithmetic is integer, or `f32` evaluated as separate multiplies and adds, so a result is the same on every host and a GPU kernel could reproduce it. The edge functions and the depth plane (32.16 fixed point) are exact integers. Colour is linear in screen space. `WordArray::store` is the one write into the pixel store. + +A fragment goes through the window-ID ownership test, the stipples, the texture environment, fog, the smoothing coverage, the alpha test, the stencil test, the depth test, blending, the logic operation and the colour mask. Everything after the ownership test and the stipples is OpenGL's order, which is the model's choice; the board's is unknown beyond the alpha test coming before the raster engines. Blending is the add equation. Dither is carried and applied as off. A draw can carry a polygon offset, and one stencil state serves both faces. + +**Fog** is OpenGL's linear, exp or exp2 factor, clamped to 0 to 1, of the fragment's eye-space distance `|z|`; red, green and blue become `f C + (1 - f) Cf` and alpha is untouched. A vertex carries the distance over w and 1 / w, set-up solves both as screen-linear planes and each fragment divides them, so the distance is perspective-correct. `exp` is built from multiplies and adds in `back/raster/fog.rs`, not the host's, so a pixel is the same on every host. The pipe keeps the enable and the mode, density, start, end and colour (the generic state query answers them); the float setter is five words and the integer one a name word with a four-word continuation (established). The pipe evaluates the function on the eye distance recovered from the window depth, and builds a table of 12-bit entries indexed by depth plus an offset (the entry width is established, the depth indexing inferred). The model evaluates the function at each fragment's interpolated eye distance and rounds the factor to a 12-bit entry, so it does not model the table's steps or its offset (approximated; how close it comes to the pipe is unknown, and a comparison of the pipe's fog output with the model's would settle it). The setter accepts seven names (index, density, start, end, mode, colour and an offset extension) and four modes (linear, exp, exp2 and a function table); any other name or mode is an invalid enumerant that leaves the mode as it was, and start, end and density are not range-checked (established). The index, the offset and the function table are accepted and not modelled. An integer colour is scaled by 2^-31 and clamped to 0 to 1 (the scale is established, reading it as a value mapping is inferred). The blend's rounding, the fog index of the colour-index mode and the fragment order are unknown. + +**Smoothing** (`back/raster/smooth.rs`) weighs a point, line or polygon by the share of each pixel it covers, and the share scales alpha after fog and before the alpha test. The enables and the sampling classes are known; the coverage rule is not, so every choice is the model's own and written at the top of that file. A polygon edge, a line side and a line end are exact half-plane areas, multiplied together at a corner; a point is a disc sampled on an 8 x 8 grid. A line is its rectangle and a polygon its clipped outline, handed to set-up whole and drawn as the usual fan, with the fan's inner edges on the plain centre test so a seam does not blend twice. Neighbouring polygons or segments do blend where they meet, as GL's do. A polygon of more than eight edges after clipping is not smoothed. Smoothed lines and points deliver coverage as alpha in 8 bits, through ramps the pipe loads with 8-bit values (established for the ramps, inferred for their use), so the model rounds their coverage to a multiple of 1/255; a smoothed polygon's coverage is computed in the pixel generator by a rule that is unknown, and stays the exact area (approximated). In a multisampled window a smoothed line or point still takes coverage (writing every sample, alpha scaled) and a polygon multisamples, as the sampling-word values and the point handler's order of tests say (established). With blending off the coverage shows only in alpha. + +The ownership test is the rule between overlapping windows. When it is enabled, a fragment whose display-ID plane value fails the compare against the draw's reference is dropped. X drawing does not take it. + +### Lines and points + +A line becomes the rectangle of the line width about its segment, as two triangles, and the line stipple indexes its pattern by the distance along the segment. A strip's stipple restarts at each segment. A one-pixel point is a unit square about the vertex, and a larger point a disc of sixteen triangles. Both forms are Iris's own. With smoothing on, a point is the square about its disc and a line its rectangle, weighed by coverage (see Per pixel). Lines and points are multisampled like triangles, and they are not textured. + +### Texture + +Texture loads, binds and sampling are in `back/gl/texture.rs` and `back/raster/texel.rs`. A level reaches texture memory as texel draws and a load finish. The load does not name its level: an address word and a bank bit say where it lands, and the model keeps levels by bank and address for each context. A bind builds a flat descriptor with no pointers, in the draw state. + +The texel layout in texture memory is the model's own: one `0xAARRGGBB` word per texel, with narrower formats reduced to their width at load. A load takes the plain component types and the packed types 3-3-2, 4-4-4-4, 5-5-5-1, 8-8-8-8 and 10-10-10-2 (RGB for the first, RGBA for the rest; the sizes are established, and the bit order and scaling are OpenGL's, inferred); other formats and types (colour index, stencil, depth, CMYK) are counted as refused loads and a bind of their level is refused. Texture memory is one pool for the pipe (established), 64 MB on an InfiniteReality2 or 2E and 256 MB on an InfiniteReality3, in two banks. The pipe keeps no allocation of its own: a load writes where its address says, an address being in units of 256 bits of a bank and relative to the owner's block, so a level reloaded at the same place overwrites it, and one a later load overlaps is gone (established). The model addresses it the same way. A level at address `a` of a bank is the words from `16 a` of that bank's plane, one model word to a texel with room for a unit's sixteen, so nothing is placed, nothing fragments, and a load is refused only when its address and extent reach past the bank, or when its texels would take the planes together past the product's texture memory (counted in `past memory`, and the levels already there are left alone). Those refusals are the model's own: the pipe has nothing to refuse with, and the kernel keeps a client inside its block. A plane is sparse, a table of 64 KiB pages made by the first nonzero word written into each, so host memory follows what was loaded and a texel at the top of a bank costs a page; the pages held across every plane are capped at the product's texture memory counted a word to a 16-bit lane, the most texels it can carry. The global-offset registers that place one owner's block are not modelled, so each context has planes of its own, a bank long at most, where the pipe's owners share one pool (approximated: contexts neither contend for memory nor alias, so a texture loaded through one member of a share group (contexts the kernel lets see the same physical block) is not visible to a bind from another member). A context whose slot is activated afresh gives its planes back. Sampling is nearest or bilinear within a level and nearest or linear between levels, with 8-bit weights (the model's own). The texture coordinate is divided by `q/w` per fragment, so texture is perspective-correct, and the level of detail comes from the differences across the quad. Only triangles sample. Texel read-backs answer from the texture store. The clip-map, bicubic filtering and the old-style bind are counted and not modelled. + +**Volumes.** A 3D texture arrives as one texel draw per slice (the slice in header word 15, the physical depth in the r exponent of word 16), every slice carrying the same address pair, so a level is still found by bank and address and its slices stack in the level's words. The 3D bind records (tokens 0x17b and 0x17c) bind the levels, the bank of a mip-mapped volume's level being its parity. The 3D enable gates the volume, the 2D enable gates a 2D texture, and a fragment takes the third coordinate only for a volume. Established: the framing, the slice and depth words, the third wrap bit, the r field of the LOD scales, and that the mip volume's bank parity is the absolute level parity. The pipe sends slice r to member r & 1 of a mode-1 volume's address pair with no per-slice bank register (inferred), and treats the single-level bit 3 as an allocation flag it ignores (inferred); the model stacks slices in the arena by the pair's shared address instead and does not read the bit, an approximation, since where texels sit inside a member is unknown. Also approximated: the trilinear arithmetic and 8-bit weights (the pipe filters a volume trilinearly over eight texels, quadlinearly over sixteen with mips, inferred), how the three coordinate steps combine into a level (the largest step under its own scale; the pipe's r-aware rule uses the r scale register 0x0c06, inferred), the bank search order for a single-level volume, and a scale of one for the single-level record, which carries none. The single-level record writes no scale register, so the pipe keeps the previous values (established); the model's scale of one is a limit. When both enables are set the pipe draws the 3D texture (established); the model holds one bound texture and gates it by its own target's enable, so the two-enable case is a limit. + +**Detail and sharpen.** The detail bind (0x17d) and the filter tables (0x17e, kept by slot as sent) are stored per context. A mode-1 base bind of a 2D texture compares its format row with the last detail bind's: whatever magnification code it carries is forced to linear on a mismatch or with no detail bind (established; the client sends mode 1 only for the detail filters), and on a match a detail code looks for the image at the address-pair word bit 28 of the mode word chooses. The combine is built at bind time from the filter table in the control word's slot and runs only for magnified fragments, through a separate loop and sampler chosen once for the draw. A filter table is 64 ten-bit codes, a signed mantissa in bits 9:2 and an exponent in bits 1:0, value m x 2^e / 128, made by the library's encoder, which clamps to -1024..1023, halves into -128..127 and counts the halvings; the detail table holds the public curve doubled and the sharpen table holds it negated, and the model undoes both (all established, and four stored hardware test vectors decode exactly). The pipe receives 64 samples at log2(i / 64), never nodes. The detail level code is the negated public value, 0 to 4 (established). The detail texel position is the base's level-0 texel position times 2^|L|, wrapped to the detail size and sampled bilinearly, with the combine T + K (D - 1/2), K being the doubled curve, and modulate times the base (all inferred); sharpen is A + K (B - A), K being the negated curve, A the base and B the next coarser level (inferred). Approximated, each isolated in `back/raster/texel.rs`: the arithmetic widths and rounding, the blend between curve samples (the pipe's interpolation between entries is unknown), the default curve's straight line, and the treatment of channel code 3 as all channels (its behaviour is unknown). + +### Pixel operations, queries and multisample + +- **Pixel operations** (`back/gl/pixels.rs`) draw, read and copy pixel rectangles and bitmaps, and run the accumulation buffer, through GL's standard unpack, scale and bias, pixel maps and fragment stages. The command table records the pipe's state, not which stages it runs, so the stages are GL's. The accumulation buffer keeps one float per component, in a format of the model's own. +- **Queries** (`back/gl/query.rs`) answer on the context's return page with a completion word written last. A query the model cannot answer fully gets an empty or zero reply and a count, so the client never waits on it. **Select** and **feedback** passes collect the primitives instead of drawing them. +- **Multisample** windows hold 4 or 8 samples, chosen by the buffer configuration. A pattern token picks one of seven patterns per count, each a pair of location words on the 1/8 grid, and a sample mask indexes the coverage words. A fragment is shaded once, at the pixel's corner, and the stencil, depth and colour operations run for each covered sample. The pixel's colour word is the mean of its samples. The layout of the samples in memory is inferred from the buffer configuration's sizes. + +## Display and scan-out + +The display thread composes the whole managed area per row from the pixel store and the tables the X server loads. A pixel's colour word means what its window's display ID says. The ID names a window. The tables say, from the ID's entry and its mode byte together, whether that window's words are an 8-bit or a 12-bit index into the X colour map, or direct colour. A pixel whose ID has no entry, and a board that has been sent no table, use the textport's default: the front colour buffer at level 0, one word a pixel, read as an index into the textport's colour map. The popup and overlay planes go over the main window, with index 0 letting the layer below through. The colour map and then the gamma RAM follow (inferred, solid). + +The XMAP's glyph cursor is drawn over the composed frame, and the host's pointer follows the cursor registers. Channel 0's DAC cursor is drawn after that, over the composed frame and past the gamma RAM: the DAC block's index and data writes (`0xb00 + 0x100 n`, index 0x200 to 0x203 the position, 0x204 the control) are followed, and the image shows when the control register holds X11 mode with the enable bit (the value the kernel writes, 5). Value 3 of a pixel is the foreground colour, 2 the background, 0 and 1 leave the frame. The kernel sends the position as four data bytes to register 2 of the owning channel's block, the index sent again before each (established; no other address carries it). The index advances after each access in 0x200 to 0x204 and in the image's 0x400 to 0x7ff, and after each blue component of the colour registers (established for the published part); 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 (established), and (0, 0) is the first pixel after the rising edge of BLANK, whose alignment to the frame's active pixels on this board is unknown (the model takes the frame's first pixel). Value 3 takes the foreground (the triple at 0x303) and 2 the background (0x304) (established for the part, inferred that this board's DAC is that part). Not drawn, for want of anything settling them: the XGA mode, the interlace bit's effect, the load for channel 0xde, and any channel but 0. Not composed: stereo, the crosshair, 16-plane luminance and 15-bit RGB windows (counted by reason), and any gamma page but the first channel's. + +The board presents every frame it composes into the screen's frame buffer (stride 2048) and on to whatever renderer the front end installed. The window, VNC and screenshots see it through `GfxDisplay` (`src/gfx_display.rs`). + +## Approximations + +The code comments grade each choice. In short: + +- The token layer stands in for the Geometry Engines and the board's firmware, and vertices travel as typed values, not bus words. +- The snap to 1/256, the 1/8 rounding rule, the sample rule and the half-pixel offset are the model's. The range clipped in depth and the depth representation are unknown, and the fourth texture word is inferred. +- Fragment-operation order, blend and dither arithmetic, line and point rules, stipple, texture filtering and format rounding are unknown or the model's own. So are the fog table's steps and rounding (the model evaluates the function per fragment instead), the smoothing coverage rules and the edges a strip or fan draws in the line mode; the sampling-class values for smoothing and the state the pipe keeps are established. +- Texture-coordinate generation and user clip planes use OpenGL's arithmetic and eye-space plane convention in single precision. The engines' own arithmetic and the pipe's handling of a plane under a singular matrix are unknown. +- Completion is by draining the tiles. The retrace register changes at the next frame boundary, and the frame rate is a fixed 60 Hz. +- One pixel store and one texture store stand in for the Raster Managers' memory. The products differ only in what they report (board set, board records, the class word the driver reads, texture size) and in the texture store's size; the three draw alike, because no source shows a difference in how the 2E or 3 renders. The 2E's raster slot word is the 2's (type 6, 64 MB), its geometry identity word is inferred, and its RM9 and TM7 revisions and chip words are unknown (the chip words are the 2's). Every product presents the DG5-8 display generator: the DG5-2 with a GVO option has its identity word checked in a test and is not presented, and presenting it would need the display channel arrays sized by the board. The GL renderer string's letter for the 2E comes from the guest's own reading of board set 4, not from the model. + +The pipe saves its own state, the pixel store and the cursor memory as chunks of a snapshot. The Onyx2 machine refuses a snapshot, because the Hubs and node memory have no saved state. + +## Fitting the board + +Use the Onyx2 profile and name the board in `[graphics]`: + +```toml +[machine] +profile = "onyx2_ip27" + +[graphics] +board = "ir2" # one Raster Manager; "ir2:2rm" for two; "ir2e", "ir2e:2rm" for the 2E +flash = "kona-flash.bin" # optional; keeps the pipe's flash between runs + +[origin2000] +io_prom = "io6prom.img" # the I/O PROM from the IRIX 6.5.30 media ``` -- **Typed jobs, not bus words.** The token layer hands the raster stage typed - vertices and state. Raster state sits in one struct per functional group behind - an `Arc`, snapshotted once per draw, not sent as register writes in the stream. - A fixed pool of group records, as VPro uses, is the fallback if the snapshots - cost too much. -- **The hand-off is vertices, not registers.** The raster stage assembles the - primitive and computes slopes itself, so set-up belongs to the raster stage. - IMPACT's `Gl::triangle` has no counterpart. -- **Window-space clip, not clamp.** The Geometry Engines clip. How the near and - far planes are handled is not publicly documented. The model clips each polygon to the - scissor in window space before quantising, the way IMPACT does, because - clamping coordinates would change slopes. -- **A fourth texture word.** The Fragment Generator interpolates texture - coordinates perspective-correct. That each textured vertex carries a fourth word - is inferred from OpenGL's four-component texture coordinates. Its form - (1/w, q/w or 1/q) is unknown. Colour is taken as linear in screen space. -- **One snap.** The token layer truncates window positions toward minus infinity - onto a 1/256 pixel grid once, and set-up takes that value to 1/8 pixel exactly, - so nothing rounds twice. -- **One pixel store.** The Raster Managers split pixels between them, but a guest - cannot tell which one holds a pixel, so the model keeps one store, plane-major - as above, with one texture store. -- **Tiles with one owner each.** A tile scheduler of the board's own follows the - rules in `clanker/odyssey/screen-tiles.md`: the raster stage queues each - primitive on every tile it touches, and a worker owns a ready tile until its - queue is empty. The raster stage evaluates plane equations at every pixel, - where IMPACT steps iterators along an edge. The per-pixel arithmetic stays in - integers or `f32` operations a GPU reproduces; `f64` appears only in set-up. - VPro's ring and queues are tied to VPro's types and are not shared. Planned - threads: `kona-hip` for the front end and token layer, `kona-raster` for set-up - and binning, `kona-tile-N` for the tile workers and `kona-display`. -- **Display at retrace.** The display thread composes the whole frame at retrace - from the pixel store and the display tables, with no change tracking, as VPro - does. Swaps and retrace requests carry a ticket and complete at a frame - boundary. -- **The host GPU, planned and not built.** A wgpu raster path would reuse the - batch, memory and scan-out layers in `src/wgpu` with the board's own records and - kernels. The CPU path keeps three constraints for it: the per-pixel stage is - integer or reproducible `f32`, `WordArray::store` is the one write point into - the pixel store, and the records at the raster boundary are flat data with no - pointers. - -### Approximations - -Everything on the planned path is approximated or unknown, and the plan grades each -item. In short: the token layer stands in for the geometry engines and the board's -firmware; vertices travel as typed values, not bus words; the vertex snap, -sample rule and half-pixel offset are the model's; the z range clipped and the depth -representation are unknown, and the fourth texture word is inferred; -fragment-operation order, blend and dither arithmetic, line and point rules, -stipple and texture filtering are unknown; completion is by draining and swaps by -ticket; no flow-control credits are counted; and an InfiniteReality3 renders as an -InfiniteReality2. +`board` defaults to `none`, which leaves a test widget in the slot. `flash` names a file of exactly the flash's size (512 KiB), made blank when absent. Unset, the flash lives in memory. `io_prom` fits the I/O PROM in the BaseIO's boot flash. The graphics console needs it, because the node PROM's own copy has none. `threads` and `tile` set the tile workers. The [Onyx2 guide](../onyx2-ip27.md) and `FEATURES.md` have the rest. ## Monitor commands and tests -This branch has no InfiniteReality monitor commands or tests. They are on the -kona branch. +`kona status` shows the configuration, the X state the last job ran under, the video channels, and the counts of what was taken and refused. `kona counters` counts jobs by kind, and `kona gl` shows what the GL path did: commands, primitives, clipping, and what reached set-up. `kona gl clear` zeroes those counts. `kona hash` shows the pixel store's hash, `kona pix X Y` a pixel's words, and `kona shot FILE.png` writes the frame the display would show. A command that reads state waits up to half a second for the command stream and otherwise says the board is busy. The commands are in `back/mod.rs` and `back/display/mod.rs`. + +Tests run under plain `cargo test`: + +- `src/dev/kona/tests/` holds the front end's tests: registers, the FIFO, DMA, the flash EEPROM, bring-up and the display handle. +- The back-end modules carry their own tests. `back/golden/` pins scene hashes for GL triangles through set-up, the scheduler and the per-pixel module, and checks a tiled run against an inline one. Those hashes are Iris's own, so they catch regressions and show nothing about correctness. +- `back/replay.rs` replays recordings of a guest's traffic to the board, made by another emulator's model of it: the PROM, the desktop and the GL demos. It compares replies and interrupts in order, and compares the composed frames with the screen captures that came with the recordings, within a tolerance. Another model's output is not hardware evidence. The recordings are not in the repository. `KONA_RECORDINGS` and `KONA_GL_RECORDING` name them, `KONA_SHOT_DIR` keeps the frame the desktop test composes, and the tests skip with a printed reason when the files are absent. +- `back/bench.rs` is an ignored fill-rate benchmark for the integer drawing path. +- `src/ip27_onyx2/tests.rs` covers the machine side: the pipe in the graphics slot, the board names, and DMA and interrupts reaching the Hub the guest names. diff --git a/docs/onyx2-ip27.md b/docs/onyx2-ip27.md index 675ebd1ad..df6129740 100644 --- a/docs/onyx2-ip27.md +++ b/docs/onyx2-ip27.md @@ -3,9 +3,12 @@ The Onyx2 deskside is one or two IP27 node boards on the 4P1G5 midplane. Both Hubs are hosts of the one Crossbow, which carries the BaseIO-G at widget 15 and the graphics slot at widget 8. Two nodes are joined by the direct cable. The -machine runs headless on the IOC3's first serial port. There is no -InfiniteReality pipe: the graphics slot holds a test widget that answers its -identification word and nothing else. +machine runs on the IOC3's first serial port. With `[graphics] board = "none"` +(the default) the graphics slot holds a test widget that answers its +identification word and nothing else; with `"ir2"`, `"ir2:2rm"`, `"ir2e"` or `"ir2e:2rm"` it holds an +InfiniteReality2 or 2E pipe with one or two raster managers (`src/dev/kona`), whose +window is the machine's display. `[graphics] flash` names a file that keeps the +pipe's flash EEPROM between runs. ## Configuration @@ -40,4 +43,8 @@ A disk installed on an Onyx2 boots; the machine cannot install without a pipe. and pointer go to the board IOC3's controller only. - The module controller, the cable and the node windows are the [Origin 2000 deskside's](origin2000-ip27.md), with its limits. -- Snapshots are not supported. +- The pipe's interrupt packets and DMA reach the Hub at the widget the guest + programs into it (widget 9 with two nodes, 10 with one), behind that + widget's Crossbow link. +- Snapshots are not supported. The pipe can save its own state, but the Hubs + and node memory cannot yet. diff --git a/src/config.rs b/src/config.rs index 61c30ba63..6a6b56537 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::net::Ipv4Addr; use crate::cpu::parts::{self, Part}; +use crate::dev::kona::KonaProduct; /// Every memory bank size in MB, for the front-ends' menus; `bank_sizes` says /// which of them a machine takes. @@ -943,8 +944,21 @@ pub enum GraphicsBoard { /// takes the Octane2's Crossbow and Bridge revisions. #[clap(name = "v12", alias = "vpro:v12", alias = "vpro_v12")] V12, + /// InfiniteReality2 with one raster manager (`src/dev/kona`). Onyx2 only. + #[clap(name = "ir2", alias = "ir2:1rm", alias = "ir2_1rm", alias = "infinitereality2")] + Ir2, + /// InfiniteReality2 with two raster managers. Onyx2 only. + #[clap(name = "ir2:2rm", alias = "ir2_2rm", alias = "ir2x2")] + Ir2x2, + /// InfiniteReality2E (GE16-4, RM9, 64 MB texture) with one raster manager. + /// Onyx2 only. + #[clap(name = "ir2e")] + Ir2e, + /// InfiniteReality2E with two raster managers. Onyx2 only. + #[clap(name = "ir2e:2rm")] + Ir2ex2, /// No graphics board: the machine runs on its serial console. Octane - /// and Fuel. + /// Fuel and the IP27 machines. #[clap(name = "none")] None, } @@ -971,6 +985,10 @@ impl GraphicsBoard { Self::V8 => "VPro V8", Self::V10 => "VPro V10", Self::V12 => "VPro V12", + Self::Ir2 => "InfiniteReality2 (1 RM)", + Self::Ir2x2 => "InfiniteReality2 (2 RM)", + Self::Ir2e => "InfiniteReality2E (1 RM)", + Self::Ir2ex2 => "InfiniteReality2E (2 RM)", Self::None => "None", } } @@ -987,6 +1005,10 @@ impl GraphicsBoard { Self::V8 => "v8", Self::V10 => "v10", Self::V12 => "v12", + Self::Ir2 => "ir2", + Self::Ir2x2 => "ir2:2rm", + Self::Ir2e => "ir2e", + Self::Ir2ex2 => "ir2e:2rm", Self::None => "none", } } @@ -1000,17 +1022,40 @@ impl GraphicsBoard { matches!(self, Self::V6 | Self::V8 | Self::V10 | Self::V12) } + /// The InfiniteReality boards (`src/dev/kona`). + pub fn is_kona(self) -> bool { + self.kona().is_some() + } + + /// The product and raster manager count of an InfiniteReality board. + pub fn kona(self) -> Option<(KonaProduct, u32)> { + match self { + Self::Ir2 => Some((KonaProduct::Ir2, 1)), + Self::Ir2x2 => Some((KonaProduct::Ir2, 2)), + Self::Ir2e => Some((KonaProduct::Ir2e, 1)), + Self::Ir2ex2 => Some((KonaProduct::Ir2e, 2)), + _ => None, + } + } + + /// The raster managers an InfiniteReality board carries. + pub fn rm_count(self) -> Option { + self.kona().map(|(_, rms)| rms) + } + /// Whether `validate()` accepts this board on `profile`. The Octane /// models only IMPACT (High as the SI with texture option, Maximum as - /// the MXI) or no board. + /// the MXI), VPro or no board. The Onyx2 takes an InfiniteReality2 or + /// none. pub fn supports(self, profile: MachineProfile) -> bool { match profile { MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, + MachineProfile::Onyx2Ip27 => self.is_kona() || self == Self::None, p if p.is_ip27() => self == Self::None, MachineProfile::FuelIp34 => matches!(self, Self::V10 | Self::V12 | Self::None), MachineProfile::TezroIp53 => matches!(self, Self::None | Self::V12), - MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, - _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), + MachineProfile::Indigo2Ip22 => !self.is_vpro() && !self.is_kona() && self != Self::None, + _ => !self.is_vpro() && !self.is_kona() && !matches!(self, Self::Extreme | Self::None), } } @@ -1041,9 +1086,13 @@ impl std::str::FromStr for GraphicsBoard { "v8" | "vpro:v8" | "vpro_v8" | "vpro-v8" => Ok(Self::V8), "v10" | "vpro:v10" | "vpro_v10" | "vpro-v10" => Ok(Self::V10), "v12" | "vpro:v12" | "vpro_v12" | "vpro-v12" => Ok(Self::V12), + "ir2" | "ir2:1rm" | "ir2_1rm" | "ir2-1rm" | "infinitereality2" => Ok(Self::Ir2), + "ir2:2rm" | "ir2_2rm" | "ir2-2rm" | "ir2x2" => Ok(Self::Ir2x2), + "ir2e" => Ok(Self::Ir2e), + "ir2e:2rm" => Ok(Self::Ir2ex2), "none" => Ok(Self::None), _ => Err(format!( - "invalid graphics board \"{s}\" (valid: newport, xz, extreme, solidimpact, highimpact, maximpact, impact:solid, impact:high, impact:max, v6, v8, v10, v12, vpro:v6, vpro:v8, vpro:v10, vpro:v12, none)" + "invalid graphics board \"{s}\" (valid: newport, xz, extreme, solidimpact, highimpact, maximpact, impact:solid, impact:high, impact:max, v6, v8, v10, v12, vpro:v6, vpro:v8, vpro:v10, vpro:v12, ir2, ir2:2rm, ir2e, ir2e:2rm, none)" )), } } @@ -1110,6 +1159,11 @@ pub struct GraphicsSection { /// `dx12` or `gl`. #[serde(default = "default_gpu_backend")] pub gpu_backend: String, + /// InfiniteReality2: the file that keeps the pipe's flash EEPROM between + /// runs, exactly the flash's size; created blank when absent. Unset + /// means volatile. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flash: Option, } /// What draws the host window (`renderer`, `--renderer`). @@ -1154,6 +1208,7 @@ impl Default for GraphicsSection { tile: default_graphics_tile(), raster: RasterUnit::Cpu, gpu_backend: default_gpu_backend(), + flash: None, } } } @@ -2911,6 +2966,8 @@ impl MachineConfig { if !board.supports(self.machine.profile) { return Err(if self.machine.profile == MachineProfile::OctaneIp30 { format!("graphics.board \"{name}\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, v6, v8, v10, v12, none)") + } else if self.machine.profile == MachineProfile::Onyx2Ip27 { + format!("graphics.board \"{name}\" is not modelled on the Onyx2 (valid: ir2, ir2:2rm, ir2e, ir2e:2rm, none)") } else if let Some(product) = self.machine.profile.ip27_name() { format!("graphics.board \"{name}\" is not modelled on the {product} (valid: none)") } else if self.machine.profile == MachineProfile::FuelIp34 { diff --git a/src/dev/gl/mod.rs b/src/dev/gl/mod.rs index 7852e64ba..1015aec58 100644 --- a/src/dev/gl/mod.rs +++ b/src/dev/gl/mod.rs @@ -8,10 +8,12 @@ //! - `vertex`: the transformed vertex, view-volume and user clip planes, //! polygon clipping, the viewport mapping. //! - `light`: the OpenGL 1.1 lighting equation and fog. +//! - `texgen`: texture-coordinate generation. //! //! Board specifics stay with the boards: how state arrives (FIFO token //! encodings), the raster engine's input formats, window clipping. pub mod light; pub mod math; +pub mod texgen; pub mod vertex; diff --git a/src/dev/gl/texgen.rs b/src/dev/gl/texgen.rs new file mode 100644 index 000000000..5b83ae341 --- /dev/null +++ b/src/dev/gl/texgen.rs @@ -0,0 +1,140 @@ +//! Texture-coordinate generation: the equations of OpenGL 1.2 section 2.10.4. +//! +//! Each of the four coordinates (s, t, r, q) can be replaced by a value computed from the +//! vertex: a plane's product with the object position (`OBJECT_LINEAR`), with the eye position +//! (`EYE_LINEAR`), or, for s and t, the sphere-map reflection of the view direction about the +//! eye-space normal. The texture matrix runs after, on the result. Single precision, like the +//! rest of the core. + +use super::light::norm; + +pub const EYE_LINEAR: u32 = 0x2400; +pub const OBJECT_LINEAR: u32 = 0x2401; +pub const SPHERE_MAP: u32 = 0x2402; + +/// The generation state of one texture unit. Plain data. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TexGen { + /// Bit i is set when coordinate i (s, t, r, q) is generated. + pub enabled: u32, + pub mode: [u32; 4], + /// Object planes, and eye planes as they are kept: already in eye space. + pub object: [[f32; 4]; 4], + pub eye: [[f32; 4]; 4], +} + +impl Default for TexGen { + /// OpenGL's start-up state: eye linear, s from x and t from y, nothing enabled. The start-up + /// mode, 0x2400 (eye linear), is established for the pipe; the planes' start-up values are + /// OpenGL's and no source gives the pipe's (inferred). + fn default() -> Self { + let mut planes = [[0.0; 4]; 4]; + planes[0][0] = 1.0; + planes[1][1] = 1.0; + TexGen { enabled: 0, mode: [EYE_LINEAR; 4], object: planes, eye: planes } + } +} + +fn dot4(p: &[f32; 4], v: [f32; 4]) -> f32 { + p[0] * v[0] + p[1] * v[1] + p[2] * v[2] + p[3] * v[3] +} + +impl TexGen { + /// Whether any coordinate is generated. + #[inline] + pub fn active(&self) -> bool { + self.enabled != 0 + } + + /// Whether a generated coordinate reads the normal. + #[inline] + pub fn needs_normal(&self) -> bool { + (0..2).any(|i| self.enabled >> i & 1 != 0 && self.mode[i] == SPHERE_MAP) + } + + /// The texture coordinate of a vertex at object position `obj` and eye position `eye`, with + /// eye-space normal `n` (read only for sphere map), given the current coordinate `st`: each + /// enabled coordinate is replaced by its generated value. + pub fn generate(&self, st: [f32; 4], obj: [f32; 4], eye: [f32; 4], n: [f32; 3]) -> [f32; 4] { + let mut out = st; + let mut r = None; + for (i, o) in out.iter_mut().enumerate() { + if self.enabled >> i & 1 == 0 { + continue; + } + *o = match self.mode[i] { + OBJECT_LINEAR => dot4(&self.object[i], obj), + EYE_LINEAR => dot4(&self.eye[i], eye), + // Sphere map: only s and t can take it, and one reflection serves both. + _ if i < 2 => { + let r = r.get_or_insert_with(|| reflect(eye, n)); + let m = 2.0 * (r[0] * r[0] + r[1] * r[1] + (r[2] + 1.0) * (r[2] + 1.0)).sqrt(); + // The reflection straight back at the viewer has no image: the middle. + if m > 0.0 { r[i] / m + 0.5 } else { 0.5 } + } + _ => *o, + }; + } + out + } +} + +/// u - 2 n' (n' . u), u the unit vector from the eye to the vertex, n' the unit normal. +fn reflect(eye: [f32; 4], n: [f32; 3]) -> [f32; 3] { + let p = if eye[3] != 0.0 { [eye[0] / eye[3], eye[1] / eye[3], eye[2] / eye[3]] } else { [eye[0], eye[1], eye[2]] }; + let u = norm(p); + let n = norm(n); + let d = 2.0 * (n[0] * u[0] + n[1] * u[1] + n[2] * u[2]); + [u[0] - n[0] * d, u[1] - n[1] * d, u[2] - n[2] * d] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn start_up_state_is_eye_linear_with_the_identity_planes_and_nothing_enabled() { + let g = TexGen::default(); + assert!(!g.active() && !g.needs_normal()); + assert_eq!(g.mode, [EYE_LINEAR; 4]); + assert_eq!(g.generate([5.0; 4], [1.0; 4], [2.0; 4], [0.0; 3]), [5.0; 4], "not enabled: the current coordinate"); + } + + #[test] + fn linear_modes_take_the_plane_product_with_the_object_or_the_eye_position() { + let mut g = TexGen { enabled: 0b1111, ..Default::default() }; + g.mode = [OBJECT_LINEAR, EYE_LINEAR, OBJECT_LINEAR, EYE_LINEAR]; + g.object[0] = [1.0, 2.0, 3.0, 4.0]; + g.eye[1] = [0.0, 1.0, 0.0, 0.5]; + g.object[2] = [0.0, 0.0, 1.0, 0.0]; + g.eye[3] = [1.0, 1.0, 1.0, 1.0]; + let out = g.generate([9.0; 4], [1.0, 1.0, 1.0, 1.0], [10.0, 20.0, 30.0, 1.0], [0.0; 3]); + assert_eq!(out, [10.0, 20.5, 1.0, 61.0]); + // Only the enabled ones are replaced. + g.enabled = 0b0010; + assert_eq!(g.generate([9.0; 4], [1.0; 4], [10.0, 20.0, 30.0, 1.0], [0.0; 3]), [9.0, 20.5, 9.0, 9.0]); + } + + #[test] + fn sphere_map_reflects_the_view_about_the_normal() { + let mut g = TexGen { enabled: 0b0011, mode: [SPHERE_MAP; 4], ..Default::default() }; + assert!(g.needs_normal()); + // Looking down -z at a surface facing +z: the reflection goes straight back, the centre. + let st = g.generate([0.0; 4], [0.0; 4], [0.0, 0.0, -5.0, 1.0], [0.0, 0.0, 1.0]); + assert_eq!(st[..2], [0.5, 0.5]); + // A normal turned 45 degrees toward +x reflects the view to +x: s above the centre, t on it. + let n = [0.5f32.sqrt(), 0.0, 0.5f32.sqrt()]; + let st = g.generate([0.0; 4], [0.0; 4], [0.0, 0.0, -5.0, 1.0], n); + let s = 0.5 + 1.0 / (2.0 * 2f32.sqrt()); + assert!((st[0] - s).abs() < 1e-6 && (st[1] - 0.5).abs() < 1e-6, "{st:?}"); + // A normal that need not be unit, and an eye position with w: same answer. + let st2 = g.generate([0.0; 4], [0.0; 4], [0.0, 0.0, -10.0, 2.0], [n[0] * 4.0, 0.0, n[2] * 4.0]); + assert!((st2[0] - st[0]).abs() < 1e-6 && (st2[1] - st[1]).abs() < 1e-6); + // R and q cannot be sphere mapped: they keep the current value. + g.enabled = 0b1111; + assert_eq!(g.generate([7.0, 7.0, 3.0, 4.0], [0.0; 4], [0.0, 0.0, -5.0, 1.0], [0.0, 0.0, 1.0])[2..], [3.0, 4.0]); + // The reflection at m = 0: the view meets the surface edge-on from behind. + let st = g.generate([0.0; 4], [0.0; 4], [0.0, 0.0, 5.0, 1.0], [0.0, 0.0, 1.0]); + assert_eq!(st[..2], [0.5, 0.5]); + } +} diff --git a/src/dev/kona/back/bench.rs b/src/dev/kona/back/bench.rs new file mode 100644 index 000000000..9ee812cbf --- /dev/null +++ b/src/dev/kona/back/bench.rs @@ -0,0 +1,260 @@ +//! A fill-rate benchmark for the integer drawing path. +//! +//! Each scene draws into a store of the managed area's size through the same +//! calls the back end makes for a textport or X command, with no guest and no +//! framing in the way, so the numbers are the fills'. Run it from a release +//! build, one test thread: +//! +//! cargo test --release --lib kona::back::bench -- --ignored --nocapture --test-threads=1 +//! +//! `BENCH_SECS` sets the time each scene takes (default half a second). The +//! figure is the best of five rounds, in megapixels of the requested region +//! per second. The pixel transfers (an 8-bit image write, a copy, a read) are +//! scenes of their own, in megapixels of the image. + +use super::raster::Surface; +use super::store::PixelStore; +use super::display::tables::Tables; +use super::display::{compose, compose_default, ColourMap}; +use super::store::DID_PLANE; +use super::x::state::XState; +use super::x::pixel::{Next, X_PIXEL_COPY, X_PIXEL_READ, X_PIXEL_SETUP, X_PIXEL_WRITE}; +use std::time::{Duration, Instant}; + +const W: u32 = 1280; +const H: u32 = 1024; +const GC_COPY: [u32; 13] = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + +fn pack(a: i32, b: i32) -> u32 { + (a as u32) << 16 | (b as u32 & 0xffff) +} + +fn secs() -> Duration { + Duration::from_secs_f64(std::env::var("BENCH_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(0.5)) +} + +/// Run `f` (one call draws `pixels` pixels) for `secs()`, five rounds; the best +/// rate in megapixels a second. +fn rate(pixels: u64, mut f: impl FnMut()) -> f64 { + let mut best: f64 = 0.0; + for _ in 0..5 { + let (start, mut calls) = (Instant::now(), 0u64); + while start.elapsed() < secs() / 5 { + f(); + calls += 1; + } + best = best.max((calls * pixels) as f64 / start.elapsed().as_secs_f64() / 1e6); + } + best +} + +/// Whether any pixel of the store was drawn: a scene that draws nothing +/// would report a fast rate for no work. +fn drawn(store: &PixelStore) -> bool { + (0..H).any(|y| (0..W).any(|x| store.read(x, y, 1) != 0)) +} + +fn x_scene(name: &str, setup: impl FnOnce(&mut XState), box_words: [u32; 2]) { + let store = PixelStore::new(W, H); + let surface = Surface::new(&store, 1); + let mut x = XState::default(); + x.command(0x129, &GC_COPY); + setup(&mut x); + let (w, h) = ((box_words[1] >> 16) as i64 - (box_words[0] >> 16) as i64, (box_words[1] as u16 as i64) - (box_words[0] as u16 as i64)); + // One draw first: an operation like xor undoes itself every second time, so + // the store is only checked after the first. + x.draw_command(&surface, 0x154, &box_words); + assert!(drawn(&store), "{name} drew nothing"); + let mpix = rate((w * h) as u64, || { + x.draw_command(&surface, 0x154, &box_words); + }); + println!("{name:<44} {mpix:>9.1} Mpix/s"); +} + +/// An 8-bit image `w` x `h` written at (0, 0) through the setup, the write and +/// its rows, as the back end takes them. +fn write_scene(name: &str, w: u32, h: u32) { + let store = PixelStore::new(W, H); + let surface = Surface::new(&store, 1); + let mut x = XState::default(); + x.command(0x129, &GC_COPY); + let rows: Vec = (0..w * h / 4).map(|i| i.wrapping_mul(0x0101_0101) ^ 0x0102_0304).collect(); + let mut once = || { + x.pixel_command(&surface, X_PIXEL_SETUP, &[0, 0, 1, 8, w, h, 0], true); + assert_eq!(x.pixel_command(&surface, X_PIXEL_WRITE, &[0, 0], true), Next::Words(w * h / 4)); + x.pixel_rows(&surface, &rows); + }; + once(); + assert!(drawn(&store), "{name} drew nothing"); + let mpix = rate((w * h) as u64, once); + println!("{name:<44} {mpix:>9.1} Mpix/s"); +} + +/// A copy of the screen's interior four pixels down and right. +fn copy_scene(name: &str, gx: u32) { + let store = PixelStore::new(W, H); + let surface = Surface::new(&store, 1); + let mut x = XState::default(); + let mut gc = GC_COPY; + gc[9] = gx * 2; + x.command(0x129, &gc); + for y in 0..H { + for c in 0..W { + store.write(c, y, 1, (c ^ y) << 8 & 0xff00); + } + } + let (w, h) = (W - 8, H - 8); + x.pixel_command(&surface, X_PIXEL_SETUP, &[1, 1, 1, 0, w, h, 0], true); + x.pixel_command(&surface, X_PIXEL_COPY, &[0, 0, 4, 4], true); + assert_eq!(x.pixel_counts().copies, 1, "{name} copied nothing"); + let mpix = rate((w * h) as u64, || { + x.pixel_command(&surface, X_PIXEL_COPY, &[0, 0, 4, 4], true); + }); + println!("{name:<44} {mpix:>9.1} Mpix/s"); +} + +/// A 64 x 64 read of 8-bit pixels (the high byte of each index). +fn read_scene(name: &str) { + let store = PixelStore::new(W, H); + let surface = Surface::new(&store, 1); + let mut x = XState::default(); + let mut gc = [0u32; 13]; + gc[6..].copy_from_slice(&[0x180c, 0x40001, 1, 6, 5, 1, 1]); + x.command(0x129, &gc); + store.write(100, 100, 1, 0x1200); + x.pixel_command(&surface, X_PIXEL_SETUP, &[2, 1, 0, 8, 64, 64, 0], true); + let mut once = || match x.pixel_command(&surface, X_PIXEL_READ, &[90, 90], true) { + Next::Return(d) => assert_eq!(d.len(), 64 * 64), + other => panic!("{other:?}"), + }; + once(); + let mpix = rate(64 * 64, once); + println!("{name:<44} {mpix:>9.1} Mpix/s"); +} + +/// The frame composed from the store, in megapixels of the frame: the default +/// (textport) composition, and with the X server's tables loaded and the +/// screen in runs of 16 pixels of four windows (8-bit, 12-bit, direct, cleared). +fn compose_scene(name: &str, tables: Option<&Tables>, run: u32) { + let store = PixelStore::new(W, H); + let mut map = ColourMap::default(); + for i in 0..256u32 { + map.load(&[i, i * 4, 1020 - i * 4, i * 2]); + } + // Windows in runs: `run` pixels each, or, for 0, runs of 150 to 700 pixels + // as a desktop's windows are. + let mut seed = 7u64; + let mut next = |lo: u32, hi: u32| { + seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + lo + (seed >> 33) as u32 % (hi - lo) + }; + for y in 0..H { + let (mut left, mut id) = (0u32, 1u32); + for c in 0..W { + if left == 0 { + left = if run == 0 { next(150, 700) } else { run }; + id = id % 4 + 1; + } + left -= 1; + store.write(c, y, 1, (c * 7 + y * 13) & 0xffff); + if tables.is_some() { + store.write(c, y, DID_PLANE, id << 2); + } + } + } + let mut out = vec![0u32; (W * H) as usize]; + let snap = tables.map(|t| t.snapshot()); + let mpix = rate((W * H) as u64, || match &snap { + Some(t) => compose(&store, 1, &map, t, &mut out, W as usize), + None => compose_default(&store, 1, &map, &mut out, W as usize), + }); + assert!(out.iter().any(|&p| p != out[0]), "{name} composed one colour"); + println!("{name:<44} {mpix:>9.1} Mpix/s"); +} + +/// What the display thread takes of the tables each frame, per second, for a +/// board with nothing loaded and for one with the tables loaded. +fn frame_tables_scene(name: &str, t: &Tables) { + let mut sink = 0usize; + let rate = rate(1, || sink += std::hint::black_box(t).snapshot().colour(0x2c01) as usize); + println!("{name:<44} {:>9.1} kframes/s", rate * 1000.0); + assert!(sink > 0 || t.snapshot().is_default()); +} + +fn tables_for_compose() -> Tables { + let mut t = Tables::default(); + t.load_colours(0x2c00, &(0..256u32).map(|i| i << 20 | (255 - i) << 10 | i).collect::>()); + t.load_colours(0x3800, &(0..4096u32).map(|i| (i & 0x3ff) << 20 | (i >> 2) << 10).collect::>()); + t.load_did(1, 0x2c00 << 7 | 7 << 3); + t.load_did(2, 0x3800 << 7 | 11 << 3); + t.load_mode(3, 4); + t.load_did(4, 0); + t.load_gamma(0, &(0..4096).collect::>()); + t +} + +#[test] +#[ignore] +fn fill_rate() { + let full = [pack(0, 0), pack(W as i32, H as i32)]; + x_scene("x box, solid, whole screen", |_| {}, full); + x_scene("x box, solid, 64 x 64", |_| {}, [pack(100, 100), pack(164, 164)]); + x_scene( + "x box, two clip rectangles", + |x| { + x.command(0x145, &[2, 0, 0, W as u32 / 2 - 8]); + x.command(0x12b, &[H, W as u32 / 2 + 8, 0, W, H]); + }, + full, + ); + x_scene( + "x box, 8 x 8 stipple, whole screen", + |x| { + x.command(0x13d, &[1, 8, 8]); + x.stipple.rows(&[0xaa00_0000, 0x5500_0000, 0xaa00_0000, 0x5500_0000, 0xf000_0000, 0x0f00_0000, 0xf000_0000, 0x0f00_0000]); + x.command(0x13c, &[1]); + }, + full, + ); + x_scene( + "x box, xor (reads the destination)", + |x| { + let mut gc = GC_COPY; + gc[9] = 12; // gx 6, xor + x.command(0x129, &gc); + }, + full, + ); + + let store = PixelStore::new(W, H); + let surface = Surface::new(&store, 1); + let mpix = rate((W * H) as u64, || surface.fill_box(0, 0, W as i64, H as i64, 7)); + assert!(drawn(&store), "the textport box drew nothing"); + println!("{:<44} {mpix:>9.1} Mpix/s", "textport box, whole screen"); + // 16 x 16 glyphs at scattered positions, half the bits set. + let bits: Vec = (0..8).map(|i| 0xa5a5_5a5a ^ (i * 0x0101_0101)).collect(); + let mut seed = 1u64; + let mut place = || { + seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + ((seed >> 33) as i64 % (W as i64 - 16), (seed >> 11) as i64 % (H as i64 - 16)) + }; + let mpix = rate(16 * 16 * 64, || { + for _ in 0..64 { + let (x, y) = place(); + surface.bitmap(x, y, 16, 16, &bits, 9); + } + }); + assert!(drawn(&store), "the glyphs drew nothing"); + println!("{:<44} {mpix:>9.1} Mpix/s", "textport bitmap, 16 x 16 glyphs"); + + write_scene("x image write, 8 bit, 64 x 64", 64, 64); + write_scene("x image write, 8 bit, whole screen", W, H); + copy_scene("x copy, whole screen less 8, copy", 3); + copy_scene("x copy, whole screen less 8, xor", 6); + read_scene("x read, 8 bit, 64 x 64"); + compose_scene("compose, default", None, 16); + compose_scene("compose, X windows, runs of 16", Some(&tables_for_compose()), 16); + compose_scene("compose, X windows, runs of 150 to 700", Some(&tables_for_compose()), 0); + frame_tables_scene("frame's tables, nothing loaded", &Tables::default()); + frame_tables_scene("frame's tables, loaded", &tables_for_compose()); +} diff --git a/src/dev/kona/back/census.rs b/src/dev/kona/back/census.rs new file mode 100644 index 000000000..46ce9d256 --- /dev/null +++ b/src/dev/kona/back/census.rs @@ -0,0 +1,340 @@ +//! A census of the tokens a command stream carries. +//! +//! The back end's census tap (a test-only field of `KonaBackend`) tallies every job: commands by +//! grammar and selector, data runs, bitmaps, video commands, retrace tags, +//! context changes, indirect stores and DMA packets. Replaying a recording +//! through it says which token families the stream really uses, so a step +//! reads only those from its token source. +//! +//! The front end frames only the commands in its selector tables and drops +//! other words (`Job::Dropped`), so a token the tables lack is not a command. +//! The census counts those words and the command each followed. +//! +//! The selector names and the GL families come from the definitions table (`defs`). + +use super::super::defs::*; +use super::super::job::{ContextEvent, Grammar, Job}; +use parking_lot::Mutex; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; +use std::sync::Arc; + +/// The GL selectors the ported decoder table has an entry for. 33 selectors; the +/// decoder is a subset of the grammar. +pub(super) const DECODER_SELECTORS: [u32; 33] = [ + 0x72, 0x73, 0x74, 0x75, 0x34, 0x35, 0x36, 0x17b, 0x17c, 0x179, 0x17d, 0x0, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x38, 0x39, + 0x71, 0x8c, 0x90, 0xc6, 0xc7, 0x110, 0x115, 0x116, 0x119, 0x1ca, 0x1cc, 0x1d6, 0x500, +]; + +/// The entries of that table whose argument count the decoder does not +/// settle, with the call it labels them: what a GL stream cannot be framed +/// from the ported table alone. +pub(super) const UNRESOLVED_LAYOUTS: [(u32, &str); 5] = + [(0x0, "glBegin"), (0x1f, "glClearDepth"), (0x119, "context resolve"), (0x1d6, "context resolve"), (0x500, "glBegin")]; + +/// A grammar as a number that orders. +fn g(grammar: Grammar) -> u8 { + match grammar { + Grammar::Textport => 0, + Grammar::Kernel => 1, + Grammar::Gl => 2, + } +} + +const GRAMMARS: [&str; 3] = ["textport", "kernel", "GL"]; + +#[derive(Clone, Default, Debug)] +pub struct Census { + pub jobs: u64, + /// (grammar, selector) to (commands, of them already handled by the front + /// end, with a packed field). + pub commands: BTreeMap<(u8, u32), (u64, u64, u64)>, + /// (grammar, selector) to (runs, words). + pub data: BTreeMap<(u8, u32), (u64, u64)>, + pub bitmaps: (u64, u64), + pub video: BTreeMap, + pub retraces: Vec, + pub activated: (u64, u64), + pub deactivated: u64, + pub released: u64, + pub slots: BTreeSet, + pub indirect: BTreeMap, + /// DMA packets: count, physical ones, rows, bytes. + pub dma_in: (u64, u64, u64, u64), + pub firmware_starts: u64, + /// The last framed command, which dropped words are put down to. + last: Option<(u8, u32)>, + /// Dropped words by the command before them: how many, and how many + /// distinct values (up to `DISTINCT_CAP`). + pub dropped_after: BTreeMap, (u64, BTreeSet)>, + pub dropped: u64, + /// Dropped words that equal a selector the ported decoder leaves unresolved. + pub dropped_unresolved: BTreeMap, + /// The record index of each (grammar, selector)'s first command. + pub first_record: BTreeMap<(u8, u32), usize>, + /// Consecutive dropped words so far, and the record where a run first + /// reached `LONG_DROP`: a run of data the front end could not size. + drop_run: u64, + pub first_long_drop: Option, + /// The census as it stood when the 64th dropped word in a row came: past it the + /// front end can misframe, so only this part is a count of what the + /// stream sends. + pub before_long_drop: Option>, +} + +/// Dropped words in a row that make a data run, not a stray token. +pub(super) const LONG_DROP: u64 = 64; + +/// The most distinct values kept per preceding command. +const DISTINCT_CAP: usize = 4096; + +impl Census { + pub(super) fn tally(&mut self, job: &Job<'_>, at: usize) { + self.jobs += 1; + if let Job::Dropped { .. } = job { + self.drop_run += 1; + if self.drop_run == LONG_DROP && self.first_long_drop.is_none() { + self.first_long_drop = Some(at); + self.before_long_drop = Some(Box::new(self.clone())); + } + } else { + self.drop_run = 0; + } + match job { + Job::Command { grammar, selector, args: _, packed, handled } => { + let e = self.commands.entry((g(*grammar), *selector)).or_default(); + e.0 += 1; + e.1 += *handled as u64; + e.2 += (*packed != 0) as u64; + self.last = Some((g(*grammar), *selector)); + self.first_record.entry((g(*grammar), *selector)).or_insert(at); + } + Job::Dropped { word } => { + self.dropped += 1; + let e = self.dropped_after.entry(self.last).or_default(); + e.0 += 1; + if e.1.len() < DISTINCT_CAP { + e.1.insert(*word); + } + if UNRESOLVED_LAYOUTS.iter().any(|(s, _)| s == word) { + *self.dropped_unresolved.entry(*word).or_default() += 1; + } + } + Job::Data { grammar, selector, words, .. } => { + let e = self.data.entry((g(*grammar), *selector)).or_default(); + e.0 += 1; + e.1 += words.len() as u64; + } + Job::Bitmap { payload, .. } => { + self.bitmaps.0 += 1; + self.bitmaps.1 += payload.len() as u64; + } + Job::Video { command, .. } => *self.video.entry(*command).or_default() += 1, + Job::Retrace { tag } => self.retraces.push(*tag), + Job::Context(e) => match e { + ContextEvent::Activated { slot, fresh } => { + self.slots.insert(*slot); + if *fresh { + self.activated.0 += 1; + } else { + self.activated.1 += 1; + } + } + ContextEvent::Deactivated { .. } => self.deactivated += 1, + ContextEvent::WindowReleased { .. } => self.released += 1, + }, + Job::Indirect { selector, .. } => *self.indirect.entry(*selector).or_default() += 1, + Job::DmaIn { physical, lines, per_line, .. } => { + self.dma_in.0 += 1; + self.dma_in.1 += *physical as u64; + self.dma_in.2 += *lines as u64; + self.dma_in.3 += *lines as u64 * *per_line as u64; + } + Job::FirmwareStart { .. } => self.firmware_starts += 1, + } + } + + /// GL commands by family: command count and distinct selectors, and the + /// selectors each family used. The family comes from the front end's table + /// names, so a selector it has no entry for cannot be told and shows + /// under "dropped words". + pub fn render_families(&self) -> String { + let mut fam: BTreeMap<&str, (u64, Vec)> = BTreeMap::new(); + for ((gr, sel), (n, ..)) in &self.commands { + if *gr != 2 { + continue; + } + let name = token_name(*sel); + let e = fam.entry(token_family(*sel)).or_default(); + e.0 += n; + e.1.push(format!("{name} {n}")); + } + let mut o = String::from("GL families:\n"); + for f in ["vertex", "array", "matrix", "texture", "query", "state and other"] { + let (n, list) = fam.get(f).cloned().unwrap_or_default(); + let _ = writeln!(o, " {f:<16} {n:>9} commands, {} selectors{}", list.len(), if list.is_empty() { String::new() } else { format!(": {}", list.join(", ")) }); + } + o + } + + /// The GL selectors seen, as framed commands. + fn gl_selectors(&self) -> BTreeSet { + self.commands.keys().filter(|k| k.0 == 2).map(|k| k.1).collect() + } + + /// The census as text. + pub fn render(&self) -> String { + let name = |gr: u8, sel: u32| if gr == 0 { "" } else { token_name(sel) }; + let mut o = String::new(); + let _ = writeln!(o, "jobs {}", self.jobs); + for (gr, label) in GRAMMARS.iter().enumerate() { + let rows: Vec<_> = self.commands.iter().filter(|(k, _)| k.0 == gr as u8).collect(); + let total: u64 = rows.iter().map(|(_, v)| v.0).sum(); + let _ = writeln!(o, "\n{label} commands: {} selectors, {total} commands", rows.len()); + for ((_, sel), (n, handled, packed)) in rows { + let first = self.first_record.get(&(gr as u8, *sel)).copied().unwrap_or(0); + let _ = writeln!(o, " {sel:#06x} {:<24} {n:>9} handled {handled} packed {packed} first at record {first}", name(gr as u8, *sel)); + } + } + let _ = writeln!(o, "\ndata runs:"); + for ((gr, sel), (runs, words)) in &self.data { + let _ = writeln!(o, " {} {sel:#06x} {:<24} {runs:>8} runs {words:>10} words", GRAMMARS[*gr as usize], name(*gr, *sel)); + } + let _ = writeln!(o, "bitmaps {} ({} payload words)", self.bitmaps.0, self.bitmaps.1); + let _ = writeln!(o, "video commands:"); + for (c, n) in &self.video { + let _ = writeln!(o, " {c:#04x} {n}"); + } + let _ = writeln!(o, "retrace requests {}", self.retraces.len()); + match self.first_long_drop { + Some(at) => { + let _ = writeln!(o, "the {LONG_DROP}th dropped word in a row came at record {at}"); + } + None => { + let _ = writeln!(o, "no run of {LONG_DROP} dropped words in a row"); + } + } + let _ = writeln!( + o, + "contexts: activated fresh {} restored {}, deactivated {}, window released {}, slots used {}", + self.activated.0, + self.activated.1, + self.deactivated, + self.released, + self.slots.len() + ); + let _ = writeln!(o, "indirect stores:"); + for (s, n) in &self.indirect { + let _ = writeln!(o, " {s:#06x} {n}"); + } + let _ = writeln!(o, "DMA packets {} ({} physical, {} rows, {} bytes)", self.dma_in.0, self.dma_in.1, self.dma_in.2, self.dma_in.3); + let _ = writeln!(o, "firmware starts {}", self.firmware_starts); + + let _ = writeln!(o, "\ndropped words (no framer had a command for them): {}", self.dropped); + let mut after: Vec<_> = self.dropped_after.iter().collect(); + after.sort_by_key(|(_, (n, _))| std::cmp::Reverse(*n)); + for (key, (n, values)) in after.into_iter().take(24) { + let who = key.map_or("(before any command)".to_string(), |(gr, sel)| format!("{} {sel:#06x} {}", GRAMMARS[gr as usize], name(gr, sel))); + let _ = writeln!(o, " after {who:<40} {n:>8} words, {} distinct values", values.len()); + } + + let gl = self.gl_selectors(); + let _ = writeln!(o, "\nGL selectors framed and seen: {}", gl.len()); + let (mut total, mut outside_cmds, mut unresolved_cmds) = (0u64, 0u64, 0u64); + for ((gr, sel), (n, ..)) in &self.commands { + if *gr != 2 { + continue; + } + total += n; + outside_cmds += n * (!DECODER_SELECTORS.contains(sel)) as u64; + unresolved_cmds += n * UNRESOLVED_LAYOUTS.iter().any(|(u, _)| u == sel) as u64; + } + let _ = writeln!( + o, + "GL commands framed: {total}; with no entry in the ported decoder table: {outside_cmds}; in its unresolved set: {unresolved_cmds}" + ); + let outside: Vec<_> = gl.iter().filter(|s| !DECODER_SELECTORS.contains(s)).map(|s| format!("{s:#x} {}", name(2, *s))).collect(); + let _ = writeln!(o, " of them with no entry in the ported decoder table: {}: {}", outside.len(), outside.join(", ")); + let _ = writeln!(o, "\nlayouts the ported decoder marks unresolved, against this stream:"); + for (sel, call) in UNRESOLVED_LAYOUTS { + let framed = self.commands.get(&(2, sel)).map_or(0, |v| v.0); + let dropped = self.dropped_unresolved.get(&sel).copied().unwrap_or(0); + let _ = writeln!(o, " {sel:#06x} {call:<16} framed {framed:>7} dropped words with that value {dropped:>7}"); + } + o + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A stream through the tap: commands by selector with the record they + /// first came at, dropped words put down to the command before them, and a + /// long run of them noted. + #[test] + fn the_tap_counts_commands_and_attributes_dropped_words() { + use super::super::super::tests::{test_cfg, TestHost}; + use super::super::super::Kona; + let at = Arc::new(AtomicUsize::new(0)); + let census = Arc::new(Mutex::new(Census::default())); + let host = TestHost::new(); + let kona = Kona::new(test_cfg(), host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + kona.with_backend(|b| { + b.tap = Some((census.clone(), at.clone())); + b.hook = Some(super::super::null_hook()); + }); + let store = |w: u32| assert_eq!(kona.test_write(0xc10000, 4, w as u64), crate::traits::BUS_OK); + // A kernel x_position (0x12a, four arguments), then data nothing frames: + // a textport sync word, then words no table has. + at.store(7, Relaxed); + for w in [0x12a, 0, 0, 0x500, 0x400] { + store(w); + } + at.store(9, Relaxed); + for w in [1, 0x7000_0001, 0x7000_0002] { + store(w); + } + at.store(10, Relaxed); + for i in 0..LONG_DROP as u32 { + store(0x7100_0000 + i); + } + let c = census.lock(); + assert_eq!(c.commands.get(&(1, 0x12a)).map(|v| v.0), Some(1)); + assert_eq!(c.first_record.get(&(1, 0x12a)), Some(&7)); + assert!(c.dropped >= LONG_DROP, "{}", c.dropped); + assert_eq!(c.dropped_after.keys().collect::>(), [&Some((1, 0x12a))]); + assert_eq!(c.first_long_drop, Some(10 + 0), "{:?}", c.first_long_drop); + let before = c.before_long_drop.as_ref().expect("a snapshot at the long run"); + assert!(before.dropped < c.dropped); + let text = c.render(); + assert!(text.contains(&format!("after kernel 0x012a {}", token_name(0x12a))), "{text}"); + assert!(text.contains("glBegin"), "{text}"); + } + + /// Every token has a name and the family the table's section gives it. + #[test] + fn gl_commands_fall_into_families_by_the_tables_sections() { + for (token, fam) in [ + (SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, "vertex"), + (BEGIN_PRIMITIVE_TRIANGLE_STRIP, "vertex"), + (END_OPEN_PRIMITIVE, "vertex"), + (SET_VIEWPORT, "matrix"), + (SELECT_CURRENT_MATRIX_STACK, "matrix"), + (LOAD_WINDOW_SYSTEM_STIPPLE, "state and other"), + (SET_ALPHA_TEST, "state and other"), + ] { + assert_eq!(token_family(token), fam, "{}", token_name(token)); + } + assert!((0..512).all(|t| !token_name(t).is_empty())); + assert_eq!(token_name(512), ""); + } + + #[test] + fn the_unresolved_layouts_are_in_the_decoder_table() { + for (sel, _) in UNRESOLVED_LAYOUTS { + assert!(DECODER_SELECTORS.contains(&sel)); + } + } +} diff --git a/src/dev/kona/back/context.rs b/src/dev/kona/back/context.rs new file mode 100644 index 000000000..cea12aec5 --- /dev/null +++ b/src/dev/kona/back/context.rs @@ -0,0 +1,197 @@ +//! The per-context state: the X path's and GL's, one copy per rendering context. +//! +//! The front end sends a `Context` job when a context becomes current, is +//! switched out or loses its window, and names the table entry current with +//! every job. Each entry keeps its own X state (graphics context, position, +//! clip, scissor, stipple cache and pixel transfer set-up), so one client's +//! loads do not reach another's draws. Commands that arrive with no context +//! current, as the kernel's and the PROM's do, use a state of their own. +//! +//! A switch-out keeps the entry's state, as the front end's table holds it +//! already; nothing is copied. A context that starts afresh begins from the +//! power-on state; one the pipe has seen keeps what it had. Saving the state +//! into the context image the front end owns, and restoring it from there, is +//! not done: a guest cannot read the back end's state through the image +//! (established), so nothing observes the difference. + +use super::gl::state::Gl; +use super::gl::texture::TexContext; +use super::x::state::XState; +use crate::dev::kona::defs::CONTEXT_SLOTS; +use crate::dev::kona::job::ContextEvent; + + +/// What one rendering context holds. +#[derive(Default)] +pub struct Context { + pub x: XState, + pub gl: Box, + pub tex: TexContext, +} + +impl Context { + /// The power-on state of the context in slot `slot` (`CONTEXT_SLOTS` for the no-context state). + pub fn new(slot: usize) -> Self { + Context { tex: TexContext::new(slot), ..Default::default() } + } + + /// Whether the context is part way through something a snapshot does not carry. + pub fn busy(&self) -> bool { + self.x.busy() || self.gl.busy() + } + + /// The context's state, as the stream module reads and writes it. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { x: _, gl: _, tex: _ } = self; + self.x.visit(s); + self.gl.visit(s); + self.tex.visit(s); + } +} + +pub struct Contexts { + none: Context, + slots: Vec>>, + /// The entry the last job named, or None for no context current. + current: Option, +} + +impl Default for Contexts { + fn default() -> Self { + Contexts { none: Context::new(CONTEXT_SLOTS), slots: Vec::new(), current: None } + } +} + +impl Contexts { + /// The state a job runs under: entry `slot`, or the no-context state. + /// An entry past the table the front end keeps uses the no-context state. + pub fn select(&mut self, slot: Option) -> &mut Context { + self.current = slot.filter(|&s| s < CONTEXT_SLOTS); + match self.current { + None => &mut self.none, + Some(s) => { + if self.slots.len() <= s { + self.slots.resize_with(s + 1, || None); + } + self.slots[s].get_or_insert_with(|| Box::new(Context::new(s))) + } + } + } + + /// The state the last job ran under. + pub fn current(&self) -> &Context { + self.current.and_then(|s| self.slots.get(s)).and_then(|s| s.as_deref()).unwrap_or(&self.none) + } + + /// A context event: a fresh activation starts from power-on; a window + /// released ends the transfer open on it. + pub fn event(&mut self, e: ContextEvent) { + match e { + ContextEvent::Activated { slot, fresh: true } if slot < CONTEXT_SLOTS => { + *self.select(Some(slot)) = Context::new(slot); + } + ContextEvent::WindowReleased { slot } if slot < CONTEXT_SLOTS => self.select(Some(slot)).x.pixel_release(), + _ => {} + } + } + + /// A swap of display id `id` showing buffer `bank`: every context whose window has that id. + pub fn swapped(&mut self, id: u32, bank: u8) { + for c in self.slots.iter_mut().flatten() { + c.gl.swapped(id, bank); + } + self.none.gl.swapped(id, bank); + } + + /// Entries that were loaded with something; one that only a job selected + /// or a window release touched does not count. + pub fn held(&self) -> usize { + self.slots.iter().flatten().filter(|c| c.x.loads != 0).count() + } + + /// Whether any context is part way through something a snapshot does not carry. + pub fn busy(&self) -> bool { + self.none.busy() || self.slots.iter().flatten().any(|c| c.busy()) + } + + /// Every context, as the stream module reads and writes them: the no-context state, + /// then each table entry that holds one. The entry the last job named is not saved + /// (the next job names it again). + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { none: _, slots: _, current: _ } = self; + self.none.visit(s); + let n = s.len(self.slots.len()); + if s.loading() { + self.slots.clear(); + self.slots.resize_with(n.min(CONTEXT_SLOTS), || None); + } + for (at, slot) in self.slots.iter_mut().enumerate() { + let mut some = slot.is_some(); + s.bool(&mut some); + if s.loading() { + *slot = some.then(|| Box::new(Context::new(at))); + } + if let Some(c) = slot { + c.visit(s); + } + } + if s.loading() { + self.current = None; + } + } + + /// Power-on state for every entry. + pub fn reset(&mut self) { + *self = Contexts::default(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::defs::*; + + const GC: [u32; 13] = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + + /// Each entry keeps the context it was loaded with; the no-context state + /// is another. + #[test] + fn each_context_keeps_its_own_loads() { + let mut c = Contexts::default(); + c.select(Some(0)).x.command(WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &GC); + c.select(Some(1)).x.command(WINDOW_SYSTEM_DRAWABLE_ORIGIN_AND_SIZE_LOAD, &[5, 6, 7, 8]); + assert!(c.select(Some(0)).x.gc().is_ok()); + assert_eq!(c.select(Some(0)).x.origin(), (0, 0)); + assert_eq!(c.select(Some(1)).x.origin(), (5, 6)); + assert!(c.select(Some(1)).x.gc().is_err()); + assert!(c.select(None).x.gc().is_err()); + assert_eq!(c.held(), 2); + c.select(Some(5)); + c.event(ContextEvent::WindowReleased { slot: 6 }); + assert_eq!(c.held(), 2, "an entry only selected, or released, holds nothing"); + } + + /// A switch-out keeps the state, a restore finds it, a fresh activation + /// starts over. + #[test] + fn a_fresh_activation_starts_over_and_a_switch_keeps_the_state() { + let mut c = Contexts::default(); + c.select(Some(2)).x.command(WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &GC); + c.event(ContextEvent::Deactivated { slot: 2 }); + c.event(ContextEvent::Activated { slot: 2, fresh: false }); + assert!(c.select(Some(2)).x.gc().is_ok()); + c.event(ContextEvent::Activated { slot: 2, fresh: true }); + assert!(c.select(Some(2)).x.gc().is_err()); + } + + /// An entry past the front end's table cannot grow the vector. + #[test] + fn a_slot_past_the_table_uses_the_no_context_state() { + let mut c = Contexts::default(); + c.select(Some(1 << 20)).x.command(WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &GC); + assert_eq!(c.held(), 0); + assert!(c.select(None).x.gc().is_ok()); + } +} diff --git a/src/dev/kona/back/display/channels.rs b/src/dev/kona/back/display/channels.rs new file mode 100644 index 000000000..adde5d3d0 --- /dev/null +++ b/src/dev/kona/back/display/channels.rs @@ -0,0 +1,800 @@ +//! The display generator's channels: the state the video commands set. +//! +//! The X server describes the screen to the display generator with video commands: it loads a +//! video format into each channel, says which rectangle of the managed area the channel shows, +//! and enables or blanks it. This module keeps what those setters leave behind, per physical +//! channel, so the Gets can answer from it (`reply`) and a combination can run through it +//! (`start`). The layouts are those of the server's combination load and its channel requests; +//! word 0 of every channel setter is the physical channel, the board mask's bit number. +//! +//! - DisableChannel [channel]: the channel stops and holds no format until the next LoadFormat. +//! - LoadFormat [channel, 1, ...]: words 2 to 18 are the format file's name and its datestamp, +//! kept for GetChannelConfig; words 23 and 24 are the active lines and pixels, the output +//! size. The channel runs. +//! - EnableChannel [channel, on, own timing]: word 1 nonzero runs a channel that holds a format. +//! - Resize [channel, x, top row from the bottom, width, height, 1, flicker, 0]: the input +//! rectangle, and GetResize's pan rectangle when word 7 is 0. +//! - PlacementRes [channel, x code, y code, w3, 1, largest width, height, smallest width, +//! height]: GetResize words 8 to 15. +//! - ChannelBlanking [channel, blank]; ChannelGammaMap [channel, map]; CursorPriority +//! [channel, priority]; Vampire [channel, source, ...]. +//! +//! Inferred: the combination load sends DisableChannel for every channel, then each enabled +//! channel's LoadFormat, Resize and blanking, then two EnableChannel passes, [channel, on, 0] +//! and [channel, 0, own timing]. Word 1 cannot be the channel's state, or the second pass would +//! turn every channel off. So a channel runs from its LoadFormat, and EnableChannel's word 1 is +//! only a request to run a channel that has had a LoadFormat since its last DisableChannel. +//! +//! The Gets answer from this state. A reply is the reply id, the data words, then the id plus the +//! data words, written from byte 8 of the return page ahead of the ready word; the reply ids and +//! sizes are the server's tables (`tables::VIDEO_REPLY`), and the words not set by `reply_words` +//! are zero: +//! +//! - GetBoardConfig: version 1, the board mask (bit n: physical channel n exists). +//! - GetChannelConfig: word 0 bit 0x1 present, 0x2 the encoder (channel 0), 0xc enabled and +//! active; words 1 to 17 the format's name and datestamp, as LoadFormat gave them; word 18 the +//! cursor priority. +//! - GetResize: the input rectangle, the pan rectangle, then PlacementRes's words. +//! - GetVampire: word 0 the channel whose timing this one takes. +//! - GetChannelBlanking: word 0 whether the channel is blanked. GetChannelEnables: word 1 whether +//! it is enabled. +//! +//! Approximated: the layouts are the server's readers'; no reply was measured from a pipe. The +//! version, the pan rectangle at power-up and every word no reader takes are the model's choices. +//! A reply goes out as the Get's checksum arrives, where a pipe answers after queued work. +//! +//! Starting the firmware (`start`) is what the display generator's processor does each time the +//! loader releases it: rebuild the default state, blank every channel and run the video format +//! combination stored in the flash, or, when that image is rejected or a command in it raises the +//! error flag, rebuild again and run the built-in fallback combination. The stored words are +//! video commands framed as on the command page: the command, its data words and their checksum; +//! the image is accepted only whole, every command known with the table's data-word count and +//! every checksum matching. The run records the size SetupFrameBuffer gave (`frame`), but the +//! board keeps the managed size it was configured with and reports that one to the X server; a +//! combination that names another is logged and the other size is ignored. +//! +//! Approximated: the default-state rebuild is the model's reset channel state with every channel +//! disabled and blanked and its format name cleared; a command the table gives no length for +//! rejects the image; a Get in the image runs for no effect; and a system reset runs nothing, +//! where a pipe's power-up also runs the stored image before the PROM. +//! +//! Approximated: a channel that takes another's timing (Vampire), one bound to an X window, the +//! timing words of LoadFormat are kept or logged and change nothing on the screen; the board shows +//! one channel's worth of pixels at the managed size. Blanking (ChannelBlanking and +//! TempBlankAllChannels) blacks the frame when it blanks channel 0 or every channel. +//! +//! Ported from hw/misc/sgi-kona.c in jamesbraid/qemu-sgi-origami. + +use crate::dev::kona::defs::*; +use crate::dev::kona::stream::Stream; +use crate::devlog::LogModule; + +/// The channels a display generator drives (the board reports eight). +pub const CHANNELS: usize = crate::dev::kona::ident::CHANNELS as usize; + +/// The words a channel's format is named by: sixteen of the file's name and its datestamp. +pub const NAME_WORDS: usize = 17; + +/// The managed area the display generator starts with. +pub const FB: (u32, u32) = (1280, 1024); + +/// The largest width or height a command may name. +const MAX_DIM: u32 = 16384; + +/// The raster managers' pixel memory, and the width the budget rounds up to: a combination fits +/// when managers x 20,971,520 / (height x width to a multiple of 160) is at least 8 for Small +/// pixels and 16 for Medium. +const RM_PIXEL_BYTES: u64 = 20_971_520; +const TILE_WIDTH: u64 = 160; + +/// One channel's state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Channel { + pub enabled: bool, + /// A LoadFormat since its DisableChannel. + pub loaded: bool, + /// The format's active size: pixels, then lines. + pub format: (u32, u32), + /// The input rectangle: x, the top row counted from the bottom of the managed area, width, + /// height. + pub input: [u32; 4], + pub blanked: bool, + pub gamma_map: u32, + pub cursor_priority: u32, + /// The channel whose timing this one takes. + pub source: u32, + pub name: [u32; NAME_WORDS], + /// GetResize words 4 to 7. + pub pan: [u32; 4], + /// GetResize words 8 to 15: PlacementRes's words 1 to 8. + pub placement: [u32; 8], +} + +impl Channel { + /// A channel as the display generator starts it: off, taking its own timing, panning over + /// the whole managed area. + fn off(index: usize) -> Channel { + Channel { + enabled: false, + loaded: false, + format: (0, 0), + input: [0; 4], + blanked: false, + gamma_map: 0, + cursor_priority: 0, + source: index as u32, + name: [0; NAME_WORDS], + pan: [0, 0, FB.0, FB.1], + placement: [1, 1, 1, 1, FB.0, FB.1, 0, 0], + } + } + + fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { enabled: _, loaded: _, format: _, input: _, blanked: _, gamma_map: _, cursor_priority: _, source: _, name: _, pan: _, placement: _ } = self; + s.bool(&mut self.enabled); + s.bool(&mut self.loaded); + s.u32(&mut self.format.0); + s.u32(&mut self.format.1); + s.u32s(&mut self.input); + s.bool(&mut self.blanked); + s.u32(&mut self.gamma_map); + s.u32(&mut self.cursor_priority); + s.u32(&mut self.source); + s.u32s(&mut self.name); + s.u32s(&mut self.pan); + s.u32s(&mut self.placement); + } +} + +/// The display generator's video state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Channels { + pub channel: [Channel; CHANNELS], + /// TempBlankAllChannels' last word. + pub blanked: bool, + /// The managed area SetupFrameBuffer last named. + pub frame: (u32, u32), + /// A setter raised the error flag, which stops a stored combination. + pub error: bool, + rm_count: u32, +} + +impl Channels { + /// The display generator at power-up: channel 0 shows the whole managed area at 1280x1024, + /// the format the PROM falls back to, and every other channel is off. + /// + /// Approximated: a real display generator runs the combination stored in its flash at + /// power-up; the board runs one only when the firmware is released (`start`), so a reset + /// starts the single-channel state every captured pipe can show. + pub fn new(rm_count: u32) -> Channels { + let mut channel: [Channel; CHANNELS] = std::array::from_fn(Channel::off); + let c = &mut channel[0]; + c.enabled = true; + c.loaded = true; + c.format = FB; + c.input = [0, FB.1 - 1, FB.0, FB.1]; + Channels { channel, blanked: false, frame: FB, error: false, rm_count } + } + + pub fn reset(&mut self) { + *self = Channels::new(self.rm_count); + } + + /// The state, as the stream module reads and writes it. + pub fn visit(&mut self, s: &mut S) { + let Self { channel: _, blanked: _, frame: _, error: _, rm_count: _ } = self; + self.channel.iter_mut().for_each(|c| c.visit(s)); + s.bool(&mut self.blanked); + s.u32(&mut self.frame.0); + s.u32(&mut self.frame.1); + s.bool(&mut self.error); + } + + /// Raster-manager pixel budget at a managed size: how many bytes a pixel may take. + fn budget(&self, width: u32, height: u32) -> u64 { + let area = (width as u64).div_ceil(TILE_WIDTH) * TILE_WIDTH * height as u64; + if area == 0 { + 0 + } else { + self.rm_count.max(1) as u64 * RM_PIXEL_BYTES / area + } + } + + /// SetupFrameBuffer [width, height, pixel depth], as the firmware runs it: a budget below + /// Small's, or a named depth (0 to 3) the budget does not cover, raises the error flag; -1, + /// -2 and other values pick a depth that fits. It never replies. + fn setup_frame_buffer(&mut self, width: u32, height: u32, depth: u32) { + const NEED: [u64; 4] = [8, 16, 32, 64]; + let budget = self.budget(width, height); + if budget < NEED[0] { + crate::dlog!(LogModule::Kona, "kona: not enough raster managers for Small pixels at {width}x{height}"); + self.error = true; + } else if let Some(&need) = NEED.get(depth as usize) { + if budget < need { + self.error = true; + } + } + self.frame = (width, height); + } + + /// The reply to a video Get, as the bytes that go on the return page from byte 8: the reply id, + /// the data words and their sum with the id, big-endian. None for a command nobody waits on, + /// one past the table, or a per-channel Get naming a channel the board lacks (logged). + pub fn reply(&self, command: u32, data: &[u32]) -> Option> { + let (id, bytes) = crate::dev::kona::tables::VIDEO_REPLY.get(command as usize).copied().flatten()?; + let channel = if names_channel(command) { data.first().copied().unwrap_or(0) as usize } else { 0 }; + if channel >= CHANNELS { + crate::dlog!(LogModule::Kona, "kona: unsupported video channel {channel} for {command:#x}"); + return None; + } + let mut words = vec![0u32; bytes as usize / 4]; + self.reply_words(command, channel, &mut words); + let sum = words.iter().fold(id as u32, |s, w| s.wrapping_add(*w)); + let mut out = Vec::with_capacity(8 + bytes as usize); + out.extend_from_slice(&(id as u32).to_be_bytes()); + out.extend(words.iter().flat_map(|w| w.to_be_bytes())); + out.extend_from_slice(&sum.to_be_bytes()); + Some(out) + } + + /// The data words of a Get's reply, from the channel state. + fn reply_words(&self, command: u32, channel: usize, words: &mut [u32]) { + let ch = &self.channel[channel]; + let mut put = |at: usize, v: &[u32]| { + for (i, w) in v.iter().enumerate() { + if let Some(slot) = words.get_mut(at + i) { + *slot = *w; + } + } + }; + match command { + VIDEO_GET_BOARD_CONFIG => put(0, &[1, (1u32 << CHANNELS) - 1]), + VIDEO_GET_CHANNEL_CONFIG => { + put(0, &[1 | if channel == 0 { 2 } else { 0 } | if ch.enabled { 0xc } else { 0 }]); + put(1, &ch.name); + put(18, &[ch.cursor_priority]); + } + VIDEO_GET_RESIZE => { + put(0, &ch.input); + put(4, &ch.pan); + put(8, &ch.placement); + } + VIDEO_GET_VAMPIRE => put(0, &[ch.source]), + VIDEO_GET_CHANNEL_BLANKING => put(0, &[ch.blanked as u32]), + VIDEO_GET_CHANNEL_ENABLES => put(1, &[ch.enabled as u32]), + _ => {} + } + } + + /// The display generator's default state, as its rebuild resets each channel: the timing + /// source, the first and last name words (the name's first word and the datestamp), the pan + /// rectangle, the enabled bit and gamma map 0. The input rectangle, placement, the other name + /// words and the cursor priority keep their values. Then every channel is blanked, as the + /// display set-up that follows does. + fn default_state(&mut self) { + for (i, ch) in self.channel.iter_mut().enumerate() { + ch.enabled = false; + ch.loaded = false; + ch.source = i as u32; + ch.name[0] = 0; + ch.name[NAME_WORDS - 1] = 0; + ch.pan = [0, 0, FB.0, FB.1]; + ch.gamma_map = 0; + ch.blanked = true; + } + } + + /// Start the firmware: run `stored`, the combination in the flash (empty when its marker or + /// count is bad), else the built-in one. Returns the managed area the run left. + pub fn start(&mut self, stored: &[u32]) -> (u32, u32) { + self.default_state(); + let ran = !stored.is_empty() && image_valid(stored) && self.run(stored); + if !ran { + if !stored.is_empty() { + crate::dlog!(LogModule::Kona, "kona: stored combination of {} words failed to load; running the built-in one", stored.len()); + self.default_state(); + } + self.run(&fallback()); + } + self.frame + } + + /// Run a combination `image_valid` accepted, command by command; false when one raises the + /// error flag. + fn run(&mut self, words: &[u32]) -> bool { + self.error = false; + let mut at = 0; + while at < words.len() { + let command = words[at]; + let n = crate::dev::kona::tables::VIDEO[command as usize].unwrap_or(0) as usize; + self.set(command, &words[at + 1..at + 1 + n]); + if self.error { + return false; + } + at += n + 2; + } + true + } + + /// Run one video command that sets state. `data` is the words between the command number and + /// its checksum; a word past its end reads as zero. Gets and commands the model gives no + /// meaning change nothing. + pub fn set(&mut self, command: u32, data: &[u32]) { + let d = |i: usize| data.get(i).copied().unwrap_or(0); + match command { + VIDEO_TEMP_BLANK => { + self.blanked = d(0) != 0; + crate::dlog!(LogModule::Kona, "kona: TempBlankAllChannels {}", d(0)); + } + VIDEO_SETUP_FRAME_BUFFER => self.setup_frame_buffer(d(0), d(1), d(2)), + VIDEO_DISABLE_CHANNEL | VIDEO_LOAD_FORMAT | VIDEO_ENABLE_CHANNEL | VIDEO_RESIZE | VIDEO_CHANNEL_BLANKING | VIDEO_CHANNEL_GAMMA_MAP | VIDEO_CURSOR_PRIORITY | VIDEO_VAMPIRE | VIDEO_BIND_TO_X_WINDOW | VIDEO_PLACEMENT_RES => { + let channel = d(0) as usize; + if channel >= CHANNELS { + crate::dlog!(LogModule::Kona, "kona: unsupported video channel {channel} for {command:#x}"); + return; + } + self.set_channel(command, channel, data); + } + _ => {} + } + } + + fn set_channel(&mut self, command: u32, channel: usize, data: &[u32]) { + let d = |i: usize| data.get(i).copied().unwrap_or(0); + let ch = &mut self.channel[channel]; + match command { + VIDEO_DISABLE_CHANNEL => { + ch.enabled = false; + ch.loaded = false; + } + VIDEO_LOAD_FORMAT => { + if d(23) == 0 || d(24) == 0 || d(23) > MAX_DIM || d(24) > MAX_DIM { + crate::dlog!(LogModule::Kona, "kona: channel {channel} format {}x{} is outside the sizes presented", d(24), d(23)); + return; + } + ch.format = (d(24), d(23)); + for (i, w) in ch.name.iter_mut().enumerate() { + *w = d(2 + i); + } + ch.loaded = true; + ch.enabled = true; + } + VIDEO_ENABLE_CHANNEL => { + if d(1) != 0 && ch.loaded { + ch.enabled = true; + } + } + VIDEO_RESIZE => { + if (1..=4).any(|i| d(i) > MAX_DIM) { + crate::dlog!(LogModule::Kona, "kona: channel {channel} input {}x{} at {},{} is outside the sizes presented", d(3), d(4), d(1), d(2)); + return; + } + ch.input = [d(1), d(2), d(3), d(4)]; + if d(7) == 0 { + ch.pan = ch.input; + } + } + VIDEO_PLACEMENT_RES => { + for (i, w) in ch.placement.iter_mut().enumerate() { + *w = d(1 + i); + } + } + VIDEO_CHANNEL_BLANKING => { + ch.blanked = d(1) != 0; + crate::dlog!(LogModule::Kona, "kona: channel {channel} blanking {}", d(1)); + } + VIDEO_CHANNEL_GAMMA_MAP => { + if d(1) != 0 && d(1) != VIDEO_GAMMA_MAP_LINEAR { + crate::dlog!(LogModule::Kona, "kona: channel {channel} gamma map {}: the model holds map 0 alone", d(1) as i32); + return; + } + ch.gamma_map = d(1); + } + VIDEO_CURSOR_PRIORITY => ch.cursor_priority = d(1), + VIDEO_VAMPIRE => { + ch.source = d(1); + if d(1) as usize != channel { + crate::dlog!(LogModule::Kona, "kona: channel {channel} taking channel {}'s timing is not modelled", d(1)); + } + } + VIDEO_BIND_TO_X_WINDOW => { + if d(1) != 0 { + crate::dlog!(LogModule::Kona, "kona: channel {channel} bound to X window {:#x} is not modelled", d(1)); + } + } + _ => unreachable!("set_channel is called for channel setters only"), + } + } +} + +/// Whether a stored combination is whole: every command known, with a data-word count in the +/// table, its checksum (the command plus its data) matching, and nothing left over. +fn image_valid(words: &[u32]) -> bool { + let mut at = 0; + while at < words.len() { + let command = words[at] as usize; + let Some(Some(n)) = crate::dev::kona::tables::VIDEO.get(command).copied() else { return false }; + let n = n as usize; + if n + 1 >= words.len() - at { + return false; + } + let sum = words[at..=at + n].iter().fold(0u32, |s, w| s.wrapping_add(*w)); + if sum != words[at + n + 1] { + return false; + } + at += n + 2; + } + true +} + +/// Append video command `command` with `data` and its checksum to `image`. +fn append(image: &mut Vec, command: u32, data: &[u32]) { + image.push(command); + image.extend_from_slice(data); + image.push(data.iter().fold(command, |s, w| s.wrapping_add(*w))); +} + +/// LoadFormat for `channel`, named `name` and datestamped `stamp`, 1280x1024 at 60 Hz in the +/// words the model reads. +fn append_format(image: &mut Vec, channel: u32, name: &str, stamp: u32, size: (u32, u32)) { + let mut format = [0u32; 383]; + format[0] = channel; + format[1] = 1; + let mut padded = [0u8; 64]; + padded[..name.len()].copy_from_slice(name.as_bytes()); + for (w, c) in format[2..18].iter_mut().zip(padded.chunks_exact(4)) { + *w = u32::from_be_bytes(c.try_into().unwrap()); + } + format[18] = stamp; + format[19] = 60 << 16; + format[20] = 1; + format[21] = 1065; + format[22] = 1680; + format[23] = size.1; + format[24] = size.0; + append(image, VIDEO_LOAD_FORMAT, &format); +} + +/// The firmware's built-in fallback combination: a 1280x1024 managed area, channel 0 showing +/// 1280x1024_60.vfo, channel 1 named 640x486_30i.vfo with channel 0's timing, taking channel 0's +/// timing and blanked. Established from the firmware's built-in table. +/// +/// Approximated: only the commands the model gives meaning are built, so not the clock, encoder, +/// gamma, sync, gain, pedestal and phase settings, the cursor priorities (which stay 0, as at +/// reset), or the commands for the option channel; and LoadFormat's words past the size and rate +/// are zero. Both channels' Resize and PlacementRes words are the firmware's. +fn fallback() -> Vec { + let mut i = Vec::new(); + for c in 0..2 { + append(&mut i, VIDEO_DISABLE_CHANNEL, &[c]); + append(&mut i, VIDEO_CHANNEL_BLANKING, &[c, 1]); + } + append(&mut i, VIDEO_SETUP_FRAME_BUFFER, &[1280, 1024, u32::MAX]); + append_format(&mut i, 0, "1280x1024_60.vfo", 0x31ea_4b22, (1280, 1024)); + append(&mut i, VIDEO_VAMPIRE, &[0, 0, 1280, 1024, 0, 5, 3]); + append(&mut i, VIDEO_RESIZE, &[0, 0, 1023, 1280, 1024, 1, 1, 0]); + append(&mut i, VIDEO_CHANNEL_BLANKING, &[0, 0]); + append_format(&mut i, 1, "640x486_30i.vfo", 0x31ea_4b3c, (1280, 1024)); + append(&mut i, VIDEO_VAMPIRE, &[1, 0, 640, 486, 1, 5, 3]); + append(&mut i, VIDEO_RESIZE, &[1, 0, 1023, 1280, 1024, 1, 1, 0]); + append(&mut i, VIDEO_PLACEMENT_RES, &[0, 4, 1, 4, 1, 1280, 1024, 4, 1]); + append(&mut i, VIDEO_PLACEMENT_RES, &[1, 4, 1, 4, 1, 2048, 2048, 640, 486]); + append(&mut i, VIDEO_CHANNEL_BLANKING, &[1, 1]); + append(&mut i, VIDEO_ENABLE_CHANNEL, &[1, 1, 0]); + append(&mut i, VIDEO_ENABLE_CHANNEL, &[1, 0, 0]); + append(&mut i, VIDEO_ENABLE_CHANNEL, &[0, 1, 0]); + append(&mut i, VIDEO_ENABLE_CHANNEL, &[0, 0, 1]); + i +} + +/// Whether a Get's first data word names a physical channel. +fn names_channel(command: u32) -> bool { + matches!(command, VIDEO_GET_CHANNEL_CONFIG | VIDEO_GET_RESIZE | VIDEO_GET_VAMPIRE | VIDEO_GET_CHANNEL_ENABLES | VIDEO_GET_FORMAT_INFO | VIDEO_GET_CHANNEL_BLANKING) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A LoadFormat request for `channel`: name words, datestamp and the active size. + fn format(channel: u32, name: &str, stamp: u32, width: u32, height: u32) -> Vec { + let mut w = vec![0u32; 383]; + w[0] = channel; + w[1] = 1; + let mut padded = [0u8; 64]; + padded[..name.len()].copy_from_slice(name.as_bytes()); + for i in 0..16 { + w[2 + i] = u32::from_be_bytes(padded[4 * i..4 * i + 4].try_into().unwrap()); + } + w[18] = stamp; + w[23] = height; + w[24] = width; + w + } + + #[test] + fn power_up_shows_channel_zero_alone() { + let c = Channels::new(2); + assert!(c.channel[0].enabled && c.channel[0].loaded); + assert_eq!((c.channel[0].format, c.channel[0].input), ((1280, 1024), [0, 1023, 1280, 1024])); + assert!(c.channel[1..].iter().all(|c| !c.enabled && !c.loaded)); + assert_eq!(c.channel[5].source, 5); + assert_eq!(c.channel[5].pan, [0, 0, 1280, 1024]); + } + + #[test] + fn a_format_runs_the_channel_and_a_disable_stops_it() { + let mut c = Channels::new(2); + c.set(VIDEO_LOAD_FORMAT, &format(3, "640x486_30i.vfo", 0x31ea_4b3c, 640, 486)); + assert!(c.channel[3].enabled && c.channel[3].loaded); + assert_eq!(c.channel[3].format, (640, 486)); + assert_eq!(c.channel[3].name[0], u32::from_be_bytes(*b"640x")); + assert_eq!(c.channel[3].name[16], 0x31ea_4b3c); + c.set(VIDEO_DISABLE_CHANNEL, &[3]); + assert!(!c.channel[3].enabled && !c.channel[3].loaded); + // An enable asks to run a channel that holds a format; this one lost it. + c.set(VIDEO_ENABLE_CHANNEL, &[3, 1, 0]); + assert!(!c.channel[3].enabled); + // Its format size stays a format of a sort, but a format with no size is refused. + c.set(VIDEO_LOAD_FORMAT, &format(3, "bad.vfo", 0, 0, 486)); + assert!(!c.channel[3].loaded); + } + + #[test] + fn the_second_enable_pass_does_not_turn_a_running_channel_off() { + let mut c = Channels::new(2); + c.set(VIDEO_LOAD_FORMAT, &format(1, "a.vfo", 0, 640, 486)); + c.set(VIDEO_ENABLE_CHANNEL, &[1, 1, 0]); + c.set(VIDEO_ENABLE_CHANNEL, &[1, 0, 0]); + assert!(c.channel[1].enabled); + } + + #[test] + fn a_resize_sets_the_input_and_the_pan_unless_word_seven_is_set() { + let mut c = Channels::new(2); + c.set(VIDEO_RESIZE, &[1, 10, 500, 640, 480, 1, 1, 0]); + assert_eq!((c.channel[1].input, c.channel[1].pan), ([10, 500, 640, 480], [10, 500, 640, 480])); + c.set(VIDEO_RESIZE, &[1, 20, 400, 320, 240, 1, 1, 1]); + assert_eq!((c.channel[1].input, c.channel[1].pan), ([20, 400, 320, 240], [10, 500, 640, 480])); + c.set(VIDEO_RESIZE, &[1, 0, 0, 99_999, 1, 1, 1, 0]); + assert_eq!(c.channel[1].input, [20, 400, 320, 240], "a size past the limit is refused"); + } + + #[test] + fn the_other_setters_land_in_their_channel() { + let mut c = Channels::new(2); + c.set(VIDEO_PLACEMENT_RES, &[2, 4, 1, 4, 1, 2048, 2048, 640, 486]); + assert_eq!(c.channel[2].placement, [4, 1, 4, 1, 2048, 2048, 640, 486]); + c.set(VIDEO_CHANNEL_BLANKING, &[2, 7]); + assert!(c.channel[2].blanked); + c.set(VIDEO_CHANNEL_BLANKING, &[2, 0]); + assert!(!c.channel[2].blanked); + c.set(VIDEO_CURSOR_PRIORITY, &[2, 5]); + assert_eq!(c.channel[2].cursor_priority, 5); + c.set(VIDEO_VAMPIRE, &[2, 0, 1280, 1024, 0, 5, 3]); + assert_eq!(c.channel[2].source, 0); + c.set(VIDEO_CHANNEL_GAMMA_MAP, &[2, VIDEO_GAMMA_MAP_LINEAR]); + assert_eq!(c.channel[2].gamma_map, VIDEO_GAMMA_MAP_LINEAR); + c.set(VIDEO_CHANNEL_GAMMA_MAP, &[2, 3]); + assert_eq!(c.channel[2].gamma_map, VIDEO_GAMMA_MAP_LINEAR, "another map is refused"); + c.set(VIDEO_TEMP_BLANK, &[1]); + assert!(c.blanked); + c.set(VIDEO_TEMP_BLANK, &[0]); + assert!(!c.blanked); + } + + #[test] + fn a_channel_the_board_lacks_changes_nothing() { + let mut c = Channels::new(2); + let before = c.clone(); + for cmd in [VIDEO_DISABLE_CHANNEL, VIDEO_CHANNEL_BLANKING, VIDEO_CURSOR_PRIORITY] { + c.set(cmd, &[CHANNELS as u32, 1, 1]); + } + assert_eq!(c, before); + } + + #[test] + fn setting_up_the_frame_buffer_checks_the_raster_managers_budget() { + let mut c = Channels::new(2); + c.set(VIDEO_SETUP_FRAME_BUFFER, &[1280, 1024, u32::MAX]); + assert!(!c.error); + assert_eq!(c.frame, (1280, 1024)); + // Two managers hold 1280x1024 at a budget of 32 bytes a pixel: depth 2 fits, depth 3 does not. + c.set(VIDEO_SETUP_FRAME_BUFFER, &[1280, 1024, 2]); + assert!(!c.error); + c.set(VIDEO_SETUP_FRAME_BUFFER, &[1280, 1024, 3]); + assert!(c.error); + let mut one = Channels::new(1); + one.set(VIDEO_SETUP_FRAME_BUFFER, &[4096, 4096, u32::MAX]); + assert!(one.error, "not even Small pixels fit"); + } + + #[test] + fn a_short_request_reads_missing_words_as_zero() { + let mut c = Channels::new(2); + c.set(VIDEO_CURSOR_PRIORITY, &[4]); + assert_eq!(c.channel[4].cursor_priority, 0); + } + + /// The words of a reply after the id, without the closing sum. + fn words(reply: &[u8]) -> Vec { + reply.chunks(4).map(|c| u32::from_be_bytes(c.try_into().unwrap())).collect() + } + + #[test] + fn the_board_config_reply_names_the_eight_channels() { + let r = Channels::new(2).reply(VIDEO_GET_BOARD_CONFIG, &[0]).unwrap(); + // Reply id 6, five data words (version, board mask, three zeros), id plus data. + assert_eq!(words(&r), [6, 1, 0xff, 0, 0, 0, 0x106]); + assert_eq!(r.len(), 28); + } + + #[test] + fn a_channel_config_reply_carries_the_loaded_format() { + let mut c = Channels::new(2); + c.set(VIDEO_LOAD_FORMAT, &format(0, "1280x1024_60.vfo", 0x31ea_4b22, 1280, 1024)); + c.set(VIDEO_CURSOR_PRIORITY, &[0, 3]); + let w = words(&c.reply(VIDEO_GET_CHANNEL_CONFIG, &[0]).unwrap()); + assert_eq!(w.len(), 1 + 19 + 1); + assert_eq!((w[0], w[1]), (7, 0xf), "id 7, present, the encoder, enabled and active"); + assert_eq!(&w[2..6], &[u32::from_be_bytes(*b"1280"), u32::from_be_bytes(*b"x102"), u32::from_be_bytes(*b"4_60"), u32::from_be_bytes(*b".vfo")]); + assert_eq!((w[18], w[19]), (0x31ea_4b22, 3)); + assert_eq!(w[20], w[..20].iter().fold(0u32, |s, v| s.wrapping_add(*v)) , "the sum is the id plus every data word"); + // A channel that is off says it is present and nothing more. + let off = words(&c.reply(VIDEO_GET_CHANNEL_CONFIG, &[4]).unwrap()); + assert_eq!(off[1], 1); + } + + #[test] + fn a_resize_reply_returns_the_rectangles() { + let mut c = Channels::new(2); + c.set(VIDEO_RESIZE, &[1, 0, 1023, 1280, 1024, 1, 1, 0]); + c.set(VIDEO_PLACEMENT_RES, &[1, 4, 1, 4, 1, 2048, 2048, 640, 486]); + let w = words(&c.reply(VIDEO_GET_RESIZE, &[1]).unwrap()); + assert_eq!(&w[1..17], &[0, 1023, 1280, 1024, 0, 1023, 1280, 1024, 4, 1, 4, 1, 2048, 2048, 640, 486]); + assert_eq!(w.len(), 1 + 18 + 1); + // The power-up channel pans over the whole area and takes steps of one pixel. + let fresh = words(&Channels::new(2).reply(VIDEO_GET_RESIZE, &[5]).unwrap()); + assert_eq!(&fresh[5..9], &[0, 0, 1280, 1024]); + assert_eq!(&fresh[9..17], &[1, 1, 1, 1, 1280, 1024, 0, 0]); + } + + #[test] + fn the_small_gets_read_one_channel() { + let mut c = Channels::new(2); + c.set(VIDEO_CHANNEL_BLANKING, &[2, 1]); + c.set(VIDEO_VAMPIRE, &[2, 0, 1280, 1024, 0, 5, 3]); + assert_eq!(words(&c.reply(VIDEO_GET_CHANNEL_BLANKING, &[2]).unwrap()), [9, 1, 10]); + assert_eq!(words(&c.reply(VIDEO_GET_CHANNEL_BLANKING, &[3]).unwrap()), [9, 0, 9]); + let v = words(&c.reply(VIDEO_GET_VAMPIRE, &[2]).unwrap()); + assert_eq!((v.len(), v[1], v[2]), (8, 0, 0), "the source is channel 0"); + assert_eq!(words(&c.reply(VIDEO_GET_CHANNEL_ENABLES, &[0]).unwrap()), [19, 0, 1, 20]); + } + + #[test] + fn a_get_for_a_missing_channel_or_a_setter_posts_nothing() { + let c = Channels::new(2); + assert_eq!(c.reply(VIDEO_GET_CHANNEL_CONFIG, &[CHANNELS as u32]), None); + assert_eq!(c.reply(VIDEO_LOAD_FORMAT, &[0]), None); + assert_eq!(c.reply(0x5d, &[0]), None, "a number past the table"); + // A Get that names no channel answers from channel 0 and is not refused for its word. + assert!(c.reply(VIDEO_GET_BOARD_CONFIG, &[99]).is_some()); + } + + #[test] + fn every_reply_id_is_the_servers_and_sized_by_its_table() { + use crate::dev::kona::tables::VIDEO_REPLY; + // The reply sizes by reply id, as the display-request table lists them (id 26 has no sender: the + // monitor command, which has a payload, has no reply). + const SIZES: [u8; 32] = [4, 4, 4, 4, 4, 4, 20, 76, 72, 4, 4, 24, 4, 4, 4, 4, 24, 12, 4, 8, 4, 4, 4, 20, 16, 68, 136, 4, 4, 8, 8, 164]; + let rows: Vec<_> = VIDEO_REPLY.iter().flatten().collect(); + assert_eq!(rows.len(), 31); + let ids: Vec = rows.iter().map(|(id, _)| *id as usize).collect(); + assert_eq!(ids, (0..32).filter(|&i| i != 26).collect::>(), "in command order, every id but 26"); + for (id, bytes) in rows { + assert_eq!(*bytes, SIZES[*id as usize], "reply {id}"); + } + assert_eq!(VIDEO_REPLY[0x51], None); + assert_eq!(VIDEO_REPLY.len(), crate::dev::kona::tables::VIDEO.len()); + } + + /// The words of a framed combination: each command, its data and checksum. + fn framed(commands: &[(u32, Vec)]) -> Vec { + let mut image = Vec::new(); + for (c, d) in commands { + append(&mut image, *c, d); + } + image + } + + #[test] + fn the_built_in_combination_is_valid_and_runs_two_channels() { + let image = fallback(); + assert!(image_valid(&image)); + let mut c = Channels::new(2); + assert_eq!(c.start(&[]), (1280, 1024)); + assert!(!c.error); + // Channel 0 runs 1280x1024_60.vfo, unblanked; channel 1 holds its format blanked; the rest are off. + assert!(c.channel[0].enabled && !c.channel[0].blanked); + assert!(c.channel[1].enabled && c.channel[1].blanked); + assert_eq!(c.channel[1].source, 0, "channel 1 takes channel 0's timing"); + assert!(c.channel[2..].iter().all(|c| !c.enabled && c.blanked)); + let name = words(&c.reply(VIDEO_GET_CHANNEL_CONFIG, &[0]).unwrap()); + assert_eq!(&name[2..6], &[u32::from_be_bytes(*b"1280"), u32::from_be_bytes(*b"x102"), u32::from_be_bytes(*b"4_60"), u32::from_be_bytes(*b".vfo")]); + assert_eq!(name[18], 0x31ea_4b22); + let two = words(&c.reply(VIDEO_GET_RESIZE, &[1]).unwrap()); + assert_eq!(&two[9..17], &[4, 1, 4, 1, 2048, 2048, 640, 486], "PlacementRes of channel 1"); + } + + #[test] + fn a_whole_stored_combination_replaces_the_built_in_one() { + let mut image = framed(&[(VIDEO_SETUP_FRAME_BUFFER, vec![1280, 1024, u32::MAX])]); + append(&mut image, VIDEO_DISABLE_CHANNEL, &[0]); + append(&mut image, VIDEO_DISABLE_CHANNEL, &[1]); + append_format(&mut image, 2, "1600x1200_60.vfo", 5, (1600, 1200)); + let mut c = Channels::new(2); + c.start(&image); + assert!(!c.channel[0].enabled && !c.channel[1].enabled); + assert!(c.channel[2].enabled); + assert_eq!(c.channel[2].format, (1600, 1200)); + } + + #[test] + fn a_damaged_or_failing_combination_falls_back_to_the_built_in_one() { + let good = framed(&[(VIDEO_DISABLE_CHANNEL, vec![0])]); + let mut bad_sum = good.clone(); + *bad_sum.last_mut().unwrap() += 1; + let unknown = framed(&[(0x7f, vec![1])]); + // A command's data must be the table's count: Test takes one word. + let wrong_length = framed(&[(0x00, vec![0, 0])]); + let cut = good[..good.len() - 1].to_vec(); + let mut trailing = good.clone(); + trailing.push(VIDEO_DISABLE_CHANNEL); + assert!(image_valid(&good)); + // Two managers cannot hold 8192x8192 pixels: a whole image whose run raises the error flag. + let too_big = framed(&[(VIDEO_DISABLE_CHANNEL, vec![0]), (VIDEO_SETUP_FRAME_BUFFER, vec![8192, 8192, u32::MAX])]); + assert!(image_valid(&too_big)); + let mut want = Channels::new(2); + want.start(&[]); + for (what, image) in [("sum", bad_sum), ("unknown", unknown), ("wrong length", wrong_length), ("cut", cut), ("trailing", trailing), ("error", too_big)] { + if what != "error" { + assert!(!image_valid(&image), "{what}"); + } + let mut c = Channels::new(2); + c.start(&image); + assert_eq!(c, want, "{what}: ran the built-in combination"); + } + } + + #[test] + fn starting_again_rebuilds_the_default_state_first() { + let mut c = Channels::new(2); + c.start(&[]); + c.set(VIDEO_LOAD_FORMAT, &format(5, "x.vfo", 7, 640, 480)); + c.set(VIDEO_VAMPIRE, &[5, 0]); + c.set(VIDEO_CHANNEL_GAMMA_MAP, &[5, VIDEO_GAMMA_MAP_LINEAR]); + c.start(&[]); + let f = &c.channel[5]; + assert!(!f.enabled && !f.loaded && f.blanked); + assert_eq!((f.source, f.gamma_map, f.name[0], f.name[16]), (5, 0, 0, 0)); + } + + #[test] + fn the_state_survives_a_stream_round_trip_and_a_reset_clears_it() { + use crate::dev::kona::stream::{Reader, Writer}; + let mut c = Channels::new(2); + c.set(VIDEO_LOAD_FORMAT, &format(1, "640x486_30i.vfo", 9, 640, 486)); + c.set(VIDEO_TEMP_BLANK, &[1]); + let mut w = Writer::default(); + c.visit(&mut w); + let bytes = w.out; + let mut back = Channels::new(2); + let mut r = Reader::new(&bytes); + back.visit(&mut r); + assert!(r.finish().is_ok()); + assert_eq!(back, c); + back.reset(); + assert_eq!(back, Channels::new(2)); + } +} diff --git a/src/dev/kona/back/display/clock.rs b/src/dev/kona/back/display/clock.rs new file mode 100644 index 000000000..d672c1bf1 --- /dev/null +++ b/src/dev/kona/back/display/clock.rs @@ -0,0 +1,302 @@ +//! The display's clock: one frame at a time, at the video format's rate. +//! +//! At each frame boundary the display completes the retrace requests that are +//! waiting and raises the video interrupt. The interrupt rises once a frame +//! even when no request is pending. The back end drains the raster jobs before +//! it queues a retrace request, so a request is satisfied when it is made (no +//! tickets: approximated), and the display completes it at the next boundary +//! (the sequence register changes at the retrace; when a real pipe changes it +//! is unknown, approximated here as the retrace). In a frame the requests are +//! completed first and the frame composed after, so a swap applied before a +//! request shows in the frame that completes it. +//! +//! The thread, `kona-display`, runs whether or not the pipe works, so it never +//! marks activity for the driver's watchdog. It only holds the handle the front +//! end gave it and what it must complete, nothing of the pipe's state. +//! +//! Grades. The frame rate is 60 Hz until a video format's refresh rate can be +//! parsed from its load (approximated). + +use super::super::super::handle::DisplayHandle; +use parking_lot::{Condvar, Mutex}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering::{Acquire, Relaxed, Release}}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::Duration; + +/// The default frame period: 60 Hz. +pub const FRAME_PERIOD: Duration = Duration::from_micros(16_667); + +/// What the board and its display thread share. +#[derive(Default)] +pub struct Clock { + /// Retrace requests waiting for a frame boundary: the epoch each was made in + /// and its tag. + pending: Mutex>, + /// Frames the display has run, and requests it has completed. + frames: AtomicU64, + completed: AtomicU64, + /// The thread is to end, and whether it is to hold still (a snapshot). + stop: AtomicBool, + pause: Mutex, + changed: Condvar, +} + +#[derive(Default, PartialEq, Eq)] +struct Pause { + /// Asked to hold still, and whether it has. + wanted: bool, + held: bool, +} + +impl Clock { + /// A retrace request made in `epoch`, to complete at the next frame. + pub fn request(&self, epoch: u64, tag: u32) { + self.pending.lock().push((epoch, tag)); + } + + /// Forget the waiting requests: a reset cancels them. + pub fn cancel(&self) { + self.pending.lock().clear(); + } + + pub fn frames(&self) -> u64 { + self.frames.load(Relaxed) + } + + pub fn completed(&self) -> u64 { + self.completed.load(Relaxed) + } + + /// One frame boundary: complete what waits, then raise the video + /// interrupt if nothing did (completing a request raises it). + pub fn frame(&self, handle: &DisplayHandle) { + let waiting = std::mem::take(&mut *self.pending.lock()); + if waiting.is_empty() { + handle.video_frame(handle.epoch()); + } + for (epoch, tag) in &waiting { + handle.retrace_done(*epoch, *tag); + } + self.completed.fetch_add(waiting.len() as u64, Relaxed); + self.frames.fetch_add(1, Release); + } + + /// Hold the thread still at a frame boundary, and wait until it has. A + /// board with no thread has nothing to hold. + pub fn quiesce(&self, running: bool) { + let mut p = self.pause.lock(); + p.wanted = true; + self.changed.notify_all(); + while running && !p.held && !self.stop.load(Acquire) { + self.changed.wait(&mut p); + } + } + + pub fn resume(&self) { + let mut p = self.pause.lock(); + p.wanted = false; + self.changed.notify_all(); + } + + fn hold_if_asked(&self) { + let mut p = self.pause.lock(); + while p.wanted && !self.stop.load(Acquire) { + p.held = true; + self.changed.notify_all(); + self.changed.wait(&mut p); + } + p.held = false; + } +} + +/// The display thread and what it joins on. +pub struct DisplayThread { + clock: Arc, + join: Option>, +} + +impl DisplayThread { + /// Start `kona-display`, running a frame every `period`. + pub fn spawn(clock: Arc, handle: DisplayHandle, period: Duration, mut sink: super::Compositor) -> DisplayThread { + clock.stop.store(false, Release); + let c = clock.clone(); + let join = std::thread::Builder::new() + .name("kona-display".into()) + .spawn(move || { + // Frames are paced from a fixed start so a slow one does not + // stretch the rate. + let mut next = std::time::Instant::now() + period; + while !c.stop.load(Acquire) { + c.hold_if_asked(); + let now = std::time::Instant::now(); + // A pause or a stalled host leaves `next` far behind: go on + // from now rather than run the missed frames back to back. + if now.saturating_duration_since(next) > period { + next = now; + } + if now < next { + // Parked, not slept, so `stop` wakes it at once. + std::thread::park_timeout(next - now); + if c.stop.load(Acquire) { + break; + } + continue; + } + next += period; + c.frame(&handle); + sink.frame(); + } + sink.finish(); + }) + .expect("kona display thread"); + DisplayThread { clock, join: Some(join) } + } + + /// End the thread and wait for it. Called with the front end's pipe slot + /// taken and no register-file lock held: the thread takes only that lock, in + /// a handle call that sends an interrupt packet after dropping it, and never + /// the slot, so the join cannot wait on a lock its caller holds. + pub fn stop(&mut self) { + let Some(join) = self.join.take() else { return }; + self.clock.stop.store(true, Release); + { + let _p = self.clock.pause.lock(); + self.clock.changed.notify_all(); + } + join.thread().unpark(); + let _ = join.join(); + } +} + +impl Drop for DisplayThread { + fn drop(&mut self) { + self.stop(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::tests::{test_cfg, TestHost}; + use crate::dev::kona::{Kona, Reply}; + use crate::traits::{Device, BUS_OK}; + + const INT_ENABLE: u64 = 0x800230; + const INT_CAUSE: u64 = 0x800238; + const RETRACE_TAG: u64 = 0x800248; + + /// A compositor over a tiny store, for a thread that has nothing to show. + fn quiet() -> crate::dev::kona::back::display::Compositor { + use crate::dev::kona::back::display::{Compositor, screen::Display, tables::Tables}; + use crate::dev::kona::back::store::PixelStore; + Compositor::new(Arc::new(PixelStore::new(4, 4)), 1, Arc::default(), Arc::new(Mutex::new(Tables::default())), Arc::new(Display::detached())) + } + + /// A started board whose back end holds the handle; the board's host to read + /// registers and see the packets. + fn board() -> (Arc, Arc, DisplayHandle) { + let kept = Arc::new(Mutex::new(None)); + let host = TestHost::new(); + let kona = Kona::new(test_cfg(), host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + let keep = kept.clone(); + kona.shared.pipe.lock().backend.hook = Some(crate::dev::kona::back::TestHook { job: Box::new(|_, _| Some(Reply::Done)), events: Arc::default(), on_attach: Some(Box::new(move |h| *keep.lock() = Some(h))) }); + for (off, v) in [(0x34u64, 0x98000u32), (0x3c, 0x180_0090), (0x5c, 0x3e_2210), (INT_ENABLE, 0xf7c)] { + assert_eq!(kona.test_write(off, 4, v as u64), BUS_OK); + } + kona.start(); + let handle = kept.lock().clone().expect("attach gave a handle"); + (kona, host, handle) + } + + /// Wait for `cond`, up to a deadline, not a count of tries. + fn wait_for(mut cond: impl FnMut() -> bool) -> bool { + let end = std::time::Instant::now() + Duration::from_secs(30); + while std::time::Instant::now() < end { + if cond() { + return true; + } + std::thread::yield_now(); + } + cond() + } + + fn read(kona: &crate::dev::kona::Kona, off: u64) -> u32 { + kona.bus_read(off, 4).1 as u32 + } + + /// A frame completes a waiting request (the tag register changes, one video + /// interrupt goes out) and, with nothing waiting, still raises the video + /// interrupt once the driver has acknowledged the last. + #[test] + fn a_frame_completes_requests_and_raises_video() { + let (kona, host, h) = board(); + let clock = Clock::default(); + clock.request(h.epoch(), 0x42); + clock.frame(&h); + assert_eq!(read(&kona, RETRACE_TAG), 0x42); + assert_eq!(host.sent().len(), 1); + assert_eq!((clock.frames(), clock.completed()), (1, 1)); + clock.frame(&h); + assert_eq!(host.sent().len(), 1, "not again before the acknowledge"); + kona.bus_write(INT_CAUSE, 4, 8); + clock.frame(&h); + assert_eq!(host.sent().len(), 2); + assert_eq!(read(&kona, RETRACE_TAG), 0x42, "a frame with no request leaves the tag"); + kona.stop(); + } + + /// A reset cancels the waiting requests, and one made in an earlier epoch is + /// dropped by the front end. + #[test] + fn a_reset_cancels_waiting_requests() { + let (kona, _host, h) = board(); + let clock = Clock::default(); + clock.request(h.epoch(), 0x7); + clock.cancel(); + clock.frame(&h); + assert_eq!(read(&kona, RETRACE_TAG), 0); + let old = h.epoch(); + kona.stop(); + crate::traits::Resettable::power_on(&*kona); + clock.request(old, 0x9); + clock.frame(&h); + assert_eq!(read(&kona, RETRACE_TAG), 0, "a stale epoch is dropped"); + } + + /// The thread runs frames by itself and completes a request; stopping joins it. + #[test] + fn the_thread_completes_a_request_and_joins() { + let (kona, _host, h) = board(); + let clock = Arc::new(Clock::default()); + let mut t = DisplayThread::spawn(clock.clone(), h.clone(), Duration::from_millis(1), quiet()); + clock.request(h.epoch(), 0x55); + assert!(wait_for(|| read(&kona, RETRACE_TAG) == 0x55)); + assert_eq!(read(&kona, RETRACE_TAG), 0x55); + // The counters follow the completion within the same frame. + assert!(wait_for(|| clock.frames() >= 1)); + t.stop(); + let f = clock.frames(); + std::thread::yield_now(); + assert_eq!(clock.frames(), f, "no frames after the join"); + kona.stop(); + } + + /// A quiesced thread holds still between frames and goes on when resumed. + #[test] + fn a_quiesced_thread_holds_still() { + let (kona, _host, h) = board(); + let clock = Arc::new(Clock::default()); + let mut t = DisplayThread::spawn(clock.clone(), h, Duration::from_millis(1), quiet()); + clock.quiesce(true); + let f = clock.frames(); + for _ in 0..10_000 { + std::thread::yield_now(); + } + assert_eq!(clock.frames(), f); + clock.resume(); + assert!(wait_for(|| clock.frames() > f)); + t.stop(); + kona.stop(); + } +} diff --git a/src/dev/kona/back/display/dac_cursor.rs b/src/dev/kona/back/display/dac_cursor.rs new file mode 100644 index 000000000..d69e44b6d --- /dev/null +++ b/src/dev/kona/back/display/dac_cursor.rs @@ -0,0 +1,457 @@ +//! The DAC cursor: the display request that loads it, decoded and kept. +//! +//! The request is 263 words (the definitions table, display requests; the packing is established +//! from the X server's builder and the firmware's stream): the channel (0 to 7, or 0xde), six colour +//! words, then 256 image words. The colours are the foreground red, green, blue and the background +//! red, green, blue, each a 16-bit X colour shifted right by 6, so 10 bits. The image is 64 rows of +//! four words, two bits a pixel, `pixel = mask << 1 | source` (the opposite plane order to the XMAP +//! glyph cursor). The firmware sends each word low byte first to DAC register 0x400 on, and the +//! table's reading of where each pixel lands in the DAC's image RAM, which that makes, is: +//! byte `16 y + 4 (3 - x / 16) + (x mod 16) / 4`, the pixel at bits `7 - 2 (x mod 4)` down. That +//! the DAC fills ascending RAM addresses from index 0x400 is inferred. +//! +//! A cursor value of 3 is the foreground colour (the triple at colour register 0x303), 2 the +//! background (0x304), and 0 and 1 are transparent: established for the published part (Analog +//! Devices' ADV7160 and ADV7162 data sheet, with the vgadoc summary of it), inferred that this +//! board's DAC is that part (its scan-chain name and register map match), and the senders agree. The +//! image's pixel value 1 (source without mask) is transparent as the part has it. +//! The cursor control register's value 5, which the kernel writes for the owning channel and 0 for +//! the rest, is the X11 mode, enabled, not interlaced (`defs::dac_cursor_control`). +//! +//! # Reaching the DAC, and drawing the cursor +//! +//! Each of the eight channels has a DAC block on the display bus at `0xb00 + 0x100 n`: register 0 +//! takes the 16-bit index as two byte writes, low then high; register 2 is the data at that index +//! (established). The kernel sets index 0x204 (bytes 4 then 2) and writes the control value, and +//! sets index 0x200 for the position's four bytes, x low, x high, y low, y high (established); it +//! sends the index again before each byte, and the bytes go to register 2 of the owning channel's +//! block like every other data write (established: nothing writes any other address). The index +//! advances after each data access in the ranges the cursor uses, 0x200 to 0x204 and the image +//! 0x400 to 0x7ff (0x7ff wrapping to 0x000), and after each blue component of the colour registers +//! 0x303 and 0x304 (established for the part; the streamed colour and image loads need it); whether it +//! advances in 0x000 to 0x00f is unknown, and the model does not advance there. +//! +//! The model shows channel 0's frame, so the cursor drawn is channel 0's: mode X11 (1) and the +//! enable bit. It goes over the composed frame and its colours skip the gamma RAM, the DAC being +//! after gamma (established). The position is a signed 16-bit two's-complement top-left in output +//! pixels with no bias from software or the part (established): the kernel subtracts the hot spot +//! itself. (0, 0) is the first pixel after the rising edge of BLANK, and where the board puts +//! that edge against the frame's active pixels is unknown; the model takes the frame's first pixel. The kernel never sets the interlace bit. +//! +//! Not drawn, because the table settles nothing about them: the XGA mode (2) and modes 0 and 3; +//! pixel value 1 (source without mask), which no sender defines; and the channel 0xde image, whose +//! channel the table does not name. The interlace bit is kept and the cursor is drawn the same either +//! way; what the part does with it is not in the table. + +use crate::dev::kona::defs::{dac_cursor_control, DAC_CURSOR_CHANNELS, DAC_CURSOR_CHANNEL_DE}; +use crate::dev::kona::stream::Stream; +use std::collections::BTreeMap; +use std::sync::Arc; + +/// Words of the request: the channel, six colours, 256 image words. +pub const WORDS: usize = 263; +/// A cursor is 64 x 64. +pub const SIZE: usize = 64; + +/// One channel's loaded cursor. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DacCursor { + pub channel: u32, + /// Foreground then background, 10-bit red, green, blue each. + pub foreground: [u16; 3], + pub background: [u16; 3], + /// The DAC's image RAM, 1024 bytes, as the stream fills it. + ram: Box<[u8; 1024]>, +} + +impl DacCursor { + /// The cursor a load request carries, or None unless it is the 263 words and names a channel the + /// table gives (0 to 7, or 0xde). The front end frames the request at exactly 263 words, so the + /// length check matters only for a job handed straight in. + pub fn from_request(w: &[u32]) -> Option { + if w.len() != WORDS || !(DAC_CURSOR_CHANNELS.contains(&w[0]) || w[0] == DAC_CURSOR_CHANNEL_DE) { + return None; + } + let colour = |i: usize| [w[i], w[i + 1], w[i + 2]].map(|v| (v & 0x3ff) as u16); + let mut ram = Box::new([0u8; 1024]); + for (k, word) in w[7..].iter().enumerate() { + ram[4 * k..4 * k + 4].copy_from_slice(&word.to_le_bytes()); + } + Some(DacCursor { channel: w[0], foreground: colour(1), background: colour(4), ram }) + } + + /// The two-bit value of the pixel at (x, y), 0 to 63 each. + pub fn pixel(&self, x: usize, y: usize) -> u8 { + let byte = self.ram[16 * y + 4 * (3 - x / 16) + (x % 16) / 4]; + byte >> (6 - 2 * (x % 4)) & 3 + } + + /// What the cursor holds, for the monitor: by the value semantics above, the pixels that show the + /// foreground (value 3), the background (2), nothing (0), and those of the undefined value 1. + pub fn describe(&self) -> String { + let n = self.census(); + format!("channel {}: {} foreground, {} background, {} transparent, {} undefined", self.channel, n[3], n[2], n[0], n[1]) + } + + /// The cursor as the stream module reads and writes it. + fn visit(&mut self, s: &mut S) { + let Self { channel: _, foreground: _, background: _, ram: _ } = self; + s.u32(&mut self.channel); + for v in self.foreground.iter_mut().chain(self.background.iter_mut()) { + s.u16(v); + } + s.bytes(&mut self.ram[..]); + } + + /// Pixels of each value, 0 to 3. + pub fn census(&self) -> [u32; 4] { + let mut n = [0u32; 4]; + for y in 0..SIZE { + for x in 0..SIZE { + n[self.pixel(x, y) as usize] += 1; + } + } + n + } +} + +/// The DAC blocks of the eight channels: the cursors loaded, and what the kernel has written to +/// each block's cursor registers. +#[derive(Clone, Debug, Default)] +pub struct Dac { + cursors: BTreeMap>, + control: [u32; CHANNELS], + /// Position bytes: x low, x high, y low, y high. + position: [[u8; 4]; CHANNELS], + index: [u16; CHANNELS], + /// The next index byte written is the high one. + high: [bool; CHANNELS], +} + +/// The channels with a DAC block. +pub const CHANNELS: usize = 8; +const BLOCK_BASE: u32 = 0xb00; +const BLOCK_STRIDE: u32 = 0x100; +const INDEX_POSITION: u16 = 0x200; +const INDEX_CONTROL: u16 = 0x204; + +/// Where and how the cursor is drawn: what a frame reads. +#[derive(Clone, Debug)] +pub struct DacDraw { + pub x0: i32, + pub y0: i32, + /// Opaque `0xAABBGGRR`. + pub foreground: u32, + pub background: u32, + pub cursor: Arc, +} + +impl Dac { + pub fn load(&mut self, c: DacCursor) { + self.cursors.insert(c.channel, Arc::new(c)); + } + + pub fn cursors(&self) -> impl Iterator { + self.cursors.values().map(|c| &**c) + } + + /// A write to the display bus address `selector`; true if a DAC block took it. + pub fn write(&mut self, selector: u32, value: u32) -> bool { + let (n, reg) = if selector >= BLOCK_BASE && selector < BLOCK_BASE + CHANNELS as u32 * BLOCK_STRIDE { + (((selector - BLOCK_BASE) / BLOCK_STRIDE) as usize, (selector - BLOCK_BASE) % BLOCK_STRIDE) + } else { + return false; + }; + match reg { + 0 => { + let b = value as u8 as u16; + self.index[n] = if self.high[n] { self.index[n] & 0xff | b << 8 } else { self.index[n] & 0xff00 | b }; + self.high[n] = !self.high[n]; + } + 2 => { + self.high[n] = false; + match self.index[n] { + i @ INDEX_POSITION..=0x203 => self.position[n][(i - INDEX_POSITION) as usize] = value as u8, + INDEX_CONTROL => self.control[n] = value & 0xff, + _ => {} + } + // The index advances after an access in the cursor's ranges (established for the part); + // for 0x000 to 0x00f and elsewhere it is unknown, and the model leaves it. + if let i @ (INDEX_POSITION..=INDEX_CONTROL | 0x400..=0x7ff) = self.index[n] { + self.index[n] = if i == 0x7ff { 0 } else { i + 1 }; + } + } + _ => return false, + } + true + } + + /// The cursor a frame draws: channel 0's, X11 mode, enabled, with an image loaded. + pub fn draw(&self, through: impl Fn([u16; 3]) -> u32) -> Option { + use dac_cursor_control::*; + let control = self.control[0]; + if MODE.get(control) != MODE_X11 || ENABLE.get(control) == 0 { + return None; + } + let cursor = self.cursors.get(&0)?; + let p = &self.position[0]; + Some(DacDraw { + x0: i16::from_le_bytes([p[0], p[1]]) as i32, + y0: i16::from_le_bytes([p[2], p[3]]) as i32, + foreground: through(cursor.foreground), + background: through(cursor.background), + cursor: cursor.clone(), + }) + } + + /// The state, as the stream module reads and writes it. + pub fn visit(&mut self, s: &mut S) { + let Self { cursors: _, control: _, position: _, index: _, high: _ } = self; + let n = s.len(self.cursors.len()); + if s.loading() { + self.cursors.clear(); + for _ in 0..n { + let mut c = DacCursor { channel: 0, foreground: [0; 3], background: [0; 3], ram: Box::new([0; 1024]) }; + c.visit(s); + if DAC_CURSOR_CHANNELS.contains(&c.channel) || c.channel == DAC_CURSOR_CHANNEL_DE { + self.cursors.insert(c.channel, Arc::new(c)); + } else { + s.fail("a saved DAC cursor names a channel the table does not give"); + } + } + } else { + for c in self.cursors.values() { + (**c).clone().visit(s); + } + } + s.u32s(&mut self.control); + for p in self.position.iter_mut() { + s.bytes(p); + } + for i in self.index.iter_mut() { + s.u16(i); + } + for h in self.high.iter_mut() { + s.bool(h); + } + } +} + +impl DacDraw { + /// Draw the cursor into `out`, a frame `stride` pixels a row, top row first, `w` x `h` of it + /// the screen. Value 3 is the foreground, 2 the background; 0 and 1 leave the frame. + pub fn draw(&self, out: &mut [u32], stride: usize, w: usize, h: usize) { + for r in 0..SIZE { + let y = self.y0 + r as i32; + if y < 0 || y >= h as i32 { + continue; + } + for c in 0..SIZE { + let x = self.x0 + c as i32; + if x < 0 || x >= w as i32 { + continue; + } + match self.cursor.pixel(c, r) { + 3 => out[y as usize * stride + x as usize] = self.foreground, + 2 => out[y as usize * stride + x as usize] = self.background, + _ => {} + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The words the X server's builder makes for a bitmap of `width` columns, rows of `source` and + /// `mask` bytes MSB first: each byte's bits are interleaved (source bit n to 2n, mask bit n to + /// 2n + 1), byte-swapped, and stored as halfword `(width + 7) / 8 - 1 - j` of the row, halfword + /// 2k being the high half of word k. + fn request(channel: u32, width: usize, rows: &[(&[u8], &[u8])]) -> Vec { + let mut w = vec![0u32; WORDS]; + w[0] = channel; + w[1..7].copy_from_slice(&[0x3ff, 0, 0x155, 0x0aa, 0x0bb, 0x0cc]); + for (y, (source, mask)) in rows.iter().enumerate() { + for (j, (s, m)) in source.iter().zip(mask.iter()).enumerate() { + let mut v = 0u16; + for n in 0..8 { + v |= (((s >> n) & 1) as u16) << (2 * n) | (((m >> n) & 1) as u16) << (2 * n + 1); + } + let half = (width + 7) / 8 - 1 - j; + let word = &mut w[7 + 4 * y + half / 2]; + let shift = if half % 2 == 0 { 16 } else { 0 }; + *word |= (v.swap_bytes() as u32) << shift; + } + } + w + } + + #[test] + fn a_load_of_the_wrong_length_is_not_a_cursor() { + assert!(DacCursor::from_request(&[0; 262]).is_none()); + assert!(DacCursor::from_request(&[0; 264]).is_none()); + } + + /// Only the channels the table gives are kept: a guest-chosen value would otherwise add a + /// 1 KB image for each one it sends. + #[test] + fn a_channel_outside_the_tables_set_is_not_a_cursor() { + for c in [0, 7, 0xde] { + assert!(DacCursor::from_request(&request(c, 64, &[])).is_some(), "channel {c:#x}"); + } + for c in [8, 0xdd, 0xdf, 0x100, u32::MAX] { + assert!(DacCursor::from_request(&request(c, 64, &[])).is_none(), "channel {c:#x}"); + } + } + + #[test] + fn the_control_value_the_kernel_writes_is_an_enabled_x11_cursor() { + use crate::dev::kona::defs::dac_cursor_control::*; + assert_eq!([MODE.get(KERNEL_OWNER), ENABLE.get(KERNEL_OWNER), INTERLACE.get(KERNEL_OWNER)], [MODE_X11, 1, 0]); + assert_eq!(describe(KERNEL_OWNER), "0x5 = X11 cursor, enabled, not interlaced"); + assert_eq!(describe(0), "0x0 = mode 0 cursor, disabled, not interlaced"); + assert_eq!(describe(MODE_XGA | 8), "0xa = XGA cursor, disabled, interlaced"); + } + + #[test] + fn the_colours_keep_ten_bits_and_the_channel_is_kept() { + let c = DacCursor::from_request(&request(0xde, 64, &[])).unwrap(); + assert_eq!((c.channel, c.foreground, c.background), (0xde, [0x3ff, 0, 0x155], [0x0aa, 0x0bb, 0x0cc])); + assert_eq!(c.census(), [4096, 0, 0, 0]); + } + + /// A 64-wide image: the builder's bytes land where the table's RAM formula reads them, in every + /// column of a row, and the value is mask << 1 | source. + #[test] + fn a_64_wide_image_reads_back_pixel_for_pixel() { + // Row 5 is source and mask alternating by column: columns with an even index have both + // (3), odd ones the mask only (2); row 6 has the source alone in column 1 (value 1). + let alt = [0xaau8; 8]; + let none = [0u8; 8]; + let mut one = [0u8; 8]; + one[0] = 0x40; + let mut rows: Vec<(&[u8], &[u8])> = vec![(&none, &none); 5]; + rows.push((&alt, &[0xffu8; 8])); + rows.push((&one, &none)); + let c = DacCursor::from_request(&request(0, 64, &rows)).unwrap(); + for x in 0..64 { + // 0xaa MSB first is columns 0, 2, 4, 6 of each byte set. + assert_eq!(c.pixel(x, 5), if x % 2 == 0 { 3 } else { 2 }, "column {x}"); + } + assert_eq!(c.pixel(1, 6), 1); + assert!((0..64).filter(|&x| x != 1).all(|x| c.pixel(x, 6) == 0)); + assert_eq!(c.census(), [4096 - 32 - 32 - 1, 1, 32, 32]); + assert_eq!(c.describe(), "channel 0: 32 foreground, 32 background, 4031 transparent, 1 undefined"); + } + + /// Write the two index bytes then a data byte to channel `n`'s block. + fn set(d: &mut Dac, n: u32, index: u16, values: &[u8]) { + let block = 0xb00 + 0x100 * n; + d.write(block, index as u32 & 0xff); + d.write(block, index as u32 >> 8); + for v in values { + d.write(block + 2, *v as u32); + } + } + + /// The cursor shows only for channel 0, in X11 mode with the enable bit, once an image is loaded; + /// the position bytes arrive at index 0x200 through the block's data register. + #[test] + fn channel_0_draws_when_enabled_in_x11_mode() { + let ones = request(0, 64, &[(&[0xff; 8], &[0xff; 8])]); + let mut d = Dac::default(); + assert!(d.draw(|_| 1).is_none()); + d.load(DacCursor::from_request(&ones).unwrap()); + assert!(d.draw(|_| 1).is_none(), "loaded, control still 0"); + set(&mut d, 0, 0x204, &[5]); + set(&mut d, 0, 0x200, &[0x20, 1, 0x30, 0]); + let draw = d.draw(|c| c[0] as u32).unwrap(); + assert_eq!((draw.x0, draw.y0), (0x120, 0x30)); + assert_eq!(draw.foreground, 0x3ff); + for control in [0u8, 1, 4, 6, 7, 5, 13] { + set(&mut d, 0, 0x204, &[control]); + assert_eq!(d.draw(|_| 0).is_some(), control & 7 == 5, "control {control}"); + } + set(&mut d, 0, 0x204, &[5]); + set(&mut d, 3, 0x204, &[5]); + assert!(d.draw(|_| 0).is_some()); + set(&mut d, 0, 0x204, &[0]); + assert!(d.draw(|_| 0).is_none(), "another channel's enable does not show"); + } + + /// The kernel writes the position as it does every cursor register: the index (low byte then high), + /// then one data byte to register 2 of the block, the index sent again before each byte. Nothing + /// else is a DAC address (the position does not go to bus address 0x22, which no sender writes). + #[test] + fn the_kernels_position_bytes_arrive_one_index_at_a_time_and_nothing_else_is_the_dacs() { + let mut d = Dac::default(); + d.load(DacCursor::from_request(&request(0, 64, &[(&[0xff; 8], &[0xff; 8])])).unwrap()); + set(&mut d, 0, 0x204, &[5]); + for (k, v) in [5u8, 0, 9, 0].into_iter().enumerate() { + set(&mut d, 0, 0x200 + k as u16, &[v]); + } + let draw = d.draw(|_| 0).unwrap(); + assert_eq!((draw.x0, draw.y0), (5, 9)); + assert!(!d.write(0x22, 0) && !d.write(0x23, 0) && !d.write(0xb00 + 0x800, 0), "other addresses are not the DAC's"); + // A negative position is two's complement, unbiased. + for (k, v) in [0xfeu8, 0xff, 0xf0, 0xff].into_iter().enumerate() { + set(&mut d, 0, 0x200 + k as u16, &[v]); + } + let draw = d.draw(|_| 0).unwrap(); + assert_eq!((draw.x0, draw.y0), (-2, -16)); + } + + /// The index advances after each data access in the ranges the cursor uses, and the image index wraps + /// from 0x7ff to 0x000; elsewhere (0x000 to 0x00f is unknown) it stays. + #[test] + fn the_index_advances_in_the_cursors_ranges_only() { + let mut d = Dac::default(); + set(&mut d, 0, 0x203, &[1]); + assert_eq!(d.index[0], 0x204); + set(&mut d, 0, 0x204, &[5]); + assert_eq!(d.index[0], 0x205, "the control register advances too"); + set(&mut d, 0, 0x7fe, &[0, 0]); + assert_eq!(d.index[0], 0x000, "0x7ff wraps to 0"); + set(&mut d, 0, 0x004, &[0xff]); + assert_eq!(d.index[0], 0x004, "unknown for the low registers: left"); + set(&mut d, 0, 0x300, &[0]); + assert_eq!(d.index[0], 0x300); + } + + #[test] + fn foreground_background_and_clear_pixels_are_drawn_and_clipped() { + let mut rows: Vec<(&[u8], &[u8])> = Vec::new(); + // Row 0: foreground in column 0, background in column 1, clear after. + rows.push((&[0x80, 0, 0, 0, 0, 0, 0, 0], &[0xc0, 0, 0, 0, 0, 0, 0, 0])); + let c = DacCursor::from_request(&request(0, 64, &rows)).unwrap(); + let draw = DacDraw { x0: -1, y0: 0, foreground: 0xf, background: 0xb, cursor: Arc::new(c.clone()) }; + let mut out = vec![7u32; 4 * 2]; + draw.draw(&mut out, 4, 4, 2); + assert_eq!(out, [0xb, 7, 7, 7, 7, 7, 7, 7], "column 0 is off the left edge"); + let draw = DacDraw { x0: 2, y0: 1, ..draw }; + let mut out = vec![7u32; 4 * 2]; + draw.draw(&mut out, 4, 4, 2); + assert_eq!(out, [7, 7, 7, 7, 7, 7, 0xf, 0xb]); + } + + #[test] + fn the_dac_state_round_trips() { + use crate::dev::kona::stream::{Reader, Writer}; + let mut d = Dac::default(); + d.load(DacCursor::from_request(&request(0, 64, &[(&[0xff; 8], &[0xff; 8])])).unwrap()); + set(&mut d, 0, 0x204, &[5]); + set(&mut d, 0, 0x200, &[1, 2, 3, 4]); + let mut w = Writer::default(); + d.visit(&mut w); + let bytes = w.out; + let mut e = Dac::default(); + let mut r = Reader::new(&bytes); + e.visit(&mut r); + r.finish().unwrap(); + let (a, b) = (d.draw(|c| c[0] as u32).unwrap(), e.draw(|c| c[0] as u32).unwrap()); + assert_eq!((a.x0, a.y0, a.foreground), (b.x0, b.y0, b.foreground)); + assert_eq!(a.cursor.census(), b.cursor.census()); + } +} diff --git a/src/dev/kona/back/display/mod.rs b/src/dev/kona/back/display/mod.rs new file mode 100644 index 000000000..1998c85b5 --- /dev/null +++ b/src/dev/kona/back/display/mod.rs @@ -0,0 +1,563 @@ +//! What the board shows: the tables the scan-out reads and the composition of a +//! frame from the pixel store. +//! +//! The display reads the store as it is, without waiting for drawing (a frame +//! can show a pixel being drawn; the next frame reads it again). It composes +//! the whole managed area per row from the store and the tables, choosing what +//! to do with a run of pixels once and not per pixel. +//! +//! Deferred, as speed work the model does not do: composing by runs of equal +//! (main, overlay) ID pairs, and presenting only the changed lines (the board +//! presents every frame, as VPro's does). The popup and overlay planes stay word +//! positions beside the display-ID plane; splitting them into a main-ID and an +//! overlay-ID plane would move the recorded stores' hashes and is not done. +//! +//! # What a pixel word is +//! +//! A pixel's colour word means what its window's display ID says. The ID plane +//! (bits 7:2 of each pixel's word there; "bits 7:2" is not confirmed, and the +//! board has two layouts for the ID in a pixel, chosen by its depth) names a +//! window; the tables the X server loads (`tables`) say, from the ID's entry and +//! its mode byte together, whether that window's words are an 8-bit or a 12-bit +//! index into the X colour map, or direct colour. Colour map, then gamma: +//! inferred, solid. A pixel whose ID has no entry, and a board that has been +//! sent no table, use the default descriptor: the front colour buffer at level 0, +//! one word a pixel, read as a colour index into the textport's colour map. That +//! is what the PROM's console needs. +//! +//! The popup and overlay planes go over the main window: popup over overlay over +//! main, index 0 letting the layer below through (inferred, `compose_layers`). +//! +//! # The cursor +//! +//! The glyph cursor here is the XMAP's (cursor mode 0x103). The XMAP injects it +//! ahead of the colour map, so it replaces the pixel's colour before gamma and +//! gamma applies to it (inferred, solid; whether gamma is bypassed for it is +//! unknown). Its colours therefore go through the same gamma as the colour map's +//! (`Tables::through_gamma`) and the glyph is drawn over the composed frame, which +//! comes to the same pixels. The DAC cursor is a different thing: it sits inside +//! the video DAC, after gamma (established), and the kernel enables exactly one +//! of the two. Its load is decoded and kept by channel (`dac_cursor`), and channel 0's is drawn +//! over the composed frame, past the gamma RAM, in X11 mode when enabled (the other modes, and +//! what is inferred, are in that module's header). The crosshair, whose shape is unknown, is not +//! drawn. +//! +//! # Blanking +//! +//! A blanked channel 0, or a temporary blank of every channel, shows black at scan-out +//! (`Baked::is_blank`); the pixel store is untouched and drawing goes on under it. Other channels' +//! blanking is kept and not shown, as the board shows one channel's frame. +//! +//! Not composed: stereo, the crosshair, the 16-plane +//! luminance and 15-bit RGB windows (counted by reason), and anything but the +//! first channel's gamma page. Widening a 10-bit entry to the gamma RAM's 12 +//! bits is inferred (the padding rule is unknown); the recorded desktop and PROM +//! frames match their captures to the pixel. +//! +//! That a textport pixel is a colour index, resolved at scan-out and not when +//! drawn, is inferred from the textport's colour-select command taking an +//! index. The textport colour map's entries are 10-bit components the sender +//! scales up by four, narrowed to eight bits as they are stored. + +pub mod channels; +pub mod clock; +pub mod dac_cursor; +pub mod screen; +pub mod tables; + +use super::store::{PixelStore, DID_PLANE}; +use super::store::{OVERLAY0_PLANE, OVERLAY1_PLANE, POPUP_PLANE}; +use crate::dev::kona::defs::{OVERLAY_ID_FIRST, POPUP_ID}; +use tables::{Baked, Tables, Window}; +use parking_lot::Mutex; +use screen::Display; +use std::io::{self, Write}; +use std::sync::Arc; + +/// Entries in the textport's colour map. +pub const COLOUR_MAP_ENTRIES: usize = 256; + +/// The textport's colour map: an index to a colour, held as the bytes red, +/// green, blue and a full alpha, the order the screens in the emulator use +/// (`0xAABBGGRR` read as a little-endian word). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ColourMap { + entries: [u32; COLOUR_MAP_ENTRIES], +} + +impl Default for ColourMap { + fn default() -> Self { + ColourMap { entries: [0xff00_0000; COLOUR_MAP_ENTRIES] } + } +} + +impl ColourMap { + /// The entries, as the stream module reads and writes them. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { entries: _ } = self; + s.u32s(&mut self.entries); + } + + /// A colour-map load: an index and three 10-bit components, each the + /// 8-bit value scaled up by four (so the top eight bits are kept). + pub fn load(&mut self, args: &[u32]) { + let [index, r, g, b] = *args else { return }; + let c = |v: u32| (v >> 2) & 0xff; + self.entries[(index & 0xff) as usize] = 0xff00_0000 | c(b) << 16 | c(g) << 8 | c(r); + } + + pub fn get(&self, index: u32) -> u32 { + self.entries[(index & 0xff) as usize] + } + + pub fn reset(&mut self) { + *self = ColourMap::default(); + } +} + +/// Compose the frame the default descriptor gives into `out`, `stride` pixels +/// a row, top row first. The store's rows run up from the bottom, so row `y` of +/// the frame is the store's row `height - 1 - y`. A word position with no array +/// yet is a store of zeros: index 0. +pub fn compose_default(store: &PixelStore, word: usize, map: &ColourMap, out: &mut [u32], stride: usize) { + let (w, h) = (store.width() as usize, store.height() as usize); + assert!(out.len() >= stride * h.saturating_sub(1) + w, "the frame buffer is too small"); + let Some(array) = store.array(word) else { + let black = map.get(0); + for y in 0..h { + out[y * stride..y * stride + w].fill(black); + } + return; + }; + let pitch = array.pitch(); + for y in 0..h { + let src = (h - 1 - y) * pitch; + let row = &mut out[y * stride..y * stride + w]; + for (x, px) in row.iter_mut().enumerate() { + // SAFETY: `src + x` is inside the array: `x < w <= pitch` and the row is in range. + *px = map.entries[(unsafe { array.load(src + x) } & 0xff) as usize]; + } + } +} + +/// Compose the frame with the tables the X server loaded: each pixel's display +/// ID (the ID plane's bits 7:2) names what its colour word is. A run of pixels +/// with the same ID is resolved to its window kind, and for an index window to +/// its slice of the baked colour map, once; each pixel is then a load, a shift +/// and a lookup. With nothing loaded this is `compose_default`, and the glyph +/// cursor goes over the top; the DAC cursor goes over that, and a blanked frame +/// is black. Each is chosen once for the frame, not per pixel. +pub fn compose(store: &PixelStore, word: usize, map: &ColourMap, tables: &Baked, out: &mut [u32], stride: usize) { + if tables.is_blank() { + // The scan-out shows black; the store is left as it is. + let (w, h) = (store.width() as usize, store.height() as usize); + assert!(out.len() >= stride * h.saturating_sub(1) + w, "the frame buffer is too small"); + (0..h).for_each(|y| out[y * stride..y * stride + w].fill(0xff00_0000)); + return; + } + if tables.is_default() { + return compose_default(store, word, map, out, stride); + } + let (w, h) = (store.width() as usize, store.height() as usize); + assert!(out.len() >= stride * h.saturating_sub(1) + w, "the frame buffer is too small"); + let (colour, back, did) = (store.array(word), store.array(word + 1), store.array(DID_PLANE)); + let pitch = store.width() as usize; + let id_at = |at: usize| -> usize { + // SAFETY: `at` is inside the managed area, which the array covers. + did.as_ref().map_or(0, |d| ((unsafe { d.load(at) } >> 2) & 0x3f) as usize) + }; + for y in 0..h { + let src = (h - 1 - y) * pitch; + let word_at = |i: usize| -> u32 { + // SAFETY: `i` is inside the managed area. + colour.as_ref().map_or(0, |c| unsafe { c.load(src + i) }) + }; + let row = &mut out[y * stride..y * stride + w]; + let mut x = 0; + while x < w { + let id = id_at(src + x); + let mut end = x + 1; + while end < w && id_at(src + end) == id { + end += 1; + } + match tables.window(id) { + Window::Textport => (x..end).for_each(|i| row[i] = map.entries[(word_at(i) & 0xff) as usize]), + Window::Index8 { base } => { + let t = tables.index8(base); + (x..end).for_each(|i| row[i] = t[(word_at(i) >> 8) as usize & 0xff]); + } + Window::Index12 { base } => { + let t = tables.index12(base); + (x..end).for_each(|i| row[i] = t[(word_at(i) >> 4) as usize & 0xfff]); + } + Window::Direct { bank } => { + // The buffer the window's swaps last showed. + let shown = if bank == 0 { &colour } else { &back }; + (x..end).for_each(|i| { + // SAFETY: `i` is inside the managed area. + row[i] = tables.direct(shown.as_ref().map_or(0, |c| unsafe { c.load(src + i) })); + }) + } + Window::Cleared | Window::Unsupported(_) | Window::Layer { .. } => row[x..end].fill(0xff00_0000), + } + x = end; + } + if tables.has_layers() { + compose_layers(store, tables, &mut row[..w], src); + } + } + tables.draw_cursor(out, stride, w, h); + tables.draw_dac_cursor(out, stride, w, h); +} + +/// The overlay and the popup over a composed row, `src` being the row's first +/// pixel in the store. Popup over overlay over main, a layer's index 0 letting +/// what is under it through and any other index replacing it (inferred, from the +/// contract the X server's visuals advertise; the hardware's own choice is read +/// nowhere in software). An opaque overlay replaces the pixel whatever its index. +/// Which overlay window a pixel belongs to is read from the ID plane's bits 12:8, +/// as ID 64 plus that field (inferred: the overlay IDs run 64 to 95, and the +/// model's ID plane keeps those bits beside the main field); the overlay's two +/// planes are its two buffers, as the main colour words are. +fn compose_layers(store: &PixelStore, tables: &Baked, row: &mut [u32], src: usize) { + let (did, popup) = (store.array(DID_PLANE), store.array(POPUP_PLANE)); + let overlay = [store.array(OVERLAY0_PLANE), store.array(OVERLAY1_PLANE)]; + let at = |a: &Option>, i: usize| -> u32 { + // SAFETY: `i` is inside the managed area, which the array covers. + a.as_ref().map_or(0, |a| unsafe { a.load(src + i) }) + }; + for (i, px) in row.iter_mut().enumerate() { + let id = OVERLAY_ID_FIRST + ((at(&did, i) >> 8) & 0x1f) as usize; + if let Window::Layer { base, planes, opaque, bank } = tables.window(id) { + let v = at(&overlay[bank as usize & 1], i) & ((1 << planes) - 1); + if v != 0 || opaque { + *px = tables.layer(base, v); + } + } + if let Window::Layer { base, planes, .. } = tables.window(POPUP_ID) { + let v = at(&popup, i) & ((1 << planes) - 1); + if v != 0 { + *px = tables.layer(base, v); + } + } + } +} + +/// The display thread's frame: compose the store through the tables every +/// retrace and present it, as VPro's display does. The store is not told when +/// drawing changes it, so a frame composed from a half-drawn store is simply +/// replaced by the next. +pub struct Compositor { + store: Arc, + word: usize, + map: Arc>, + tables: Arc>, + display: Arc, + frame: Vec, +} + +impl Compositor { + pub fn new(store: Arc, word: usize, map: Arc>, tables: Arc>, display: Arc) -> Compositor { + let n = store.width() as usize * store.height() as usize; + Compositor { store, word, map, tables, display, frame: vec![0; n] } + } +} + +impl Compositor { + /// Compose the frame and present it. + pub fn frame(&mut self) { + // The requests this frame answers are noted before it is composed. + let noted = self.display.note(); + // The tables are copied once per frame, under their lock; the store + // is read as it is, without waiting for drawing. + let map = self.map.lock().clone(); + let (w, h) = (self.store.width() as usize, self.store.height() as usize); + let tables = self.tables.lock().snapshot(); + compose(&self.store, self.word, &map, &tables, &mut self.frame, w); + self.display.present(&self.frame, w, h, noted); + } + + /// The display thread's last act. + pub fn finish(&mut self) { + self.display.finish(); + } +} + +/// The monitor's pixel reads, `hash`, `pix` and `shot`: they read the store and +/// the colour map through the storage the back end shares with the display, so +/// they run while the command stream does and never hold it up (a screenshot +/// composes 1.3 million pixels, and the pipe's state is not taken out for that). +pub struct Pixels { + store: Arc, + map: Arc>, + tables: Arc>, +} + +impl Pixels { + pub fn new(store: Arc, map: Arc>, tables: Arc>) -> Pixels { + Pixels { store, map, tables } + } + + /// The words of one pixel that have an array, as the planes hold them. + fn pix(&self, x: u32, y: u32, w: &mut dyn Write) -> io::Result<()> { + if self.store.word_slot(x, y, 0).is_none() { + return writeln!(w, "({x}, {y}) is outside the {} x {} managed area", self.store.width(), self.store.height()); + } + let words = self.store.allocated(); + for i in &words { + writeln!(w, "word {i:2} {:#010x}", self.store.read(x, y, *i))?; + } + writeln!(w, "{} word arrays allocated", words.len()) + } + + /// The frame the display would show now, composed here from the store as it + /// stands, without waiting for a display thread: rows from the top, one word a + /// pixel (0xAABBGGRR). + pub fn frame(&self) -> Vec { + let w = self.store.width() as usize; + let mut frame = vec![0u32; w * self.store.height() as usize]; + let map = self.map.lock().clone(); + let tables = self.tables.lock().snapshot(); + compose(&self.store, super::textport::COLOUR_WORD, &map, &tables, &mut frame, w); + frame + } + + /// The frame written as a PNG. + fn shot(&self, path: &str) -> Result<(usize, usize), String> { + let (w, h) = (self.store.width() as usize, self.store.height() as usize); + crate::hostinput::save_png(path, &self.frame(), w, h, None) + } +} + +impl Pixels { + /// A pixel monitor command; false if `args` is not one. + pub fn read(&self, args: &[&str], w: &mut dyn Write) -> io::Result { + match args { + ["hash"] => writeln!(w, "{:016x}", self.store.checksum())?, + ["shot", path] => match self.shot(path) { + Ok((w_, h_)) => writeln!(w, "{path}: {w_} x {h_}")?, + Err(e) => writeln!(w, "kona: {e}")?, + }, + ["pix", x, y] => { + let (Ok(x), Ok(y)) = (x.parse::(), y.parse::()) else { return Ok(false) }; + self.pix(x, y, w)? + } + _ => return Ok(false), + } + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_load_keeps_the_top_eight_bits_of_each_ten() { + let mut m = ColourMap::default(); + m.load(&[5, 1020, 4, 0x3ff]); + assert_eq!(m.get(5), 0xffff_01ff); + assert_eq!(m.get(5) >> 24, 0xff, "alpha is full"); + assert_eq!(m.get(5) & 0xff, 0xff, "red 1020 is 255"); + assert_eq!((m.get(5) >> 8) & 0xff, 1, "green 4 is 1"); + assert_eq!((m.get(5) >> 16) & 0xff, 0xff, "blue 1023 is 255"); + m.load(&[0x105, 0, 0, 0]); + assert_eq!(m.get(5) & 0xff_ffff, 0, "the index is a byte"); + m.load(&[1, 2, 3]); + assert_eq!(m.get(1), 0xff00_0000, "a short load is ignored"); + m.reset(); + assert_eq!(m, ColourMap::default()); + } + + /// The frame is top row first: the store's bottom row is the frame's last. + #[test] + fn the_frame_is_composed_top_row_first_through_the_colour_map() { + let s = PixelStore::new(4, 3); + let mut m = ColourMap::default(); + m.load(&[1, 1020, 0, 0]); // red + m.load(&[2, 0, 1020, 0]); // green + s.write(0, 0, 1, 1); // bottom-left + s.write(3, 2, 1, 2); // top-right + let mut out = vec![0u32; 4 * 3]; + compose_default(&s, 1, &m, &mut out, 4); + assert_eq!(out[2 * 4], m.get(1), "the store's row 0 is the frame's last row"); + assert_eq!(out[3], m.get(2), "the store's top row is the frame's first"); + assert_eq!(out[5], m.get(0)); + // A stride past the width leaves the gap alone. + let mut wide = vec![0xdead_beefu32; 8 * 3]; + compose_default(&s, 1, &m, &mut wide, 8); + assert_eq!(wide[4], 0xdead_beef); + assert_eq!(wide[8 * 2], m.get(1)); + // Only the low byte of a word is an index. + s.write(1, 1, 1, 0x1_0001); + compose_default(&s, 1, &m, &mut out, 4); + assert_eq!(out[4 + 1], m.get(1)); + } + + /// With no array the whole frame is index 0. + #[test] + fn an_unallocated_word_position_is_index_zero() { + let s = PixelStore::new(3, 2); + let mut m = ColourMap::default(); + m.load(&[0, 400, 800, 100]); + let mut out = vec![0u32; 6]; + compose_default(&s, 1, &m, &mut out, 3); + assert!(out.iter().all(|&p| p == m.get(0))); + } + + /// Each pixel's display ID, in the ID plane's bits 7:2, picks what its colour + /// word is: an 8-bit index in the top byte, a 12-bit one in bits 15:4, direct + /// colour, a window the model does not read, or (with no entry) the textport. + #[test] + fn the_display_id_picks_how_a_word_becomes_a_colour() { + let s = PixelStore::new(6, 1); + let mut t = Tables::default(); + t.load_colours(0x100, &[0x3ff << 20, 0, 0x3ff << 10]); + t.load_colours(0x200, &[0, 0x3ff]); + t.load_did(1, 0x100 << 7 | 7 << 3); + t.load_did(2, 0x200 << 7 | 11 << 3); + t.load_did(4, 0x140808); + t.load_mode(3, 4); + // Pixels: ID 1 index 2, ID 2 index 1 (word 0x10), ID 3 direct, ID 4, ID 5, and ID 1 again. + for (x, (id, word)) in [(1u32, 2u32 << 8), (2, 0x10), (3, 0x00_aa_bb_cc), (4, 0x1234), (5, 3), (1, 0)].into_iter().enumerate() { + s.write(x as u32, 0, 1, word); + s.write(x as u32, 0, DID_PLANE, id << 2 | 0x100); + } + let mut m = ColourMap::default(); + m.load(&[3, 1020, 0, 0]); + let mut out = vec![0u32; 6]; + compose(&s, 1, &m, &t.snapshot(), &mut out, 6); + assert_eq!(out[0], 0xff00_ff00, "index 2 of the map at 0x100 is green"); + assert_eq!(out[1], 0xffff_0000, "the 12-bit index 1 at 0x200 is blue"); + assert_eq!(out[2], 0xffcc_bbaa, "direct colour: red aa, green bb, blue cc"); + assert_eq!(out[3], 0xff00_0000, "a window the model does not read is black"); + assert_eq!(out[4], m.get(3), "no entry for the ID: the textport's colour map"); + assert_eq!(out[5], 0xff00_00ff, "index 0 of the map at 0x100 is red"); + } + + /// A cursor image for channel 0 of 64 x 64: row 0 is foreground then background then clear. + fn dac_load() -> dac_cursor::DacCursor { + let mut w = vec![0u32; dac_cursor::WORDS]; + w[1..7].copy_from_slice(&[0x3ff, 0, 0, 0, 0, 0x3ff]); + // Pixels 0 to 3 of row 0 are the one byte at RAM offset 12 (the low byte of word 3), pixel 0 + // in its top two bits. + w[7 + 3] = 0b11_10_00_00; + dac_cursor::DacCursor::from_request(&w).unwrap() + } + + /// The DAC cursor is drawn over the composed frame, in the DAC's colours and past the gamma + /// RAM, only once channel 0's control register enables it in X11 mode. + #[test] + fn the_dac_cursor_is_drawn_after_the_frame() { + let s = PixelStore::new(8, 2); + let mut t = Tables::default(); + let mut m = ColourMap::default(); + m.load(&[0, 1020, 1020, 1020]); + s.write(0, 0, 1, 0); + let frame = |t: &Tables| { + let mut out = vec![0u32; 16]; + compose(&s, 1, &m, &t.snapshot(), &mut out, 8); + out + }; + t.load_dac_cursor(dac_load()); + assert!(t.snapshot().is_default(), "loaded and not enabled changes nothing"); + assert!(frame(&t).iter().all(|&p| p == m.get(0))); + // Control 5 at index 0x204, then the position bytes at 0x200: x = 2, y = 1. + for (selector, value) in [(0xb00, 0x04), (0xb00, 0x02), (0xb02, 5), (0xb00, 0x00), (0xb00, 0x02), (0xb02, 2), (0xb02, 0), (0xb02, 1), (0xb02, 0)] { + t.indirect(selector, value); + } + assert!(!t.snapshot().is_default()); + let shown = frame(&t); + // Foreground is full red (10 bits), background full blue. + assert_eq!((shown[8 + 2], shown[8 + 3]), (0xff00_00ff, 0xffff_0000)); + assert_eq!(shown.iter().filter(|&&p| p != m.get(0)).count(), 2); + // A cursor that sits over a blanked frame is blanked with it. + t.set_blank(true); + assert!(frame(&t).iter().all(|&p| p == 0xff00_0000)); + } + + /// A blanked frame is black and leaves the store alone; showing it again restores the frame. + #[test] + fn blanking_blacks_the_frame_and_leaves_the_store() { + let s = PixelStore::new(8, 2); + let mut t = Tables::default(); + let mut m = ColourMap::default(); + m.load(&[0, 1020, 1020, 1020]); + s.write(0, 0, 1, 0); + let frame = |t: &Tables| { + let mut out = vec![0u32; 16]; + compose(&s, 1, &m, &t.snapshot(), &mut out, 8); + out + }; + let shown = frame(&t); + assert!(shown.iter().all(|&p| p == 0xffff_ffff)); + t.set_blank(true); + assert!(!t.snapshot().is_default()); + assert!(frame(&t).iter().all(|&p| p == 0xff00_0000)); + assert_eq!(s.read(0, 0, 1), 0, "the store is not touched"); + t.set_blank(false); + assert!(t.snapshot().is_default()); + assert_eq!(frame(&t), shown); + } + + /// The popup lies over the overlay and the overlay over the main window; an + /// index of 0 lets what is under it through, an opaque overlay does not, and + /// the overlay's second buffer shows after its swap. + #[test] + fn the_popup_lies_over_the_overlay_over_the_main_window() { + use crate::dev::kona::back::store::{OVERLAY0_PLANE, OVERLAY1_PLANE, POPUP_PLANE}; + let s = PixelStore::new(6, 1); + let mut t = Tables::default(); + // Main window ID 1: direct colour. Overlay ID 64 + 2, 4 planes, map at 0x100; popup ID 63, map at 0x200. + t.load_mode(1, 4); + t.load_did(66, 0x100 << 7 | 3 << 3); + t.load_mode(66, 1); + t.load_did(63, 0x200 << 7 | 1 << 3); + t.load_colours(0x100, &[0, 0x3ff << 20, 0x3ff << 10, 0x3ff]); + t.load_colours(0x200, &[0, 0x3ff << 20 | 0x3ff << 10, 0x3ff << 10 | 0x3ff, 0]); + // Six pixels: main only; overlay 1; overlay 0 (transparent); popup 1 over overlay 1; popup 0 (transparent) over overlay 2; no overlay window. + let main = 0x00_11_22_33u32; + for x in 0..6u32 { + s.write(x, 0, 1, main); + s.write(x, 0, 2, 0x00_44_55_66); + s.write(x, 0, DID_PLANE, 1 << 2 | if x == 5 { 0 } else { 2 << 8 }); + } + s.write(1, 0, OVERLAY0_PLANE, 1); + s.write(2, 0, OVERLAY0_PLANE, 0); + s.write(3, 0, OVERLAY0_PLANE, 1); + s.write(3, 0, POPUP_PLANE, 1); + s.write(4, 0, OVERLAY0_PLANE, 2); + s.write(5, 0, OVERLAY0_PLANE, 1); + let mut out = vec![0u32; 6]; + compose(&s, 1, &ColourMap::default(), &t.snapshot(), &mut out, 6); + let main_px = 0xff33_2211; + assert_eq!(out[0], main_px, "no layer: the main window"); + assert_eq!(out[1], 0xff00_00ff, "overlay index 1 is red"); + assert_eq!(out[2], main_px, "overlay index 0 lets the main window through"); + assert_eq!(out[3], 0xff00_ffff, "the popup over the overlay: its index 1 is red and green"); + assert_eq!(out[4], 0xff00_ff00, "popup index 0 shows the overlay's index 2, green"); + assert_eq!(out[5], main_px, "a pixel whose ID plane names no decoded overlay"); + // The overlay's second buffer after a swap, and an opaque overlay replacing even index 0. + t.load_bank(66, 1); + s.write(1, 0, OVERLAY1_PLANE, 3); + compose(&s, 1, &ColourMap::default(), &t.snapshot(), &mut out, 6); + assert_eq!(out[1], 0xffff_0000, "buffer 1, index 3 is blue"); + t.load_mode(66, 0x11); + compose(&s, 1, &ColourMap::default(), &t.snapshot(), &mut out, 6); + assert_eq!(out[2], 0xff00_0000, "opaque: index 0 of buffer 1 is the map's first entry, black"); + } + + /// With no table loaded the composition is the default one. + #[test] + fn with_no_tables_the_frame_is_the_default() { + let s = PixelStore::new(3, 1); + s.write(1, 0, 1, 2); + let mut m = ColourMap::default(); + m.load(&[2, 1020, 0, 0]); + let (mut a, mut b) = (vec![0u32; 3], vec![0u32; 3]); + compose(&s, 1, &m, &Tables::default().snapshot(), &mut a, 3); + compose_default(&s, 1, &m, &mut b, 3); + assert_eq!(a, b); + } +} diff --git a/src/dev/kona/back/display/screen.rs b/src/dev/kona/back/display/screen.rs new file mode 100644 index 000000000..c959ef730 --- /dev/null +++ b/src/dev/kona/back/display/screen.rs @@ -0,0 +1,262 @@ +//! The window the board shows the guest: what `GfxDisplay` hands out. +//! +//! The board owns its screen. Its display thread composes a frame at each +//! retrace and presents it here: the pixels go into the screen's frame buffer +//! (stride 2048, opaque `0xAABBGGRR`) and on to whatever renderer the front end +//! installed in the renderer slot. Screenshot and frame requests ask for the +//! next composed frame. The board presents every frame it composes, as VPro's +//! does, so there is no change detection or idle refresh here. The machine +//! keeps a clone of the `Arc` and returns it from `get_display`. + +use crate::cpu::mips_core::CyclesPtr; +use crate::dev::ng1::rex3::Renderer; +use crate::disp::{Rex3Screen, StatusSource, FB_STRIDE}; +use crate::gfx_display::{DisplayStop, GfxDisplay}; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use std::sync::Arc; + + +struct Local { + overlay: crate::debug_overlay::DebugOverlay, + status: crate::disp::StatusBar, + texture: crate::disp::StatusBarTexture, + size: (usize, usize), +} + +pub struct Display { + renderer: Mutex>>, + screen: Mutex, + /// Requests and what has been presented of them, as counts: a request is + /// pending until a frame composed after it has been presented, and one made + /// while a frame is composed is still pending after that frame. + screenshot_asked: AtomicU64, + screenshot_done: AtomicU64, + frame_asked: AtomicU64, + frame_done: AtomicU64, + screenshots: AtomicU32, + display_stop: DisplayStop, + local: Mutex>, + /// The machine's status-bar inputs. + status: Mutex>, + /// The tables the cursor registers live in, for `hw_cursor`. + tables: std::sync::OnceLock>>, +} + +/// The requests noted before a frame was composed: the ones presenting it answers. +#[derive(Clone, Copy, Debug)] +pub struct Due { + screenshot: u64, + frame: u64, +} + +impl Display { + pub fn new(status: Arc) -> Display { + Display { + renderer: Mutex::new(None), + screen: Mutex::new(Rex3Screen::new()), + screenshot_asked: AtomicU64::new(0), + screenshot_done: AtomicU64::new(0), + frame_asked: AtomicU64::new(0), + frame_done: AtomicU64::new(0), + screenshots: AtomicU32::new(0), + display_stop: DisplayStop::default(), + local: Mutex::new(None), + status: Mutex::new(status), + tables: std::sync::OnceLock::new(), + } + } + + /// Wire in the machine's status-bar inputs. + pub fn set_status(&self, status: Arc) { + *self.status.lock() = status; + } + + /// Wire in the tables whose cursor registers `hw_cursor` reads. + pub fn set_tables(&self, tables: Arc>) { + let _ = self.tables.set(tables); + } + + /// A display with a status source of its own, for a board no machine has + /// wired a status bar to. + pub fn detached() -> Display { + Display::new(StatusSource::new(Arc::default(), Arc::default())) + } + + /// The requests a frame composed from now on will answer. Take it before + /// composing and hand it to `present`: a request made while the frame is + /// composed is not in it, and stays pending after that frame is presented. + pub fn note(&self) -> Due { + Due { screenshot: self.screenshot_asked.load(Ordering::Acquire), frame: self.frame_asked.load(Ordering::Acquire) } + } + + /// Present a composed frame: `rows` are opaque `0xAABBGGRR` pixels, `width` + /// to a row, the first being the frame's top line. + pub fn present(&self, rows: &[u32], width: usize, height: usize, due: Due) { + let mut screen = self.screen.lock(); + let w = width.min(FB_STRIDE); + let h = height.min(screen.rgba.len() / FB_STRIDE).min(rows.len() / width.max(1)); + screen.width = w; + screen.height = h; + screen.prebuilt = true; + screen.status_bar_only = false; + let planes = &mut *screen; + for (y, src) in rows.chunks_exact(width.max(1)).take(h).enumerate() { + let at = y * FB_STRIDE; + planes.fb_rgb[at..at + w].copy_from_slice(&src[..w]); + planes.rgba[at..at + w].copy_from_slice(&src[..w]); + } + screen.frame_seq = screen.frame_seq.wrapping_add(1); + // A requested screenshot is saved as presented. + if due.screenshot != self.screenshot_done.load(Ordering::Acquire) { + let pixels = screen.rgba.clone(); + let n = self.screenshots.fetch_add(1, Ordering::Relaxed); + let path = format!("screenshot_{:04}.png", n); + std::thread::spawn(move || match crate::disp::save_screenshot(&path, &pixels, w, h) { + Ok(()) => println!("iris: screenshot saved to {}", path), + Err(e) => println!("iris: screenshot failed: {}", e), + }); + } + let mut renderer = self.renderer.lock(); + if let Some(renderer) = renderer.as_mut() { + let mut local = self.local.lock(); + let local = local.get_or_insert_with(|| Local { + overlay: crate::debug_overlay::DebugOverlay::new(), + status: crate::disp::StatusBar::new(), + texture: crate::disp::StatusBarTexture::new(), + size: (0, 0), + }); + if local.size != (w, h) { + renderer.resize(w, h); + local.size = (w, h); + } + let stats = self.status.lock().snapshot(std::time::Instant::now(), 0); + renderer.present(&mut screen, &mut local.overlay, &mut local.status, &mut local.texture, &stats, false, None, None); + } + // Mark only the requests this frame answers: one made while it was + // composed stays pending for the next. + self.screenshot_done.store(due.screenshot, Ordering::Release); + self.frame_done.store(due.frame, Ordering::Release); + } + + /// The display thread's last act: release the renderer, unless the stop came + /// from the main thread, and drop the per-frame overlay state. + pub fn finish(&self) { + self.display_stop.finish(&self.renderer); + self.local.lock().take(); + } +} + +impl GfxDisplay for Display { + fn display_stop(&self) -> &DisplayStop { + &self.display_stop + } + + fn renderer_slot(&self) -> &Mutex>> { + &self.renderer + } + + fn screen(&self) -> &Mutex { + &self.screen + } + + fn request_screenshot(&self) { + self.screenshot_asked.fetch_add(1, Ordering::AcqRel); + } + + fn screenshot_pending(&self) -> bool { + self.screenshot_asked.load(Ordering::Acquire) != self.screenshot_done.load(Ordering::Acquire) + } + + fn cycles(&self) -> CyclesPtr { + self.status.lock().cycles() + } + + fn hw_cursor(&self) -> Option { + // Read live from the registers, not from the last composed frame. + self.tables.get()?.lock().snapshot().hw_cursor() + } + + fn request_frame(&self) { + self.frame_asked.fetch_add(1, Ordering::AcqRel); + } + + fn frame_pending(&self) -> bool { + self.frame_asked.load(Ordering::Acquire) != self.frame_done.load(Ordering::Acquire) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn frame(w: usize, h: usize, px: u32) -> Vec { + vec![px; w * h] + } + + /// A presented frame lands in the screen's buffers, row by row at the + /// screen's stride, and a requested frame clears when it is presented. + #[test] + fn a_presented_frame_lands_in_the_screen() { + let d = Display::detached(); + let mut f = frame(4, 3, 0xff00_0001); + f[4 + 2] = 0xff12_3456; + d.request_frame(); + assert!(d.frame_pending()); + let due = d.note(); + d.present(&f, 4, 3, due); + assert!(!d.frame_pending()); + let s = d.screen().lock(); + assert_eq!((s.width, s.height, s.frame_seq), (4, 3, 1)); + assert_eq!(s.rgba[FB_STRIDE + 2], 0xff12_3456); + assert_eq!(s.fb_rgb[FB_STRIDE + 2], 0xff12_3456); + assert_eq!(s.rgba[3], 0xff00_0001); + assert!(s.prebuilt); + } + + /// A short frame presents what it has, and a wide one is cut to the screen. + #[test] + fn a_short_or_wide_frame_is_cut_to_what_it_has() { + let d = Display::detached(); + d.present(&frame(5, 2, 0xff00_0002)[..7], 5, 2, d.note()); + assert_eq!(d.screen().lock().height, 1); + d.present(&frame(3000, 1, 0xff00_0003), 3000, 1, d.note()); + assert_eq!(d.screen().lock().width, FB_STRIDE); + } + + /// A request made while a frame is composed is not lost when that frame is + /// presented: it is still pending, and the next frame answers it. + #[test] + fn a_request_made_during_composition_is_not_lost() { + let d = Display::detached(); + d.request_frame(); + let due = d.note(); + d.request_frame(); // arrives while the frame is composed + d.present(&frame(2, 2, 0xff00_0004), 2, 2, due); + assert!(d.frame_pending(), "the second request is still pending"); + let due = d.note(); + d.present(&frame(2, 2, 0xff00_0004), 2, 2, due); + assert!(!d.frame_pending()); + // Two requests answered by one frame are both answered. + d.request_frame(); + d.request_frame(); + d.present(&frame(2, 2, 0xff00_0004), 2, 2, d.note()); + assert!(!d.frame_pending()); + } + + /// A request made after the requests were noted and before the frame is + /// presented is not answered by that frame: it stays pending and makes the + /// next one due. + #[test] + fn a_request_after_the_note_waits_for_the_next_frame() { + let d = Display::detached(); + let noted = d.note(); + d.request_frame(); + let due = noted; + d.present(&frame(2, 2, 0xff00_0005), 2, 2, due); + assert!(d.frame_pending(), "the request made while the frame was composed is still pending"); + let due = d.note(); + d.present(&frame(2, 2, 0xff00_0005), 2, 2, due); + assert!(!d.frame_pending()); + } +} diff --git a/src/dev/kona/back/display/tables.rs b/src/dev/kona/back/display/tables.rs new file mode 100644 index 000000000..aedb5aead --- /dev/null +++ b/src/dev/kona/back/display/tables.rs @@ -0,0 +1,979 @@ +//! The tables the X server loads and the scan-out reads for windows that are +//! not the textport's: the X colour map, and the display-ID table that says, +//! for each window's ID, how its pixel words become colours. +//! +//! - 0x12e (`x_color_map`): a start address, a count and that many entries, +//! each 10:10:10 with red in bits 29:20 (software expectation from the X +//! server's install routine). Entry `i` lands at `start + i`. +//! - 0x132 (`x_did_map`): a display ID and a word, the ID-table entry +//! `(cmap << 7) & 0xffff80 | ((planes - 1) & 0xf) << 3 | lum << 2 | grey << 1` +//! (established layout; the same word the X server stores through the XMAP's +//! indirect registers 0x118 (index) and 0x11c (data), which `indirect` takes +//! too). The model reads `cmap` as the first entry of the window's colour map +//! and `planes` as the width of its index. +//! - 0x1e6 (`display_mode`): zero, a display ID and the X-built mode byte. The +//! mode byte is a second word, separate from the entry (established from the +//! X server's case split): bit 0 back buffer shown, +//! bit 1 stereo, bit 2 RGB below 24 bits, bit 3 colour index or luminance, +//! bit 4 direct colour; for overlays bits 3:1 hold a code (2, or 3 when double +//! buffered and starting in the back buffer, plus 4 for an underlay), bit 0 is +//! set for non-RGB overlays and bit 4 for opaque ones. A 30-bit RGB window +//! sends no mode bits. What the hardware does with the bits is not read +//! anywhere; the model uses them to choose the window kind. +//! +//! # Window kinds +//! +//! The kind of a display ID comes from the two words together (`decode`). +//! Main-plane IDs are 0 to 62; 63 is the popup (a 2-plane colour index); 64 to +//! 95 are overlays. Decoded, because the recordings use them: an 8-plane and a +//! 12-plane colour index (the entry's index-plane field 7 and 11) through the X +//! colour map; RGB, from mode bit 2 or 4, and 30-bit RGB (field 13, no map +//! offset, no mode bits), as direct colour; the popup and colour-index overlays +//! of 2, 4 or 8 planes as `Layer`s. Counted and shown black or transparent, with +//! the reason (`Why`): 16-plane luminance, 15-bit RGB overlays and any other +//! width. The pipe treats a colour index as a left-justified 12-bit component (an 8-bit +//! index is its value shifted left 4, `defs::colour_index`), and the draw-pixels path puts +//! an index of 8 bits or fewer at bits 15:8 of a 16-bit lane, which is the top byte the model +//! reads (established for the component, the lane inferred). Which lane holds the index and its +//! offset in the stored bank word are unknown. The ID in the pixel is the painted value: the +//! main ID at bits 7:2 and an overlay or popup group at bits 12:8 (`defs::PAINT_MAIN_SHIFT`); the +//! model keeps one 13-bit ID plane that holds both fields. +//! +//! A window's pixel words are an index shifted left by 8 for the X server's +//! 16-bit main-plane index (a pixel write shifts by the setup's 8), and the +//! colour map index is the word's top byte; for a 12-bit window the index is +//! bits 15:4. Direct colour is `0x00RRGGBB`, as the fills write it. +//! +//! # Baked when loaded +//! +//! What the scan-out reads is a `Baked` snapshot the loads keep up to date: the +//! colour map through the gamma RAM at full table size, direct colour's bytes +//! through it, each ID's window kind and the glyph cursor's colours and rows. A +//! load changes only what it names (a colour map load, the entries it covers; a +//! gamma load, every entry), and a frame takes the snapshot by `Arc`, so a frame +//! on a board with nothing loaded, and one between loads, copies and computes +//! nothing. + +use crate::dev::kona::defs::*; +use super::dac_cursor::{Dac, DacCursor, DacDraw}; +use std::sync::Arc; + +/// Entries in the X colour map. +pub const ENTRIES: usize = 0x8000; +/// Display IDs the table holds: main planes 0 to 62, the popup at 63 and the +/// overlays at 64 to 95. The ID plane's main field is six bits. +pub const IDS: usize = DISPLAY_IDS; + +/// What a window's pixels are, resolved from its table entries. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Window { + /// No table entry for the ID: the textport's own colour map by the word's + /// low byte (the PROM's console). + Textport, + /// An 8-bit index in the word's top byte, through the map from `base`. + Index8 { base: u32 }, + /// A 12-bit index in the word's bits 15:4, through the map from `base`. + Index12 { base: u32 }, + /// The word's low 24 bits are red, green and blue, in the colour word of + /// buffer `bank` (0 front, 1 back): which buffer a double-buffered window + /// shows follows its swaps. + Direct { bank: u8 }, + /// The popup or an overlay: a colour index of `planes` bits through the map + /// from `base`, in a plane of its own, laid over the layer below. Index 0 lets + /// that layer through unless the window is `opaque` (inferred). `bank` is the + /// overlay buffer its swaps last showed. + Layer { base: u32, planes: u8, opaque: bool, bank: u8 }, + /// A table word of zero: an ID the server cleared. Shown black. + Cleared, + /// A kind the model does not decode, counted by reason. Shown black (a + /// main-plane window) or not shown (a layer). + Unsupported(Why), +} + +/// Why a display ID is not decoded. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Why { + /// A 16-plane luminance or grey window. + Luminance, + /// A 15-bit RGB overlay, whose transparency rule is unread. + Rgb15Overlay, + /// An index width the model has no kind for, with its planes. + Planes(u8), + /// A colour map the table cannot hold from the entry's offset. + Map, +} + +/// The glyph cursor as the scan-out draws it: where its 64 x 64 box starts, its +/// colours and its rows (four words each: source bits, then mask bits). +#[derive(Clone, Debug)] +struct Cursor { + x0: i64, + y0: i64, + third: u32, + fg: u32, + bg: u32, + rows: [u32; 256], +} + +/// What a frame reads: the tables, through the gamma RAM, ready to index. +#[derive(Clone, Debug)] +pub struct Baked { + /// The colour map's entries as opaque `0xAABBGGRR` pixels, `ENTRIES` of them. + cmap: Vec, + /// Direct colour's bytes through the gamma RAM, by channel (red, green, blue). + lut: [[u8; 256]; 3], + windows: [Window; IDS], + default: bool, + /// Some popup or overlay window is decoded, so a frame has layers to compose. + layers: bool, + cursor: Option, + /// The DAC cursor, drawn after everything else and past the gamma RAM. + dac_cursor: Option, + /// Channel 0 is blanked: the frame is black. + blank: bool, +} + +#[derive(Clone, Debug)] +pub struct Tables { + /// The colour map's entries as loaded, 10:10:10. + raw: Vec, + /// The gamma RAM: a load address is the entry in bits 11:0, the colour in + /// 13:12 (red 0, green 1, blue 2), the page in 17:14 and a flag in 18 (what + /// the flag does is unknown; both halves are held). Zero until loaded. + gamma: Vec, + gamma_loaded: bool, + /// The XMAP's index register (0x118): which ID or channel the data registers + /// write, and the set-up word of channel 0, the one frame the model shows. Its + /// bits 3:0 pick the gamma page and bit 5 means no gamma; bit 4 (external + /// gamma) is not modelled. + xmap_index: u32, + channel0: u32, + /// A display-ID table word, once one was loaded for the ID. + did: [Option; IDS], + /// The mode word of each ID, 0 until `display_mode` sends one, and the + /// buffer each ID's swaps last showed. + mode: [u32; IDS], + bank: [u8; IDS], + /// The cursor: its glyph memory, which glyph, whether it shows, and where. + cursor_ram: Vec, + cursor_mode: u32, + cursor_at: u32, + cursor_pos: u32, + /// The DAC's cursor and its registers, and whether the frame is blanked. + dac: Dac, + blank: bool, + baked: Arc, + /// Colour-map and gamma entries past the tables, and cursor words past the + /// glyph memory, dropped. + pub dropped: u64, +} + +impl Default for Tables { + fn default() -> Self { + let mut t = Tables { + raw: vec![0; ENTRIES], + gamma: vec![0; GAMMA_ENTRIES], + gamma_loaded: false, + xmap_index: 0, + channel0: 0, + did: [None; IDS], + mode: [0; IDS], + bank: [0; IDS], + cursor_ram: vec![0; CURSOR_WORDS], + cursor_mode: 0, + cursor_at: 0, + cursor_pos: 0, + dac: Dac::default(), + blank: false, + baked: Arc::new(Baked { cmap: vec![BLACK; ENTRIES], lut: [[0; 256]; 3], windows: [Window::Textport; IDS], default: true, layers: false, cursor: None, dac_cursor: None, blank: false }), + dropped: 0, + }; + t.rebake_all(); + t + } +} + +const BLACK: u32 = 0xff00_0000; +/// Words of glyph memory: 64 slots of 260. +pub const CURSOR_SLOT: usize = 260; +const CURSOR_WORDS: usize = 64 * CURSOR_SLOT; +/// Gamma entries held: the 19-bit load address space. +const GAMMA_ENTRIES: usize = 1 << 19; + +/// A 12-bit gamma output to the DAC's 8-bit inputs: its top eight bits. Established on the link from +/// the colour map's output chips to the video-output chips, which carry only bits 11:4 of each colour +/// and have no rounding or dither control (read from the board's wiring); inferred end to end, since a chip between +/// them and the DAC is not read to add none. +fn narrow(c: u32) -> u32 { + c >> 4 +} + +impl Tables { + pub fn reset(&mut self) { + *self = Tables::default(); + } + + /// The tables as the guest loaded them, as the stream module reads and writes them. + /// What a frame reads (`Baked`) is made again from them on a load; the glyph memory + /// is the pixel store's chunks'. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { raw: _, gamma: _, gamma_loaded: _, xmap_index: _, channel0: _, did: _, mode: _, bank: _, cursor_ram: _, cursor_mode: _, cursor_at: _, cursor_pos: _, dac: _, blank: _, baked: _, dropped: _ } = self; + s.vec_u32(&mut self.raw); + s.bool(&mut self.gamma_loaded); + if self.gamma_loaded { + let n = s.len(self.gamma.len()); + if s.loading() { + self.gamma.clear(); + self.gamma.resize(n, 0); + } + s.u16s(&mut self.gamma); + } else if s.loading() { + self.gamma.iter_mut().for_each(|g| *g = 0); + } + s.u32(&mut self.xmap_index); + s.u32(&mut self.channel0); + for e in self.did.iter_mut() { + s.opt(e, |s, v| s.u32(v)); + } + s.u32s(&mut self.mode); + for b in self.bank.iter_mut() { + s.u8(b); + } + s.u32(&mut self.cursor_mode); + s.u32(&mut self.cursor_at); + s.u32(&mut self.cursor_pos); + self.dac.visit(s); + // `blank` is not saved: the back end derives it from the channels it restores. + if s.loading() { + if self.raw.len() != ENTRIES || self.gamma.len() != GAMMA_ENTRIES { + s.fail("the saved display tables are the wrong size"); + self.raw.resize(ENTRIES, 0); + self.gamma.resize(GAMMA_ENTRIES, 0); + } + self.rebake_all(); + } + } + + /// The tables a frame reads. Cheap: a reference count. + pub fn snapshot(&self) -> Arc { + self.baked.clone() + } + + /// One gamma output: entry `v12` of colour `ch`'s table in the page channel 0 + /// selects (its set-up word's bits 3:0; the page of a channel is chosen per + /// channel, established), or `v12` itself with no gamma loaded or the channel's + /// no-gamma bit (5) set. Only bank 0 of the load address is read. + fn gamma_out(&self, ch: usize, v12: u32) -> u32 { + if self.gamma_loaded && ChannelSetup::NO_GAMMA.get(self.channel0) == 0 { + let address = GammaAddress::PAGE.put(GammaAddress::COLOUR.put(v12, ch as u32), ChannelSetup::GAMMA_PAGE.get(self.channel0)); + narrow(self.gamma[address as usize] as u32) + } else { + narrow(v12) + } + } + + /// A 10:10:10 word as the scan-out shows it: each component widened to 12 + /// bits by replicating its top two bits (`c << 2 | c >> 8`, inferred: the only rule the + /// board's gamma addressing is known to fit), through the gamma RAM, narrowed to 8 by dropping + /// the low four. Without a gamma load the RAM is the identity. That the output goes through the + /// RAM is inferred. The glyph cursor's colours go through it too (inferred); the DAC cursor + /// does not (`rebake_dac`). + fn through_gamma(&self, e: u32) -> u32 { + let w10 = |c: u32| c << 2 | c >> 8; + BLACK | self.gamma_out(2, w10(e & 0x3ff)) << 16 | self.gamma_out(1, w10((e >> 10) & 0x3ff)) << 8 | self.gamma_out(0, w10((e >> 20) & 0x3ff)) + } + + /// Redo the colour map entries `from..to` of the snapshot. + fn rebake_colours(&mut self, from: usize, to: usize) { + let mut cmap = std::mem::take(&mut Arc::make_mut(&mut self.baked).cmap); + for i in from..to.min(ENTRIES) { + cmap[i] = self.through_gamma(self.raw[i]); + } + Arc::make_mut(&mut self.baked).cmap = cmap; + } + + /// Redo what the gamma RAM shapes: every colour map entry, direct colour's + /// bytes and the cursor's colours. + fn rebake_gamma(&mut self) { + self.rebake_colours(0, ENTRIES); + let mut lut = [[0u8; 256]; 3]; + for c in 0..256u32 { + for ch in 0..3 { + lut[ch][c as usize] = self.gamma_out(ch, c << 4 | c >> 4) as u8; + } + } + Arc::make_mut(&mut self.baked).lut = lut; + self.rebake_cursor(); + } + + /// The cursor's glyph memory, for a snapshot. + pub fn cursor_words(&self) -> &[u32] { + &self.cursor_ram + } + + /// Restore the glyph memory from a snapshot (the same length), and redo the cursor. + pub fn load_cursor_words(&mut self, words: &[u32]) { + if words.len() == self.cursor_ram.len() { + self.cursor_ram.copy_from_slice(words); + } + self.rebake_cursor(); + } + + /// Redo the glyph cursor: whether it shows, where, and its colours and rows. + /// The position's fields less the 32 the driver adds give the top-left pixel + /// of the 64 x 64 box (x from the left, y from the top). The crosshair + /// (mode 1) is not drawn. + fn rebake_cursor(&mut self) { + let at = self.cursor_at as usize; + let cursor = self.cursor_ram.get(at..at + CURSOR_SLOT).filter(|_| self.cursor_mode == 2).map(|g| Cursor { + x0: CursorPosition::X.get(self.cursor_pos) as i64 - CursorPosition::BIAS, + y0: CursorPosition::Y.get(self.cursor_pos) as i64 - CursorPosition::BIAS, + third: self.through_gamma(g[256]), + fg: self.through_gamma(g[257]), + bg: self.through_gamma(g[258]), + rows: g[..256].try_into().expect("256 words"), + }); + let b = Arc::make_mut(&mut self.baked); + b.cursor = cursor; + self.refresh_default(); + } + + /// Redo what the DAC adds to a frame: its cursor, and the blank. + fn rebake_dac(&mut self) { + // The DAC is after gamma: 10-bit components, narrowed to the eight the screen shows. + let narrow10 = |c: [u16; 3]| BLACK | (c[2] as u32 >> 2) << 16 | (c[1] as u32 >> 2) << 8 | c[0] as u32 >> 2; + let draw = self.dac.draw(narrow10); + let b = Arc::make_mut(&mut self.baked); + b.dac_cursor = draw; + b.blank = self.blank; + self.refresh_default(); + } + + /// Blank the frame, or show it: channel 0 blanked, or every channel temporarily. + pub fn set_blank(&mut self, blank: bool) { + if self.blank != blank { + self.blank = blank; + self.rebake_dac(); + } + } + + /// A DAC cursor load. + pub fn load_dac_cursor(&mut self, c: DacCursor) { + self.dac.load(c); + self.rebake_dac(); + } + + /// The DAC's state, for the monitor. + pub fn dac(&self) -> &Dac { + &self.dac + } + + fn refresh_default(&mut self) { + let default = self.baked.cursor.is_none() && self.baked.dac_cursor.is_none() && !self.baked.blank && !self.gamma_loaded && self.channel0 == 0 && self.did.iter().all(Option::is_none) && self.mode.iter().all(|&m| m == 0); + Arc::make_mut(&mut self.baked).default = default; + } + + fn rebake_all(&mut self) { + for id in 0..IDS { + self.set_window(id); + } + self.rebake_gamma(); + self.rebake_dac(); + } + + /// 0x12e: `words` are the entries from `start`. + pub fn load_colours(&mut self, start: u32, words: &[u32]) { + for (i, &w) in words.iter().enumerate() { + match self.raw.get_mut(start as usize + i) { + Some(e) => *e = w, + None => self.dropped += 1, + } + } + self.rebake_colours(start as usize, start as usize + words.len()); + } + + /// 0x12f: `words` are gamma entries from load address `start` (entry in bits + /// 11:0, colour 13:12, page 17:14, flag 18). An address past the 19 bits is + /// dropped and counted. Which page and bank the display reads is the channel's + /// choice (`indirect`), page 0 until one is sent. + pub fn load_gamma(&mut self, start: u32, words: &[u32]) { + self.gamma_loaded = true; + for (i, &w) in words.iter().enumerate() { + let at = start as usize + i; + match self.gamma.get_mut(at) { + Some(e) => *e = w as u16 & 0xfff, + None => self.dropped += 1, + } + } + self.rebake_gamma(); + self.refresh_default(); + } + + /// 0x130: a glyph's 260 words at glyph-memory address `at`. + pub fn load_cursor(&mut self, at: u32, words: &[u32]) { + for (i, &w) in words.iter().enumerate() { + match self.cursor_ram.get_mut(at as usize + i) { + Some(e) => *e = w, + None => self.dropped += 1, + } + } + self.rebake_cursor(); + } + + /// A store to the display's indirect register, the XMAP's: 0x101 (glyph + /// address), 0x102 (position) and 0x103 (cursor mode: 0 off, 1 crosshair, 2 + /// glyph) are the cursor's; 0x118 selects an ID or channel, and 0x11c writes + /// that ID's table entry and 0x11d that channel's set-up word. Others are not + /// modelled and are ignored. + pub fn indirect(&mut self, selector: u32, value: u32) { + match selector { + XMAP_CURSOR_IMAGE_BASE => self.cursor_at = value, + XMAP_CURSOR_POSITION => self.cursor_pos = value, + XMAP_CURSOR_MODE => self.cursor_mode = value, + XMAP_INDEX => self.xmap_index = value, + XMAP_ID_TABLE_DATA => return self.load_did(self.xmap_index, value), + XMAP_CHANNEL_SETUP_DATA => { + // Only channel 0, the one frame the model shows, is kept. + if self.xmap_index == 0 { + self.channel0 = value; + self.rebake_gamma(); + self.refresh_default(); + } + return; + } + _ if self.dac.write(selector, value) => return self.rebake_dac(), + _ => return, + } + self.rebake_cursor(); + } + + /// 0x132: the table word of display ID `id`. + pub fn load_did(&mut self, id: u32, word: u32) { + if let Some(e) = self.did.get_mut(id as usize) { + *e = Some(word); + self.set_window(id as usize); + self.refresh_default(); + } + } + + /// The buffer display ID `id`'s swaps last showed (0 before any). + pub fn bank(&self, id: u32) -> u8 { + self.bank.get(id as usize).copied().unwrap_or(0) + } + + /// A swap (0x1e6 with a window id): the buffer display ID `id` shows now. + pub fn load_bank(&mut self, id: u32, bank: u8) { + if let Some(e) = self.bank.get_mut(id as usize) { + *e = bank; + self.set_window(id as usize); + } + } + + /// 0x1e6: the mode word of display ID `id`. + pub fn load_mode(&mut self, id: u32, mode: u32) { + if let Some(e) = self.mode.get_mut(id as usize) { + *e = mode; + self.set_window(id as usize); + self.refresh_default(); + } + } + + /// What display ID `id`'s pixels are, from its table entry and mode byte + /// together (see the module doc). + fn decode(&self, id: usize) -> Window { + let (mode, entry) = (self.mode[id], self.did[id]); + let bank = self.bank[id]; + if id >= POPUP_ID { + return Self::decode_layer(id, mode, entry, bank); + } + // RGB below 24 bits (mode bit 2) or direct colour (bit 4) says what the + // words are, entry or none. + if ModeByte::RGB.get(mode) != 0 || ModeByte::DIRECT_OR_OPAQUE.get(mode) != 0 { + return Window::Direct { bank }; + } + let Some(w) = entry else { return Window::Textport }; + if w == 0 { + return Window::Cleared; + } + let (base, field) = (DisplayIdEntry::COLOUR_MAP_BASE.get(w), DisplayIdEntry::INDEX_PLANES.get(w)); + let fits = |n: usize| base as usize + n <= ENTRIES; + match field { + // A 30-bit RGB window sends no mode bits, and its entry has no map + // offset, no luminance or grey bit and an index-plane field of 13 (the + // 30 planes less one, kept to four bits). The recorded GL windows show + // exactly that, 0x68, beside the 24-bit RGB ones that say so in mode bit + // 0x4 (above). A 24-plane entry also has no offset and a field of 7, as + // an 8-plane index window's does, so only 13 is read as colour here; + // whether the hardware tells them apart by anything but the mode byte + // is not known. + 13 if base == 0 && DisplayIdEntry::LUMINANCE.get(w) == 0 && DisplayIdEntry::GREY.get(w) == 0 && ModeByte::COLOUR_INDEX.get(mode) == 0 => Window::Direct { bank }, + 7 if fits(256) => Window::Index8 { base }, + 11 if fits(4096) => Window::Index12 { base }, + 7 | 11 => Window::Unsupported(Why::Map), + 15 => Window::Unsupported(Why::Luminance), + f => Window::Unsupported(Why::Planes(f as u8 + 1)), + } + } + + /// The popup (ID 63) or an overlay (64 to 95). A colour-index layer of 2, 4 or + /// 8 planes is decoded; an overlay whose mode byte has bit 0 clear is RGB (15 + /// bits), whose transparency rule is unread, and is counted. An opaque overlay + /// (bit 4) replaces what is under it whatever its index. + fn decode_layer(id: usize, mode: u32, entry: Option, bank: u8) -> Window { + let Some(w) = entry else { return Window::Textport }; + if w == 0 { + return Window::Cleared; + } + if id >= OVERLAY_ID_FIRST && ModeByte::OVERLAY_NON_RGB.get(mode) == 0 { + return Window::Unsupported(Why::Rgb15Overlay); + } + let (base, planes) = (DisplayIdEntry::COLOUR_MAP_BASE.get(w), DisplayIdEntry::INDEX_PLANES.get(w) as u8 + 1); + match planes { + 2 | 4 | 8 if base as usize + (1 << planes) <= ENTRIES => Window::Layer { base, planes, opaque: id >= OVERLAY_ID_FIRST && ModeByte::DIRECT_OR_OPAQUE.get(mode) != 0, bank }, + 2 | 4 | 8 => Window::Unsupported(Why::Map), + n => Window::Unsupported(Why::Planes(n)), + } + } + + /// Decode display ID `id` into the snapshot and note whether any layer is in use. + fn set_window(&mut self, id: usize) { + let w = self.decode(id); + let b = Arc::make_mut(&mut self.baked); + b.windows[id] = w; + b.layers = b.windows[POPUP_ID..].iter().any(|w| matches!(w, Window::Layer { .. })); + } + + /// A line for the monitor: what is loaded, and the kinds not decoded by reason. + pub fn describe(&self) -> String { + let ids = self.did.iter().filter(|e| e.is_some()).count(); + let modes = self.mode.iter().filter(|&&m| m != 0).count(); + let cleared = self.baked.windows.iter().filter(|&&w| w == Window::Cleared).count(); + let mut why: std::collections::BTreeMap = Default::default(); + for w in &self.baked.windows { + if let Window::Unsupported(y) = w { + *why.entry(*y).or_default() += 1; + } + } + let unsupported: usize = why.values().sum(); + let reasons = if why.is_empty() { String::new() } else { format!(": {}", why.iter().map(|(y, n)| format!("{n} {y:?}")).collect::>().join(", ")) }; + format!( + "{ids} display-ID entries ({cleared} cleared, {unsupported} not decoded{reasons}), {modes} mode words, gamma {}, cursor mode {} at {:#x}, {} entries dropped", + if self.gamma_loaded { "loaded" } else { "none" }, + self.cursor_mode, + self.cursor_at, + self.dropped + ) + } +} + +impl Baked { + /// Whether nothing but the textport's own colour map is in use. + /// The frame is blanked: black, whatever the store holds. + pub fn is_blank(&self) -> bool { + self.blank + } + + /// A DAC cursor is drawn. + pub fn has_dac_cursor(&self) -> bool { + self.dac_cursor.is_some() + } + + /// Draw the DAC cursor over the composed frame, as `draw_cursor` does the XMAP's. + pub fn draw_dac_cursor(&self, out: &mut [u32], stride: usize, w: usize, h: usize) { + if let Some(d) = &self.dac_cursor { + d.draw(out, stride, w, h); + } + } + + pub fn is_default(&self) -> bool { + self.default + } + + pub fn window(&self, id: usize) -> Window { + self.windows[id] + } + + /// Whether a popup or an overlay is decoded, so a frame has layers to compose. + pub fn has_layers(&self) -> bool { + self.layers + } + + /// The colour of index `v` of a layer whose map starts at `base`; `Layer` was + /// only made with its whole map inside the table. + pub fn layer(&self, base: u32, v: u32) -> u32 { + self.cmap[base as usize + v as usize] + } + + /// Entry `i` of the colour map as an opaque `0xAABBGGRR` pixel; black past + /// the table. The frame reads whole windows of it (`index8`, `index12`). + #[cfg(test)] + pub fn colour(&self, i: usize) -> u32 { + self.cmap.get(i).copied().unwrap_or(BLACK) + } + + /// The 256 entries from `base` of an 8-bit window; `base + 256` is inside the + /// table, as `Window::Index8` was only made so. + pub fn index8(&self, base: u32) -> &[u32; 256] { + self.cmap[base as usize..base as usize + 256].try_into().expect("the window was checked against the table") + } + + /// The 4096 entries from `base` of a 12-bit window. + pub fn index12(&self, base: u32) -> &[u32; 4096] { + self.cmap[base as usize..base as usize + 4096].try_into().expect("the window was checked against the table") + } + + /// A direct-colour word `0x00RRGGBB` as an opaque `0xAABBGGRR` pixel, through + /// the gamma RAM. + pub fn direct(&self, word: u32) -> u32 { + BLACK | (self.lut[2][(word & 0xff) as usize] as u32) << 16 | (self.lut[1][((word >> 8) & 0xff) as usize] as u32) << 8 | self.lut[0][((word >> 16) & 0xff) as usize] as u32 + } + + /// The glyph cursor as the host draws a pointer: its 64 x 64 box's top-left and + /// which pixels are opaque (any of the pixel's source and mask bits, as + /// `draw_cursor` shows it). None with the cursor off or the crosshair, whose shape + /// is unknown, so the host keeps its own pointer. + pub fn hw_cursor(&self) -> Option { + let Some(c) = self.cursor.as_ref() else { + let d = self.dac_cursor.as_ref()?; + let mask = (0..super::dac_cursor::SIZE).map(|y| (0..super::dac_cursor::SIZE).fold(0u64, |m, x| m | ((d.cursor.pixel(x, y) >= 2) as u64) << x)).collect(); + return Some(crate::gfx_display::HwCursor { x: d.x0, y: d.y0, size: 64, mask }); + }; + let mask = (0..64usize) + .map(|r| { + let row = &c.rows[4 * r..4 * r + 4]; + (0..64usize).fold(0u64, |m, col| { + let bit = |hi: u32, lo: u32| if col < 32 { hi >> (31 - col) & 1 } else { lo >> (63 - col) & 1 }; + m | ((bit(row[0], row[1]) | bit(row[2], row[3])) as u64) << col + }) + }) + .collect(); + Some(crate::gfx_display::HwCursor { x: c.x0 as i32, y: c.y0 as i32, size: 64, mask }) + } + + /// Draw the glyph cursor into `out`, a frame `stride` pixels a row, top row + /// first, `w` x `h` of it the screen. A glyph is 64 rows of four words, the + /// first two the row's source bits and the next two its mask bits, most + /// significant bit leftmost; words 256 to 258 are the third colour, the + /// foreground and the background, 10:10:10. A pixel with only its mask bit + /// takes the background, only its source bit the foreground, both the third + /// colour, neither leaves the screen. Which bit pair means which colour is + /// inferred from one recorded arrow (white outline from the mask, red fill + /// from the source), as is the bit order. Colours go through the gamma RAM + /// as the colour map's do. + /// + /// Known gaps: the plan reads the glyph as a two-bit value picking colour word + /// `CURSOR_COLOUR_WORDS + (3 - v)` with no transparent value; this rule, with + /// (0, 0) leaving the screen, is what one recorded arrow showed, and the two + /// disagree about transparency. The crosshair (mode 1) is not drawn. The DAC + /// cursor is drawn by `draw_dac_cursor`. + pub fn draw_cursor(&self, out: &mut [u32], stride: usize, w: usize, h: usize) { + let Some(c) = &self.cursor else { return }; + for r in 0..64usize { + let y = c.y0 + r as i64; + if y < 0 || y >= h as i64 { + continue; + } + let row = &c.rows[4 * r..4 * r + 4]; + for col in 0..64usize { + let x = c.x0 + col as i64; + if x < 0 || x >= w as i64 { + continue; + } + let bit = |hi: u32, lo: u32| if col < 32 { hi >> (31 - col) & 1 } else { lo >> (63 - col) & 1 }; + let px = match (bit(row[0], row[1]), bit(row[2], row[3])) { + (0, 0) => continue, + (1, 0) => c.fg, + (0, 1) => c.bg, + _ => c.third, + }; + out[y as usize * stride + x as usize] = px; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn entries_load_at_their_address_and_narrow_to_eight_bits() { + let mut t = Tables::default(); + t.load_colours(0x2d00, &[0x3ff << 20, 0x3ff << 10, 0x3ff, 0x1ff << 20 | 0x1ff << 10 | 0x1ff]); + assert_eq!(t.snapshot().colour(0x2d00), 0xff00_00ff, "red is the low byte of the pixel"); + assert_eq!(t.snapshot().colour(0x2d01), 0xff00_ff00); + assert_eq!(t.snapshot().colour(0x2d02), 0xffff_0000); + assert_eq!(t.snapshot().colour(0x2d03), 0xff7f_7f7f, "511 of 1023 widens to 2047 of 4095, 127 of 255"); + t.load_colours(ENTRIES as u32 - 1, &[1, 2]); + assert_eq!(t.dropped, 1, "an entry past the table is dropped and counted"); + assert_eq!(t.snapshot().colour(ENTRIES + 5), 0xff00_0000); + } + + /// The table words the X server sent for the greeter: 8-bit windows at + /// 0x2c00, 0x2d00 and so on, 12-bit ones at 0x3800.., the popup a two-bit + /// window the model does not read. + #[test] + fn a_display_id_word_names_a_colour_map_and_an_index_width() { + let mut t = Tables::default(); + assert_eq!(t.snapshot().window(1), Window::Textport); + assert!(t.snapshot().is_default()); + t.load_did(1, 0x16803c); + t.load_did(6, 0x1c005c); + t.load_did(0x3f, 0x140808); + t.load_did(9, 0x7e); + t.load_mode(9, 8); + assert_eq!(t.snapshot().window(1), Window::Index8 { base: 0x2d00 }); + assert_eq!(t.snapshot().window(6), Window::Index12 { base: 0x3800 }); + assert_eq!(t.snapshot().window(0x3f), Window::Layer { base: 0x2810, planes: 2, opaque: false, bank: 0 }, "the popup: a 2-plane map"); + assert_eq!(t.snapshot().window(9), Window::Unsupported(Why::Luminance), "a 16-plane luminance window is counted, not read"); + // A window with no map offset that is not colour index holds colour: 0x38 with mode 4, 0x68 with mode 0. + t.load_did(20, 0x38); + t.load_mode(20, 4); + t.load_did(21, 0x68); + assert_eq!([t.snapshot().window(20), t.snapshot().window(21)], [Window::Direct { bank: 0 }; 2]); + t.load_bank(21, 1); + assert_eq!(t.snapshot().window(21), Window::Direct { bank: 1 }, "a swap names the buffer shown"); + assert!(!t.snapshot().is_default()); + t.load_did(2, 0); + assert_eq!(t.snapshot().window(2), Window::Cleared, "a word of zero is an ID the server cleared"); + assert!(t.describe().contains("7 display-ID entries (1 cleared, 1 not decoded: 1 Luminance)"), "{}", t.describe()); + } + + /// A mode word that says RGB makes the window direct colour, whatever the + /// table says; a colour-index mode with no table entry stays the default. + #[test] + fn a_mode_word_that_says_rgb_makes_the_window_direct() { + let mut t = Tables::default(); + t.load_mode(10, 4); + t.load_mode(11, 0x14); + t.load_mode(2, 8); + t.load_did(11, 0); + assert_eq!([t.snapshot().window(10), t.snapshot().window(11), t.snapshot().window(2)], [Window::Direct { bank: 0 }, Window::Direct { bank: 0 }, Window::Textport]); + assert_eq!(t.snapshot().direct(0x00_11_22_33), 0xff33_2211, "no gamma: the identity"); + t.load_did(99, 1); + t.load_mode(99, 4); + t.reset(); + assert!(t.snapshot().is_default()); + assert_eq!(t.describe(), Tables::default().describe()); + } +} + +#[cfg(test)] +mod gamma_tests { + use super::*; + + /// The gamma RAM maps a widened component to 12 bits and the table narrows + /// that to 8; a load past the first page is dropped and counted. + #[test] + fn the_gamma_ram_shapes_the_colour_map_and_direct_colour() { + let mut t = Tables::default(); + // Red halves, green is the identity, blue is zero. + let red: Vec = (0..4096).map(|i| i / 2).collect(); + let green: Vec = (0..4096).collect(); + t.load_gamma(0, &red); + t.load_gamma(0x1000, &green); + t.load_gamma(0x2000, &[0; 4096]); + assert!(!t.snapshot().is_default()); + t.load_colours(0, &[0x3ff << 20 | 0x3ff << 10 | 0x3ff]); + assert_eq!(t.snapshot().colour(0), 0xff00_ff7f, "red 4095 -> 2047 -> 127 of 255; green full; blue none"); + assert_eq!(t.snapshot().direct(0x00ff_ffff), 0xff00_ff7f); + t.load_gamma((1 << 19) - 1, &[1, 2]); + assert_eq!(t.dropped, 1, "the last entry fits, the next is past the address space"); + } +} + +#[cfg(test)] +mod cursor_tests { + use super::*; + + const RED: u32 = 0x3ff << 20; + const WHITE: u32 = 0x3fff_ffff; + const GREEN: u32 = 0x3ff << 10; + + fn glyph() -> Vec { + let mut g = vec![0u32; CURSOR_SLOT]; + // Row 0: pixel 0 source only, pixel 1 mask only. Row 1: pixel 0 both. + g[0] = 0x8000_0000; + g[2] = 0x4000_0000; + g[4] = 0x8000_0000; + g[6] = 0x8000_0000; + // A pixel in the right half of a row: the second word of each pair. + g[8 + 1] = 1; + g[256] = GREEN; + g[257] = RED; + g[258] = WHITE; + g + } + + /// A glyph cursor lands with its 64 x 64 box's corner at the position less + /// the 32 the driver adds; each bit pair picks a colour or leaves the pixel. + #[test] + fn a_glyph_cursor_is_drawn_where_its_registers_say() { + let mut t = Tables::default(); + t.load_cursor(260, &glyph()); + t.indirect(0x101, 260); + t.indirect(0x102, 34 | 33 << 13); + t.indirect(0x103, 2); + let (w, h) = (80usize, 80usize); + let mut out = vec![7u32; w * h]; + t.snapshot().draw_cursor(&mut out, w, w, h); + let at = |x: usize, y: usize| out[y * w + x]; + assert_eq!(at(2, 1), 0xff00_00ff, "source only: the foreground"); + assert_eq!(at(3, 1), 0xffff_ffff, "mask only: the background"); + assert_eq!(at(4, 1), 7, "neither: the screen"); + assert_eq!(at(2, 2), 0xff00_ff00, "both: the third colour"); + assert_eq!(at(2 + 63, 1 + 2), 0xff00_00ff, "the right half is the second word of each pair"); + assert_eq!(at(1, 1), 7); + // A cursor off, or the crosshair, draws nothing; a glyph off the frame is clipped. + t.indirect(0x103, 1); + let mut out = vec![7u32; w * h]; + t.snapshot().draw_cursor(&mut out, w, w, h); + assert!(out.iter().all(|&p| p == 7)); + t.indirect(0x103, 2); + t.indirect(0x102, 0); + t.snapshot().draw_cursor(&mut out, w, w, h); + assert_eq!(at_of(&out, w, 0, 0), 7, "its corner is 32 left of the frame"); + assert!(t.dropped == 0); + t.load_cursor(CURSOR_WORDS as u32 - 1, &[1, 2]); + assert_eq!(t.dropped, 1); + } + + fn at_of(out: &[u32], w: usize, x: usize, y: usize) -> u32 { + out[y * w + x] + } + + /// The host's pointer follows the registers: nothing with the cursor off or the + /// crosshair, the glyph's opaque pixels and top-left otherwise. + #[test] + fn the_host_cursor_is_read_from_the_registers() { + let mut t = Tables::default(); + assert_eq!(t.snapshot().hw_cursor(), None, "out of reset the cursor is off"); + t.load_cursor(260, &glyph()); + t.indirect(0x101, 260); + t.indirect(0x102, 34 | 33 << 13); + assert_eq!(t.snapshot().hw_cursor(), None, "loaded but not enabled"); + t.indirect(0x103, 1); + assert_eq!(t.snapshot().hw_cursor(), None, "the crosshair keeps the host's pointer"); + t.indirect(0x103, 2); + let c = t.snapshot().hw_cursor().expect("enabled"); + assert_eq!((c.x, c.y, c.size, c.mask.len()), (2, 1, 64, 64)); + assert_eq!(c.mask[0], 0b11, "row 0: pixel 0 source only and pixel 1 mask only are both opaque"); + assert_eq!(c.mask[1], 1, "row 1: pixel 0 both"); + assert_eq!(c.mask[2], 1 << 63, "row 2: the last pixel, in the second word of the pair"); + assert_eq!(c.mask[3], 0); + t.indirect(0x102, 0); + let c = t.snapshot().hw_cursor().expect("enabled"); + assert_eq!((c.x, c.y), (-32, -32), "the box starts 32 left of and above the frame"); + } + + /// A shown cursor takes the frame off the default composition, so it is + /// drawn even with no table loaded. + #[test] + fn a_shown_cursor_is_not_the_default() { + let mut t = Tables::default(); + assert!(t.snapshot().is_default()); + t.indirect(0x103, 2); + assert!(!t.snapshot().is_default()); + t.indirect(0x103, 0); + assert!(t.snapshot().is_default()); + } +} + +#[cfg(test)] +mod kind_tests { + use super::*; + + /// The kind comes from the entry and the mode byte together: the same + /// 8-plane entry is an index window with a colour-index mode, and an RGB one + /// when the mode byte says RGB or direct colour. + #[test] + fn the_mode_byte_and_the_entry_together_choose_the_kind() { + let mut t = Tables::default(); + t.load_did(1, 0x16803c); + assert_eq!(t.snapshot().window(1), Window::Index8 { base: 0x2d00 }); + t.load_mode(1, 8); + assert_eq!(t.snapshot().window(1), Window::Index8 { base: 0x2d00 }, "colour index says what the entry says"); + t.load_mode(1, 4); + assert_eq!(t.snapshot().window(1), Window::Direct { bank: 0 }, "RGB below 24 bits"); + t.load_mode(1, 0x10); + assert_eq!(t.snapshot().window(1), Window::Direct { bank: 0 }, "direct colour"); + // A 24-plane entry (field 7, no map offset) with no mode bits is an 8-plane index window. + t.load_did(2, 0x38); + assert_eq!(t.snapshot().window(2), Window::Index8 { base: 0 }); + // A 30-plane entry with no mode bits is RGB; with the colour-index bit it is not decoded. + t.load_did(3, 0x68); + assert_eq!(t.snapshot().window(3), Window::Direct { bank: 0 }); + t.load_mode(3, 8); + assert_eq!(t.snapshot().window(3), Window::Unsupported(Why::Planes(14))); + // 16-plane luminance, and a map that does not fit. + t.load_did(4, 0x7e); + assert_eq!(t.snapshot().window(4), Window::Unsupported(Why::Luminance)); + t.load_did(5, ((ENTRIES as u32 - 8) << 7) | 7 << 3); + assert_eq!(t.snapshot().window(5), Window::Unsupported(Why::Map)); + assert!(t.describe().contains("3 not decoded: 1 Luminance, 1 Planes(14), 1 Map"), "{}", t.describe()); + } + + /// The popup is ID 63 and overlays 64 to 95; an overlay is decoded from its + /// entry when its mode byte says non-RGB, counted when RGB, and opaque with + /// mode bit 4. The XMAP's indirect registers write the same table. + #[test] + fn popup_and_overlay_ids_decode_as_layers() { + let mut t = Tables::default(); + t.load_did(64, 0x1400 << 7 | 7 << 3); + t.load_mode(64, 0x05); + assert_eq!(t.snapshot().window(64), Window::Layer { base: 0x1400, planes: 8, opaque: false, bank: 0 }); + t.load_mode(64, 0x15); + assert_eq!(t.snapshot().window(64), Window::Layer { base: 0x1400, planes: 8, opaque: true, bank: 0 }); + t.load_mode(64, 0x04); + assert_eq!(t.snapshot().window(64), Window::Unsupported(Why::Rgb15Overlay), "an RGB overlay's transparency rule is unread"); + t.load_mode(64, 0x05); + t.load_did(65, 0x1500 << 7 | 3 << 3); + t.load_mode(65, 1); + assert_eq!(t.snapshot().window(65), Window::Layer { base: 0x1500, planes: 4, opaque: false, bank: 0 }); + t.load_bank(65, 1); + assert_eq!(t.snapshot().window(65), Window::Layer { base: 0x1500, planes: 4, opaque: false, bank: 1 }); + assert!(t.snapshot().has_layers()); + t.load_did(200, 1); + t.indirect(0x118, 66); + t.indirect(0x11c, 0x1600 << 7 | 1 << 3); + t.load_mode(66, 1); + assert_eq!(t.snapshot().window(66), Window::Layer { base: 0x1600, planes: 2, opaque: false, bank: 0 }, "the XMAP's index and data registers write the entry"); + t.load_did(67, 0x16 << 7 | 4 << 3); + t.load_mode(67, 1); + assert_eq!(t.snapshot().window(67), Window::Unsupported(Why::Planes(5))); + } +} + +#[cfg(test)] +mod page_tests { + use super::*; + + /// Gamma pages are chosen per channel by the set-up word's bits 3:0, loaded + /// at the address's bits 17:14; bit 5 of the word means no gamma. + #[test] + fn a_channel_picks_its_gamma_page_and_can_have_none() { + let mut t = Tables::default(); + let ident: Vec = (0..4096).collect(); + let half: Vec = (0..4096).map(|i| i / 2).collect(); + for ch in 0..3u32 { + t.load_gamma(ch << 12, &ident); + t.load_gamma(1 << 14 | ch << 12, &half); + } + let white = 0x3ff << 20 | 0x3ff << 10 | 0x3ff; + t.load_colours(0, &[white]); + assert_eq!(t.snapshot().colour(0), 0xffff_ffff, "page 0 is the identity"); + t.indirect(0x118, 0); + t.indirect(0x11d, 1); + assert_eq!(t.snapshot().colour(0), 0xff7f_7f7f, "page 1 halves"); + assert_eq!(t.snapshot().direct(0x00ff_ffff), 0xff7f_7f7f, "direct colour too"); + t.indirect(0x11d, 0x21); + assert_eq!(t.snapshot().colour(0), 0xffff_ffff, "bit 5: no gamma, whatever the page"); + // Another channel's set-up word is not the displayed channel's. + t.indirect(0x118, 1); + t.indirect(0x11d, 1); + assert_eq!(t.snapshot().colour(0), 0xffff_ffff); + t.load_gamma(1 << 19, &[1]); + assert_eq!(t.dropped, 1, "an address past the 19 bits is dropped"); + } +} diff --git a/src/dev/kona/back/gl/mod.rs b/src/dev/kona/back/gl/mod.rs new file mode 100644 index 000000000..616907756 --- /dev/null +++ b/src/dev/kona/back/gl/mod.rs @@ -0,0 +1,741 @@ +//! The GL client's commands: state into the context, triangles out. +//! +//! This is the GL half of what the geometry engines do with the stream. State +//! commands (`state`) update the context's `Gl`; a begin opens a primitive, +//! vertex commands transform, light and assemble it, and an end closes it. +//! Each primitive is clipped in clip space against the view volume by the GL +//! core (`crate::dev::gl`, with `vertex::KonaGeometry`), divided, put through +//! the viewport and quantised to the vertex bus's precision (`WVertex`), and +//! appended to a `Prims` as triangles with the draw state they run under. There +//! is no clip in window space: x and y are cut only at a guard band, and set-up +//! cuts the rest with the job's rectangle. +//! +//! Assembly. Triangles, strips, fans, quads, quad strips and polygons are broken +//! into independent triangles, a polygon of more than three vertices into one +//! fan from its first. That the engines split polygons this way is inferred; +//! what is established is that the bus carries strips and fans. Points and +//! lines are clipped and then drawn as triangles (a point as a square or a +//! disc, a line as a rectangle about its segment, with the distance along it for +//! the line stipple). A polygon whose face is in the line or point mode is drawn +//! as lines between its boundary-edge vertices or as points at them. +//! +//! Flat shading takes the colour of the vertex GL names (the last of a +//! triangle, the first of a polygon). Two-sided lighting picks the back colour +//! by the polygon's projected orientation. Culling is by that orientation +//! too. The half-pixel offset is not applied here (see `raster::setup`). +//! +//! The double forms of the colour, normal, texture-coordinate and vertex tokens +//! are narrowed to single precision once, as the command arrives, and run the +//! float forms' path (see `narrow`). +//! +//! User clip planes are tested in eye space with the core's clip planes 6 to 11, +//! along with the view volume, and a plane cuts a primitive wherever the view +//! volume would (see `GlPath::begin`). +//! +//! Texture coordinates are the current ones, or, for the coordinates enabled for +//! generation, computed from the vertex (`crate::dev::gl::texgen`), and then go +//! through the texture matrix (inferred: no source states the +//! arithmetic). The set of generated coordinates is read once, at +//! a primitive's begin. +//! +//! Not taken, counted by selector: the colour-index double form, the +//! generation parameters of the point and line distance planes, colour material by token, the +//! IRIS GL compatibility tokens, vertex arrays, display-list tokens and the +//! texture commands other than the coordinate and the environment's mode and +//! colour. Fog is taken: the state is kept, each vertex carries its fog +//! coordinate and the per-pixel stage applies it. Texture loads and binds are +//! not this module's: the back end hands them to `texture`, and a bound 1D or 2D +//! texture is sampled in `raster`. + +pub mod pixels; +pub mod query; +pub mod state; +pub mod texture; +pub mod vertex; + +use crate::dev::gl::math; +use crate::dev::kona::defs::{gl, *}; +use crate::dev::gl::vertex::{Clip, MAX_POLY}; +use super::raster::{ClearReq, Prims, Rect, SmoothPoint, SmoothPoly, WVertex}; +use state::{f, Gl, Taken}; +use std::collections::BTreeMap; +use vertex::{colour13, colour_word, depth32, grid, project, KVertex, KonaGeometry}; + +pub const BEGIN_FIRST: u32 = BEGIN_PRIMITIVE_POINTS; +pub const BEGIN_LAST: u32 = BEGIN_PRIMITIVE_POLYGON; +pub const END: u32 = END_OPEN_PRIMITIVE; +/// Selectors of every texture-coordinate form the front end frames. +pub const TEXCOORD: [u32; 5] = [SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT, SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT, SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT, SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT, SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT]; + +/// Whether `selector` is one of the commands `GlPath::command` is given. +pub fn is_geometry(selector: u32) -> bool { + matches!(selector, CLEAR_SELECTED_BUFFERS | SET_COLOUR_CLEAR_VALUE | SET_DEPTH_CLEAR_VALUE | SELECT_DRAW_BUFFER | SELECT_CURRENT_MATRIX_STACK..=TRANSLATE_FLOATS | SET_VIEWPORT | SET_SCISSOR_BOX | ENABLE_CAPABILITY | DISABLE_CAPABILITY | SET_FLAT_OR_SMOOTH_SHADING | SET_FACE_CULLING | SET_FRONT_FACE_WINDING | SET_DEPTH_TEST | SET_ALPHA_TEST | SET_STENCIL_TEST | SET_STENCIL_OPERATIONS | SET_STENCIL_WRITE_MASK | SET_BLEND_FACTORS | SET_CONSTANT_BLEND_COLOUR | SET_LOGIC_OPERATION | SET_COLOUR_WRITE_MASK | SET_DEPTH_WRITE_MASK | SET_POLYGON_OFFSET | SET_STENCIL_CLEAR_VALUE | SET_LINE_WIDTH | SET_POINT_SIZE | SET_LINE_STIPPLE | SET_MULTISAMPLE_PATTERN | SET_MULTISAMPLE_COVERAGE_MASK | SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM | SET_TEXTURE_ENVIRONMENT_PARAMETER_INTEGER_FORM | SET_LIGHT_PARAMETER_FLOAT_FORM | SET_LIGHT_PARAMETER_INTEGER_FORM | SET_LIGHTING_MODEL_PARAMETER_FLOAT_FORM | SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM | SET_MATERIAL_PARAMETER_FLOAT_FORM | BEGIN_FIRST..=BEGIN_LAST | END | SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_BYTE..=SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_32_BIT_INTEGER | SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_BYTE..=SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_DOUBLE | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_DOUBLE | SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_DOUBLE | SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_DOUBLE | SUBMIT_VERTEX_2_COMPONENTS_DOUBLE..=SUBMIT_VERTEX_4_COMPONENTS_SIGNED_SHORT | SET_USER_CLIP_PLANE | TEXTURE_COORDINATE_GENERATION_FLOAT_FORM | SET_CURRENT_EDGE_FLAG) +} + +/// The float form a double-precision colour, normal, texture-coordinate or vertex token runs as, +/// with its words narrowed to single precision. A double is two words, the high one first (the +/// order of the depth clear's double, established; for these tokens inferred); the word counts +/// are established for the texture coordinates and inferred for the colour, normal and +/// vertex, and a count that differs is not narrowed. The +/// colour double is narrowed and not scaled or clamped at entry (established; the clamp to 0 to 1 comes later, in the +/// colour-clamp pass). +fn narrow(selector: u32, a: &[u32]) -> Option<(u32, [u32; 4])> { + let float_form = match selector { + SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE => SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT, + SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE => SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, + SET_CURRENT_NORMAL_3_COMPONENTS_DOUBLE => SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT, + SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_DOUBLE => SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT, + SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_DOUBLE => SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT, + SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_DOUBLE => SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT, + SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_DOUBLE => SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT, + SUBMIT_VERTEX_2_COMPONENTS_DOUBLE => SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT, + SUBMIT_VERTEX_3_COMPONENTS_DOUBLE => SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, + SUBMIT_VERTEX_4_COMPONENTS_DOUBLE => SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT, + _ => return None, + }; + if a.len() % 2 != 0 || a.len() > 8 { + return None; + } + let mut out = [0u32; 4]; + for (o, w) in out.iter_mut().zip(a.chunks_exact(2)) { + *o = (f64::from_bits((w[0] as u64) << 32 | w[1] as u64) as f32).to_bits(); + } + Some((float_form, out)) +} + +/// A vertex as feedback reports it: window position, depth as a fraction, 1 / w, colour and texture. +fn feed_vertex(gl: &Gl, v: &KVertex) -> query::Fv { + let [vx, vy, vw, vh] = gl.raster.viewport; + let [dn, df] = gl.raster.depth_range; + let r = if v.h[3] != 0.0 { 1.0 / v.h[3] } else { 0.0 }; + query::Fv { x: vx + (v.h[0] * r + 1.0) * 0.5 * vw, y: vy + (v.h[1] * r + 1.0) * 0.5 * vh, z: dn + (v.h[2] * r * 0.5 + 0.5) * (df - dn), w: r, c: v.c, t: v.t } +} + +/// `v` with every edge a boundary edge. A triangle or polygon takes its edges from the vertices' flags. +/// A quad strip's pieces take the constant flag word 0xc1400000 whatever the flags (the constant is +/// established, its meaning, both outer edges of each triangle on and the diagonal off, is inferred), +/// which draws a quad's four sides. A triangle strip and fan combine their vertices' words with no +/// per-edge mask, and which of their edges draw in line mode is unknown: the model draws every edge of +/// a piece, inner ones too (its own choice). +fn all_edges(mut v: [KVertex; N]) -> [KVertex; N] { + v.iter_mut().for_each(|w| w.edge = true); + v +} + +/// What the path did, for the monitor and the census. +#[derive(Clone, Debug, Default)] +pub struct GlCounts { + pub commands: u64, + /// Selectors seen that are geometry commands but whose form is not taken. + pub unmodelled: BTreeMap, + /// glEnable and glDisable capabilities the path does not read. + pub caps: BTreeMap, + pub errors: u64, + pub begins: [u64; 10], + pub texcoord: [u64; 5], + pub vertices: u64, + pub primitives: u64, + pub clears: u64, + /// Primitives the volume clip cut, that vanished in it, that had a vertex + /// with no window position, and that culling dropped. + pub clipped: u64, + pub clipped_away: u64, + pub no_window_position: u64, + pub culled: u64, + /// Points and lines that survived clipping and were handed to set-up as + /// triangles, and lines of no length, which draw nothing. + pub points: u64, + pub lines: u64, + pub empty_lines: u64, + /// Window placements whose ID word is in none of the ranges the board encodes. + pub odd_window_ids: u64, + /// Vertices outside a vertex command's begin, which draw nothing. + pub stray: u64, + /// With `census` on: each vertex's place against the window, once. + pub behind: u64, + pub outside_volume: u64, + pub outside_window: u64, + pub inside: u64, +} + +impl GlCounts { + pub fn note(&mut self, selector: u32) { + *self.unmodelled.entry(selector).or_default() += 1; + } +} + +/// The GL path: the scratch it assembles in. +pub struct GlPath { + pub counts: GlCounts, + /// Measure every vertex against the window (an extra divide each). + #[cfg(test)] + pub census: bool, + /// The managed area, which a draw's clip rectangle is cut to. + screen: Rect, + clip: Clip, + /// The primitive's vertices so far, and the strip's or fan's held ones. + prim: Vec, + /// Vertices of the open primitive that have been taken. + seen: u32, + /// For the open primitive: whether texture coordinates are generated, and whether that reads the + /// normal. Decided at begin, so a vertex tests a flag and no more. + texgen: bool, + texgen_normal: bool, +} + +impl GlPath { + /// The managed area, which a draw's clip rectangle is cut to. + pub fn screen(&self) -> Rect { + self.screen + } + + /// A path for a managed area of `screen` pixels, width and height. + pub fn new(screen: (u32, u32)) -> GlPath { + GlPath { counts: GlCounts::default(), #[cfg(test)] census: false, screen: Rect { x0: 0, y0: 0, x1: screen.0 as i32, y1: screen.1 as i32 }, clip: Clip::default(), prim: Vec::with_capacity(64), seen: 0, texgen: false, texgen_normal: false } + } + + /// One GL command. `packed` is the opening word's top half, which the + /// byte, short and packed forms carry data in. + pub fn command(&mut self, gl: &mut Gl, selector: u32, a: &[u32], packed: u32, prims: &mut Prims) { + self.counts.commands += 1; + let narrowed = narrow(selector, a); + let (selector, a) = match &narrowed { + Some((float_form, w)) => (*float_form, &w[..a.len() / 2]), + None => (selector, a), + }; + let taken = match selector { + BEGIN_FIRST..=BEGIN_LAST => { + self.begin(gl, selector); + Taken::Yes + } + END => { + self.end(gl, prims); + Taken::Yes + } + CLEAR_SELECTED_BUFFERS if a.len() == 1 => { + self.clear(gl, a[0], prims); + Taken::Yes + } + SUBMIT_VERTEX_2_COMPONENTS_DOUBLE..=SUBMIT_VERTEX_4_COMPONENTS_SIGNED_SHORT => self.vertex(gl, selector, a, packed, prims), + SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_BYTE..=SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_32_BIT_INTEGER | SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_BYTE..=SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_EDGE_FLAG => self.attribute(gl, selector, a, packed), + SELECT_CURRENT_MATRIX_STACK..=TRANSLATE_FLOATS if selector != SET_DEPTH_RANGE => gl.matrix(selector, a), + _ => gl.state(selector, a), + }; + match taken { + Taken::Yes => {} + Taken::Unmodelled if matches!(selector, ENABLE_CAPABILITY | DISABLE_CAPABILITY) => *self.counts.caps.entry(a[0]).or_default() += 1, + Taken::Unmodelled => self.counts.note(selector), + Taken::Error(e) => { + self.counts.errors += 1; + if gl.error == 0 { + gl.error = e; + } + } + } + } + + /// glClear: the colour and depth planes of the window, to the clear values. + /// The mask's bits are OpenGL's (colour 0x4000, depth 0x100, stencil 0x400); + /// accumulation is not cleared yet. Each plane clears under its write mask. + fn clear(&mut self, gl: &mut Gl, mask: u32, prims: &mut Prims) { + self.counts.clears += 1; + let ds = gl.draw_state(self.screen); + prims.clears.push(ClearReq { + rect: ds.clip, + colour: (mask & gl::COLOR_BUFFER_BIT != 0).then(|| colour_word(gl.clear_colour)), + depth: (mask & gl::DEPTH_BUFFER_BIT != 0).then(|| depth32(gl.clear_depth)), + stencil: (mask & gl::STENCIL_BUFFER_BIT != 0).then_some(gl.clear_stencil as u8), + colour_mask: ds.colour_mask, + depth_mask: ds.depth_mask, + stencil_mask: ds.stencil.write_mask, + }); + prims.state = Some(ds); + } + + fn begin(&mut self, gl: &mut Gl, mode: u32) { + // `mode` is the begin token itself. + if gl.in_begin { + // GL keeps the first error until it is read. + if gl.error == 0 { + gl.error = gl::INVALID_OPERATION; + } + self.counts.errors += 1; + return; + } + gl.in_begin = true; + gl.prim = mode; + // The clip planes in force for this primitive, decided once here, not per vertex. + self.clip.user = gl.user_plane; + self.clip.user_on = gl.user_on; + self.texgen = (gl.raster.texture_2d || gl.raster.texture_3d) && gl.texgen.active(); + self.texgen_normal = self.texgen && gl.texgen.needs_normal(); + self.counts.begins[(mode - BEGIN_FIRST) as usize] += 1; + self.prim.clear(); + self.seen = 0; + } + + fn end(&mut self, gl: &mut Gl, prims: &mut Prims) { + if !gl.in_begin { + // GL keeps the first error until it is read. + if gl.error == 0 { + gl.error = gl::INVALID_OPERATION; + } + self.counts.errors += 1; + return; + } + match gl.prim { + BEGIN_PRIMITIVE_LINE_LOOP if self.prim.len() >= 2 => { + let (a, b) = (*self.prim.last().expect("two"), self.prim[0]); + self.line(gl, a, b, prims); + } + BEGIN_PRIMITIVE_POLYGON if self.prim.len() >= 3 => { + let p = std::mem::take(&mut self.prim); + self.polygon(gl, &p, 0, prims); + self.prim = p; + } + _ => {} + } + gl.in_begin = false; + self.prim.clear(); + } + + /// A colour, normal, texture coordinate or edge flag: the current values. + fn attribute(&mut self, gl: &mut Gl, selector: u32, a: &[u32], packed: u32) -> Taken { + let cur = |gl: &mut Gl, c: [f32; 4]| { + gl.color = c; + gl.light.track_color(c); + }; + match (selector, a.len()) { + (SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, 4) => cur(gl, [f(a[0]), f(a[1]), f(a[2]), f(a[3])]), + (SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT, 3) => cur(gl, [f(a[0]), f(a[1]), f(a[2]), 1.0]), + (SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_BYTE, 1) | (SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_BYTE, 1) => { + let b = |v: u32| (v & 0xff) as f32 / 255.0; + let alpha = if selector == SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_BYTE { b(a[0] >> 16) } else { 1.0 }; + cur(gl, [b(packed >> 24), b(packed >> 16), b(a[0] >> 24), alpha]); + } + (SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT, 3) => gl.normal = [f(a[0]), f(a[1]), f(a[2])], + (SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_32_BIT_INTEGER, 3) => gl.normal = [a[0] as i32 as f32 / 2147483647.0, a[1] as i32 as f32 / 2147483647.0, a[2] as i32 as f32 / 2147483647.0].map(|v| v.max(-1.0)), + (SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT, 1) | (SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT, 2) | (SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT, 3) | (SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT, 4) | (SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT, 1) => { + let i = TEXCOORD.iter().position(|&s| s == selector).expect("listed"); + self.counts.texcoord[i] += 1; + gl.texcoord = match selector { + SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT => [f(a[0]), 0.0, 0.0, 1.0], + SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT => [f(a[0]), f(a[1]), 0.0, 1.0], + SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT => [f(a[0]), f(a[1]), f(a[2]), 1.0], + SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT => [f(a[0]), f(a[1]), f(a[2]), f(a[3])], + // s << 16 | selector, then t << 16; shorts convert as integers. + _ => [(packed >> 16) as i16 as f32, (a[0] >> 16) as i16 as f32, 0.0, 1.0], + }; + } + (SET_CURRENT_EDGE_FLAG, 1) => gl.edge_flag = a[0] != 0, + _ => return Taken::Unmodelled, + } + Taken::Yes + } + + /// A vertex: transform, light, take into the open primitive. + fn vertex(&mut self, gl: &mut Gl, selector: u32, a: &[u32], packed: u32, prims: &mut Prims) -> Taken { + let p = match (selector, a.len()) { + (SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT, 2) => [f(a[0]), f(a[1]), 0.0, 1.0], + (SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, 3) => [f(a[0]), f(a[1]), f(a[2]), 1.0], + (SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT, 4) => [f(a[0]), f(a[1]), f(a[2]), f(a[3])], + (SUBMIT_VERTEX_2_COMPONENTS_SIGNED_32_BIT_INTEGER, 2) => [a[0] as i32 as f32, a[1] as i32 as f32, 0.0, 1.0], + (SUBMIT_VERTEX_3_COMPONENTS_SIGNED_32_BIT_INTEGER, 3) => [a[0] as i32 as f32, a[1] as i32 as f32, a[2] as i32 as f32, 1.0], + (SUBMIT_VERTEX_2_COMPONENTS_SIGNED_SHORT, 1) => [(packed >> 16) as i16 as f32, (a[0] >> 16) as i16 as f32, 0.0, 1.0], + (SUBMIT_VERTEX_3_COMPONENTS_SIGNED_SHORT, 1) => [(packed >> 16) as i16 as f32, (a[0] >> 16) as i16 as f32, a[0] as i16 as f32, 1.0], + _ => return Taken::Unmodelled, + }; + if !gl.in_begin { + self.counts.stray += 1; + return Taken::Yes; + } + self.counts.vertices += 1; + let v = self.transform(gl, p); + self.take(gl, v, prims); + Taken::Yes + } + + fn transform(&mut self, gl: &mut Gl, p: [f32; 4]) -> KVertex { + let e = math::xform(gl.mv.get(), p); + let h = math::xform(&*gl.proj.get(), e); + let (mut c, mut cb) = (gl.color, gl.color); + if gl.raster.lighting { + gl.refresh_normal_matrix(); + (c, cb) = gl.light.light_vertex(e, gl.normal, gl.color); + } + let t = if gl.raster.texture_2d || gl.raster.texture_3d { + let st = if self.texgen { + let n = if self.texgen_normal { gl.eye_normal() } else { [0.0; 3] }; + gl.texgen.generate(gl.texcoord, p, e, n) + } else { + gl.texcoord + }; + math::xform(gl.tex.get(), st) + } else { + [0.0; 4] + }; + let mut v = KVertex { h, e, c, cb, t, oc: 0, edge: gl.edge_flag }; + v.oc = self.clip.outcode_by::(&v); + #[cfg(test)] + if self.census { + self.measure(gl, &v); + } + v + } + + /// Where a vertex falls against the window, for the census (tests only). + #[cfg(test)] + fn measure(&mut self, gl: &Gl, v: &KVertex) { + let Some(p) = project(gl.raster.viewport, gl.raster.depth_range, v, false, false) else { + self.counts.behind += 1; + return; + }; + if v.oc != 0 { + self.counts.outside_volume += 1; + return; + } + let r = Self::window_rect(gl); + let (x, y) = (p.x as f32 / 256.0, p.y as f32 / 256.0); + if x < r[0] || x > r[2] || y < r[1] || y > r[3] { + self.counts.outside_window += 1; + } else { + self.counts.inside += 1; + } + } + + /// The window the census measures against: the scissor when it is on, else + /// the window the kernel placed. + #[cfg(test)] + fn window_rect(gl: &Gl) -> [f32; 4] { + let r = &gl.raster; + if r.scissor_test { + let s = r.scissor; + [s[0] as f32, s[1] as f32, (s[0] + s[2]) as f32, (s[1] + s[3]) as f32] + } else { + let c = gl.clip; + [(c.x0 - gl.origin[0]) as f32, (c.y0 - gl.origin[1]) as f32, (c.x1 - gl.origin[0]) as f32, (c.y1 - gl.origin[1]) as f32] + } + } + + /// Add a vertex to the open primitive and draw what it completes. + fn take(&mut self, gl: &mut Gl, v: KVertex, prims: &mut Prims) { + self.seen += 1; + let n = self.prim.len(); + match gl.prim { + BEGIN_PRIMITIVE_POINTS => self.point(gl, v, prims), + BEGIN_PRIMITIVE_INDEPENDENT_LINES => { + if n == 1 { + let a = self.prim[0]; + self.prim.clear(); + self.line(gl, a, v, prims); + return; + } + self.prim.push(v); + } + BEGIN_PRIMITIVE_LINE_LOOP | BEGIN_PRIMITIVE_LINE_STRIP => { + // A strip keeps the newest vertex; a loop keeps the first and the + // newest, the first for the segment that closes it. + if n >= 1 { + let a = *self.prim.last().expect("one"); + self.line(gl, a, v, prims); + } + self.prim.truncate(if gl.prim == BEGIN_PRIMITIVE_LINE_LOOP { 1 } else { 0 }); + self.prim.push(v); + } + BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES => { + self.prim.push(v); + if self.prim.len() == 3 { + let t = [self.prim[0], self.prim[1], self.prim[2]]; + self.prim.clear(); + self.polygon(gl, &t, 2, prims); + } + } + BEGIN_PRIMITIVE_TRIANGLE_STRIP => { + self.prim.push(v); + let n = self.prim.len(); + if n >= 3 { + let t = if n % 2 == 1 { [self.prim[n - 3], self.prim[n - 2], self.prim[n - 1]] } else { [self.prim[n - 2], self.prim[n - 3], self.prim[n - 1]] }; + self.polygon(gl, &all_edges(t), 2, prims); + if n > 3 { + self.prim.remove(0); + } + } + } + BEGIN_PRIMITIVE_TRIANGLE_FAN => { + self.prim.push(v); + if self.prim.len() >= 3 { + let t = [self.prim[0], self.prim[1], self.prim[2]]; + self.polygon(gl, &all_edges(t), 2, prims); + self.prim.remove(1); + } + } + BEGIN_PRIMITIVE_INDEPENDENT_QUADS => { + self.prim.push(v); + if self.prim.len() == 4 { + let q = [self.prim[0], self.prim[1], self.prim[2], self.prim[3]]; + self.prim.clear(); + self.polygon(gl, &q, 3, prims); + } + } + BEGIN_PRIMITIVE_QUAD_STRIP => { + self.prim.push(v); + if self.prim.len() >= 4 && self.prim.len() % 2 == 0 { + let n = self.prim.len(); + let q = [self.prim[n - 4], self.prim[n - 3], self.prim[n - 1], self.prim[n - 2]]; + self.polygon(gl, &all_edges(q), 3, prims); + self.prim.drain(..2); + } + } + _ => { + if self.prim.len() < MAX_POLY - 12 { + self.prim.push(v); + } + } + } + } + + fn point(&mut self, gl: &mut Gl, v: KVertex, prims: &mut Prims) { + self.counts.primitives += 1; + if v.oc != 0 { + self.counts.clipped_away += 1; + return; + } + if gl.feed.mode != query::RENDER { + let fv = feed_vertex(gl, &v); + gl.feed.primitive(query::POINT, &[fv]); + return; + } + self.emit_point(gl, &v, false, prims); + } + + /// A point inside the volume, handed to set-up as a square or a disc. `of_polygon` is a vertex of a + /// polygon drawn in the point mode, which follows the polygon-smooth switch and not the point one. + fn emit_point(&mut self, gl: &mut Gl, v: &KVertex, of_polygon: bool, prims: &mut Prims) { + let Some(p) = project(gl.raster.viewport, gl.raster.depth_range, v, false, gl.raster.fog.on) else { + self.counts.no_window_position += 1; + return; + }; + self.counts.points += 1; + let size = gl.raster.point_size.max(0.0); + let (cx, cy) = (p.x as f32 / 256.0, p.y as f32 / 256.0); + let at = |dx: f32, dy: f32| WVertex { x: grid(cx + dx), y: grid(cy + dy), ..p }; + let mut ds = gl.draw_state(self.screen); + // Polygon offset reaches a polygon drawn as points under its own enable, and never a point + // primitive (established). The model has no enable for the polygon-mode case, so neither is offset. + ds.offset = None; + let smooth = if of_polygon { ds.smooth.polygons } else { ds.smooth.points }; + if smooth && size > 0.0 { + // Smoothed: the square about the disc, whose pixels `raster::smooth` weighs by the share of + // each the disc covers. Its centre is the vertex's position as the sample plane has it. + let r = size / 2.0 + 0.5; + let quad = [at(-r, -r), at(r, -r), at(r, r), at(-r, r)]; + prims.discs.push(SmoothPoint { quad, x: cx, y: cy, r: size / 2.0 }); + } else if size <= POINT_PIXEL_MAX { + // One pixel: a unit square about the vertex, which holds exactly one sample. + let (a, b, c, d) = (at(-0.5, -0.5), at(0.5, -0.5), at(0.5, 0.5), at(-0.5, 0.5)); + prims.tris.extend([[a, b, c], [a, c, d]]); + } else { + // A disc: sixteen triangles about the centre (Iris's own form). + let r = size / 2.0; + let rim = |k: usize| { + let t = k as f32 * std::f32::consts::TAU / 16.0; + at(r * t.cos(), r * t.sin()) + }; + prims.tris.extend((0..16).map(|k| [at(0.0, 0.0), rim(k), rim(k + 1)])); + } + prims.state = Some(ds); + } + + fn line(&mut self, gl: &mut Gl, mut a: KVertex, b: KVertex, prims: &mut Prims) { + self.counts.primitives += 1; + if gl.raster.flat { + a.c = b.c; + } + let any = a.oc | b.oc; + let (a, b) = if any != 0 { + self.counts.clipped += 1; + match self.clip.clip_line_by::(a, b, any) { + Some(p) => p, + None => { + self.counts.clipped_away += 1; + return; + } + } + } else { + (a, b) + }; + if gl.feed.mode != query::RENDER { + let (fa, fb) = (feed_vertex(gl, &a), feed_vertex(gl, &b)); + gl.feed.primitive(query::LINE, &[fa, fb]); + return; + } + self.emit_line(gl, &a, &b, false, prims); + } + + /// A segment inside the volume, handed to set-up as the rectangle about it. + /// A line inside the volume. `of_polygon` is an edge of a polygon drawn in the line mode, which follows + /// the polygon-smooth switch and not the line one (inferred). + fn emit_line(&mut self, gl: &mut Gl, a: &KVertex, b: &KVertex, of_polygon: bool, prims: &mut Prims) { + let (vp, dr, fog) = (gl.raster.viewport, gl.raster.depth_range, gl.raster.fog.on); + let (Some(pa), Some(pb)) = (project(vp, dr, a, false, fog), project(vp, dr, b, false, fog)) else { + self.counts.no_window_position += 1; + return; + }; + let (ax, ay, bx, by) = (pa.x as f32 / 256.0, pa.y as f32 / 256.0, pb.x as f32 / 256.0, pb.y as f32 / 256.0); + let (dx, dy) = (bx - ax, by - ay); + let len = (dx * dx + dy * dy).sqrt(); + if !(len > 0.0) { + self.counts.empty_lines += 1; + return; + } + self.counts.lines += 1; + // The rectangle of half-width w / 2 about the segment: two triangles; `d` is the + // distance along the line, restarting at each segment (a strip's stipple does not + // continue across segments here). + let h = gl.raster.line_width.max(0.0) / 2.0; + let (nx, ny) = (-dy / len * h, dx / len * h); + let corner = |base: &WVertex, x: f32, y: f32, d: f32| WVertex { x: grid(x), y: grid(y), d, ..*base }; + let (a0, a1) = (corner(&pa, ax + nx, ay + ny, 0.0), corner(&pa, ax - nx, ay - ny, 0.0)); + let (b0, b1) = (corner(&pb, bx + nx, by + ny, len), corner(&pb, bx - nx, by - ny, len)); + let mut ds = gl.draw_state(self.screen); + // Polygon offset reaches a polygon drawn as lines under its own enable, and never a line + // primitive (established). The model has no enable for the polygon-mode case, so neither is offset. + ds.offset = None; + if if of_polygon { ds.smooth.polygons } else { ds.smooth.lines } { + // Smoothed: the rectangle whole, for set-up to weigh by coverage. + let start = prims.aa_verts.len() as u32; + prims.aa_verts.extend([a0, a1, b1, b0]); + prims.aa.push(SmoothPoly { start, n: 4, line: true }); + } else { + prims.lines.extend([[a0, a1, b1], [a0, b1, b0]]); + } + prims.state = Some(ds); + } + + /// A convex polygon of three or four or more vertices, whose flat colour is + /// vertex `prov`'s. + fn polygon(&mut self, gl: &mut Gl, poly: &[KVertex], prov: usize, prims: &mut Prims) { + // The common primitive, a triangle or a quad wholly inside the volume, + // needs workspace for four vertices, not for the most a clip can make. + if poly.len() <= 4 && poly.iter().all(|w| w.oc == 0) { + self.polygon_in::<4>(gl, poly, prov, prims); + } else { + self.polygon_in::(gl, poly, prov, prims); + } + } + + /// `polygon` with room for `K` vertices through the volume clip. + fn polygon_in(&mut self, gl: &mut Gl, poly: &[KVertex], prov: usize, prims: &mut Prims) { + self.counts.primitives += 1; + let n = poly.len(); + let mut v = [KVertex::default(); K]; + v[..n].copy_from_slice(poly); + if gl.raster.flat { + let (c, cb) = (v[prov].c, v[prov].cb); + v[..n].iter_mut().for_each(|w| (w.c, w.cb) = (c, cb)); + } + let any = v[..n].iter().fold(0, |o, w| o | w.oc); + let mut n = n; + if any != 0 { + self.counts.clipped += 1; + let mut tmp = [KVertex::default(); K]; + n = self.clip.clip_polygon_by::(&mut v, &mut tmp, n, any).0; + if n < 3 { + self.counts.clipped_away += 1; + return; + } + } + let tex = gl.raster.texture_2d || gl.raster.texture_3d; + let (vp, dr, fog) = (gl.raster.viewport, gl.raster.depth_range, gl.raster.fog.on); + let mut p = [WVertex::default(); K]; + for i in 0..n { + let Some(q) = project(vp, dr, &v[i], tex, fog) else { + self.counts.no_window_position += 1; + return; + }; + p[i] = q; + } + // Orientation: positive area is counter-clockwise in a window whose y + // runs up, front-facing unless the front face is clockwise (OpenGL's + // own convention). The recorded demos set the cull face to back (0x405, + // 23,484 times) and the front face twice in the whole recording (once + // each way), so what is culled is the back faces of a counter-clockwise + // front; which face the hardware calls back is not tested. The area is + // that of the quantised positions, the ones the triangles are drawn from. + let area: i64 = (0..n).map(|i| p[i].x as i64 * p[(i + 1) % n].y as i64 - p[(i + 1) % n].x as i64 * p[i].y as i64).sum(); + let back = (area < 0) != gl.raster.front_cw; + let r = &gl.raster; + // A polygon of no area faces neither way: it is not culled, and set-up counts it as having no area. + if area != 0 && r.cull && (r.cull_mode == gl::FRONT_AND_BACK || (r.cull_mode == gl::FRONT) != back) { + self.counts.culled += 1; + return; + } + // A feedback or select pass takes what survived clipping and culling, and draws nothing. + if gl.feed.mode != query::RENDER { + let fvs: Vec = v[..n].iter().map(|w| feed_vertex(gl, w)).collect(); + gl.feed.primitive(query::POLYGON, &fvs); + return; + } + // The pipe subtracts the line mode from the face's mode: zero is line, below is point and above is + // fill, whatever the value (established), so an unchecked mode word still draws as one of the three. + let drawn = (gl.raster.polygon_mode[back as usize] as i32).wrapping_sub(gl::LINE as i32); + if drawn <= 0 { + self.outline(gl, &v[..n], back, if drawn == 0 { gl::LINE } else { gl::POINT }, prims); + return; + } + if back && gl.light.two_sided != 0 { + for i in 0..n { + p[i].c = v[i].cb.map(colour13); + } + } + // A fan from the first vertex. A smoothed polygon goes whole, for set-up to cut into the fan and + // weigh by coverage (`raster::smooth`). + if gl.draw_state(self.screen).smooth.polygons { + let start = prims.aa_verts.len() as u32; + prims.aa_verts.extend_from_slice(&p[..n]); + prims.aa.push(SmoothPoly { start, n: n as u32, line: false }); + } else { + for i in 1..n - 1 { + prims.tris.push([p[0], p[i], p[i + 1]]); + } + } + prims.state = Some(gl.draw_state(self.screen)); + } +} + +impl GlPath { + /// A polygon drawn as its boundary edges (`mode` is `gl::LINE`) or its vertices (`gl::POINT`), + /// from the polygon after clipping and culling. An edge or a vertex is drawn when its vertex's + /// edge flag is set: the flag of the vertex an edge starts from, and one clipping added is set + /// (the shared clipper's rule). Lines and points take the line and point state, not the + /// polygon's: width, size and stipple, and the polygon stipple does not apply. The colours are + /// the face's, as for a fill. Smoothing follows the polygon-smooth switch, not the line and point + /// ones (inferred). The pipe applies polygon offset to these modes under their own enables + /// (established); the model keeps no enables for them and applies none (a limit). + fn outline(&mut self, gl: &mut Gl, v: &[KVertex], back: bool, mode: u32, prims: &mut Prims) { + let n = v.len(); + let back_colour = back && gl.light.two_sided != 0; + let face = |w: &KVertex| { + let mut w = *w; + if back_colour { + w.c = w.cb; + } + w + }; + for i in 0..n { + if !v[i].edge { + continue; + } + if mode == gl::POINT { + let a = face(&v[i]); + self.emit_point(gl, &a, true, prims); + } else { + let (a, b) = (face(&v[i]), face(&v[(i + 1) % n])); + self.emit_line(gl, &a, &b, true, prims); + } + } + } +} + +#[cfg(test)] +mod tests; diff --git a/src/dev/kona/back/gl/pixels.rs b/src/dev/kona/back/gl/pixels.rs new file mode 100644 index 000000000..d1c310ee9 --- /dev/null +++ b/src/dev/kona/back/gl/pixels.rs @@ -0,0 +1,1505 @@ +//! GL pixel operations: the pixel-store, transfer and zoom state, draw, read and copy of +//! pixel rectangles, bitmaps and the accumulation buffer. +//! +//! The tokens and their layouts are the definitions table's section on GL pixel +//! operations. What the table grades established is taken as stated. Where it grades +//! a thing inferred or unknown, this model makes the simplest choice and says so at the +//! choice; each is counted in `Counts`, which `kona status` shows. The choices: +//! +//! - The pixel path is the standard OpenGL one: unpack, scale and bias, index shift and +//! offset, the pixel maps, then a fragment at each destination pixel (zoom by replication) +//! through the fragment operations after ownership and the stipples, with the raster +//! position's colour and depth. The table records the pipe's state but not which stages +//! it runs, so the stages are GL's (approximated). Post-convolution and post-colour-matrix +//! scale and bias are stored and not applied (counted). +//! - A packed type's component layout is the public one (the table does not record the +//! pipe's unpacker). Format 8 (the ABGR extension) is read and written with its components +//! in the order alpha, blue, green, red, in the plain types only. Formats 9 and 10 (CMYK and +//! CMYKA) are sized by the pipe's component table (four and five components) so the words +//! that follow a header are consumed, and refused. Type codes 14 to 20 are refused (their +//! element sizes are known, their bit layouts are not). +//! - A colour-index image has no index window to land in: it goes through the +//! index-to-colour pixel maps when they are loaded, else as an eight-bit grey (the +//! model's own). +//! - Accumulation keeps one float per component in the layout's accumulation words (three +//! words, or four with alpha). The storage format in frame-buffer memory is unknown, so the +//! floats are the model's own. An operation is one pass, except that a value of exactly 1.0 +//! (which does not fit the 13-bit value field, inferred) runs accumulate and add twice at 0.5, +//! load as a load at 0.5 then an accumulate at 0.5, and multiply by 1.0 as nothing; a return +//! is always one pass (established). A return writes the draw buffers under the colour +//! mask and ownership, and no blend or test. +//! - A read's reply is the packed image at offset 8 of the return page when it fits +//! `READ_REPLY_LIMIT` bytes, led by the destination's byte offset in its first word. A +//! read that would go by DMA, a video-source copy and the convolution queries' wider +//! replies are not modelled (counted). +//! - The overlap query (token 0x110) answers whether the copy's source and destination +//! rectangles meet. The convolution overlap query's reply (0x115) is that bit and nothing +//! else; the single-component query (0x116) answers a zero word (the table says it is a +//! result record's mode word, whose contents are unknown). +//! - The table's word-count column gives tokens 0x005, 0x006, 0x03a and 0x111 seven, seven, +//! seven and one words. The front end frames them with eight, nine, eight and two, which is +//! what the recording and the table's text show (the recording's bitmaps are eight words +//! and an image); the model follows the framer. + +use super::state::{f, Gl}; +use super::vertex::{colour13, depth32}; +use crate::dev::gl::math; +use crate::dev::kona::back::raster::ops::ownership_plane; +use crate::dev::kona::back::raster::pixel::Planes; +use crate::dev::kona::back::raster::DrawState; +use crate::dev::kona::back::store::{PixelStore, WordArray, DID_PLANE}; +use crate::dev::kona::defs::pixel::*; +use crate::dev::kona::defs::*; +use crate::dev::kona::stream::Stream; +use std::collections::BTreeMap; + +/// Most values a pixel map keeps. The pipe's and the client's table sizes are unknown (model cap). +pub const MAP_LIMIT: u32 = 1 << 16; + +/// A pixel-store record: the unpack side's or the pack side's. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Record { + pub swap_bytes: bool, + pub lsb_first: bool, + pub row_length: u32, + pub skip_rows: u32, + pub skip_pixels: u32, + pub alignment: u32, + /// Skip images, image height, skip volumes and image depth, kept and unused (no volumes). + pub volume: [u32; 4], +} + +impl Default for Record { + fn default() -> Self { + Record { swap_bytes: false, lsb_first: false, row_length: 0, skip_rows: 0, skip_pixels: 0, alignment: 4, volume: [0; 4] } + } +} + +impl Record { + /// The record display-list images are read with: unpadded. + fn list() -> Record { + Record { alignment: 1, ..Record::default() } + } + + /// Set the field a name in a group (0 to 5 from the group's base) or a volume name + /// stands for. An alignment other than 1, 2, 4 and 8 is stored and gives the pipe a zero + /// mask (inferred from its table), which `mask` returns. + fn set(&mut self, index: u32, value: u32) { + match index { + 0 => self.swap_bytes = value != 0, + 1 => self.lsb_first = value != 0, + 2 => self.row_length = value, + 3 => self.skip_rows = value, + 4 => self.skip_pixels = value, + 5 => self.alignment = value, + _ => {} + } + } + + fn mask(&self) -> usize { + match self.alignment { + 2 => 1, + 4 => 3, + 8 => 7, + _ => 0, + } + } + + fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { swap_bytes: _, lsb_first: _, row_length: _, skip_rows: _, skip_pixels: _, alignment: _, volume: _ } = self; + s.bool(&mut self.swap_bytes); + s.bool(&mut self.lsb_first); + s.u32(&mut self.row_length); + s.u32(&mut self.skip_rows); + s.u32(&mut self.skip_pixels); + s.u32(&mut self.alignment); + s.u32s(&mut self.volume); + } +} + +/// A pixel map's values and the form they arrived in (float, or an unsigned integer of 32 or 16 bits, +/// which a colour lookup divides down to 0 to 1). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Map { + pub max: f32, + pub values: Vec, +} + +impl Map { + fn at(&self, v: f32) -> f32 { + let n = self.values.len(); + let i = (v.clamp(0.0, 1.0) * (n - 1) as f32 + 0.5) as usize; + let raw = self.values[i.min(n - 1)]; + if self.max > 0.0 { raw / self.max } else { raw } + } + + /// An integer lookup (index or stencil), clamped to the table. + fn index(&self, i: i64) -> i64 { + let n = self.values.len() as i64; + self.values[i.clamp(0, n - 1) as usize] as i64 + } +} + +/// The current raster position. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RasterPos { + pub valid: bool, + /// Window x and y, and depth as 32 bits. + pub x: f32, + pub y: f32, + pub z: u32, + pub colour: [f32; 4], +} + +impl Default for RasterPos { + fn default() -> Self { + RasterPos { valid: true, x: 0.0, y: 0.0, z: 0, colour: [1.0; 4] } + } +} + +/// What a context's pixel operations keep. +#[derive(Clone, Debug, PartialEq)] +pub struct Pixels { + pub unpack: Record, + pub pack: Record, + pub scale: [f32; 4], + pub bias: [f32; 4], + pub depth_scale: f32, + pub depth_bias: f32, + pub index_shift: i32, + pub index_offset: i32, + pub map_colour: bool, + pub map_stencil: bool, + /// Post-convolution scales then biases, post-colour-matrix scales then biases. + pub post: [f32; 16], + pub zoom: [f32; 2], + pub maps: BTreeMap, + /// Between a suspend's save and its restore the fixed-function stages are off. + pub suspended: bool, + /// Which record a suspend switched the unpack side to: 0 the context's own. + pub records: u32, + /// The read buffer: 1 front, 2 back; None is the draw default. + pub read: Option, + pub pos: RasterPos, + pub accum_clear: [f32; 4], + /// A draw announced by DMA waits here for its packet: the header words. + pub pending: Option<[u32; 8]>, +} + +impl Default for Pixels { + fn default() -> Self { + let mut post = [0.0; 16]; + post[..4].fill(1.0); + post[8..12].fill(1.0); + Pixels { + unpack: Record::default(), + pack: Record::default(), + scale: [1.0; 4], + bias: [0.0; 4], + depth_scale: 1.0, + depth_bias: 0.0, + index_shift: 0, + index_offset: 0, + map_colour: false, + map_stencil: false, + post, + zoom: [1.0; 2], + maps: BTreeMap::new(), + suspended: false, + records: 0, + read: None, + pos: RasterPos::default(), + accum_clear: [0.0; 4], + pending: None, + } + } +} + +impl Pixels { + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { unpack: _, pack: _, scale: _, bias: _, depth_scale: _, depth_bias: _, index_shift: _, index_offset: _, map_colour: _, map_stencil: _, post: _, zoom: _, maps: _, suspended: _, records: _, read: _, pos: _, accum_clear: _, pending: _ } = self; + self.unpack.visit(s); + self.pack.visit(s); + s.f32s(&mut self.scale); + s.f32s(&mut self.bias); + s.f32(&mut self.depth_scale); + s.f32(&mut self.depth_bias); + s.i32(&mut self.index_shift); + s.i32(&mut self.index_offset); + s.bool(&mut self.map_colour); + s.bool(&mut self.map_stencil); + s.f32s(&mut self.post); + s.f32s(&mut self.zoom); + let mut n = self.maps.len() as u32; + s.u32(&mut n); + if s.loading() { + self.maps.clear(); + for _ in 0..n.min(64) { + if !s.ok() { + break; + } + let (mut name, mut max, mut bits) = (0u32, 0f32, Vec::new()); + s.u32(&mut name); + s.f32(&mut max); + s.vec_u32(&mut bits); + self.maps.insert(name, Map { max, values: bits.into_iter().map(f32::from_bits).collect() }); + } + } else { + for (name, m) in &self.maps { + let (mut name, mut max, mut bits) = (*name, m.max, m.values.iter().map(|v| v.to_bits()).collect::>()); + s.u32(&mut name); + s.f32(&mut max); + s.vec_u32(&mut bits); + } + } + s.bool(&mut self.suspended); + s.u32(&mut self.records); + s.opt(&mut self.read, |s, v| s.u32(v)); + s.bool(&mut self.pos.valid); + s.f32(&mut self.pos.x); + s.f32(&mut self.pos.y); + s.u32(&mut self.pos.z); + s.f32s(&mut self.pos.colour); + s.f32s(&mut self.accum_clear); + let mut has = self.pending.is_some(); + s.bool(&mut has); + if has { + let mut h = self.pending.unwrap_or_default(); + s.u32s(&mut h); + self.pending = Some(h); + } else { + self.pending = None; + } + } + + /// The unpack record in force: a suspend can switch to a fixed one. + pub fn unpack_in_force(&self) -> Record { + match self.records { + SUSPEND_USE_DEFAULT_RECORDS => Record::default(), + SUSPEND_USE_LIST_RECORD => Record::list(), + _ => self.unpack, + } + } +} + +/// What the pixel operations did, for `kona status`. +#[derive(Clone, Debug, Default)] +pub struct Counts { + pub draws: u64, + pub reads: u64, + pub copies: u64, + pub bitmaps: u64, + pub accumulations: u64, + pub fragments: u64, + /// Operations refused for a format or type the model does not know, or an image too short. + pub refused: u64, + /// Draws, copies and bitmaps with an invalid raster position, which draw nothing. + pub invalid_position: u64, + /// Reads that would go by DMA, which are not modelled. + pub read_by_dma: u64, + /// Queries answered with a model choice (overlap, convolution overlap, single component). + pub queries: u64, + /// Transfers that had post-convolution or post-colour-matrix terms set, which are not applied. + pub post_stages_ignored: u64, + /// Colour-index images drawn without an index-to-colour map. + pub index_without_map: u64, + /// Pixel maps longer than `MAP_LIMIT`, or named in none of the groups. + pub maps_refused: u64, + /// Parameter names the transfer or store tokens did not recognise. + pub unknown_names: u64, + /// Tokens nothing sends (the float and integer sibling forms), seen anyway. + pub unsent: u64, + /// Current-raster-position token 0x10e, whose words the table does not describe. + pub list_positions: u64, + /// Draw announced by DMA whose packet arrived. + pub dma_draws: u64, +} + +impl Counts { + pub fn render(&self) -> String { + format!( + "{} draws ({} by DMA), {} reads, {} copies, {} bitmaps, {} accumulations; {} fragments; {} refused, {} with no valid raster position, {} reads by DMA not modelled, {} queries answered by the model, {} transfers with post stages not applied, {} index images without a map, {} maps refused, {} unknown names, {} unsent tokens, {} list positions", + self.draws, self.dma_draws, self.reads, self.copies, self.bitmaps, self.accumulations, self.fragments, self.refused, self.invalid_position, self.read_by_dma, self.queries, self.post_stages_ignored, self.index_without_map, self.maps_refused, self.unknown_names, self.unsent, self.list_positions + ) + } +} + +/// The tokens `command` takes. +pub fn is_state(selector: u32) -> bool { + matches!( + selector, + SET_PIXEL_STORE_PARAMETER_INTEGER_FORM + | SET_PIXEL_STORE_PARAMETER_FLOAT_FORM + | SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM + | SET_PIXEL_TRANSFER_PARAMETER_INTEGER_FORM + | SET_PIXEL_ZOOM_FACTORS + | SUSPEND_PIXEL_MODE_STATE_AROUND_TRANSFER + | LOAD_PIXEL_MAP_FLOAT_VALUES + | LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES + | LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES + | SELECT_READ_BUFFER + | SET_ACCUMULATION_BUFFER_CLEAR_VALUE + | SET_RASTER_POSITION_2_COMPONENTS_DOUBLE..=SET_RASTER_POSITION_4_COMPONENTS_SHORT + | SET_CURRENT_RASTER_POSITION + ) +} + +/// The tokens whose data run `Job::Data` brings back. +pub fn is_data(selector: u32) -> bool { + matches!(selector, LOAD_PIXEL_MAP_FLOAT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES | DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA | DRAW_BITMAP_FROM_DISPLAY_LIST | DRAW_BITMAP) +} + +/// The tokens that read or write the planes: they wait for the drawing queued before them. +pub fn is_plane(selector: u32) -> bool { + matches!(selector, DRAW_BITMAP_FROM_DISPLAY_LIST | DRAW_BITMAP | DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA | COPY_PIXEL_RECTANGLE_WITHIN_FRAME_BUFFER | READ_PIXEL_RECTANGLE_BACK_TO_HOST | ACCUMULATION_BUFFER_OPERATION | ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION | ASK_WHETHER_CONVOLUTION_COPY_OVERLAPS | RESOLVE_WHETHER_PIXEL_FORMAT_IS_SINGLE_COMPONENT) +} + +/// Pixel-state tokens: stored in the context, no plane touched. False for a token not +/// in `is_state`. (The stored values are GL's names and values as the client sends them.) +pub fn command(gl: &mut Gl, c: &mut Counts, selector: u32, a: &[u32]) -> bool { + let p = &mut gl.pix; + match (selector, a) { + (SET_PIXEL_STORE_PARAMETER_INTEGER_FORM, &[name, value]) => { + let g = |base: u32| (name >= base && name < base + STORE_NAMES).then(|| name - base); + match name { + UNPACK_SKIP_IMAGES | UNPACK_IMAGE_HEIGHT | UNPACK_SKIP_VOLUMES | UNPACK_IMAGE_DEPTH => p.unpack.volume[[UNPACK_SKIP_IMAGES, UNPACK_IMAGE_HEIGHT, UNPACK_SKIP_VOLUMES, UNPACK_IMAGE_DEPTH].iter().position(|&n| n == name).unwrap_or(0)] = value, + PACK_SKIP_IMAGES | PACK_IMAGE_HEIGHT | PACK_SKIP_VOLUMES | PACK_IMAGE_DEPTH => p.pack.volume[[PACK_SKIP_IMAGES, PACK_IMAGE_HEIGHT, PACK_SKIP_VOLUMES, PACK_IMAGE_DEPTH].iter().position(|&n| n == name).unwrap_or(0)] = value, + _ => match (g(UNPACK_BASE), g(PACK_BASE)) { + (Some(i), _) => p.unpack.set(i, value), + (_, Some(i)) => p.pack.set(i, value), + _ => c.unknown_names += 1, + }, + } + true + } + (SET_PIXEL_STORE_PARAMETER_FLOAT_FORM | SET_PIXEL_TRANSFER_PARAMETER_INTEGER_FORM, _) => { + c.unsent += 1; + true + } + (SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM, &[name, value]) => { + let v = f(value); + match name { + RED_SCALE => p.scale[0] = v, + GREEN_SCALE => p.scale[1] = v, + BLUE_SCALE => p.scale[2] = v, + ALPHA_SCALE => p.scale[3] = v, + RED_BIAS => p.bias[0] = v, + GREEN_BIAS => p.bias[1] = v, + BLUE_BIAS => p.bias[2] = v, + ALPHA_BIAS => p.bias[3] = v, + DEPTH_SCALE => p.depth_scale = v, + DEPTH_BIAS => p.depth_bias = v, + // The client rounds half away from zero and sends an integer. + INDEX_SHIFT => p.index_shift = value as i32, + INDEX_OFFSET => p.index_offset = value as i32, + MAP_COLOUR => p.map_colour = value != 0, + MAP_STENCIL => p.map_stencil = value != 0, + n if (POST_CONVOLUTION_SCALE..POST_CONVOLUTION_SCALE + 8).contains(&n) => p.post[(n - POST_CONVOLUTION_SCALE) as usize] = v, + n if (POST_COLOUR_MATRIX_SCALE..POST_COLOUR_MATRIX_SCALE + 8).contains(&n) => p.post[8 + (n - POST_COLOUR_MATRIX_SCALE) as usize] = v, + _ => c.unknown_names += 1, + } + true + } + (SET_PIXEL_ZOOM_FACTORS, &[x, y]) => { + p.zoom = [f(x), f(y)]; + true + } + // Word 0 says which record the unpack side uses, and bit 29 of the mode word switches to it + // (the client's own records otherwise). Mode zero restores; bit 30 saves, and turns the + // fixed-function stages off. + (SUSPEND_PIXEL_MODE_STATE_AROUND_TRANSFER, &[selector, mode]) => { + p.suspended = mode & SUSPEND_SAVE != 0; + p.records = if mode & SUSPEND_SWITCH_RECORDS != 0 && matches!(selector, SUSPEND_USE_DEFAULT_RECORDS | SUSPEND_USE_LIST_RECORD) { selector } else { 0 }; + true + } + (LOAD_PIXEL_MAP_FLOAT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES, _) => true, + (SELECT_READ_BUFFER, &[mask]) => { + p.read = Some(match mask { + gl::FRONT | gl::FRONT_LEFT | gl::RIGHT => 1, + gl::BACK | gl::BACK_LEFT => 2, + _ => 1, + }); + true + } + (SET_ACCUMULATION_BUFFER_CLEAR_VALUE, &[r, g, b, al]) => { + p.accum_clear = [f(r), f(g), f(b), f(al)]; + true + } + (SET_CURRENT_RASTER_POSITION, _) => { + c.list_positions += 1; + true + } + (SET_RASTER_POSITION_2_COMPONENTS_FLOAT, &[x, y]) => raster_position(gl, [f(x), f(y), 0.0, 1.0]), + (SET_RASTER_POSITION_3_COMPONENTS_FLOAT, &[x, y, z]) => raster_position(gl, [f(x), f(y), f(z), 1.0]), + (SET_RASTER_POSITION_4_COMPONENTS_FLOAT, &[x, y, z, w]) => raster_position(gl, [f(x), f(y), f(z), f(w)]), + (SET_RASTER_POSITION_2_COMPONENTS_32_BIT_INTEGER, &[x, y]) => raster_position(gl, [x as i32 as f32, y as i32 as f32, 0.0, 1.0]), + (SET_RASTER_POSITION_3_COMPONENTS_32_BIT_INTEGER, &[x, y, z]) => raster_position(gl, [x as i32 as f32, y as i32 as f32, z as i32 as f32, 1.0]), + (SET_RASTER_POSITION_4_COMPONENTS_32_BIT_INTEGER, &[x, y, z, w]) => raster_position(gl, [x as i32 as f32, y as i32 as f32, z as i32 as f32, w as i32 as f32]), + // A short in each word's low half (inferred: the table counts one word a component). + (SET_RASTER_POSITION_2_COMPONENTS_SHORT, &[x, y]) => raster_position(gl, [x as i16 as f32, y as i16 as f32, 0.0, 1.0]), + (SET_RASTER_POSITION_3_COMPONENTS_SHORT, &[x, y, z]) => raster_position(gl, [x as i16 as f32, y as i16 as f32, z as i16 as f32, 1.0]), + (SET_RASTER_POSITION_4_COMPONENTS_SHORT, &[x, y, z, w]) => raster_position(gl, [x as i16 as f32, y as i16 as f32, z as i16 as f32, w as i16 as f32]), + // The double forms: two words a component, high word first (as the matrix doubles are). + (SET_RASTER_POSITION_2_COMPONENTS_DOUBLE, &[..]) if a.len() == 4 => raster_position(gl, [double(a, 0), double(a, 1), 0.0, 1.0]), + (SET_RASTER_POSITION_3_COMPONENTS_DOUBLE, &[..]) if a.len() == 6 => raster_position(gl, [double(a, 0), double(a, 1), double(a, 2), 1.0]), + (SET_RASTER_POSITION_4_COMPONENTS_DOUBLE, &[..]) if a.len() == 8 => raster_position(gl, [double(a, 0), double(a, 1), double(a, 2), double(a, 3)]), + _ => false, + } +} + +fn double(a: &[u32], i: usize) -> f32 { + f64::from_bits((a[2 * i] as u64) << 32 | a[2 * i + 1] as u64) as f32 +} + +/// Take a pixel map: four header words and the values, as the framer hands them (`a` is the +/// header, `words` the values). Only the map names the table lists are kept. +pub fn load_map(gl: &mut Gl, c: &mut Counts, selector: u32, a: &[u32], words: &[u32]) { + let (max, value): (f32, fn(u32) -> f32) = match selector { + LOAD_PIXEL_MAP_FLOAT_VALUES => (0.0, f), + LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES => (4_294_967_295.0, |w| w as f32), + _ => (65_535.0, |w| (w & 0xffff) as f32), + }; + let name = a.first().copied().unwrap_or(0); + let known = (MAP_INDEX_TO_INDEX..MAP_INDEX_TO_INDEX + 2 + 4 + 4).contains(&name); + if !known || words.is_empty() || words.len() as u32 > MAP_LIMIT { + c.maps_refused += 1; + return; + } + gl.pix.maps.insert(name, Map { max, values: words.iter().map(|&w| value(w)).collect() }); +} + +/// The current raster position from an object-space position: through the model-view and +/// projection matrices, valid when inside the clip volume; colour the current colour (unlit: +/// the model's own, as lighting at the raster position is not modelled). +fn raster_position(gl: &mut Gl, p: [f32; 4]) -> bool { + let e = math::xform(gl.mv.get(), p); + let h = math::xform(gl.proj.get(), e); + let w = h[3]; + let valid = w > 0.0 && h[..3].iter().all(|v| v.abs() <= w); + let [vx, vy, vw, vh] = gl.raster.viewport; + let [dn, df] = gl.raster.depth_range; + let (nx, ny, nz) = if w > 0.0 { (h[0] / w, h[1] / w, h[2] / w) } else { (0.0, 0.0, 0.0) }; + gl.pix.pos = RasterPos { valid, x: vx + (nx + 1.0) * 0.5 * vw, y: vy + (ny + 1.0) * 0.5 * vh, z: depth32(dn + (nz * 0.5 + 0.5) * (df - dn)), colour: gl.color }; + true +} + +// ---- Images ---- + +/// A pixel rectangle's components, by the kind of data. +#[derive(Clone, Debug, PartialEq)] +pub enum Plane { + Colour(Vec<[f32; 4]>), + Index(Vec), + Stencil(Vec), + Depth(Vec), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Image { + pub w: usize, + pub h: usize, + pub plane: Plane, +} + +/// Components in a pixel of `format`, or None for a code the model does not know. +fn components(format: u32) -> Option { + match format { + FORMAT_RED | FORMAT_GREEN | FORMAT_BLUE | FORMAT_ALPHA | FORMAT_LUMINANCE | FORMAT_COLOUR_INDEX | FORMAT_STENCIL | FORMAT_DEPTH => Some(1), + FORMAT_LUMINANCE_ALPHA => Some(2), + FORMAT_RGB => Some(3), + FORMAT_RGBA | FORMAT_ABGR | FORMAT_CMYK => Some(4), + FORMAT_CMYKA => Some(5), + _ => None, + } +} + +/// Bytes in an element of `ty`, or the whole pixel for a packed type (flag second). +fn element_bytes(ty: u32) -> Option<(usize, bool)> { + match ty { + TYPE_BYTE | TYPE_UNSIGNED_BYTE => Some((1, false)), + TYPE_SHORT | TYPE_UNSIGNED_SHORT => Some((2, false)), + TYPE_INT | TYPE_UNSIGNED_INT | TYPE_FLOAT => Some((4, false)), + TYPE_3_3_2 => Some((1, true)), + TYPE_4_4_4_4 | TYPE_5_5_5_1 => Some((2, true)), + TYPE_8_8_8_8 | TYPE_10_10_10_2 => Some((4, true)), + _ => None, + } +} + +/// Bytes a pixel takes (a bitmap's is one bit: reported as 0 here and handled by the callers). +fn pixel_bytes(format: u32, ty: u32) -> Option { + let n = components(format)?; + if ty == TYPE_BITMAP { + return (n == 1).then_some(0); + } + let (b, packed) = element_bytes(ty)?; + if packed { + // The ABGR extension's packed layouts are not recorded. + if format == FORMAT_ABGR { + return None; + } + // A packed type is a whole pixel: RGB for the 3-3-2 form, RGBA for the others. + let want = if ty == TYPE_3_3_2 { 3 } else { 4 }; + return (n == want).then_some(b); + } + Some(b * n) +} + +/// An element's bytes as an unsigned integer in the client's order, swapped if asked. +fn word_at(bytes: &[u8], at: usize, n: usize, swap: bool) -> Option { + let s = bytes.get(at..at + n)?; + let mut v = 0u32; + if swap { + for &b in s.iter().rev() { + v = v << 8 | b as u32; + } + } else { + for &b in s { + v = v << 8 | b as u32; + } + } + Some(v) +} + +/// An element as a number in 0 to 1 (signed types: -1 to 1 by GL's rule), or for an index or +/// stencil image the integer itself. +fn element(bytes: &[u8], at: usize, ty: u32, swap: bool, integer: bool) -> Option { + let (n, _) = element_bytes(ty)?; + let w = word_at(bytes, at, n, swap)?; + Some(match ty { + TYPE_FLOAT => f32::from_bits(w), + _ if integer => match ty { + TYPE_BYTE => w as u8 as i8 as f32, + TYPE_SHORT => w as u16 as i16 as f32, + TYPE_INT => w as i32 as f32, + _ => w as f32, + }, + TYPE_UNSIGNED_BYTE => w as f32 / 255.0, + TYPE_UNSIGNED_SHORT => w as f32 / 65535.0, + TYPE_UNSIGNED_INT => (w as f64 / 4_294_967_295.0) as f32, + TYPE_BYTE => (2.0 * (w as u8 as i8) as f32 + 1.0) / 255.0, + TYPE_SHORT => (2.0 * (w as u16 as i16) as f32 + 1.0) / 65535.0, + TYPE_INT => ((2.0 * (w as i32) as f64 + 1.0) / 4_294_967_295.0) as f32, + _ => return None, + }) +} + +/// A packed pixel's components, public layouts: red, green, blue, alpha from the high bits. +fn unpack_packed(ty: u32, w: u32) -> [f32; 4] { + let g = |shift: u32, bits: u32| ((w >> shift) & ((1 << bits) - 1)) as f32 / ((1u32 << bits) - 1) as f32; + match ty { + TYPE_3_3_2 => [g(5, 3), g(2, 3), g(0, 2), 1.0], + TYPE_4_4_4_4 => [g(12, 4), g(8, 4), g(4, 4), g(0, 4)], + TYPE_5_5_5_1 => [g(11, 5), g(6, 5), g(1, 5), g(0, 1)], + TYPE_8_8_8_8 => [g(24, 8), g(16, 8), g(8, 8), g(0, 8)], + _ => [g(22, 10), g(12, 10), g(2, 10), g(0, 2)], + } +} + +/// Bytes a draw's header has the stream carry. The pipe computes this from the unpack state. +#[derive(Clone, Copy, Debug)] +pub struct Header { + pub w: u32, + pub h: u32, + pub format: u32, + pub ty: u32, + pub offset: u32, + pub dma: bool, + pub row_mode: u32, + pub column: u32, +} + +impl Header { + /// The eight words of a pixel draw's header (section 7.15). + pub fn parse(a: &[u32]) -> Option
{ + let a: &[u32; 8] = a.get(..8)?.try_into().ok()?; + Some(Header { w: a[0], h: a[1], format: a[2], ty: a[3], offset: a[4] & 3, dma: a[5] != 0, row_mode: a[6], column: a[7] }) + } + + fn geometry(&self, rec: &Record) -> Option { + let p = pixel_bytes(self.format, self.ty)?; + let bits = self.ty == TYPE_BITMAP; + // A row length below the width puts a row's end before its start: no size, and the draw is refused (counted). + if rec.row_length != 0 && rec.row_length < self.w { + return None; + } + let rl = if rec.row_length > 0 { rec.row_length } else { self.w } as usize; + let r = if bits { rl.div_ceil(8) } else { rl * p }; + let mask = rec.mask(); + let s = (r + mask) & !mask; + // The unpack state is the guest's: a size past what fits is no size, and the draw is refused (counted). + let skip = (rec.skip_rows as usize).checked_mul(s)?.checked_add(if bits { rec.skip_pixels as usize / 8 } else { rec.skip_pixels as usize * p })?; + Some(Geometry { p, bits, row: r, stride: s, start: (self.offset as usize).checked_add(skip)?, bit_skip: if bits { rec.skip_pixels as usize % 8 } else { 0 } }) + } + + /// The words the image takes in the stream, or None for a format or type the model does not know. + pub fn image_words(&self, rec: &Record) -> Option { + let g = self.geometry(rec)?; + let h = self.h as usize; + if h == 0 || self.w == 0 { + return Some(0); + } + if self.row_mode == 0 { + let total = g.stride.checked_mul(h)?.checked_sub(g.stride.checked_sub(g.row)?)?; + u32::try_from(g.start.checked_add(total)?.div_ceil(4) - g.start / 4).ok() + } else { + // A row's first byte sits at `(start + j * stride) % 4`, which repeats every four rows. + let row = |j: usize| (g.start.wrapping_add(j.wrapping_mul(g.stride)) % 4 + g.row).div_ceil(4) as u64; + let (cycles, rest) = (h / 4, h % 4); + let sum = (cycles as u64).checked_mul((0..4).map(row).sum::())?.checked_add((0..rest).map(row).sum::())?; + u32::try_from(sum).ok() + } + } +} + +struct Geometry { + p: usize, + bits: bool, + row: usize, + stride: usize, + start: usize, + bit_skip: usize, +} + +/// Decode a draw's image from its stream words. +pub fn unpack(h: &Header, rec: &Record, bytes: &[u8]) -> Option { + if matches!(h.format, FORMAT_CMYK | FORMAT_CMYKA) { + return None; + } + let g = h.geometry(rec)?; + let (w, ht) = (h.w as usize, h.h as usize); + let packed = element_bytes(h.ty).is_some_and(|e| e.1); + let esize = element_bytes(h.ty).map_or(0, |e| e.0); + let n = components(h.format)?; + // Where row `j` starts in `bytes`. + let mut row_at = Vec::with_capacity(ht); + let mut block = 0usize; + for j in 0..ht { + if h.row_mode == 0 { + row_at.push(g.start % 4 + j * g.stride); + } else { + let o = (g.start + j * g.stride) % 4; + row_at.push(block + o); + block += (o + g.row).div_ceil(4) * 4; + } + } + let swap = rec.swap_bytes; + let int_plane = matches!(h.format, FORMAT_COLOUR_INDEX | FORMAT_STENCIL); + let mut colour = Vec::new(); + let mut ints = Vec::new(); + let mut depth = Vec::new(); + for j in 0..ht { + for i in 0..w { + if g.bits { + let bit = g.bit_skip + i; + let byte = *bytes.get(row_at[j] + bit / 8)?; + let v = if rec.lsb_first { byte >> (bit % 8) & 1 } else { byte >> (7 - bit % 8) & 1 }; + if int_plane { + ints.push(v as i64); + } else if h.format == FORMAT_DEPTH { + depth.push(v as f32); + } else { + colour.push(to_colour(h.format, &[v as f32; 4])); + } + continue; + } + let at = row_at[j] + i * g.p; + if packed { + let c = unpack_packed(h.ty, word_at(bytes, at, esize, swap)?); + colour.push(if h.ty == TYPE_3_3_2 || h.format == FORMAT_RGB { [c[0], c[1], c[2], 1.0] } else { c }); + continue; + } + if int_plane { + ints.push(element(bytes, at, h.ty, swap, true)?.round() as i64); + continue; + } + let mut c = [0.0f32; 4]; + for (k, slot) in c.iter_mut().enumerate().take(n) { + *slot = element(bytes, at + k * esize, h.ty, swap, false)?; + } + if h.format == FORMAT_DEPTH { + depth.push(c[0]); + } else { + colour.push(to_colour(h.format, &c)); + } + } + } + Some(Image { + w, + h: ht, + plane: match h.format { + FORMAT_COLOUR_INDEX => Plane::Index(ints), + FORMAT_STENCIL => Plane::Stencil(ints), + FORMAT_DEPTH => Plane::Depth(depth), + _ => Plane::Colour(colour), + }, + }) +} + +/// The components read from a pixel of `format`, as red, green, blue and alpha. +fn to_colour(format: u32, c: &[f32; 4]) -> [f32; 4] { + match format { + FORMAT_RED => [c[0], 0.0, 0.0, 1.0], + FORMAT_GREEN => [0.0, c[0], 0.0, 1.0], + FORMAT_BLUE => [0.0, 0.0, c[0], 1.0], + FORMAT_ALPHA => [0.0, 0.0, 0.0, c[0]], + FORMAT_RGB => [c[0], c[1], c[2], 1.0], + FORMAT_LUMINANCE => [c[0], c[0], c[0], 1.0], + FORMAT_LUMINANCE_ALPHA => [c[0], c[0], c[0], c[1]], + FORMAT_ABGR => [c[3], c[2], c[1], c[0]], + _ => *c, + } +} + +/// The bytes of a packed row of `w` pixels and the stride between rows, under the pack record: +/// None for a format or type the packer does not know. +pub fn row_bytes(w: usize, format: u32, ty: u32, rec: &Record) -> Option<(usize, usize)> { + let p = pixel_bytes(format, ty)?; + let rl = if rec.row_length > 0 { rec.row_length as usize } else { w }; + let r = if ty == TYPE_BITMAP { rl.div_ceil(8) } else { rl * p }; + let mask = rec.mask(); + Some((r, (r + mask) & !mask)) +} + +/// The most bytes `pack` will build. A read's size is the guest's (the pack record's skips and a row length +/// are its pixel-store state), so it is bounded before anything is allocated; the largest bracket buffer +/// the host side takes is this size too. +pub const MAX_PACK_BYTES: u64 = 1 << 26; + +/// The bytes `pack` builds for a `w` by `h` image, before it pads them to whole words: the leading skips, +/// the rows and the last row. None for a format or type the packer does not know, or for a size past what +/// 64 bits hold. +pub fn packed_len(w: usize, h: usize, format: u32, ty: u32, low: u32, rec: &Record) -> Option { + if matches!(format, FORMAT_CMYK | FORMAT_CMYKA) { + return None; + } + let p = pixel_bytes(format, ty)? as u64; + components(format)?; + let (r, s) = row_bytes(w, format, ty, rec)?; + let (r, s) = (r as u64, s as u64); + let skip = if ty == TYPE_BITMAP { rec.skip_pixels as u64 / 8 } else { rec.skip_pixels as u64 * p }; + let first = (low as u64).checked_add((rec.skip_rows as u64).checked_mul(s)?)?.checked_add(skip)?; + first.checked_add((h.max(1) as u64 - 1).checked_mul(s)?)?.checked_add(r.max(1)) +} + +/// Encode an image back to the host's layout under a pack record: the bytes from the +/// destination's low two address bits on, padded to whole words. None for an image +/// that would take more than `MAX_PACK_BYTES`. +pub fn pack(img: &Image, format: u32, ty: u32, low: u32, rec: &Record) -> Option> { + let len = packed_len(img.w, img.h, format, ty, low, rec).filter(|&n| n <= MAX_PACK_BYTES)?; + let p = pixel_bytes(format, ty)?; + let n = components(format)?; + let bits = ty == TYPE_BITMAP; + let (esize, packed) = element_bytes(ty).unwrap_or((0, false)); + let (r, s) = row_bytes(img.w, format, ty, rec)?; + let first = low as usize + rec.skip_rows as usize * s + if bits { rec.skip_pixels as usize / 8 } else { rec.skip_pixels as usize * p }; + let mut out = vec![0u8; len as usize]; + let put = |out: &mut Vec, at: usize, n: usize, v: u32| { + for k in 0..n { + let b = if rec.swap_bytes { v >> (8 * k) } else { v >> (8 * (n - 1 - k)) } as u8; + out[at + k] = b; + } + }; + for j in 0..img.h { + for i in 0..img.w { + let k = j * img.w + i; + let at = first + j * s + if bits { 0 } else { i * p }; + if bits { + let v = match &img.plane { + Plane::Index(v) | Plane::Stencil(v) => v[k] & 1 != 0, + Plane::Depth(d) => d[k] >= 0.5, + Plane::Colour(c) => c[k][0] >= 0.5, + }; + let bit = (rec.skip_pixels as usize % 8) + i; + let m = if rec.lsb_first { 1 << (bit % 8) } else { 0x80 >> (bit % 8) }; + if v { + out[first + j * s + bit / 8] |= m; + } + continue; + } + // The components in the format's order. + let comps: Vec = match &img.plane { + Plane::Colour(c) => { + let c = c[k]; + match format { + FORMAT_RED => vec![c[0]], + FORMAT_GREEN => vec![c[1]], + FORMAT_BLUE => vec![c[2]], + FORMAT_ALPHA => vec![c[3]], + FORMAT_RGB => vec![c[0], c[1], c[2]], + FORMAT_RGBA => c.to_vec(), + FORMAT_ABGR => vec![c[3], c[2], c[1], c[0]], + // Luminance is the sum of red, green and blue (GL's rule). + FORMAT_LUMINANCE => vec![(c[0] + c[1] + c[2]).clamp(0.0, 1.0)], + _ => vec![(c[0] + c[1] + c[2]).clamp(0.0, 1.0), c[3]], + } + } + Plane::Depth(d) => vec![d[k]], + Plane::Index(v) | Plane::Stencil(v) => vec![v[k] as f32], + }; + if packed { + let g = |c: f32, bits: u32| ((c.clamp(0.0, 1.0) * ((1u32 << bits) - 1) as f32 + 0.5) as u32) & ((1 << bits) - 1); + let c = &comps; + let a = c.get(3).copied().unwrap_or(1.0); + let w = match ty { + TYPE_3_3_2 => g(c[0], 3) << 5 | g(c[1], 3) << 2 | g(c[2], 2), + TYPE_4_4_4_4 => g(c[0], 4) << 12 | g(c[1], 4) << 8 | g(c[2], 4) << 4 | g(a, 4), + TYPE_5_5_5_1 => g(c[0], 5) << 11 | g(c[1], 5) << 6 | g(c[2], 5) << 1 | g(a, 1), + TYPE_8_8_8_8 => g(c[0], 8) << 24 | g(c[1], 8) << 16 | g(c[2], 8) << 8 | g(a, 8), + _ => g(c[0], 10) << 22 | g(c[1], 10) << 12 | g(c[2], 10) << 2 | g(a, 2), + }; + put(&mut out, at, esize, w); + continue; + } + let int_plane = matches!(img.plane, Plane::Index(_) | Plane::Stencil(_)); + for (m, &v) in comps.iter().enumerate().take(n) { + let w: u32 = match ty { + TYPE_FLOAT => v.to_bits(), + _ if int_plane => v as i64 as u32, + TYPE_UNSIGNED_BYTE => (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u32, + TYPE_UNSIGNED_SHORT => (v.clamp(0.0, 1.0) * 65535.0 + 0.5) as u32, + TYPE_UNSIGNED_INT => (v.clamp(0.0, 1.0) as f64 * 4_294_967_295.0 + 0.5) as u32, + TYPE_BYTE => (((v.clamp(0.0, 1.0) * 255.0 - 1.0) / 2.0).round() as i32) as u32, + TYPE_SHORT => (((v.clamp(0.0, 1.0) * 65535.0 - 1.0) / 2.0).round() as i32) as u32, + _ => (((v.clamp(0.0, 1.0) as f64 * 4_294_967_295.0 - 1.0) / 2.0).round() as i64) as u32, + }; + put(&mut out, at + m * esize, esize, w); + } + } + } + out.resize(out.len().div_ceil(4) * 4, 0); + Some(out) +} + +// ---- The planes ---- + +/// Where fragments land: the draw state's planes, with ownership resolved. +struct Canvas<'a> { + store: &'a PixelStore, + ds: DrawState, + planes: Planes<'a>, + did: Option>, + pitch: usize, +} + +impl<'a> Canvas<'a> { + fn new(store: &'a PixelStore, mut ds: DrawState) -> Canvas<'a> { + // A pixel rectangle is not textured, and the model does not stipple it (see the module doc). + ds.texture = None; + ds.polygon_stipple = None; + let did = ds.ownership.is_some().then(|| store.array(DID_PLANE)).flatten(); + Canvas { store, planes: Planes::new(store, &ds), ds, did, pitch: store.width() as usize } + } + + /// The plane index of window pixel (`x`, `y`), when it is inside the clip rectangle and owned. + fn at(&self, x: i64, y: i64) -> Option { + let (px, py) = (self.ds.origin[0] as i64 + x, self.ds.origin[1] as i64 + y); + let c = &self.ds.clip; + if px < c.x0 as i64 || px >= c.x1 as i64 || py < c.y0 as i64 || py >= c.y1 as i64 { + return None; + } + let index = py as usize * self.pitch + px as usize; + match &self.ds.ownership { + None => Some(index), + // SAFETY: the pixel is inside the clip rectangle, which is inside the screen. + Some(o) => o.owns(ownership_plane(self.did.as_ref().map_or(0, |d| unsafe { d.load(index) }))).then_some(index), + } + } + + fn frag(&self, c: &mut Counts, x: i64, y: i64, colour: [f32; 4], z: u32) { + if let Some(index) = self.at(x, y) { + c.fragments += 1; + let src = colour.map(|v| colour13(v) as u32); + match &self.ds.samples { + None => self.planes.shade(&self.ds, index, src, z), + // A pixel rectangle covers every sample of its pixel, at one depth. + Some(ms) => self.planes.shade_samples(&self.ds, index, src, |_| z, (1u32 << ms.count) - 1), + } + } + } +} + +/// A plane word `0xAARRGGBB` as red, green, blue and alpha in 0 to 1, at the word's own eight bits. +fn components8(w: u32) -> [f32; 4] { + [w >> 16 & 0xff, w >> 8 & 0xff, w & 0xff, w >> 24 & 0xff].map(|v| v as f32 / 255.0) +} + +/// The colour word a read or accumulation takes its pixels from. +fn read_word(gl: &Gl, ds: &DrawState) -> Option { + let l = gl.layout.as_ref()?; + let mask = gl.pix.read.unwrap_or(if gl.double { 2 } else { 1 }); + let shown = if gl.double { gl.shown as u32 } else { 0 }; + let word = if mask & 1 != 0 { l.colour.start + shown } else { l.colour.start + 1 - shown }; + let _ = ds; + (word < l.colour.end).then_some(word as usize) +} + +impl Pixels { + /// The transfer stages for a colour. + fn colour(&self, mut c: [f32; 4]) -> [f32; 4] { + if self.suspended { + return c; + } + for k in 0..4 { + c[k] = (c[k] * self.scale[k] + self.bias[k]).clamp(0.0, 1.0); + } + if self.map_colour { + for k in 0..4 { + if let Some(m) = self.maps.get(&(MAP_RED_TO_RED + k as u32)) { + c[k] = m.at(c[k]); + } + } + } + c + } + + fn depth(&self, d: f32) -> f32 { + if self.suspended { d } else { (d * self.depth_scale + self.depth_bias).clamp(0.0, 1.0) } + } + + /// An index or stencil value through the shift, offset and (for the maps' flags) the table. + fn integer(&self, v: i64, map: u32, mapped: bool) -> i64 { + if self.suspended { + return v; + } + let s = self.index_shift; + let v = (if s >= 0 { v << s.min(40) } else { v >> (-s).min(40) }) + self.index_offset as i64; + match self.maps.get(&map) { + Some(m) if mapped => m.index(v), + _ => v, + } + } + + fn index_colour(&self, i: i64, c: &mut Counts) -> [f32; 4] { + let m: Vec<_> = (0..4).map(|k| self.maps.get(&(MAP_INDEX_TO_RED + k))).collect(); + if m.iter().all(|m| m.is_some()) && self.map_colour { + let v = |k: usize| m[k].map_or(0.0, |m| m.index(i) as f32 / if m.max > 0.0 { m.max } else { 1.0 }); + return [v(0), v(1), v(2), v(3)]; + } + c.index_without_map += 1; + let g = (i & 0xff) as f32 / 255.0; + [g, g, g, 1.0] + } +} + +/// Fragments for `img` with its lower-left pixel at the raster position, zoomed. +fn emit(gl: &Gl, c: &mut Counts, canvas: &Canvas<'_>, img: &Image) { + let p = &gl.pix; + let pos = p.pos; + let (rx, ry) = (pos.x.floor() as i64, pos.y.floor() as i64); + let [zx, zy] = p.zoom; + let span = |origin: i64, i: usize, z: f32| { + let (a, b) = ((origin as f32 + i as f32 * z).floor() as i64, (origin as f32 + (i + 1) as f32 * z).floor() as i64); + (a.min(b), a.max(b)) + }; + let (rc, rz) = (pos.colour, pos.z); + for j in 0..img.h { + let (y0, y1) = span(ry, j, zy); + for i in 0..img.w { + let (x0, x1) = span(rx, i, zx); + let k = j * img.w + i; + for y in y0..y1.max(y0) { + for x in x0..x1.max(x0) { + match &img.plane { + Plane::Colour(col) => canvas.frag(c, x, y, p.colour(col[k]), rz), + Plane::Index(v) => { + let col = p.index_colour(p.integer(v[k], MAP_INDEX_TO_INDEX, p.map_colour), c); + canvas.frag(c, x, y, col, rz) + } + Plane::Depth(d) => canvas.frag(c, x, y, rc, depth32(p.depth(d[k]))), + Plane::Stencil(v) => { + let value = p.integer(v[k], MAP_STENCIL_TO_STENCIL, p.map_stencil) as u32 as u8; + if let (Some(index), true, true) = (canvas.at(x, y), canvas.ds.stencil_bits != 0, canvas.ds.samples.is_some()) { + canvas.planes.set_stencil(&canvas.ds, index, value, canvas.ds.stencil.write_mask); + } else if let (Some(index), true) = (canvas.at(x, y), canvas.ds.stencil_bits != 0) { + if let Some(a) = canvas.store.array(canvas.ds.stencil_word as usize) { + let wm = canvas.ds.stencil.write_mask; + // SAFETY: the index is inside the clip rectangle, inside the screen. + unsafe { a.store(index, ((a.load(index) as u8 & !wm) | (value & wm)) as u32) }; + } + } + } + } + } + } + } + } + if p.post[..4] != [1.0; 4] || p.post[4..8] != [0.0; 4] || p.post[8..12] != [1.0; 4] || p.post[12..] != [0.0; 4] { + c.post_stages_ignored += 1; + } +} + +/// The rectangle of the read buffer, as an image of `kind`. Pixels outside the window read zero. +fn capture(gl: &Gl, canvas: &Canvas<'_>, x: i32, y: i32, w: usize, h: usize, kind: u32) -> Option { + let ds = &canvas.ds; + let colour = (kind == COPY_COLOUR).then(|| read_word(gl, ds).and_then(|word| canvas.store.array(word))).flatten(); + let plain = ds.samples.is_none(); + let depth = (kind == COPY_DEPTH && ds.depth_bits != 0 && plain).then(|| canvas.store.array(ds.depth_word as usize)).flatten(); + let stencil = (kind == COPY_STENCIL && ds.stencil_bits != 0 && plain).then(|| canvas.store.array(ds.stencil_word as usize)).flatten(); + let mut col = Vec::new(); + let mut ints = Vec::new(); + let mut zs = Vec::new(); + for j in 0..h { + for i in 0..w { + let at = canvas.at(x as i64 + i as i64, y as i64 + j as i64); + // Each load below is of a pixel `at` found inside the clip rectangle, inside the screen. + match kind { + COPY_COLOUR => col.push(match (at, &colour) { + // SAFETY: `a` is inside the clip rectangle, inside the screen. + (Some(a), Some(arr)) => components8(unsafe { arr.load(a) }), + _ => [0.0; 4], + }), + COPY_DEPTH => zs.push(match (at, &depth) { + // SAFETY: as above. + (Some(a), Some(arr)) => (unsafe { arr.load(a) }) as f32 / 4_294_967_040.0, + // A multisampled window's depth is in its samples: a read takes the first. + (Some(a), None) => canvas.planes.sample_depth(ds, a).map_or(0.0, |z| z as f32 / 4_294_967_040.0), + _ => 0.0, + }), + _ => ints.push(match (at, &stencil) { + // SAFETY: as above. + (Some(a), Some(arr)) => (unsafe { arr.load(a) }) as i64 & 0xff, + (Some(a), None) => canvas.planes.sample_stencil(a).map_or(0, |v| v as i64), + _ => 0, + }), + } + } + } + Some(Image { w, h, plane: match kind { COPY_COLOUR => Plane::Colour(col), COPY_DEPTH => Plane::Depth(zs), _ => Plane::Stencil(ints) } }) +} + +/// What a plane-touching token needs of the board around it. +pub struct Env<'a> { + pub store: &'a PixelStore, + pub ds: DrawState, +} + +/// What a token answered. +#[derive(Debug, PartialEq)] +pub enum Out { + Nothing, + /// Bytes for the return page, at offset 8. + Reply(Vec), +} + +/// The words a draw, bitmap or map token asks the stream for after its framed words; None for +/// a token that carries none. Maps load the values the header names; a draw's image is sized from +/// the unpack record in force, and a draw announced by DMA has none in the stream. +pub fn run_words(gl: &Gl, selector: u32, a: &[u32]) -> Option { + match selector { + LOAD_PIXEL_MAP_FLOAT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES => a.get(1).copied().map(|n| n.min(MAP_LIMIT)), + DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA => { + let h = Header::parse(a)?; + Some(if h.dma { 0 } else { h.image_words(&gl.pix.unpack_in_force()).unwrap_or(0) }) + } + // The small bitmap's word count includes the byte count, which is in the framed words. + DRAW_BITMAP_FROM_DISPLAY_LIST if a.len() == 8 => Some(a[6].saturating_sub(1)), + DRAW_BITMAP => { + let h = large_bitmap(a)?; + Some(if h.dma { 0 } else { h.image_words(&gl.pix.unpack_in_force()).unwrap_or(0) }) + } + _ => None, + } +} + +/// The large bitmap's header: width, height, byte offset, DMA flag, row mode, then four floats. +pub fn large_bitmap(a: &[u32]) -> Option
{ + let a: &[u32; 9] = a.get(..9)?.try_into().ok()?; + Some(Header { w: a[0], h: a[1], format: FORMAT_COLOUR_INDEX, ty: TYPE_BITMAP, offset: a[2] & 3, dma: a[3] != 0, row_mode: a[4], column: 0 }) +} + +/// A plane-touching token with no data run: copy, read, accumulate and the queries. +pub fn plane_command(gl: &mut Gl, c: &mut Counts, env: &Env<'_>, selector: u32, a: &[u32]) -> Out { + let canvas = Canvas::new(env.store, env.ds); + match (selector, a) { + (COPY_PIXEL_RECTANGLE_WITHIN_FRAME_BUFFER, &[x, y, w, h, _offset, kind, _overlap]) => { + c.copies += 1; + if !gl.pix.pos.valid { + c.invalid_position += 1; + return Out::Nothing; + } + let Some(img) = capture(gl, &canvas, x as i32, y as i32, w as usize, h as usize, kind) else { + c.refused += 1; + return Out::Nothing; + }; + emit(gl, c, &canvas, &img); + Out::Nothing + } + (READ_PIXEL_RECTANGLE_BACK_TO_HOST, &[x, y, w, h, format, ty, low]) => { + c.reads += 1; + let kind = match format { + FORMAT_DEPTH => COPY_DEPTH, + FORMAT_STENCIL => COPY_STENCIL, + _ => COPY_COLOUR, + }; + let Some(mut img) = capture(gl, &canvas, x as i32, y as i32, w as usize, h as usize, kind) else { + c.refused += 1; + return Out::Nothing; + }; + let p = &gl.pix; + match &mut img.plane { + Plane::Colour(col) => col.iter_mut().for_each(|v| *v = p.colour(*v)), + Plane::Depth(d) => d.iter_mut().for_each(|v| *v = p.depth(*v)), + Plane::Stencil(v) => v.iter_mut().for_each(|v| *v = p.integer(*v, MAP_STENCIL_TO_STENCIL, p.map_stencil)), + Plane::Index(_) => {} + } + // A colour-index read has no index plane to read: it is refused. + let Some(bytes) = (format != FORMAT_COLOUR_INDEX).then(|| pack(&img, format, ty, low & 3, &p.pack)).flatten() else { + c.refused += 1; + return Out::Nothing; + }; + if bytes.len() > READ_REPLY_LIMIT { + c.read_by_dma += 1; + return Out::Nothing; + } + Out::Reply(bytes) + } + (ACCUMULATION_BUFFER_OPERATION, &[op, value]) => { + c.accumulations += 1; + accumulate(gl, c, &canvas, op, f(value)); + Out::Nothing + } + (ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION | ASK_WHETHER_CONVOLUTION_COPY_OVERLAPS, &[x, y, w, h]) => { + c.queries += 1; + let p = &gl.pix; + let (dx, dy) = (p.pos.x.floor() as i64, p.pos.y.floor() as i64); + let (sx, sy) = (x as i32 as i64, y as i32 as i64); + let (zw, zh) = ((w as f32 * p.zoom[0].abs()) as i64, (h as f32 * p.zoom[1].abs()) as i64); + let meets = sx < dx + zw && dx < sx + w as i64 && sy < dy + zh && dy < sy + h as i64; + let mut reply = vec![0u8; if selector == ASK_WHETHER_CONVOLUTION_COPY_OVERLAPS { 40 } else { 4 }]; + reply[..4].copy_from_slice(&(if meets { OVERLAP_BIT } else { 0 }).to_be_bytes()); + Out::Reply(reply) + } + (RESOLVE_WHETHER_PIXEL_FORMAT_IS_SINGLE_COMPONENT, &[..]) => { + c.queries += 1; + Out::Reply(vec![0; 4]) + } + _ => { + c.refused += 1; + Out::Nothing + } + } +} + +/// A draw's header and its image words (or, for a DMA draw, the bytes the packet carried). +pub fn draw(gl: &mut Gl, c: &mut Counts, env: &Env<'_>, hdr: &[u32; 8], bytes: &[u8]) { + c.draws += 1; + let Some(h) = Header::parse(hdr) else { return }; + let Some(img) = unpack(&h, &gl.pix.unpack_in_force(), bytes) else { + c.refused += 1; + return; + }; + if !gl.pix.pos.valid { + c.invalid_position += 1; + return; + } + let canvas = Canvas::new(env.store, env.ds); + // A strip of a wide image starts at its column (inferred from the table's word 7). + let saved = gl.pix.pos.x; + gl.pix.pos.x += h.column as f32 * gl.pix.zoom[0]; + emit(gl, c, &canvas, &img); + gl.pix.pos.x = saved; +} + +/// A bitmap: a set bit is a fragment of the raster position's colour at the position less the +/// origin; the position then moves. `rec` reads the image: the small form's rows are packed. +pub fn bitmap(gl: &mut Gl, c: &mut Counts, env: &Env<'_>, h: &Header, rec: &Record, orig: [f32; 2], mv: [f32; 2], bytes: &[u8]) { + c.bitmaps += 1; + if !gl.pix.pos.valid { + c.invalid_position += 1; + return; + } + let Some(Image { plane: Plane::Index(bits), w, .. }) = unpack(h, rec, bytes) else { + c.refused += 1; + return; + }; + let canvas = Canvas::new(env.store, env.ds); + let pos = gl.pix.pos; + let (x0, y0) = ((pos.x - orig[0]).floor() as i64, (pos.y - orig[1]).floor() as i64); + for (k, _) in bits.iter().enumerate().filter(|(_, &b)| b != 0) { + canvas.frag(c, x0 + (k % w) as i64, y0 + (k / w) as i64, pos.colour, pos.z); + } + gl.pix.pos.x += mv[0]; + gl.pix.pos.y += mv[1]; +} + +/// The passes an accumulation operation runs for `v`, each an operation and its value. Only a value +/// of exactly 1.0 splits: accumulate and add run twice at 0.5, load is a load at 0.5 then an +/// accumulate at 0.5, and multiply by 1.0 does nothing. Every other value, and a return, is one pass. +fn accumulation_passes(op: u32, v: f32) -> Vec<(u32, f32)> { + match (op, v == 1.0) { + (ACCUM | ACCUM_ADD, true) => vec![(op, 0.5), (op, 0.5)], + (ACCUM_LOAD, true) => vec![(ACCUM_LOAD, 0.5), (ACCUM, 0.5)], + (ACCUM_MULT, true) => Vec::new(), + _ => vec![(op, v)], + } +} + +/// One accumulation operation over the window (the draw state's clip rectangle). +fn accumulate(gl: &Gl, c: &mut Counts, canvas: &Canvas<'_>, op: u32, v: f32) { + let Some(l) = &gl.layout else { return }; + if l.accumulation.is_empty() || !matches!(op, ACCUM | ACCUM_LOAD | ACCUM_RETURN | ACCUM_MULT | ACCUM_ADD) { + return; + } + let passes = accumulation_passes(op, v); + let words: Vec> = l.accumulation.clone().filter_map(|w| canvas.store.array(w as usize)).collect(); + let colour = read_word(gl, &canvas.ds).and_then(|w| canvas.store.array(w)); + let ds = &canvas.ds; + let r = ds.clip; + for y in r.y0..r.y1 { + for x in r.x0..r.x1 { + let Some(index) = canvas.at((x - ds.origin[0]) as i64, (y - ds.origin[1]) as i64) else { continue }; + // SAFETY: the pixel is inside the clip rectangle, inside the screen. + let get = |k: usize| words.get(k).map_or(1.0, |a| f32::from_bits(unsafe { a.load(index) })); + let put = |k: usize, v: f32| { + if let Some(a) = words.get(k) { + // SAFETY: as above. + unsafe { a.store(index, v.to_bits()) }; + } + }; + // SAFETY: as above. + let src = colour.map_or([0.0; 4], |a| components8(unsafe { a.load(index) })); + for &(op, v) in &passes { + match op { + ACCUM | ACCUM_LOAD => (0..words.len().min(4)).for_each(|k| put(k, if op == ACCUM { get(k) } else { 0.0 } + v * src[k])), + ACCUM_MULT => (0..words.len()).for_each(|k| put(k, get(k) * v)), + ACCUM_ADD => (0..words.len()).for_each(|k| put(k, get(k) + v)), + _ => { + // Return: the accumulated colour times the value, to the draw buffers under the colour mask. + let out = [0, 1, 2, 3].map(|k| (get(k) * v).clamp(0.0, 1.0)); + for &w in &ds.colour[..ds.colours as usize] { + if let Some(a) = canvas.store.array(w as usize) { + // SAFETY: as above. + let old = unsafe { a.load(index) }; + let keep = (!ds.colour_mask[0] as u32 * 0xff) << 16 | (!ds.colour_mask[1] as u32 * 0xff) << 8 | (!ds.colour_mask[2] as u32 * 0xff) | (!ds.colour_mask[3] as u32 * 0xff) << 24; + let word = out.map(|v| (v * 255.0 + 0.5) as u32); + let new = word[3] << 24 | word[0] << 16 | word[1] << 8 | word[2]; + // SAFETY: as above. + unsafe { a.store(index, (new & !keep) | (old & keep)) }; + } + } + } + } + } + c.fragments += 1; + } + } +} + +/// glClear's accumulation bit: the accumulation words of the window go to the clear value, one +/// component a word, under ownership and the clip rectangle. +pub fn clear_accumulation(gl: &Gl, c: &mut Counts, env: &Env<'_>) { + let Some(l) = &gl.layout else { return }; + let canvas = Canvas::new(env.store, env.ds); + let r = env.ds.clip; + for w in l.accumulation.clone() { + let Some(a) = env.store.array(w as usize) else { continue }; + let v = gl.pix.accum_clear[(w - l.accumulation.start) as usize].to_bits(); + for y in r.y0..r.y1 { + for x in r.x0..r.x1 { + if let Some(index) = canvas.at((x - env.ds.origin[0]) as i64, (y - env.ds.origin[1]) as i64) { + // SAFETY: the pixel is inside the clip rectangle, inside the screen. + unsafe { a.store(index, v) }; + c.fragments += 1; + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::stream::{Reader, Writer}; + + fn header(w: u32, h: u32, format: u32, ty: u32, row_mode: u32) -> Header { + Header { w, h, format, ty, offset: 0, dma: false, row_mode, column: 0 } + } + + /// The size rule of the table: a 3 x 2 RGB image of bytes is rows of nine bytes padded to + /// twelve under alignment 4, 21 bytes in all; under alignment 1 it is 18. + #[test] + fn the_image_length_follows_the_unpack_state() { + let h = header(3, 2, FORMAT_RGB, TYPE_UNSIGNED_BYTE, 0); + assert_eq!(h.image_words(&Record::default()), Some(6)); + assert_eq!(h.image_words(&Record { alignment: 1, ..Record::default() }), Some(5)); + // Skips move the start: two rows of 12, and the 4-byte word boundary rounds the end up. + assert_eq!(h.image_words(&Record { skip_rows: 1, ..Record::default() }), Some(6)); + assert_eq!(h.image_words(&Record { skip_pixels: 1, alignment: 1, ..Record::default() }), Some(6)); + // Row mode 1 counts each row's words alone; a row of nine bytes starting at a word edge is three. + assert_eq!(header(3, 2, FORMAT_RGB, TYPE_UNSIGNED_BYTE, 1).image_words(&Record::default()), Some(6)); + // An unknown format (14, which the pipe counts as no components) or type is no length, and an empty image carries nothing. + assert_eq!(header(1, 1, 14, TYPE_BYTE, 0).image_words(&Record::default()), None); + assert_eq!(header(1, 1, FORMAT_RGBA, 14, 0).image_words(&Record::default()), None); + assert_eq!(header(0, 5, FORMAT_RGBA, TYPE_BYTE, 0).image_words(&Record::default()), Some(0)); + // A bitmap row of ten pixels is two bytes. + assert_eq!(header(10, 3, FORMAT_COLOUR_INDEX, TYPE_BITMAP, 0).image_words(&Record { alignment: 1, ..Record::default() }), Some(2)); + } + + /// A guest's unpack state whose sizes overflow is no image size: the draw is refused, not a panic or a wrapped length. + #[test] + fn an_unpack_state_whose_sizes_overflow_has_no_image_size() { + let big = Record { row_length: u32::MAX, skip_rows: u32::MAX, skip_pixels: u32::MAX, alignment: 8, ..Record::default() }; + assert_eq!(header(4, u32::MAX, FORMAT_RGBA, TYPE_FLOAT, 0).image_words(&big), None); + assert_eq!(header(4, u32::MAX, FORMAT_RGBA, TYPE_FLOAT, 1).image_words(&big), None); + assert_eq!(header(4, u32::MAX, FORMAT_RGBA, TYPE_FLOAT, 1).image_words(&Record::default()), None); + } + + /// An unpack row length below the width is no size, not an underflow. + #[test] + fn a_row_length_below_the_width_has_no_image_size() { + let rec = Record { row_length: 10, ..Record::default() }; + assert_eq!(header(100, 4, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, 0).image_words(&rec), None); + assert_eq!(header(100, 4, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, 1).image_words(&rec), None); + assert!(unpack(&header(100, 4, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, 0), &rec, &[0; 2000]).is_none()); + assert_eq!(header(100, 4, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, 0).image_words(&Record { row_length: 100, ..Record::default() }), Some(400)); + } + + fn colours() -> Image { + Image { w: 3, h: 2, plane: Plane::Colour(vec![[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 0.5], [0.0, 0.0, 1.0, 0.0], [1.0, 1.0, 1.0, 1.0], [0.25, 0.5, 0.75, 1.0], [0.0; 4]]) } + } + + /// What a pack writes, an unpack of the same record reads back (to the type's precision), + /// in every layout the table lists: each format and type, alignment, row length, skips and swap. + #[test] + fn pack_and_unpack_round_trip() { + let img = colours(); + let records = [Record::default(), Record { alignment: 1, ..Record::default() }, Record { row_length: 5, skip_rows: 1, skip_pixels: 2, alignment: 8, ..Record::default() }, Record { swap_bytes: true, alignment: 2, ..Record::default() }]; + for rec in records { + for (format, ty, eps) in [(FORMAT_RGBA, TYPE_UNSIGNED_BYTE, 1.0 / 255.0), (FORMAT_RGBA, TYPE_UNSIGNED_SHORT, 1e-4), (FORMAT_RGBA, TYPE_UNSIGNED_INT, 1e-6), (FORMAT_RGBA, TYPE_FLOAT, 0.0), (FORMAT_RGBA, TYPE_BYTE, 2.0 / 255.0), (FORMAT_RGBA, TYPE_SHORT, 1e-4), (FORMAT_RGBA, TYPE_INT, 1e-6), (FORMAT_RGBA, TYPE_8_8_8_8, 1.0 / 255.0), (FORMAT_RGBA, TYPE_4_4_4_4, 0.07), (FORMAT_RGBA, TYPE_5_5_5_1, 0.5), (FORMAT_RGBA, TYPE_10_10_10_2, 0.34), (FORMAT_RGB, TYPE_3_3_2, 0.2), (FORMAT_RGB, TYPE_UNSIGNED_BYTE, 1.0 / 255.0)] { + let bytes = pack(&img, format, ty, 0, &rec).unwrap_or_else(|| panic!("{format} {ty}")); + let h = header(3, 2, format, ty, 0); + let words = h.image_words(&rec).unwrap() as usize; + assert!(bytes.len() >= words * 4, "{format} {ty}: {} bytes, {words} words", bytes.len()); + // The stream starts at the word holding the first pixel. + let from = h.geometry(&rec).unwrap().start / 4 * 4; + let back = unpack(&h, &rec, &bytes[from..]).unwrap_or_else(|| panic!("{format} {ty}")); + let (Plane::Colour(a), Plane::Colour(b)) = (&img.plane, &back.plane) else { panic!() }; + for (x, y) in a.iter().zip(b) { + let want = if format == FORMAT_RGB { [x[0], x[1], x[2], 1.0] } else { *x }; + // The 3-3-2 and 5-5-5-1 forms hold too little of green or alpha to compare more tightly. + assert!(want.iter().zip(y).all(|(p, q)| (p - q).abs() <= eps + 1e-6), "{format} {ty} {rec:?}: {want:?} read back as {y:?}"); + } + } + } + } + + /// Luminance reads as red plus green plus blue, clamped; index, stencil and depth round trip too. + #[test] + fn other_formats_pack_by_their_own_rules() { + let img = Image { w: 2, h: 1, plane: Plane::Colour(vec![[0.25, 0.25, 0.25, 1.0], [0.9, 0.9, 0.9, 0.5]]) }; + let b = pack(&img, FORMAT_LUMINANCE, TYPE_UNSIGNED_BYTE, 0, &Record { alignment: 1, ..Record::default() }).unwrap(); + assert_eq!(&b[..2], &[191, 255]); + let idx = Image { w: 3, h: 1, plane: Plane::Index(vec![0, 7, 200]) }; + let rec = Record { alignment: 1, ..Record::default() }; + let bytes = pack(&idx, FORMAT_COLOUR_INDEX, TYPE_UNSIGNED_BYTE, 0, &rec).unwrap(); + assert_eq!(unpack(&header(3, 1, FORMAT_COLOUR_INDEX, TYPE_UNSIGNED_BYTE, 0), &rec, &bytes).unwrap().plane, Plane::Index(vec![0, 7, 200])); + let z = Image { w: 2, h: 1, plane: Plane::Depth(vec![0.0, 1.0]) }; + let bytes = pack(&z, FORMAT_DEPTH, TYPE_UNSIGNED_SHORT, 0, &rec).unwrap(); + assert_eq!(&bytes[..4], &[0, 0, 0xff, 0xff]); + // A bitmap packs most significant bit first, or least when asked. + let bits = Image { w: 10, h: 1, plane: Plane::Index(vec![1, 0, 0, 0, 0, 0, 0, 1, 1, 0]) }; + assert_eq!(&pack(&bits, FORMAT_COLOUR_INDEX, TYPE_BITMAP, 0, &rec).unwrap()[..2], &[0x81, 0x80]); + assert_eq!(&pack(&bits, FORMAT_COLOUR_INDEX, TYPE_BITMAP, 0, &Record { lsb_first: true, ..rec }).unwrap()[..2], &[0x81, 0x01]); + } + + /// The skips in a pack record are the guest's, so the packer sizes its buffer with checked arithmetic and + /// refuses one past its cap: a skip of 2^32 rows must not ask the allocator for terabytes. + #[test] + fn a_huge_pack_skip_is_no_buffer() { + let img = Image { w: 8000, h: 1, plane: Plane::Colour(vec![[0.0; 4]; 8000]) }; + let rec = Record { alignment: 1, ..Record::default() }; + assert_eq!(packed_len(8000, 1, FORMAT_RED, TYPE_UNSIGNED_SHORT, 0, &rec), Some(16000)); + for skipped in [Record { skip_rows: u32::MAX, ..rec }, Record { skip_pixels: u32::MAX, ..rec }, Record { skip_rows: 1 << 20, row_length: 1 << 20, ..rec }] { + assert!(pack(&img, FORMAT_RED, TYPE_UNSIGNED_SHORT, 0, &skipped).is_none(), "{skipped:?}"); + } + // A skip that fits is built, with the image after it. + let b = pack(&img, FORMAT_RED, TYPE_UNSIGNED_SHORT, 0, &Record { skip_rows: 2, ..rec }).unwrap(); + assert_eq!(b.len(), 3 * 16000); + assert_eq!(packed_len(1, 1, FORMAT_RED, TYPE_UNSIGNED_BYTE, 0, &Record { skip_rows: u32::MAX, row_length: u32::MAX, ..rec }), Some(u32::MAX as u64 * u32::MAX as u64 + u32::MAX as u64)); + } + + /// The extension formats: ABGR keeps its components in the order alpha, blue, green, red; CMYK and CMYKA + /// are sized by the pipe's component table (four and five) so a header's image words are consumed, and refused. + #[test] + fn the_extension_formats_have_their_sizes_and_abgr_its_order() { + let rec = Record { alignment: 1, ..Record::default() }; + let rgba = Image { w: 1, h: 1, plane: Plane::Colour(vec![[1.0, 0.5, 0.25, 0.0]]) }; + let b = pack(&rgba, FORMAT_ABGR, TYPE_UNSIGNED_BYTE, 0, &rec).unwrap(); + assert_eq!(&b[..4], &[0, 64, 128, 255], "alpha, blue, green, red"); + assert_eq!(unpack(&header(1, 1, FORMAT_ABGR, TYPE_UNSIGNED_BYTE, 0), &rec, &b).unwrap().plane, Plane::Colour(vec![[1.0, 128.0 / 255.0, 64.0 / 255.0, 0.0]])); + assert_eq!(pixel_bytes(FORMAT_ABGR, TYPE_UNSIGNED_SHORT), Some(8)); + assert_eq!(pixel_bytes(FORMAT_ABGR, TYPE_8_8_8_8), None, "its packed layouts are not recorded"); + assert_eq!((pixel_bytes(FORMAT_CMYK, TYPE_UNSIGNED_BYTE), pixel_bytes(FORMAT_CMYKA, TYPE_UNSIGNED_BYTE)), (Some(4), Some(5))); + assert!(unpack(&header(1, 1, FORMAT_CMYK, TYPE_UNSIGNED_BYTE, 0), &rec, &[0; 4]).is_none()); + assert!(pack(&rgba, FORMAT_CMYKA, TYPE_UNSIGNED_BYTE, 0, &rec).is_none()); + assert_eq!(header(3, 2, FORMAT_CMYKA, TYPE_UNSIGNED_BYTE, 0).image_words(&rec), Some(8), "3 x 2 pixels of 5 bytes, rows packed by 1"); + } + + /// The destination's low address bits lead the buffer, and a row of 3 bytes pads to the alignment. + #[test] + fn a_read_starts_at_the_destinations_byte_offset() { + let img = Image { w: 3, h: 2, plane: Plane::Colour(vec![[1.0; 4]; 6]) }; + let b = pack(&img, FORMAT_RED, TYPE_UNSIGNED_BYTE, 2, &Record::default()).unwrap(); + assert_eq!(b, vec![0, 0, 255, 255, 255, 0, 255, 255, 255, 0, 0, 0]); + } + + /// The transfer stages: scale and bias with a clamp, the index shift and offset, and the + /// maps only when their flag is on. + #[test] + fn transfer_scale_bias_and_maps() { + let mut p = Pixels::default(); + p.scale = [0.5, 1.0, 1.0, 1.0]; + p.bias = [0.0, 0.5, 0.0, 0.0]; + assert_eq!(p.colour([1.0, 0.75, 0.0, 1.0]), [0.5, 1.0, 0.0, 1.0]); + p.map_colour = true; + p.maps.insert(MAP_RED_TO_RED, Map { max: 0.0, values: vec![1.0, 0.0] }); + assert_eq!(p.colour([1.0, 0.0, 0.0, 1.0])[0], 0.0, "a two-entry map flips red"); + p.suspended = true; + assert_eq!(p.colour([1.0, 0.75, 0.0, 1.0]), [1.0, 0.75, 0.0, 1.0], "suspended, nothing applies"); + p.suspended = false; + p.index_shift = -1; + p.index_offset = 3; + assert_eq!(p.integer(10, MAP_INDEX_TO_INDEX, false), 8); + p.maps.insert(MAP_STENCIL_TO_STENCIL, Map { max: 0.0, values: vec![9.0, 8.0, 7.0] }); + assert_eq!(p.integer(0, MAP_STENCIL_TO_STENCIL, true), 7, "a lookup past the table takes the last entry"); + } + + /// Only a value of exactly 1.0 makes an accumulation operation two passes. + #[test] + fn only_a_value_of_one_splits_an_accumulation_into_passes() { + assert_eq!(accumulation_passes(ACCUM, 1.0), vec![(ACCUM, 0.5), (ACCUM, 0.5)]); + assert_eq!(accumulation_passes(ACCUM_ADD, 1.0), vec![(ACCUM_ADD, 0.5), (ACCUM_ADD, 0.5)]); + assert_eq!(accumulation_passes(ACCUM_LOAD, 1.0), vec![(ACCUM_LOAD, 0.5), (ACCUM, 0.5)]); + assert_eq!(accumulation_passes(ACCUM_MULT, 1.0), vec![]); + assert_eq!(accumulation_passes(ACCUM_RETURN, 1.0), vec![(ACCUM_RETURN, 1.0)]); + for op in [ACCUM, ACCUM_LOAD, ACCUM_MULT, ACCUM_ADD, ACCUM_RETURN] { + for v in [0.0, 0.5, 0.999_999_9, 1.000_000_1, 2.0, -1.0] { + assert_eq!(accumulation_passes(op, v), vec![(op, v)], "{op:#x} by {v}"); + } + } + } + + /// What the context keeps of the pixel operations is saved and restored. + #[test] + fn the_pixel_state_round_trips_through_a_stream() { + let mut p = Pixels::default(); + p.unpack.alignment = 1; + p.pack.swap_bytes = true; + p.scale[2] = 0.25; + p.zoom = [2.0, -1.0]; + p.maps.insert(MAP_INDEX_TO_RED, Map { max: 65535.0, values: vec![1.0, 2.0, 3.0] }); + p.read = Some(2); + p.pos = RasterPos { valid: false, x: 3.5, y: 4.5, z: 9, colour: [0.1, 0.2, 0.3, 0.4] }; + p.pending = Some([1, 2, 3, 4, 5, 6, 7, 8]); + let mut w = Writer::default(); + p.visit(&mut w); + let mut q = Pixels::default(); + let mut r = Reader::new(&w.out); + q.visit(&mut r); + assert!(r.finish().is_ok()); + assert_eq!(q, p); + } +} diff --git a/src/dev/kona/back/gl/query.rs b/src/dev/kona/back/gl/query.rs new file mode 100644 index 000000000..e0be47cd3 --- /dev/null +++ b/src/dev/kona/back/gl/query.rs @@ -0,0 +1,732 @@ +//! Queries, the error query, select and feedback. +//! +//! A query is a short token and an answer in the context's return page: a completion word +//! (word 0, which the pipe writes last), the count of data words, then the data from byte 8 +//! (the definitions table's section on queries, which establishes all three). The client waits +//! on the completion word, so every query token the model knows of is answered; one it cannot answer fully gets an empty or zero reply and is counted, +//! which is the table's own inference of what keeps the client from hanging. Where the table +//! grades a layout inferred or unknown, the choice is made where the code makes it and +//! counted. The choices: +//! +//! - Current values (0x03b): the table's established layout, in the order the pipe pushes the +//! current attributes: colour at byte 0, colour index at 16, normal at 20, texture coordinates +//! at 32 and the edge flag at 48. The colour index is zero (the model keeps none). The edge +//! flag word is a single-precision float, which the client truncates to an integer for integer +//! and boolean reads (established), so a set flag is 1.0 and a clear one 0.0; the engine +//! handler's own bit pattern is not decoded (inferred). +//! - Raster position (0x03d): colour at 0, index at 16, texture coordinates at 20, position at 36 +//! (window x, y, depth as a fraction, 1), the valid flag at 52 (an integer word) and the distance +//! at 56. The index, texture coordinates and distance are zero (the model keeps none). +//! - Generic state (0x04c): the 171 names the table lists, each with its reply's word count; the +//! values the context keeps, as floats (the depth range and depth clear value as doubles, as the +//! firmware sends them), zeros of the right length for a name it keeps nothing for, counted by +//! name; a name with no row is an empty reply and an invalid-enumerant error. +//! - Light and material replies carry what `Lighting` keeps: three components of an ambient, +//! diffuse (material) or specular colour, so the fourth reads as 1; a spot cutoff is the +//! arc cosine of the stored cosine. +//! - The line-mode limit table (0x0ee) is a value the recordings show another model of the board +//! returning (eight rows of 0x2000, 0x2000, 0, 0). The hardware's is not known. +//! - Select and feedback follow GL's own layouts for the words the table does not decode: +//! feedback is the record code, then (polygons) the vertex count, then each vertex as the +//! feedback type says; a select record is a marker word of 1, the name count, the nearest and +//! farthest depth and the names. The table establishes the record codes and sizes. Inferred: +//! the order of the values in a vertex, the vertex scaling (x and y by 0.0625, z by 2^-20, which +//! take the engine's sixteenths of a pixel and its 20-bit depth back to a pixel and a fraction; +//! see `defs::reply`) and the select depths, unsigned 32-bit integers clamped at 4294967040.0. +//! A hit is a primitive that survives clipping and culling. Names are pushed up to 64 deep (a +//! model cap). Output past the buffer the client gave sets the overflow flag in the completion's +//! status. + +use super::state::Gl; +use crate::dev::kona::defs::*; +use crate::dev::kona::stream::Stream; +use std::collections::BTreeMap; + +pub const RENDER: u32 = 0x1c00; +pub const FEEDBACK: u32 = 0x1c01; +pub const SELECT: u32 = 0x1c02; +/// Feedback types. +pub const FEEDBACK_2D: u32 = 0x600; +pub const FEEDBACK_3D: u32 = 0x601; +pub const FEEDBACK_3D_COLOUR: u32 = 0x602; +pub const FEEDBACK_3D_COLOUR_TEXTURE: u32 = 0x603; +pub const FEEDBACK_4D_COLOUR_TEXTURE: u32 = 0x604; +/// Feedback record codes. +pub const PASS_THROUGH: u32 = 0x700; +pub const POINT: u32 = 0x701; +pub const LINE: u32 = 0x702; +pub const POLYGON: u32 = 0x703; +/// The completion's status bit for a full buffer. +pub const OVERFLOW_BIT: u32 = 0x800; +/// The most names the stack holds (the model's cap). +pub const NAME_STACK_DEPTH: usize = 64; + +/// What a feedback or select pass keeps while it runs, and until the host collects it. +#[derive(Clone, Debug, PartialEq)] +pub struct Feed { + pub mode: u32, + pub kind: u32, + pub names: Vec, + /// A select hit not yet written: the depth range so far, and whether the names it + /// would write are the stack's now. + pub open: Option<(u32, u32)>, + pub words: Vec, + /// The buffer the client gave: words that fit. None: no start seen. + pub limit: Option, + pub overflow: bool, + /// A bracket was opened (a start) while a feedback or select mode was current: its finish + /// takes the result. The client switches the mode before it finishes the one it leaves, so + /// the first finish of a pass belongs to the mode before. + pub bracket: bool, +} + +impl Default for Feed { + fn default() -> Self { + Feed { mode: RENDER, kind: FEEDBACK_2D, names: Vec::new(), open: None, words: Vec::new(), limit: None, overflow: false, bracket: false } + } +} + +impl Feed { + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { mode: _, kind: _, names: _, open: _, words: _, limit: _, overflow: _, bracket: _ } = self; + s.u32(&mut self.mode); + s.u32(&mut self.kind); + s.vec_u32(&mut self.names); + s.opt(&mut self.open, |s, v| { + s.u32(&mut v.0); + s.u32(&mut v.1); + }); + s.vec_u32(&mut self.words); + let mut limit = self.limit.map_or(u32::MAX, |l| l as u32); + s.u32(&mut limit); + self.limit = (limit != u32::MAX).then_some(limit as usize); + s.bool(&mut self.overflow); + s.bool(&mut self.bracket); + } + + /// A start: the buffer's size in bytes, and an empty result. + pub fn start(&mut self, bytes: u32) { + self.words.clear(); + self.open = None; + self.overflow = false; + self.bracket = self.mode != RENDER; + self.limit = Some(bytes as usize / 4); + } + + fn push(&mut self, words: &[u32]) { + let room = self.limit.unwrap_or(usize::MAX).saturating_sub(self.words.len()); + if words.len() > room { + self.overflow = true; + self.words.extend_from_slice(&words[..room]); + } else { + self.words.extend_from_slice(words); + } + } + + /// A select hit written out: the marker, the name count, the depths, the names. + fn close(&mut self) { + if let Some((near, far)) = self.open.take() { + let mut r = vec![1, self.names.len() as u32, near, far]; + r.extend_from_slice(&self.names); + self.push(&r); + } + } + + /// A primitive that survived clipping and culling, with its window vertices, in the active mode. + /// Returns whether the pass took it (and so nothing is drawn). + pub fn primitive(&mut self, code: u32, v: &[Fv]) -> bool { + match self.mode { + FEEDBACK => { + let mut r = vec![code]; + if code == POLYGON { + r.push(v.len() as u32); + } + for p in v { + r.extend(p.words(self.kind)); + } + self.push(&r); + true + } + SELECT => { + let (near, far) = v.iter().fold((u32::MAX, 0), |(n, f), p| { + let z = (p.z.clamp(0.0, 1.0) * reply::SELECT_DEPTH_MAX) as u32; + (n.min(z), f.max(z)) + }); + self.open = Some(self.open.map_or((near, far), |(n, f)| (n.min(near), f.max(far)))); + true + } + _ => false, + } + } + + /// A name-stack token. Select mode only: in the other modes the words are dropped. + fn names(&mut self, selector: u32, a: &[u32]) -> Option { + if self.mode != SELECT { + return None; + } + self.close(); + match selector { + CLEAR_SELECTION_NAME_STACK => self.names.clear(), + REPLACE_TOP_SELECTION_NAME => match self.names.last_mut() { + Some(t) => *t = a.first().copied().unwrap_or(0), + None => return Some(0x502), + }, + PUSH_SELECTION_NAME if self.names.len() >= NAME_STACK_DEPTH => return Some(0x503), + PUSH_SELECTION_NAME => self.names.push(a.first().copied().unwrap_or(0)), + _ => { + if self.names.pop().is_none() { + return Some(0x504); + } + } + } + None + } + + /// Take the result for the host: what was collected and whether the buffer overflowed. The + /// mode is left as it is. + pub fn take(&mut self) -> (Vec, bool) { + self.close(); + self.bracket = false; + self.limit = None; + (std::mem::take(&mut self.words), std::mem::take(&mut self.overflow)) + } +} + +/// A vertex as feedback reports it: window position, depth as a fraction, 1 / w, colour and texture. +#[derive(Clone, Copy, Debug, Default)] +pub struct Fv { + pub x: f32, + pub y: f32, + pub z: f32, + pub w: f32, + pub c: [f32; 4], + pub t: [f32; 4], +} + +impl Fv { + /// The vertex as the pipe's formatter writes it (inferred, see `defs::reply`): the engine holds + /// window x and y in sixteenths of a pixel and depth over the 20-bit window range, and the + /// formatter scales them back. + fn words(&self, kind: u32) -> Vec { + let b = |v: f32| v.to_bits(); + let xy = |v: f32| v * 16.0 * reply::FEEDBACK_XY_SCALE; + let z = self.z * reply::WINDOW_DEPTH_RANGE * reply::FEEDBACK_Z_SCALE; + let mut r = vec![b(xy(self.x)), b(xy(self.y))]; + match kind { + FEEDBACK_2D => {} + FEEDBACK_3D => r.push(b(z)), + FEEDBACK_3D_COLOUR => { + r.push(b(z)); + r.extend(self.c.map(b)); + } + FEEDBACK_3D_COLOUR_TEXTURE => { + r.push(b(z)); + r.extend(self.c.map(b)); + r.extend(self.t.map(b)); + } + _ => { + r.extend([b(z), b(self.w)]); + r.extend(self.c.map(b)); + r.extend(self.t.map(b)); + } + } + r + } +} + +/// What the queries did. +#[derive(Clone, Debug, Default)] +pub struct Counts { + pub answered: u64, + /// Recognised query tokens answered with an empty reply, by selector. + pub empty: BTreeMap, + /// Generic-state names answered with one zero word. + pub unknown_state: BTreeMap, + /// Tokens nothing sends (the integer and double query forms), refused by the pipe. + pub unsent: u64, + pub hits: u64, + pub feedback_values: u64, + pub select_errors: u64, + pub passes: u64, + pub overflows: u64, + /// Replies with no context current or no return page registered, which are not posted. + pub unposted: u64, + /// Push-attributes tokens, each answered with a bare completion. + pub pushes: u64, + /// Pushes onto a full stack, which the pipe also closes. + pub pushes_full: u64, + /// Pushes whose mask had a bit the pipe reads back, which the model answers with no data. + pub pushes_unread: u64, + pub pops: u64, +} + +impl Counts { + pub fn render(&self) -> String { + format!( + "{} answered, {} empty by selector {:?}, {} unknown state names {:?}, {} unsent forms, {} not posted; attribute pushes {} ({} onto a full stack, {} with a read-back left empty), pops {}; select and feedback {} passes, {} words, {} hits, {} overflowed", + self.answered, + self.empty.values().sum::(), + self.empty.iter().map(|(k, v)| format!("{k:#x} x {v}")).collect::>(), + self.unknown_state.values().sum::(), + self.unknown_state.iter().map(|(k, v)| format!("{k:#x} x {v}")).collect::>(), + self.unsent, + self.unposted, + self.pushes, + self.pushes_full, + self.pushes_unread, + self.pops, + self.passes, + self.feedback_values, + self.hits, + self.overflows + ) + } +} + +/// The tokens `answer` handles: every query token the table lists, including those that +/// nothing sends. +pub fn is_query(selector: u32) -> bool { + matches!( + selector, + QUERY_CURRENT_VERTEX_ATTRIBUTES + | QUERY_CLIP_PLANE + | QUERY_RASTER_POSITION_STATE + | QUERY_PENDING_ERROR + | QUERY_STATE_FLOAT + | QUERY_STATE_INTEGER + | QUERY_LIGHT_FLOAT + | QUERY_LIGHT_INTEGER + | QUERY_MATERIAL_FLOAT + | QUERY_MATERIAL_INTEGER + | QUERY_PIXEL_MAP_FLOAT + | QUERY_POLYGON_STIPPLE + | QUERY_STATE_GENERIC + | QUERY_TEXTURE_ENVIRONMENT_FLOAT + | QUERY_TEXTURE_ENVIRONMENT_INTEGER + | QUERY_TEXTURE_GENERATION_FLOAT + | QUERY_COLOUR_TABLE + | QUERY_COLOUR_TABLE_PARAMETERS + | QUERY_LINE_MODE_LIMIT_TABLE + | QUERY_CONVOLUTION_FILTER + | QUERY_SEPARABLE_FILTER + | QUERY_PIXEL_TRANSFORM_PARAMETERS_INTEGER + | QUERY_HISTOGRAM + | QUERY_HISTOGRAM_PARAMETERS + | QUERY_MIN_MAX_RESULTS + | QUERY_MIN_MAX_PARAMETERS + | QUERY_ENGINE_SIDE_TEXTURE_ATTRIBUTES + | QUERY_DEFORMATION_STATE + ) +} + +/// The tokens of the select and feedback modes that act on the context. +pub fn is_mode(selector: u32) -> bool { + matches!(selector, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES | SELECT_FEEDBACK_RECORD_TYPE | CLEAR_SELECTION_NAME_STACK | REPLACE_TOP_SELECTION_NAME | PUSH_SELECTION_NAME | POP_SELECTION_NAME | INSERT_FEEDBACK_PASS_THROUGH_MARKER) +} + +/// A mode, type, name-stack or pass-through token. Returns whether it was one. +pub fn mode_command(gl: &mut Gl, c: &mut Counts, selector: u32, a: &[u32]) -> bool { + match (selector, a) { + (SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[mode]) => { + if matches!(mode, RENDER | FEEDBACK | SELECT) { + gl.feed.close(); + gl.feed.mode = mode; + c.passes += (mode != RENDER) as u64; + } else if gl.error == 0 { + gl.error = 0x500; + } + true + } + (SELECT_FEEDBACK_RECORD_TYPE, &[kind]) => { + if (FEEDBACK_2D..=FEEDBACK_4D_COLOUR_TEXTURE).contains(&kind) { + gl.feed.kind = kind; + } else if gl.error == 0 { + gl.error = 0x500; + } + true + } + (CLEAR_SELECTION_NAME_STACK | REPLACE_TOP_SELECTION_NAME | PUSH_SELECTION_NAME | POP_SELECTION_NAME, _) => { + if let Some(e) = gl.feed.names(selector, a) { + c.select_errors += 1; + if gl.error == 0 { + gl.error = e; + } + } + true + } + // A pass-through marker acts only in feedback mode. + (INSERT_FEEDBACK_PASS_THROUGH_MARKER, &[value]) => { + if gl.feed.mode == FEEDBACK { + gl.feed.push(&[PASS_THROUGH, value]); + } + true + } + _ => false, + } +} + +/// The attribute stack's depth: a push onto a full one overflows. +pub const ATTRIBUTE_STACK_DEPTH: u32 = 16; +/// The error a push onto a full stack leaves (the GL stack-overflow error). +const STACK_OVERFLOW: u32 = 0x503; +/// The mask bits the pipe's handler does not test: the evaluator bit and the list bit. Every other +/// bit is tested and some send data to the host. +const MASK_UNTESTED: u32 = 0x10000 | 0x20000; + +/// A push-attributes token (0xc9). The pipe opens a read-back before it looks at the mask and closes it +/// on every path, a push onto a full stack included, and the close writes the completion word, so every +/// push is answered and the client's wait on it ends (inferred: the table grades the bracket's +/// always completing as inferred, and so the literal completion value, as for every query). The reply carries no data: the model reads nothing back for the +/// mask bits that do send some, and counts those pushes. A full stack sets the stack-overflow error +/// (inferred from the handler's call to its overflow routine) and does not grow. +pub fn push_attributes(gl: &mut Gl, c: &mut Counts, mask: u32) -> Vec { + c.pushes += 1; + c.pushes_unread += (mask & !MASK_UNTESTED != 0) as u64; + if gl.attrib_depth >= ATTRIBUTE_STACK_DEPTH { + c.pushes_full += 1; + if gl.error == 0 { + gl.error = STACK_OVERFLOW; + } + } else { + gl.attrib_depth += 1; + } + Vec::new() +} + +/// A pop-attributes token (0xc8). Nothing is known to wait on it, so nothing is posted; the depth +/// only keeps a later push honest about a full stack. +pub fn pop_attributes(gl: &mut Gl, c: &mut Counts) { + c.pops += 1; + gl.attrib_depth = gl.attrib_depth.saturating_sub(1); +} + +fn floats(v: &[f32]) -> Vec { + v.iter().map(|x| x.to_bits()).collect() +} + +/// The reply words to a query (after the count), and whether the model could answer it. +pub fn answer(gl: &mut Gl, c: &mut Counts, selector: u32, a: &[u32]) -> Vec { + let out = match (selector, a) { + (QUERY_CURRENT_VERTEX_ATTRIBUTES, _) => { + let mut r = [0u32; reply::CURRENT_WORDS]; + r[reply::CURRENT_COLOUR..][..4].copy_from_slice(&floats(&gl.color)); + r[reply::CURRENT_NORMAL..][..3].copy_from_slice(&floats(&gl.normal)); + r[reply::CURRENT_TEXTURE..][..4].copy_from_slice(&floats(&gl.texcoord)); + r[reply::CURRENT_EDGE_FLAG] = (gl.edge_flag as u32 as f32).to_bits(); + r.to_vec() + } + (QUERY_RASTER_POSITION_STATE, _) => { + let p = gl.pix.pos; + let mut r = [0u32; reply::RASTER_WORDS]; + r[reply::RASTER_COLOUR..][..4].copy_from_slice(&floats(&p.colour)); + r[reply::RASTER_POSITION..][..4].copy_from_slice(&floats(&[p.x, p.y, p.z as f32 / 4_294_967_040.0, 1.0])); + r[reply::RASTER_VALID] = p.valid as u32; + r.to_vec() + } + (QUERY_PENDING_ERROR, _) => vec![std::mem::take(&mut gl.error)], + // The plane, four floats (established; that the pipe keeps it in eye space is inferred from + // OpenGL's query). A name outside 0x3000 to 0x3005 is the client's error to + // raise (it checks the name before it asks); one that arrives anyway is answered with nothing. + (QUERY_CLIP_PLANE, &[name]) if (name.wrapping_sub(gl::CLIP_PLANE0) as usize) < gl::CLIP_PLANES => floats(&gl.user_plane[(name - gl::CLIP_PLANE0) as usize]), + (QUERY_CLIP_PLANE, _) => { + *c.empty.entry(selector).or_default() += 1; + if gl.error == 0 { + gl.error = gl::INVALID_ENUM; + } + Vec::new() + } + (QUERY_LIGHT_FLOAT, &[n, name]) => light(gl, n, name), + (QUERY_MATERIAL_FLOAT, &[face, name]) => material(gl, face, name), + (QUERY_PIXEL_MAP_FLOAT, &[name, start, count]) => pixel_map(gl, name, start, count), + (QUERY_POLYGON_STIPPLE, _) => gl.raster.polygon_stipple.to_vec(), + (QUERY_STATE_GENERIC, &[name]) => state(gl, c, name), + (QUERY_TEXTURE_ENVIRONMENT_FLOAT, &[name]) => match name { + 0x2200 => vec![gl.raster.env_mode], + 0x2201 => floats(&gl.raster.env_colour), + _ => vec![0; 4], + }, + (QUERY_LINE_MODE_LIMIT_TABLE, _) => (0..8).flat_map(|_| [0x2000, 0x2000, 0, 0]).collect(), + // Anything else the table lists: nothing the model keeps answers it. + (QUERY_STATE_FLOAT | QUERY_STATE_INTEGER | QUERY_LIGHT_INTEGER | QUERY_MATERIAL_INTEGER | QUERY_TEXTURE_ENVIRONMENT_INTEGER, _) => { + c.unsent += 1; + Vec::new() + } + _ => { + *c.empty.entry(selector).or_default() += 1; + Vec::new() + } + }; + c.answered += 1; + out +} + +fn light(gl: &Gl, n: u32, name: u32) -> Vec { + let Some(l) = gl.light.lights.get(n as usize) else { return vec![0; 4] }; + let rgba = |c: [f32; 3]| floats(&[c[0], c[1], c[2], 1.0]); + match name { + 0x1200 => rgba(l.ambient), + 0x1201 => rgba(l.diffuse), + 0x1202 => rgba(l.specular), + 0x1203 => floats(&l.pos), + 0x1204 => floats(&l.spot_dir), + 0x1205 => floats(&[l.spot_exp]), + 0x1206 => floats(&[l.spot_cos_cutoff.clamp(-1.0, 1.0).acos().to_degrees()]), + 0x1207 => floats(&[l.atten[2]]), + 0x1208 => floats(&[l.atten[0]]), + 0x1209 => floats(&[l.atten[1]]), + _ => vec![0; 4], + } +} + +fn material(gl: &Gl, face: u32, name: u32) -> Vec { + let m = &gl.light.mat[(face == gl::BACK) as usize]; + let rgba = |c: [f32; 3]| floats(&[c[0], c[1], c[2], 1.0]); + match name { + 0x1200 => rgba(m.ambient), + 0x1201 => floats(&m.diffuse), + 0x1202 => rgba(m.specular), + 0x1600 => rgba(m.emission), + 0x1601 => floats(&[m.shininess]), + _ => vec![0; 3], + } +} + +/// One 16-byte entry a map entry, the value in the component's slot (0 for index and red). +fn pixel_map(gl: &Gl, name: u32, start: u32, count: u32) -> Vec { + use crate::dev::kona::defs::pixel::*; + let slot = match name { + n if (MAP_INDEX_TO_RED..MAP_INDEX_TO_RED + 4).contains(&n) => (n - MAP_INDEX_TO_RED) as usize, + n if (MAP_RED_TO_RED..MAP_RED_TO_RED + 4).contains(&n) => (n - MAP_RED_TO_RED) as usize, + _ => 0, + }; + let Some(m) = gl.pix.maps.get(&name) else { return vec![0; 4 * count.min(512) as usize] }; + (start..start.saturating_add(count.min(512))) + .flat_map(|i| { + let v = m.values.get(i as usize).copied().unwrap_or(0.0); + let v = if m.max > 0.0 { v / m.max } else { v }; + let mut e = [0u32; 4]; + e[slot] = v.to_bits(); + e + }) + .collect() +} + +/// The words of a double as two reply words, high first (the client's element type for the depth range +/// and the depth clear value, which the firmware answers in two words each). +fn double(v: f32) -> [u32; 2] { + let b = (v as f64).to_bits(); + [(b >> 32) as u32, b as u32] +} + +/// The reply to a generic state name: the table's rows give the word count of each of the 171 names the +/// pipe answers (`defs::state_query`); a name with no row is answered with an empty reply and an +/// invalid-enumerant error, as the firmware does. The values are what the context keeps, as floats; a row +/// the model keeps nothing for replies zeros of the right length and is counted by name, as is a name with +/// no row. +fn state(gl: &mut Gl, c: &mut Counts, name: u32) -> Vec { + let Some(words) = state_query::words(name) else { + *c.unknown_state.entry(name).or_default() += 1; + if gl.error == 0 { + gl.error = 0x500; + } + return Vec::new(); + }; + let r = &gl.raster; + let mut out = match name { + 0xba6 => floats(gl.mv.get()), + 0xba7 => floats(gl.proj.get()), + 0xba8 => floats(gl.tex.get()), + 0xba0 => floats(&[gl.mode as f32]), + 0xba2 => floats(&r.viewport), + 0xb70 => r.depth_range.iter().flat_map(|&v| double(v)).collect(), + 0xc10 => floats(&r.scissor.map(|v| v as f32)), + 0xc22 => floats(&gl.clear_colour), + 0xb73 => double(gl.clear_depth).to_vec(), + 0xb91 => floats(&[gl.clear_stencil as f32]), + 0xb80 => floats(&gl.pix.accum_clear), + 0xbc2 => floats(&[r.alpha_ref]), + 0x8005 => floats(&r.blend_colour), + 0xb21 => floats(&[r.line_width]), + 0xb11 => floats(&[r.point_size]), + // A capability's enable bit, as an integer word (the table says the client reads booleans by the low byte). + 0xbd0 => vec![r.dither as u32], + // The fog rows: the enable, then mode, density, start, end and colour as floats (the colour's + // fourth word is not kept). + 0xb60 => vec![r.fog.on as u32], + // The polygon modes, front then back, as numbers. + 0xb40 => floats(&r.polygon_mode.map(|m| m as f32)), + 0xb65 => floats(&[r.fog.mode as f32]), + 0xb62 => floats(&[r.fog.density]), + 0xb63 => floats(&[r.fog.start]), + 0xb64 => floats(&[r.fog.end]), + 0xb66 => floats(&r.fog.colour), + _ => { + *c.unknown_state.entry(name).or_default() += 1; + Vec::new() + } + }; + out.resize(words, 0); + out +} + +/// The bytes of a reply: the count, then the words. +pub fn reply_bytes(words: &[u32]) -> Vec { + std::iter::once(words.len() as u32).chain(words.iter().copied()).flat_map(u32::to_be_bytes).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn select_writes_a_record_per_name_change_with_the_depths_it_saw() { + let mut f = Feed::default(); + f.start(1024); + let mut c = Counts::default(); + let mut gl = Gl::default(); + gl.feed = f; + mode_command(&mut gl, &mut c, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[SELECT]); + mode_command(&mut gl, &mut c, CLEAR_SELECTION_NAME_STACK, &[0]); + mode_command(&mut gl, &mut c, PUSH_SELECTION_NAME, &[7]); + let v = |z: f32| Fv { z, ..Fv::default() }; + assert!(gl.feed.primitive(POLYGON, &[v(0.25), v(0.5)])); + assert!(gl.feed.primitive(POLYGON, &[v(0.75)])); + // A new name closes the open hit. + mode_command(&mut gl, &mut c, REPLACE_TOP_SELECTION_NAME, &[9]); + assert!(gl.feed.primitive(POINT, &[v(1.0)])); + mode_command(&mut gl, &mut c, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[RENDER]); + let (words, overflow) = gl.feed.take(); + assert!(!overflow); + let z = |x: f32| (x * 4_294_967_040.0) as u32; + assert_eq!(words, vec![1, 1, z(0.25), z(0.75), 7, 1, 1, z(1.0), z(1.0), 9]); + } + + #[test] + fn feedback_records_follow_the_type_and_overflow_sets_the_flag() { + let mut gl = Gl::default(); + let mut c = Counts::default(); + gl.feed.start(4 * 8); + mode_command(&mut gl, &mut c, SELECT_FEEDBACK_RECORD_TYPE, &[FEEDBACK_3D]); + mode_command(&mut gl, &mut c, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[FEEDBACK]); + let v = Fv { x: 1.0, y: 2.0, z: 0.5, ..Fv::default() }; + mode_command(&mut gl, &mut c, INSERT_FEEDBACK_PASS_THROUGH_MARKER, &[3.0f32.to_bits()]); + gl.feed.primitive(LINE, &[v, v]); + // Two words of the pass-through, then the line's code and the first vertex (3 words); the rest does not fit. + let (w, overflow) = gl.feed.take(); + assert!(overflow); + assert_eq!(w.len(), 8); + assert_eq!(&w[..2], &[PASS_THROUGH, 3.0f32.to_bits()]); + let z = 0.5 * reply::WINDOW_DEPTH_RANGE * reply::FEEDBACK_Z_SCALE; + assert_eq!(&w[2..6], &[LINE, 1.0f32.to_bits(), 2.0f32.to_bits(), z.to_bits()]); + } + + #[test] + fn name_stack_errors_and_modes_other_than_select_drop_the_tokens() { + let mut gl = Gl::default(); + let mut c = Counts::default(); + mode_command(&mut gl, &mut c, PUSH_SELECTION_NAME, &[1]); + assert!(gl.feed.names.is_empty() && gl.error == 0, "in render mode the words are dropped"); + mode_command(&mut gl, &mut c, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[SELECT]); + mode_command(&mut gl, &mut c, POP_SELECTION_NAME, &[0]); + assert_eq!(gl.error, 0x504); + for i in 0..=NAME_STACK_DEPTH { + mode_command(&mut gl, &mut c, PUSH_SELECTION_NAME, &[i as u32]); + } + assert_eq!(gl.feed.names.len(), NAME_STACK_DEPTH); + assert_eq!(c.select_errors, 2); + } + + #[test] + fn current_values_and_the_error_query() { + let mut gl = Gl::default(); + let mut c = Counts::default(); + gl.color = [0.1, 0.2, 0.3, 0.4]; + gl.texcoord = [5.0, 6.0, 7.0, 8.0]; + gl.normal = [0.0, 1.0, 0.0]; + gl.edge_flag = false; + let r = answer(&mut gl, &mut c, QUERY_CURRENT_VERTEX_ATTRIBUTES, &[0]); + assert_eq!(r.len(), 13); + assert_eq!(f32::from_bits(r[0]), 0.1); + assert_eq!([f32::from_bits(r[5]), f32::from_bits(r[6]), f32::from_bits(r[7])], [0.0, 1.0, 0.0], "the normal at byte 20"); + assert_eq!(r.iter().skip(8).take(4).map(|w| f32::from_bits(*w)).collect::>(), [5.0, 6.0, 7.0, 8.0], "all four texture coordinates at byte 32"); + assert_eq!(r[12], 0, "the edge flag at byte 48, a clear flag the float 0.0"); + gl.edge_flag = true; + assert_eq!(answer(&mut gl, &mut c, QUERY_CURRENT_VERTEX_ATTRIBUTES, &[0])[12], 1.0f32.to_bits(), "a set one the float 1.0"); + gl.error = 0x501; + assert_eq!(answer(&mut gl, &mut c, QUERY_PENDING_ERROR, &[0xdbdb_dbdb]), vec![0x501]); + assert_eq!(answer(&mut gl, &mut c, QUERY_PENDING_ERROR, &[0xdbdb_dbdb]), vec![0], "a read clears it"); + assert_eq!(answer(&mut gl, &mut c, QUERY_STATE_GENERIC, &[0x1234]), Vec::::new(), "no row: an empty reply"); + assert_eq!((c.unknown_state[&0x1234], gl.error), (1, 0x500), "and an invalid-enumerant error"); + gl.error = 0; + assert_eq!(answer(&mut gl, &mut c, QUERY_STATE_GENERIC, &[0xb44]), vec![0], "a row the model keeps nothing for: a zero word"); + assert_eq!(answer(&mut gl, &mut c, QUERY_STATE_GENERIC, &[0xb53]), vec![0; 4], "of the row's length"); + assert_eq!(c.unknown_state[&0xb53], 1); + gl.raster.fog.end = 7.0; + gl.raster.fog.on = true; + assert_eq!((answer(&mut gl, &mut c, QUERY_STATE_GENERIC, &[0xb64]), answer(&mut gl, &mut c, QUERY_STATE_GENERIC, &[0xb60])), (vec![7.0f32.to_bits()], vec![1]), "the fog rows answer from the context"); + gl.raster.depth_range = [0.25, 0.75]; + gl.clear_depth = 0.5; + let doubles = |r: Vec| r.chunks(2).map(|w| f64::from_bits((w[0] as u64) << 32 | w[1] as u64)).collect::>(); + assert_eq!(doubles(answer(&mut gl, &mut c, QUERY_STATE_GENERIC, &[0xb70])), [0.25, 0.75], "two doubles, four words"); + assert_eq!(doubles(answer(&mut gl, &mut c, QUERY_STATE_GENERIC, &[0xb73])), [0.5]); + assert_eq!(answer(&mut gl, &mut c, QUERY_LINE_MODE_LIMIT_TABLE, &[0]).len(), 32); + assert_eq!(reply_bytes(&[7]), vec![0, 0, 0, 1, 0, 0, 0, 7]); + } + + #[test] + fn the_raster_position_reply_tiles_its_fifteen_words() { + let mut gl = Gl::default(); + let mut c = Counts::default(); + gl.pix.pos = crate::dev::kona::back::gl::pixels::RasterPos { valid: true, x: 3.0, y: 4.0, z: 0, colour: [0.25, 0.5, 0.75, 1.0] }; + let r = answer(&mut gl, &mut c, QUERY_RASTER_POSITION_STATE, &[0]); + assert_eq!(r.len(), 15); + let f = |i: usize| f32::from_bits(r[i]); + assert_eq!([f(0), f(1), f(2), f(3)], [0.25, 0.5, 0.75, 1.0], "colour at byte 0"); + assert_eq!([f(9), f(10), f(11), f(12)], [3.0, 4.0, 0.0, 1.0], "position at byte 36"); + assert_eq!(r[13], 1, "valid at byte 52"); + assert_eq!(r[14], 0, "distance at byte 56"); + gl.pix.pos.valid = false; + assert_eq!(answer(&mut gl, &mut c, QUERY_RASTER_POSITION_STATE, &[0])[13], 0); + } + + #[test] + fn feedback_vertices_are_scaled_back_from_the_engines_units() { + let v = Fv { x: 100.5, y: 7.25, z: 1.0, ..Fv::default() }; + let w = v.words(FEEDBACK_3D); + assert_eq!(f32::from_bits(w[0]), 100.5, "x: sixteenths of a pixel times 0.0625"); + assert_eq!(f32::from_bits(w[1]), 7.25); + assert_eq!(f32::from_bits(w[2]), 1_048_575.0 / 1_048_576.0, "z: the 20-bit window range times 2^-20"); + } + + #[test] + fn select_depths_are_unsigned_and_clamp_at_the_float_below_2_to_the_32() { + let mut f = Feed::default(); + f.start(64); + f.mode = SELECT; + let v = |z: f32| Fv { z, ..Fv::default() }; + f.primitive(POINT, &[v(3.0)]); + f.primitive(POINT, &[v(-1.0)]); + let (w, _) = f.take(); + assert_eq!(&w[2..4], &[0, 4_294_967_040], "below 0 and above 1 clamp"); + } + + #[test] + fn the_select_and_feedback_state_round_trips_through_a_stream() { + use crate::dev::kona::stream::{Reader, Writer}; + let mut f = Feed::default(); + f.start(64); + f.mode = SELECT; + f.names = vec![1, 2]; + f.open = Some((3, 4)); + f.words = vec![9, 8]; + f.bracket = true; + let mut w = Writer::default(); + f.visit(&mut w); + let mut g = Feed::default(); + let mut r = Reader::new(&w.out); + g.visit(&mut r); + assert!(r.finish().is_ok()); + assert_eq!(g, f); + } +} diff --git a/src/dev/kona/back/gl/state.rs b/src/dev/kona/back/gl/state.rs new file mode 100644 index 000000000..5b5fc1a14 --- /dev/null +++ b/src/dev/kona/back/gl/state.rs @@ -0,0 +1,1220 @@ +//! The GL client's state as the pipe keeps it for one rendering context: +//! matrix stacks, the viewport, scissor and depth range, the lights and +//! material, the current colour, normal and texture coordinate, and the few +//! switches the vertex path reads. +//! +//! The pipe, not the library, keeps the matrices and lights the vertices +//! (software expectation from the senders: the client only writes its +//! arguments). Word layouts are the decoder's in QEMU's public KONA model +//! (jamesbraid/qemu-sgi-origami, `hw/misc/sgi-kona.c`); enumerant values are +//! OpenGL's. A float argument is a 32-bit word holding the float's bits. Colour +//! tables are not taken (counted). + +use crate::dev::kona::back::raster::fog::Fog; +use crate::dev::kona::back::raster::ops::{BlendEq, BlendFactor, Compare, LogicOp, Ownership, StencilOp}; +use crate::dev::kona::back::raster::texel::{EnvMode, TexDesc}; +use crate::dev::kona::back::raster::{AlphaTest, BlendState, DrawState, Rect, Smooth, StencilState}; +use crate::dev::kona::defs::{gl, *}; +use crate::dev::kona::stream::Stream; +use crate::dev::gl::light::{Lighting, MAX_LIGHTS}; +use crate::dev::gl::math::{self, Mat4, Stack}; +use crate::dev::gl::texgen::{self, TexGen}; + +/// Matrix stack depths: OpenGL's minimums for the view stacks and texture. +pub const MV_DEPTH: usize = 32; +pub const PROJ_DEPTH: usize = 4; +pub const TEX_DEPTH: usize = 4; + +pub fn f(bits: u32) -> f32 { + f32::from_bits(bits) +} + +/// Where a draw lands: the window's rectangle in plane pixels, the colour +/// words of the buffers drawn to and the depth word. From the buffer +/// configuration (the layout), the window the kernel placed and glDrawBuffer. +/// A draw with no colour word (no layout yet, an overlay, draw buffer none) +/// writes no colour. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Target { + pub colour: [u32; 2], + pub colours: u8, + /// The depth word and the bits of depth in it; 0 bits: no depth plane. + pub depth_word: u32, + pub depth_bits: u32, + /// The stencil word and its bits (8, or 0: none). + pub stencil_word: u32, + pub stencil_bits: u32, +} + +/// The fog parameters the pipe keeps: mode, density, start, end and colour, with the enable (the +/// generic state query lists a row for each, and for the enable). The fog index is the colour-index +/// mode's and is not kept. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FogState { + pub on: bool, + /// `gl::LINEAR`, `gl::EXP` or `gl::EXP2`. + pub mode: u32, + pub density: f32, + pub start: f32, + pub end: f32, + pub colour: [f32; 3], +} + +impl Default for FogState { + /// OpenGL's initial values (the table lists no start-up contents for the rows). + fn default() -> Self { + FogState { on: false, mode: gl::EXP, density: 1.0, start: 0.0, end: 1.0, colour: [0.0; 3] } + } +} + +/// The switches a draw reads, grouped so a snapshot changes only when one does. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Raster { + pub viewport: [f32; 4], + pub depth_range: [f32; 2], + pub scissor: [i32; 4], + pub scissor_test: bool, + pub depth_test: bool, + pub depth_func: u32, + pub cull: bool, + pub cull_mode: u32, + pub front_cw: bool, + pub flat: bool, + pub texture_2d: bool, + pub texture_3d: bool, + pub lighting: bool, + pub target: Target, + // The fragment operations. The tokens' argument layouts are GL's argument orders + // (inferred: no recording sends one of them, but the blend enable). + pub alpha_test: bool, + pub alpha_func: u32, + pub alpha_ref: f32, + pub stencil_test: bool, + pub stencil_func: u32, + pub stencil_ref: u32, + pub stencil_read_mask: u32, + pub stencil_write_mask: u32, + pub stencil_ops: [u32; 3], + pub blend: bool, + pub blend_factors: [u32; 2], + pub blend_colour: [f32; 4], + pub logic_op_on: bool, + pub logic_op: u32, + pub colour_mask: [bool; 4], + pub depth_mask: bool, + pub dither: bool, + pub offset_fill: bool, + pub offset: [f32; 2], + pub line_width: f32, + pub point_size: f32, + pub line_stipple_on: bool, + /// The line stipple's repeat count and 16-bit pattern. + pub line_stipple: (u32, u16), + pub polygon_stipple_on: bool, + pub polygon_stipple: [u32; 32], + /// The texture environment mode number and colour. + pub env_mode: u32, + pub env_colour: [f32; 4], + pub fog: FogState, + /// How the front and the back faces of a polygon are drawn: `gl::POINT`, `gl::LINE` or `gl::FILL`. + pub polygon_mode: [u32; 2], + /// The smoothing enables (see `raster::smooth`). + pub point_smooth: bool, + pub line_smooth: bool, + pub polygon_smooth: bool, + pub ms: crate::dev::kona::back::raster::multisample::Multisample, +} + +impl Default for Raster { + fn default() -> Self { + Raster { + viewport: [0.0; 4], + depth_range: [0.0, 1.0], + scissor: [0; 4], + scissor_test: false, + depth_test: false, + depth_func: gl::LESS, + cull: false, + cull_mode: gl::BACK, + front_cw: false, + flat: false, + texture_2d: false, + texture_3d: false, + lighting: false, + target: Target::default(), + alpha_test: false, + alpha_func: gl::ALWAYS, + alpha_ref: 0.0, + stencil_test: false, + stencil_func: gl::ALWAYS, + stencil_ref: 0, + stencil_read_mask: !0, + stencil_write_mask: !0, + stencil_ops: [gl::KEEP; 3], + blend: false, + blend_factors: [1, 0], + blend_colour: [0.0; 4], + logic_op_on: false, + logic_op: gl::LOGIC_OP_CLEAR + 3, + colour_mask: [true; 4], + depth_mask: true, + dither: true, + offset_fill: false, + offset: [0.0; 2], + line_width: 1.0, + point_size: 1.0, + line_stipple_on: false, + line_stipple: (1, 0xffff), + polygon_stipple_on: false, + polygon_stipple: [!0; 32], + env_mode: 0x2100, + fog: FogState::default(), + polygon_mode: [gl::FILL; 2], + point_smooth: false, + line_smooth: false, + polygon_smooth: false, + ms: Default::default(), + env_colour: [0.0; 4], + } + } +} + +/// Plain data: a context copy is a `clone`. +#[derive(Clone)] +pub struct Gl { + pub mode: u32, + pub mv: Stack, + pub proj: Stack, + pub tex: Stack, + pub light: Lighting, + /// The normal matrix follows the model-view matrix; recomputed when used + /// after a change. + pub normal_stale: bool, + pub color: [f32; 4], + pub normal: [f32; 3], + pub texcoord: [f32; 4], + pub edge_flag: bool, + /// The user clip planes in eye space, and which are enabled (bit n is plane n). + pub user_plane: [[f32; 4]; gl::CLIP_PLANES], + pub user_on: u32, + /// Texture coordinate generation. + pub texgen: TexGen, + pub raster: Raster, + pub in_begin: bool, + /// The begin token of the open primitive. + pub prim: u32, + pub error: u32, + /// Enabled lights, as bits. + pub light_bits: u32, + /// The raster group changed since the last snapshot. + pub dirty: bool, + /// The buffer configuration's layout (level 0), the window the kernel placed + /// (its origin and clip rectangle in plane pixels), glDrawBuffer's mask (1 front, 2 + /// back; start-up: the back buffer of a double-buffered window, else the front), + /// the double-buffer flag and the clear values. + pub layout: Option, + pub double: bool, + pub origin: [i32; 2], + pub clip: Rect, + pub placed: bool, + /// The window ID the kernel placed, as the signed word, and whether its test is on. + pub window_id: i32, + pub id_test: bool, + /// The display id the kernel gave the window, and which colour buffer the + /// display shows (0 the first word, 1 the second): drawing goes to the other + /// one, and a swap flips them. + pub display_id: u32, + pub shown: u8, + pub draw: Option, + pub clear_colour: [f32; 4], + pub clear_depth: f32, + pub clear_stencil: u32, + /// The bound texture, set by a bind and read when the draw state is built. + pub texture: Option, + /// Pixel-store, transfer and zoom state, the raster position, maps and the accumulation clear value. + pub pix: super::pixels::Pixels, + /// The render mode and what a select or feedback pass has collected. + pub feed: super::query::Feed, + /// How many attribute-stack entries a push has made and a pop has not taken back. + pub attrib_depth: u32, + /// The draw state built from the above, kept until a state command changes it. + ds: Option, +} + +impl std::fmt::Debug for Gl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Gl").field("mode", &self.mode).field("raster", &self.raster).field("in_begin", &self.in_begin).finish_non_exhaustive() + } +} + +impl Default for Gl { + fn default() -> Self { + let mut g = Gl { + mode: gl::MODELVIEW, + mv: Stack { m: [math::IDENT; MV_DEPTH], top: 0 }, + proj: Stack { m: [math::IDENT; PROJ_DEPTH], top: 0 }, + tex: Stack { m: [math::IDENT; TEX_DEPTH], top: 0 }, + light: Lighting::default(), + normal_stale: false, + color: [1.0; 4], + normal: [0.0, 0.0, 1.0], + texcoord: [0.0, 0.0, 0.0, 1.0], + edge_flag: true, + user_plane: [[0.0; 4]; gl::CLIP_PLANES], + user_on: 0, + texgen: TexGen::default(), + raster: Raster::default(), + in_begin: false, + prim: BEGIN_PRIMITIVE_POINTS, + error: 0, + light_bits: 0, + dirty: true, + layout: None, + double: false, + origin: [0; 2], + clip: Rect::default(), + placed: false, + window_id: 0, + id_test: false, + display_id: u32::MAX, + shown: 0, + draw: None, + clear_colour: [0.0; 4], + clear_depth: 1.0, + clear_stencil: 0, + texture: None, + pix: Default::default(), + feed: Default::default(), + attrib_depth: 0, + ds: None, + }; + g.light.init(); + g + } +} + +/// What a state command did that the caller counts. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Taken { + Yes, + /// A form or enumerant the model does not carry out. + Unmodelled, + /// Illegal here (a matrix command between begin and end, a stack error). + Error(u32), +} + + +/// A matrix stack, as the stream module reads and writes it. +fn visit_stack(s: &mut S, st: &mut Stack) { + for m in st.m.iter_mut() { + s.f32s(m); + } + s.u32(&mut st.top); +} + +fn visit_lighting(s: &mut S, l: &mut Lighting) { + for v in [&mut l.on, &mut l.two_sided, &mut l.local_viewer, &mut l.ambient_folded, &mut l.count, &mut l.cmat_param, &mut l.cmat_face, &mut l.cmat_on, &mut l.normalize, &mut l.fog_on, &mut l.fog_mode] { + s.u32(v); + } + s.f32s(&mut l.amb_sum); + for m in l.mat.iter_mut() { + s.f32s(&mut m.emission); + s.f32s(&mut m.ambient); + s.f32s(&mut m.diffuse); + s.f32s(&mut m.specular); + s.f32(&mut m.shininess); + s.u32(&mut m.spec_table); + s.f32(&mut m.spec_scale); + s.f32(&mut m.spec_start); + s.f32s(&mut m.spec); + } + for g in l.lights.iter_mut() { + s.f32s(&mut g.ambient); + s.f32s(&mut g.diffuse); + s.f32s(&mut g.specular); + s.f32s(&mut g.pos); + s.f32s(&mut g.spot_dir); + s.f32s(&mut g.atten); + s.u32(&mut g.spot_on); + s.f32(&mut g.spot_exp); + s.f32(&mut g.spot_cos_cutoff); + s.u32(&mut g.spot_table); + s.f32(&mut g.spot_scale); + s.f32(&mut g.spot_start); + s.f32s(&mut g.spot); + s.i32(&mut g.next); + } + s.i32(&mut l.head); + s.f32s(&mut l.normal_matrix); + s.f32(&mut l.fog_a); + s.f32(&mut l.fog_b); + s.f32s(&mut l.fog_color); +} + +impl Raster { + /// The switches, as the stream module reads and writes them. The target is made again + /// from the layout and the window when the draw state is built. + fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { viewport: _, depth_range: _, scissor: _, scissor_test: _, depth_test: _, depth_func: _, cull: _, cull_mode: _, front_cw: _, flat: _, texture_2d: _, texture_3d: _, lighting: _, target: _, alpha_test: _, alpha_func: _, alpha_ref: _, stencil_test: _, stencil_func: _, stencil_ref: _, stencil_read_mask: _, stencil_write_mask: _, stencil_ops: _, blend: _, blend_factors: _, blend_colour: _, logic_op_on: _, logic_op: _, colour_mask: _, depth_mask: _, dither: _, offset_fill: _, offset: _, line_width: _, point_size: _, line_stipple_on: _, line_stipple: _, polygon_stipple_on: _, polygon_stipple: _, env_mode: _, env_colour: _, fog: _, polygon_mode: _, point_smooth: _, line_smooth: _, polygon_smooth: _, ms: _ } = self; + s.f32s(&mut self.viewport); + s.f32s(&mut self.depth_range); + s.i32s(&mut self.scissor); + for b in [&mut self.scissor_test, &mut self.depth_test, &mut self.cull, &mut self.front_cw, &mut self.flat, &mut self.texture_2d, &mut self.texture_3d, &mut self.lighting, &mut self.alpha_test, &mut self.stencil_test, &mut self.blend, &mut self.logic_op_on, &mut self.depth_mask, &mut self.dither, &mut self.offset_fill, &mut self.line_stipple_on, &mut self.polygon_stipple_on, &mut self.point_smooth, &mut self.line_smooth, &mut self.polygon_smooth] { + s.bool(b); + } + for v in [&mut self.depth_func, &mut self.cull_mode, &mut self.alpha_func, &mut self.stencil_func, &mut self.stencil_ref, &mut self.stencil_read_mask, &mut self.stencil_write_mask, &mut self.logic_op, &mut self.env_mode] { + s.u32(v); + } + s.u32s(&mut self.stencil_ops); + s.u32s(&mut self.blend_factors); + s.f32(&mut self.alpha_ref); + s.f32s(&mut self.blend_colour); + for b in self.colour_mask.iter_mut() { + s.bool(b); + } + s.f32s(&mut self.offset); + s.f32(&mut self.line_width); + s.f32(&mut self.point_size); + s.u32(&mut self.line_stipple.0); + s.u16(&mut self.line_stipple.1); + s.u32s(&mut self.polygon_stipple); + s.f32s(&mut self.env_colour); + let fog = &mut self.fog; + s.bool(&mut fog.on); + s.u32(&mut fog.mode); + for v in [&mut fog.density, &mut fog.start, &mut fog.end] { + s.f32(v); + } + s.f32s(&mut fog.colour); + s.u32s(&mut self.polygon_mode); + self.ms.visit(s); + } +} + +impl Gl { + /// Whether a primitive is open, which a snapshot does not carry. + pub fn busy(&self) -> bool { + self.in_begin + } + + /// The context's GL state, as the stream module reads and writes it. The draw state + /// is made again (`dirty`) on a load. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { mode: _, mv: _, proj: _, tex: _, light: _, normal_stale: _, color: _, normal: _, texcoord: _, edge_flag: _, user_plane: _, user_on: _, texgen: _, raster: _, in_begin: _, prim: _, error: _, light_bits: _, dirty: _, layout: _, double: _, origin: _, clip: _, placed: _, window_id: _, id_test: _, display_id: _, shown: _, draw: _, clear_colour: _, clear_depth: _, clear_stencil: _, texture: _, pix: _, feed: _, attrib_depth: _, ds: _ } = self; + s.u32(&mut self.mode); + visit_stack(s, &mut self.mv); + visit_stack(s, &mut self.proj); + visit_stack(s, &mut self.tex); + visit_lighting(s, &mut self.light); + s.bool(&mut self.normal_stale); + s.f32s(&mut self.color); + s.f32s(&mut self.normal); + s.f32s(&mut self.texcoord); + s.bool(&mut self.edge_flag); + for p in &mut self.user_plane { + s.f32s(p); + } + s.u32(&mut self.user_on); + s.u32(&mut self.texgen.enabled); + for i in 0..4 { + s.u32(&mut self.texgen.mode[i]); + s.f32s(&mut self.texgen.object[i]); + s.f32s(&mut self.texgen.eye[i]); + } + self.raster.visit(s); + s.u32(&mut self.prim); + s.u32(&mut self.error); + s.u32(&mut self.light_bits); + let mut has_layout = self.layout.is_some(); + s.bool(&mut has_layout); + if s.loading() { + self.layout = has_layout.then(|| crate::dev::kona::back::store::layout::Layout { colour: 0..0, depth: 0..0, stencil: 0..0, multisample: 0..0, accumulation: 0..0, last: 0 }); + } + if let Some(l) = &mut self.layout { + for r in [&mut l.colour, &mut l.depth, &mut l.stencil, &mut l.multisample, &mut l.accumulation] { + s.u32(&mut r.start); + s.u32(&mut r.end); + } + s.u32(&mut l.last); + } + s.bool(&mut self.double); + s.i32s(&mut self.origin); + for v in [&mut self.clip.x0, &mut self.clip.y0, &mut self.clip.x1, &mut self.clip.y1] { + s.i32(v); + } + s.bool(&mut self.placed); + s.i32(&mut self.window_id); + s.bool(&mut self.id_test); + s.u32(&mut self.display_id); + s.u8(&mut self.shown); + s.opt(&mut self.draw, |s, v| s.u32(v)); + s.f32s(&mut self.clear_colour); + s.f32(&mut self.clear_depth); + s.u32(&mut self.clear_stencil); + s.opt(&mut self.texture, |s, t| t.visit(s)); + self.pix.visit(s); + self.feed.visit(s); + s.u32(&mut self.attrib_depth); + if s.loading() { + self.in_begin = false; + self.dirty = true; + self.ds = None; + } + } + + fn matrix_op(&mut self, op: impl FnOnce(&mut Mat4)) { + match self.mode { + gl::MODELVIEW => { + let mut m = *self.mv.get(); + op(&mut m); + self.mv.load(m); + self.normal_stale = true; + } + gl::PROJECTION => { + let mut m = *self.proj.get(); + op(&mut m); + self.proj.load(m); + } + gl::TEXTURE => { + let mut m = *self.tex.get(); + op(&mut m); + self.tex.load(m); + } + _ => {} + } + } + + fn push_pop(&mut self, push: bool) -> Taken { + let ok = match (self.mode, push) { + (gl::MODELVIEW, true) => self.mv.push(), + (gl::MODELVIEW, false) => self.mv.pop(), + (gl::PROJECTION, true) => self.proj.push(), + (gl::PROJECTION, false) => self.proj.pop(), + (gl::TEXTURE, true) => self.tex.push(), + (gl::TEXTURE, false) => self.tex.pop(), + _ => return Taken::Unmodelled, + }; + if self.mode == gl::MODELVIEW { + self.normal_stale = true; + } + if ok { + Taken::Yes + } else { + Taken::Error(if push { gl::STACK_OVERFLOW } else { gl::STACK_UNDERFLOW }) + } + } + + /// The matrix family: matrix mode, load, mult, push, pop, rotate, scale, + /// translate, in float and double forms, and frustum and orthographic projection + /// (the double forms' word order is inferred from the depth clear's). Between a begin and its end it is an error and changes nothing. + pub fn matrix(&mut self, selector: u32, a: &[u32]) -> Taken { + if self.in_begin { + return Taken::Error(gl::INVALID_OPERATION); + } + let fl = |i: usize| f(a[i]); + // A double is two words, the high one first (as the depth clear's is, established). + let db = |i: usize| f64::from_bits((a[2 * i] as u64) << 32 | a[2 * i + 1] as u64) as f32; + match (selector, a.len()) { + (SELECT_CURRENT_MATRIX_STACK, 1) => self.mode = a[0], + // Load identity, push and pop carry one word, which means nothing. + (LOAD_IDENTITY_MATRIX, 1) => self.matrix_op(|m| *m = math::IDENT), + (LOAD_MATRIX_16_FLOATS, 16) => { + let m: Mat4 = std::array::from_fn(|i| fl(i)); + self.matrix_op(|t| *t = m); + } + (MULTIPLY_BY_MATRIX_16_FLOATS, 16) => { + let m: Mat4 = std::array::from_fn(|i| fl(i)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (LOAD_MATRIX_16_DOUBLES, 32) => { + let m: Mat4 = std::array::from_fn(db); + self.matrix_op(|t| *t = m); + } + (MULTIPLY_BY_MATRIX_16_DOUBLES, 32) => { + let m: Mat4 = std::array::from_fn(db); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (ROTATE_DOUBLES, 8) => { + let m = math::rotate(db(0), db(1), db(2), db(3)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (SCALE_DOUBLES, 6) => { + let m = math::scale(db(0), db(1), db(2)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (TRANSLATE_DOUBLES, 6) => { + let m = math::translate(db(0), db(1), db(2)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (MULTIPLY_CURRENT_MATRIX_BY_FRUSTUM, 12) => { + let m = math::frustum(db(0), db(1), db(2), db(3), db(4), db(5)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (MULTIPLY_CURRENT_MATRIX_BY_ORTHOGRAPHIC_PROJECTION, 12) => { + let m = math::ortho(db(0), db(1), db(2), db(3), db(4), db(5)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (POP_MATRIX_STACK, 1) => return self.push_pop(false), + (PUSH_MATRIX_STACK, 1) => return self.push_pop(true), + (ROTATE_FLOATS, 4) => { + let m = math::rotate(fl(0), fl(1), fl(2), fl(3)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (SCALE_FLOATS, 3) => { + let m = math::scale(fl(0), fl(1), fl(2)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + (TRANSLATE_FLOATS, 3) => { + let m = math::translate(fl(0), fl(1), fl(2)); + self.matrix_op(|t| *t = math::mul(t, &m)); + } + _ => return Taken::Unmodelled, + } + Taken::Yes + } + + /// glEnable and glDisable of the capabilities the vertex path reads. + pub fn enable(&mut self, cap: u32, on: bool) -> Taken { + let r = &mut self.raster; + match cap { + gl::LIGHTING => { + r.lighting = on; + self.light.on = on as u32; + } + gl::CULL_FACE => r.cull = on, + gl::DEPTH_TEST => r.depth_test = on, + gl::TEXTURE_2D => r.texture_2d = on, + gl::TEXTURE_3D => r.texture_3d = on, + gl::SCISSOR_TEST => r.scissor_test = on, + gl::ALPHA_TEST => r.alpha_test = on, + gl::BLEND => r.blend = on, + gl::STENCIL_TEST => r.stencil_test = on, + gl::COLOR_LOGIC_OP => r.logic_op_on = on, + gl::DITHER => r.dither = on, + gl::POLYGON_OFFSET_FILL => r.offset_fill = on, + gl::FOG => r.fog.on = on, + gl::POINT_SMOOTH => r.point_smooth = on, + gl::LINE_SMOOTH => r.line_smooth = on, + gl::POLYGON_SMOOTH => r.polygon_smooth = on, + gl::LINE_STIPPLE => r.line_stipple_on = on, + gl::POLYGON_STIPPLE => r.polygon_stipple_on = on, + gl::MULTISAMPLE => r.ms.enable = on, + gl::SAMPLE_ALPHA_TO_MASK => r.ms.alpha_to_mask = on, + gl::SAMPLE_ALPHA_TO_ONE => r.ms.alpha_to_one = on, + gl::SAMPLE_MASK => r.ms.mask_on = on, + gl::NORMALIZE => self.light.normalize = on as u32, + gl::COLOR_MATERIAL => self.light.cmat_on = on as u32, + // A clip plane's enable goes through the generic enable token, which takes the names 0x3000 to + // 0x3005 (established). Not part of the draw state: + // the vertex path reads the planes at begin. + gl::CLIP_PLANE0..=gl::CLIP_PLANE5 => { + let bit = 1 << (cap - gl::CLIP_PLANE0); + self.user_on = if on { self.user_on | bit } else { self.user_on & !bit }; + return Taken::Yes; + } + // Each coordinate's enable goes through the generic enable token (established). + gl::TEXTURE_GEN_S..=gl::TEXTURE_GEN_Q => { + let bit = 1 << (cap - gl::TEXTURE_GEN_S); + self.texgen.enabled = if on { self.texgen.enabled | bit } else { self.texgen.enabled & !bit }; + return Taken::Yes; + } + gl::LIGHT0..=gl::LIGHT7 => { + let bit = 1 << (cap - gl::LIGHT0); + if on { + self.light_bits |= bit; + } else { + self.light_bits &= !bit; + } + self.rechain_lights(); + return Taken::Yes; + } + // The enable tokens take only the six planes in 0x3000 to 0x3fff; the rest of that range is an + // invalid enumerant (established). + gl::CLIP_PLANE_RANGE_FIRST..=gl::CLIP_PLANE_RANGE_LAST => return Taken::Error(gl::INVALID_ENUM), + _ => return Taken::Unmodelled, + } + self.dirty = true; + Taken::Yes + } + + /// The chain of enabled lights, lowest first. + fn rechain_lights(&mut self) { + let on: Vec = (0..MAX_LIGHTS).filter(|i| self.light_bits >> i & 1 != 0).collect(); + self.light.head = on.first().map_or(-1, |&i| i as i32); + self.light.count = on.len() as u32; + for (k, &i) in on.iter().enumerate() { + self.light.lights[i].next = on.get(k + 1).map_or(-1, |&n| n as i32); + } + } + + /// The other state commands, by selector. + pub fn state(&mut self, selector: u32, a: &[u32]) -> Taken { + match (selector, a.len()) { + (SET_VIEWPORT, 4) => { + self.raster.viewport = [f(a[0]), f(a[1]), f(a[2]), f(a[3])]; + // A guess: the viewport words are integers or floats, and a word + // below 0x10000 is read as an integer (no float of an ordinary + // size has that bit pattern). The recorded streams send + // integers (0, 0, 500, 400); a capture of a client that sends + // a fractional viewport, or the sender's own code, would settle it. + for (v, w) in self.raster.viewport.iter_mut().zip(a) { + if *w < 0x0001_0000 { + *v = *w as f32; + } + } + } + (SET_SCISSOR_BOX, 4) => self.raster.scissor = [a[0] as i32, a[1] as i32, a[2] as i32, a[3] as i32], + (ENABLE_CAPABILITY, 1) => return self.enable(a[0], true), + (DISABLE_CAPABILITY, 1) => return self.enable(a[0], false), + (SET_FLAT_OR_SMOOTH_SHADING, 1) => self.raster.flat = a[0] == gl::FLAT, + (SET_FACE_CULLING, 1) => self.raster.cull_mode = a[0], + (SET_FRONT_FACE_WINDING, 1) => self.raster.front_cw = a[0] == gl::CW, + (SET_DEPTH_TEST, 1) => self.raster.depth_func = a[0], + (SET_ALPHA_TEST, 2) => { + self.raster.alpha_func = a[0]; + self.raster.alpha_ref = f(a[1]); + } + (SET_STENCIL_TEST, 3) => { + self.raster.stencil_func = a[0]; + self.raster.stencil_ref = a[1]; + self.raster.stencil_read_mask = a[2]; + } + (SET_STENCIL_OPERATIONS, 3) => self.raster.stencil_ops = [a[0], a[1], a[2]], + (SET_STENCIL_WRITE_MASK, 1) => self.raster.stencil_write_mask = a[0], + (SET_BLEND_FACTORS, 2) => self.raster.blend_factors = [a[0], a[1]], + (SET_CONSTANT_BLEND_COLOUR, 4) => self.raster.blend_colour = [f(a[0]), f(a[1]), f(a[2]), f(a[3])], + (SET_LOGIC_OPERATION, 1) => self.raster.logic_op = a[0], + (SET_COLOUR_WRITE_MASK, 4) => self.raster.colour_mask = [a[0] != 0, a[1] != 0, a[2] != 0, a[3] != 0], + (SET_DEPTH_WRITE_MASK, 1) => self.raster.depth_mask = a[0] != 0, + (SET_POLYGON_OFFSET, 2) => self.raster.offset = [f(a[0]), f(a[1])], + // The polygon fill mode: two words, the face then the mode (established: the pipe reads + // the face from the first word and the mode from the last). It validates neither: a face + // other than front, back or both is ignored without an error, and a mode is kept as sent + // (the client checks both). A triangle's mode then reads as line when it equals GL's line + // mode, point when below it and fill when above (established), see `GlPath::polygon_mode`. + // The pipe also writes a per-context flag from each call's mode alone (1 for line); nothing + // in the model reads it, so it is not kept. + (SET_POLYGON_FILL_MODE, 2) => { + let faces = match a[0] { + gl::FRONT => [true, false], + gl::BACK => [false, true], + gl::FRONT_AND_BACK => [true; 2], + _ => return Taken::Yes, + }; + for (m, _) in self.raster.polygon_mode.iter_mut().zip(faces).filter(|&(_, on)| on) { + *m = a[1]; + } + } + // The first word is the float (inferred from GL's one-float call; the second is unread). + (SET_USER_CLIP_PLANE, 9) => return self.clip_plane(a), + (TEXTURE_COORDINATE_GENERATION_FLOAT_FORM, 9) => return self.tex_gen(a), + (SET_LINE_WIDTH, 2) => self.raster.line_width = f(a[0]), + (SET_POINT_SIZE, 2) => self.raster.point_size = f(a[0]), + // GL's argument order: the repeat count, then the pattern. + (SET_LINE_STIPPLE, 2) => self.raster.line_stipple = (a[0].clamp(1, 256), a[1] as u16), + (SET_STENCIL_CLEAR_VALUE, 1) => self.clear_stencil = a[0], + // The texture environment: the parameter's name, then four values. The mode's number + // is a float in the float form (inferred from its start-up value), the colour four floats. + (SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM, 5) if a[0] == 0x2200 => self.raster.env_mode = f(a[1]) as u32, + (SET_TEXTURE_ENVIRONMENT_PARAMETER_INTEGER_FORM, 5) if a[0] == 0x2200 => self.raster.env_mode = a[1], + (SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM, 5) if a[0] == 0x2201 => self.raster.env_colour = [f(a[1]), f(a[2]), f(a[3]), f(a[4])], + (SELECT_DRAW_BUFFER, 1) => { + // glDrawBuffer: front gl::FRONT, back gl::BACK, front and back gl::FRONT_AND_BACK, none 0. + self.draw = Some(match a[0] { + gl::FRONT | gl::FRONT_LEFT | gl::RIGHT => 1, + gl::BACK | gl::BACK_LEFT => 2, + gl::FRONT_AND_BACK => 3, + 0 => 0, + _ => return Taken::Error(gl::INVALID_ENUM), + }); + } + (SET_MULTISAMPLE_PATTERN, 1) => { + if !self.raster.ms.set_pattern(a[0]) { + return Taken::Error(gl::INVALID_ENUM); + } + } + // The value (clamped to 0 to 1 by the client) and the invert flag's low byte. + (SET_MULTISAMPLE_COVERAGE_MASK, 2) => { + self.raster.ms.mask_value = f(a[0]).clamp(0.0, 1.0); + self.raster.ms.mask_invert = a[1] & 0xff != 0; + } + (SET_COLOUR_CLEAR_VALUE, 4) => self.clear_colour = [f(a[0]), f(a[1]), f(a[2]), f(a[3])], + (SET_DEPTH_CLEAR_VALUE, 2) => self.clear_depth = f64::from_bits((a[0] as u64) << 32 | a[1] as u64) as f32, + (SET_DEPTH_RANGE, 4) => { + // Two doubles, high word first (the senders' order is assumed). + let d = |i: usize| f64::from_bits((a[i] as u64) << 32 | a[i + 1] as u64) as f32; + self.raster.depth_range = [d(0), d(2)]; + } + (SET_FOG_PARAMETER_FLOAT_FORM, 5) | (SET_FOG_PARAMETER_INTEGER_FORM, 5) => return self.fog(selector == SET_FOG_PARAMETER_INTEGER_FORM, a), + (SET_LIGHT_PARAMETER_FLOAT_FORM, 6) | (SET_LIGHT_PARAMETER_INTEGER_FORM, 6) => return self.light(selector == SET_LIGHT_PARAMETER_INTEGER_FORM, a), + (SET_LIGHTING_MODEL_PARAMETER_FLOAT_FORM, 5) | (SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM, 5) => return self.light_model(selector == SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM, a), + (SET_MATERIAL_PARAMETER_FLOAT_FORM, 6) => return self.material(a), + _ => return Taken::Unmodelled, + } + self.dirty = true; + Taken::Yes + } + + /// A user clip plane: the plane name, 0x3000 + n, then the four coefficients as doubles, high word + /// first (nine words; established). The pipe's engine multiplies them at the call by a 16-word block + /// and keeps the eye-space plane (established); that the block is the inverse of the model-view + /// matrix, as OpenGL specifies, is inferred. A model-view matrix with no inverse leaves the plane as it was (what the + /// pipe does is unknown). + /// No draw state changes: the vertex path reads the planes when a primitive begins. + fn clip_plane(&mut self, a: &[u32]) -> Taken { + let Some(n) = (a[0] as usize).checked_sub(gl::CLIP_PLANE0 as usize).filter(|&n| n < gl::CLIP_PLANES) else { + return Taken::Error(gl::INVALID_ENUM); + }; + let d = |i: usize| f64::from_bits((a[1 + 2 * i] as u64) << 32 | a[2 + 2 * i] as u64) as f32; + if let Some(plane) = self.to_eye(std::array::from_fn(d)) { + self.user_plane[n] = plane; + } + Taken::Yes + } + + /// Texture coordinate generation (established): the + /// coordinate (0x2000 to 0x2003), the parameter name, then seven value words. The mode name + /// takes one integer word; the object and eye planes four floats. The pipe accepts the coordinates + /// 0x2000 to 0x2003, the names 0x2500 to 0x2502 and 0x81f4 to 0x81f7 and the modes 0x2400 to 0x2402 + /// and 0x81f0 to 0x81f3, and refuses any other with an invalid enumerant (established). The eye + /// plane is multiplied at the call by the block a clip plane uses (established; that the block + /// is the inverse model-view is inferred) and the object plane is stored unchanged; the + /// equations are OpenGL's (inferred). The distance names and modes are accepted and + /// not modelled: they are counted and change nothing. + fn tex_gen(&mut self, a: &[u32]) -> Taken { + let Some(i) = (a[0] as usize).checked_sub(gl::TEXTURE_COORD_S as usize).filter(|&i| i < 4) else { + return Taken::Error(gl::INVALID_ENUM); + }; + let plane: [f32; 4] = std::array::from_fn(|k| f(a[2 + k])); + match a[1] { + // The pipe accepts the three modes for any coordinate and the four distance modes; sphere map + // on r or q is not refused and the engine appears to ignore it (inferred): the mode is kept + // and the generator leaves the coordinate as it was. The distance modes are accepted and + // not modelled. Any other value is an invalid enumerant (established). + gl::TEXTURE_GEN_MODE => match a[2] { + texgen::EYE_LINEAR..=texgen::SPHERE_MAP => self.texgen.mode[i] = a[2], + gl::DISTANCE_MODE_FIRST..=gl::DISTANCE_MODE_LAST => return Taken::Unmodelled, + _ => return Taken::Error(gl::INVALID_ENUM), + }, + gl::OBJECT_PLANE => self.texgen.object[i] = plane, + gl::EYE_PLANE => { + if let Some(p) = self.to_eye(plane) { + self.texgen.eye[i] = p; + } + } + gl::DISTANCE_PLANE_FIRST..=gl::DISTANCE_PLANE_LAST => return Taken::Unmodelled, + _ => return Taken::Error(gl::INVALID_ENUM), + } + Taken::Yes + } + + /// The current normal in eye space, not normalised: what a sphere-mapped coordinate reflects about. + pub fn eye_normal(&mut self) -> [f32; 3] { + self.refresh_normal_matrix(); + let (nm, n) = (&self.light.normal_matrix, self.normal); + [nm[0] * n[0] + nm[3] * n[1] + nm[6] * n[2], nm[1] * n[0] + nm[4] * n[1] + nm[7] * n[2], nm[2] * n[0] + nm[5] * n[1] + nm[8] * n[2]] + } + + /// A plane given in object coordinates, in eye coordinates: its coefficients, as a row + /// vector, times the inverse of the current model-view matrix. None if that has no inverse. + fn to_eye(&self, p: [f32; 4]) -> Option<[f32; 4]> { + let inv = math::invert(self.mv.get())?; + Some(std::array::from_fn(|j| (0..4).map(|k| p[k] * inv[j * 4 + k]).sum())) + } + + fn params(int: bool, a: &[u32]) -> [f32; 4] { + std::array::from_fn(|i| if int { a[i] as i32 as f32 } else { f(a[i]) }) + } + + /// A fog parameter: the parameter's name, then four values, in float or integer form. The float + /// form is five words and the integer form a one-word token and a four-word continuation; both + /// layouts are established, though how the name word reaches the engine on the integer path is + /// unknown. The pipe accepts seven names (index, density, start, + /// end, mode, colour and the offset extension) and four modes (linear, exp, exp2 and the function + /// table extension), and answers any other name or mode with an invalid enumerant, keeping the old + /// mode (established). It does not range-check start, end or density (established). The index, the + /// offset and the function-table mode are accepted and not modelled. The integer colour scale is + /// 2^-31, clamped to 0..1 (the scale is established; reading it as a value mapping is inferred). + fn fog(&mut self, int: bool, a: &[u32]) -> Taken { + let fog = &mut self.raster.fog; + let v = |k: usize| if int { a[1 + k] as i32 as f32 } else { f(a[1 + k]) }; + match a[0] { + gl::FOG_MODE => { + let mode = if int { a[1] } else { f(a[1]) as u32 }; + match mode { + gl::LINEAR | gl::EXP | gl::EXP2 => fog.mode = mode, + gl::FOG_FUNC => return Taken::Unmodelled, + _ => return Taken::Error(gl::INVALID_ENUM), + } + } + gl::FOG_DENSITY => fog.density = v(0), + gl::FOG_START => fog.start = v(0), + gl::FOG_END => fog.end = v(0), + gl::FOG_COLOR => { + let c = |k: usize| if int { (a[1 + k] as i32 as f64 / 2147483648.0).clamp(0.0, 1.0) as f32 } else { f(a[1 + k]) }; + fog.colour = [c(0), c(1), c(2)]; + } + gl::FOG_INDEX | gl::FOG_OFFSET => return Taken::Unmodelled, + _ => return Taken::Error(gl::INVALID_ENUM), + } + self.dirty = true; + Taken::Yes + } + + fn light(&mut self, int: bool, a: &[u32]) -> Taken { + let Some(n) = (a[0] as usize).checked_sub(gl::LIGHT0 as usize).filter(|&n| n < MAX_LIGHTS) else { + return Taken::Error(gl::INVALID_ENUM); + }; + let p = Self::params(int, &a[2..6]); + let (mv, l) = (*self.mv.get(), &mut self.light.lights[n]); + let c3 = [p[0], p[1], p[2]]; + match a[1] { + gl::AMBIENT => l.ambient = c3, + gl::DIFFUSE => l.diffuse = c3, + gl::SPECULAR => l.specular = c3, + gl::POSITION => l.pos = math::xform(&mv, p), + gl::SPOT_DIRECTION => { + let e = math::xform(&mv, [p[0], p[1], p[2], 0.0]); + l.spot_dir = [e[0], e[1], e[2]]; + } + gl::SPOT_EXPONENT => l.spot_exp = p[0], + gl::SPOT_CUTOFF => { + l.spot_on = (p[0] != 180.0) as u32; + l.spot_cos_cutoff = if p[0] == 180.0 { -1.0 } else { p[0].to_radians().cos() }; + } + gl::CONSTANT_ATTENUATION => l.atten[2] = p[0], + gl::LINEAR_ATTENUATION => l.atten[0] = p[0], + gl::QUADRATIC_ATTENUATION => l.atten[1] = p[0], + _ => return Taken::Unmodelled, + } + Taken::Yes + } + + fn light_model(&mut self, int: bool, a: &[u32]) -> Taken { + let p = Self::params(int, &a[1..5]); + match a[0] { + gl::LIGHT_MODEL_AMBIENT => self.light.amb_sum = [p[0], p[1], p[2]], + gl::LIGHT_MODEL_LOCAL_VIEWER => self.light.local_viewer = (p[0] != 0.0) as u32, + gl::LIGHT_MODEL_TWO_SIDE => self.light.two_sided = (p[0] != 0.0) as u32, + _ => return Taken::Unmodelled, + } + Taken::Yes + } + + fn material(&mut self, a: &[u32]) -> Taken { + let p = [f(a[2]), f(a[3]), f(a[4]), f(a[5])]; + let c3 = [p[0], p[1], p[2]]; + for face in 0..2 { + // gl::FRONT front, gl::BACK back, gl::FRONT_AND_BACK both. + if (a[0] == gl::FRONT && face != 0) || (a[0] == gl::BACK && face != 1) { + continue; + } + let m = &mut self.light.mat[face]; + match a[1] { + gl::AMBIENT => m.ambient = c3, + gl::DIFFUSE => m.diffuse = p, + gl::SPECULAR => m.specular = c3, + gl::EMISSION => m.emission = c3, + gl::SHININESS => m.shininess = p[0], + gl::AMBIENT_AND_DIFFUSE => { + m.ambient = c3; + m.diffuse = p; + } + _ => return Taken::Unmodelled, + } + } + Taken::Yes + } + + /// A buffer configuration for level 0: the layout the planes follow, and + /// whether the window is double buffered. A level above 0 draws overlay + /// planes, which are not drawn (the target stays as it was). + pub fn configure(&mut self, level: i32, double: bool, layout: crate::dev::kona::back::store::layout::Layout, samples: u32) { + if level == 0 { + self.layout = Some(layout); + self.double = double; + // A configuration resets the mask, the pattern and the alpha options. + self.raster.ms.configure(samples); + self.dirty = true; + } + } + + /// The window the kernel placed, in plane pixels, under display id `display_id` + /// whose swaps so far last showed buffer `shown`: a context placed after a swap + /// draws into the buffer the display is not showing, as one placed before does. + /// `origin` is where window coordinates start; `clip` is the rectangle the + /// kernel allows drawing in, which need not start at the origin or equal + /// the window. + pub fn place(&mut self, origin: [i32; 2], clip: Rect, display_id: u32, shown: u8, id_test: bool) { + self.origin = origin; + self.clip = clip; + self.placed = true; + self.window_id = display_id as i32; + self.id_test = id_test; + self.display_id = display_id; + self.shown = shown; + self.dirty = true; + } + + /// A swap of display id `id` that shows buffer `bank`. + pub fn swapped(&mut self, id: u32, bank: u8) { + if self.display_id == id && self.shown != bank { + self.shown = bank; + self.dirty = true; + } + } + + /// The window moved to `x`, `y`: its origin and its clip rectangle move by the + /// same amount. + pub fn move_to(&mut self, x: i32, y: i32) { + if self.placed { + let (dx, dy) = (x.saturating_sub(self.origin[0]), y.saturating_sub(self.origin[1])); + self.origin = [x, y]; + let c = &mut self.clip; + *c = Rect { x0: c.x0.saturating_add(dx), y0: c.y0.saturating_add(dy), x1: c.x1.saturating_add(dx), y1: c.y1.saturating_add(dy) }; + self.dirty = true; + } + } + + /// A bind made `d` the bound texture. + pub fn set_texture(&mut self, d: Option) { + self.texture = d; + self.dirty = true; + } + + /// Load the polygon stipple's 32 rows. + pub fn set_polygon_stipple(&mut self, rows: [u32; 32]) { + self.raster.polygon_stipple = rows; + self.dirty = true; + } + + /// The target, from the layout, window and draw buffer, into the raster group. + pub fn retarget(&mut self) { + let mut t = Target::default(); + if let Some(l) = &self.layout { + let mask = self.draw.unwrap_or(if self.double { 2 } else { 1 }); + // One word a buffer: the first is shown at start-up, and a swap flips + // which; the front buffer is the one shown, the back the other. + let shown = if self.double { self.shown as u32 } else { 0 }; + for (bit, word) in [(1u32, l.colour.start + shown), (2, l.colour.start + 1 - shown)] { + if mask & bit != 0 && word < l.colour.end && (bit == 1 || self.double) { + t.colour[t.colours as usize] = word; + t.colours += 1; + } + } + if !l.depth.is_empty() { + t.depth_word = l.depth.start; + t.depth_bits = 32; + } + if !l.stencil.is_empty() { + t.stencil_word = l.stencil.start; + t.stencil_bits = 8; + } + } + self.raster.target = t; + } + + /// The draw state for the raster group as it stands, built again when a + /// state command has changed it. `screen` is the managed area. + pub fn draw_state(&mut self, screen: Rect) -> DrawState { + if self.dirty || self.ds.is_none() { + self.retarget(); + let r = &self.raster; + let t = &r.target; + let [ox, oy] = self.origin; + // Nothing is drawn before the kernel has placed the window. + let mut clip = if self.placed { self.clip } else { Rect::default() }; + if r.scissor_test { + let s = r.scissor; + let (x0, y0) = (ox.saturating_add(s[0]), oy.saturating_add(s[1])); + clip = clip.intersect(&Rect { x0, y0, x1: x0.saturating_add(s[2]), y1: y0.saturating_add(s[3]) }); + } + let [vx, vy, vw, vh] = r.viewport; + // The viewport rectangle holds every pixel the viewport touches. + let (ox, oy) = (ox as f64, oy as f64); + let cut = |v: f64| v.clamp(i32::MIN as f64, i32::MAX as f64) as i32; + let viewport = Rect { x0: cut(ox + (vx as f64).floor()), y0: cut(oy + (vy as f64).floor()), x1: cut(ox + (vx as f64 + vw as f64).ceil()), y1: cut(oy + (vy as f64 + vh as f64).ceil()) }; + let samples = self.layout.as_ref().filter(|l| !l.multisample.is_empty()).and_then(|l| { + let run = |r: &std::ops::Range| (r.start, r.end - r.start); + r.ms.draw(run(&l.multisample), run(&l.depth), run(&l.stencil)) + }); + self.ds = Some(DrawState { + origin: self.origin, + clip: clip.intersect(&screen), + viewport, + colour: t.colour, + colours: t.colours, + depth_word: t.depth_word, + depth_bits: t.depth_bits, + depth_test: r.depth_test, + depth_func: Compare::from_gl(r.depth_func).unwrap_or_default(), + depth_mask: r.depth_mask, + stencil_word: t.stencil_word, + stencil_bits: t.stencil_bits, + stencil: StencilState { + enabled: r.stencil_test, + func: Compare::from_gl(r.stencil_func).unwrap_or_default(), + reference: r.stencil_ref as u8, + read_mask: r.stencil_read_mask as u8, + write_mask: r.stencil_write_mask as u8, + fail: StencilOp::from_gl(r.stencil_ops[0]).unwrap_or_default(), + zfail: StencilOp::from_gl(r.stencil_ops[1]).unwrap_or_default(), + zpass: StencilOp::from_gl(r.stencil_ops[2]).unwrap_or_default(), + }, + // The alpha reference is the token's float in 13-bit units, rounded: the model's own. + alpha: AlphaTest { enabled: r.alpha_test, func: Compare::from_gl(r.alpha_func).unwrap_or_default(), reference: (r.alpha_ref.clamp(0.0, 1.0) * 8191.0 + 0.5) as u32 }, + blend: BlendState { + enabled: r.blend, + src: BlendFactor::from_gl(r.blend_factors[0]).unwrap_or(BlendFactor::One), + dst: BlendFactor::from_gl(r.blend_factors[1]).unwrap_or(BlendFactor::Zero), + eq: BlendEq::Add, + constant: r.blend_colour.map(|c| (c.clamp(0.0, 1.0) * 8191.0 + 0.5) as u32), + }, + logic: r.logic_op_on.then(|| LogicOp::from_gl(r.logic_op)).flatten(), + colour_mask: r.colour_mask, + dither: r.dither, + offset: r.offset_fill.then_some(r.offset), + ownership: self.id_test.then(|| Ownership::encode(self.window_id).0), + line_stipple: r.line_stipple_on.then_some((r.line_stipple.1, r.line_stipple.0)), + polygon_stipple: r.polygon_stipple_on.then_some(r.polygon_stipple), + samples, + // Lines and points smooth in a multisampled window and polygons multisample there (the + // sampling word and the point handler's order of tests, established; see + // `raster::smooth`). + smooth: Smooth { points: r.point_smooth, lines: r.line_smooth, polygons: r.polygon_smooth && samples.is_none() }, + fog: Fog::new(r.fog.on, r.fog.mode, r.fog.density, r.fog.start, r.fog.end, r.fog.colour), + texture: self.texture.filter(|d| if d.volume { r.texture_3d } else { r.texture_2d }).map(|d| TexDesc { env: EnvMode::from_gl(r.env_mode).unwrap_or_default(), env_colour: r.env_colour.map(|c| (c.clamp(0.0, 1.0) * 8191.0 + 0.5) as u32), ..d }), + }); + self.dirty = false; + } + self.ds.expect("just built") + } + + /// The normal matrix, brought up to date with the model-view matrix. + pub fn refresh_normal_matrix(&mut self) { + if self.normal_stale { + self.light.normal_matrix = math::normal_matrix(self.mv.get()); + self.normal_stale = false; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bits(v: &[f32]) -> Vec { + v.iter().map(|x| x.to_bits()).collect() + } + + #[test] + fn matrix_commands_compose_on_the_current_stack() { + let mut g = Gl::default(); + assert_eq!(g.matrix(TRANSLATE_FLOATS, &bits(&[1.0, 2.0, 3.0])), Taken::Yes); + assert_eq!(g.mv.get()[12..15], [1.0, 2.0, 3.0]); + assert_eq!(g.matrix(PUSH_MATRIX_STACK, &[0]), Taken::Yes); + g.matrix(SCALE_FLOATS, &bits(&[2.0, 2.0, 2.0])); + assert_eq!(g.mv.get()[0], 2.0); + assert_eq!(g.matrix(POP_MATRIX_STACK, &[0]), Taken::Yes); + assert_eq!(g.mv.get()[0], 1.0); + assert_eq!(g.matrix(POP_MATRIX_STACK, &[0]), Taken::Error(gl::STACK_UNDERFLOW)); + g.matrix(SELECT_CURRENT_MATRIX_STACK, &[gl::PROJECTION]); + g.matrix(LOAD_MATRIX_16_FLOATS, &bits(&math::IDENT.map(|x| x * 3.0))); + assert_eq!(g.proj.get()[0], 3.0); + assert_eq!(g.mv.get()[0], 1.0, "the model-view matrix is untouched"); + for _ in 0..PROJ_DEPTH - 1 { + assert_eq!(g.matrix(PUSH_MATRIX_STACK, &[0]), Taken::Yes); + } + assert_eq!(g.matrix(PUSH_MATRIX_STACK, &[0]), Taken::Error(gl::STACK_OVERFLOW)); + g.in_begin = true; + assert_eq!(g.matrix(LOAD_IDENTITY_MATRIX, &[0]), Taken::Error(gl::INVALID_OPERATION)); + } + + #[test] + fn lights_are_set_up_in_eye_space_and_chained_when_enabled() { + let mut g = Gl::default(); + g.matrix(TRANSLATE_FLOATS, &bits(&[0.0, 0.0, -5.0])); + let pos = [gl::LIGHT0 + 1, gl::POSITION, 0, 0, 0, 0]; + let mut a = pos; + a[2..].copy_from_slice(&bits(&[1.0, 2.0, 3.0, 1.0])); + assert_eq!(g.state(SET_LIGHT_PARAMETER_FLOAT_FORM, &a), Taken::Yes); + assert_eq!(g.light.lights[1].pos, [1.0, 2.0, -2.0, 1.0]); + g.enable(gl::LIGHT0 + 1, true); + g.enable(gl::LIGHT0, true); + assert_eq!((g.light.head, g.light.count, g.light.lights[0].next), (0, 2, 1)); + g.enable(gl::LIGHT0, false); + assert_eq!((g.light.head, g.light.count), (1, 1)); + assert_eq!(g.state(SET_LIGHT_PARAMETER_FLOAT_FORM, &[gl::LIGHT0 + 9, gl::AMBIENT, 0, 0, 0, 0]), Taken::Error(gl::INVALID_ENUM)); + assert_eq!(g.state(SET_LIGHT_PARAMETER_FLOAT_FORM, &[gl::LIGHT0, 0x9999, 0, 0, 0, 0]), Taken::Unmodelled); + } + + #[test] + fn a_material_or_model_word_lands_on_the_faces_it_names() { + let mut g = Gl::default(); + let a = [gl::FRONT, gl::SHININESS].into_iter().chain(bits(&[30.0, 0.0, 0.0, 0.0])).collect::>(); + g.state(SET_MATERIAL_PARAMETER_FLOAT_FORM, &a); + assert_eq!((g.light.mat[0].shininess, g.light.mat[1].shininess), (30.0, 0.0)); + let a = [gl::LIGHT_MODEL_TWO_SIDE].into_iter().chain(bits(&[1.0, 0.0, 0.0, 0.0])).collect::>(); + g.state(SET_LIGHTING_MODEL_PARAMETER_FLOAT_FORM, &a); + assert_eq!(g.light.two_sided, 1); + let a = [gl::LIGHT_MODEL_TWO_SIDE, 0, 0, 0, 0]; + g.state(SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM, &a); + assert_eq!(g.light.two_sided, 0, "the integer form"); + } + + /// The double forms of the matrix tokens equal the float forms, and the frustum and + /// orthographic tokens multiply GL's own matrices (a double is two words, high first). + #[test] + fn matrix_double_forms_and_projections_match_the_float_ones() { + let d = |v: f64| [(v.to_bits() >> 32) as u32, v.to_bits() as u32]; + let words = |vs: &[f64]| vs.iter().flat_map(|&v| d(v)).collect::>(); + let mut a = Gl::default(); + let mut b = Gl::default(); + a.matrix(LOAD_MATRIX_16_FLOATS, &(0..16).map(|i| (i as f32 * 0.5).to_bits()).collect::>()); + b.matrix(LOAD_MATRIX_16_DOUBLES, &words(&(0..16).map(|i| i as f64 * 0.5).collect::>())); + assert_eq!(a.mv.get(), b.mv.get()); + a.matrix(TRANSLATE_FLOATS, &[1.0f32.to_bits(), 2.0f32.to_bits(), 3.0f32.to_bits()]); + b.matrix(TRANSLATE_DOUBLES, &words(&[1.0, 2.0, 3.0])); + a.matrix(SCALE_FLOATS, &[2.0f32.to_bits(), 3.0f32.to_bits(), 4.0f32.to_bits()]); + b.matrix(SCALE_DOUBLES, &words(&[2.0, 3.0, 4.0])); + a.matrix(ROTATE_FLOATS, &[30.0f32.to_bits(), 0.0f32.to_bits(), 1.0f32.to_bits(), 0.0f32.to_bits()]); + b.matrix(ROTATE_DOUBLES, &words(&[30.0, 0.0, 1.0, 0.0])); + assert_eq!(a.mv.get(), b.mv.get()); + for g in [&mut a, &mut b] { + g.matrix(SELECT_CURRENT_MATRIX_STACK, &[gl::PROJECTION]); + } + b.matrix(MULTIPLY_CURRENT_MATRIX_BY_FRUSTUM, &words(&[-1.0, 1.0, -1.0, 1.0, 1.0, 10.0])); + a.matrix(MULTIPLY_BY_MATRIX_16_FLOATS, &math::frustum(-1.0, 1.0, -1.0, 1.0, 1.0, 10.0).map(f32::to_bits)); + assert_eq!(a.proj.get(), b.proj.get()); + let mut c = Gl::default(); + c.matrix(SELECT_CURRENT_MATRIX_STACK, &[gl::PROJECTION]); + c.matrix(MULTIPLY_CURRENT_MATRIX_BY_ORTHOGRAPHIC_PROJECTION, &words(&[0.0, 4.0, 0.0, 2.0, -1.0, 1.0])); + assert_eq!(c.proj.get(), &math::ortho(0.0, 4.0, 0.0, 2.0, -1.0, 1.0)); + } + + /// The words a double-buffered window draws into are the ones the display + /// composes: it shows `COLOUR_WORD + bank` and the draw goes to the other. + #[test] + fn the_draw_target_is_the_word_the_display_is_not_showing() { + use crate::dev::kona::back::store::layout::{BufferConfig, Layout, MAIN_COLOUR_WORD}; + let cfg = BufferConfig::from_words(&[0, 8, 0, 1, 0, 0, 24, 0, 0, 0]); + let layout = Layout::new(&cfg, 0, 64).expect("a plain double-buffered layout"); + assert_eq!(layout.colour.start, MAIN_COLOUR_WORD, "the layout starts where the display reads"); + let mut g = Gl::default(); + g.configure(0, true, layout, 0); + g.place([0, 0], Rect { x0: 0, y0: 0, x1: 8, y1: 8 }, 5, 0, false); + g.retarget(); + assert_eq!((g.raster.target.colours, g.raster.target.colour[0]), (1, MAIN_COLOUR_WORD + 1), "bank 0 shown: the back word"); + g.swapped(5, 1); + g.retarget(); + assert_eq!(g.raster.target.colour[0], MAIN_COLOUR_WORD, "bank 1 shown: the first word"); + // A context placed after the swap starts from the buffer the display shows now. + let mut late = Gl::default(); + late.configure(0, true, Layout::new(&cfg, 0, 64).expect("layout"), 0); + late.place([0, 0], Rect { x0: 0, y0: 0, x1: 8, y1: 8 }, 5, 1, false); + late.retarget(); + assert_eq!(late.raster.target.colour[0], MAIN_COLOUR_WORD); + } +} diff --git a/src/dev/kona/back/gl/tests.rs b/src/dev/kona/back/gl/tests.rs new file mode 100644 index 000000000..d9fbc47c7 --- /dev/null +++ b/src/dev/kona/back/gl/tests.rs @@ -0,0 +1,1162 @@ +//! Vertex goldens: matrix and light setups through the GL path, no raster. + +use super::*; +use crate::dev::kona::back::raster::fog::Curve; +use crate::dev::kona::back::raster::{DrawState, Smooth, SmoothPoint, SmoothPoly}; +use vertex::grid; + +fn bits(v: &[f32]) -> Vec { + v.iter().map(|x| x.to_bits()).collect() +} + +struct Rig { + gl: Gl, + path: GlPath, + /// Every triangle made so far, and the draw state each ran under. + tris: Vec<[WVertex; 3]>, + states: Vec, + clears: Vec, + lines: Vec<[WVertex; 3]>, + discs: Vec, + aa: Vec, + aa_verts: Vec, +} + +impl Rig { + /// A 100 x 100 window and viewport, an orthographic projection of -1..1. + fn new() -> Rig { + let mut r = Rig { gl: Gl::default(), path: GlPath::new((100, 100)), tris: Vec::new(), states: Vec::new(), clears: Vec::new(), lines: Vec::new(), discs: Vec::new(), aa: Vec::new(), aa_verts: Vec::new() }; + r.gl.place([0, 0], Rect { x0: 0, y0: 0, x1: 100, y1: 100 }, 0, 0, false); + r.cmd(SET_VIEWPORT, &bits(&[0.0, 0.0, 100.0, 100.0])); + r + } + + fn cmd(&mut self, selector: u32, a: &[u32]) { + let mut prims = Prims::default(); + self.path.command(&mut self.gl, selector, a, 0, &mut prims); + self.states.extend(prims.tris.iter().map(|_| prims.state.expect("triangles come with a state"))); + self.tris.append(&mut prims.tris); + self.clears.append(&mut prims.clears); + self.lines.append(&mut prims.lines); + self.discs.append(&mut prims.discs); + // Indices into the vertices are rebased as the runs are appended. + let base = self.aa_verts.len() as u32; + self.aa.extend(prims.aa.iter().map(|p| SmoothPoly { start: p.start + base, ..*p })); + self.aa_verts.append(&mut prims.aa_verts); + } + + fn begin(&mut self, primitive: u32) { + self.cmd(primitive, &[0]); + } + + fn v(&mut self, x: f32, y: f32) { + self.cmd(SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT, &bits(&[x, y])); + } + + fn v3(&mut self, x: f32, y: f32, z: f32) { + self.cmd(SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, &bits(&[x, y, z])); + } + + fn end(&mut self) { + self.cmd(END, &[0]); + } + + fn colour(&mut self, r: f32, g: f32, b: f32) { + self.cmd(SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT, &bits(&[r, g, b])); + } + + fn triangle(&mut self, p: [(f32, f32); 3]) { + self.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + for (x, y) in p { + self.v(x, y); + } + self.end(); + } +} + +fn xy(d: &[WVertex; 3]) -> Vec<(i32, i32)> { + d.iter().map(|v| (v.x, v.y)).collect() +} + +/// A triangle in the unit view volume lands on the viewport: ndc -1 and 1 are +/// the viewport's edges, in 1/256 pixels. +#[test] +fn a_triangle_is_mapped_to_the_viewport_on_the_quarter_pixel_grid() { + let mut r = Rig::new(); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(xy(&r.tris[0]), [(0, 0), (100 * 256, 0), (50 * 256, 100 * 256)]); + // Depth: ndc z 0 is the middle of the depth range. + assert!(r.tris[0].iter().all(|v| v.z == depth32(0.5))); +} + +/// Matrices compose, push and pop, and the model-view applies before the +/// projection. +#[test] +fn matrices_move_vertices_before_the_viewport() { + let mut r = Rig::new(); + r.cmd(TRANSLATE_FLOATS, &bits(&[0.5, 0.0, 0.0])); + r.cmd(PUSH_MATRIX_STACK, &[0]); + r.cmd(SCALE_FLOATS, &bits(&[0.5, 0.5, 1.0])); + r.triangle([(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]); + r.cmd(POP_MATRIX_STACK, &[0]); + r.triangle([(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]); + // Scaled by a half and moved 0.5: x from 0.5 to 1.0 (ndc), 75 to 100 pixels. + assert_eq!(xy(&r.tris[0]), [(75 * 256, 50 * 256), (100 * 256, 50 * 256), (75 * 256, 75 * 256)]); + assert_eq!((r.gl.mv.get()[0], r.gl.mv.get()[12]), (1.0, 0.5), "the pop restored the translation alone"); +} + +/// Nothing cuts a triangle in window space: one that crosses the window edge +/// keeps its vertices where the projection puts them, with their own colours. +/// Set-up cuts the job's rectangle instead. +#[test] +fn a_triangle_crossing_the_window_is_not_cut_in_window_space() { + let mut r = Rig::new(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.colour(1.0, 0.0, 0.0); + r.v(0.0, -0.5); + r.colour(0.0, 1.0, 0.0); + r.v(3.0, -0.5); + r.colour(0.0, 0.0, 1.0); + r.v(0.0, 0.5); + r.end(); + assert_eq!(r.tris.len(), 1); + // x = 3 is ndc 3: 50 * (3 + 1) = 200 pixels. + assert_eq!(xy(&r.tris[0]), [(50 * 256, 25 * 256), (200 * 256, 25 * 256), (50 * 256, 75 * 256)]); + assert_eq!(r.tris[0].map(|v| v.c), [[8191, 0, 0, 8191], [0, 8191, 0, 8191], [0, 0, 8191, 8191]]); + assert_eq!(r.path.counts.clipped, 0); +} + +/// A primitive wholly outside the volume is clipped away. +#[test] +fn a_primitive_outside_the_volume_draws_nothing() { + let mut r = Rig::new(); + // Beyond the far plane. + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(0.0, 0.0, 2.0); + r.v3(0.5, 0.0, 2.0); + r.v3(0.0, 0.5, 2.0); + r.end(); + assert!(r.tris.is_empty()); + assert_eq!(r.path.counts.clipped_away, 1); + // In front of the near plane. + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(0.0, 0.0, -2.0); + r.v3(0.5, 0.0, -2.0); + r.v3(0.0, 0.5, -2.0); + r.end(); + assert!(r.tris.is_empty()); + assert_eq!(r.path.counts.clipped_away, 2); +} + +/// The far plane is clipped geometrically: a triangle with one vertex beyond it +/// is cut where it meets the plane, at the depth range's far end. The cut +/// vertices are those of the unit square's far edge, found by hand: the +/// vertex at z = 3 against the others at z = 0 meets z = 1 a third of the way +/// along each edge. +#[test] +fn a_triangle_straddling_the_far_plane_is_cut_at_it() { + let mut r = Rig::new(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(-0.5, -0.5, 0.0); + r.v3(0.5, -0.5, 0.0); + r.v3(0.0, 0.5, 3.0); + r.end(); + assert_eq!(r.tris.len(), 2, "a quadrilateral, as a fan of two"); + assert_eq!(r.path.counts.clipped, 1); + let far = depth32(1.0); + let verts: Vec<_> = r.tris.iter().flatten().collect(); + assert!(verts.iter().all(|v| v.z <= far)); + // Cut vertices: from (-0.5, -0.5, 0) to (0, 0.5, 3) at t = 1/3, and from (0.5, -0.5, 0) the same. + let cut = |x: f32, y: f32| (grid(50.0 * (x + 1.0)), grid(50.0 * (y + 1.0))); + for want in [cut(-0.5 + 0.5 / 3.0, -0.5 + 1.0 / 3.0), cut(0.5 - 0.5 / 3.0, -0.5 + 1.0 / 3.0)] { + let hit = verts.iter().find(|v| (v.x - want.0).abs() <= 2 && (v.y - want.1).abs() <= 2); + let hit = hit.unwrap_or_else(|| panic!("no vertex near {want:?} in {verts:?}")); + assert!(hit.z >= far - 0x2000, "on the far plane: {:#x} of {far:#x}", hit.z); + } +} + +/// The near plane likewise: the vertex at z = -3 is cut where the edges meet +/// z = -1, two thirds of the way along each, and the cut vertices sit at the +/// near end of the depth range. +#[test] +fn a_triangle_straddling_the_near_plane_is_cut_at_it() { + let mut r = Rig::new(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(-0.5, -0.5, -3.0); + r.v3(0.5, -0.5, 0.0); + r.v3(0.0, 0.5, 0.0); + r.end(); + assert_eq!(r.tris.len(), 2); + let verts: Vec<_> = r.tris.iter().flatten().collect(); + let cut = |x: f32, y: f32| (grid(50.0 * (x + 1.0)), grid(50.0 * (y + 1.0))); + for want in [cut(-0.5 + 1.0 * 2.0 / 3.0, -0.5), cut(-0.5 + 0.5 * 2.0 / 3.0, -0.5 + 1.0 * 2.0 / 3.0)] { + let hit = verts.iter().find(|v| (v.x - want.0).abs() <= 2 && (v.y - want.1).abs() <= 2); + let hit = hit.unwrap_or_else(|| panic!("no vertex near {want:?} in {verts:?}")); + assert!(hit.z <= 0x2000, "on the near plane: {:#x}", hit.z); + } +} + +/// Strips, fans, quads and polygons come out as triangles. +#[test] +fn strips_fans_quads_and_polygons_are_assembled() { + let sq = [(0.0, 0.0), (0.5, 0.0), (0.5, 0.5), (0.0, 0.5)]; + let mut r = Rig::new(); + r.begin(BEGIN_PRIMITIVE_TRIANGLE_STRIP); + for (x, y) in [(-0.5, -0.5), (-0.5, 0.0), (0.0, -0.5), (0.0, 0.0), (0.5, -0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!(r.tris.len(), 3, "a strip of five vertices is three triangles"); + r.tris.clear(); + r.begin(BEGIN_PRIMITIVE_TRIANGLE_FAN); + for (x, y) in [(0.0, 0.0), (0.5, 0.0), (0.5, 0.5), (0.0, 0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!(r.tris.len(), 2, "a fan of four is two triangles"); + r.tris.clear(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_QUADS); + sq.iter().for_each(|&(x, y)| r.v(x, y)); + r.end(); + assert_eq!(r.tris.len(), 2, "a quad is a fan of two triangles"); + assert!(r.tris.iter().all(|t| t[0] == r.tris[0][0]), "from its first vertex"); + r.tris.clear(); + r.begin(BEGIN_PRIMITIVE_POLYGON); + for (x, y) in [(0.0, 0.0), (0.5, 0.0), (0.7, 0.3), (0.5, 0.6), (0.0, 0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!(r.tris.len(), 3, "a polygon of five is one fan of three"); + r.tris.clear(); + r.begin(BEGIN_PRIMITIVE_QUAD_STRIP); + for (x, y) in [(-0.5, -0.5), (0.5, -0.5), (-0.5, 0.0), (0.5, 0.0), (-0.5, 0.5), (0.5, 0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!(r.tris.len(), 4, "a quad strip of six is two quads"); +} + +/// A line is the rectangle of half-width w / 2 about the segment, two triangles whose +/// vertices carry the distance along it; a strip and a loop are their segments; a +/// segment is clipped to the volume; a segment of no length draws nothing. +#[test] +fn lines_are_rectangles_about_their_segments() { + let mut r = Rig::new(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_LINES); + r.v(-0.5, 0.0); + r.v(0.5, 0.0); + r.end(); + assert_eq!((r.path.counts.lines, r.lines.len()), (1, 2)); + // Width 1 about a horizontal segment from x = 25 to 75 at y = 50: 49.5..50.5 each side. + let ys: Vec<_> = r.lines.iter().flatten().map(|v| v.y).collect(); + assert_eq!((ys.iter().min().copied(), ys.iter().max().copied()), (Some(grid(49.5)), Some(grid(50.5)))); + let ds: Vec<_> = r.lines.iter().flatten().map(|v| v.d).collect(); + assert_eq!((ds.iter().cloned().fold(f32::MAX, f32::min), ds.iter().cloned().fold(0.0, f32::max)), (0.0, 50.0), "d runs along the segment in pixels"); + r.cmd(SET_LINE_WIDTH, &[4.0f32.to_bits(), 0]); + r.lines.clear(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_LINES); + r.v(-0.5, 0.0); + r.v(0.5, 0.0); + r.end(); + let ys: Vec<_> = r.lines.iter().flatten().map(|v| v.y).collect(); + assert_eq!((ys.iter().min().copied(), ys.iter().max().copied()), (Some(grid(48.0)), Some(grid(52.0))), "width 4"); + r.lines.clear(); + r.begin(BEGIN_PRIMITIVE_LINE_LOOP); + for (x, y) in [(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!(r.lines.len(), 6, "a loop of three is three segments"); + r.lines.clear(); + r.begin(BEGIN_PRIMITIVE_LINE_STRIP); + for (x, y) in [(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!(r.lines.len(), 4, "a strip of three is two"); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_LINES); + r.v(0.0, 0.0); + r.v(0.0, 0.0); + r.end(); + assert_eq!(r.path.counts.empty_lines, 1); + let before = r.lines.len(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_LINES); + r.v3(-3.0, 0.0, 5.0); + r.v3(3.0, 0.0, 5.0); + r.end(); + assert_eq!((r.lines.len(), r.path.counts.clipped_away), (before, 1), "beyond the far plane: clipped away"); +} + +/// A point of a size to one pixel is a unit square about the vertex holding one +/// sample; a larger one is a sixteen-triangle disc. +#[test] +fn points_are_a_square_or_a_disc() { + let mut r = Rig::new(); + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(0.0, 0.0); + r.end(); + assert_eq!((r.path.counts.points, r.tris.len()), (1, 2)); + let xs: Vec<_> = r.tris.iter().flatten().map(|v| v.x).collect(); + assert_eq!((xs.iter().min().copied(), xs.iter().max().copied()), (Some(grid(49.5)), Some(grid(50.5)))); + r.cmd(SET_POINT_SIZE, &[POINT_PIXEL_MAX.to_bits(), 0]); + r.tris.clear(); + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(0.0, 0.0); + r.end(); + assert_eq!(r.tris.len(), 2, "3.5 is still one pixel"); + r.cmd(SET_POINT_SIZE, &[8.0f32.to_bits(), 0]); + r.tris.clear(); + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(0.0, 0.0); + r.end(); + assert_eq!(r.tris.len(), 16, "a disc of sixteen triangles"); + let xs: Vec<_> = r.tris.iter().flatten().map(|v| v.x).collect(); + assert_eq!((xs.iter().min().copied(), xs.iter().max().copied()), (Some(grid(46.0)), Some(grid(54.0))), "size 8: radius 4"); + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(5.0, 0.0); + r.end(); + assert_eq!(r.path.counts.points, 4, "a point outside the window is still handed over: set-up cuts it"); +} + +/// Culling and two-sided orientation follow the front face. +#[test] +fn faces_are_culled_by_their_orientation() { + let mut r = Rig::new(); + r.cmd(ENABLE_CAPABILITY, &[gl::CULL_FACE]); + r.cmd(SET_FACE_CULLING, &[gl::BACK]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + assert_eq!(r.tris.len(), 1, "counter-clockwise is front, back is culled"); + r.triangle([(-0.5, -0.5), (0.0, 0.5), (0.5, -0.5)]); + assert_eq!((r.tris.len(), r.path.counts.culled), (1, 1)); + r.cmd(SET_FRONT_FACE_WINDING, &[gl::CW]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + assert_eq!(r.path.counts.culled, 2, "clockwise front flips it"); + r.cmd(SET_FACE_CULLING, &[gl::FRONT_AND_BACK]); + r.triangle([(-0.5, -0.5), (0.0, 0.5), (0.5, -0.5)]); + assert_eq!(r.path.counts.culled, 3, "front and back culls both"); +} + +/// Flat shading takes the provoking vertex's colour; smooth keeps each. +#[test] +fn flat_shading_takes_the_colour_of_the_vertex_gl_names() { + let mut r = Rig::new(); + r.cmd(SET_FLAT_OR_SMOOTH_SHADING, &[gl::FLAT]); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.colour(1.0, 0.0, 0.0); + r.v(-0.5, -0.5); + r.colour(0.0, 1.0, 0.0); + r.v(0.5, -0.5); + r.colour(0.0, 0.0, 1.0); + r.v(0.0, 0.5); + r.end(); + assert!(r.tris[0].iter().all(|v| v.c == [0, 0, 8191, 8191]), "the last of a triangle"); + r.cmd(SET_FLAT_OR_SMOOTH_SHADING, &[0x1d01]); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.colour(1.0, 0.0, 0.0); + r.v(-0.5, -0.5); + r.colour(0.0, 1.0, 0.0); + r.v(0.5, -0.5); + r.colour(0.0, 0.0, 1.0); + r.v(0.0, 0.5); + r.end(); + assert_eq!(r.tris[1].map(|v| v.c), [[8191, 0, 0, 8191], [0, 8191, 0, 8191], [0, 0, 8191, 8191]]); +} + +/// A light at the eye, facing a surface head on, gives the diffuse colour; the +/// scene ambient and a light off give the ambient term. +#[test] +fn a_light_lights_the_vertex() { + let mut r = Rig::new(); + r.cmd(ENABLE_CAPABILITY, &[gl::LIGHTING]); + r.cmd(ENABLE_CAPABILITY, &[gl::LIGHT0]); + // Light 0 directional, along +z, white diffuse (the initial value). + let mut a = vec![gl::LIGHT0, gl::POSITION]; + a.extend(bits(&[0.0, 0.0, 1.0, 0.0])); + r.cmd(SET_LIGHT_PARAMETER_FLOAT_FORM, &a); + // Material diffuse red. + let mut m = vec![gl::FRONT_AND_BACK, gl::DIFFUSE]; + m.extend(bits(&[1.0, 0.0, 0.0, 1.0])); + r.cmd(SET_MATERIAL_PARAMETER_FLOAT_FORM, &m); + r.cmd(SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT, &bits(&[0.0, 0.0, 1.0])); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v(-0.5, -0.5); + r.v(0.5, -0.5); + r.v(0.0, 0.5); + r.end(); + let c = r.tris[0][0].c; + // Diffuse 1.0 plus ambient 0.2 * 0.2 on the red channel, clamped; blue gets the ambient only. + assert_eq!(c[0], 8191); + assert_eq!(c[2], colour13(0.2 * 0.2)); + r.cmd(DISABLE_CAPABILITY, &[gl::LIGHT0]); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v(-0.5, -0.5); + r.v(0.5, -0.5); + r.v(0.0, 0.5); + r.end(); + assert_eq!(r.tris[1][0].c[0], colour13(0.2 * 0.2), "no light: ambient only"); +} + +/// Texture words are `s/w, t/w, r/w, q/w` of a textured vertex and zero +/// otherwise; the form of each coordinate token is counted. +#[test] +fn textured_vertices_carry_the_perspective_words() { + let mut r = Rig::new(); + // A perspective projection: w = -z_eye. + let mut p = math::IDENT; + p[11] = -1.0; + p[15] = 0.0; + r.cmd(SELECT_CURRENT_MATRIX_STACK, &[gl::PROJECTION]); + r.cmd(LOAD_MATRIX_16_FLOATS, &bits(&p)); + r.cmd(SELECT_CURRENT_MATRIX_STACK, &[gl::MODELVIEW]); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_2D]); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + for (s, t, x) in [(0.0f32, 0.0f32, -0.5f32), (1.0, 0.0, 0.5), (0.5, 1.0, 0.0)] { + r.cmd(SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT, &bits(&[s, t])); + r.v3(x, 0.0 + if x == 0.0 { 0.5 } else { -0.5 }, -2.0); + } + r.end(); + let d = &r.tris[0]; + // w is 2 and q is 1, so s/w = s / 2 and the fourth word is q/w = 1/w. + assert_eq!(d[1].t[0], [0.5, 0.0, 0.0, 0.5]); + assert_eq!(d[2].t[0], [0.25, 0.5, 0.0, 0.5]); + assert_eq!(r.path.counts.texcoord, [0, 3, 0, 0, 0]); + // With q = 2 the fourth word is q/w, not 1/w. + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + for x in [-0.5f32, 0.5, 0.0] { + r.cmd(SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT, &bits(&[1.0, 1.0, 0.0, 2.0])); + r.v3(x, if x == 0.0 { 0.5 } else { -0.5 }, -2.0); + } + r.end(); + assert_eq!(r.tris[1][0].t[0], [0.5, 0.5, 0.0, 1.0], "s/w, t/w, r/w and q/w with w 2 and q 2: the fourth is 1, not 1/w = 0.5"); + r.tris.remove(1); + // Texturing off: the words are zero. + r.cmd(DISABLE_CAPABILITY, &[gl::TEXTURE_2D]); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(-0.5, -0.5, -2.0); + r.v3(0.5, -0.5, -2.0); + r.v3(0.0, 0.5, -2.0); + r.end(); + assert_eq!(r.tris[1][0].t[0], [0.0; 4]); +} + +/// The fog setter takes a parameter name and four values, in float or integer form; the enable is the +/// capability; the draw state carries the curve once fog is on, and a mode outside the three changes +/// nothing and is counted. +#[test] +fn fog_parameters_reach_the_draw_state() { + let mut r = Rig::new(); + assert!(r.gl.draw_state(r.path.screen()).fog.is_none(), "off at start-up"); + let fog = |r: &mut Rig, name: u32, v: [f32; 4]| r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[name, v[0].to_bits(), v[1].to_bits(), v[2].to_bits(), v[3].to_bits()]); + fog(&mut r, gl::FOG_MODE, [gl::LINEAR as f32, 0.0, 0.0, 0.0]); + fog(&mut r, gl::FOG_START, [2.0, 0.0, 0.0, 0.0]); + fog(&mut r, gl::FOG_END, [6.0, 0.0, 0.0, 0.0]); + fog(&mut r, gl::FOG_COLOR, [1.0, 0.5, 2.0, 1.0]); + assert!(r.gl.draw_state(r.path.screen()).fog.is_none(), "set but not enabled"); + r.cmd(ENABLE_CAPABILITY, &[gl::FOG]); + let f = r.gl.draw_state(r.path.screen()).fog.expect("on"); + assert_eq!((f.curve, f.a, f.b), (Curve::Linear, 6.0, 0.25)); + assert_eq!(f.colour, [8191.0, 4095.5, 8191.0], "colour clamped to 0..1, in 13-bit units"); + // The exponential curves take the density. + fog(&mut r, gl::FOG_MODE, [gl::EXP2 as f32, 0.0, 0.0, 0.0]); + fog(&mut r, gl::FOG_DENSITY, [0.3, 0.0, 0.0, 0.0]); + let f = r.gl.draw_state(r.path.screen()).fog.expect("on"); + assert_eq!((f.curve, f.a), (Curve::Exp2, 0.3)); + // The integer form: the mode is its number, a colour component is the integer times 2^-31 clamped to 0..1. + r.cmd(SET_FOG_PARAMETER_INTEGER_FORM, &[gl::FOG_MODE, gl::EXP, 0, 0, 0]); + r.cmd(SET_FOG_PARAMETER_INTEGER_FORM, &[gl::FOG_COLOR, 0x4000_0000, 0x8000_0000, 0x7fff_ffff, 0]); + let s = &r.gl.raster.fog; + assert_eq!(s.mode, gl::EXP); + assert_eq!(s.colour, [0.5, 0.0, 1.0], "negative integers give 0; 2^31 - 1 is within a single's step of 1"); + // An unknown mode or name is an invalid enumerant and changes nothing; the index, the offset and the + // function-table mode are accepted and not modelled; start, end and density are not range-checked. + r.cmd(SET_FOG_PARAMETER_INTEGER_FORM, &[gl::FOG_MODE, 0x1234, 0, 0, 0]); + r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[0xb67, 0, 0, 0, 0]); + r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[gl::FOG_MODE, (gl::LINEAR as f32 + 1.0).to_bits(), 0, 0, 0]); + assert_eq!((r.gl.raster.fog.mode, r.path.counts.errors, r.gl.error), (gl::EXP, 3, gl::INVALID_ENUM)); + r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[gl::FOG_INDEX, 0, 0, 0, 0]); + r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[gl::FOG_OFFSET, 0, 0, 0, 0]); + r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[gl::FOG_MODE, (gl::FOG_FUNC as f32).to_bits(), 0, 0, 0]); + assert_eq!((r.gl.raster.fog.mode, r.path.counts.unmodelled[&SET_FOG_PARAMETER_FLOAT_FORM]), (gl::EXP, 3)); + r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[gl::FOG_START, (-5.0f32).to_bits(), 0, 0, 0]); + r.cmd(SET_FOG_PARAMETER_FLOAT_FORM, &[gl::FOG_DENSITY, (-1.0f32).to_bits(), 0, 0, 0]); + assert_eq!((r.gl.raster.fog.start, r.gl.raster.fog.density, r.path.counts.errors), (-5.0, -1.0, 3)); + r.cmd(DISABLE_CAPABILITY, &[gl::FOG]); + assert!(r.gl.draw_state(r.path.screen()).fog.is_none()); +} + +/// With fog on a vertex carries its eye-space distance over w, and 1 / w; with fog off it carries +/// nothing. +#[test] +fn fogged_vertices_carry_the_coordinate_words() { + let mut r = Rig::new(); + let mut p = math::IDENT; + p[11] = -1.0; + p[15] = 0.0; + r.cmd(SELECT_CURRENT_MATRIX_STACK, &[gl::PROJECTION]); + r.cmd(LOAD_MATRIX_16_FLOATS, &bits(&p)); + r.cmd(SELECT_CURRENT_MATRIX_STACK, &[gl::MODELVIEW]); + let tri = |r: &mut Rig, z: f32| { + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(-0.5, -0.5, z); + r.v3(0.5, -0.5, z); + r.v3(0.0, 0.5, z); + r.end(); + }; + tri(&mut r, -4.0); + assert_eq!(r.tris[0][0].fog, [0.0; 2]); + r.cmd(ENABLE_CAPABILITY, &[gl::FOG]); + tri(&mut r, -4.0); + // Eye z is -4 and w is 4: the coordinate is 4, over w is 1, and 1 / w is 0.25. + assert_eq!(r.tris[1][0].fog, [1.0, 0.25]); + // A line and a point carry them too. + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_LINES); + r.v3(-0.5, 0.0, -2.0); + r.v3(0.5, 0.0, -2.0); + r.end(); + assert_eq!(r.lines[0][0].fog, [1.0, 0.5]); +} + +/// The polygon mode: front and back are set apart, an unmatched face is ignored with no error and a +/// mode is kept as sent (the pipe checks neither), and the query answers the pair. +#[test] +fn the_polygon_mode_setter_takes_a_face_and_a_mode() { + let mut r = Rig::new(); + assert_eq!(r.gl.raster.polygon_mode, [gl::FILL; 2]); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::BACK, gl::LINE]); + assert_eq!(r.gl.raster.polygon_mode, [gl::FILL, gl::LINE]); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT, gl::POINT]); + assert_eq!(r.gl.raster.polygon_mode, [gl::POINT, gl::LINE]); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT_AND_BACK, gl::FILL]); + assert_eq!(r.gl.raster.polygon_mode, [gl::FILL; 2]); + r.cmd(SET_POLYGON_FILL_MODE, &[0x1234, gl::LINE]); + assert_eq!(r.gl.raster.polygon_mode, [gl::FILL; 2], "an unmatched face is ignored"); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT, 0x1234]); + assert_eq!(r.gl.raster.polygon_mode, [0x1234, gl::FILL], "the mode is kept as sent"); + assert!(r.path.counts.unmodelled.get(&SET_POLYGON_FILL_MODE).is_none()); + assert_eq!((r.gl.error, r.path.counts.errors), (0, 0), "no error for either"); + // Below the line mode reads as point and above it as fill, whatever the value. + for (mode, points) in [(0x1234, 3), (0x1b00, 3), (0x2000, 0)] { + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT_AND_BACK, mode]); + let before = (r.path.counts.points, r.path.counts.lines); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + let after = (r.path.counts.points, r.path.counts.lines); + assert_eq!((after.0 - before.0, after.1 - before.1), (points, 0), "mode {mode:#x}"); + } +} + +/// A face in the line mode draws its boundary edges as lines and no fill; the other face still fills. +#[test] +fn a_polygon_in_the_line_mode_is_drawn_as_its_edges() { + let mut r = Rig::new(); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT, gl::LINE]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + assert_eq!((r.tris.len(), r.lines.len()), (0, 6), "three edges, two triangles each"); + assert_eq!(r.path.counts.lines, 3); + // The same triangle wound the other way is a back face, still filled. + r.lines.clear(); + r.triangle([(-0.5, -0.5), (0.0, 0.5), (0.5, -0.5)]); + assert_eq!((r.tris.len(), r.lines.len()), (1, 0)); + // Width follows the line width, and each edge starts and ends at the polygon's vertices. + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT_AND_BACK, gl::LINE]); + r.cmd(SET_LINE_WIDTH, &[2.0f32.to_bits(), 0]); + r.lines.clear(); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + let first = &r.lines[0]; + assert_eq!((first[0].y, first[1].y), (grid(25.0 + 1.0), grid(25.0 - 1.0)), "a rectangle two pixels wide about the bottom edge"); +} + +/// A face in the point mode draws a point at each vertex that starts a boundary edge. +#[test] +fn a_polygon_in_the_point_mode_is_drawn_as_its_vertices() { + let mut r = Rig::new(); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT_AND_BACK, gl::POINT]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + assert_eq!((r.tris.len(), r.path.counts.points), (6, 3), "three one-pixel squares"); + let xs: Vec<_> = r.tris.iter().flatten().map(|v| v.x).collect(); + assert_eq!((xs.iter().min().copied(), xs.iter().max().copied()), (Some(grid(24.5)), Some(grid(75.5)))); +} + +/// The edge flag of a vertex decides whether the edge from it is drawn; a strip's, a fan's and a +/// quad strip's edges are all drawn. +#[test] +fn edge_flags_choose_which_edges_the_line_mode_draws() { + let mut r = Rig::new(); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT_AND_BACK, gl::LINE]); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v(-0.5, -0.5); + r.cmd(SET_CURRENT_EDGE_FLAG, &[0]); + r.v(0.5, -0.5); + r.cmd(SET_CURRENT_EDGE_FLAG, &[1]); + r.v(0.0, 0.5); + r.end(); + assert_eq!(r.path.counts.lines, 2, "the edge that starts at the second vertex is not drawn"); + for (prim, verts, lines) in [(BEGIN_PRIMITIVE_TRIANGLE_STRIP, 4, 6), (BEGIN_PRIMITIVE_TRIANGLE_FAN, 4, 6), (BEGIN_PRIMITIVE_QUAD_STRIP, 4, 4)] { + let before = r.path.counts.lines; + r.cmd(SET_CURRENT_EDGE_FLAG, &[0]); + r.begin(prim); + for k in 0..verts { + r.v(-0.5 + 0.25 * k as f32, if k % 2 == 0 { -0.5 } else { 0.5 }); + } + r.end(); + assert_eq!(r.path.counts.lines - before, lines, "primitive {prim:#x}"); + } +} + +/// Clipping keeps the flags of the edges it keeps and draws the edge it adds along the plane. +#[test] +fn clipping_leaves_the_outline_of_what_is_inside() { + let mut r = Rig::new(); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT_AND_BACK, gl::LINE]); + // Past the right guard band (x > 8): a triangle with one vertex far outside is cut to a quad. + r.triangle([(-0.5, -0.5), (20.0, -0.5), (-0.5, 0.5)]); + assert_eq!(r.path.counts.lines, 4, "the clipped triangle is a four-sided polygon"); +} + +/// The smoothing enables reach the draw state; a multisampled window smooths lines only (see the board test). +#[test] +fn the_smoothing_enables_reach_the_draw_state() { + let mut r = Rig::new(); + let smooth = |r: &mut Rig| r.gl.draw_state(r.path.screen()).smooth; + assert_eq!(smooth(&mut r), Smooth::default()); + r.cmd(ENABLE_CAPABILITY, &[gl::POINT_SMOOTH]); + r.cmd(ENABLE_CAPABILITY, &[gl::LINE_SMOOTH]); + assert_eq!(smooth(&mut r), Smooth { points: true, lines: true, polygons: false }); + r.cmd(ENABLE_CAPABILITY, &[gl::POLYGON_SMOOTH]); + r.cmd(DISABLE_CAPABILITY, &[gl::POINT_SMOOTH]); + assert_eq!(smooth(&mut r), Smooth { points: false, lines: true, polygons: true }); + assert!(r.path.counts.caps.is_empty(), "the three are taken"); +} + +/// A smoothed point is the square about its disc, with the disc's centre and radius, and no triangles +/// of the unsmoothed forms; a size of nothing draws nothing. +#[test] +fn a_smoothed_point_is_a_square_about_a_disc() { + let mut r = Rig::new(); + r.cmd(ENABLE_CAPABILITY, &[gl::POINT_SMOOTH]); + r.cmd(SET_POINT_SIZE, &[3.0f32.to_bits(), 0]); + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(0.0, 0.0); + r.end(); + assert_eq!((r.tris.len(), r.discs.len(), r.path.counts.points), (0, 1, 1)); + let d = r.discs[0]; + assert_eq!((d.x, d.y, d.r), (50.0, 50.0, 1.5)); + let xs: Vec<_> = d.quad.iter().map(|v| v.x).collect(); + assert_eq!((xs.iter().min().copied(), xs.iter().max().copied()), (Some(grid(48.0)), Some(grid(52.0))), "the radius and half a pixel about the centre"); + r.cmd(SET_POINT_SIZE, &[0.0f32.to_bits(), 0]); + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(0.0, 0.0); + r.end(); + assert_eq!((r.tris.len(), r.discs.len()), (2, 1), "no size: the unsmoothed rule's one pixel square, as before"); +} + +/// A smoothed line is its rectangle whole, four vertices from the first end's left, for set-up to weigh; +/// an unsmoothed line is two triangles. +#[test] +fn a_smoothed_line_is_one_rectangle() { + let mut r = Rig::new(); + let line = |r: &mut Rig| { + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_LINES); + r.v(-0.5, 0.0); + r.v(0.5, 0.0); + r.end(); + }; + line(&mut r); + assert_eq!((r.lines.len(), r.aa.len()), (2, 0)); + r.cmd(ENABLE_CAPABILITY, &[gl::LINE_SMOOTH]); + line(&mut r); + assert_eq!((r.lines.len(), r.aa.len()), (2, 1)); + let (poly, verts) = (r.aa[0], &r.aa_verts); + assert_eq!((poly.n, poly.line), (4, true)); + let ys: Vec<_> = verts.iter().map(|v| v.y).collect(); + assert_eq!(ys, [grid(50.5), grid(49.5), grid(49.5), grid(50.5)]); + assert_eq!(verts.iter().map(|v| v.d).collect::>(), [0.0, 0.0, 50.0, 50.0], "the distance along the line, at each end"); +} + +/// A smoothed polygon goes whole, a fan's worth of vertices in the order given; a smoothed polygon in a +/// line mode is drawn as lines, which are smoothed by the polygon switch (inferred), not the line one. +#[test] +fn a_smoothed_polygon_goes_whole() { + let mut r = Rig::new(); + r.cmd(ENABLE_CAPABILITY, &[gl::POLYGON_SMOOTH]); + r.begin(BEGIN_PRIMITIVE_POLYGON); + for (x, y) in [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5), (-0.6, 0.0)] { + r.v(x, y); + } + r.end(); + assert_eq!((r.tris.len(), r.aa.len(), r.aa[0].n, r.aa[0].line), (0, 1, 5, false)); + r.cmd(SET_POLYGON_FILL_MODE, &[gl::FRONT_AND_BACK, gl::LINE]); + r.begin(BEGIN_PRIMITIVE_POLYGON); + for (x, y) in [(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!((r.aa.len(), r.lines.len(), r.path.counts.lines), (4, 0, 3), "the outline's lines follow the polygon switch, not the line one"); + r.cmd(DISABLE_CAPABILITY, &[gl::POLYGON_SMOOTH]); + r.cmd(ENABLE_CAPABILITY, &[gl::LINE_SMOOTH]); + r.begin(BEGIN_PRIMITIVE_POLYGON); + for (x, y) in [(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)] { + r.v(x, y); + } + r.end(); + assert_eq!((r.aa.len(), r.lines.len()), (4, 6), "the line switch alone leaves them plain"); +} + +/// The draw state is built again only when a state command changes it, and +/// it holds the window, the clip rectangle, the viewport and the depth switches. +#[test] +fn the_draw_state_changes_only_with_state_commands() { + let mut r = Rig::new(); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + assert_eq!(r.states[0], r.states[1]); + let s = r.states[0]; + let full = Rect { x0: 0, y0: 0, x1: 100, y1: 100 }; + assert_eq!((s.origin, s.clip, s.viewport, s.depth_test), ([0, 0], full, full, false)); + r.cmd(ENABLE_CAPABILITY, &[gl::DEPTH_TEST]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + assert_ne!(r.states[1], r.states[2]); + assert!(r.states[2].depth_test); +} + +/// A window away from the origin moves the origin and the clip rectangle, and +/// a scissor narrows the clip rectangle but not the viewport; a smaller +/// viewport inside a larger window is its own rectangle, in window-relative +/// coordinates. +#[test] +fn the_clip_and_viewport_rectangles_follow_the_window_scissor_and_viewport() { + let mut r = Rig::new(); + r.path = GlPath::new((1000, 1000)); + r.gl.place([300, 200], Rect { x0: 300, y0: 200, x1: 400, y1: 300 }, 0, 0, false); + r.cmd(SET_VIEWPORT, &bits(&[10.0, 20.0, 30.0, 40.0])); + r.cmd(ENABLE_CAPABILITY, &[gl::SCISSOR_TEST]); + r.cmd(SET_SCISSOR_BOX, &[5, 5, 50, 50]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + let s = r.states[0]; + assert_eq!(s.origin, [300, 200]); + assert_eq!(s.clip, Rect { x0: 305, y0: 205, x1: 355, y1: 255 }); + assert_eq!(s.viewport, Rect { x0: 310, y0: 220, x1: 340, y1: 260 }); + // The clip rectangle is also cut to the screen. + r.gl.place([950, 960], Rect { x0: 950, y0: 960, x1: 1050, y1: 1060 }, 0, 0, false); + r.cmd(DISABLE_CAPABILITY, &[gl::SCISSOR_TEST]); + r.triangle([(-0.5, -0.5), (0.5, -0.5), (0.0, 0.5)]); + assert_eq!(r.states[1].clip, Rect { x0: 950, y0: 960, x1: 1000, y1: 1000 }); +} + +/// A clear carries the draw state's clip rectangle and the values to clear to. +#[test] +fn a_clear_is_a_request_over_the_clip_rectangle() { + let mut r = Rig::new(); + r.cmd(SET_COLOUR_CLEAR_VALUE, &bits(&[0.0, 0.0, 1.0, 1.0])); + r.cmd(CLEAR_SELECTED_BUFFERS, &[gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT]); + assert_eq!(r.clears.len(), 1); + assert_eq!(r.clears[0].rect, Rect { x0: 0, y0: 0, x1: 100, y1: 100 }); + assert_eq!(r.clears[0].colour, Some(0xff00_00ff)); + assert!(r.clears[0].depth.is_some()); +} + +/// Forms and tokens the path does not take are counted by selector, a vertex +/// outside a begin draws nothing, and a matrix command inside one is an error. +#[test] +fn what_is_not_taken_is_counted() { + let mut r = Rig::new(); + r.cmd(SET_CURRENT_COLOUR_INDEX_DOUBLE, &[0; 2]); + r.cmd(SET_LIGHT_PARAMETER_FLOAT_FORM, &[gl::LIGHT0, 0x9999, 0, 0, 0, 0]); + assert_eq!(r.path.counts.unmodelled.get(&0x72), Some(&1)); + assert_eq!(r.path.counts.unmodelled.get(&0x7), Some(&1)); + r.v(0.0, 0.0); + assert_eq!((r.path.counts.stray, r.tris.len()), (1, 0)); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.cmd(LOAD_IDENTITY_MATRIX, &[0]); + assert_eq!((r.path.counts.errors, r.gl.error), (1, gl::INVALID_OPERATION)); +} + +/// The census: with it on, each vertex is told apart by where it fell. +#[test] +fn the_census_tells_vertices_apart_by_the_window() { + let mut r = Rig::new(); + r.path.census = true; + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(0.0, 0.0); + r.v(5.0, 0.0); + r.v3(0.0, 0.0, 9.0); + r.cmd(SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT, &bits(&[0.0, 0.0, 0.0, -1.0])); + r.end(); + let c = &r.path.counts; + assert_eq!((c.inside, c.outside_window, c.outside_volume, c.behind), (1, 1, 1, 1)); +} + +/// A double is two words, the high one first. +fn dbl(v: &[f64]) -> Vec { + v.iter().flat_map(|x| [(x.to_bits() >> 32) as u32, x.to_bits() as u32]).collect() +} + +/// The double forms of the vertex, colour, normal and texture-coordinate tokens leave the +/// context where the float forms do. +#[test] +fn the_double_forms_equal_the_float_forms() { + let mut a = Rig::new(); + let mut b = Rig::new(); + for r in [&mut a, &mut b] { + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_2D]); + } + a.cmd(SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE, &dbl(&[0.25, 0.5, 0.75])); + b.cmd(SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT, &bits(&[0.25, 0.5, 0.75])); + a.cmd(SET_CURRENT_NORMAL_3_COMPONENTS_DOUBLE, &dbl(&[0.0, 0.5, 1.0])); + b.cmd(SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT, &bits(&[0.0, 0.5, 1.0])); + assert_eq!((a.gl.color, a.gl.normal), (b.gl.color, b.gl.normal)); + assert_eq!(a.gl.color, [0.25, 0.5, 0.75, 1.0]); + a.cmd(SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE, &dbl(&[0.125, 0.25, 0.5, 0.75])); + b.cmd(SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, &bits(&[0.125, 0.25, 0.5, 0.75])); + assert_eq!(a.gl.color, [0.125, 0.25, 0.5, 0.75]); + for (n, sel, fsel) in [ + (1, SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_DOUBLE, SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT), + (2, SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_DOUBLE, SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT), + (3, SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_DOUBLE, SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT), + (4, SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_DOUBLE, SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT), + ] { + let v = [0.5f64, 0.25, 2.0, 4.0]; + a.cmd(sel, &dbl(&v[..n])); + b.cmd(fsel, &bits(&v[..n].iter().map(|&x| x as f32).collect::>())); + assert_eq!(a.gl.texcoord, b.gl.texcoord, "{n} components"); + } + assert_eq!(a.gl.texcoord, [0.5, 0.25, 2.0, 4.0]); + // Vertices of every width: the same triangles, and a value a float cannot hold rounds as `as f32` does. + for (r, double) in [(&mut a, true), (&mut b, false)] { + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + let p: [&[f64]; 3] = [&[-1.0, -1.0], &[1.0, -1.0, 0.0], &[0.0, 1.0, 0.0, 1.0]]; + let (dsel, fsel) = [ + (SUBMIT_VERTEX_2_COMPONENTS_DOUBLE, SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT), + (SUBMIT_VERTEX_3_COMPONENTS_DOUBLE, SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT), + (SUBMIT_VERTEX_4_COMPONENTS_DOUBLE, SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT), + ].into_iter().unzip::<_, _, Vec, Vec>(); + for (k, v) in p.iter().enumerate() { + if double { + r.cmd(dsel[k], &dbl(v)); + } else { + r.cmd(fsel[k], &bits(&v.iter().map(|&x| x as f32).collect::>())); + } + } + r.end(); + } + assert_eq!(a.tris.len(), 1); + assert_eq!(a.tris, b.tris); + assert_eq!(a.path.counts.unmodelled.len(), 0); + // A count that does not match the form is not narrowed, and is counted under the float form. + a.cmd(SUBMIT_VERTEX_3_COMPONENTS_DOUBLE, &dbl(&[1.0, 2.0])); + assert_eq!(a.path.counts.unmodelled.get(&SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT), Some(&1)); + let c = 1.0 + 2f64.powi(-40); + a.cmd(SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE, &dbl(&[c, 0.0, 0.0])); + assert_eq!(a.gl.color[0], c as f32); + // A colour double is narrowed and nothing more: it is not scaled or clamped at entry (the clamp to 0..1 + // comes later, in the colour-clamp pass). + a.cmd(SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE, &dbl(&[2.0, -0.5, 0.25, 8.0])); + assert_eq!(a.gl.color, [2.0, -0.5, 0.25, 8.0]); +} + +fn plane(r: &mut Rig, n: u32, p: [f64; 4]) { + let mut a = vec![gl::CLIP_PLANE0 + n]; + a.extend(dbl(&p)); + r.cmd(SET_USER_CLIP_PLANE, &a); +} + +fn xs(r: &Rig) -> (i32, i32) { + let all = r.tris.iter().flatten().map(|v| v.x); + (all.clone().min().expect("a triangle"), all.max().expect("a triangle")) +} + +/// An enabled user plane cuts a triangle in eye space, a disabled one does nothing, and +/// the cut lands on the plane. +#[test] +fn an_enabled_user_plane_cuts_a_triangle_and_a_disabled_one_does_not() { + let mut r = Rig::new(); + plane(&mut r, 0, [1.0, 0.0, 0.0, 0.0]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(xs(&r), (0, 100 * 256), "set but not enabled"); + r.tris.clear(); + r.cmd(ENABLE_CAPABILITY, &[gl::CLIP_PLANE0]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(xs(&r), (50 * 256, 100 * 256), "the left half is gone"); + r.tris.clear(); + r.cmd(DISABLE_CAPABILITY, &[gl::CLIP_PLANE0]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(xs(&r), (0, 100 * 256)); + // A triangle wholly behind an enabled plane is gone. + r.tris.clear(); + r.cmd(ENABLE_CAPABILITY, &[gl::CLIP_PLANE0]); + r.triangle([(-0.9, -0.5), (-0.1, -0.5), (-0.5, 0.5)]); + assert!(r.tris.is_empty()); + assert_eq!(r.path.counts.clipped_away, 1); +} + +/// A plane is kept in eye space: the coefficients times the inverse of the model-view matrix at +/// the call, so later model-view changes do not move it. A matrix with no inverse leaves it. +#[test] +fn a_plane_is_kept_in_eye_space_at_the_model_view_of_the_call() { + let mut r = Rig::new(); + r.cmd(TRANSLATE_FLOATS, &bits(&[-0.5, 0.0, 0.0])); + plane(&mut r, 2, [1.0, 0.0, 0.0, 0.0]); + assert_eq!(r.gl.user_plane[2], [1.0, 0.0, 0.0, 0.5]); + // Singular: unchanged. + r.cmd(SCALE_FLOATS, &bits(&[0.0, 1.0, 1.0])); + plane(&mut r, 2, [0.0, 1.0, 0.0, 0.0]); + assert_eq!(r.gl.user_plane[2], [1.0, 0.0, 0.0, 0.5]); + // The model-view changes afterwards; the plane stays where it was put (eye x >= -0.5). + r.cmd(LOAD_IDENTITY_MATRIX, &[0]); + r.cmd(ENABLE_CAPABILITY, &[gl::CLIP_PLANE0 + 2]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(xs(&r), (25 * 256, 100 * 256)); +} + +/// A name out of range is an error, and the planes answer their query in eye space. +#[test] +fn a_plane_name_out_of_range_is_an_error_and_the_query_returns_the_plane() { + let mut r = Rig::new(); + let mut a = vec![gl::CLIP_PLANE0 + 6]; + a.extend(dbl(&[1.0, 0.0, 0.0, 0.0])); + r.cmd(SET_USER_CLIP_PLANE, &a); + assert_eq!((r.path.counts.errors, r.gl.error), (1, gl::INVALID_ENUM)); + r.gl.error = 0; + plane(&mut r, 5, [0.0, 1.0, 0.0, -0.5]); + let mut c = query::Counts::default(); + assert_eq!(query::answer(&mut r.gl, &mut c, QUERY_CLIP_PLANE, &[gl::CLIP_PLANE0 + 5]), bits(&[0.0, 1.0, 0.0, -0.5])); + assert_eq!(query::answer(&mut r.gl, &mut c, QUERY_CLIP_PLANE, &[gl::CLIP_PLANE0 + 9]), Vec::::new()); + assert_eq!(r.gl.error, gl::INVALID_ENUM); +} + +/// Lines and points are cut by a user plane too, and a user plane and the far plane cut together. +#[test] +fn lines_points_and_the_volume_planes_work_with_a_user_plane() { + let mut r = Rig::new(); + r.cmd(ENABLE_CAPABILITY, &[gl::CLIP_PLANE0]); + plane(&mut r, 0, [1.0, 0.0, 0.0, 0.0]); + r.begin(BEGIN_PRIMITIVE_POINTS); + r.v(-0.5, 0.0); + r.v(0.5, 0.0); + r.end(); + assert_eq!((r.path.counts.points, r.path.counts.clipped_away), (1, 1)); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_LINES); + r.v(-0.5, 0.0); + r.v(0.5, 0.0); + r.end(); + assert_eq!(r.path.counts.lines, 1); + let x0 = r.lines.iter().flatten().map(|v| v.x).min().expect("a line"); + assert!(x0 >= 50 * 256 - 256, "{x0}"); + // A triangle across both the user plane and the far plane. + r.tris.clear(); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(-1.0, -1.0, 0.0); + r.v3(1.0, -1.0, 0.0); + r.v3(0.0, 1.0, 3.0); + r.end(); + assert!(!r.tris.is_empty()); + let (lo, _) = xs(&r); + assert!(lo >= 50 * 256); + assert!(r.tris.iter().flatten().all(|v| v.z <= 4_294_967_040)); +} + +/// The clip plane command is framed at nine words: a name and four doubles, high word first. +#[test] +fn the_clip_plane_command_frames_nine_words() { + use crate::dev::kona::tables::GL; + assert_eq!(GL.iter().find(|s| s.selector == SET_USER_CLIP_PLANE).map(|s| s.argc), Some(9)); +} + +/// The enable tokens take the six clip planes in 0x3000 to 0x3fff; the rest of the range is an +/// invalid enumerant that changes nothing. +#[test] +fn the_enable_tokens_take_only_six_clip_planes() { + let mut r = Rig::new(); + for n in 0..6 { + r.cmd(ENABLE_CAPABILITY, &[gl::CLIP_PLANE0 + n]); + } + assert_eq!((r.gl.user_on, r.path.counts.errors), (0b11_1111, 0)); + for name in [0x3006, 0x3007, 0x3100, 0x3fff] { + r.cmd(ENABLE_CAPABILITY, &[name]); + r.cmd(DISABLE_CAPABILITY, &[name]); + } + assert_eq!((r.gl.user_on, r.path.counts.errors, r.gl.error), (0b11_1111, 8, gl::INVALID_ENUM)); +} + +fn tex_gen(r: &mut Rig, coord: u32, name: u32, v: [u32; 7]) { + let mut a = vec![gl::TEXTURE_COORD_S + coord, name]; + a.extend(v); + r.cmd(TEXTURE_COORDINATE_GENERATION_FLOAT_FORM, &a); +} + +fn tex_mode(r: &mut Rig, coord: u32, mode: u32) { + tex_gen(r, coord, gl::TEXTURE_GEN_MODE, [mode, 0, 0, 0, 0, 0, 0]); +} + +fn tex_plane(r: &mut Rig, coord: u32, name: u32, p: [f32; 4]) { + let w = bits(&p); + tex_gen(r, coord, name, [w[0], w[1], w[2], w[3], 0, 0, 0]); +} + +/// The texture words (s, t, r, q, over w) of the first triangle's vertices. +fn tex_of(r: &Rig) -> Vec<[f32; 4]> { + r.tris[0].iter().map(|v| v.t[0]).collect() +} + +fn textured() -> Rig { + let mut r = Rig::new(); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_2D]); + r +} + +/// Object linear: the plane's product with the vertex as given, for the coordinates enabled; the +/// others keep the current coordinate. +#[test] +fn object_linear_generation_replaces_only_the_enabled_coordinates() { + let mut r = textured(); + r.cmd(SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT, &bits(&[0.0, 0.75, 0.0, 1.0])); + tex_mode(&mut r, 0, 0x2401); + tex_plane(&mut r, 0, gl::OBJECT_PLANE, [0.5, 0.0, 0.0, 0.5]); + // Set up but not enabled: no effect. + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(tex_of(&r), vec![[0.0, 0.75, 0.0, 1.0]; 3]); + r.tris.clear(); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_GEN_S]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(tex_of(&r), [[0.0, 0.75, 0.0, 1.0], [1.0, 0.75, 0.0, 1.0], [0.5, 0.75, 0.0, 1.0]]); + // The texture matrix runs after generation. + r.tris.clear(); + r.cmd(SELECT_CURRENT_MATRIX_STACK, &[gl::TEXTURE]); + r.cmd(SCALE_FLOATS, &bits(&[2.0, 1.0, 1.0])); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(tex_of(&r)[1], [2.0, 0.75, 0.0, 1.0]); + // Disabled again: back to the current coordinate. + r.tris.clear(); + r.cmd(DISABLE_CAPABILITY, &[gl::TEXTURE_GEN_S]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(tex_of(&r)[1][0], 0.0); +} + +/// Generation runs under the 3D enable as it does under the 2D one: a volume with generated s, t and r +/// is the usual way to draw one. +#[test] +fn generation_runs_under_the_3d_enable_too() { + let mut r = Rig::new(); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_3D]); + r.cmd(SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT, &bits(&[0.0, 0.75, 0.0, 1.0])); + tex_mode(&mut r, 0, 0x2401); + tex_plane(&mut r, 0, gl::OBJECT_PLANE, [0.5, 0.0, 0.0, 0.5]); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_GEN_S]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(tex_of(&r), [[0.0, 0.75, 0.0, 1.0], [1.0, 0.75, 0.0, 1.0], [0.5, 0.75, 0.0, 1.0]]); +} + +/// Eye linear: the eye plane is kept in eye space as of the call, and the eye position moves +/// with the model-view matrix. Eye linear is the start-up mode, s from x and t from y. +#[test] +fn eye_linear_generation_uses_the_eye_position_and_an_eye_plane_set_in_eye_space() { + let mut r = textured(); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_GEN_S]); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_GEN_S + 1]); + r.cmd(TRANSLATE_FLOATS, &bits(&[0.25, 0.0, 0.0])); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + // Start-up planes, set at the identity: s = eye x = x + 0.25, t = eye y. + assert_eq!(tex_of(&r), [[-0.75, -1.0, 0.0, 1.0], [1.25, -1.0, 0.0, 1.0], [0.25, 1.0, 0.0, 1.0]]); + // A plane given under the translation is kept in eye space: x + 0 in object space is eye x - 0.25. + r.tris.clear(); + tex_plane(&mut r, 0, gl::EYE_PLANE, [1.0, 0.0, 0.0, 0.0]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(tex_of(&r)[0][0], -1.0); +} + +/// Sphere map reflects the view direction about the eye-space normal. +#[test] +fn sphere_map_generation_reflects_the_view_about_the_normal() { + let mut r = textured(); + for c in 0..2 { + tex_mode(&mut r, c, 0x2402); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_GEN_S + c]); + } + r.cmd(SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT, &bits(&[0.0, 0.0, 1.0])); + r.begin(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES); + r.v3(0.0, 0.0, -0.5); + r.v3(0.5, 0.0, -0.5); + r.v3(0.0, 0.5, -0.5); + r.end(); + let t = tex_of(&r); + assert_eq!(t[0][..2], [0.5, 0.5], "straight at the viewer"); + // u = (1, 0, -1) / sqrt 2 reflects to (1, 0, 1) / sqrt 2: s = rx / (2 sqrt(rx^2 + (rz + 1)^2)) + 1/2. + let r2 = 0.5f32.sqrt(); + let s = r2 / (2.0 * (r2 * r2 + (r2 + 1.0) * (r2 + 1.0)).sqrt()) + 0.5; + assert!((t[1][0] - s).abs() < 1e-5 && (t[1][1] - 0.5).abs() < 1e-5, "{:?}", t[1]); + assert!((t[2][1] - s).abs() < 1e-5 && (t[2][0] - 0.5).abs() < 1e-5, "{:?}", t[2]); +} + +/// Generation shows only when texturing is on, and the parameters are checked. +#[test] +fn generation_needs_texturing_and_the_parameters_are_checked() { + let mut r = Rig::new(); + tex_plane(&mut r, 0, gl::OBJECT_PLANE, [1.0, 0.0, 0.0, 0.0]); + tex_mode(&mut r, 0, 0x2401); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_GEN_S]); + r.triangle([(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)]); + assert_eq!(tex_of(&r), vec![[0.0; 4]; 3], "texturing is off"); + assert_eq!(r.path.counts.errors, 0); + // A coordinate or name the pipe does not accept is an invalid enumerant; changes nothing. + tex_mode(&mut r, 4, 0x2401); + assert_eq!((r.path.counts.errors, r.gl.error), (1, gl::INVALID_ENUM)); + tex_gen(&mut r, 0, 0x1234, [0; 7]); + assert_eq!(r.path.counts.errors, 2); + // The distance planes and modes are accepted and counted, and change nothing. + tex_plane(&mut r, 0, 0x81f4, [1.0; 4]); + tex_mode(&mut r, 0, 0x81f0); + tex_mode(&mut r, 0, 0x81f3); + assert_eq!(r.path.counts.unmodelled.get(&TEXTURE_COORDINATE_GENERATION_FLOAT_FORM), Some(&3)); + assert_eq!((r.path.counts.errors, r.gl.texgen.mode[0]), (2, 0x2401)); + // Names and modes outside the accepted ranges are invalid enumerants, whatever their neighbours. + for name in [0x24ff, 0x2503, 0x81f3, 0x81f8] { + tex_gen(&mut r, 0, name, [0; 7]); + } + for mode in [0x23ff, 0x2403, 0x81ef, 0x81f4] { + tex_mode(&mut r, 0, mode); + } + assert_eq!((r.path.counts.errors, r.gl.texgen.mode[0]), (10, 0x2401)); + // Sphere map on r is not refused: the mode is kept and the coordinate is left as it was (inferred). + tex_mode(&mut r, 2, 0x2402); + assert_eq!((r.path.counts.errors, r.gl.texgen.mode[2]), (10, 0x2402)); + // The framing: the command is nine words. + let argc = crate::dev::kona::tables::GL.iter().find(|s| s.selector == TEXTURE_COORDINATE_GENERATION_FLOAT_FORM).map(|s| s.argc); + assert_eq!(argc, Some(9)); +} + +/// Start-up is OpenGL's: s = (1,0,0,0), t = (0,1,0,0), r and q zero, eye linear. An eye plane is +/// multiplied by the inverse model-view at the call; an object plane is stored as sent. +#[test] +fn texgen_starts_as_gl_does_and_planes_are_stored_as_the_pipe_stores_them() { + let mut r = Rig::new(); + let g = r.gl.texgen; + assert_eq!((g.eye[0], g.eye[1], g.eye[2], g.eye[3]), ([1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0; 4], [0.0; 4])); + assert_eq!((g.object[0], g.object[1], g.object[2], g.object[3]), (g.eye[0], g.eye[1], [0.0; 4], [0.0; 4])); + assert_eq!(g.mode, [0x2400; 4]); + r.cmd(SELECT_CURRENT_MATRIX_STACK, &[gl::MODELVIEW]); + r.cmd(TRANSLATE_FLOATS, &bits(&[0.0, 0.0, -5.0])); + tex_plane(&mut r, 0, gl::EYE_PLANE, [0.0, 0.0, 1.0, 0.0]); + tex_plane(&mut r, 0, gl::OBJECT_PLANE, [0.0, 0.0, 1.0, 0.0]); + assert_eq!(r.gl.texgen.object[0], [0.0, 0.0, 1.0, 0.0], "stored unchanged"); + assert_eq!(r.gl.texgen.eye[0], [0.0, 0.0, 1.0, 5.0], "times the inverse model-view"); +} + +/// The generation and clip-plane state is part of a context's saved state. +#[test] +fn texgen_and_clip_plane_state_round_trips_through_a_stream() { + use crate::dev::kona::stream::{Reader, Writer}; + let mut r = Rig::new(); + tex_mode(&mut r, 1, 0x2402); + tex_plane(&mut r, 3, gl::OBJECT_PLANE, [1.0, 2.0, 3.0, 4.0]); + tex_plane(&mut r, 2, gl::EYE_PLANE, [5.0, 6.0, 7.0, 8.0]); + r.cmd(ENABLE_CAPABILITY, &[gl::TEXTURE_GEN_S + 3]); + plane(&mut r, 4, [1.0, 0.5, 0.25, 0.125]); + r.cmd(ENABLE_CAPABILITY, &[gl::CLIP_PLANE0 + 4]); + let mut w = Writer::default(); + r.gl.visit(&mut w); + let mut g = Gl::default(); + let mut rd = Reader::new(&w.out); + g.visit(&mut rd); + assert!(rd.finish().is_ok()); + assert_eq!(g.texgen, r.gl.texgen); + assert_eq!((g.user_plane, g.user_on), (r.gl.user_plane, r.gl.user_on)); +} diff --git a/src/dev/kona/back/gl/texture.rs b/src/dev/kona/back/gl/texture.rs new file mode 100644 index 000000000..bdab4a0b5 --- /dev/null +++ b/src/dev/kona/back/gl/texture.rs @@ -0,0 +1,2073 @@ +//! The texture commands: loads into texture memory, binds, and the unpack state a +//! load's sizing reads. +//! +//! A level reaches texture memory as texel draws (`DRAW_TEXELS_INTO_TEXTURE_MEMORY`) +//! and a load finish. The load does not name its level: the header's address word +//! (word 18, or 19 when the bank bit is set) and the bank bit say where it lands. +//! The model keeps its levels by (bank, address) per context (the plan keyed images +//! by cache entry; the entry number reaches a level only through the bind's table, +//! which names the same addresses). A bind and its record list the levels' addresses +//! in order; the levels are found by that table and the first level's bank, flipped +//! for every second level. That is the contract for a numbered bind and for an +//! all-ones bind alike, which is why an image survives its entry's freeing. +//! +//! Grades (the table's): the header words' roles, the texel width by size code, the unit of +//! a level address (256 bits per bank) and the rule that places a small level in its tile are +//! established; the unit as the hardware's own is inferred, and the bank flip is inferred. The +//! texel layout in texture memory is the model's own (`store::texture`). +//! +//! A bind carries no storage code and the pipe stores none per level: a level is found by its bank and address, +//! and its words are sampled under the format the bind names (bits 20:14 of its mode word), whatever storage +//! code loaded them. Sixteen-bit words reloaded under the one-component code (the library's read-back and reload +//! of a level) are therefore kept raw until a bind samples them, and a read-back under that code returns the +//! words themselves. +//! +//! A level smaller than its tile on an axis is sent by the client as 2^(tile exponent - level +//! exponent) copies along that axis, each at its own origin, and the last copy gives the level's +//! origin (established). A level at least as large as the tile is sent once, at the origin, and a +//! later load of part of it is an update. The level's logical size is the bind's level-0 size +//! halved per level (established); the model keeps every load's box and the last load, takes the +//! last copy's origin on an axis the bind says is small, and the origin and logical size on any +//! other. +//! +//! A level takes `pw * ph` texels of the width its size code says, which in units of +//! 256 bits per bank is the extent that address names. Two levels of one bank cannot share a +//! unit, so a load at a new address retires the levels it overlaps (the hardware overwrites +//! them); a bind to a retired level finds nothing. Texture memory is addressed the way the pipe +//! addresses it (`store::texture`): a level at address `a` of a bank is the words from +//! `a * UNIT_WORDS` of that bank's plane, a load at an address that holds a level of the same +//! shape writes it in place, and nothing is placed, so a load is refused only when its units +//! reach past the bank or its texels would take the pool past the product's texture memory +//! (the planes are sparse pages, and the pages held across all of them are capped). A context whose slot is activated afresh gives its planes back +//! (`TexContext::release`). Established: the pool is one per pipe, loads write in place at +//! addresses relative to the owner's block, and a level reloaded at the same place overwrites +//! it; the entry-to-address tables are per context. Approximated: that the overlap is by the +//! unit arithmetic above in each bank separately, that the planes are a context's own (the +//! owner's block offset is not modelled, so contexts neither contend for memory nor alias, so a texture loaded through one member of a share group (contexts the kernel lets see the same physical block) is not visible to a bind from another member), +//! and that a context's texture memory is freed when its slot is activated afresh (the kernel's +//! block release is not read). The clip-map, bicubic, the old-style bind and the load set-up have no +//! sender in the recordings (or are out of scope) and are counted, not modelled. +//! +//! A volume (target 0x806f) is one command per slice, the slice number in header word 15 and the +//! level's physical depth in the r exponent of word 16 (both established). Every slice of a level +//! carries the same pair of address words, so a level is still found by (bank, address) and its +//! slices are stacked in the arena in the model's own layout; where the hardware puts the texels +//! inside the pair is unknown. A volume's bind is the short bind and a record of the 3D forms +//! (tokens 0x17b and 0x17c, framing established). A level's address in the record is looked for +//! in the bank its parity names and then in the other (inferred: the table says the bank bit of +//! a mip-mapped volume's level is its parity and that of a single-level volume is "bank state"). + +use super::super::raster::texel::{default_weights, spline_samples, Combine, Detail, EnvMode, Filter, Sharpen, TexDesc, TexLevel, Wrap, MAX_LEVELS, CURVE_NODES}; +use super::super::store::texture::{TextureStore, PLANES_PER_CONTEXT, UNIT_WORDS}; +use crate::dev::kona::stream::Stream; +use crate::dev::kona::defs::*; +use std::collections::HashMap; + +/// The unpack state a load's sizing and decoding read (the pixel-store parameters). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Unpack { + pub row_length: u32, + pub skip_rows: u32, + pub skip_pixels: u32, + pub alignment: u32, + pub swap_bytes: bool, +} + +impl Default for Unpack { + fn default() -> Self { + Unpack { row_length: 0, skip_rows: 0, skip_pixels: 0, alignment: 4, swap_bytes: false } + } +} + +impl Unpack { + /// A pixel-store parameter, GL's names. An alignment that is not 1, 2, 4 or 8 is + /// refused (the board keeps only a power of two up to 8). The pack side's names + /// are kept by the board and read by no load, so they are ignored here. + pub fn set(&mut self, name: u32, value: u32) { + match name { + 0xcf0 => self.swap_bytes = value != 0, + 0xcf2 => self.row_length = value, + 0xcf3 => self.skip_rows = value, + 0xcf4 => self.skip_pixels = value, + 0xcf5 if matches!(value, 1 | 2 | 4 | 8) => self.alignment = value, + _ => {} + } + } +} + +/// A texel draw's header, the twenty words by their roles. +#[derive(Clone, Copy, Debug)] +pub struct Header { + pub target: u32, + pub w: u32, + pub h: u32, + pub format: u32, + pub ty: u32, + pub offset: u32, + pub dma: bool, + pub strided: bool, + pub storage: u32, + pub x0: u32, + pub y0: u32, + pub x1: u32, + pub y1: u32, + pub phys: u32, + pub misc: u32, + pub address: u32, + /// The destination slice (word 15), for a volume. + pub slice: u32, +} + +/// The extent of a level in units of 256 bits: its rectangle's texels at the width the size +/// code says, in 16-bit lanes, over sixteen lanes a unit. +fn units(pw: u32, ph: u32, size_code: u32) -> u64 { + ((pw as u64 * ph as u64) << size_code).div_ceil(texture::ADDRESS_UNIT_LANES as u64) +} + +impl Header { + pub fn parse(a: &[u32]) -> Option
{ + let a: &[u32; 20] = a.try_into().ok()?; + let bank = texture::BANK.get(a[17]); + Some(Header { target: a[0], w: a[1], h: a[2], format: a[3], ty: a[4], offset: a[5] & 3, dma: a[6] != 0, strided: a[7] != 0, storage: a[9], x0: a[11] & 0xffff, y0: a[12] & 0xffff, x1: a[13] & 0xffff, y1: a[14] & 0xffff, phys: a[16], misc: a[17], address: a[18 + bank as usize], slice: a[15] & 0x7ff }) + } + + pub fn bank(&self) -> u8 { + texture::BANK.get(self.misc) as u8 + } + + /// Whether the load is of a detail image (target 0x8095), which the model keeps apart from the base levels. + pub fn is_detail(&self) -> bool { + self.target == texture::DETAIL_TARGET + } + + /// Whether the load is of a volume (target 0x806f). + pub fn is_volume(&self) -> bool { + self.target == texture::VOLUME_TARGET + } + + /// The level's physical depth in slices: the r exponent of word 16 for a volume (established), one otherwise. + pub fn depth(&self) -> u32 { + if self.is_volume() { 1 << ((self.phys >> 8 & 0xf).min(11)) } else { 1 } + } + + /// The log2 of a texel's width in 16-bit lanes. + pub fn size_code(&self) -> u32 { + texture::SIZE_CODE.get(self.misc) + } + + /// Components of the source format, or None for one the model does not decode + /// (CMYK, colour index, stencil, depth). + fn components(&self) -> Option { + Some(match self.format { + 0..=3 | 6 => 1, + 4 => 3, + 5 | 8 => 4, + 7 => 2, + _ => return None, + }) + } + + /// Bytes of a component, for the plain types (signed and unsigned byte, short, int, float). + fn type_bytes(&self) -> Option { + Some(match self.ty { + 0 | 1 => 1, + 2 | 3 => 2, + 4..=6 => 4, + _ => return None, + }) + } + + /// Bytes of a pixel of a packed type: 3-3-2 (code 8), 4-4-4-4 (9), 5-5-5-1 (10), 8-8-8-8 (11) and 10-10-10-2 (12), + /// the sizes of OpenGL's packed types of those names (established: the library sends the codes, and the pipe + /// takes their own size as the pixel's). The library requires RGB with 3-3-2 and RGBA with the others, and + /// refuses any other format (an error in the client), so the model does not decode one. + fn packed_bytes(&self) -> Option { + match (self.ty, self.format) { + (8, 4) => Some(1), + (9 | 10, 5) => Some(2), + (11 | 12, 5) => Some(4), + _ => None, + } + } + + fn pixel_bytes(&self) -> Option { + match self.ty { + 8..=12 => self.packed_bytes(), + _ => Some(self.components()? * self.type_bytes()?), + } + } + + /// The row stride and the first byte, from the unpack state. + fn geometry(&self, u: &Unpack) -> Option<(u64, u64, u64)> { + let p = self.pixel_bytes()? as u64; + let l = if u.row_length == 0 { self.w } else { u.row_length } as u64; + let a = u.alignment.max(1) as u64; + // The unpack state is the guest's: a product past what fits is no size (counted as an unsupported load). + let stride = (l * p).div_ceil(a).checked_mul(a)?; + let start = (u.skip_rows as u64).checked_mul(stride)?.checked_add(self.offset as u64)?.checked_add(u.skip_pixels as u64 * p)?; + Some((p, stride, start)) + } + + /// Words of image that follow the header in the command stream, from the header + /// and the unpack state (established); None for a format or type the model does + /// not decode, whose size is not computed. + pub fn image_words(&self, u: &Unpack) -> Option { + let (p, stride, start) = self.geometry(u)?; + let (w, h) = (self.w as u64, self.h as u64); + if w == 0 || h == 0 { + return Some(0); + } + let n = if self.strided { + // A row's first byte sits at `(start + r * stride) % 4`, which repeats every four rows. + let row = |r: u64| (start.wrapping_add(r.wrapping_mul(stride)) % 4 + w * p).div_ceil(4); + let (cycles, rest) = (h / 4, h % 4); + cycles.checked_mul((0..4).map(row).sum::())?.checked_add((0..rest).map(row).sum::())? + } else { + // A row length below the width puts the last row's end before its start: no size (counted as an unsupported load). + let total = stride.checked_mul(h)?.checked_sub(stride.checked_sub(w * p)?)?; + start.checked_add(total)?.div_ceil(4) - start / 4 + }; + u32::try_from(n).ok() + } +} + +/// A texel read-back's header (token 0x052), eighteen words (the table, section 7.16): the target, +/// the size, the format, type and storage codes, a spare, the first and last texel as x, y, z, and +/// five words copied from the texture's load record: a word of the texture mode, whose bit 28 is the +/// level's bank, the size exponents as a load header's word 16 has them, a format-and-layout byte, +/// and the level's address (word 16, with word 17 zero for an ordinary 2D level). Established; the +/// level is looked up by its bank and address. +#[derive(Clone, Copy, Debug)] +pub struct ReadHeader { + pub w: u32, + pub h: u32, + pub format: u32, + pub ty: u32, + pub storage: u32, + pub first: [u32; 3], + pub mode: u32, + pub phys: u32, + pub address: u32, +} + +impl ReadHeader { + pub fn parse(a: &[u32]) -> Option { + let a: &[u32; 18] = a.try_into().ok()?; + Some(ReadHeader { w: a[1], h: a[2], format: a[3], ty: a[4], storage: a[5], first: [a[7], a[8], a[9]], mode: a[13], phys: a[14], address: a[16] }) + } + + /// The level's bank: bit 28 of the texture-mode word. + pub fn bank(&self) -> u8 { + (self.mode >> 28 & 1) as u8 + } +} + +/// The most texels a read-back is answered for. A read asks for what the guest names, and the answer is built +/// as one colour (sixteen bytes) a texel before it is packed, so this bounds that vector. It is a separate cap +/// from the byte one: a one-byte texel makes a 2048 by 1024 read only 2 MiB packed but a 32 MiB colour vector. +pub const MAX_READ_TEXELS: u64 = 1 << 20; +/// The most bytes a read-back may pack, for a read in a bracket (the bracket's buffer holds 8192 by 8192 bytes at +/// most). The packed size includes the pack record's skips, which are the guest's. +pub const MAX_READ_BYTES: u64 = super::pixels::MAX_PACK_BYTES; + +/// A texel in the model's layout from component values 0..=255 of a format: missing +/// alpha is one and missing colour zero (alpha only), luminance is the colour. +fn texel(format: u32, c: [u32; 4]) -> u32 { + let (r, g, b, a) = match format { + 0 => (c[0], 0, 0, 255), + 1 => (0, c[0], 0, 255), + 2 => (0, 0, c[0], 255), + 3 => (0, 0, 0, c[0]), + 4 => (c[0], c[1], c[2], 255), + 5 => (c[0], c[1], c[2], c[3]), + 6 => (c[0], c[0], c[0], 255), + 7 => (c[0], c[0], c[0], c[1]), + _ => (c[3], c[2], c[1], c[0]), + }; + a << 24 | r << 16 | g << 8 | b +} + +/// The red, green, blue and alpha of a packed pixel of type `ty` (8 to 12, as `Header::packed_bytes`), each as 0..=255, from +/// its bytes: big-endian like the rest of the stream unless the unpack state swaps bytes. The first component is +/// in the most significant bits, as in OpenGL's packed types; a field narrower than 8 bits is scaled to 255 (the +/// pipe's unpacker turns each into an unsigned byte, established, but its scaling is not read: OpenGL's, inferred). +fn packed(ty: u32, bytes: &[u8], swap: bool) -> [u32; 4] { + let n = bytes.len(); + let raw = bytes.iter().enumerate().fold(0u32, |a, (i, &x)| a | (x as u32) << if swap { 8 * i } else { 8 * (n - 1 - i) }); + let field = |shift: u32, bits: u32| (raw >> shift & ((1 << bits) - 1)) * 255 / ((1 << bits) - 1); + match ty { + 8 => [field(5, 3), field(2, 3), field(0, 2), 255], + 9 => [field(12, 4), field(8, 4), field(4, 4), field(0, 4)], + 10 => [field(11, 5), field(6, 5), field(1, 5), field(0, 1)], + 11 => [field(24, 8), field(16, 8), field(8, 8), field(0, 8)], + _ => [field(22, 10), field(12, 10), field(2, 10), field(0, 2)], + } +} + +/// A component as 0..=255 from its bytes: the stream is big-endian (the client's order) +/// unless the unpack state swaps bytes. Signed types keep the non-negative half. +fn component(ty: u32, bytes: &[u8], swap: bool) -> u32 { + let n = bytes.len(); + let mut raw = 0u32; + for (i, &x) in bytes.iter().enumerate() { + raw |= (x as u32) << if swap { 8 * i } else { 8 * (n - 1 - i) }; + } + match ty { + 0 => (raw as u8 as i8).max(0) as u32 * 255 / 127, + 1 => raw, + 2 => ((raw as u16 as i16).max(0) as u32 * 255 + 16383) / 32767, + 3 => (raw * 255 + 32767) / 65535, + 4 => ((raw as i32).max(0) as u64 * 255 / 0x7fff_ffff) as u32, + 5 => (raw as u64 * 255 / 0xffff_ffff) as u32, + _ => (f32::from_bits(raw).clamp(0.0, 1.0) * 255.0 + 0.5) as u32, + } +} + +/// Reduce a texel to the width a storage code holds: 0x14 is 5-5-5-1, 0x10 is 4-4-4-4, 0x18 is +/// R5-G6-B5 (no alpha), and anything else keeps its 8 bits. The table gives 0x20 as 12-bit luminance-alpha +/// and 0x40 as 12-bit RGB, which the model's 8 bits a component holds without loss, so they keep 8 (the +/// model's own layout, `store::texture`). The codes the recordings use are 0x30, 0x14, 0x10 and 0x28. +fn reduce(storage: u32, t: u32) -> u32 { + let (r, g, b, a) = ((t >> 16 & 0xff) as u32, (t >> 8 & 0xff) as u32, (t & 0xff) as u32, (t >> 24) as u32); + let expand = |v: u32, bits: u32| { + let max = (1u32 << bits) - 1; + let q = (v * max + 127) / 255; + q * 255 / max + }; + match storage { + 0x14 => expand(a, 1) << 24 | expand(r, 5) << 16 | expand(g, 5) << 8 | expand(b, 5), + 0x10 => expand(a, 4) << 24 | expand(r, 4) << 16 | expand(g, 4) << 8 | expand(b, 4), + 0x18 => 0xff << 24 | expand(r, 5) << 16 | expand(g, 6) << 8 | expand(b, 5), + _ => t, + } +} + +/// The storage code of a read that takes texture memory as one 16-bit component a texel: luminance (the table's +/// 0x0d), which the library reads back with format 6 and type 3. +const STORAGE_ONE_COMPONENT: u32 = 0x0d; + +/// A texel in the model's layout from the 16-bit word a storage code with 16-bit lanes holds, as the pipe samples +/// the word under the format the bind names: 0x14 is 5-5-5-1, 0x10 4-4-4-4 and 0x18 R5-G6-B5, and any other code +/// is one component, taken as luminance. The lane layouts are the table's bus layouts (established); sampling +/// whatever words are there under the bind's format is the table's bind rule. +fn unpack16(storage: u32, w: u32) -> u32 { + let expand = |v: u32, bits: u32| v * 255 / ((1 << bits) - 1); + match storage { + 0x14 => expand(w & 1, 1) << 24 | expand(w >> 11 & 31, 5) << 16 | expand(w >> 6 & 31, 5) << 8 | expand(w >> 1 & 31, 5), + 0x10 => expand(w & 15, 4) << 24 | expand(w >> 12 & 15, 4) << 16 | expand(w >> 8 & 15, 4) << 8 | expand(w >> 4 & 15, 4), + 0x18 => 0xff << 24 | expand(w >> 11 & 31, 5) << 16 | expand(w >> 5 & 63, 6) << 8 | expand(w & 31, 5), + _ => { + let l = (w * 255 + 32767) / 65535; + 0xff << 24 | l << 16 | l << 8 | l + } + } +} + +/// A 16-bit word as the float the packer turns back into the same word as an unsigned short. +fn word_as_float(w: u32) -> f32 { + w as f32 / 65535.0 +} + +/// The 16-bit word a texel loaded under `storage` holds, the inverse of `unpack16` on the texels `reduce` makes: +/// None for a code whose lanes are not 16-bit packed colour (the table gives 0x10 and 0x14 as read back raw; 0x18 is +/// the same lane layout and is inferred to behave the same). +fn pack16(storage: u32, t: u32) -> Option { + let q = |v: u32, bits: u32| (v * ((1 << bits) - 1) + 127) / 255; + let (a, r, g, b) = (t >> 24, t >> 16 & 0xff, t >> 8 & 0xff, t & 0xff); + Some(match storage { + 0x14 => q(r, 5) << 11 | q(g, 5) << 6 | q(b, 5) << 1 | q(a, 1), + 0x10 => q(r, 4) << 12 | q(g, 4) << 8 | q(b, 4) << 4 | q(a, 4), + 0x18 => q(r, 5) << 11 | q(g, 6) << 5 | q(b, 5), + _ => return None, + }) +} + +/// Sample the raw 16-bit words of a level's loaded box under `format`, in place. +fn decode_level(store: &TextureStore, l: &Level, format: u32) { + for z in 0..l.pd { + for y in l.min.1..=l.max.1.min(l.ph - 1) { + for x in l.min.0..=l.max.0.min(l.pw - 1) { + let at = l.off + z * l.pw * l.ph + y * l.pw + x; + store.set(l.plane, at, unpack16(format, store.get(l.plane, at) & 0xffff)); + } + } + } +} + +/// A level in texture memory: the physical rectangle (`pw` by `ph`) it was +/// loaded as and the box of texels loaded into it. It starts at word `off` (its address times +/// `UNIT_WORDS`) of plane `plane`. +#[derive(Clone, Copy, Debug)] +struct Level { + plane: u16, + off: u32, + pw: u32, + ph: u32, + /// The physical depth in slices: one for a 2D level. + pd: u32, + /// The texel size code the level was loaded with. + size_code: u32, + /// The box every load so far covered. + min: (u32, u32), + max: (u32, u32), + /// The most recent load, per axis: where it starts and how many texels it spans. + last: [(u32, u32); 2], + /// Per axis, whether the first load covered the whole rectangle. A level that fills the tile on an axis + /// is sent once, whole; a level smaller than the tile is sent as copies, each narrower than the rectangle. + /// So a level whose first load was whole is never a run of copies on that axis. + whole: [bool; 2], + /// The storage code the level's words are in. A bind never matches on it (the pipe stores none per level and + /// samples the words under the bind's format); it says only how to read the words back as they were loaded. + storage: u32, + /// The words are the raw 16-bit lanes a reload under the one-component code sent, not yet sampled under a + /// bind's format: a bind turns them into texels, and a read-back of the level returns them unchanged. + raw: bool, +} + +impl Level { + /// The level's origin and size in its rectangle, per axis. A level smaller than the tile on an axis was + /// sent as copies and the last copy gives its origin; the bind's logical size says which axes those are + /// (`logical`). Without one (a read-back names no bind) an axis is a small level's when its first load + /// was not the whole rectangle and the last load is narrower than it, ends at its edge and sits on a + /// multiple of its own width, which is where the last of a run of copies lands. That cannot be told from + /// an update of a small level that ends at the edge, since the header does not say; an update of a level + /// that was first loaded whole is told, and left alone. On any other axis the level starts at the + /// origin and is the box loaded to (or the logical size, when the bind gives one). + fn place(&self, logical: [Option; 2]) -> [(u32, u32); 2] { + std::array::from_fn(|a| { + let p = [self.pw, self.ph][a]; + let (at, n) = self.last[a]; + let (lo, hi) = [(self.min.0, self.max.0), (self.min.1, self.max.1)][a]; + match logical[a] { + Some(l) if l < p => (at, l.min(p - at)), + Some(l) => (0, l.min(p)), + None if !self.whole[a] && n > 0 && n < p && at + n == p && at % n == 0 => (at, n), + None => (lo, hi.saturating_sub(lo) + 1), + } + }) + } +} + +/// What the texture path counted, for `kona status`. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TexCounts { + pub loads: u64, + pub dma_loads: u64, + pub finishes: u64, + pub binds: u64, + pub bind_refused: u64, + pub unsupported_load: u64, + /// Loads refused because the level's address and extent lie outside a bank, or because its texels would take + /// texture memory past the product's pool. + pub full: u64, + /// Tokens the table says no software sends, 3D and detail binds, and the like. + pub unmodelled: u64, + /// Level loads that were detail images (also counted in `loads`). + pub detail_loads: u64, + /// Level loads that were slices of a volume (also counted in `loads`), and binds of a volume. + pub volume_loads: u64, + pub volume_binds: u64, + /// Filter-coefficient tables stored (token 0x17e) and detail binds seen (token 0x17d). + pub filter_tables: u64, + pub detail_binds: u64, + /// Mode-1 base binds, by what the detail interaction did: the format row differed from the detail bind's (or + /// there was none), so the magnification was forced to linear; or it matched, and the detail image the bind's + /// address pair names was found (resolved) or not (unresolved). + pub detail_mismatched: u64, + pub detail_resolved: u64, + pub detail_unresolved: u64, + /// Texel read-backs (token 0x052), by route and by how they were answered. + pub reads: u64, + pub reads_bracketed: u64, + /// Answered with zeros: the format or type is not one the packer knows, or the level is not loaded. + pub read_unsupported: u64, + pub read_missing: u64, + /// Too big to answer: over `MAX_READ_TEXELS`, or, bare, more than the return page holds. The + /// completion is posted with no image, or the bracket is refused. + pub read_oversize: u64, + /// With no context or return page to answer on. + pub read_unposted: u64, +} + +/// A map of levels, in key order so the same state is the same bytes. +fn visit_levels(s: &mut S, levels: &mut HashMap<(u8, u32), Level>) { + let mut keys: Vec<(u8, u32)> = levels.keys().copied().collect(); + keys.sort_unstable(); + let n = s.len(keys.len()); + let mut out = HashMap::new(); + for i in 0..n { + if !s.ok() { + break; + } + let (mut bank, mut addr) = keys.get(i).copied().unwrap_or((0, 0)); + let mut l = levels.get(&(bank, addr)).copied().unwrap_or(Level { plane: 0, off: 0, pw: 0, ph: 0, pd: 1, size_code: 0, min: (0, 0), max: (0, 0), last: [(0, 0); 2], whole: [false; 2], storage: 0, raw: false }); + s.u8(&mut bank); + s.u32(&mut addr); + let mut plane = l.plane as u32; + s.u32(&mut plane); + l.plane = plane as u16; + let [mut lx, mut ly] = l.last; + for v in [&mut l.off, &mut l.pw, &mut l.ph, &mut l.pd, &mut l.size_code, &mut l.min.0, &mut l.min.1, &mut l.max.0, &mut l.max.1, &mut lx.0, &mut lx.1, &mut ly.0, &mut ly.1] { + s.u32(v); + } + l.last = [lx, ly]; + s.bool(&mut l.whole[0]); + s.bool(&mut l.whole[1]); + s.u32(&mut l.storage); + s.bool(&mut l.raw); + out.insert((bank, addr), l); + } + if s.loading() { + *levels = out; + } +} + +/// The most filter-coefficient tables a context keeps (the slots are allocated by the client from zero; a stream +/// that sends more is the guest's, and the extra are dropped). +const MAX_FILTER_TABLES: usize = 512; + +/// The detail bind's (token 0x17d) words the pipe keeps: the entry number (or all ones), the s and t size +/// exponents (written to the detail-size register), the detail image's format row, and the address pair. The detail +/// level, add or modulate mode, channel select and coefficient slot are not here: they come from the base bind +/// (established). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DetailBind { + pub entry: u32, + pub exps: [u32; 2], + pub row: u32, + pub pair: [u32; 2], +} + +/// One context's texture state. +#[derive(Default)] +pub struct TexContext { + /// The context slot (the no-context state is `CONTEXT_SLOTS`): which planes of texture memory are the + /// context's own. + slot: usize, + pub unpack: Unpack, + levels: HashMap<(u8, u32), Level>, + /// The detail images (target 0x8095), by the bank and address they were loaded to. A detail image + /// is one level, sent once to each bank, and is taken to have an address space of its own (inferred: the table + /// gives its addresses and not that they are apart from the base's): it must not retire or overwrite a base + /// level at the same address. A detail image's arena space is taken and given back as a base level's is. + detail: HashMap<(u8, u32), Level>, + /// The filter-coefficient tables by slot (token 0x17e): the 33 words the firmware stores, kept as sent. The + /// command carries no type, so a table is decoded only by the bind that names its slot. + filters: Vec<(u32, [u32; 33])>, + /// The last detail bind (token 0x17d). None until one is sent: the firmware's format row starts at -1, which + /// no base bind matches. + detail_bind: Option, + /// A texel draw waiting for its image by DMA. + pub pending: Option<[u32; 20]>, + /// The short bind that waits for its record. + bind: Option<[u32; 5]>, +} + +impl TexContext { + /// The texture state of context slot `slot`. + pub fn new(slot: usize) -> Self { + TexContext { slot, ..Default::default() } + } + + /// The plane of texture memory that holds the base levels, or the detail images, of `bank`. + fn plane_of(&self, detail: bool, bank: u8) -> u16 { + (self.slot.min(CONTEXT_SLOTS) * PLANES_PER_CONTEXT + detail as usize * 2 + (bank & 1) as usize) as u16 + } + + /// Whether the context holds any level. + pub fn holds(&self) -> bool { + !self.levels.is_empty() || !self.detail.is_empty() + } + + /// Give the context's texture memory back and forget the levels, as when its slot is activated + /// afresh. Call only with the tiles drained. + pub fn release(&mut self, store: &TextureStore) { + for p in 0..PLANES_PER_CONTEXT { + store.drop_plane((self.slot.min(CONTEXT_SLOTS) * PLANES_PER_CONTEXT + p) as u16); + } + self.levels.clear(); + self.detail.clear(); + } + + /// The unpack state, the levels loaded and the bind waiting for its record, as the stream + /// module reads and writes them. Levels go in key order so the same state is the same + /// bytes. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { slot: _, unpack: _, levels: _, detail: _, filters: _, detail_bind: _, pending: _, bind: _ } = self; + for v in [&mut self.unpack.row_length, &mut self.unpack.skip_rows, &mut self.unpack.skip_pixels, &mut self.unpack.alignment] { + s.u32(v); + } + s.bool(&mut self.unpack.swap_bytes); + visit_levels(s, &mut self.levels); + visit_levels(s, &mut self.detail); + let n = s.len(self.filters.len()); + self.filters.resize(n.min(MAX_FILTER_TABLES), (0, [0; 33])); + for (slot, words) in self.filters.iter_mut() { + s.u32(slot); + s.u32s(words); + } + let mut have = self.detail_bind.is_some(); + s.bool(&mut have); + let mut d = self.detail_bind.unwrap_or_default(); + let DetailBind { entry, exps: [sx, tx], row, pair: [p0, p1] } = &mut d; + for v in [entry, sx, tx, row, p0, p1] { + s.u32(v); + } + if s.loading() { + self.detail_bind = have.then_some(d); + } + let mut p = self.pending.is_some(); + s.bool(&mut p); + let mut a = self.pending.unwrap_or([0; 20]); + s.u32s(&mut a); + if s.loading() { + self.pending = p.then_some(a); + } + let mut b = self.bind.is_some(); + s.bool(&mut b); + let mut w = self.bind.unwrap_or([0; 5]); + s.u32s(&mut w); + if s.loading() { + self.bind = b.then_some(w); + } + } + + /// Decode a texel draw's image (`bytes`, the stream as bytes after the first + /// `offset` of its first word are skipped) into texture memory. False when the + /// format or type is not decoded or the level's address lies outside the bank. + pub fn load(&mut self, store: &TextureStore, a: &[u32], bytes: &[u8], dma: bool, counts: &mut TexCounts) -> bool { + let Some(h) = Header::parse(a) else { return false }; + let Some((p, stride, start)) = h.geometry(&self.unpack) else { + counts.unsupported_load += 1; + return false; + }; + let (pw, ph, pd) = (1u32 << (h.phys & 0xf).min(12), 1u32 << (h.phys >> 4 & 0xf).min(12), h.depth()); + let slice = if h.is_volume() { h.slice } else { 0 }; + if h.x1 < h.x0 || h.y1 < h.y0 || h.x1 >= pw || h.y1 >= ph || h.w == 0 || h.h == 0 || slice >= pd { + counts.unsupported_load += 1; + return false; + } + let key = (h.bank(), h.address); + let plane = self.plane_of(h.is_detail(), h.bank()); + // The load's own texels are the memory it asks for; one that would take texture memory past the product's + // pool is refused before anything is retired or written (the model's own limit, as the bank's edge is). + let (rw, rh) = ((h.x1 - h.x0 + 1).min(h.w), (h.y1 - h.y0 + 1).min(h.h)); + let first = h.address as u64 * UNIT_WORDS as u64 + (slice * pw * ph) as u64 + h.y0 as u64 * pw as u64 + h.x0 as u64; + if !store.fits(plane, first, rw as u64, pw as u64, rh as u64) { + counts.full += 1; + return false; + } + // A detail image is a level of its own kind: one level, sent to each bank in turn, at addresses that + // are not the base levels'. + let levels = if h.is_detail() { &mut self.detail } else { &mut self.levels }; + let mut lv = match levels.get(&key).filter(|l| l.pw == pw && l.ph == ph && l.pd == pd && l.size_code == h.size_code()) { + Some(l) => *l, + None => { + // The level takes the units from its address on: whatever else of the bank sat there is gone, + // and so is a level of another shape at this very address. A level is refused only when + // its units reach past the bank, and then nothing is retired. + let (lo, hi) = (h.address as u64, h.address as u64 + units(pw, ph * pd, h.size_code())); + if !store.contains(h.address, hi - lo) { + counts.full += 1; + return false; + } + levels.retain(|&(bank, at), l| !(bank == h.bank() && (at == h.address || (at as u64) < hi && at as u64 + units(l.pw, l.ph * l.pd, l.size_code) > lo))); + // The words are the address's, zeroed: the model's layout puts the level's texels from the + // start of its first unit, and a level whose units overlap no other's shares no word with it. + let off = h.address * UNIT_WORDS; + store.clear(plane, off, pw as usize * ph as usize * pd as usize); + Level { plane, off, pw, ph, pd, size_code: h.size_code(), min: (u32::MAX, u32::MAX), max: (0, 0), last: [(0, 0); 2], whole: [false; 2], storage: h.storage, raw: false } + } + }; + // A reload of 16-bit words under the one-component code keeps them as they are, to be sampled under whatever + // format the next bind names. A load that covers everything the level holds replaces it, raw or not; a smaller + // one cannot mix the two kinds, so it is decoded as one-component texels and a raw level is decoded first. + let is_raw = h.format == pixel::FORMAT_LUMINANCE && h.ty == pixel::TYPE_UNSIGNED_SHORT && h.storage == STORAGE_ONE_COMPONENT; + let covers = h.x0 <= lv.min.0 && h.y0 <= lv.min.1 && h.x0 + rw - 1 >= lv.max.0 && h.y0 + rh - 1 >= lv.max.1; + if !covers && lv.raw && !is_raw { + decode_level(store, &lv, STORAGE_ONE_COMPONENT); + } + let raw = if covers { is_raw } else { lv.raw && is_raw }; + lv.raw = raw; + lv.storage = h.storage; + let mut words_before = 0u64; + for row in 0..rh as u64 { + // Where row `row` starts in the bytes: a DMA image is its rows, `w * p` bytes + // each; an inline one is one byte stream (row mode 0) or rows that each start on + // a word and take the words spanning their bytes (row mode 1). + let address = start + row * stride; + let at = if dma { + (row * h.w as u64 * p) as usize + } else if h.strided { + let base = (words_before * 4 + address % 4) as usize; + words_before += (address % 4 + h.w as u64 * p).div_ceil(4); + base + } else { + (start % 4 + row * stride) as usize + }; + for col in 0..rw as u64 { + let from = at + (col * p) as usize; + let Some(px) = bytes.get(from..from + p as usize) else { continue }; + let n = p as usize / h.components().unwrap_or(1) as usize; + let c: [u32; 4] = if (8..=12).contains(&h.ty) { packed(h.ty, px, self.unpack.swap_bytes) } else { std::array::from_fn(|k| px.get(k * n..(k + 1) * n).map_or(0, |b| component(h.ty, b, self.unpack.swap_bytes))) }; + let t = if raw { + let w = [px[0], px[1]]; + (if self.unpack.swap_bytes { u16::from_le_bytes(w) } else { u16::from_be_bytes(w) }) as u32 + } else { + reduce(h.storage, texel(h.format, c)) + }; + store.set(lv.plane, lv.off + slice * pw * ph + (h.y0 + row as u32) * pw + h.x0 + col as u32, t); + } + } + // The box grows to take the load; the load is also kept as the last, since the last copy of a small level + // gives its origin (`Level::place`) and a partial update must not shrink the level. + let first = lv.min.0 == u32::MAX; + let whole = if first { [h.x0 == 0 && rw == pw, h.y0 == 0 && rh == ph] } else { lv.whole }; + let lv = Level { min: (lv.min.0.min(h.x0), lv.min.1.min(h.y0)), max: (lv.max.0.max(h.x0 + rw - 1), lv.max.1.max(h.y0 + rh - 1)), last: [(h.x0, rw), (h.y0, rh)], whole, ..lv }; + levels.insert(key, lv); + counts.loads += 1; + counts.detail_loads += h.is_detail() as u64; + counts.volume_loads += h.is_volume() as u64; + true + } + + /// The texels of a level the read-back names, `h.w` by `h.h` from its first texel, as colour: the + /// level in the header's bank (bit 28 of its mode word) at its address (word 16) whose rectangle matches its + /// exponents. The empty slot of a level in the other bank is address zero, which can name a real level, + /// so no other (bank, address) pair is tried. None when no such level is loaded. A texel past the level's own + /// size reads as zero. The caller bounds `w * h`. + pub fn read_level(&self, store: &TextureStore, h: &ReadHeader) -> Option> { + let (pw, ph) = (1u32 << (h.phys & 0xf).min(12), 1u32 << (h.phys >> 4 & 0xf).min(12)); + let l = self.levels.get(&(h.bank(), h.address)).filter(|l| l.pw == pw && l.ph == ph)?; + let [(ox, lw), (oy, lh)] = l.place([None, None]); + // A read of a volume's level names the slice by the first texel's third coordinate; a 2D level has one. + let z = if l.pd > 1 { h.first[2] } else { 0 }; + if z >= l.pd { + return Some(vec![[0.0; 4]; h.w as usize * h.h as usize]); + } + // A read of luminance in unsigned shorts under the one-component code takes texture memory as 16-bit + // words, whatever loaded them (the table: the storage code describes the source texels), and returns the + // words themselves: a level held as raw words as they are, and one loaded with 16-bit lanes (0x10, 0x14) + // as the packed word it would hold. The packer returns a word carried as `word_as_float` exactly. + let words = h.format == pixel::FORMAT_LUMINANCE && h.ty == pixel::TYPE_UNSIGNED_SHORT && h.storage == STORAGE_ONE_COMPONENT; + Some( + (0..h.h) + .flat_map(|j| (0..h.w).map(move |i| (i, j))) + .map(|(i, j)| { + let (x, y) = (h.first[0].saturating_add(i), h.first[1].saturating_add(j)); + if x >= lw || y >= lh { + return [0.0; 4]; + } + let t = store.get(l.plane, l.off + z * l.pw * l.ph + (oy + y) * l.pw + ox + x); + if words { + if let Some(w) = if l.raw { Some(t & 0xffff) } else { pack16(l.storage, t) } { + return [word_as_float(w), 0.0, 0.0, 1.0]; + } + } + let t = if l.raw { unpack16(STORAGE_ONE_COMPONENT, t & 0xffff) } else { t }; + [t >> 16 & 0xff, t >> 8 & 0xff, t & 0xff, t >> 24].map(|c| c as f32 / 255.0) + }) + .collect(), + ) + } + + /// A filter-coefficient table (token 0x17e): the slot, then 33 words, stored as sent and replaced by a later + /// table for the slot (established: the firmware stores the 33-word record at the slot and sends nothing to + /// the hardware until a bind needs it). False for any other length or past the cap. + pub fn filter_table(&mut self, slot: u32, words: &[u32]) -> bool { + let Ok(w) = <[u32; 33]>::try_from(words) else { return false }; + if let Some(i) = self.filters.iter().position(|(s, _)| *s == slot) { + self.filters[i].1 = w; + } else if self.filters.len() < MAX_FILTER_TABLES { + self.filters.push((slot, w)); + } else { + return false; + } + true + } + + /// A detail bind (token 0x17d, six words): the pipe stores the format row and the address pair, and the + /// exponents reach the detail-size register (established). + pub fn bind_detail(&mut self, a: &[u32]) -> bool { + let Ok(a) = <[u32; 6]>::try_from(a) else { return false }; + self.detail_bind = Some(DetailBind { entry: a[0], exps: [a[1], a[2]], row: a[3], pair: [a[4], a[5]] }); + true + } + + /// The detail image a mode-1 base bind (mode word `mode`) would combine with, if any: the pipe reads the + /// address pair word the mode word's bit 28 chooses (word 4 when set, word 5 when clear, established). The + /// client loads the detail image once to each bank, so it is looked for at that address in either; its size must + /// be the detail bind's exponents. + fn detail_image(&self, mode: u32) -> Option<&Level> { + let d = self.detail_bind?; + let addr = d.pair[if mode >> 28 & 1 != 0 { 0 } else { 1 }]; + [0u8, 1].into_iter().find_map(|bank| self.detail.get(&(bank, addr))).filter(|l| l.min.0 != u32::MAX && l.max.0 + 1 >= 1u32 << d.exps[0].min(12) && l.max.1 + 1 >= 1u32 << d.exps[1].min(12)) + } + + /// The magnification combine a mode-1 base bind asks for, from its mode word's code (3 detail add, 4 detail + /// modulate, 5 sharpen; the detail codes only with the detail image `found`, and a sharpen with a coarser level + /// to push away from). The curve is the filter table the control word's slot holds (bits 11:0, inferred), + /// read as the spline form; the stored curve is the public one doubled for detail and negated for sharpen, which + /// is undone here (established for the two transforms; that detail uses the doubled form and sharpen the negated + /// one is inferred from the bind, since the table command carries no type). A slot that holds no table, or one + /// that is not a spline, stands the client's default curve in (approximated). + fn combine(&self, ctl: u32, mode: u32, found: Option, levels: u8) -> Combine { + let (code, channels) = (mode >> 5 & 7, (mode >> 26 & 3) as u8); + let curve = |scale: f32| -> [f32; CURVE_NODES] { + match self.filters.iter().find(|(s, _)| *s == ctl & 0xfff).and_then(|(_, w)| spline_samples(w)) { + Some(v) => v.map(|x| x * scale), + None => default_weights(), + } + }; + match (code, found, self.detail_bind) { + (3 | 4, Some(l), Some(b)) => { + let [(ox, w), (oy, h)] = l.place([Some(1 << b.exps[0].min(12)), Some(1 << b.exps[1].min(12))]); + let image = TexLevel { plane: l.plane, off: l.off, pitch: l.pw as u16, ox: ox as u16, oy: oy as u16, w: w as u16, h: h as u16, ..Default::default() }; + Combine::Detail(Detail { modulate: code == 4, scale: (mode >> 21 & 7) as u8, channels, image, weights: curve(0.5) }) + } + (5, ..) if levels > 1 => Combine::Sharpen(Sharpen { channels, weights: curve(-1.0) }), + _ => Combine::None, + } + } + + /// The short bind's five words, kept until its record arrives. + pub fn short_bind(&mut self, words: &[u32]) { + self.bind = <[u32; 5]>::try_from(words).ok(); + } + + /// The 2D record (22 words) completes the bind: the levels its table names, found by + /// the first level's bank (bit 28 of the short bind's mode word) flipped for every + /// second level. None when no short bind came before it, or level 0 is not found. + pub fn record(&mut self, store: &TextureStore, r: &[u32], env: (EnvMode, [u32; 4]), counts: &mut TexCounts) -> Option { + let b = self.bind.take()?; + let r: &[u32; 22] = r.try_into().ok()?; + let (ctl, mut mode, size) = (b[1], b[2], b[3]); + let first_bank = (mode >> 28 & 1) as u8; + // The detail interaction of a mode-1 base bind of a 2D texture (target index 2): its format row (bits 23:12 of + // the control word) is compared with the last detail bind's. A mismatch, or no detail bind, forces the + // magnification code to 1, plain linear, whatever code the bind carried (nearest, linear, detail or + // sharpen); a match sends the filter table and the detail address (established). The client sends mode 1 + // only for the detail filters, so in practice only the detail codes are rewritten. + let mut found = None; + if ctl >> 24 == 0x21 { + if self.detail_bind.is_some_and(|d| d.row == ctl >> 12 & 0xfff) { + if matches!(mode >> 5 & 7, 3 | 4) { + found = self.detail_image(mode).copied(); + counts.detail_resolved += found.is_some() as u64; + counts.detail_unresolved += found.is_none() as u64; + } + } else { + mode = mode & !(7 << 5) | 1 << 5; + counts.detail_mismatched += 1; + } + } + let mut d = TexDesc { env: env.0, env_colour: env.1, class: format_class(ctl >> 12 & 0xfff), ..Default::default() }; + for (k, &addr) in r[6..].iter().take_while(|&&w| w != 0xffff_ffff).take(MAX_LEVELS).enumerate() { + let Some(l) = self.levels.get_mut(&(first_bank ^ (k as u8 & 1), addr)) else { break }; + // The bind's format samples whatever words the level holds, whatever loaded them. + if l.raw { + decode_level(store, l, mode >> 14 & 0x7f); + l.raw = false; + l.storage = mode >> 14 & 0x7f; + } + let l = *l; + // The logical size is the bind's level-0 size halved per level; where it is smaller than the + // rectangle the last copy gives the origin. + let logical = |a: u32| Some(((1u32 << (size >> (4 * a) & 0xf).min(12)) >> k.min(31)).max(1)); + let [(ox, w), (oy, h)] = l.place([logical(0), logical(1)]); + d.levels[k] = TexLevel { plane: l.plane, off: l.off, pitch: l.pw as u16, ox: ox as u16, oy: oy as u16, w: w as u16, h: h as u16, ..Default::default() }; + d.count += 1; + } + if d.count == 0 { + return None; + } + apply_mode(&mut d, mode, size); + d.lod_scale = [(r[1] & 0xff) as u8, (r[1] >> 8 & 0xff) as u8, 0]; + if ctl >> 24 == 0x21 { + d.combine = self.combine(ctl, mode, found, d.count); + } + Some(d) + } + /// A volume record (token 0x17b with `mip`, 14 words, or 0x17c, 4 words) completes the bind a short bind + /// began. The 3D mip-mapped form has the four state words of the 2D record (the layout pair, the LOD scales with + /// r in bits 23:16, the bounds, the physical sizes) and ten descriptor words, level k at word k; the single-level + /// form has the layout pair, the sizes and the one descriptor pair (framing established). A level's descriptor + /// word is its address, found in the bank its parity names and then in the other (inferred), under + /// the first level's bank for the single-level form. The logical depth is the size word's r exponent + /// (bits 11:8) halved per level, as the other axes are (established). None when no short bind came + /// before it or level 0 is not found. + pub fn volume_record(&mut self, store: &TextureStore, r: &[u32], mip: bool, env: (EnvMode, [u32; 4])) -> Option { + let b = self.bind.take()?; + let (ctl, mode, size) = (b[1], b[2], b[3]); + let table: &[u32] = if mip { + let r: &[u32; 14] = r.try_into().ok()?; + &r[4..] + } else { + let r: &[u32; 4] = r.try_into().ok()?; + &r[2..] + }; + let first_bank = (mode >> 28 & 1) as u8; + let mut d = TexDesc { env: env.0, env_colour: env.1, class: format_class(ctl >> 12 & 0xfff), volume: true, ..Default::default() }; + for k in 0..(if mip { table.len().min(MAX_LEVELS) } else { 1 }) { + // A mip-mapped volume's pair k holds levels 2k and 2k + 1, the first in its even word. + let (primary, addr) = if mip { ((k & 1) as u8, table[k]) } else { (first_bank, 0) }; + let key = [primary, primary ^ 1].into_iter().map(|bank| (bank, if mip { addr } else { table[bank as usize] })).find(|key| self.levels.contains_key(key)); + let Some(key) = key else { break }; + let l = self.levels.get_mut(&key).expect("just found"); + if l.raw { + decode_level(store, l, mode >> 14 & 0x7f); + l.raw = false; + l.storage = mode >> 14 & 0x7f; + } + let l = *l; + let logical = |a: u32| ((1u32 << (size >> (4 * a) & 0xf).min(12)) >> k.min(31)).max(1); + let [(ox, w), (oy, h)] = l.place([Some(logical(0)), Some(logical(1))]); + d.levels[k] = TexLevel { plane: l.plane, off: l.off, pitch: l.pw as u16, ox: ox as u16, oy: oy as u16, w: w as u16, h: h as u16, d: logical(2).min(l.pd) as u16, slice: l.pw * l.ph }; + d.count += 1; + } + if d.count == 0 { + return None; + } + apply_mode(&mut d, mode, size); + // The single-level form carries no LOD scales: a scale of one (16) stands in, which only decides where a + // fragment is magnified or minified (approximated). + d.lod_scale = if mip { [(r[1] & 0xff) as u8, (r[1] >> 8 & 0xff) as u8, (r[1] >> 16 & 0xff) as u8] } else { [16; 3] }; + Some(d) + } +} + +/// The filters and wraps the short bind's mode and size words give a bound texture (shared by the 2D and +/// volume binds). The wrap bits are the table's bits 12, 13 and 14 of the size word (established; one bit +/// cannot tell the non-repeat modes apart, so every one clamps). +fn apply_mode(d: &mut TexDesc, mode: u32, size: u32) { + d.min = match mode >> 2 & 7 { + 0 => Filter::Nearest, + 1 | 2 => Filter::Linear, + n @ 3..=6 => Filter::Mip { within_linear: n & 1 == 0, between_linear: n >= 5 }, + _ => Filter::Linear, + }; + // Magnification codes 3 and 4 add or modulate a detail image, and 5 sharpens; a mode-1 base bind builds the + // combine on top of this linear magnification (`TexContext::combine`). With no detail image bound, or one whose + // format differs from the base's, the pipe magnifies the base linearly and shows it (the pipe has no + // flat-colour state), which is what every nonzero code draws without a combine. + d.mag = if mode >> 5 & 7 == 0 { Filter::Nearest } else { Filter::Linear }; + let wrap = |bit: u32| if size >> bit & 1 != 0 { Wrap::Clamp } else { Wrap::Repeat }; + d.wrap = [wrap(12), wrap(13), wrap(14)]; +} + +/// The format class of an internal-format index (section 7.12): 1 alpha, 2 luminance +/// or RGB, 3 luminance-alpha or RGBA, 4 intensity. Rows the table does not read count as +/// RGBA. +fn format_class(index: u32) -> u8 { + match index { + 7 | 11 | 17 | 18 | 23 => 1, + 0 | 3 | 6 | 10 | 15 | 16 | 25 | 27 | 28 | 30 | 33 | 37 | 38 | 63 => 2, + 1 | 9 | 13 | 14 | 21 | 2 | 26 | 29 | 31 | 34 | 41 => 3, + _ => 3, + } +} + +/// Whether `selector` is a texture token the texture path takes. +pub fn is_texture(selector: u32) -> bool { + matches!( + selector, + DRAW_TEXELS_INTO_TEXTURE_MEMORY + | TEXTURE_LOAD_BY_IDENTIFIER + | BIND_TEXTURE_CURRENT_FORM + | BIND_LINEAR_TEXTURE + | BIND_2D_MIP_MAPPED_TEXTURE + | BIND_3D_MIP_MAPPED_TEXTURE + | BIND_3D_TEXTURE + | BIND_DETAIL_TEXTURE + | BIND_TEXTURE_BY_IDENTIFIER + | BIND_ARM_TEXTURE_CACHE_ENTRY + | FREE_ARM_TEXTURE_CACHE_ENTRY + | SET_TEXEL_STORE_PARAMETER + | SET_PIXEL_STORE_PARAMETER_INTEGER_FORM + | TEXTURE_LOAD_SET_UP + | BIND_TEXTURE_BY_OLD_STYLE_BINDING + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::back::store::texture::PAGE_WORDS; + + /// A texel draw's header: a w x h RGBA image of unsigned bytes, one level at `address`. + fn header(w: u32, h: u32) -> [u32; 20] { + let mut a = [0u32; 20]; + a[0] = 0x0de1; + a[1] = w; + a[2] = h; + a[3] = 5; + a[4] = 1; + a[9] = 0x30; + a[13] = w.wrapping_sub(1); + a[14] = h.wrapping_sub(1); + a[16] = 0x11; + a[17] = 0x2200; + a[18] = 0x1000; + a + } + + fn parse(a: &[u32; 20]) -> Header { + Header::parse(a).unwrap() + } + + /// A guest's unpack state can make the sizing products overflow 64 bits (a skip of 2^32 rows of a stride of 2^36 + /// bytes): that is no size, not a panic or a wrapped one. + #[test] + fn an_unpack_state_whose_sizes_overflow_has_no_image_size() { + let big = Unpack { row_length: u32::MAX, skip_rows: u32::MAX, skip_pixels: u32::MAX, alignment: 8, swap_bytes: false }; + let mut h = parse(&header(4, u32::MAX)); + assert_eq!(h.image_words(&big), None); + h.strided = true; + assert_eq!(h.image_words(&big), None); + // A tall strided image past what a stream carries stops adding. + assert_eq!(h.image_words(&Unpack::default()), None); + } + + /// An unpack row length below the width is no size, not an underflow. + #[test] + fn a_row_length_below_the_width_has_no_image_size() { + let h = parse(&header(100, 4)); + assert_eq!(h.image_words(&Unpack { row_length: 10, ..Unpack::default() }), None); + assert_eq!(h.image_words(&Unpack { row_length: 100, ..Unpack::default() }), Some(400)); + } + + /// The image's length in words, by the sizing rule: contiguous rows are one byte + /// stream, strided rows each take the words spanning their bytes. + #[test] + fn the_image_length_follows_the_unpack_state() { + let u = Unpack::default(); + assert_eq!(parse(&header(4, 2)).image_words(&u), Some(8)); + let mut a = header(4, 2); + a[5] = 2; + assert_eq!(parse(&a).image_words(&u), Some(9), "a start two bytes into the first word spans one more"); + a[7] = 1; + assert_eq!(parse(&a).image_words(&u), Some(10), "strided: each row's own span, five words"); + // RGB bytes, three wide, rows padded to the alignment of 4: stride 12, 21 bytes. + let mut rgb = header(3, 2); + rgb[3] = 4; + assert_eq!(parse(&rgb).image_words(&u), Some(6)); + assert_eq!(parse(&rgb).image_words(&Unpack { alignment: 1, ..u }), Some(5), "no padding: 18 bytes"); + assert_eq!(parse(&rgb).image_words(&Unpack { row_length: 8, ..u }), Some(9), "a row length of 8 pixels: stride 24, 33 bytes"); + assert_eq!(parse(&header(0, 0)).image_words(&u), Some(0)); + let mut cmyk = header(2, 2); + cmyk[3] = 9; + assert_eq!(parse(&cmyk).image_words(&u), None, "a format the model does not decode"); + let mut packed = header(2, 2); + packed[4] = 8; + assert_eq!(parse(&packed).image_words(&u), None, "nor a packed type"); + } + + fn words_to_bytes(w: &[u32]) -> Vec { + w.iter().flat_map(|w| w.to_be_bytes()).collect() + } + + /// Word `i` of the level at (`bank`, `address`) in `c`'s texture memory: a level is where its address puts it. + fn word(c: &TexContext, store: &TextureStore, bank: u8, address: u32, i: u32) -> u32 { + store.get(c.plane_of(false, bank), address * UNIT_WORDS + i) + } + + /// A load puts the texels where the header says, in the model's layout, and a load + /// into the same level again replaces it. + #[test] + fn a_load_stores_texels_in_the_models_layout() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let img = [0xff00_00ffu32, 0x00ff_00ff, 0x0000_ffff, 0xffff_ffff]; + assert!(c.load(&store, &header(2, 2), &words_to_bytes(&img), false, &mut n)); + assert_eq!([0, 1, 2, 3].map(|i| word(&c, &store, 0, 0x1000, i)), [0xffff_0000, 0xff00_ff00, 0xff00_00ff, 0xffff_ffff]); + let again = [0x0101_01ffu32; 4]; + assert!(c.load(&store, &header(2, 2), &words_to_bytes(&again), false, &mut n)); + assert_eq!((store.resident_pages(), word(&c, &store, 0, 0x1000, 0)), (1, 0xff01_0101), "same address and size: replaced in place"); + assert_eq!(n.loads, 2); + // RGB: alpha is one. Luminance: the colour, alpha one. Luminance-alpha: both. + let mut rgb = header(1, 1); + rgb[3] = 4; + rgb[18] = 0x2000; + c.load(&store, &rgb, &words_to_bytes(&[0x1020_3000]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x2000, 0), 0xff10_2030, "a level at another address is another place"); + let mut lum = header(1, 1); + lum[3] = 6; + lum[18] = 0x3000; + c.load(&store, &lum, &words_to_bytes(&[0x8000_0000]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x3000, 0), 0xff80_8080); + let mut la = header(1, 1); + la[3] = 7; + la[18] = 0x4000; + c.load(&store, &la, &words_to_bytes(&[0x4020_0000]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x4000, 0), 0x2040_4040); + // Alpha only: no colour. + let mut al = header(1, 1); + al[3] = 3; + al[18] = 0x5000; + c.load(&store, &al, &words_to_bytes(&[0x7700_0000]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x5000, 0), 0x7700_0000); + // 16-bit unsigned shorts keep their top byte; a float clamps and scales. + let mut sh = header(1, 1); + sh[3] = 6; + sh[4] = 3; + sh[18] = 0x6000; + c.load(&store, &sh, &words_to_bytes(&[0x8000_0000]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x6000, 0), 0xff80_8080); + let mut fl = header(1, 1); + fl[3] = 6; + fl[4] = 6; + fl[18] = 0x7000; + c.load(&store, &fl, &words_to_bytes(&[1.5f32.to_bits()]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x7000, 0), 0xffff_ffff); + } + + /// The packed pixel types (codes 8 to 12) load: the library sends 4-4-4-4 textures that way, and a load the + /// model refused left every bind of its level refused too. The first component is in the high bits, the stream + /// is big-endian, and the library pairs 3-3-2 with RGB and the others with RGBA. + #[test] + fn a_packed_pixel_type_loads_as_texels() { + let u = Unpack::default(); + let load = |ty: u32, format: u32, storage: u32, w: u32, bytes: &[u8]| -> Option> { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let mut a = header(w, 1); + (a[3], a[4], a[9], a[16]) = (format, ty, storage, 0x33); + let words = parse(&a).image_words(&u)?; + assert_eq!(words as usize, bytes.len().div_ceil(4), "the stream is as long as the packed pixels"); + c.load(&store, &a, bytes, false, &mut n).then(|| (0..w).map(|i| word(&c, &store, 0, 0x1000, i)).collect()) + }; + assert_eq!(load(9, 5, 0x10, 2, &[0xf0, 0x5a, 0x12, 0x34]), Some(vec![0xaaff_0055, 0x4411_2233]), "4-4-4-4: red, green, blue, alpha from the top"); + assert_eq!(load(10, 5, 0x14, 2, &[0xf8, 0x01, 0x07, 0xc0]), Some(vec![0xffff_0000, 0x0000_ff00]), "5-5-5-1"); + assert_eq!(load(8, 4, 0x30, 4, &[0xe0, 0x1f, 0x03, 0x00]), Some(vec![0xffff_0000, 0xff00_ffff, 0xff00_00ff, 0xff00_0000]), "3-3-2, RGB"); + assert_eq!(load(11, 5, 0x30, 1, &[0x11, 0x22, 0x33, 0x44]), Some(vec![0x4411_2233]), "8-8-8-8"); + assert_eq!(load(12, 5, 0x30, 1, &[0xff, 0xc0, 0x0f, 0xff]), Some(vec![0xffff_00ff]), "10-10-10-2"); + // A type with the wrong format is the library's error, not a load. + assert_eq!(load(9, 4, 0x10, 1, &[0, 0]), None); + assert_eq!(load(8, 5, 0x30, 1, &[0]), None); + // Swapped bytes read the packed word little-endian. + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext { unpack: Unpack { swap_bytes: true, ..u }, ..Default::default() }, TexCounts::default()); + let mut a = header(2, 1); + (a[3], a[4], a[9], a[16]) = (5, 9, 0x10, 0x33); + assert!(c.load(&store, &a, &[0x5a, 0xf0, 0x34, 0x12], false, &mut n)); + assert_eq!((word(&c, &store, 0, 0x1000, 0), word(&c, &store, 0, 0x1000, 1)), (0xaaff_0055, 0x4411_2233)); + } + + /// A storage code narrower than a texel reduces it as the board's texel would be. + #[test] + fn a_narrow_storage_code_reduces_the_texel() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let mut a = header(1, 1); + a[9] = 0x10; + c.load(&store, &a, &words_to_bytes(&[0x8040_20ff]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x1000, 0), 0xffff_0000 & 0xff00_0000 | 0x0088_4422, "4 bits a component: 0x80 is 8 of 15, 0x88"); + let mut a = header(1, 1); + a[9] = 0x14; + a[18] = 0x2000; + c.load(&store, &a, &words_to_bytes(&[0xff00_0000]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x2000, 0), 0x00ff_0000, "5-5-5-1: a clear alpha bit, full red"); + let mut a = header(1, 1); + a[9] = 0x18; + a[18] = 0x3000; + c.load(&store, &a, &words_to_bytes(&[0x8040_20ff]), false, &mut n); + assert_eq!(word(&c, &store, 0, 0x3000, 0), 0xff83_4020, "R5-G6-B5: red 0x80 is 16 of 31 (0x83), green 0x40 is 16 of 63 (0x40), blue 0x20 is 4 of 31 (0x20), alpha one"); + } + + /// A DMA image is its rows, `w * p` bytes each, with none of the stream's offset or padding. + #[test] + fn a_dma_image_is_its_rows() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let mut a = header(2, 1); + a[6] = 1; + c.load(&store, &a, &[1, 2, 3, 4, 5, 6, 7, 8], true, &mut n); + assert_eq!((word(&c, &store, 0, 0x1000, 0), word(&c, &store, 0, 0x1000, 1)), (0x0401_0203, 0x0805_0607)); + // A load whose box leaves its physical rectangle is refused, and memory is not touched. + let mut bad = header(2, 2); + bad[13] = 9; + assert!(!c.load(&store, &bad, &[0; 16], false, &mut n)); + assert_eq!(n.unsupported_load, 1); + } + + /// A short bind and its record find the levels by (bank, address): the first level's bank + /// from bit 28 of the mode word, flipped for every second level; filters, wraps and the + /// LOD scale come from the words; no short bind before it, or no level found, binds nothing. + #[test] + fn a_bind_finds_its_levels_by_bank_and_address() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + // Level 0, 2 x 2, in bank 1 at 0x1000; level 1, 1 x 1, in bank 0 at 0x2000. + let mut l0 = header(2, 2); + l0[17] |= 8; + l0[19] = 0x1000; + l0[18] = 0; + c.load(&store, &l0, &words_to_bytes(&[0xff00_00ff; 4]), false, &mut n); + let mut l1 = header(1, 1); + l1[18] = 0x2000; + l1[16] = 0x00; + c.load(&store, &l1, &words_to_bytes(&[0x0000_ffff]), false, &mut n); + let mut rec = [0u32; 22]; + rec[0] = 0x44; + rec[1] = 0x2020; + rec[6] = 0x1000; + rec[7] = 0x2000; + rec[8] = 0xffff_ffff; + assert!(c.record(&store, &rec, Default::default(), &mut TexCounts::default()).is_none(), "a record with no short bind"); + // Mode: minification 6 (mip, linear within, linear between), magnification 1, first bank 1. + let bind = [0xffff_ffffu32, 2 << 12, 6 << 2 | 1 << 5 | 1 << 28, 0x3011, 1]; + c.short_bind(&bind); + let d = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).expect("both levels found"); + assert_eq!((d.count, d.levels[0].w, d.levels[1].w, d.class), (2, 2, 1, 3)); + assert_eq!(d.min, Filter::Mip { within_linear: true, between_linear: true }); + assert_eq!((d.mag, d.wrap, d.lod_scale), (Filter::Linear, [Wrap::Clamp, Wrap::Clamp, Wrap::Repeat], [0x20, 0x20, 0])); + // The wrong first bank finds nothing. + c.short_bind(&[0xffff_ffff, 2 << 12, 6 << 2, 0x11, 1]); + assert!(c.record(&store, &rec, Default::default(), &mut TexCounts::default()).is_none()); + // Only level 0 loaded: a table of two finds one. + c.short_bind(&bind); + rec[7] = 0x9999; + assert_eq!(c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap().count, 1); + } + + /// The bind's size word gives the logical size, so an update of part of a level changes nothing, and a small + /// level's origin is its last copy's. + #[test] + fn a_bind_places_levels_by_its_logical_size() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0x100)); + load_copy(&mut c, &store, ©(4, 2, 2, 1, 0x100)); + load_copy(&mut c, &store, ©(1, 1, 7, 3, 0x200)); + let mut rec = [0u32; 22]; + rec[6..8].copy_from_slice(&[0x100, 0xffff_ffff]); + c.short_bind(&[0xffff_ffff, 2 << 12, 0, 0x23, 1]); + let d = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap(); + assert_eq!([d.levels[0].ox, d.levels[0].oy, d.levels[0].w, d.levels[0].h], [0, 0, 8, 4]); + rec[6] = 0x200; + c.short_bind(&[0xffff_ffff, 2 << 12, 0, 0x00, 1]); + let d = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap(); + assert_eq!([d.levels[0].ox, d.levels[0].oy, d.levels[0].w, d.levels[0].h], [7, 3, 1, 1]); + } + + /// A slice of a 2 x 2 volume (physical 2 x 2 x 2) at `address` in `bank`, filled with `texel`. + fn slice(c: &mut TexContext, store: &TextureStore, n: &mut TexCounts, bank: u32, address: u32, z: u32, texel: u32) -> bool { + let mut a = header(2, 2); + a[0] = 0x806f; + a[15] = z; + a[16] = 0x111; + a[17] |= bank << 3; + a[18 + bank as usize] = address; + c.load(store, &a, &words_to_bytes(&[texel; 4]), false, n) + } + + /// A volume's slices stack in one level found by the slices' shared address; a slice past the level's physical + /// depth is refused. The single-level record (token 0x17c) finds that level in the first level's bank and the + /// sampler reads each slice at its own r. + #[test] + fn a_volume_is_its_slices_and_binds_by_its_pair() { + use super::super::super::raster::texel::sample3; + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + assert!(slice(&mut c, &store, &mut n, 0, 0x40, 0, 0xff00_00ff)); + assert!(slice(&mut c, &store, &mut n, 0, 0x40, 1, 0x0000_ffff)); + assert!(!slice(&mut c, &store, &mut n, 0, 0x40, 2, 0), "the level is two slices deep"); + assert_eq!((n.loads, n.volume_loads, n.unsupported_load, c.levels.len()), (2, 2, 1, 1)); + assert_eq!((store.resident_pages(), word(&c, &store, 0, 0x40, 0), word(&c, &store, 0, 0x40, 4)), (1, 0xffff_0000, 0xff00_00ff), "slices stacked a slice apart"); + // Size word: 2 x 2 x 2, s and t repeat, r clamps. Mode word: nearest, bank 0. + c.short_bind(&[0xffff_ffff, 3 << 28 | 2 << 12, 0, 0x4011 | 1 << 8, 1]); + let d = c.volume_record(&store, &[0x44, 0x111, 0x40, 0], false, Default::default()).expect("the pair finds the level"); + assert!(d.volume && d.count == 1); + assert_eq!((d.levels[0].d, d.levels[0].slice, d.levels[0].w, d.levels[0].h), (2, 4, 2, 2)); + assert_eq!(d.wrap, [Wrap::Repeat, Wrap::Repeat, Wrap::Clamp]); + assert_eq!(sample3(&store, &d, 0.25, 0.25, 0.25, 0.0), [255, 0, 0, 255]); + assert_eq!(sample3(&store, &d, 0.25, 0.25, 0.75, 0.0), [0, 0, 255, 255]); + // No short bind before it, a pair that names nothing, or a record of the wrong length binds nothing. + assert!(c.volume_record(&store, &[0x44, 0x111, 0x40, 0], false, Default::default()).is_none()); + c.short_bind(&[0xffff_ffff, 3 << 28, 0, 0x111, 1]); + assert!(c.volume_record(&store, &[0x44, 0x111, 0x99, 0], false, Default::default()).is_none()); + c.short_bind(&[0xffff_ffff, 3 << 28, 0, 0x111, 1]); + assert!(c.volume_record(&store, &[0x44, 0x111, 0x40], false, Default::default()).is_none()); + } + + /// A mip-mapped volume record (token 0x17b) lists one descriptor word a level and a level's bank is its parity: + /// level 1 is in bank 1, found by its word of the pair. Each level halves every axis, depth included, and the + /// LOD scales carry r in bits 23:16. + #[test] + fn a_mip_mapped_volume_finds_its_levels_by_parity() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + for z in 0..2 { + assert!(slice(&mut c, &store, &mut n, 0, 0x40, z, 0xff00_00ff)); + } + // Level 1: 1 x 1 x 1 (physical 1 x 1 x 1: exponent 0), bank 1. + let mut a = header(1, 1); + a[0] = 0x806f; + a[16] = 0; + a[17] |= 8; + a[19] = 0x80; + assert!(c.load(&store, &a, &words_to_bytes(&[0x0000_ffff]), false, &mut n)); + let mut rec = [0u32; 14]; + rec[0] = 0x44; + rec[1] = 0x10_20_30; + rec[3] = 0x111; + rec[4] = 0x40; + rec[5] = 0x80; + c.short_bind(&[0xffff_ffff, 3 << 28, 6 << 2 | 1 << 5, 0x111, 1]); + let d = c.volume_record(&store, &rec, true, Default::default()).expect("both levels found"); + assert_eq!(d.count, 2); + assert_eq!(d.min, Filter::Mip { within_linear: true, between_linear: true }); + assert_eq!([(d.levels[0].w, d.levels[0].h, d.levels[0].d), (d.levels[1].w, d.levels[1].h, d.levels[1].d)], [(2, 2, 2), (1, 1, 1)]); + assert_eq!(d.lod_scale, [0x30, 0x20, 0x10]); + assert_eq!(n.volume_loads, 3); + } + + /// A filter-coefficient table is stored as sent under its slot and replaced by a later one; a table of any + /// other length is refused. + #[test] + fn a_filter_table_is_kept_by_slot() { + let mut c = TexContext::default(); + assert!(c.filter_table(3, &[7; 33])); + assert!(c.filter_table(4, &[8; 33])); + assert!(c.filter_table(3, &[9; 33])); + assert!(!c.filter_table(5, &[1; 32])); + assert_eq!(c.filters, vec![(3, [9; 33]), (4, [8; 33])]); + for slot in 10..10 + MAX_FILTER_TABLES as u32 { + c.filter_table(slot, &[0; 33]); + } + assert_eq!(c.filters.len(), MAX_FILTER_TABLES, "the extra slots are dropped"); + } + + /// A mode-1 base bind of a 2D texture compares its format row with the last detail bind's. With none, or a + /// different row, a detail magnification code is forced to linear; with the same row it is + /// left alone, and the detail image the address pair names (word 4 when the mode word's bit 28 is set, else + /// word 5) is looked for. The 0x106 form (mode 3) and a bind of another target do neither. + #[test] + fn a_mode_1_base_bind_checks_the_last_detail_bind() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0)); + let mut other = copy(8, 4, 0, 0, 0); + other[17] |= 8; + other[19] = 0; + other[18] = 0; + load_copy(&mut c, &store, &other); + // A 8 x 4 detail image in both banks at address 0x200. + for bank in [0u32, 1] { + let mut d = copy(8, 4, 0, 0, 0); + d[0] = 0x8095; + d[17] |= bank << 3; + d[18 + bank as usize] = 0x200; + assert!(c.load(&store, &d, &vec![0x40; 8 * 4 * 4], false, &mut n)); + } + let mut rec = [0u32; 22]; + rec[6..8].copy_from_slice(&[0, 0xffff_ffff]); + let mode1 = |row: u32| 2 << 28 | 1 << 24 | row << 12; + let bind = |c: &mut TexContext, ctl: u32, code: u32, bank: u32, n: &mut TexCounts| { + c.short_bind(&[0xffff_ffff, ctl, code << 5 | bank << 28, 0x23, 1]); + c.record(&store, &rec, Default::default(), n).unwrap() + }; + let mut n = TexCounts::default(); + // No detail bind: every code is forced to linear, a nearest one and a sharpen one too (established). + assert_eq!(bind(&mut c, mode1(0x30), 4, 0, &mut n).mag, Filter::Linear); + assert_eq!(n.detail_mismatched, 1); + assert_eq!(bind(&mut c, mode1(0x30), 0, 0, &mut n).mag, Filter::Linear); + assert_eq!(bind(&mut c, mode1(0x30), 5, 0, &mut n).mag, Filter::Linear); + assert_eq!(n.detail_mismatched, 3); + // A detail bind of another row: the same. + assert!(c.bind_detail(&[0xffff_ffff, 3, 2, 0x31, 0x200, 0x200])); + assert_eq!(bind(&mut c, mode1(0x30), 3, 0, &mut n).mag, Filter::Linear); + assert_eq!(n.detail_mismatched, 4); + // The same row: the image is found by the pair word the bit names; a code that is not a detail one + // keeps itself. + bind(&mut c, mode1(0x31), 3, 0, &mut n); + assert_eq!((n.detail_mismatched, n.detail_resolved, n.detail_unresolved), (4, 1, 0)); + assert_eq!(bind(&mut c, mode1(0x31), 0, 0, &mut n).mag, Filter::Nearest); + assert_eq!(n.detail_mismatched, 4); + // The pair word the bit chooses names nothing loaded: unresolved. A size that differs is unresolved too. + assert!(c.bind_detail(&[0xffff_ffff, 3, 2, 0x31, 0x300, 0x200])); + bind(&mut c, mode1(0x31), 4, 1, &mut n); + assert_eq!((n.detail_resolved, n.detail_unresolved), (1, 1), "bit 28 set reads word 4, 0x300"); + bind(&mut c, mode1(0x31), 4, 0, &mut n); + assert_eq!((n.detail_resolved, n.detail_unresolved), (2, 1), "bit 28 clear reads word 5, 0x200"); + assert!(c.bind_detail(&[0xffff_ffff, 4, 2, 0x31, 0x200, 0x200])); + bind(&mut c, mode1(0x31), 4, 0, &mut n); + assert_eq!(n.detail_unresolved, 2, "a 16 wide detail bind does not match an 8 wide image"); + // Other forms: no interaction. + let before = (n.detail_mismatched, n.detail_resolved, n.detail_unresolved); + assert_eq!(bind(&mut c, 2 << 28 | 3 << 24 | 0x32 << 12, 4, 0, &mut n).mag, Filter::Linear); + assert_eq!(bind(&mut c, 3 << 28 | 1 << 24 | 0x32 << 12, 4, 0, &mut n).mag, Filter::Linear); + assert_eq!((n.detail_mismatched, n.detail_resolved, n.detail_unresolved), before); + assert!(!c.bind_detail(&[0; 5])); + } + + /// The combine a mode-1 base bind asks for: detail add or modulate with the image found, from the spline table in the + /// control word's slot with the stored doubling undone; sharpen from the same slot with the negation undone; the + /// default curve when the slot holds no table; and none when the image is not found or the code is plain. + #[test] + fn a_mode_1_base_bind_builds_its_combine_from_the_slot_s_table() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + // Two levels, 8 x 4 in bank 0 and 4 x 2 in bank 1 (the bank flips for every second level). + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0)); + let mut l1 = copy(4, 2, 0, 0, 0); + l1[17] |= 8; + l1[18] = 0; + l1[19] = 0x100; + load_copy(&mut c, &store, &l1); + let mut d = copy(8, 4, 0, 0, 0x200); + d[0] = 0x8095; + assert!(c.load(&store, &d, &vec![0x40; 8 * 4 * 4], false, &mut n)); + let mut rec = [0u32; 22]; + rec[6..9].copy_from_slice(&[0, 0x100, 0xffff_ffff]); + let code = 256u32; // mantissa 64, exponent 0: one half + let mut table = [code | code << 10 | code << 20; 33]; + for pair in table.chunks_exact_mut(2) { + pair[1] = code >> 2; + } + assert!(c.filter_table(7, &table[..33])); + assert!(c.bind_detail(&[0xffff_ffff, 3, 2, 0x31, 0x200, 0x200])); + let ctl = 2 << 28 | 1 << 24 | 0x31 << 12 | 7; + let bind = |c: &mut TexContext, code: u32, level_code: u32, channels: u32| { + c.short_bind(&[0xffff_ffff, ctl, code << 5 | level_code << 21 | channels << 26, 0x23, 1]); + c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap() + }; + match bind(&mut c, 3, 4, 1).combine { + Combine::Detail(x) => assert_eq!((x.modulate, x.scale, x.channels, x.image.w, x.image.h, x.weights[10]), (false, 4, 1, 8, 4, 0.25)), + other => panic!("{other:?}"), + } + assert!(matches!(bind(&mut c, 4, 0, 0).combine, Combine::Detail(x) if x.modulate && x.weights[63] == 0.25)); + assert!(matches!(bind(&mut c, 5, 0, 0).combine, Combine::Sharpen(x) if x.weights[0] == -0.5)); + for plain in [0, 1, 2] { + assert_eq!(bind(&mut c, plain, 0, 0).combine, Combine::None, "code {plain}"); + } + // No table in the slot: the client's default curve, still combined. No image: no detail combine, only linear. + let ctl = 2 << 28 | 1 << 24 | 0x31 << 12 | 8; + c.short_bind(&[0xffff_ffff, ctl, 3 << 5, 0x23, 1]); + match c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap().combine { + Combine::Detail(x) => assert_eq!(x.weights, default_weights()), + other => panic!("{other:?}"), + } + assert!(c.bind_detail(&[0xffff_ffff, 3, 2, 0x31, 0x900, 0x900])); + c.short_bind(&[0xffff_ffff, ctl, 3 << 5, 0x23, 1]); + let d = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap(); + assert_eq!((d.combine, d.mag), (Combine::None, Filter::Linear), "the detail image is not loaded: the base shows linearly"); + // A sharpen of a one-level texture has nothing to push away from. + let mut one = [0u32; 22]; + one[6..8].copy_from_slice(&[0, 0xffff_ffff]); + c.short_bind(&[0xffff_ffff, ctl, 5 << 5, 0x23, 1]); + assert_eq!(c.record(&store, &one, Default::default(), &mut TexCounts::default()).unwrap().combine, Combine::None); + } + + /// A detail image (target 0x8095) is sent once to each bank, at the offsets the base levels use too (0 and 0 + /// are recorded), and must not retire or overwrite a base level there. The base binds as it was loaded, with + /// linear magnification for the detail codes 3 and 4 and for sharpen, since nothing combines them. + #[test] + fn a_detail_load_leaves_the_base_level_alone() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + let mut n = TexCounts::default(); + let base = copy(8, 4, 0, 0, 0); + assert!(c.load(&store, &base, &vec![0x11; 8 * 4 * 4], false, &mut n)); + for bank in [0u32, 1] { + let mut d = copy(8, 4, 0, 0, 0); + d[0] = 0x8095; + d[17] |= bank << 3; + d[18 + bank as usize] = 0; + assert!(c.load(&store, &d, &vec![0x77; 8 * 4 * 4], false, &mut n)); + } + assert_eq!((n.loads, n.detail_loads), (3, 2)); + assert_eq!(c.detail.len(), 2, "the same image in each bank"); + assert_eq!(c.levels.len(), 1, "the base level was not retired"); + let l = c.levels[&(0, 0)]; + assert_eq!(store.get(l.plane, l.off), 0x1111_1111, "nor overwritten"); + // The base binds, magnified linearly whatever the detail code says. + let mut rec = [0u32; 22]; + rec[6..8].copy_from_slice(&[0, 0xffff_ffff]); + for (code, mag) in [(0, Filter::Nearest), (1, Filter::Linear), (3, Filter::Linear), (4, Filter::Linear), (5, Filter::Linear)] { + c.short_bind(&[0xffff_ffff, 2 << 12 | 1 << 24, code << 5, 0x23, 1]); + let d = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap(); + assert_eq!((d.count, d.mag), (1, mag), "magnification code {code}"); + } + } + + /// A texel draw of a w x h level at (x0, y0) of an 8 x 4 tile, at `address`. + fn copy(w: u32, h: u32, x0: u32, y0: u32, address: u32) -> [u32; 20] { + let mut a = header(w, h); + a[11] = x0; + a[12] = y0; + a[13] = x0 + w - 1; + a[14] = y0 + h - 1; + a[16] = 0x23; + a[18] = address; + a + } + + fn load_copy(c: &mut TexContext, store: &TextureStore, a: &[u32; 20]) { + let (w, h) = (a[1] as usize, a[2] as usize); + assert!(c.load(store, a, &vec![0x80; w * h * 4], false, &mut TexCounts::default())); + } + + /// A whole load of a `1 << se` by `1 << te` level at `address`. + fn whole(se: u32, te: u32, address: u32) -> [u32; 20] { + let mut a = copy(1 << se, 1 << te, 0, 0, address); + a[16] = te << 4 | se; + a + } + + fn load_whole(c: &mut TexContext, store: &TextureStore, a: &[u32; 20], n: &mut TexCounts) -> bool { + let (w, h) = (a[1] as usize, a[2] as usize); + c.load(store, a, &vec![0x80; w * h * 4], false, n) + } + + /// A long run of reloads at the same addresses keeps texture memory where the first round left it: a + /// level reloaded at the same place overwrites it, so nothing is made again. + #[test] + fn a_long_run_of_reloads_at_the_same_addresses_stays_bounded() { + let store = TextureStore::with_capacity_bytes(1 << 20); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + for k in 0..16 { + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x100 + 0x10 * k), &mut n)); + } + let after_first = (store.resident_pages(), store.used()); + for round in 0..2000 { + for k in 0..16 { + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x100 + 0x10 * k), &mut n), "round {round}"); + } + } + assert_eq!((store.resident_pages(), store.used()), after_first); + assert_eq!((n.loads, n.full), (16 * 2001, 0)); + } + + /// A level reloaded at the same address in another size takes the old one's place: the old is retired, so + /// alternating sizes leave one level and no growth. + #[test] + fn a_level_reloaded_in_another_size_replaces_the_old_one() { + let store = TextureStore::with_capacity_bytes(1 << 20); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + for round in 0..1000 { + let (se, te) = if round % 2 == 0 { (3, 2) } else { (4, 3) }; + assert!(load_whole(&mut c, &store, &whole(se, te, 0x200), &mut n)); + assert_eq!(c.levels.len(), 1, "round {round}"); + } + assert_eq!(store.resident_pages(), 1); + } + + /// A load at a new address that overlaps a level retires it; a level it does not overlap stays. + #[test] + fn a_load_that_overlaps_a_level_retires_it() { + let store = TextureStore::with_capacity_bytes(1 << 20); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + // 16 x 8 texels of 16 bits are eight units, 8 x 4 are two. + assert!(load_whole(&mut c, &store, &whole(4, 3, 0x100), &mut n)); + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x200), &mut n)); + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x104), &mut n)); + assert_eq!(c.levels.keys().copied().collect::>(), [(0, 0x104), (0, 0x200)].into(), "the level at 0x100 was retired, the one at 0x200 was not"); + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x108), &mut n) && c.levels.len() == 3, "the next unit is free"); + } + + /// A fragmented memory refuses nothing: the reviewer's case, four small levels with a hole each side of + /// one, then a level that is larger than either hole and smaller than the free total. The hardware has no + /// allocator to refuse with, so the load goes where its address says and overwrites what is there. + #[test] + fn a_load_that_fits_no_hole_is_not_refused() { + // A bank of 16 units. + let store = TextureStore::with_capacity_bytes(16 * 64); + assert_eq!(store.bank_units(), 16); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + // 4 x 4 texels of 16 bits are one unit: levels at units 0, 2, 4 and 6, with a free unit between. + for u in [0, 2, 4, 6] { + assert!(load_whole(&mut c, &store, &whole(2, 2, u), &mut n)); + } + // Three units from unit 1 reach over the levels at 2 and 4 and the holes beside them. + assert_eq!(units(8, 8, 0), 4); + assert!(load_whole(&mut c, &store, &whole(3, 3, 1), &mut n)); + assert_eq!(c.levels.keys().copied().collect::>(), [(0, 0), (0, 1), (0, 6)].into(), "the levels at 2 and 4 were overwritten"); + assert_eq!((n.loads, n.full), (5, 0)); + // Only an address that reaches past the bank is refused, and nothing is touched. + assert!(!load_whole(&mut c, &store, &whole(3, 3, 13), &mut n)); + assert_eq!((n.full, c.levels.len()), (1, 3)); + assert!(load_whole(&mut c, &store, &whole(3, 3, 12), &mut n)); + } + + /// Any run of loads whose addresses lie inside the bank is accepted, however memory fills: a random + /// stress over many sizes and addresses with no refusal, the levels in the map never overlapping and + /// the newest always readable where its address puts it. + #[test] + fn random_loads_inside_the_bank_are_never_refused() { + let bank = 256u32; + let store = TextureStore::with_capacity_bytes(bank as u64 * 64); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let mut x = 0x2545_f491_4f6c_dd1du64; + let mut next = move |m: u64| { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + x % m + }; + for step in 0..20_000 { + let (se, te) = (next(5) as u32, next(5) as u32); + let need = units(1 << se, 1 << te, 0) as u32; + let address = next((bank - need + 1) as u64) as u32; + let bank_bit = next(2) as u8; + let mut a = whole(se, te, address); + a[17] |= (bank_bit as u32) << 3; + a[18] = 0; + a[18 + bank_bit as usize] = address; + let (w, h) = (a[1] as usize, a[2] as usize); + assert!(c.load(&store, &a, &vec![(step & 0xff) as u8; w * h * 4], false, &mut n), "step {step}: {se} {te} at {address}"); + let texel = (step as u32 & 0xff) * 0x0101_0101; + assert_eq!(word(&c, &store, bank_bit, address, 0), texel, "step {step}"); + let mut spans: Vec<(u8, u64, u64)> = c.levels.iter().map(|(&(b, at), l)| (b, at as u64, at as u64 + units(l.pw, l.ph * l.pd, l.size_code))).collect(); + spans.sort(); + assert!(spans.windows(2).all(|p| p[0].0 != p[1].0 || p[0].2 <= p[1].1), "step {step}: levels overlap"); + assert!(spans.iter().all(|s| s.2 <= bank as u64), "step {step}: a level reaches past the bank"); + } + assert_eq!(n.full, 0); + assert!(store.used().iter().all(|&(_, pages)| pages <= store.plane_pages())); + } + + /// A load whose address and extent reach past the bank is refused and counted, as the model's own + /// limit: nothing already loaded is touched, and a reload of a level that is there always fits. Giving a + /// context's texture memory back empties it. + #[test] + fn loads_past_the_bank_are_refused_and_a_released_context_is_empty() { + let store = TextureStore::with_capacity_bytes(0x300 * 64); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x100), &mut n)); + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x2fe), &mut n)); + assert!(!load_whole(&mut c, &store, &whole(3, 2, 0x2ff), &mut n), "two units from 0x2ff end at 0x301"); + assert_eq!((n.loads, n.full, c.levels.len()), (2, 1, 2)); + // A level of another size at an address in use is replaced only if the new one lies inside the bank. + assert!(!load_whole(&mut c, &store, &whole(7, 4, 0x2fe), &mut n)); + assert_eq!((n.full, c.levels.len()), (2, 2), "refused: the old level stays"); + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x2fe), &mut n), "the same size at the same place is a reload"); + assert!(c.holds()); + c.release(&store); + assert_eq!((c.holds(), store.used(), store.resident_pages()), (false, vec![], 0)); + assert!(load_whole(&mut c, &store, &whole(3, 2, 0x2ff - 1), &mut n)); + } + + /// One texel at the top of a bank holds one page of host memory, not the bank: a plane is sparse. A tiny load + /// into a level of the largest shape does the same. + #[test] + fn one_texel_at_the_top_of_a_bank_holds_one_page() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::new(3), TexCounts::default()); + let top = store.bank_units() - 1; + assert!(load_whole(&mut c, &store, &whole(0, 0, top), &mut n)); + assert_eq!((store.resident_pages(), word(&c, &store, 0, top, 0), n.full), (1, 0x8080_8080, 0)); + // A level of 4096 x 4096 texels fills the bank and retires the texel at its top; a 1 x 1 load into it + // holds the page of its corner, not 64 MiB. + let mut a = copy(1, 1, 0, 0, 0); + a[16] = 0xcc; + assert!(c.load(&store, &a, &[0x80; 4], false, &mut n)); + assert_eq!((store.resident_pages(), word(&c, &store, 0, top, 0), word(&c, &store, 0, 0, 0), n.full), (1, 0, 0x8080_8080, 0)); + } + + /// Many contexts that load at high addresses stay within the product's pool: the loads that would pass it are + /// refused and counted, and what the store holds never passes the ceiling. + #[test] + fn many_contexts_loading_high_stay_within_the_pool() { + // 4 MiB: a bank of 64 pages, 128 pages in all. + let store = TextureStore::with_capacity_bytes(4 << 20); + let top = store.bank_units() - 1; + let (mut n, mut asked) = (TexCounts::default(), 0); + let mut ctxs: Vec = (0..=crate::dev::kona::defs::CONTEXT_SLOTS).map(TexContext::new).collect(); + for c in ctxs.iter_mut() { + for bank in 0..2u32 { + let mut a = whole(0, 0, top); + a[17] |= bank << 3; + a[18] = 0; + a[18 + bank as usize] = top; + asked += 1; + c.load(&store, &a, &[0x80; 4], false, &mut n); + assert!(store.resident_pages() <= store.cap_pages()); + } + } + assert_eq!((store.resident_pages(), n.loads + n.full, n.loads), (store.cap_pages(), asked, store.cap_pages() as u64)); + assert!(n.full > 0); + } + + /// A load past the pool is refused and counted, and the levels already there are left alone, including when the + /// refused load would have retired one. A reload of what is held still fits. + #[test] + fn a_load_past_the_pool_is_refused_and_leaves_the_old_levels() { + // A bank of 3 pages and a pool of 6. + let store = TextureStore::with_capacity_bytes(12 * PAGE_WORDS as u64); + assert_eq!((store.plane_pages(), store.cap_pages()), (3, 6)); + let (mut a, mut b, mut n) = (TexContext::new(1), TexContext::new(2), TexCounts::default()); + let page_units = PAGE_WORDS as u32 / UNIT_WORDS; + // `b` takes five pages: bank 0's three and bank 1's first two. + for (bank, pg) in [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1)] { + let mut l = whole(0, 0, pg * page_units); + l[17] |= bank << 3; + l[18] = 0; + l[18 + bank as usize] = pg * page_units; + assert!(b.load(&store, &l, &[0x80; 4], false, &mut n)); + } + // `a` takes the sixth, with a 1 x 1 level at the start of bank 0. + assert!(load_whole(&mut a, &store, &whole(0, 0, 0), &mut n)); + assert_eq!((store.resident_pages(), n.full), (6, 0)); + // A 256 x 128 level at the same address is another shape: it would retire the 1 x 1 and need bank 0's second page. + assert!(!load_whole(&mut a, &store, &whole(8, 7, 0), &mut n)); + assert_eq!((n.full, a.levels.len(), store.resident_pages(), word(&a, &store, 0, 0, 0)), (1, 1, 6, 0x8080_8080), "refused: the 1 x 1 level and its page stay"); + assert!(a.levels.values().all(|l| l.pw == 1)); + // The same shape at the same place is a reload: its page is held. + assert!(load_whole(&mut a, &store, &whole(0, 0, 0), &mut n)); + assert_eq!(n.full, 1); + } + + /// Two contexts that use the same addresses do not share a word: each has planes of its own. + #[test] + fn contexts_with_the_same_addresses_do_not_alias() { + let store = TextureStore::default(); + let (mut a, mut b, mut n) = (TexContext::new(1), TexContext::new(2), TexCounts::default()); + let l = whole(1, 1, 0x40); + assert!(a.load(&store, &l, &[0x11; 16], false, &mut n)); + assert!(b.load(&store, &l, &[0x22; 16], false, &mut n)); + assert_eq!((word(&a, &store, 0, 0x40, 0), word(&b, &store, 0, 0x40, 0)), (0x1111_1111, 0x2222_2222)); + a.release(&store); + assert_eq!((word(&a, &store, 0, 0x40, 0), word(&b, &store, 0, 0x40, 0)), (0, 0x2222_2222), "releasing one leaves the other"); + } + + /// The client sends a level smaller than the tile as one copy per tile cell of its size, and the + /// last copy gives the origin: a 1 x 1 level of an 8 x 4 tile is at (7, 3), a 4 x 4 level at (4, 0), + /// and an 8 x 1 level (which fills the width) at (0, 3). The box of every copy is not the level. + /// With the bind's logical size or without it, the read-back's case, the placement is the same. + #[test] + fn the_last_copy_of_a_small_level_gives_its_origin() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + for y in 0..4 { + for x in 0..8 { + load_copy(&mut c, &store, ©(1, 1, x, y, 0x100)); + } + } + assert_eq!(c.levels[&(0, 0x100)].place([None, None]), [(7, 1), (3, 1)]); + assert_eq!(c.levels[&(0, 0x100)].place([Some(1), Some(1)]), [(7, 1), (3, 1)]); + for x in [0, 4] { + load_copy(&mut c, &store, ©(4, 4, x, 0, 0x200)); + } + assert_eq!(c.levels[&(0, 0x200)].place([None, None]), [(4, 4), (0, 4)]); + assert_eq!(c.levels[&(0, 0x200)].place([Some(4), Some(4)]), [(4, 4), (0, 4)]); + for y in 0..4 { + load_copy(&mut c, &store, ©(8, 1, 0, y, 0x300)); + } + assert_eq!(c.levels[&(0, 0x300)].place([None, None]), [(0, 8), (3, 1)]); + assert_eq!(c.levels[&(0, 0x300)].place([Some(8), Some(1)]), [(0, 8), (3, 1)]); + } + + /// A level that fills the tile is sent once at the origin, and an update of part of it later does + /// not make the level that part: an 8 x 4 level, then 4 x 2 at (2, 1), is still (0, 0) to (7, 3). + #[test] + fn a_partial_update_does_not_shrink_a_level() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0x100)); + assert_eq!(c.levels[&(0, 0x100)].place([None, None]), [(0, 8), (0, 4)]); + load_copy(&mut c, &store, ©(4, 2, 2, 1, 0x100)); + let l = c.levels[&(0, 0x100)]; + assert_eq!((l.min, l.max), ((0, 0), (7, 3))); + assert_eq!(l.place([None, None]), [(0, 8), (0, 4)], "without a bind"); + assert_eq!(l.place([Some(8), Some(4)]), [(0, 8), (0, 4)], "with the bind's size"); + } + + /// A read-back names no bind, so it takes a level's placement from its loads. A level whose first load was the + /// whole rectangle on an axis is never a run of copies there: an update ending at the edge (the right half, the + /// bottom half) leaves it alone. A small level's last copy still gives its origin. + #[test] + fn an_update_that_ends_at_the_edge_does_not_move_a_level_read_back_without_a_bind() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0x100)); + load_copy(&mut c, &store, ©(4, 4, 4, 0, 0x100)); + assert_eq!(c.levels[&(0, 0x100)].place([None, None]), [(0, 8), (0, 4)], "the right half"); + load_copy(&mut c, &store, ©(8, 2, 0, 2, 0x100)); + assert_eq!(c.levels[&(0, 0x100)].place([None, None]), [(0, 8), (0, 4)], "the bottom half"); + // A level first sent as copies is small on that axis: 2 x 4 copies at x = 0, 2, 4, 6 of a 2 x 4 level. + for x in [0, 2, 4, 6] { + load_copy(&mut c, &store, ©(2, 4, x, 0, 0x200)); + } + assert_eq!(c.levels[&(0, 0x200)].place([None, None]), [(6, 2), (0, 4)]); + } + + /// The read-back's guess needs both the edge and the alignment of a run of copies. A level first sent in part + /// whose later load starts off a multiple of its width, or stops short of the edge, is the box that was loaded. + #[test] + fn the_read_back_guess_wants_the_edge_and_the_alignment() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + // 2 texels at the origin, then 3 ending at the edge but starting at 5 (not a multiple of 3). + load_copy(&mut c, &store, ©(2, 4, 0, 0, 0x100)); + load_copy(&mut c, &store, ©(3, 4, 5, 0, 0x100)); + assert_eq!(c.levels[&(0, 0x100)].place([None, None])[0], (0, 8), "off the alignment: the box"); + // 2 texels at 0, then 2 at 2: aligned, but short of the edge. + load_copy(&mut c, &store, ©(2, 4, 0, 0, 0x200)); + load_copy(&mut c, &store, ©(2, 4, 2, 0, 0x200)); + assert_eq!(c.levels[&(0, 0x200)].place([None, None])[0], (0, 4), "short of the edge: the box"); + // And the same two conditions met are the copy. + load_copy(&mut c, &store, ©(2, 4, 0, 0, 0x300)); + load_copy(&mut c, &store, ©(2, 4, 6, 0, 0x300)); + assert_eq!(c.levels[&(0, 0x300)].place([None, None])[0], (6, 2)); + } + + /// The bind's size is halved for each level down: a four-level mip chain of an 8 x 4 texture is 8 x 4, 4 x 2, 2 x 1 + /// and 1 x 1, each sent as copies and each at its last copy's origin. The levels alternate banks. + #[test] + fn a_bind_halves_its_logical_size_per_level() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + let in_bank_1 = |mut a: [u32; 20]| { + a[17] |= 8; + a[19] = a[18]; + a[18] = 0; + a + }; + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0x100)); + for y in [0, 2] { + for x in [0, 4] { + load_copy(&mut c, &store, &in_bank_1(copy(4, 2, x, y, 0x200))); + } + } + for y in 0..4 { + for x in [0, 2, 4, 6] { + load_copy(&mut c, &store, ©(2, 1, x, y, 0x300)); + } + } + for y in 0..4 { + for x in 0..8 { + load_copy(&mut c, &store, &in_bank_1(copy(1, 1, x, y, 0x400))); + } + } + let mut rec = [0u32; 22]; + rec[6..11].copy_from_slice(&[0x100, 0x200, 0x300, 0x400, 0xffff_ffff]); + // Level 0 is 8 x 4 (size nibbles 3 and 2), in bank 0; levels 1 and 3 are in bank 1. + c.short_bind(&[0xffff_ffff, 2 << 12, 0, 0x23, 1]); + let d = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap(); + let got: Vec<_> = d.levels[..d.count as usize].iter().map(|l| (l.ox, l.oy, l.w, l.h)).collect(); + assert_eq!(got, vec![(0, 0, 8, 4), (4, 2, 4, 2), (6, 3, 2, 1), (7, 3, 1, 1)]); + } + + /// A bind says a level is the size of the tile, so a level whose first load was only part of it is the whole + /// rectangle from the origin, not the box that happened to be loaded. + #[test] + fn a_bind_takes_the_logical_size_over_the_loaded_box() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + load_copy(&mut c, &store, ©(4, 2, 0, 0, 0x100)); + assert_eq!(c.levels[&(0, 0x100)].place([None, None]), [(0, 4), (0, 2)], "without a bind, the box"); + let mut rec = [0u32; 22]; + rec[6..8].copy_from_slice(&[0x100, 0xffff_ffff]); + c.short_bind(&[0xffff_ffff, 2 << 12, 0, 0x23, 1]); + let d = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap(); + assert_eq!([d.levels[0].ox, d.levels[0].oy, d.levels[0].w, d.levels[0].h], [0, 0, 8, 4]); + } + + /// The words of a read: the level's texels as the 16-bit unsigned shorts a luminance read under the one-component + /// storage code returns. + fn read_words(c: &TexContext, store: &TextureStore, address: u32) -> Vec { + let h = ReadHeader::parse(&[0x0de1, 8, 4, 6, 3, 0xd, 0, 0, 0, 0, 7, 3, 0, 0x0385_0030, 0x23, 0x44, address, 0]).unwrap(); + c.read_level(store, &h).expect("the level is loaded").iter().map(|t| (t[0] * 65535.0 + 0.5) as u32).collect() + } + + /// 8 x 4 RGBA bytes: red, green, blue and alpha all different from texel to texel. + fn varied_bytes() -> Vec { + (0..32u32).flat_map(|i| [(i * 8) as u8, 255 - (i * 8) as u8, (i * 37 % 256) as u8, if i % 3 == 0 { 255 } else { 0 }]).collect() + } + + #[test] + fn a_word_and_its_float_and_a_texel_and_its_word_round_trip() { + for w in 0..=0xffffu32 { + assert_eq!((word_as_float(w) * 65535.0 + 0.5) as u32, w); + for storage in [0x10, 0x14, 0x18] { + assert_eq!(pack16(storage, unpack16(storage, w)), Some(w), "{storage:#x} {w:#x}"); + } + } + assert_eq!(pack16(0x30, 0xffff_ffff), None, "a 32-bit texel has no 16-bit word"); + assert_eq!(unpack16(0x14, 0xf801), 0xffff_0000, "red 31, alpha 1"); + assert_eq!(unpack16(0x10, 0x0f00), 0x00_00_ff_00 | 0 << 24 | 0 << 16, "green 15: alpha 0"); + assert_eq!(unpack16(0xd, 0x8000), 0xff80_8080, "any other code is luminance, as an upload of unsigned shorts is"); + } + + /// The library reads a level back as luminance in unsigned shorts under the one-component code and reloads the + /// bytes it got, then binds under the format the level was made with. The read returns the packed 16-bit words of + /// a level loaded as 5-5-5-1 (or 4-4-4-4), the reload keeps them, and the bind samples them as the level was. + #[test] + fn a_level_read_back_under_the_one_component_code_and_reloaded_is_sampled_as_it_was() { + for storage in [0x14u32, 0x10] { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let mut a = copy(8, 4, 0, 0, 0x100); + a[9] = storage; + assert!(c.load(&store, &a, &varied_bytes(), false, &mut n)); + let mut rec = [0u32; 22]; + rec[6..8].copy_from_slice(&[0x100, 0xffff_ffff]); + c.short_bind(&[0xffff_ffff, 2 << 12, storage << 14, 0x23, 1]); + let lv = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap().levels[0]; + let before: Vec = (0..32).map(|i| store.get(lv.plane, lv.off + (i / 8) * 8 + i % 8)).collect(); + let words = read_words(&c, &store, 0x100); + // The words are the quantised components in the lanes: texel 1 is red 8, green 247, blue 37, alpha 0. + let q = |v: u32, bits: u32| (v * ((1 << bits) - 1) + 127) / 255; + let expect = if storage == 0x14 { q(8, 5) << 11 | q(247, 5) << 6 | q(37, 5) << 1 } else { q(8, 4) << 12 | q(247, 4) << 8 | q(37, 4) << 4 }; + assert_eq!(words[1], expect, "{storage:#x}"); + // Free and reload the bytes it returned, as 16-bit luminance. + let bytes: Vec = words.iter().flat_map(|w| (*w as u16).to_be_bytes()).collect(); + let mut l = copy(8, 4, 0, 0, 0x100); + l[3] = 6; + l[4] = 3; + l[9] = 0xd; + assert!(c.load(&store, &l, &bytes, false, &mut n)); + assert_eq!(read_words(&c, &store, 0x100), words, "a level reloaded as raw words reads back as it was loaded"); + // A bind under the original format samples the original texels, whatever storage loaded the words. + c.short_bind(&[0xffff_ffff, 2 << 12, storage << 14, 0x23, 1]); + let lv = c.record(&store, &rec, Default::default(), &mut TexCounts::default()).unwrap().levels[0]; + let after: Vec = (0..32).map(|i| store.get(lv.plane, lv.off + (i / 8) * 8 + i % 8)).collect(); + assert_eq!(after, before, "{storage:#x}"); + assert_eq!(read_words(&c, &store, 0x100), words, "and again after the bind has sampled them"); + } + } + + /// A bind under another format samples the same words as that format: the reloaded 16-bit words bound as + /// luminance are grey, and bound as 4-4-4-4 are not what they were as 5-5-5-1. No bind drops a level on its code. + #[test] + fn a_bind_samples_the_raw_words_under_its_own_format() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let mut l = copy(8, 4, 0, 0, 0x100); + l[3] = 6; + l[4] = 3; + l[9] = 0xd; + let bytes: Vec = (0..32u32).flat_map(|i| ((i * 2000) as u16).to_be_bytes()).collect(); + assert!(c.load(&store, &l, &bytes, false, &mut n)); + assert_eq!(read_words(&c, &store, 0x100)[3], 6000); + let mut rec = [0u32; 22]; + rec[6..8].copy_from_slice(&[0x100, 0xffff_ffff]); + for (format, texel3) in [(0x0du32, unpack16(0xd, 6000)), (0x10, unpack16(0x10, 6000))] { + let mut c2 = TexContext::default(); + assert!(c2.load(&store, &l, &bytes, false, &mut n)); + c2.short_bind(&[0xffff_ffff, 2 << 12, format << 14, 0x23, 1]); + let lv = c2.record(&store, &rec, Default::default(), &mut TexCounts::default()).expect("found by bank and address, not by storage").levels[0]; + assert_eq!(store.get(lv.plane, lv.off + 3), texel3, "{format:#x}"); + } + } + + /// A load that covers what a level holds replaces it, raw or not. One that does not cannot mix raw words with + /// decoded texels: it is decoded as one-component texels, and a raw level is decoded first. + #[test] + fn a_partial_load_does_not_mix_raw_words_and_decoded_texels() { + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + let mut raw = copy(8, 4, 0, 0, 0x100); + raw[3] = 6; + raw[4] = 3; + raw[9] = 0xd; + let words: Vec = (0..32u32).flat_map(|_| 0x8000u16.to_be_bytes()).collect(); + assert!(c.load(&store, &raw, &words, false, &mut n)); + assert!(c.levels[&(0, 0x100)].raw); + // An RGBA update of one corner: the raw words around it become luminance texels first. + let mut part = copy(2, 1, 0, 0, 0x100); + part[9] = 0x30; + assert!(c.load(&store, &part, &[1, 2, 3, 255, 4, 5, 6, 255], false, &mut n)); + let l = c.levels[&(0, 0x100)]; + assert!(!l.raw); + assert_eq!((store.get(l.plane, l.off), store.get(l.plane, l.off + 2)), (0xff01_0203, 0xff80_8080)); + // A partial raw load into a decoded level is decoded as luminance too. + assert!(c.load(&store, &{ let mut p = raw; p[1] = 2; p[2] = 1; p[13] = 1; p[14] = 0; p }, &[0xff, 0xff, 0, 0], false, &mut n)); + assert!(!c.levels[&(0, 0x100)].raw); + assert_eq!(store.get(l.plane, l.off), 0xffff_ffff); + // A raw load of everything it holds replaces it as raw again. + assert!(c.load(&store, &raw, &words, false, &mut n)); + assert!(c.levels[&(0, 0x100)].raw); + } + + /// The levels, the detail images and the unpack state are what a snapshot keeps: restored, the detail map has + /// its images and every level places as it did, and saving the restored state gives the same bytes. + #[test] + fn a_snapshot_keeps_the_levels_and_the_detail_images() { + use crate::dev::kona::stream::{Reader, Writer}; + let store = TextureStore::default(); + let (mut c, mut n) = (TexContext::default(), TexCounts::default()); + c.unpack.skip_rows = 3; + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0x100)); + load_copy(&mut c, &store, ©(4, 4, 4, 0, 0x100)); + for x in 0..8 { + load_copy(&mut c, &store, ©(1, 4, x, 0, 0x200)); + } + for bank in [0u32, 1] { + let mut d = copy(8, 4, 0, 0, 0x300); + d[0] = 0x8095; + d[17] |= bank << 3; + d[18 + bank as usize] = 0x300; + assert!(c.load(&store, &d, &vec![0x77; 8 * 4 * 4], false, &mut n)); + } + assert!(slice(&mut c, &store, &mut n, 0, 0x40, 1, 0x1234_5678)); + assert!(c.filter_table(2, &[0xabcd; 33]) && c.bind_detail(&[1, 3, 2, 0x31, 0x300, 0x301])); + assert_eq!((c.levels.len(), c.detail.len()), (3, 2)); + let mut w = Writer::default(); + c.visit(&mut w); + let mut back = TexContext::default(); + let mut r = Reader::new(&w.out); + back.visit(&mut r); + r.finish().unwrap(); + assert_eq!((back.levels.len(), back.detail.len(), back.unpack.skip_rows), (3, 2, 3)); + assert_eq!(back.levels[&(0, 0x40)].pd, 2, "a volume level keeps its depth"); + assert_eq!((back.filters.as_slice(), back.detail_bind), (&[(2, [0xabcd; 33])][..], c.detail_bind), "the filter tables and the detail bind"); + for key in [(0, 0x100), (0, 0x200)] { + assert_eq!(back.levels[&key].place([None, None]), c.levels[&key].place([None, None]), "{key:?}"); + } + assert_eq!(back.levels[&(0, 0x100)].place([None, None]), [(0, 8), (0, 4)], "the whole-first-load bit survived"); + assert_eq!(back.levels[&(0, 0x200)].place([None, None]), [(7, 1), (0, 4)]); + assert_eq!(back.detail.keys().collect::>(), c.detail.keys().collect()); + let mut again = Writer::default(); + back.visit(&mut again); + assert_eq!(again.out, w.out); + } + + /// A level address counts 256 bits a bank, so a rectangle of 32 texels takes 2 units at + /// 16 bits, 4 at 32 and 8 at the 64-bit slot. + #[test] + fn a_levels_extent_is_its_texels_in_units_of_256_bits() { + assert_eq!([units(8, 4, 0), units(8, 4, 1), units(8, 4, 2)], [2, 4, 8]); + assert_eq!(units(1, 1, 0), 1, "a part unit still takes the unit"); + assert_eq!(parse(&{ let mut a = header(1, 1); a[17] |= 2; a }).size_code(), 2); + } + + /// Two levels of one bank cannot share a unit: loading at an address inside another level's + /// extent retires that level; a level in the other bank, or past the extent, stays. + #[test] + fn a_load_retires_the_levels_it_overlaps_in_its_bank() { + let store = TextureStore::default(); + let mut c = TexContext::default(); + // 8 x 4 at 32 bits is 4 units: [0x100, 0x104). + let mut a = copy(8, 4, 0, 0, 0x100); + a[17] |= 1; + load_copy(&mut c, &store, &a); + let mut other_bank = copy(8, 4, 0, 0, 0x101); + other_bank[17] |= 8; + other_bank[19] = 0x101; + load_copy(&mut c, &store, &other_bank); + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0x104)); + assert_eq!(c.levels.len(), 3, "past the extent, and in the other bank, nothing is retired"); + load_copy(&mut c, &store, ©(8, 4, 0, 0, 0x103)); + let mut keys: Vec<_> = c.levels.keys().copied().collect(); + keys.sort(); + assert_eq!(keys, vec![(0, 0x103), (1, 0x101)], "the 32-bit level and the one after it were overwritten"); + } + + #[test] + fn the_unpack_state_keeps_the_names_the_board_keeps() { + let mut u = Unpack::default(); + u.set(0xcf2, 8); + u.set(0xcf3, 2); + u.set(0xcf4, 3); + u.set(0xcf5, 8); + u.set(0xcf0, 1); + assert_eq!(u, Unpack { row_length: 8, skip_rows: 2, skip_pixels: 3, alignment: 8, swap_bytes: true }); + u.set(0xcf5, 3); + assert_eq!(u.alignment, 8, "an alignment that is not a power of two up to 8 is refused"); + u.set(0xd05, 1); + assert_eq!(u.alignment, 8, "the pack side is not kept"); + } +} diff --git a/src/dev/kona/back/gl/vertex.rs b/src/dev/kona/back/gl/vertex.rs new file mode 100644 index 000000000..456d12d31 --- /dev/null +++ b/src/dev/kona/back/gl/vertex.rs @@ -0,0 +1,233 @@ +//! KONA's vertex: what the GL core clips, and the policy that clips it. +//! +//! `KVertex` is the clip-space vertex with the attributes the pipe carries +//! (front and back colour, one texture coordinate of four words). It +//! implements the core's `ClipVertex`, so `Clip::clip_polygon_by` cuts it as +//! it cuts VPro's and IMPACT's. `KonaGeometry` is the core's `Geometry` for +//! this board: near and far are clipped geometrically against the planes +//! z + w >= 0 and w - z >= 0, which cut where the board's z' = (z - w) / 2 planes do +//! (the half cancels in the cut position) (inferred: the vertex bus carries z already +//! scaled, and the engines clip the volume's third axis; which exact range is +//! cut is unconfirmed); x and y are clipped only at a guard band +//! of eight clip widths, which keeps window positions inside the `i32` grid, +//! and the job's rectangle takes the rest. The +//! divide is a single-precision reciprocal and a multiply, and window +//! positions truncate toward minus infinity, as VPro's do (unknown for this +//! board). + +use crate::dev::gl::vertex::{ClipVertex, Geometry}; +use crate::dev::kona::back::raster::WVertex; + +/// The guard band: x and y are cut where |x| or |y| passes 8 w, so the kept region +/// is eight viewport widths across in all (four either side of the viewport's +/// edge), which keeps window positions inside the `i32` grid. +pub const GUARD: f32 = 8.0; + +#[derive(Clone, Copy, Debug, Default)] +pub struct KVertex { + /// Clip-space and eye-space positions: both linear in the object position, + /// so clipping interpolates them along an edge exactly. + pub h: [f32; 4], + pub e: [f32; 4], + /// Front and back colour, 0 to 1. + pub c: [f32; 4], + pub cb: [f32; 4], + /// Texture coordinate (s, t, r, q) after the texture matrix. + pub t: [f32; 4], + /// The planes the vertex is outside of. + pub oc: u32, + /// The edge flag in force when the vertex was submitted: the edge from it to the next vertex of + /// its polygon is a boundary edge, which a polygon mode of line or point draws. + pub edge: bool, +} + +impl ClipVertex for KVertex { + #[inline(always)] + fn h(&self) -> &[f32; 4] { + &self.h + } + #[inline(always)] + fn e(&self) -> &[f32; 4] { + &self.e + } + #[inline(always)] + fn cross(a: &Self, b: &Self, da: f32, db: f32) -> Self { + let t = da / (da - db); + let l = |x: f32, y: f32| x + (y - x) * t; + KVertex { + h: std::array::from_fn(|k| l(a.h[k], b.h[k])), + e: std::array::from_fn(|k| l(a.e[k], b.e[k])), + c: std::array::from_fn(|k| l(a.c[k], b.c[k])), + cb: std::array::from_fn(|k| l(a.cb[k], b.cb[k])), + t: std::array::from_fn(|k| l(a.t[k], b.t[k])), + oc: 0, + // The clipper sets it. + edge: false, + } + } + const EDGE_FLAGS: bool = true; + #[inline(always)] + fn edge(&self) -> bool { + self.edge + } + #[inline(always)] + fn set_edge(&mut self, edge: bool) { + self.edge = edge; + } +} + +pub struct KonaGeometry; + +impl Geometry for KonaGeometry { + const VOLUME: u32 = 0x3f; + #[inline(always)] + fn volume(h: &[f32; 4], i: usize) -> f32 { + match i { + 0 => GUARD * h[3] + h[0], + 1 => GUARD * h[3] - h[0], + 2 => GUARD * h[3] + h[1], + 3 => GUARD * h[3] - h[1], + 4 => h[3] + h[2], + _ => h[3] - h[2], + } + } + #[inline(always)] + fn divide(h: &[f32; 4]) -> Option<[f32; 3]> { + let [cx, cy, cz, cw] = *h; + if !(cw > 0.0) { + return None; + } + let r = 1.0 / cw; + Some([cx * r, cy * r, cz * r]) + } + #[inline(always)] + fn snap(v: f32, subpixel: f32) -> f32 { + (v * subpixel).round_ties_even() / subpixel + } +} + +/// The window position of a clip-space vertex relative to the window origin: +/// the viewport applied to the divided position, depth through the depth range, +/// colour and texture words pre-divided, all quantised to the vertex bus's +/// precision. None when the vertex has no divide (w at or below zero). With `fogged`, the vertex +/// carries the fog coordinate, the eye-space distance `|z|` (the model's own choice; see +/// `raster::fog`), over w. +pub fn project(viewport: [f32; 4], depth_range: [f32; 2], v: &KVertex, textured: bool, fogged: bool) -> Option { + let [nx, ny, nz] = KonaGeometry::divide(&v.h)?; + let r = 1.0 / v.h[3]; + let [vx, vy, vw, vh] = viewport; + let [dn, df] = depth_range; + let t = if textured { [v.t[0] * r, v.t[1] * r, v.t[2] * r, v.t[3] * r] } else { [0.0; 4] }; + Some(WVertex { + x: grid(vx + (nx + 1.0) * 0.5 * vw), + y: grid(vy + (ny + 1.0) * 0.5 * vh), + z: depth32(dn + (nz * 0.5 + 0.5) * (df - dn)), + c: v.c.map(colour13), + t: [t, [0.0; 4]], + d: 0.0, + fog: if fogged { [fog_coordinate(&v.e) * r, r] } else { [0.0; 2] }, + }) +} + +/// The fog coordinate of an eye-space position: the absolute depth, over the homogeneous w when it is +/// not 1. +#[inline] +pub fn fog_coordinate(e: &[f32; 4]) -> f32 { + (if e[3] != 0.0 { e[2] / e[3] } else { e[2] }).abs() +} + +/// A colour as the word the planes hold: `0xAARRGGBB`, eight bits a component +/// (the model holds a window's colour at eight bits a component, one word a +/// buffer, whatever depth the visual names). +pub fn colour_word(c: [f32; 4]) -> u32 { + let b = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u32; + b(c[3]) << 24 | b(c[0]) << 16 | b(c[1]) << 8 | b(c[2]) +} + +/// Full intensity of a 13-bit colour component, as the vertex bus carries it. +pub const COLOUR_MAX: f32 = 8191.0; + +/// A colour component, 0 to 1, as a 13-bit value. +#[inline] +pub fn colour13(c: f32) -> u16 { + (c.clamp(0.0, 1.0) * COLOUR_MAX + 0.5) as u16 +} + +/// A window coordinate in pixels on the 1/256 grid, saturating at what an `i32` holds. The geometry +/// engine subtracts 49152.0 (1.5 x 2^15) from window x and y before a vertex leaves it (established +/// as the store-data rule reads it; the operand order is inferred). Within +-16384 of the origin the +/// result lies in the binade [2^15, 2^16), where one single-precision step is exactly 1/256, so that +/// subtract is what puts the position on the 12.8 grid. A single-precision subtract rounds to +/// nearest, ties to even (the one tie test the diagnostic has fits that and truncation alike, so +/// nearest-even is assumed, inferred), and 49152 is a multiple of 1/256, so the position rounds to +/// the nearest 1/256 with ties to even. Beyond +-16384 the engine's grid is coarser or finer and +/// the clip rejects the vertex first; the model rounds the same way. +#[inline] +pub fn grid(v: f32) -> i32 { + (v * 256.0).round_ties_even() as i32 +} + +/// A depth fraction, 0 at the near end of the depth range, as 32 bits. +#[inline] +pub fn depth32(z: f32) -> u32 { + (z.clamp(0.0, 1.0) * 4_294_967_040.0) as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::gl::vertex::Clip; + + fn kv(x: f32, y: f32, z: f32, w: f32) -> KVertex { + let mut v = KVertex { h: [x, y, z, w], e: [x, y, z, w], ..Default::default() }; + v.oc = Clip::default().outcode_by::(&v); + v + } + + /// Near and far are planes here, where VPro's geometry has none. + #[test] + fn near_and_far_clip_geometrically_and_x_and_y_only_at_the_guard_band() { + let c = Clip::default(); + assert_eq!(kv(0.0, 0.0, 0.5, 1.0).oc, 0); + assert_ne!(kv(0.0, 0.0, 1.5, 1.0).oc & 0x20, 0, "beyond the far plane"); + assert_ne!(kv(0.0, 0.0, -1.5, 1.0).oc & 0x10, 0, "in front of the near plane"); + assert_eq!(kv(5.0, -5.0, 0.0, 1.0).oc, 0, "outside the window, inside the guard band"); + assert_ne!(kv(9.0, 0.0, 0.0, 1.0).oc & 0x2, 0); + // A triangle straddling the far plane is cut at it. + let mut v = [kv(0.0, 0.0, 0.0, 1.0), kv(0.0, 0.0, 3.0, 1.0), kv(0.5, 0.0, 0.0, 1.0), KVertex::default()]; + let any = v[..3].iter().fold(0, |o, w| o | w.oc); + let mut tmp = v; + let (n, over) = c.clip_polygon_by::(&mut v, &mut tmp, 3, any); + assert!(!over && n == 4, "{n}"); + assert!(v[..n].iter().all(|w| w.h[2] <= w.h[3] + 1e-6)); + } + + #[test] + fn the_divide_multiplies_by_a_reciprocal_and_refuses_w_at_or_below_zero() { + let n = KonaGeometry::divide(&[7.0, 0.0, 0.0, 49.0]).unwrap(); + assert_eq!(n[0], 7.0 * (1.0 / 49.0)); + assert!(KonaGeometry::divide(&[1.0, 1.0, 1.0, 0.0]).is_none()); + assert!(KonaGeometry::divide(&[1.0, 1.0, 1.0, -1.0]).is_none()); + assert_eq!(KonaGeometry::snap(-0.001, 256.0), 0.0, "nearest, not toward minus infinity"); + assert_eq!(KonaGeometry::snap(-0.003, 256.0), -1.0 / 256.0); + } + + #[test] + fn components_quantise_to_thirteen_bits_and_depth_to_a_fraction() { + assert_eq!((colour13(0.0), colour13(1.0), colour13(2.0), colour13(-1.0)), (0, 8191, 8191, 0)); + assert_eq!(colour13(0.5), 4096); + assert_eq!((depth32(0.0), depth32(2.0)), (0, 4_294_967_040)); + } + + #[test] + fn positions_round_to_the_nearest_step_of_the_grid_ties_to_even() { + assert_eq!((grid(1.0), grid(1.0 / 256.0), grid(0.999), grid(-0.001), grid(-1.0)), (256, 1, 256, 0, -256)); + // Halfway between steps: the even one, as the engine's single-precision subtract of 49152.0 lands them. + assert_eq!((grid(0.5 / 256.0), grid(1.5 / 256.0), grid(2.5 / 256.0), grid(-0.5 / 256.0), grid(-1.5 / 256.0)), (0, 2, 2, 0, -2)); + // The subtract itself, within +-16384 of the origin: the same result as rounding the position. + for v in [100.37f32, 0.001953125 * 3.0, 1279.999, 16383.5, -16383.25, 0.0] { + let engine = ((v - 49152.0) as f64 + 49152.0) * 256.0; + assert_eq!(grid(v) as f64, engine, "{v}"); + } + } +} diff --git a/src/dev/kona/back/golden/mod.rs b/src/dev/kona/back/golden/mod.rs new file mode 100644 index 000000000..cf6e3b4a0 --- /dev/null +++ b/src/dev/kona/back/golden/mod.rs @@ -0,0 +1,730 @@ +//! Golden scenes: GL triangles and clears through set-up, the tile scheduler and the +//! per-pixel module, against pinned hashes, and a tiled run against an inline one. + +use super::raster::ops::{BlendEq, BlendFactor, Compare, LogicOp, Ownership, StencilOp}; +use super::store::DID_PLANE; +use super::raster::{ClearReq, DrawState, Prims, Rect, WVertex}; +use super::store::PixelStore; +use super::KonaBackend; +use crate::dev::kona::KonaConfig; + +const N: u32 = 64; + +/// A board with an `N` x `N` managed area, `pool.0` tile workers and `pool.1`-pixel tiles. +fn board(pool: (usize, u32)) -> KonaBackend { + KonaBackend::new(KonaConfig { managed: (N, N), ..KonaConfig::default() }.with_pool(pool.0, pool.1)) +} + +/// A draw state drawing into word 1 (and 2 for the back buffer), depth in +/// word 3, over the whole 64 x 64 screen. +fn state(f: impl FnOnce(&mut DrawState)) -> DrawState { + let full = Rect { x0: 0, y0: 0, x1: N as i32, y1: N as i32 }; + let mut d = DrawState { clip: full, viewport: full, colour: [1, 0], colours: 1, depth_word: 3, depth_bits: 32, depth_func: Compare::Less, ..DrawState::default() }; + f(&mut d); + d +} + +fn v(x: f32, y: f32, c: [u16; 4], z: u32) -> WVertex { + WVertex { x: (x * 256.0) as i32, y: (y * 256.0) as i32, z, c, t: [[0.0; 4]; 2], d: 0.0, fog: [0.0; 2] } +} + +const WHITE: [u16; 4] = [8191; 4]; + +/// A triangle as GL window coordinates. +fn tri(p: [(f32, f32); 3], c: [[u16; 4]; 3], z: [u32; 3]) -> [WVertex; 3] { + [v(p[0].0, p[0].1, c[0], z[0]), v(p[1].0, p[1].1, c[1], z[1]), v(p[2].0, p[2].1, c[2], z[2])] +} + +/// Flush `tris` under `ds` through set-up and the tiles. +fn draw(st: &mut KonaBackend, ds: &DrawState, tris: &[[WVertex; 3]]) { + let p = Prims { tris: tris.to_vec(), clears: Vec::new(), state: Some(*ds), ..Default::default() }; + st.prims = p; + st.flush(); +} + +fn clear(st: &mut KonaBackend, ds: &DrawState, colour: Option, depth: Option) { + let p = Prims { tris: Vec::new(), clears: vec![ClearReq { rect: ds.clip, colour, depth, ..Default::default() }], state: Some(*ds), ..Default::default() }; + st.prims = p; + st.flush(); +} + +fn pixels(s: &PixelStore, word: usize) -> Vec<(u32, u32, u32)> { + let mut out = Vec::new(); + for y in 0..N { + for x in 0..N { + let w = s.read(x, y, word); + if w != 0 { + out.push((x, y, w)); + } + } + } + out +} + +fn hash(s: &PixelStore) -> u64 { + s.checksum() +} + +/// A GL unit square lights exactly the pixel GL specifies, in two triangles +/// sharing a diagonal, with no gap and no pixel twice. +#[test] +fn a_gl_unit_square_lights_one_pixel_and_a_quad_is_watertight() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let ds = state(|_| {}); + let quad = |x0: f32, y0: f32, x1: f32, y1: f32| vec![tri([(x0, y0), (x1, y0), (x1, y1)], [[8191; 4]; 3], [0; 3]), tri([(x0, y0), (x1, y1), (x0, y1)], [[8191; 4]; 3], [0; 3])]; + draw(&mut st, &ds, &quad(3.0, 4.0, 4.0, 5.0)); + assert_eq!(pixels(&s, 1).iter().map(|&(x, y, _)| (x, y)).collect::>(), [(3, 4)]); + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &ds, &quad(10.0, 20.0, 18.0, 26.0)); + let lit = pixels(&s, 1); + assert_eq!(lit.len(), 8 * 6); + assert!(lit.iter().all(|&(x, y, _)| (10..18).contains(&x) && (20..26).contains(&y))); + // A quad of odd corners, cut by a diagonal that passes through samples, still has no gap. + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &ds, &quad(0.0, 0.0, 17.0, 17.0)); + assert_eq!(pixels(&s, 1).len(), 17 * 17); +} + +/// Colour follows the plane from the vertices; the word is `0xAARRGGBB`. +#[test] +fn colour_is_interpolated_across_the_triangle() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let (r, g, b) = ([8191, 0, 0, 8191], [0, 8191, 0, 8191], [0, 0, 8191, 8191]); + draw(&mut st, &state(|_| {}), &[tri([(0.5, 0.5), (32.5, 0.5), (0.5, 32.5)], [r, g, b], [0; 3])]); + let at = |x, y| s.read(x, y, 1); + // Next to the red vertex it is nearly red; on the bottom edge's middle it is half red, half green. + assert_eq!(at(1, 1) >> 24, 0xff); + assert!((at(1, 1) >> 16) & 0xff > 0xe0); + let mid = at(16, 1); + assert!(((mid >> 16) & 0xff).abs_diff((mid >> 8) & 0xff) < 12 && (mid >> 16) & 0xff > 100, "{mid:#x}"); + assert_eq!(at(40, 40), 0, "outside"); +} + +/// A nearer triangle wins where two cross, whichever was drawn first; with the +/// test off the later one wins. +#[test] +fn depth_decides_which_triangle_shows() { + let near = 0x1000_0000; + let far = 0xc000_0000; + let red = [8191, 0, 0, 8191]; + let blue = [0, 0, 8191, 8191]; + let a = tri([(0.5, 0.5), (40.5, 0.5), (0.5, 40.5)], [red; 3], [near; 3]); + let b = tri([(0.5, 0.5), (40.5, 0.5), (0.5, 40.5)], [blue; 3], [far; 3]); + let test = state(|d| { + d.depth_test = true; + d.depth_func = Compare::Less; + }); + for first_near in [true, false] { + let mut st = board((0, 32)); + let s = st.store.clone(); + clear(&mut st, &test, None, Some(0xffff_ff00)); + if first_near { + draw(&mut st, &test, &[a]); + draw(&mut st, &test, &[b]); + } else { + draw(&mut st, &test, &[b]); + draw(&mut st, &test, &[a]); + } + assert_eq!(s.read(5, 5, 1) & 0xffffff, 0xff0000, "near is red, drawn {} first", if first_near { "near" } else { "far" }); + } + let mut st = board((0, 32)); + let s = st.store.clone(); + let off = state(|_| {}); + draw(&mut st, &off, &[a]); + draw(&mut st, &off, &[b]); + assert_eq!(s.read(5, 5, 1) & 0xffffff, 0x0000ff, "no test: the later wins"); + assert_eq!(s.read(5, 5, 3), 0, "and no depth is written"); +} + +/// The scissor, in window coordinates, narrows a draw and a clear. +#[test] +fn the_scissor_narrows_draws_and_clears() { + let mut st = board((0, 32)); + let s = st.store.clone(); + // The GL path puts the scissor into the clip rectangle when it builds the draw state. + let scissored = state(|d| d.clip = Rect { x0: 10, y0: 10, x1: 15, y1: 15 }); + draw(&mut st, &scissored, &[tri([(0.5, 0.5), (60.5, 0.5), (0.5, 60.5)], [WHITE; 3], [0; 3])]); + let lit = pixels(&s, 1); + assert!(!lit.is_empty() && lit.iter().all(|&(x, y, _)| (10..15).contains(&x) && (10..15).contains(&y))); + clear(&mut st, &scissored, Some(0x1122_3344), None); + let lit = pixels(&s, 1); + assert_eq!(lit.len(), 25); + assert!(lit.iter().all(|&(_, _, w)| w == 0x1122_3344)); +} + +/// A clear writes the colour word of every buffer drawn to and the depth word. +#[test] +fn a_clear_writes_colour_and_depth_to_the_targets() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let both = state(|d| { + d.colour = [1, 2]; + d.colours = 2; + }); + clear(&mut st, &both, Some(0xff00_ff00), Some(0xffff_ff00)); + assert_eq!((s.read(0, 0, 1), s.read(63, 63, 2), s.read(30, 30, 3)), (0xff00_ff00, 0xff00_ff00, 0xffff_ff00)); + // No target: no colour, no write at all. + let none = state(|d| d.colours = 0); + clear(&mut st, &none, Some(0xabab_abab), None); + assert_eq!(s.read(0, 0, 1), 0xff00_ff00); + assert_eq!(st.raster.off_target, 1); +} + +/// The window's origin moves a draw and cuts it at the window's edge. +#[test] +fn the_window_origin_places_the_draw_and_bounds_it() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let win = state(|d| { + d.origin = [20, 30]; + d.clip = Rect { x0: 20, y0: 30, x1: 25, y1: 40 }; + }); + draw(&mut st, &win, &[tri([(0.5, 0.5), (30.5, 0.5), (0.5, 30.5)], [WHITE; 3], [0; 3])]); + let lit = pixels(&s, 1); + assert!(lit.iter().all(|&(x, y, _)| (20..25).contains(&x) && (30..40).contains(&y))); + // The triangle's own left and bottom edges lie on window column 0 and row 0: those samples are out. + assert_eq!(lit.len(), 4 * 9); +} + +/// The store hash of the 150-triangle scene below, drawn inline: a regression net +/// for set-up and the per-pixel module that moves only if they do (or the scene). +/// Re-pinned when depth became a 32.16 integer plane: a depth that rounded one way +/// in `f32` now rounds to the plane's own integer, so a few depth-test results moved. +const SCENE_150: u64 = 0xa949_d8e0_9bb7_4571; + +/// A tiled run, with workers, small tiles and a small ring, is bit-exact with +/// the inline one, depth test and overlap included. +#[test] +fn a_tiled_run_is_bit_exact_with_the_inline_one() { + let mut seed = 0x9e37_79b9_7f4a_7c15u64; + let mut next = move |m: u32| { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + (seed >> 11) as u32 % m + }; + let draws: Vec<[WVertex; 3]> = (0..150) + .map(|_| { + let p = |next: &mut dyn FnMut(u32) -> u32| (next(90) as f32 - 12.5, next(90) as f32 - 12.5); + let c = |next: &mut dyn FnMut(u32) -> u32| [next(8192) as u16, next(8192) as u16, next(8192) as u16, 8191]; + let z = next(0xffff_ff00); + tri([p(&mut next), p(&mut next), p(&mut next)], [c(&mut next), c(&mut next), c(&mut next)], [z, z.saturating_add(next(1 << 20)), z]) + }) + .collect(); + let run = |pool: (usize, u32)| { + let mut st = board(pool); + let s = st.store.clone(); + let ds = state(|d| { + d.depth_test = true; + d.depth_func = Compare::Lequal; + }); + clear(&mut st, &ds, Some(0xff10_2030), Some(0xffff_ff00)); + for d in &draws { + draw(&mut st, &ds, &[*d]); + } + st.drain(); + assert!(!st.is_poisoned()); + hash(&s) + }; + let inline = run((0, 32)); + assert_eq!(inline, run((0, 32)), "the inline run is deterministic"); + assert_eq!(inline, SCENE_150, "the pinned scene hash: {inline:#018x}"); + for pool in [(1, 32), (3, 16), (4, 64), (2, 8)] { + assert_eq!(run(pool), inline, "pool {pool:?}"); + } +} + +/// A tile size that is not a power of two is refused and the default used: the +/// draw still lands. +#[test] +fn a_bad_tile_size_falls_back_to_the_default() { + let mut st = board((1, 24)); + let s = st.store.clone(); + draw(&mut st, &state(|_| {}), &[tri([(0.5, 0.5), (20.5, 0.5), (0.5, 20.5)], [WHITE; 3], [0; 3])]); + st.drain(); + assert!(!pixels(&s, 1).is_empty()); +} + +/// A triangle covering pixel (5, 5) and a good area about it, with colour `c` and depth `z`. +fn big(c: [u16; 4], z: u32) -> [WVertex; 3] { + tri([(0.5, 0.5), (40.5, 0.5), (0.5, 40.5)], [c; 3], [z; 3]) +} + +const RED: [u16; 4] = [8191, 0, 0, 8191]; + +/// Fill colour word 1 with `word` over the whole screen. +fn fill(st: &mut KonaBackend, ds: &DrawState, word: u32) { + clear(st, ds, Some(word), None); +} + +/// The alpha test drops a fragment whose alpha fails the comparison against the +/// reference, in 13-bit units. +#[test] +fn the_alpha_test_drops_failing_fragments() { + let half = [8191, 8191, 8191, 4096]; + for (func, reference, drawn) in [(Compare::Greater, 6000, false), (Compare::Less, 6000, true), (Compare::Gequal, 4096, true), (Compare::Never, 0, false)] { + let mut st = board((0, 32)); + let s = st.store.clone(); + let ds = state(|d| d.alpha = super::raster::AlphaTest { enabled: true, func, reference }); + draw(&mut st, &ds, &[big(half, 0)]); + assert_eq!(s.read(5, 5, 1) != 0, drawn, "{func:?} against {reference}"); + } + // Off, the same fragment draws whatever the function says. + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &state(|d| d.alpha = super::raster::AlphaTest { enabled: false, func: Compare::Never, reference: 0 }), &[big(half, 0)]); + assert_ne!(s.read(5, 5, 1), 0); +} + +/// A depth mask of off tests depth without writing it; colour is written. +#[test] +fn the_depth_mask_keeps_the_depth_plane() { + for (mask, depth) in [(true, 0x1000_0000u32), (false, 0xffff_ff00)] { + let mut st = board((0, 32)); + let s = st.store.clone(); + let ds = state(|d| { + d.depth_test = true; + d.depth_func = Compare::Less; + d.depth_mask = mask; + }); + clear(&mut st, &ds, None, Some(0xffff_ff00)); + draw(&mut st, &ds, &[big(RED, 0x1000_0000)]); + assert_eq!(s.read(5, 5, 3), depth, "depth mask {mask}"); + assert_ne!(s.read(5, 5, 1), 0, "the colour is written either way"); + } +} + +/// The stencil test, each outcome's operation and the write mask: a clear to 1; a +/// test of equal to 1 passes and increments on depth pass; a failing test applies +/// its own operation; a depth fail its own; the write mask keeps the other bits. +#[test] +fn the_stencil_test_and_its_operations() { + let stencil = |f: &dyn Fn(&mut super::raster::StencilState)| { + state(|d| { + (d.stencil_word, d.stencil_bits) = (4, 8); + d.stencil.enabled = true; + f(&mut d.stencil); + }) + }; + let mut st = board((0, 32)); + let s = st.store.clone(); + let on = stencil(&|t| { + t.func = Compare::Equal; + t.reference = 1; + t.zpass = StencilOp::Incr; + t.fail = StencilOp::Zero; + }); + let mut p = Prims { clears: vec![ClearReq { rect: on.clip, stencil: Some(1), ..Default::default() }], state: Some(on), ..Default::default() }; + st.prims = std::mem::take(&mut p); + st.flush(); + draw(&mut st, &on, &[big(RED, 0)]); + assert_eq!(s.read(5, 5, 4), 2, "passed the test, depth always passes: incremented"); + assert_ne!(s.read(5, 5, 1), 0, "and drawn"); + // The value is now 2: equal-to-1 fails, the fail operation zeroes it, and nothing is drawn. + let before = s.read(5, 5, 1); + draw(&mut st, &on, &[big([0, 8191, 0, 8191], 0)]); + assert_eq!((s.read(5, 5, 4), s.read(5, 5, 1)), (0, before), "failed: zeroed, not drawn"); + // A depth failure takes the zfail operation, and the write mask keeps the other bits. + let mut st = board((0, 32)); + let s = st.store.clone(); + let deep = stencil(&|t| { + t.zfail = StencilOp::Replace; + t.reference = 0xf5; + t.write_mask = 0x0f; + }); + let deep = DrawState { depth_test: true, depth_func: Compare::Less, ..deep }; + let mut p = Prims { clears: vec![ClearReq { rect: deep.clip, depth: Some(0x1000_0000), stencil: Some(0xa0), stencil_mask: 0xff, ..Default::default() }], state: Some(deep), ..Default::default() }; + st.prims = std::mem::take(&mut p); + st.flush(); + draw(&mut st, &deep, &[big(RED, 0x2000_0000)]); + assert_eq!(s.read(5, 5, 4), 0xa5, "the depth test failed: replaced under the write mask 0x0f, the high nibble kept"); + assert_eq!(s.read(5, 5, 1), 0, "and nothing drawn"); +} + +/// Source-over blending against a cleared buffer, and the constant colour. +#[test] +fn blending_mixes_with_the_buffer() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let ds = state(|d| { + d.blend = super::raster::BlendState { enabled: true, src: BlendFactor::SrcAlpha, dst: BlendFactor::OneMinusSrcAlpha, eq: BlendEq::Add, constant: [0; 4] }; + }); + fill(&mut st, &ds, 0xff00_00ff); // opaque blue + draw(&mut st, &ds, &[big([8191, 0, 0, 4096], 0)]); + let w = s.read(5, 5, 1); + // Red at half alpha over blue: red about 0x80, blue about 0x7f, alpha the blend of 0.5 and 1. + assert_eq!((w >> 16 & 0xff, w & 0xff), (0x80, 0x7f), "{w:#x}"); + assert_eq!(s.read(60, 60, 1), 0xff00_00ff, "outside the triangle the buffer is as filled"); + // A constant colour of (0, 1, 0) takes the destination's green and nothing else. + let ds = state(|d| { + d.blend = super::raster::BlendState { enabled: true, src: BlendFactor::ConstantColour, dst: BlendFactor::Zero, eq: BlendEq::Add, constant: [0, 8191, 0, 8191] }; + }); + draw(&mut st, &ds, &[big([8191, 8191, 8191, 8191], 0)]); + assert_eq!(s.read(5, 5, 1), 0xff00_ff00, "white times a constant of (0, 1, 0, 1)"); +} + +/// A triangle with fog coordinates `c` (eye distance) at its corners and w of 1 there, so a vertex's +/// words are (c, 1). +fn fogged(c: [f32; 3]) -> [WVertex; 3] { + let mut t = big(WHITE, 0); + for (v, c) in t.iter_mut().zip(c) { + v.fog = [c, 1.0]; + } + t +} + +/// Fog blends toward the fog colour by the factor of the fragment's coordinate; with fog off the +/// fragment is untouched. The fragment is white, the fog colour black. +#[test] +fn fog_darkens_a_fragment_by_its_distance() { + use super::raster::fog::Fog; + use crate::dev::kona::defs::gl; + let red_of = |s: &PixelStore, x, y| s.read(x, y, 1) >> 16 & 0xff; + // Constant coordinate 5, linear from 0 to 10: half way. + let on = state(|d| d.fog = Fog::new(true, gl::LINEAR, 1.0, 0.0, 10.0, [0.0; 3])); + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &on, &[fogged([5.0; 3])]); + assert_eq!(s.read(5, 5, 1), 0xff80_8080, "half way to black, alpha untouched"); + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &state(|_| {}), &[fogged([5.0; 3])]); + assert_eq!(s.read(5, 5, 1), 0xffff_ffff, "fog off: untouched"); + // The coordinate is interpolated across the triangle: 0 at the corner (0.5, 0.5), 20 at the others, + // so the fragment darkens with distance from the first corner and is black past an end of 10. + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &on, &[fogged([0.0, 20.0, 20.0])]); + assert!(red_of(&s, 1, 1) > 0xe0 && red_of(&s, 8, 1) < red_of(&s, 4, 1) && red_of(&s, 4, 1) < red_of(&s, 2, 1), "darker with distance"); + assert_eq!(red_of(&s, 30, 5), 0, "past the end it is all fog"); + // The exponential curves, density 0.2 at distance 5: exp(-1) and exp(-1) again. + for mode in [gl::EXP, gl::EXP2] { + let ds = state(|d| d.fog = Fog::new(true, mode, 0.2, 0.0, 1.0, [0.0; 3])); + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &ds, &[fogged([5.0; 3])]); + assert_eq!(red_of(&s, 5, 5), 94, "255 exp(-1) = 93.8, mode {mode:#x}"); + } + // The fog colour replaces the fragment's colour where the factor is 0. + let blue = state(|d| d.fog = Fog::new(true, gl::LINEAR, 1.0, 0.0, 10.0, [0.0, 0.0, 1.0])); + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &blue, &[fogged([50.0; 3])]); + assert_eq!(s.read(5, 5, 1), 0xff00_00ff); +} + +/// The coordinate is perspective-correct: it is the quotient of two screen-linear words, so a vertex of +/// larger w pulls the coordinate toward the others, not along the screen. +#[test] +fn the_fog_coordinate_is_interpolated_perspective_correct() { + use super::raster::fog::Fog; + use crate::dev::kona::defs::gl; + let ds = state(|d| d.fog = Fog::new(true, gl::LINEAR, 1.0, 0.0, 100.0, [0.0; 3])); + let mut st = board((0, 32)); + let s = st.store.clone(); + // Corner 0 at distance 1 (w 1), the others at distance 9 (w 9): words (c / w, 1 / w). + let mut t = big(WHITE, 0); + t[0].fog = [1.0, 1.0]; + t[1].fog = [1.0, 1.0 / 9.0]; + t[2].fog = [1.0, 1.0 / 9.0]; + draw(&mut st, &ds, &[t]); + // At pixel (20, 1) the weights are .475 on the near corner and .525 on the far two: 1/w is .533 and + // c/w is 1, so the distance is 1.875 and linear fog over 0..100 leaves 98.1 percent (250 of 255). + // A screen-linear coordinate would be 5.2 and leave 94.8 percent (242). + let mid = (s.read(20, 1, 1) >> 16 & 0xff) as i32; + assert!((mid - 250).abs() <= 2, "{mid}"); +} + +/// A logic operation combines the written word with the buffer's, and a colour +/// mask keeps the masked components of the buffer. +#[test] +fn a_logic_operation_and_a_colour_mask_write_what_they_allow() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let xor = state(|d| d.logic = Some(LogicOp::Xor)); + fill(&mut st, &xor, 0xff12_3456); + draw(&mut st, &xor, &[big([8191, 8191, 8191, 8191], 0)]); + assert_eq!(s.read(5, 5, 1), 0xffff_ffff ^ 0xff12_3456, "white xor the buffer"); + let masked = state(|d| d.colour_mask = [true, false, false, true]); + fill(&mut st, &masked, 0x0011_2233); + draw(&mut st, &masked, &[big(RED, 0)]); + assert_eq!(s.read(5, 5, 1), 0xffff_2233 & !0x0000_ff00 & !0x0000_00ff | 0x0000_2233 & 0xffff, "red and alpha written, green and blue kept"); +} + +/// A clear writes only what its masks allow. +#[test] +fn a_clear_obeys_the_write_masks() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let ds = state(|d| (d.stencil_word, d.stencil_bits) = (4, 8)); + fill(&mut st, &ds, 0x0011_2233); + let p = Prims { clears: vec![ClearReq { rect: ds.clip, colour: Some(0xffaa_bbcc), colour_mask: [true, false, false, true], stencil: Some(0xff), stencil_mask: 0x0f, ..Default::default() }], state: Some(ds), ..Default::default() }; + st.prims = p; + st.flush(); + assert_eq!(s.read(3, 3, 1), 0xffaa_2233, "alpha and red cleared, green and blue kept"); + assert_eq!(s.read(3, 3, 4), 0x0f, "the stencil's low nibble"); + // A depth mask of off skips the depth clear. + let ds = state(|_| {}); + let p = Prims { clears: vec![ClearReq { rect: ds.clip, depth: Some(0x1234_5600), depth_mask: false, ..Default::default() }], state: Some(ds), ..Default::default() }; + st.prims = p; + st.flush(); + assert_eq!(s.read(3, 3, 3), 0); +} + +/// A board whose ID plane holds ID 5 on the left half of the screen and ID 6 on the right. +fn two_windows() -> KonaBackend { + let st = board((0, 32)); + st.store.ensure(DID_PLANE); + for y in 0..N { + for x in 0..N { + st.store.write(x, y, DID_PLANE, (if x < N / 2 { 5 } else { 6 }) << 2); + } + } + st +} + +/// With the enable set, a draw writes only the pixels its window ID owns: ID 5 of +/// ordinary window 5 across a triangle that spans pixels holding 5 and 6. +#[test] +fn ownership_drops_the_fragments_of_other_windows() { + let mut st = two_windows(); + let s = st.store.clone(); + let ds = state(|d| d.ownership = Some(Ownership::encode(5).0)); + draw(&mut st, &ds, &[big(RED, 0)]); + let lit = pixels(&s, 1); + assert!(!lit.is_empty() && lit.iter().all(|&(x, _, _)| x < N / 2), "only ID 5's half"); + // The same triangle with the test off covers both halves. + let mut st = two_windows(); + let s = st.store.clone(); + draw(&mut st, &state(|_| {}), &[big(RED, 0)]); + assert!(pixels(&s, 1).iter().any(|&(x, _, _)| x >= N / 2)); + // A mask of 0 compares no bit, so every pixel passes (the polarity the model infers). + let mut st = two_windows(); + let s = st.store.clone(); + let open = state(|d| d.ownership = Some(Ownership { reference: 5, mask: 0 })); + draw(&mut st, &open, &[big(RED, 0)]); + assert!(pixels(&s, 1).iter().any(|&(x, _, _)| x >= N / 2)); +} + +/// The ID plane holds the painted value, the main ID in bits 7:2 and an overlay group in bits 12:8, so a window +/// in the group range owns the pixels painted with its group, whatever main ID is beneath: group 4 is the +/// middle band of `two_windows` here. +#[test] +fn a_group_range_window_owns_the_pixels_painted_with_its_group() { + let mut st = two_windows(); + for y in 0..N { + for x in N / 4..3 * N / 4 { + let v = st.store.read(x, y, DID_PLANE); + st.store.write(x, y, DID_PLANE, v | 4 << 8); + } + } + let s = st.store.clone(); + draw(&mut st, &state(|d| d.ownership = Some(Ownership::encode(0x44).0)), &[big(RED, 0)]); + let lit = pixels(&s, 1); + assert!(!lit.is_empty() && lit.iter().all(|&(x, _, _)| (N / 4..3 * N / 4).contains(&x)), "group 4's band, across IDs 5 and 6"); + let mut st2 = two_windows(); + let s2 = st2.store.clone(); + draw(&mut st2, &state(|d| d.ownership = Some(Ownership::encode(0x40).0)), &[big(RED, 0)]); + assert!(pixels(&s2, 1).iter().any(|&(x, _, _)| x < N / 2) && pixels(&s2, 1).iter().any(|&(x, _, _)| x >= N / 2), "group 0 is every pixel with no overlay painted"); +} + +/// A window ID of all ones owns no pixel while the test is on, for draws and clears. +#[test] +fn an_all_ones_window_id_writes_nothing() { + let mut st = two_windows(); + let s = st.store.clone(); + let ds = state(|d| d.ownership = Some(Ownership::encode(-1).0)); + draw(&mut st, &ds, &[big(RED, 0)]); + clear(&mut st, &ds, Some(0xff11_2233), None); + assert!(pixels(&s, 1).is_empty()); +} + +/// A GL clear obeys the test too, per pixel, for colour, depth and stencil. +#[test] +fn a_clear_writes_only_the_pixels_the_window_owns() { + let mut st = two_windows(); + let s = st.store.clone(); + let ds = state(|d| { + d.ownership = Some(Ownership::encode(6).0); + (d.stencil_word, d.stencil_bits) = (4, 8); + }); + let p = Prims { clears: vec![ClearReq { rect: ds.clip, colour: Some(0xff11_2233), depth: Some(0x1000_0000), stencil: Some(7), ..Default::default() }], state: Some(ds), ..Default::default() }; + st.prims = p; + st.flush(); + assert_eq!((s.read(5, 5, 1), s.read(5, 5, 3), s.read(5, 5, 4)), (0, 0, 0), "ID 5 is not the window's"); + assert_eq!((s.read(50, 5, 1), s.read(50, 5, 3), s.read(50, 5, 4)), (0xff11_2233, 0x1000_0000, 7), "ID 6 is"); +} + +/// The lit pixels of triangles drawn in white each into its own store. +fn coverage(tris: &[[WVertex; 3]]) -> Vec> { + tris.iter() + .map(|t| { + let mut st = board((0, 32)); + let s = st.store.clone(); + draw(&mut st, &state(|_| {}), &[*t]); + pixels(&s, 1).into_iter().map(|(x, y, _)| (x, y)).collect() + }) + .collect() +} + +/// Triangles that share an edge cover every pixel once: a convex polygon fanned +/// from one vertex or from another is lit the same, and no pixel is lit twice. +#[test] +fn shared_edges_are_watertight() { + let mut seed = 0x2545_f491_4f6c_dd1du64; + let mut next = move || { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + (seed >> 11) as f32 / (1u64 << 53) as f32 + }; + for _ in 0..200 { + // Five corners on a circle about (32, 32), each in its own fifth of it, so the polygon is convex. + let pts: Vec<(f32, f32)> = (0..5) + .map(|k| { + let a = (k as f32 + 0.15 + 0.7 * next()) * std::f32::consts::TAU / 5.0; + (32.0 + 20.0 * a.cos(), 32.0 + 20.0 * a.sin()) + }) + .collect(); + let fan = |root: usize| -> Vec<[WVertex; 3]> { + (1..4) + .map(|i| { + let at = |j: usize| pts[(root + j) % 5]; + tri([at(0), at(i), at(i + 1)], [[8191; 4]; 3], [0; 3]) + }) + .collect() + }; + let mut sets: Vec> = Vec::new(); + for root in [0, 2] { + let parts = coverage(&fan(root)); + let mut all: Vec<(u32, u32)> = parts.iter().flatten().copied().collect(); + let n = all.len(); + all.sort(); + all.dedup(); + assert_eq!(all.len(), n, "a pixel lit by two triangles of one fan, polygon {pts:?}"); + sets.push(all); + } + assert_eq!(sets[0], sets[1], "the fans from two roots light different pixels, polygon {pts:?}"); + } +} + +/// A triangle far larger than the screen keeps its slopes and so its colour +/// where it crosses the screen: set-up does not cut it in window space, and +/// values its planes at the visible corner. +#[test] +fn a_guard_band_triangle_keeps_its_colour_at_the_screen_edge() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let (r, g, b) = ([8191, 0, 0, 8191], [0, 8191, 0, 8191], [0, 0, 8191, 8191]); + // Red at (-1000, -1000), green at (3000, -1000), blue at (-1000, 3000): the colour at (x, y) + // is (1 - u - w, u, w) with u = (x + 1000) / 4000 and w = (y + 1000) / 4000. + draw(&mut st, &state(|_| {}), &[tri([(-1000.0, -1000.0), (3000.0, -1000.0), (-1000.0, 3000.0)], [r, g, b], [0; 3])]); + for (x, y) in [(0u32, 0u32), (63, 0), (0, 63), (63, 63), (30, 20)] { + let word = s.read(x, y, 1); + let (u, w) = ((x as f64 + 1000.5) / 4000.0, (y as f64 + 1000.5) / 4000.0); + let want = [(1.0 - u - w, 16u32), (u, 8), (w, 0)]; + for (weight, shift) in want { + let got = (word >> shift & 0xff) as f64; + assert!((got - weight * 255.0).abs() <= 1.5, "pixel ({x}, {y}): channel {shift} is {got}, wanted {}", weight * 255.0); + } + } +} + +/// A triangle crossing a small viewport inside a large window draws only +/// inside the viewport; a clear is cut to the clip rectangle alone. +#[test] +fn a_triangle_crossing_a_small_viewport_draws_only_inside_it() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let ds = state(|d| d.viewport = Rect { x0: 20, y0: 10, x1: 40, y1: 30 }); + draw(&mut st, &ds, &[tri([(0.5, 0.5), (200.5, 0.5), (0.5, 200.5)], [WHITE; 3], [0; 3])]); + let lit = pixels(&s, 1); + assert!(!lit.is_empty() && lit.iter().all(|&(x, y, _)| (20..40).contains(&x) && (10..30).contains(&y)), "{lit:?}"); + assert_eq!(lit.len(), 20 * 20, "the viewport is wholly inside the triangle"); + clear(&mut st, &ds, Some(0x0102_0304), None); + assert_eq!(pixels(&s, 1).len(), (N * N) as usize, "the viewport does not apply to a clear"); +} + +/// The triangles' counts: a sliver with area and no sample, one outside the +/// clip rectangle, and one of no area. +#[test] +fn skipped_triangles_are_counted_apart() { + let mut st = board((0, 32)); + let s = st.store.clone(); + let ds = state(|d| d.clip = Rect { x0: 0, y0: 0, x1: 10, y1: 10 }); + draw( + &mut st, + &ds, + &[ + tri([(1.0, 1.0), (2.0, 2.0), (3.0, 3.0)], [[0; 4]; 3], [0; 3]), + tri([(1.625, 1.5), (1.875, 1.5), (1.625, 5.5)], [[0; 4]; 3], [0; 3]), + tri([(30.5, 30.5), (40.5, 30.5), (30.5, 40.5)], [[0; 4]; 3], [0; 3]), + ], + ); + let c = st.raster; + assert_eq!((c.triangles, c.no_area, c.no_sample, c.off_target), (3, 1, 1, 1)); +} + +/// The per-pixel module has no `f64`: the edge functions are `i64` and the +/// planes `f32`, so a GPU kernel could reproduce them. This reads the file's +/// text, so its limits are the text's: it finds the type's name and a float +/// literal with no `f32` suffix (which a context could turn into a double), +/// and any write other than the one `store` call or a compound atomic; it does +/// not see a write made by a helper in another module, or a double that comes +/// from a function's return type. +#[test] +fn the_per_pixel_module_has_no_f64() { + let src = include_str!("../raster/pixel.rs"); + assert!(!src.contains(concat!("f", "64")), "pixel.rs uses a double"); + assert!(include_str!("../raster/setup.rs").contains(concat!("f", "64")), "set-up is where the doubles are"); + // Every float literal carries its type. Comments are skipped. + for (n, line) in src.lines().enumerate() { + let code = line.split("//").next().unwrap_or(""); + let b: Vec = code.chars().collect(); + for i in 1..b.len().saturating_sub(1) { + let literal = b[i] == '.' && b[i - 1].is_ascii_digit() && b[i + 1].is_ascii_digit(); + if literal { + let end = (i + 1..b.len()).find(|&j| !(b[j].is_ascii_digit() || b[j] == '_' || b[j] == 'e')).unwrap_or(b.len()); + let suffix: String = b[end..].iter().take(3).collect(); + assert_eq!(suffix, "f32", "pixel.rs line {}: a float literal with no f32 suffix: {line}", n + 1); + } + } + } + // Every write is `WordArray::store`, inside an unsafe block, and no other way into the planes. + for (n, line) in src.lines().enumerate().filter(|(_, l)| l.contains(".store(")) { + assert!(line.contains("unsafe"), "pixel.rs line {}: a write outside an unsafe block that says why: {line}", n + 1); + } + // Every `unsafe` block says why it is sound. + assert_eq!(src.matches("unsafe {").count(), src.matches("// SAFETY").count(), "an unsafe block with no SAFETY comment"); + for other in ["fetch_", "compare_exchange", "write_volatile", "ptr::write", "copy_nonoverlapping", ".swap(", ".write(", "mul_add"] { + assert!(!src.contains(other), "pixel.rs writes through {other}"); + } +} + +/// The files besides the per-pixel module that do per-fragment work keep its one-store-point property: +/// texel sampling, the sample patterns and the pixel operations write only through `WordArray::store` +/// inside an `unsafe` block that says why, and use none of the other ways into the planes. The two +/// that are float kernels (`texel.rs`, `multisample.rs`) also have no `f64`. `gl/pixels.rs` does: its +/// unpacking and packing of host images runs once a transfer, on `kona-hip` and not on a tile +/// worker, and takes 32-bit integer components and the raster position's double forms through doubles. +#[test] +fn the_other_per_fragment_files_keep_the_store_point_rule() { + for (name, src, no_f64) in [("fog.rs", include_str!("../raster/fog.rs"), true), ("smooth.rs", include_str!("../raster/smooth.rs"), true), ("texel.rs", include_str!("../raster/texel.rs"), true), ("multisample.rs", include_str!("../raster/multisample.rs"), true), ("gl/pixels.rs", include_str!("../gl/pixels.rs"), false)] { + // Tests in the files are not the kernel. + let src = src.split("#[cfg(test)]").next().unwrap_or(src); + if no_f64 { + assert!(!src.contains(concat!("f", "64")), "{name} uses a double"); + } + for (n, line) in src.lines().enumerate().filter(|(_, l)| l.contains(".store(")) { + assert!(line.contains("unsafe"), "{name} line {}: a write outside an unsafe block that says why: {line}", n + 1); + } + assert_eq!(src.matches("unsafe {").count(), src.matches("// SAFETY").count(), "{name}: an unsafe block with no SAFETY comment"); + for other in ["fetch_", "compare_exchange", "write_volatile", "ptr::write", "copy_nonoverlapping", ".swap(", ".write(", "mul_add"] { + assert!(!src.contains(other), "{name} writes through {other}"); + } + } +} diff --git a/src/dev/kona/back/mod.rs b/src/dev/kona/back/mod.rs new file mode 100644 index 000000000..4b21172c9 --- /dev/null +++ b/src/dev/kona/back/mod.rs @@ -0,0 +1,3449 @@ +//! The InfiniteReality pipe's drawing and display back end. +//! +//! `KonaBackend` takes the jobs the front end frames, keeps the pipe's +//! configuration (raster managers, texture memory per manager, managed area), +//! counts what it was sent and answers the `kona` monitor command. Behind it +//! are the commands that say what to draw, the tile scheduler, the pixel store +//! and the display. +//! +//! - `store`: the pixel store and the plane layout inside a pixel's words. +//! - `raster`: drawing into the store, the fill rule, and triangle set-up. +//! - `tiles`: the tile scheduler (slot ring, tile queues, workers, budget). +//! - `x`, `textport`, `gl`: the X server's integer path, the PROM's console and +//! the GL client's commands; `context`: the state each rendering context keeps. +//! - `display`: the tables the scan-out reads and the frame's composition. +//! - `bench` (an ignored test): the fill rate of the integer drawing path. +//! - `census`, `replay` (tests only): the jobs a command stream sends the back +//! end, and the replay of a recorded stream into a board. + +#[cfg(test)] +mod bench; +pub mod display; +pub mod raster; +pub mod tiles; +pub mod snapshot; +pub mod store; +pub mod context; +pub mod gl; +pub mod textport; +pub mod x; +#[cfg(test)] +mod golden; +#[cfg(test)] +pub mod census; +#[cfg(test)] +mod replay; +#[cfg(test)] +mod switch_tests; + + +use super::defs::*; +use super::ident::KonaConfig; +use super::handle::DisplayHandle; +use super::job::{BackPort, Grammar, Job, Reply}; +use display::clock::{Clock, DisplayThread, FRAME_PERIOD}; +use crate::gfx_display::GfxDisplay; +use display::screen::Display; +use display::tables::Tables; +use display::{ColourMap, Compositor}; +use store::layout::{BufferConfig, Layout, Refusal}; +use textport::Textport; +use context::Contexts; +use raster::setup::Shape; +use raster::Rect; +use tiles::{ClearJob, RasterJob, Tiles}; +use store::texture::{TextureStore, PAGE_WORDS}; +use store::PixelStore; +use super::stream; +use crate::devlog::LogModule; +use std::io::{self, Write}; +use std::sync::Arc; + +/// The kinds of job the counters tell apart. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum JobKind { + Textport, + Kernel, + Gl, + Data, + Bitmap, + Video, + Retrace, + Context, + Indirect, + DmaIn, + Dropped, + FirmwareStart, +} + +impl JobKind { + const ALL: [JobKind; 12] = [ + JobKind::Textport, + JobKind::Kernel, + JobKind::Gl, + JobKind::Data, + JobKind::Bitmap, + JobKind::Video, + JobKind::Retrace, + JobKind::Context, + JobKind::Indirect, + JobKind::DmaIn, + JobKind::Dropped, + JobKind::FirmwareStart, + ]; + + fn name(self) -> &'static str { + match self { + JobKind::Textport => "textport commands", + JobKind::Kernel => "kernel commands", + JobKind::Gl => "GL commands", + JobKind::Data => "data runs", + JobKind::Bitmap => "bitmaps", + JobKind::Video => "video commands", + JobKind::Retrace => "retrace requests", + JobKind::Context => "context changes", + JobKind::Indirect => "indirect stores", + JobKind::DmaIn => "DMA packets", + JobKind::Dropped => "dropped words", + JobKind::FirmwareStart => "firmware starts", + } + } + + fn of(job: &Job<'_>) -> JobKind { + match job { + Job::Command { grammar: Grammar::Textport, .. } => JobKind::Textport, + Job::Command { grammar: Grammar::Kernel, .. } => JobKind::Kernel, + Job::Command { grammar: Grammar::Gl, .. } => JobKind::Gl, + Job::Data { .. } => JobKind::Data, + Job::Bitmap { .. } => JobKind::Bitmap, + Job::Video { .. } => JobKind::Video, + Job::Retrace { .. } => JobKind::Retrace, + Job::Context(_) => JobKind::Context, + Job::Indirect { .. } => JobKind::Indirect, + Job::DmaIn { .. } => JobKind::DmaIn, + Job::Dropped { .. } => JobKind::Dropped, + Job::FirmwareStart { .. } => JobKind::FirmwareStart, + } + } +} + +/// Whether `selector` moves a region's pixels: a copy-down or a swap. +fn moves_pixels(selector: u32) -> bool { + matches!(selector, PIPE_MEMORY_COPY_DOWN | PIPE_MEMORY_SWAP_IN | PIPE_MEMORY_SWAP_OUT) +} + +/// The kernel's pipe-memory region commands the back end has seen, and the bytes +/// the swaps named. The front end frames them, answers them and moves the bytes +/// (a swap-out writes zeros of the region's size, a swap-in reads the buffer and +/// discards it); nothing in a pool lives in the back end yet, so it counts what +/// it was told. The texture and list steps give a swap-out the model's own +/// encoding of the region (the hardware's is unknown, and nothing the guest does +/// interprets the bytes between the two, inferred). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct RegionCounts { + placed: u64, + copy_downs: u64, + swapped_out: u64, + swapped_in: u64, + bytes_out: u64, + bytes_in: u64, +} + +impl RegionCounts { + /// Count a kernel command if it is a region command. A swap's size is its + /// size in granules times the granule times the copy count. + fn note(&mut self, selector: u32, args: &[u32]) { + let bytes = |a: &[u32]| a.get(2..5).map_or(0, |s| s.iter().map(|&w| w as u64).product()); + match selector { + PIPE_MEMORY_RESERVE => self.placed += 1, + PIPE_MEMORY_COPY_DOWN => self.copy_downs += 1, + PIPE_MEMORY_SWAP_OUT => { + self.swapped_out += 1; + self.bytes_out += bytes(args); + } + PIPE_MEMORY_SWAP_IN => { + self.swapped_in += 1; + self.bytes_in += bytes(args); + } + _ => {} + } + } +} + +/// A test double for the back end: it may answer a job (`Some` short-circuits +/// the real one), and it notes the lifecycle calls, which it takes in place of the +/// real back end's (its display thread is not started). Only tests install one. +#[cfg(test)] +pub(super) struct TestHook { + pub job: Box, &Job<'_>) -> Option + Send>, + pub events: Arc>>, + pub on_attach: Option>, +} + +/// A hook that answers every job as nothing, as a board with no drawing does: +/// retrace requests complete at once, DMA rows are unclaimed. +#[cfg(test)] +pub(super) fn null_hook() -> TestHook { + TestHook { + job: Box::new(|port, job| { + Some(match job { + Job::Retrace { tag } => { + port.retrace_done(*tag); + Reply::Done + } + Job::DmaIn { .. } => Reply::Unclaimed, + _ => Reply::Done, + }) + }), + events: Arc::default(), + on_attach: None, + } +} + +/// The InfiniteReality back end. +pub struct KonaBackend { + cfg: KonaConfig, + counts: [u64; JobKind::ALL.len()], + resets: u64, + /// Held between `attach` and `stop`, for the display thread once there is + /// one. + handle: Option, + /// What the display thread completes, and the thread while the board runs. + clock: Arc, + thread: Option, + /// The window the board shows the guest, which the machine keeps a clone + /// of (`KonaBackend::display`). + display: Arc, + /// The textport's colour map, copied by the display once a frame. + map: Arc>, + /// The X server's colour map and display-ID table, which the display + /// reads a frame at a time. + tables: Arc>, + store: Arc, + textport: Textport, + /// The X path's state, one copy per rendering context. + contexts: Contexts, + regions: RegionCounts, + /// DAC cursor loads and refusals, as counts. The cursors themselves are in the display tables. + dac_cursor_loads: u64, + dac_cursor_refused: u64, + /// The display generator's channels, which the video commands set. + channels: display::channels::Channels, + /// What the texture commands did. + tex_counts: gl::texture::TexCounts, + pix_counts: gl::pixels::Counts, + query_counts: gl::query::Counts, + /// Test hooks: a back end test double that may answer a job, and the census tap. + #[cfg(test)] + pub(super) hook: Option, + #[cfg(test)] + pub(super) tap: Option<(Arc>, Arc)>, + /// The GL half of the token layer: assembly scratch and counts, and where its vertices go. + gl: gl::GlPath, + /// What the GL path made in one job, until set-up takes it. + prims: raster::Prims, + /// The tile scheduler, made when the first raster job comes so a board that + /// never draws GL has no worker threads, and what set-up counted. + tiles: Option, + raster: RasterCounts, + /// Buffer-configuration commands that got a layout, and those refused by + /// reason (indexed by `Refusal`). + layouts: u64, + refused: [u64; Refusal::ALL.len()], + /// Buffer-configuration commands that were not ten words, and whether that + /// has been logged. + malformed: u64, + malformed_logged: bool, + /// The layout of the last buffer configuration that got one. + last_layout: Option, + /// A layout `buffer_config` made, for the job to hand the current context: + /// its level and whether the window is double buffered. + pending_config: Option<(i32, bool, Layout, u32)>, +} + +/// Where on the return page a video Get's reply goes. +const VIDEO_REPLY_OFFSET: u32 = 8; + +/// Where on the return page a pixel read's bytes go: after the ready word and +/// the word the reader skips. +const PIXEL_REPLY_OFFSET: u32 = 8; + +/// Write a finished read to its host buffer a line at a time, after checking +/// that every line has somewhere to go, so a read is written whole or not at +/// all. False if a line is unmapped or fails on the bus. +fn write_lines(port: &mut BackPort<'_>, done: &x::pixel::FeedDone) -> bool { + let lines = || done.data.chunks(done.line).enumerate().map(|(i, line)| (done.io + i as u64 * done.pitch, line)); + lines().all(|(at, line)| port.device_mapped(at, line.len())) && lines().all(|(at, line)| port.device_write(at, line)) +} + +impl KonaBackend { + pub fn new(cfg: KonaConfig) -> Self { + let map = Arc::new(parking_lot::Mutex::new(ColourMap::default())); + let store = Arc::new(PixelStore::with_texture(cfg.managed.0, cfg.managed.1, TextureStore::with_capacity_bytes(cfg.texture_bytes_per_rm()))); + let tables = Arc::>::default(); + let display = Arc::new(Display::detached()); + display.set_tables(tables.clone()); + KonaBackend { + store: store.clone(), + textport: Textport::with_map(map.clone()), + map, + tables, + display, + contexts: Contexts::default(), + regions: RegionCounts::default(), + dac_cursor_loads: 0, + dac_cursor_refused: 0, + channels: display::channels::Channels::new(cfg.rm_count), + tex_counts: Default::default(), + pix_counts: Default::default(), + query_counts: Default::default(), + #[cfg(test)] + hook: None, + #[cfg(test)] + tap: None, + gl: gl::GlPath::new(cfg.managed), + prims: raster::Prims::default(), + tiles: None, + raster: RasterCounts::default(), + cfg, + counts: [0; JobKind::ALL.len()], + resets: 0, + handle: None, + clock: Arc::default(), + thread: None, + layouts: 0, + refused: [0; Refusal::ALL.len()], + malformed: 0, + malformed_logged: false, + last_layout: None, + pending_config: None, + } + } + + /// A buffer configuration: lay its planes out in a pixel's words, within + /// the host's budget, or count why not. The token layer will keep the + /// layout per context; until then the last one is shown by `kona status`. + fn buffer_config(&mut self, args: &[u32]) { + let Ok(words) = <&[u32; BufferConfig::WORDS]>::try_from(args) else { + self.malformed += 1; + if !std::mem::replace(&mut self.malformed_logged, true) { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: buffer configuration of {} words, not {}; refused", args.len(), BufferConfig::WORDS); + } + return; + }; + let config = BufferConfig::from_words(words); + match Layout::new(&config, 0, self.cfg.record_words()) { + Ok(l) => { + self.layouts += 1; + self.pending_config = Some((config.level, config.double_buffer, l.clone(), config.samples)); + self.last_layout = Some(l); + } + Err(why) => self.refused[why as usize] += 1, + } + } + + /// The window this board shows the guest, to hand to the machine so + /// `get_display` can return it. + pub fn display(&self) -> Arc { + self.display.clone() + } + + /// Parse the small state over throwaway objects, to see that it parses, changing nothing. + pub fn check_small(s: &mut S) { + Tables::default().visit(s); + Textport::default().visit(s); + Contexts::default().visit(s); + display::channels::Channels::new(0).visit(s); + } + + /// Tell the display whether channel 0, the frame it shows, is blanked: by its own blanking or by + /// a temporary blank of every channel. + fn sync_blank(&self) { + self.tables.lock().set_blank(self.channels.blanked || self.channels.channel[0].blanked); + } + + /// The small state the back end holds, as the stream module reads and writes it: the + /// display tables, the textport and every context. Refused on save when a context is + /// part way through a primitive or a pixel transfer. The counters, the tile scheduler and + /// what a frame is composed from are not saved. + pub fn visit_small(&mut self, s: &mut S) -> Result<(), String> { + if !s.loading() { + self.drain(); + if self.contexts.busy() { + return Err("a context is in the middle of a primitive or a pixel transfer".into()); + } + } + self.tables.lock().visit(s); + self.textport.visit(s); + self.contexts.visit(s); + self.channels.visit(s); + self.sync_blank(); + if s.loading() { + self.drain(); + self.last_layout = None; + self.pending_config = None; + } + Ok(()) + } + + /// The bulk state as chunks (see `snapshot`), after every queued raster job has run. + pub fn save_chunks(&mut self, cs: &crate::chunk_store::ChunkStore) -> std::io::Result> { + self.drain(); + snapshot::save(&self.store, &self.tables.lock(), cs) + } + + /// Read and check the bulk state from chunks, changing nothing. + pub fn prepare_chunks(&mut self, cs: &crate::chunk_store::ChunkStore, list: &[snapshot::KonaChunk]) -> Result { + self.drain(); + snapshot::prepare(&self.store, &self.tables.lock(), cs, list) + } + + /// Put what `prepare_chunks` read into the store. + pub fn apply_chunks(&mut self, p: snapshot::Prepared) -> Result<(), String> { + self.drain(); + snapshot::apply(&self.store, &mut self.tables.lock(), p) + } + + /// Restore the bulk state from chunks. + pub fn load_chunks(&mut self, cs: &crate::chunk_store::ChunkStore, list: &[snapshot::KonaChunk]) -> Result<(), String> { + let p = self.prepare_chunks(cs, list)?; + self.apply_chunks(p) + } + + /// Wire the machine's status-bar inputs into the display. + pub fn set_status(&self, status: Arc) { + // The same window, whether the machine took `display()` before or after. + self.display.set_status(status); + } + + /// A reader of the store that needs no access to the back end's state. + pub(super) fn pixels(&self) -> display::Pixels { + display::Pixels::new(self.store.clone(), self.map.clone(), self.tables.clone()) + } + + pub fn config(&self) -> &KonaConfig { + &self.cfg + } + + fn status(&self, w: &mut dyn Write) -> io::Result<()> { + let c = &self.cfg; + writeln!(w, "product {}", c.spec().label)?; + writeln!(w, "raster managers {}", c.rm_count)?; + writeln!(w, "texture per RM {} MiB", c.texture_bytes_per_rm() >> 20)?; + writeln!(w, "managed area {} x {}", c.managed.0, c.managed.1)?; + writeln!(w, "record bound {} words per pixel", c.record_words())?; + writeln!(w, "display handle {}", if self.handle.is_some() { "attached" } else { "detached" })?; + writeln!(w, "display {} frames, {} retraces completed, thread {}", self.clock.frames(), self.clock.completed(), if self.thread.is_some() { "running" } else { "stopped" })?; + writeln!(w, "pixel store {} word arrays of {} x {}", self.store.allocated().len(), self.store.width(), self.store.height())?; + writeln!(w, "textport colour {} raster {:?}, {} drawn, {} unmodelled", self.textport.colour(), self.textport.raster(), self.textport.drawn, self.textport.unmodelled)?; + let x = &self.contexts.current().x; + let gc = match x.gc() { + Ok(g) => format!("{:?} fg {:#x} bg {:#x} mask {:#x} gx {}", g.target, g.foreground, g.background, g.mask, g.gx), + Err(why) => format!("not decoded ({why:?})"), + }; + writeln!(w, "X contexts {} loaded; the last job's:", self.contexts.held())?; + writeln!(w, "X gc {gc}")?; + writeln!( + w, + "X state origin {:?}, {} clip rects ({} refused), scissor {:?}, {} runs dropped", + x.origin(), + x.clip_rects().len(), + x.clips_refused, + x.scissor(), + x.runs_dropped + )?; + writeln!(w, "X stipple {}", if x.stipple.is_loaded() { "loaded" } else { "none loaded" })?; + let f = &x.fills; + writeln!(w, "X fills {} drawn, refused: {} context, {} target, {} destination", f.drawn, f.bad_context, f.other_target, f.bad_destination)?; + let p = x.pixel_counts(); + writeln!( + w, + "X pixels {} setups, {} writes ({} by DMA), {} copies, {} reads, {} finishes", + p.setups, p.writes, p.dma_writes, p.copies, p.reads, p.finishes + )?; + writeln!( + w, + " refused {} context, {} target, {} destination, {} window copies, {} reads answered with zeros ({} frame-buffer word)", + p.bad_context, p.other_target, p.bad_destination, p.window_copies, p.reads_refused, p.fb_word_reads + )?; + writeln!(w, " rows {} short or left owing, {} with no write waiting", p.short, p.unexpected)?; + writeln!(w, " read brackets {} refused, {} with no completion posted (host buffer unwritable or no reason page)", p.bracket_refused, p.bracket_unposted)?; + let r = &self.regions; + writeln!( + w, + "memory regions {} placed, {} copied down, {} swapped out ({} bytes), {} swapped in ({} bytes)", + r.placed, r.copy_downs, r.swapped_out, r.bytes_out, r.swapped_in, r.bytes_in + )?; + writeln!(w, "display tables {}", self.tables.lock().describe())?; + let on: Vec = (0..display::channels::CHANNELS).filter(|&c| self.channels.channel[c].enabled).collect(); + let blanked: Vec = (0..display::channels::CHANNELS).filter(|&c| self.channels.channel[c].blanked).collect(); + writeln!(w, "video channels enabled {:?}, blanked {:?} (channel 0's, and all while temporarily blanked, are drawn black), frame {} x {}{}", on, blanked, self.channels.frame.0, self.channels.frame.1, if self.channels.blanked { ", all blanked" } else { "" })?; + let tables = self.tables.lock(); + let dac = tables.dac(); + writeln!(w, "DAC cursor {} loads, {} refused; held for channels {:?}; channel 0's is {}", self.dac_cursor_loads, self.dac_cursor_refused, dac.cursors().map(|c| c.channel).collect::>(), if tables.snapshot().has_dac_cursor() { "drawn" } else { "not drawn" })?; + writeln!(w, " the kernel's control value for the owning channel: {}", crate::dev::kona::defs::dac_cursor_control::describe(crate::dev::kona::defs::dac_cursor_control::KERNEL_OWNER))?; + for c in dac.cursors() { + writeln!(w, " {}", c.describe())?; + } + drop(tables); + let c = &self.tex_counts; + let t = self.store.texture(); + writeln!( + w, + "textures {} level loads ({} by DMA, {} refused, {} past memory), {} finishes, {} binds ({} refused), {} tokens not modelled; {} volume slice loads, {} volume binds; texture memory {} KiB held (peak {} KiB, ceiling {} KiB)", + c.loads, c.dma_loads, c.unsupported_load, c.full, c.finishes, c.binds, c.bind_refused, c.unmodelled, c.volume_loads, c.volume_binds, t.resident_pages() * (PAGE_WORDS / 256), t.peak_pages() * (PAGE_WORDS / 256), t.cap_pages() * (PAGE_WORDS / 256) + )?; + writeln!(w, "detail {} binds, {} filter tables; mode-1 base binds: {} forced linear (no matching detail bind), {} with a detail image found, {} without", c.detail_binds, c.filter_tables, c.detail_mismatched, c.detail_resolved, c.detail_unresolved)?; + let g = &self.gl.counts; + let t = self.tiles.as_ref().map(|t| t.stats()).unwrap_or_default(); + writeln!( + w, + "GL path {} commands, {} vertices in {} primitives, {} draws handed over; {} clipped ({} away), {} culled, {} with no window position, {} errors", + g.commands, g.vertices, g.primitives, self.raster.draws, g.clipped, g.clipped_away, g.culled, g.no_window_position, g.errors + )?; + let r = &self.raster; + writeln!( + w, + "raster stage {} triangles ({} with no area, {} with no sample, {} off the target), {} clears; {} points and {} lines handed over as triangles ({} of no length); {} workers, {}-pixel tiles; {} drains, {} with work queued{}", + r.triangles, r.no_area, r.no_sample, r.off_target, r.clears, g.points, g.lines, g.empty_lines, self.cfg.threads, self.cfg.tile, t.drains, t.busy_drains, if self.is_poisoned() { ", POISONED" } else { "" } + )?; + if let Some(t) = &self.tiles { + let s = t.stats(); + writeln!(w, " scheduler {} jobs, {} tile runs, at most {} pixels in flight, producer waited {} times", s.submitted, s.tile_runs, s.peak_cost, s.waits)?; + } + if !g.unmodelled.is_empty() || !g.caps.is_empty() { + let list: Vec = g.unmodelled.iter().map(|(s, n)| format!("{s:#x} x {n}")).chain(g.caps.iter().map(|(c, n)| format!("enable/disable of {c:#x} x {n}"))).collect(); + writeln!(w, " not taken {}", list.join(", "))?; + } + writeln!(w, "layouts {} made", self.layouts)?; + if self.malformed != 0 { + writeln!(w, " refused {} x not {} words", self.malformed, BufferConfig::WORDS)?; + } + for r in Refusal::ALL { + let n = self.refused[r as usize]; + if n != 0 { + writeln!(w, " refused {n} x {r:?}")?; + } + } + if let Some(l) = &self.last_layout { + writeln!(w, " last colour {:?} depth {:?} stencil {:?} multisample {:?} accumulation {:?} ({} words)", l.colour, l.depth, l.stencil, l.multisample, l.accumulation, l.words())?; + } + writeln!(w, "resets {}", self.resets) + } + + /// What the GL path counted: the census of vertices, assembly, the + /// texture-coordinate forms and what it did not take. + fn gl_census(&self, w: &mut dyn Write) -> io::Result<()> { + let g = &self.gl.counts; + writeln!(w, "commands {}", g.commands)?; + writeln!(w, "begins by mode {:?} (points, lines, loop, strip, triangles, tri strip, fan, quads, quad strip, polygon)", g.begins)?; + writeln!(w, "vertices {} in {} primitives; {} outside a begin", g.vertices, g.primitives, g.stray)?; + writeln!(w, "vertex census {} inside the window, {} outside it, {} outside the volume, {} behind the eye (counted only in tests)", g.inside, g.outside_window, g.outside_volume, g.behind)?; + writeln!(w, "texcoord forms 1f {}, 2f {}, 2s {}, 3f {}, 4f {}", g.texcoord[0], g.texcoord[1], g.texcoord[2], g.texcoord[3], g.texcoord[4])?; + writeln!(w, "clipping {} clipped ({} away), {} culled, {} with no window position", g.clipped, g.clipped_away, g.culled, g.no_window_position)?; + writeln!(w, "handed over {} draws, {} triangles", self.raster.draws, self.raster.triangles)?; + let r = &self.raster; + writeln!(w, "raster stage {} triangles ({} no area, {} no sample, {} off the target), {} clears; {} points and {} lines", r.triangles, r.no_area, r.no_sample, r.off_target, r.clears, g.points, g.lines)?; + writeln!(w, "multisample {} draws ({} with alpha to mask, {} with a sample mask), by the model's own rules", r.multisampled, r.alpha_to_mask, r.sample_mask)?; + let c = &self.tex_counts; + writeln!(w, "textures {} level loads ({} by DMA, {} detail image loads, {} volume slice loads, {} refused, {} past memory), {} finishes, {} binds ({} of volumes, {} refused), {} tokens not modelled", c.loads, c.dma_loads, c.detail_loads, c.volume_loads, c.unsupported_load, c.full, c.finishes, c.binds, c.volume_binds, c.bind_refused, c.unmodelled)?; + let t = self.store.texture(); + writeln!(w, "texture memory {} KiB held in {} pages, peak {} KiB, ceiling {} KiB", t.resident_pages() * (PAGE_WORDS / 256), t.resident_pages(), t.peak_pages() * (PAGE_WORDS / 256), t.cap_pages() * (PAGE_WORDS / 256))?; + writeln!(w, "texel read-backs {} ({} in a bracket); answered with zeros: {} for a format or type the packer does not know, {} for a level not loaded; {} too big; {} with nowhere to answer", c.reads, c.reads_bracketed, c.read_unsupported, c.read_missing, c.read_oversize, c.read_unposted)?; + if let Some(l) = &self.last_layout { + writeln!(w, "last layout colour {:?} depth {:?} stencil {:?} multisample {:?} accumulation {:?}", l.colour, l.depth, l.stencil, l.multisample, l.accumulation)?; + } + writeln!(w, "pixel operations {}", self.pix_counts.render())?; + writeln!(w, "queries {}", self.query_counts.render())?; + for (s, n) in &g.unmodelled { + writeln!(w, "not taken {s:#x} x {n}")?; + } + for (c, n) in &g.caps { + writeln!(w, "capability {c:#x} (enable or disable) not read x {n}")?; + } + Ok(()) + } + + fn counters(&self, w: &mut dyn Write) -> io::Result<()> { + for k in JobKind::ALL { + writeln!(w, "{:18}{}", k.name(), self.counts[k as usize])?; + } + Ok(()) + } +} + +/// What the front end calls on the back end: a job, and the board's lifecycle. +impl KonaBackend { + pub fn job(&mut self, port: &mut BackPort<'_>, job: Job<'_>) -> Reply { + #[cfg(test)] + { + if let Some((census, at)) = &self.tap { + census.lock().tally(&job, at.load(std::sync::atomic::Ordering::Relaxed)); + } + if let Some(hook) = &mut self.hook { + if let Some(reply) = (hook.job)(port, &job) { + return reply; + } + } + } + self.handle_job(port, job) + } + + /// The board is starting: spawn the display thread, which completes retrace + /// requests and raises the video interrupt through `handle`. + pub fn attach(&mut self, handle: DisplayHandle) { + #[cfg(test)] + if let Some(hook) = &mut self.hook { + hook.events.lock().push("attach"); + if let Some(f) = &mut hook.on_attach { + f(handle); + } + return; + } + let sink = Compositor::new(self.store.clone(), textport::COLOUR_WORD, self.map.clone(), self.tables.clone(), self.display.clone()); + self.thread = Some(DisplayThread::spawn(self.clock.clone(), handle.clone(), FRAME_PERIOD, sink)); + self.handle = Some(handle); + } + + pub fn reset(&mut self) { + #[cfg(test)] + if let Some(hook) = &self.hook { + hook.events.lock().push("reset"); + return; + } + self.resets += 1; + self.clock.cancel(); + // Jobs queued before the reset finish first: they write the store it clears. + self.drain(); + self.store.reset(); + self.textport.reset(); + self.tables.lock().reset(); + self.contexts.reset(); + self.regions = RegionCounts::default(); + self.dac_cursor_loads = 0; + self.dac_cursor_refused = 0; + self.channels.reset(); + self.sync_blank(); + self.tex_counts = Default::default(); + self.pix_counts = Default::default(); + self.query_counts = Default::default(); + #[cfg(test)] + let census = self.gl.census; + self.gl = gl::GlPath::new(self.cfg.managed); + #[cfg(test)] + { + self.gl.census = census; + } + self.raster = Default::default(); + self.last_layout = None; + self.pending_config = None; + } + + pub fn stop(&mut self) { + #[cfg(test)] + if let Some(hook) = &self.hook { + hook.events.lock().push("stop"); + return; + } + // The tile workers run what is queued, then join; the display thread joins after. + self.tiles = None; + // As VPro's stop does: a stop from the main thread must leave the + // renderer to the main thread (the macOS hang `DisplayStop` exists for), + // so the request is made before the display thread, which releases the + // renderer as its last act, is joined. + self.display.display_stop().request(); + // The display thread joins first: it may be inside a handle call. + self.thread = None; + self.handle = None; + self.clock.cancel(); + } + + pub fn quiesce(&mut self) { + #[cfg(test)] + if let Some(hook) = &self.hook { + hook.events.lock().push("quiesce"); + return; + } + self.drain(); + self.clock.quiesce(self.thread.is_some()); + } + + pub fn resume(&mut self) { + #[cfg(test)] + if let Some(hook) = &self.hook { + hook.events.lock().push("resume"); + return; + } + self.clock.resume(); + } + + pub fn monitor(&mut self, args: &[&str], w: &mut dyn Write) -> io::Result { + match args { + ["status"] => self.status(w)?, + ["counters"] => self.counters(w)?, + _ if self.pixels().read(args, w)? => {} + ["gl"] => self.gl_census(w)?, + #[cfg(test)] + ["gl", "census"] => { + self.gl.census = true; + writeln!(w, "kona: GL census on: every vertex is also measured against the window")?; + } + ["gl", "clear"] => { + self.gl.counts = Default::default(); + self.raster = Default::default(); + } + ["help"] | [] => writeln!(w, "kona: status|counters|hash|pix X Y|shot FILE.png|gl [clear]")?, + _ => return Ok(false), + } + Ok(true) + } +} + +/// Whether `job` reads or writes the pixel store without taking a surface, and +/// so has to wait for the queued raster jobs itself: a swap, a retrace, a GL +/// finish and the region moves. Anything that draws or reads pixels gets its +/// `Surface` from `raster::surface`, which waits. +fn drains_first(job: &Job<'_>) -> bool { + match job { + Job::Command { grammar: Grammar::Gl, selector, .. } => *selector == FINISH_DRAIN_PIPE_AND_SIGNAL_COMPLETION, + Job::Retrace { .. } => true, + Job::Command { grammar: Grammar::Kernel, selector, args, .. } => { + moves_pixels(*selector) + // A swap, the display-mode token with a window. + || (*selector == DISPLAY_MODE_BYTE && args.first().is_some_and(|&window| window != 0)) + } + _ => false, + } +} + +/// What set-up counted, over the draws handed to it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RasterCounts { + pub draws: u64, + pub triangles: u64, + pub no_area: u64, + /// Triangles with area that no pixel's sample falls inside. + pub no_sample: u64, + pub clears: u64, + /// Triangles and clears that had no pixel to write: no colour or depth + /// target, or nothing inside the clip rectangle and viewport. + pub off_target: u64, + /// Draws and clears in a multisampled window, and of those the draws with alpha-to-mask and with + /// the sample mask on: each is a model choice the table does not settle (see `raster::multisample`). + pub multisampled: u64, + pub alpha_to_mask: u64, + pub sample_mask: u64, +} + +/// Slots in the job ring, and the budget in pixels of work in flight. +const RING: usize = 1 << 10; +const BUDGET: u64 = 1 << 17; +/// The tile size used when `[graphics] tile` is not a power of two. +const DEFAULT_TILE: u32 = 32; + +impl KonaBackend { + /// The tile scheduler, made now if it is not yet. + fn tiles(&mut self) -> &mut Tiles { + let (store, cfg, handle) = (&self.store, &self.cfg, &self.handle); + self.tiles.get_or_insert_with(|| { + // A tile size that is not a power of two is a configuration mistake: say so and use the default. + Tiles::new(store.clone(), cfg.tile, cfg.threads, RING, BUDGET, handle.clone()).unwrap_or_else(|why| { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: tile size {} refused ({why:?}); using {DEFAULT_TILE}", cfg.tile); + Tiles::new(store.clone(), DEFAULT_TILE, cfg.threads, RING, BUDGET, handle.clone()).expect("the default tile size is a power of two") + }) + }) + } + + /// Wait until every raster job has run. + fn drain(&mut self) { + if let Some(t) = &mut self.tiles { + let _ = t.drain(); + } + } + + /// A worker panicked: the board is dead. + pub(super) fn is_poisoned(&self) -> bool { + self.tiles.as_ref().is_some_and(|t| t.is_poisoned()) + } + + /// Allocate the arrays a job writes, before a worker can see it. + fn arrays(&self, ds: &raster::DrawState) { + ds.colour[..ds.colours as usize].iter().for_each(|&w| self.store.ensure(w as usize)); + if ds.depth_bits != 0 { + self.store.ensure(ds.depth_word as usize); + } + if ds.stencil_bits != 0 { + self.store.ensure(ds.stencil_word as usize); + } + if ds.ownership.is_some() { + self.store.ensure(store::DID_PLANE); + } + if let Some(ms) = &ds.samples { + [ms.colour, ms.depth, ms.stencil].iter().for_each(|&(first, n)| (first..first + n).for_each(|w| self.store.ensure(w as usize))); + } + } + + /// Set up what the GL path made in `self.prims` and submit it, then empty it. + fn flush(&mut self) { + let mut prims = std::mem::take(&mut self.prims); + if let Some(ds) = prims.state.filter(|_| !prims.tris.is_empty() || !prims.lines.is_empty() || !prims.aa.is_empty() || !prims.discs.is_empty() || !prims.clears.is_empty()) { + self.raster.draws += !(prims.tris.is_empty() && prims.lines.is_empty() && prims.aa.is_empty() && prims.discs.is_empty()) as u64; + self.raster.triangles += (prims.tris.len() + prims.lines.len() + prims.aa.iter().map(|p| p.n as usize - 2).sum::() + 2 * prims.discs.len()) as u64; + self.raster.clears += prims.clears.len() as u64; + if let Some(ms) = &ds.samples { + self.raster.multisampled += 1; + self.raster.alpha_to_mask += ms.alpha_to_mask as u64; + self.raster.sample_mask += (ms.cover != !0) as u64; + } + let drawable = !ds.clip.is_empty() && (ds.colours != 0 || (ds.depth_test && ds.depth_bits != 0) || (ds.stencil.enabled && ds.stencil_bits != 0)); + let state = self.tiles().state(ds); + if drawable { + self.arrays(&ds); + } + // A poisoned scheduler refuses a job; it stays poisoned and the board is dead, + // so a refusal carries nothing more. + let run = |b: &mut Self, t: &[raster::WVertex; 3], line: bool, shape: Shape<'_>| { + if !drawable { + b.raster.off_target += 1; + return; + } + match raster::setup::shaped(t, &ds, state, shape) { + Ok(mut job) => { + job.line = line; + let _ = b.tiles().submit(RasterJob::Tri(job)); + } + Err(raster::setup::Skip::NoArea) => b.raster.no_area += 1, + Err(raster::setup::Skip::NoSample) => b.raster.no_sample += 1, + Err(raster::setup::Skip::Outside) => b.raster.off_target += 1, + } + }; + for t in &prims.tris { + run(self, t, false, Shape::Plain); + } + for t in &prims.lines { + run(self, t, true, Shape::Plain); + } + // A smoothed polygon or line: its fan, each triangle weighing its pixels by the whole outline. + for poly in &prims.aa { + let v = &prims.aa_verts[poly.start as usize..(poly.start + poly.n) as usize]; + let Some(outline) = raster::setup::Outline::new(v, &ds) else { + self.raster.no_area += 1; + continue; + }; + for i in 1..v.len() - 1 { + // The edges from the fan's apex and back to it are inner, but at its two ends. + let inner = [i != 1, false, i != v.len() - 2]; + run(self, &[v[0], v[i], v[i + 1]], poly.line, Shape::Smooth { inner, outline: &outline }); + } + } + for d in &prims.discs { + let shape = Shape::Disc { x: d.x, y: d.y, r: d.r }; + let q = d.quad; + run(self, &[q[0], q[1], q[2]], false, shape); + run(self, &[q[0], q[2], q[3]], false, shape); + } + for c in &prims.clears { + let mut req = *c; + req.colour = req.colour.filter(|_| ds.colours != 0); + req.depth = req.depth.filter(|_| ds.depth_bits != 0); + req.stencil = req.stencil.filter(|_| ds.stencil_bits != 0); + if req.rect.is_empty() || (req.colour.is_none() && req.depth.is_none() && req.stencil.is_none()) { + self.raster.off_target += 1; + continue; + } + self.arrays(&ds); + let _ = self.tiles().submit(RasterJob::Clear(ClearJob { req, state })); + } + } + prims.clear(); + self.prims = prims; + } + + /// A texel draw's header: the image words that follow in the stream, sized from the + /// header and the unpack state, or the DMA packet that will carry them. + fn texel_draw(&mut self, port: &BackPort<'_>, args: &[u32]) -> Reply { + let tex = &mut self.contexts.select(port.context()).tex; + let Some(h) = gl::texture::Header::parse(args) else { return Reply::Done }; + let Some(n) = h.image_words(&tex.unpack) else { + self.tex_counts.unsupported_load += 1; + return Reply::Done; + }; + if h.dma { + tex.pending = <[u32; 20]>::try_from(args).ok(); + return Reply::Done; + } + if n == 0 { + self.drain(); + let tex = &mut self.contexts.select(port.context()).tex; + tex.load(self.store.texture(), args, &[], false, &mut self.tex_counts); + return Reply::Done; + } + Reply::Data(n) + } + + /// A texel draw's image words, in the stream, as bytes in the client's order. + fn texel_words(&mut self, port: &BackPort<'_>, args: &[u32], words: &[u32]) { + self.drain(); + let bytes: Vec = words.iter().flat_map(|w| w.to_be_bytes()).collect(); + self.contexts.select(port.context()).tex.load(self.store.texture(), args, &bytes, false, &mut self.tex_counts); + } + + /// A texel read-back (token 0x052): the named level's texels from the texture arena, packed in the + /// requested host format and type by the pixel packer under the pack record, with no header. There are two routes, + /// chosen by the client from the size of the read (established): a read of more than 0x3ff0 bytes comes inside a + /// select-feed bracket and returns by the bracket's output DMA; a smaller one is bare and returns on the return + /// page from offset 8, with the completion word written last. The client has no timeout on the bare form, so + /// every read is answered. A level that is not loaded answers zeros, and so does a format or type the packer does + /// not know (the whole reply area is zeroed, then the completion word), counted. A read too big to build, or a bare + /// one whose image would not fit the page, is refused and counted: only the completion word is posted, or the + /// bracket is refused. A read with no context or return page to answer on is counted. + /// + /// Luminance and luminance-alpha come from the sum of red, green and blue clamped at 1.0, as the pipe's colour path + /// makes them, unless the texture holds one channel, which is read as it is (the table: stage selection established, + /// the clamped sum inferred). Alpha is kept. + fn texel_read(&mut self, port: &mut BackPort<'_>, args: &[u32]) { + use crate::dev::kona::defs::{pixel::*, texture::storage_is_one_channel}; + self.tex_counts.reads += 1; + let c = self.contexts.select(port.context()); + let header = gl::texture::ReadHeader::parse(args); + let pack = c.gl.pix.pack; + let bracket = c.x.bracket_open(); + // What the read answers with: the packed bytes and the row they step by, or nothing to pack. + enum Answer { + Image(Vec, u32), + Zeros, + TooBig, + } + let sized = header.and_then(|h| { + let (_, stride) = gl::pixels::row_bytes(h.w as usize, h.format, h.ty, &pack)?; + // The bytes the packer will build, which include the pack record's skips: all of them the guest's. + let bytes = gl::pixels::packed_len(h.w as usize, h.h as usize, h.format, h.ty, 0, &pack).unwrap_or(u64::MAX); + Some((h, stride, bytes)) + }); + let answer = match sized { + None => { + self.tex_counts.read_unsupported += 1; + Answer::Zeros + } + Some((h, stride, bytes)) => { + let texels = h.w as u64 * h.h as u64; + if texels > gl::texture::MAX_READ_TEXELS || bytes > gl::texture::MAX_READ_BYTES || (!bracket && bytes > READ_REPLY_LIMIT as u64) { + self.tex_counts.read_oversize += 1; + Answer::TooBig + } else { + let texels = match c.tex.read_level(self.store.texture(), &h) { + Some(t) => t, + None => { + self.tex_counts.read_missing += 1; + vec![[0.0; 4]; texels as usize] + } + }; + let texels = match h.format { + FORMAT_LUMINANCE | FORMAT_LUMINANCE_ALPHA if !storage_is_one_channel(h.storage) => texels.into_iter().map(|[r, g, b, a]| [(r + g + b).min(1.0), 0.0, 0.0, a]).collect(), + FORMAT_LUMINANCE | FORMAT_LUMINANCE_ALPHA => texels.into_iter().map(|[r, _, _, a]| [r, 0.0, 0.0, a]).collect(), + _ => texels, + }; + let image = gl::pixels::Image { w: h.w as usize, h: h.h as usize, plane: gl::pixels::Plane::Colour(texels) }; + Answer::Image(gl::pixels::pack(&image, h.format, h.ty, 0, &pack).unwrap_or_default(), stride as u32) + } + } + }; + if bracket { + self.tex_counts.reads_bracketed += 1; + match (answer, header) { + (Answer::Image(bytes, row), Some(h)) => c.x.feed_texels(row, h.h, bytes), + // Nothing to describe to the bracket: a row of none refuses it. + _ => c.x.feed_texels(0, 0, Vec::new()), + }; + } else if port.context().is_some() && port.return_page_valid() { + match answer { + Answer::Image(bytes, _) if bytes.len() <= READ_REPLY_LIMIT => port.post_return(8, &bytes), + Answer::Zeros => port.post_return(8, &[0; READ_REPLY_LIMIT]), + _ => port.post_return(8, &[]), + }; + } else { + self.tex_counts.read_unposted += 1; + } + } + + /// The texture tokens that are not an image: the load finish, binds and their records, + /// the pixel-store parameters, and the ones nothing sends. + fn texture_command(&mut self, port: &BackPort<'_>, selector: u32, args: &[u32]) { + let c = self.contexts.select(port.context()); + match selector { + TEXTURE_LOAD_BY_IDENTIFIER => self.tex_counts.finishes += 1, + SET_TEXEL_STORE_PARAMETER | SET_PIXEL_STORE_PARAMETER_INTEGER_FORM => { + if let [name, value] = *args { + c.tex.unpack.set(name, value); + } + } + // The linear form (0x106) is the same five words; the record follows it as it does the other. + BIND_TEXTURE_CURRENT_FORM | BIND_LINEAR_TEXTURE => c.tex.short_bind(args), + BIND_2D_MIP_MAPPED_TEXTURE => { + self.tex_counts.binds += 1; + match c.tex.record(self.store.texture(), args, Default::default(), &mut self.tex_counts) { + Some(d) => c.gl.set_texture(Some(d)), + None => { + self.tex_counts.bind_refused += 1; + c.gl.set_texture(None); + } + } + } + BIND_DETAIL_TEXTURE => { + self.tex_counts.detail_binds += 1; + if !c.tex.bind_detail(args) { + self.tex_counts.unmodelled += 1; + } + } + BIND_3D_MIP_MAPPED_TEXTURE | BIND_3D_TEXTURE => { + self.tex_counts.binds += 1; + self.tex_counts.volume_binds += 1; + match c.tex.volume_record(self.store.texture(), args, selector == BIND_3D_MIP_MAPPED_TEXTURE, Default::default()) { + Some(d) => c.gl.set_texture(Some(d)), + None => { + self.tex_counts.bind_refused += 1; + c.gl.set_texture(None); + } + } + } + // Naming the bound texture by number, and the cache entry's bind and free, change + // nothing the model keeps: the record's table found the image. + BIND_TEXTURE_BY_IDENTIFIER | BIND_ARM_TEXTURE_CACHE_ENTRY | FREE_ARM_TEXTURE_CACHE_ENTRY => {} + // Detail, the load set-up and the old-style bind: no + // recording draws from them (or no software sends them); counted. + _ => self.tex_counts.unmodelled += 1, + } + } + + /// Make the arrays a pixel operation reads or writes, and the draw state it runs under. + fn pixel_state(&mut self, port: &BackPort<'_>) -> raster::DrawState { + let screen = self.gl.screen(); + let c = self.contexts.select(port.context()); + let ds = c.gl.draw_state(screen); + if let Some(l) = c.gl.layout.clone() { + l.colour.chain(l.accumulation).for_each(|w| self.store.ensure(w as usize)); + } + self.arrays(&ds); + ds + } + + /// glClear with the accumulation bit: the accumulation words to the clear value. + fn clear_accumulation(&mut self, port: &BackPort<'_>) { + self.drain(); + let ds = self.pixel_state(port); + let c = self.contexts.select(port.context()); + gl::pixels::clear_accumulation(&c.gl, &mut self.pix_counts, &gl::pixels::Env { store: &self.store, ds }); + } + + /// A pixel-state token, which stores into the context, or a pixel operation, which waits for + /// the drawing queued before it and runs inline: its image words or reply follow. + fn pixel_command(&mut self, port: &mut BackPort<'_>, selector: u32, args: &[u32]) -> Reply { + // The texel loads read the same pixel-store token (the recordings send no texel-store one). + if selector == SET_PIXEL_STORE_PARAMETER_INTEGER_FORM { + self.texture_command(port, selector, args); + } + if gl::pixels::is_state(selector) { + let c = self.contexts.select(port.context()); + gl::pixels::command(&mut c.gl, &mut self.pix_counts, selector, args); + return match gl::pixels::run_words(&c.gl, selector, args) { + Some(n) if n > 0 => Reply::Data(n), + _ => Reply::Done, + }; + } + self.drain(); + if let Some(n) = gl::pixels::run_words(&self.contexts.select(port.context()).gl, selector, args) { + let c = self.contexts.select(port.context()); + if selector == DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA && gl::pixels::Header::parse(args).is_some_and(|h| h.dma) { + c.gl.pix.pending = <[u32; 8]>::try_from(args).ok(); + return Reply::Done; + } + if n > 0 { + return Reply::Data(n); + } + self.pixel_data(port, selector, args, &[]); + return Reply::Done; + } + let ds = self.pixel_state(port); + let c = self.contexts.select(port.context()); + match gl::pixels::plane_command(&mut c.gl, &mut self.pix_counts, &gl::pixels::Env { store: &self.store, ds }, selector, args) { + gl::pixels::Out::Reply(bytes) if port.context().is_some() && port.return_page_valid() => { + port.post_return(PIXEL_REPLY_OFFSET, &bytes); + } + _ => {} + } + Reply::Done + } + + /// The words behind a pixel-state or draw token: pixel map values, a pixel draw's image, a bitmap's. + fn pixel_data(&mut self, port: &BackPort<'_>, selector: u32, args: &[u32], words: &[u32]) { + let bytes: Vec = words.iter().flat_map(|w| w.to_be_bytes()).collect(); + self.pixel_bytes(port, selector, args, &bytes, words); + } + + fn pixel_bytes(&mut self, port: &BackPort<'_>, selector: u32, args: &[u32], bytes: &[u8], words: &[u32]) { + use gl::pixels::{bitmap, draw, large_bitmap, load_map, Env, Header, Record}; + if matches!(selector, LOAD_PIXEL_MAP_FLOAT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES | LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES) { + let c = self.contexts.select(port.context()); + load_map(&mut c.gl, &mut self.pix_counts, selector, args, words); + return; + } + self.drain(); + let ds = self.pixel_state(port); + let c = self.contexts.select(port.context()); + let env = Env { store: &self.store, ds }; + let f = |w: u32| f32::from_bits(w); + match selector { + DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA => { + if let Ok(h) = <[u32; 8]>::try_from(args) { + draw(&mut c.gl, &mut self.pix_counts, &env, &h, bytes); + } + } + DRAW_BITMAP_FROM_DISPLAY_LIST if args.len() == 8 => { + let h = Header { w: args[0], h: args[1], format: crate::dev::kona::defs::pixel::FORMAT_COLOUR_INDEX, ty: crate::dev::kona::defs::pixel::TYPE_BITMAP, offset: 0, dma: false, row_mode: 0, column: 0 }; + let (xo, yo) = (f(args[2]), f(args[3])); + // The fifth and sixth words are the origin plus the move. + let image = &bytes[..bytes.len().min(args[7] as usize)]; + bitmap(&mut c.gl, &mut self.pix_counts, &env, &h, &Record { alignment: 1, ..Record::default() }, [xo, yo], [f(args[4]) - xo, f(args[5]) - yo], image); + } + DRAW_BITMAP => { + if let Some(h) = large_bitmap(args) { + let rec = c.gl.pix.unpack_in_force(); + bitmap(&mut c.gl, &mut self.pix_counts, &env, &h, &rec, [f(args[5]), f(args[6])], [f(args[7]), f(args[8])], bytes); + } + } + _ => {} + } + } + + /// A DMA packet: the rows of a pixel write that announced them, read a + /// line at a time into words (big-endian, as the programmed-I/O path + /// sends them). A packet with no write waiting is unclaimed. A read that + /// fails on the bus faults the packet and draws nothing. + fn dma_rows(&mut self, port: &mut BackPort<'_>, io: u64, physical: bool, lines: u32, per_line: u32, pitch: u64) -> Reply { + if let Some(args) = self.contexts.select(port.context()).tex.pending.take() { + // A texel draw's image: its rows, `per_line` bytes each, exactly. + let mut bytes = vec![0u8; (lines as u64 * per_line as u64) as usize]; + let ok = (0..lines as u64).all(|l| port.dma_read(io.wrapping_add(l.wrapping_mul(pitch)), physical, &mut bytes[(l * per_line as u64) as usize..][..per_line as usize])); + if !ok { + port.dma_fault(); + return Reply::Done; + } + self.drain(); + self.tex_counts.dma_loads += 1; + self.contexts.select(port.context()).tex.load(self.store.texture(), &args, &bytes, true, &mut self.tex_counts); + return Reply::Done; + } + if let Some(args) = self.contexts.select(port.context()).gl.pix.pending.take() { + // A pixel draw's image: its rows, `per_line` bytes each, as the stream would have carried them. + let mut bytes = vec![0u8; (lines as u64 * per_line as u64) as usize]; + let ok = (0..lines as u64).all(|l| port.dma_read(io.wrapping_add(l.wrapping_mul(pitch)), physical, &mut bytes[(l * per_line as u64) as usize..][..per_line as usize])); + if !ok { + port.dma_fault(); + return Reply::Done; + } + self.pix_counts.dma_draws += 1; + self.pixel_bytes(port, DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, &args, &bytes, &[]); + return Reply::Done; + } + let x = &mut self.contexts.select(port.context()).x; + let Some(owed) = x.dma_write_words() else { + x.pixel_unexpected(); + return Reply::Unclaimed; + }; + let surface = raster::surface(&mut self.tiles, &self.store, textport::COLOUR_WORD); + let total = lines as u64 * per_line as u64; + // The packet's bytes, held to what the write owes: more is dropped. + let take = (total.min(owed as u64 * 4) as usize) / 4 * 4; + let mut bytes = vec![0u8; take]; + let mut done = 0; + for l in 0..lines as u64 { + if done == take { + break; + } + let n = (per_line as usize).min(take - done); + if !port.dma_read(io.wrapping_add(l.wrapping_mul(pitch)), physical, &mut bytes[done..done + n]) { + port.dma_fault(); + x.pixel_rows(&surface, &[]); + return Reply::Done; + } + done += n; + } + let words: Vec = bytes.chunks_exact(4).map(|c| u32::from_be_bytes([c[0], c[1], c[2], c[3]])).collect(); + x.pixel_rows(&surface, &words); + Reply::Done + } + + fn handle_job(&mut self, port: &mut BackPort<'_>, job: Job<'_>) -> Reply { + self.counts[JobKind::of(&job) as usize] += 1; + // What reads or writes the store without a surface waits for the jobs queued before it. + if drains_first(&job) { + self.drain(); + } + match job { + // With the display thread running, a retrace completes at its next + // frame boundary; with none (before the board starts, in a test) + // there is no boundary, so it completes at once. + Job::Retrace { tag } => match &self.handle { + Some(h) if self.thread.is_some() => self.clock.request(h.epoch(), tag), + _ => port.retrace_done(tag), + }, + Job::DmaIn { io, physical, lines, per_line, pitch } => return self.dma_rows(port, io, physical, lines, per_line, pitch), + Job::FirmwareStart { combination } => { + // The store keeps the configured size; a combination that sets another is logged and the + // configured size is what the configuration request reports. + let frame = self.channels.start(combination); + self.sync_blank(); + if frame != self.cfg.managed { + crate::dlog!(LogModule::Kona, "kona: the combination set a managed area of {}x{}; the board keeps {}x{}", frame.0, frame.1, self.cfg.managed.0, self.cfg.managed.1); + } + port.set_managed_size(self.cfg.managed.0, self.cfg.managed.1) + } + Job::Command { grammar: Grammar::Gl, selector: CONFIGURE_FRAME_BUFFER_LAYOUT, args, .. } => { + self.buffer_config(args); + if let Some((level, double, layout, samples)) = self.pending_config.take() { + self.contexts.select(port.context()).gl.configure(level, double, layout, samples); + } + } + Job::Command { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args, .. } => return self.texel_draw(port, args), + Job::Data { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args, words } => self.texel_words(port, args, words), + Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args, .. } => self.texel_read(port, args), + Job::Command { grammar: Grammar::Gl, selector, args, .. } if gl::query::is_query(selector) => { + let c = self.contexts.select(port.context()); + let words = gl::query::answer(&mut c.gl, &mut self.query_counts, selector, args); + // The reply: the count at byte 4, the words from byte 8, then the completion word. + if port.context().is_some() && port.return_page_valid() { + port.post_return(4, &gl::query::reply_bytes(&words)); + } else { + self.query_counts.unposted += 1; + } + } + // A push-attributes token is a bracketed read-back whatever its mask: the client waits for the + // completion word after every one. + Job::Command { grammar: Grammar::Gl, selector: PUSH_ATTRIBUTE_STACK, args, .. } => { + let c = self.contexts.select(port.context()); + let words = gl::query::push_attributes(&mut c.gl, &mut self.query_counts, args.first().copied().unwrap_or(0)); + if port.context().is_some() && port.return_page_valid() { + port.post_return(4, &gl::query::reply_bytes(&words)); + } else { + self.query_counts.unposted += 1; + } + } + Job::Command { grammar: Grammar::Gl, selector: POP_ATTRIBUTE_STACK, .. } => { + let c = self.contexts.select(port.context()); + gl::query::pop_attributes(&mut c.gl, &mut self.query_counts); + } + // The restart (token 0x1bc) reprograms the output DMA from the saved start words while a feed is active. + // The client sends it when it registers the instruments buffer again, which the model does not + // keep; the select and feedback passes here have no DMA channel to reprogram, so it changes nothing. + Job::Command { grammar: Grammar::Gl, selector: RESTART_SELECT_OR_FEEDBACK, .. } => {} + Job::Command { grammar: Grammar::Gl, selector, args, .. } if gl::query::is_mode(selector) => { + let c = self.contexts.select(port.context()); + gl::query::mode_command(&mut c.gl, &mut self.query_counts, selector, args); + } + Job::Command { grammar: Grammar::Gl, selector, args, .. } if gl::pixels::is_state(selector) || gl::pixels::is_plane(selector) => return self.pixel_command(port, selector, args), + Job::Data { grammar: Grammar::Gl, selector, args, words } if gl::pixels::is_data(selector) => self.pixel_data(port, selector, args, words), + Job::Data { grammar: Grammar::Gl, selector: LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE, args: &[slot], words } => { + self.tex_counts.filter_tables += self.contexts.select(port.context()).tex.filter_table(slot, words) as u64; + } + Job::Command { grammar: Grammar::Gl, selector, args, .. } if gl::texture::is_texture(selector) => self.texture_command(port, selector, args), + // The polygon stipple's 32 rows follow its command as a run the back end sizes. + Job::Command { grammar: Grammar::Gl, selector: LOAD_POLYGON_STIPPLE_PATTERN, .. } => return Reply::Data(32), + Job::Data { grammar: Grammar::Gl, selector: LOAD_POLYGON_STIPPLE_PATTERN, words, .. } => { + if let Ok(rows) = <[u32; 32]>::try_from(words) { + self.contexts.select(port.context()).gl.set_polygon_stipple(rows); + } + } + Job::Command { grammar: Grammar::Gl, selector, args, packed, .. } if gl::is_geometry(selector) => { + if selector == CLEAR_SELECTED_BUFFERS && args.first().is_some_and(|m| m & crate::dev::kona::defs::gl::ACCUM_BUFFER_BIT != 0) { + self.clear_accumulation(port); + } + let c = self.contexts.select(port.context()); + self.gl.command(&mut c.gl, selector, args, packed, &mut self.prims); + self.flush(); + } + Job::Command { grammar: Grammar::Textport, selector, args, .. } => { + let (tiles, store) = (&mut self.tiles, &self.store); + self.textport.command(|| raster::surface(tiles, store, textport::COLOUR_WORD), selector, args); + } + Job::Command { grammar: Grammar::Kernel, selector, args, .. } => { + self.regions.note(selector, args); + match (selector, args) { + // The kernel placing the current context's window: words 0 and 1 are the + // origin, 4 to 7 the clip rectangle (origin and size), each a halfword; + // words 2 and 3 hold the window's own size, which no draw reads. + (WINDOW_CLIP_SETUP, a) if a.len() == 11 => { + let shown = self.tables.lock().bank(a[9]); + let (signed, size) = (|w: u32| w as u16 as i16 as i32, |w: u32| w as u16 as i32); + let clip = Rect { x0: signed(a[4]), y0: signed(a[5]), x1: signed(a[4]) + size(a[6]), y1: signed(a[5]) + size(a[7]) }; + { + if !raster::ops::Ownership::encode(a[9] as i32).1 { + self.gl.counts.odd_window_ids += 1; + } + self.contexts.select(port.context()).gl.place([signed(a[0]), signed(a[1])], clip, a[9], shown, a[10] != 0) + } + } + (WINDOW_READ_CLIP_SET_UP, a) if a.len() == 3 => self.contexts.select(port.context()).gl.move_to(a[0] as u16 as i16 as i32, a[1] as u16 as i16 as i32), + // A swap: the window's id, the display id and the mode. The low bit of + // the display's mode byte is the back buffer being shown (the X server's + // case split, established); that this command's third word is that same + // byte is inferred from the recorded swaps, which set it to 0 and 1 + // alternately. + (DISPLAY_MODE_BYTE, &[window, id, mode]) if window != 0 => { + self.tables.lock().load_bank(id, (mode & 1) as u8); + self.contexts.swapped(id, (mode & 1) as u8); + } + (LOAD_ONE_ENTRY_OF_DISPLAY_ID_TABLE, &[id, word]) => self.tables.lock().load_did(id, word), + (DISPLAY_MODE_BYTE, &[0, id, mode]) => self.tables.lock().load_mode(id, mode), + _ => {} + } + let c = self.contexts.select(port.context()); + let x = &mut c.x; + if !x.command(selector, args) { + if x::fill::paints(selector) { + let surface = raster::surface(&mut self.tiles, &self.store, textport::COLOUR_WORD); + x.draw_command(&surface, selector, args); + } else if x::fill::is_draw(selector) { + // A data token only collects words for the paint that follows. + x.collect_command(selector, args); + } else if x::pixel::is_pixel(selector) { + let surface = raster::surface(&mut self.tiles, &self.store, textport::COLOUR_WORD); + let answered = port.context().is_some() && port.return_page_valid(); + match x.pixel_command(&surface, selector, args, answered) { + x::pixel::Next::Nothing => {} + x::pixel::Next::Words(n) => return Reply::Data(n), + x::pixel::Next::Dma => {} + x::pixel::Next::Return(data) => { + if answered { + port.post_return(PIXEL_REPLY_OFFSET, &data); + } + } + } + } + } + match selector { + START_SELECT_FEEDBACK_RETURN_DMA => { + x.feed_start(args, port.context().is_some()); + if let (Some(&bytes), true) = (args.get(2), port.context().is_some()) { + c.gl.feed.start(bytes); + } + } + // A bracket opened in a select or feedback mode takes the pass's records; the client + // switches the mode before it finishes the one it leaves. + FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE if c.gl.feed.bracket => { + let echo = args.get(1).copied().unwrap_or(0); + let (words, overflow) = c.gl.feed.take(); + let buffer = x.feed_buffer(); + self.query_counts.overflows += overflow as u64; + self.query_counts.feedback_values += words.len() as u64; + let mut remaining = buffer.map_or(0, |b| b.3); + if let Some((io, pitch, line, total)) = buffer { + let mut data: Vec = words.iter().flat_map(|w| w.to_be_bytes()).collect(); + data.truncate(total); + let done = x::pixel::FeedDone { io, pitch, line, data, echo }; + if write_lines(port, &done) { + remaining = total - done.data.len(); + } else { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: select or feedback buffer not written"); + } + } + if port.reason_page_valid() { + port.post_reason_words(FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE, &[0x100, echo, remaining as u32, if overflow { gl::query::OVERFLOW_BIT } else { 0 }]); + } + } + FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE => { + if let Some(done) = x.feed_finish(args) { + // As the recorder's model does: a read that cannot be + // written, or a finish with no reason page to post to, + // posts no completion. It is counted and logged. + if port.reason_page_valid() && write_lines(port, &done) { + port.post_reason_words(FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE, &[0x100, done.echo, 0, 0]); + } else { + x.bracket_unposted(); + crate::dlog!(crate::devlog::LogModule::Kona, "kona: read bracket's {} bytes not written or no reason page; no completion posted", done.data.len()); + } + } + } + _ => {} + } + // A stipple store's rows follow it as a run the back end sizes. + if selector == x::stipple::X_STORE_STIPPLE && x.stipple.pending_rows() != 0 { + return Reply::Data(x.stipple.pending_rows()); + } + } + Job::Data { grammar: Grammar::Kernel, selector: LOAD_GAMMA_TABLE_ENTRIES, args: &[start, _], words } => self.tables.lock().load_gamma(start, words), + Job::Data { grammar: Grammar::Kernel, selector: LOAD_COLOUR_MAP_ENTRIES, args: &[start, _], words } => self.tables.lock().load_colours(start, words), + Job::Data { grammar: Grammar::Kernel, selector: x::stipple::X_STORE_STIPPLE, words, .. } => self.contexts.select(port.context()).x.stipple.rows(words), + Job::Data { grammar: Grammar::Kernel, selector: x::pixel::X_PIXEL_WRITE, words, .. } => { + let surface = raster::surface(&mut self.tiles, &self.store, textport::COLOUR_WORD); + self.contexts.select(port.context()).x.pixel_rows(&surface, words); + } + Job::Data { grammar: Grammar::Kernel, selector: LOAD_CURSOR_IMAGE_BLOCK, args: &[at], words } => self.tables.lock().load_cursor(at, words), + Job::Indirect { selector, value } => self.tables.lock().indirect(selector, value), + Job::Video { command: VIDEO_DAC_CURSOR_LOAD, data } => { + self.dac_cursor_loads += 1; + match display::dac_cursor::DacCursor::from_request(data) { + Some(c) => self.tables.lock().load_dac_cursor(c), + None => self.dac_cursor_refused += 1, + } + } + Job::Video { command, data } => { + self.channels.set(command, data); + self.sync_blank(); + // A Get's reply: the id, the data and their sum from byte 8, then the ready word. + if let Some(reply) = self.channels.reply(command, data) { + port.post_return(VIDEO_REPLY_OFFSET, &reply); + } + } + Job::Context(e) => { + // A slot activated afresh starts from power-on: its levels give their texture memory back first, + // after the workers that may be reading it are done. + if let super::job::ContextEvent::Activated { slot, fresh: true } = e { + if slot < CONTEXT_SLOTS && self.contexts.select(Some(slot)).tex.holds() { + self.drain(); + self.contexts.select(Some(slot)).tex.release(self.store.texture()); + } + } + self.contexts.event(e) + } + Job::Bitmap { header, payload } => { + let surface = raster::surface(&mut self.tiles, &self.store, textport::COLOUR_WORD); + self.textport.bitmap(&surface, header, payload) + } + _ => {} + } + Reply::Done + } +} + +#[cfg(test)] +mod tests { + use super::super::tests::{test_cfg, TestHost}; + use std::sync::atomic::Ordering::Relaxed; + use super::super::Kona; + use super::*; + use std::sync::Arc; + + /// A monitor writer a test can read afterwards. + #[derive(Clone, Default)] + struct Capture(Arc>>); + + impl Write for Capture { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + impl Capture { + fn text(&self) -> String { + String::from_utf8(self.0.lock().clone()).unwrap() + } + } + + fn board() -> (Arc, Arc) { + let cfg = test_cfg(); + let host = TestHost::new(); + let kona = Kona::new(cfg, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + (host, kona) + } + + fn monitor(kona: &Kona, args: &[&str]) -> (bool, String) { + let mut out = Vec::new(); + let known = kona.with_backend(|b| b.monitor(args, &mut out)).unwrap(); + (known, String::from_utf8(out).unwrap()) + } + + #[test] + fn the_config_follows_the_pipe() { + assert_eq!(KonaConfig::ir2(1).unwrap().record_words(), 16); + assert_eq!(KonaConfig::ir2(2).unwrap().record_words(), 32); + let c = KonaConfig::default().with_pool(0, 8); + assert_eq!((c.managed, c.threads, c.tile, c.texture_bytes_per_rm()), ((1280, 1024), 0, 8, 64 << 20)); + } + + /// The texture store holds what the product's texture board holds. + #[test] + fn the_texture_store_is_the_products_size() { + use crate::dev::kona::ident::Product; + for (product, mb) in [(Product::Ir2, 64), (Product::Ir2e, 64), (Product::Ir3, 256)] { + let be = KonaBackend::new(KonaConfig::new(product, 1).unwrap()); + assert_eq!(be.store.texture().bank_units(), mb << 14, "{product:?}: units of 256 bits in one of two banks"); + } + } + + #[test] + fn status_and_counters_answer_and_other_words_do_not() { + let (_host, kona) = board(); + let (known, text) = monitor(&kona, &["status"]); + assert!(known); + assert!(text.contains("raster managers 2") && text.contains("texture per RM 64 MiB"), "{text}"); + assert!(text.contains("record bound 32 words per pixel"), "{text}"); + let (known, text) = monitor(&kona, &["counters"]); + assert!(known && text.contains("GL commands 0"), "{text}"); + assert!(!monitor(&kona, &["frob", "x.png"]).0); + } + + /// The monitor reaches the back end through the board's own command. + #[test] + fn the_board_routes_the_kona_command() { + use crate::traits::Device; + let (_host, kona) = board(); + assert_eq!(kona.register_commands()[0].0, "kona"); + let out = Capture::default(); + kona.execute_command("kona", &["status"], Box::new(out.clone())).unwrap(); + assert!(out.text().contains("managed area 1280 x 1024")); + assert!(kona.execute_command("kona", &["frob"], Box::new(out)).is_err()); + } + + /// Jobs are counted by kind, a retrace completes at once and the managed + /// area is reported when the firmware starts. + #[test] + fn jobs_are_counted_and_retraces_complete() { + let (_host, kona) = board(); + let w = |off: u64, v: u32| assert_eq!(kona.test_write(off, 4, v as u64), crate::traits::BUS_OK); + w(0xc10000, 1); + w(0xc10000, 2); + w(0xc10000, 7); + w(0xc10000, 0x1f1); + w(0xc10000, 0x1000_0000); + w(0xc10000, 0); + w(0xc10000, 0x4000); + w(0xc10000, 0x1e7); + w(0xc10000, 0x42); + let (_, text) = monitor(&kona, &["counters"]); + assert!(text.contains("textport commands 1"), "{text}"); + assert!(text.contains("kernel commands 2"), "{text}"); + assert!(text.contains("retrace requests 1"), "{text}"); + assert_eq!(kona.bus_read(0x800248, 4), (crate::traits::BUS_OK, 0x42)); + } + + /// A word that opens no command is counted, not answered. + #[test] + fn dropped_words_are_counted() { + let (_host, kona) = board(); + for w in [1, 2, 7, 0x500, 0x500, 0x7f7f] { + assert_eq!(kona.test_write(0xc10000, 4, w), crate::traits::BUS_OK); + } + let (_, text) = monitor(&kona, &["counters"]); + assert!(text.contains("dropped words 3"), "{text}"); + } + + /// Wait for `cond`, up to a deadline, not a count of tries. + fn wait_for(mut cond: impl FnMut() -> bool) -> bool { + let end = std::time::Instant::now() + std::time::Duration::from_secs(30); + while std::time::Instant::now() < end { + if cond() { + return true; + } + std::thread::yield_now(); + } + cond() + } + + fn backend() -> KonaBackend { + KonaBackend::new(KonaConfig::default()) + } + + fn run(b: &mut KonaBackend, args: &[&str]) -> (bool, String) { + let mut out = Vec::new(); + let known = b.monitor(args, &mut out).unwrap(); + (known, String::from_utf8(out).unwrap()) + } + + /// A GL buffer configuration is laid out within the budget; one the hardware + /// documents do not cover is refused and counted; so is one of the wrong + /// length; the monitor shows them. + #[test] + fn a_buffer_configuration_gets_a_layout_or_a_counted_refusal() { + let mut b = backend(); + assert!(run(&mut b, &["status"]).1.contains("layouts 0 made")); + b.buffer_config(&[0, 10, 0, 1, 0, 4, 24, 8, 25, 0]); + b.buffer_config(&[0, 8, 0, 0, 0, 2, 0, 0, 0, 0]); + b.buffer_config(&[0, 8, 0]); + b.buffer_config(&[0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("layouts 1 made"), "{text}"); + assert!(text.contains("refused 2 x not 10 words"), "{text}"); + assert!(text.contains("refused 1 x Samples"), "{text}"); + assert!(text.contains("(13 words)"), "{text}"); + } + + /// Run one job through `Backend::job` with a front end behind it. + fn send(b: &mut KonaBackend, job: Job<'_>) -> Reply { + let (host, mut core) = crate::dev::kona::tests::bare(&TestHost::new(), test_cfg()); + b.job(&mut BackPort { core: &mut core, host: &host }, job) + } + + fn command(b: &mut KonaBackend, grammar: Grammar, selector: u32, args: &[u32]) -> Reply { + send(b, Job::Command { grammar, selector, args, packed: 0, handled: false }) + } + + /// A framed GL command reaches the layout through `Backend::job`; the + /// same selector in the textport grammar does not. + #[test] + fn a_framed_gl_buffer_configuration_is_laid_out() { + let mut b = backend(); + let args = [0, 10, 0, 1, 0, 4, 24, 8, 25, 0]; + command(&mut b, Grammar::Textport, 1, &args); + command(&mut b, Grammar::Gl, 1, &args); + command(&mut b, Grammar::Gl, 1, &args[..3]); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("layouts 1 made") && text.contains("1 x not 10 words"), "{text}"); + assert!(text.contains("(13 words)"), "{text}"); + } + + /// Textport commands draw into the colour word of the store, a bitmap + /// draws at the raster position, and the hash follows the contents. + #[test] + fn textport_commands_draw_into_the_store() { + let mut b = backend(); + let empty = run(&mut b, &["hash"]).1; + command(&mut b, Grammar::Textport, 2, &[7]); + command(&mut b, Grammar::Textport, 5, &[0, 0, 3, 3]); + command(&mut b, Grammar::Textport, 4, &[10, 10]); + send(&mut b, Job::Bitmap { header: &[1, 1, 0, 0, 2, 0], payload: &[1 << 31] }); + assert_eq!(b.store.read(10, 10, 1), 7); + assert_eq!(b.store.read(3, 3, 1), 7); + assert_eq!(b.store.read(1, 1, 1), 7); + assert_eq!(b.store.read(0, 0, 1), 0, "the near corner is out"); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("colour 7 raster (12, 10), 2 drawn"), "{text}"); + let drawn = run(&mut b, &["hash"]).1; + assert_ne!(empty, drawn); + b.reset(); + assert_ne!(run(&mut b, &["hash"]).1, drawn); + assert_eq!(b.store.read(3, 3, 1), 0); + } + + /// X state commands load the context, position, clip and scissor; the + /// monitor shows what was understood. + #[test] + fn x_state_commands_are_taken_and_shown() { + let mut b = backend(); + let gc = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &gc); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_DRAWABLE_ORIGIN_AND_SIZE_LOAD, &[240, 295, 100, 50]); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_CLIP_LOAD, &[1, 10, 20, 110, 220]); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_SCISSOR_LOAD, &[10, 20, 100, 200]); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("MainIndex16 fg 0x5a00 bg 0xa500 mask 0xffff gx 3"), "{text}"); + assert!(text.contains("origin (240, 295), 1 clip rects (0 refused), scissor Some([10, 20, 100, 200])"), "{text}"); + // The same selector numbers in another grammar are not X state. + command(&mut b, Grammar::Gl, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &[0; 13]); + command(&mut b, Grammar::Kernel, PIPE_SET_UP, &[0, 0, 0]); + assert!(run(&mut b, &["status"]).1.contains("MainIndex16")); + b.reset(); + assert!(run(&mut b, &["status"]).1.contains("not decoded")); + } + + /// An X box through `Backend::job` draws into the colour word of the store + /// under the loaded context, and a command that is no X token leaves a + /// waiting run alone only if it belongs to the run. + #[test] + fn an_x_box_draws_into_the_store() { + let mut b = backend(); + let gc = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &gc); + command(&mut b, Grammar::Kernel, FILL_BOXES_TWO_CORNERS, &[2 << 16 | 3, 5 << 16 | 7]); + assert_eq!(b.store.read(2, 1023 - 3, 1), 0x5a00); + assert_eq!(b.store.read(5, 1023 - 3, 1), 0); + // A data token, another command, then a box: the run did not survive. + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_2_WORD_DATA_PRELOAD, &[10 << 16 | 10, 12 << 16 | 12]); + command(&mut b, Grammar::Kernel, PIPE_SET_UP, &[0, 0, 0]); + command(&mut b, Grammar::Kernel, FILL_BOXES_TWO_CORNERS, &[20 << 16 | 20, 21 << 16 | 21]); + assert_eq!(b.store.read(10, 1023 - 10, 1), 0); + assert_eq!(b.store.read(20, 1023 - 20, 1), 0x5a00); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("X fills 2 drawn, refused: 0 context, 0 target, 0 destination"), "{text}"); + } + + /// A stipple store asks for its rows as a data run; with the pattern + /// stored and loaded, a box fill is masked by it. + #[test] + fn a_stipple_is_stored_loaded_and_masks_a_fill() { + let mut b = backend(); + let gc = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &gc); + // A 2 x 2 checkerboard in entry 4: the reply asks for its two rows. + assert_eq!(command(&mut b, Grammar::Kernel, STORE_WINDOW_SYSTEM_STIPPLE, &[4, 2, 2]), Reply::Data(2)); + send(&mut b, Job::Data { grammar: Grammar::Kernel, selector: STORE_WINDOW_SYSTEM_STIPPLE, args: &[4, 2, 2], words: &[0x8000_0000, 0x4000_0000] }); + assert_eq!(command(&mut b, Grammar::Kernel, LOAD_WINDOW_SYSTEM_STIPPLE, &[4]), Reply::Done); + command(&mut b, Grammar::Kernel, FILL_BOXES_TWO_CORNERS, &[0, 4 << 16 | 4]); + assert_eq!(b.store.read(0, 1023 - 0, 1), 0x5a00); + assert_eq!(b.store.read(1, 1023 - 0, 1), 0); + assert_eq!(b.store.read(1, 1023 - 1, 1), 0x5a00); + assert!(run(&mut b, &["status"]).1.contains("X stipple loaded")); + command(&mut b, Grammar::Kernel, LOAD_WINDOW_SYSTEM_STIPPLE, &[u32::MAX]); + command(&mut b, Grammar::Kernel, FILL_BOXES_TWO_CORNERS, &[10 << 16 | 10, 12 << 16 | 12]); + assert_eq!(b.store.read(11, 1023 - 10, 1), 0x5a00, "no stipple: solid"); + // A store of no rows asks for none. + assert_eq!(command(&mut b, Grammar::Kernel, STORE_WINDOW_SYSTEM_STIPPLE, &[4, 2, 0]), Reply::Done); + } + + /// With the board running, a retrace request waits for the display thread's + /// next frame boundary; the tag register then reads its tag and `status` shows + /// the thread and its frames. + #[test] + fn a_retrace_completes_at_a_frame_boundary_on_a_running_board() { + use crate::traits::Device; + let (_host, kona) = board(); + kona.start(); + for w in [0x1e7u32, 0x77] { + assert_eq!(kona.bus_write(0xc10000, 4, w as u64), crate::traits::BUS_OK); + } + wait_for(|| kona.bus_read(0x800248, 4).1 == 0x77); + assert_eq!(kona.bus_read(0x800248, 4).1, 0x77); + // The counter follows the completion within the same frame. + wait_for(|| monitor(&kona, &["status"]).1.contains("1 retraces completed")); + let text = monitor(&kona, &["status"]).1; + assert!(text.contains("thread running"), "{text}"); + kona.stop(); + assert!(monitor(&kona, &["status"]).1.contains("thread stopped")); + } + + /// What the pixel tests send jobs through: a front end with a context + /// current and a return and a reason page registered, over guest RAM the + /// test can look at. + struct Rig { + ram: Arc, + host: super::super::front::Host, + core: super::super::pipe::Core, + } + + const RETURN_PAGE: u64 = 0x30_0000; + const REASON_PAGE: u64 = 0x31_0000; + + impl Rig { + fn new() -> Rig { + let ram = TestHost::new(); + let (host, mut core) = crate::dev::kona::tests::bare(&ram, test_cfg()); + core.current = Some(0); + core.return_page = Some(RETURN_PAGE); + core.reason_page = Some(REASON_PAGE); + Rig { ram, host, core } + } + + fn send(&mut self, b: &mut KonaBackend, job: Job<'_>) -> Reply { + b.job(&mut BackPort { core: &mut self.core, host: &self.host }, job) + } + + fn x(&mut self, b: &mut KonaBackend, selector: u32, args: &[u32]) -> Reply { + self.send(b, Job::Command { grammar: Grammar::Kernel, selector, args, packed: 0, handled: false }) + } + } + + const INDEX_GC: [u32; 13] = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + + /// A tile worker that panics (here: a job that names a plane set-up never + /// allocated) poisons the scheduler, and the board reports itself dead. + #[test] + fn a_poisoned_tile_scheduler_makes_the_board_dead() { + let kona = Kona::new(test_cfg().with_pool(2, 32), TestHost::new(), TestHost::new(), crate::dev::kona::tests::test_status(), None).unwrap(); + assert_eq!(kona.failure(), None); + kona.with_backend(|b| { + let full = Rect { x0: 0, y0: 0, x1: 64, y1: 64 }; + let state = b.tiles().state(raster::DrawState { clip: full, colour: [77, 0], colours: 1, ..Default::default() }); + let req = raster::ClearReq { rect: full, colour: Some(1), ..Default::default() }; + let _ = b.tiles().submit(RasterJob::Clear(ClearJob { req, state })); + b.drain(); + }); + let why = kona.failure().expect("poisoned"); + assert!(why.contains("tile worker"), "{why}"); + } + + /// The placement's word 9 and the enable in word 10 become the draw state's + /// ownership test: off with the enable clear; encoded as the board does when set; + /// a word in none of the ranges is encoded as the ordinary pair and counted. + #[test] + fn a_placement_sets_the_ownership_test_from_words_nine_and_ten() { + let (mut b, mut r) = (backend(), Rig::new()); + let screen = Rect { x0: 0, y0: 0, x1: 1280, y1: 1024 }; + let own = |b: &mut KonaBackend| b.contexts.select(Some(0)).gl.draw_state(screen).ownership; + r.x(&mut b, WINDOW_CLIP_SETUP, &[0, 0, 64, 64, 0, 0, 64, 64, 0, 0xe, 0]); + assert_eq!(own(&mut b), None, "the enable is clear"); + r.x(&mut b, WINDOW_CLIP_SETUP, &[0, 0, 64, 64, 0, 0, 64, 64, 0, 0xe, 1]); + assert_eq!(own(&mut b).map(|o| o.register()), Some(0x7e01d)); + r.x(&mut b, WINDOW_CLIP_SETUP, &[0, 0, 64, 64, 0, 0, 64, 64, 0, 0xffff_ffff, 1]); + assert_eq!(own(&mut b), Some(raster::ops::Ownership { reference: 0x3f, mask: 0x3f }), "all ones is the ordinary pair 0x3f, 0x3f"); + assert_eq!(b.gl.counts.odd_window_ids, 0); + r.x(&mut b, WINDOW_CLIP_SETUP, &[0, 0, 64, 64, 0, 0, 64, 64, 0, 0x77, 1]); + assert_eq!(b.gl.counts.odd_window_ids, 1); + } + + /// The window placement keeps the window's origin and its clip rectangle + /// apart: the recording's tenth placement has a clip of the whole screen + /// and a window origin of 314, and a later move carries both. + #[test] + fn a_placement_keeps_the_window_origin_and_the_clip_rectangle_apart() { + let (mut b, mut r) = (backend(), Rig::new()); + r.x(&mut b, WINDOW_CLIP_SETUP, &[314, 0, 299, 1, 0, 0, 1280, 1024, 298, 0, 0]); + let ds = b.contexts.select(Some(0)).gl.draw_state(Rect { x0: 0, y0: 0, x1: 1280, y1: 1024 }); + assert_eq!((ds.origin, ds.clip), ([314, 0], Rect { x0: 0, y0: 0, x1: 1280, y1: 1024 })); + // The placement before it in the recording has clip equal to window. + r.x(&mut b, WINDOW_CLIP_SETUP, &[100, 50, 200, 100, 100, 50, 200, 100, 0, 0, 0]); + let ds = b.contexts.select(Some(0)).gl.draw_state(Rect { x0: 0, y0: 0, x1: 1280, y1: 1024 }); + assert_eq!((ds.origin, ds.clip), ([100, 50], Rect { x0: 100, y0: 50, x1: 300, y1: 150 })); + // Words 0 and 1 are halfwords: 0xfff0 is -16, a window partly off the left of the screen. + r.x(&mut b, WINDOW_CLIP_SETUP, &[0xfff0, 0, 64, 64, 0xfff0, 0, 64, 64, 0, 0, 0]); + let ds = b.contexts.select(Some(0)).gl.draw_state(Rect { x0: 0, y0: 0, x1: 1280, y1: 1024 }); + assert_eq!((ds.origin, ds.clip), ([-16, 0], Rect { x0: 0, y0: 0, x1: 48, y1: 64 })); + r.x(&mut b, WINDOW_READ_CLIP_SET_UP, &[16, 10, 0]); + let ds = b.contexts.select(Some(0)).gl.draw_state(Rect { x0: 0, y0: 0, x1: 1280, y1: 1024 }); + assert_eq!((ds.origin, ds.clip), ([16, 10], Rect { x0: 16, y0: 10, x1: 80, y1: 74 }), "the clip moves with the origin"); + } + + /// An image whose rows follow the command as a data run draws once the + /// run is in, the store's top row first. + #[test] + fn an_inline_pixel_write_asks_for_its_rows_and_draws_them() { + let (mut b, mut r) = (backend(), Rig::new()); + r.x(&mut b, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &INDEX_GC); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_PATH_SET_UP, &[0, 0, 1, 8, 3, 2, 0]); + assert_eq!(r.x(&mut b, WINDOW_SYSTEM_PIXEL_WRITE, &[4, 2]), Reply::Data(2)); + r.send(&mut b, Job::Data { grammar: Grammar::Kernel, selector: WINDOW_SYSTEM_PIXEL_WRITE, args: &[4, 2], words: &[0x0102_0300, 0x0405_0600] }); + assert_eq!([b.store.read(4, 1021, 1), b.store.read(6, 1021, 1), b.store.read(5, 1020, 1)], [0x100, 0x300, 0x500]); + assert_eq!(r.x(&mut b, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]), Reply::Done); + assert!(run(&mut b, &["status"]).1.contains("1 writes (0 by DMA), 0 copies, 0 reads, 1 finishes")); + } + + /// With the DMA bit set the command asks for nothing and the DMA packet + /// that arrives next carries the rows, read from the guest a line at a + /// time; one that arrives with no write waiting is unclaimed, and a read + /// that fails faults the packet and draws nothing. + #[test] + fn a_dma_pixel_write_takes_the_packet_that_follows() { + let (mut b, mut r) = (backend(), Rig::new()); + r.x(&mut b, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &INDEX_GC); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_PATH_SET_UP, &[0x10, 0, 1, 8, 4, 2, 0]); + assert_eq!(r.x(&mut b, WINDOW_SYSTEM_PIXEL_WRITE, &[0, 0]), Reply::Done); + // Two rows of one word each, the lines 8 bytes apart. + r.ram.put_words(0x40_0000, &[0x0102_0304, 0xdead_beef, 0x0506_0708]); + let dma = |io, lines, per_line, pitch| Job::DmaIn { io, physical: true, lines, per_line, pitch }; + assert_eq!(r.send(&mut b, dma(0x40_0000, 2, 4, 8)), Reply::Done); + assert_eq!([b.store.read(0, 1023, 1), b.store.read(3, 1023, 1), b.store.read(3, 1022, 1)], [0x100, 0x400, 0x800]); + assert_eq!(r.send(&mut b, dma(0x40_0000, 1, 4, 4)), Reply::Unclaimed, "nothing waits for it"); + assert!(!r.core.dma_fault); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_PATH_SET_UP, &[0x10, 0, 1, 8, 4, 1, 0]); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_WRITE, &[0, 4]); + assert_eq!(r.send(&mut b, dma(0x7f_ff_ff_00, 1, 4, 4)), Reply::Done); + assert!(r.core.dma_fault, "the bus read failed"); + assert_eq!(b.store.read(0, 1019, 1), 0); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("1 writes (1 by DMA)") && text.contains("1 with no write waiting"), "{text}"); + } + + /// A read answers on the return page after its first word, which is set + /// last; with no context current it is not answered. + #[test] + fn a_pixel_read_answers_on_the_return_page() { + let (mut b, mut r) = (backend(), Rig::new()); + r.x(&mut b, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &INDEX_GC); + r.x(&mut b, FILL_BOXES_TWO_CORNERS, &[2 << 16, 4 << 16 | 1]); + assert_eq!(b.store.read(3, 1023, 1), 0x5a00); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_PATH_SET_UP, &[2, 1, 0, 8, 3, 1, 0]); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_READ, &[2, 0]); + assert_eq!(r.ram.word(RETURN_PAGE), 1); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 4), [0x5a, 0x5a, 0, 0], "pixels 2 and 3 are filled, 4 is not"); + r.ram.put(RETURN_PAGE, &[0; 16]); + r.core.current = None; + r.x(&mut b, WINDOW_SYSTEM_PIXEL_READ, &[2, 0]); + assert_eq!(r.ram.word(RETURN_PAGE), 0, "no context, no answer"); + } + + /// A copy under the context moves pixels the same way a write puts them. + #[test] + fn a_pixel_copy_moves_pixels_inside_the_store() { + let (mut b, mut r) = (backend(), Rig::new()); + r.x(&mut b, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &INDEX_GC); + r.x(&mut b, FILL_BOXES_TWO_CORNERS, &[0, 2 << 16 | 1]); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_PATH_SET_UP, &[1, 1, 1, 0, 2, 1, 0]); + r.x(&mut b, WINDOW_SYSTEM_PIXEL_COPY, &[0, 0, 10, 5]); + assert_eq!([b.store.read(10, 1018, 1), b.store.read(11, 1018, 1), b.store.read(12, 1018, 1)], [0x5a00, 0x5a00, 0]); + } + + /// Each context has its own X state, and a fresh activation starts over. + #[test] + fn the_x_state_follows_the_context() { + let (mut b, mut r) = (backend(), Rig::new()); + r.x(&mut b, WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, &INDEX_GC); + r.core.current = Some(1); + r.send(&mut b, Job::Context(super::super::ContextEvent::Activated { slot: 1, fresh: true })); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("X contexts 1 loaded"), "{text}"); + assert!(text.contains("X gc not decoded"), "{text}"); + r.x(&mut b, FILL_BOXES_TWO_CORNERS, &[0, 2 << 16 | 2]); + assert_eq!(b.store.read(0, 1023, 1), 0, "context 1 loaded no colour"); + r.core.current = Some(0); + r.x(&mut b, FILL_BOXES_TWO_CORNERS, &[0, 2 << 16 | 2]); + assert_eq!(b.store.read(1, 1022, 1), 0x5a00, "context 0 kept its own"); + } + + /// A 64 x 64 level loaded at address 0x400 of bank 0: texel (x, y) is the grey `(4 x + y) mod 256`. + fn load_grey_level(b: &mut KonaBackend, r: &mut Rig) { + let mut header = [0u32; 20]; + header[..5].copy_from_slice(&[0x0de1, 64, 64, 5, 1]); + header[9] = 0x30; + header[13] = 63; + header[14] = 63; + header[16] = 0x66; + header[17] = 0x2200; + header[18] = 0x400; + let words: Vec = (0..64 * 64).map(|i| { let g = ((4 * (i % 64) + i / 64) % 256) as u32; g << 24 | g << 16 | g << 8 | 0xff }).collect(); + assert_eq!(r.send(b, Job::Command { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &header, packed: 0, handled: false }), Reply::Data(4096)); + r.send(b, Job::Data { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &header, words: &words }); + } + + /// The 18 words of a read-back of that level as luminance in unsigned shorts, which Quake sends: the target, + /// the size, format 6, type 3, storage 0xd, a spare, the first and last texel, then the load record's words. + const GREY_READ: [u32; 18] = [0x0de1, 64, 64, 6, 3, 0xd, 0, 0, 0, 0, 63, 63, 0, 0x0385_0030, 0x66, 0x44, 0x400, 0]; + + /// A bare texel read-back (token 0x052) answers on the return page: the level's texels in the requested + /// format and type at offset 8, then a non-zero word 0. A level that is not loaded, or a format the packer does + /// not know, answers zeros all the same, counted; with no context there is nothing to answer on. + #[test] + fn a_bare_texel_read_back_answers_on_the_return_page() { + let (mut b, mut r) = (backend(), Rig::new()); + load_grey_level(&mut b, &mut r); + r.send(&mut b, Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args: &GREY_READ, packed: 0, handled: false }); + assert_eq!(r.ram.word(RETURN_PAGE), 1, "the completion word is set"); + let image = r.ram.bytes(RETURN_PAGE + 8, 64 * 64 * 2); + let texel = |x: usize, y: usize| u16::from_be_bytes([image[2 * (64 * y + x)], image[2 * (64 * y + x) + 1]]); + assert_eq!([texel(0, 0), texel(1, 0), texel(0, 1), texel(63, 63)], [0, 4 * 257, 257, (((4 * 63 + 63) % 256) * 257) as u16]); + // The same read of a sub-rectangle: first (2, 1), 2 x 2. + let mut part = GREY_READ; + part[1..3].copy_from_slice(&[2, 2]); + part[7..9].copy_from_slice(&[2, 1]); + part[10..12].copy_from_slice(&[3, 2]); + r.ram.put(RETURN_PAGE, &[0; 16]); + r.send(&mut b, Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args: &part, packed: 0, handled: false }); + assert_eq!(r.ram.word(RETURN_PAGE), 1); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 8), [(9 * 257u16).to_be_bytes(), (13 * 257u16).to_be_bytes(), (10 * 257u16).to_be_bytes(), (14 * 257u16).to_be_bytes()].concat()); + // No level there: zeros, counted. An unknown format: nothing to pack, still answered. + let mut missing = GREY_READ; + missing[16] = 0x800; + r.ram.put(RETURN_PAGE, &[0xff; 16]); + r.send(&mut b, Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args: &missing, packed: 0, handled: false }); + assert_eq!((r.ram.word(RETURN_PAGE), r.ram.bytes(RETURN_PAGE + 8, 4)), (1, vec![0; 4])); + let mut unknown = GREY_READ; + unknown[3] = 0x77; + r.ram.put(RETURN_PAGE, &[0xff; 0x4000]); + r.send(&mut b, Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args: &unknown, packed: 0, handled: false }); + assert_eq!(r.ram.word(RETURN_PAGE), 1, "never left unposted"); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 0x3ff0), vec![0; 0x3ff0], "the reply area is zeros, not what the page held"); + r.core.current = None; + r.ram.put(RETURN_PAGE, &[0; 16]); + r.send(&mut b, Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args: &GREY_READ, packed: 0, handled: false }); + assert_eq!(r.ram.word(RETURN_PAGE), 0, "no context, no answer"); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("texel read-backs 5 (0 in a bracket); answered with zeros: 1 for a format or type the packer does not know, 2 for a level not loaded; 0 too big; 1 with nowhere to answer"), "{text}"); + } + + /// A texel level of 8 x 4 at `address` of bank `bank`, each texel the word `texel(x, y)` (red, green, blue, alpha). + fn load_level_words(b: &mut KonaBackend, r: &mut Rig, address: u32, bank: u32, texel: impl Fn(u32, u32) -> u32) { + let mut header = [0u32; 20]; + header[..5].copy_from_slice(&[0x0de1, 8, 4, 5, 1]); + header[9] = 0x30; + header[13] = 7; + header[14] = 3; + header[16] = 0x23; + header[17] = 0x2200 | bank << 3; + header[18 + bank as usize] = address; + let words: Vec = (0..32).map(|i| texel(i % 8, i / 8)).collect(); + r.send(b, Job::Command { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &header, packed: 0, handled: false }); + r.send(b, Job::Data { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &header, words: &words }); + } + + /// The 18 words of a read-back of an 8 x 4 level of `bank` at `address`: `format`, `ty`, storage `storage`. + fn level_read(format: u32, ty: u32, storage: u32, bank: u32, address: u32) -> [u32; 18] { + let mut a = [0x0de1, 8, 4, format, ty, storage, 0, 0, 0, 0, 7, 3, 0, 0x0385_0030 | bank << 28, 0x23, 0x44, address, 0]; + a[13] = 0x0385_0030 | bank << 28; + a + } + + fn read_texels(b: &mut KonaBackend, r: &mut Rig, args: &[u32]) { + r.send(b, Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args, packed: 0, handled: false }); + } + + /// A level is found by the bank in the read's mode word and the address in word 16. The empty slot of a + /// level in the other bank is address zero, which names a real level in this one, and must not be taken. + #[test] + fn a_texel_read_finds_its_level_by_bank_and_address() { + let (mut b, mut r) = (backend(), Rig::new()); + load_level_words(&mut b, &mut r, 0, 1, |_, _| 0x1000_00ff); + load_level_words(&mut b, &mut r, 0x400, 0, |_, _| 0xf000_00ff); + // The read is of bank 0 at 0x400 (its other slot, word 17, is the empty zero). + r.ram.put(RETURN_PAGE, &[0; 16]); + read_texels(&mut b, &mut r, &level_read(6, 1, 0x0d, 0, 0x400)); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 2), [0xf0, 0xf0], "bank 0 at 0x400 is the second level"); + // Bank 0 at address 0 is empty, though bank 1 holds a level at address 0. + r.ram.put(RETURN_PAGE, &[0xff; 16]); + read_texels(&mut b, &mut r, &level_read(6, 1, 0x0d, 0, 0)); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 2), [0, 0], "no level in bank 0 at address 0"); + // And bank 1 at address 0 is the first. + read_texels(&mut b, &mut r, &level_read(6, 1, 0x0d, 1, 0)); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 2), [0x10, 0x10]); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("1 for a level not loaded"), "{text}"); + } + + /// A read of a colour texture as luminance is the sum of red, green and blue clamped at 1.0, with the + /// alpha kept; a texture that holds one channel is read as it is. + #[test] + fn a_texel_read_as_luminance_sums_the_colour_of_a_colour_texture() { + let (mut b, mut r) = (backend(), Rig::new()); + // Texel (0, 0): red 0x10, green 0x20, blue 0x30, alpha 0x40. Texel (1, 0): 0x80 in all three, alpha 0xff. + load_level_words(&mut b, &mut r, 0x100, 0, |x, y| match (x, y) { + (0, 0) => 0x1020_3040, + (1, 0) => 0x8080_80ff, + _ => 0, + }); + let mut luminance = level_read(6, 1, 0x30, 0, 0x100); + luminance[1..3].copy_from_slice(&[2, 1]); + read_texels(&mut b, &mut r, &luminance); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 2), [0x10 + 0x20 + 0x30, 0xff], "0x60 and a clamped sum"); + let mut both = level_read(7, 1, 0x30, 0, 0x100); + both[1..3].copy_from_slice(&[2, 1]); + read_texels(&mut b, &mut r, &both); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 4), [0x60, 0x40, 0xff, 0xff], "luminance then the alpha the texel holds"); + // The same level read as the one-channel texture it would be is its red alone. + let mut one = level_read(6, 1, 0x0d, 0, 0x100); + one[1..3].copy_from_slice(&[2, 1]); + read_texels(&mut b, &mut r, &one); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 2), [0x10, 0x80]); + } + + /// A push-attributes token is answered whatever its mask: the pipe's handler opens and closes the + /// read-back on every path, so the completion word is set last and the count word is zero. A + /// push onto a full stack is answered as well (and sets the overflow error); a pop and a bare + /// light-state step (0x10b) post nothing. + #[test] + fn a_push_of_attributes_posts_its_completion_on_every_path() { + let (mut b, mut r) = (backend(), Rig::new()); + let gl_token = |b: &mut KonaBackend, r: &mut Rig, selector: u32, args: &[u32]| { + r.send(b, Job::Command { grammar: Grammar::Gl, selector, args, packed: 0, handled: false }); + }; + let words = |r: &Rig| (r.ram.word(RETURN_PAGE), r.ram.bytes(RETURN_PAGE + 4, 4)); + // The list bit alone: nothing is read back, and the client still waits. + gl_token(&mut b, &mut r, PUSH_ATTRIBUTE_STACK, &[0x20000]); + assert_eq!(words(&r), (1, vec![0; 4])); + // A mask with a bit the pipe reads back is answered too, with no data, and counted. + r.ram.put(RETURN_PAGE, &[0; 16]); + gl_token(&mut b, &mut r, PUSH_ATTRIBUTE_STACK, &[0x8]); + assert_eq!(words(&r), (1, vec![0; 4])); + assert_eq!((b.query_counts.pushes, b.query_counts.pushes_unread), (2, 1)); + // Fill the stack to 16 entries; the push onto the full stack still completes. + for _ in 0..14 { + gl_token(&mut b, &mut r, PUSH_ATTRIBUTE_STACK, &[0x20000]); + } + assert_eq!(b.contexts.select(Some(0)).gl.attrib_depth, 16); + assert_eq!(b.contexts.select(Some(0)).gl.error, 0); + r.ram.put(RETURN_PAGE, &[0; 16]); + gl_token(&mut b, &mut r, PUSH_ATTRIBUTE_STACK, &[0x20000]); + assert_eq!(r.ram.word(RETURN_PAGE), 1, "a push onto a full stack completes"); + assert_eq!((b.contexts.select(Some(0)).gl.attrib_depth, b.contexts.select(Some(0)).gl.error, b.query_counts.pushes_full), (16, 0x503, 1)); + // A pop posts nothing and frees an entry. + r.ram.put(RETURN_PAGE, &[0; 16]); + gl_token(&mut b, &mut r, POP_ATTRIBUTE_STACK, &[0]); + assert_eq!(r.ram.word(RETURN_PAGE), 0); + assert_eq!(b.contexts.select(Some(0)).gl.attrib_depth, 15); + // The light-state step posts no completion of its own. + gl_token(&mut b, &mut r, SAVE_LIGHT_ATTRIBUTES_ON_ENGINE_ATTRIBUTE_STACK, &[0]); + assert_eq!(r.ram.word(RETURN_PAGE), 0); + // With no return page the completion is dropped and counted. + r.core.return_page = None; + gl_token(&mut b, &mut r, PUSH_ATTRIBUTE_STACK, &[0x20000]); + assert_eq!(b.query_counts.unposted, 1); + } + + /// A bare read whose image would not fit the page is not built: only the completion word is posted, the + /// page's reply area is not written past, and the read is counted. Inside a bracket the same read is + /// refused when it asks for more texels than the model will build. + #[test] + fn a_texel_read_too_big_to_answer_is_refused_and_counted() { + let (mut b, mut r) = (backend(), Rig::new()); + load_grey_level(&mut b, &mut r); + let mut big = GREY_READ; + big[1..3].copy_from_slice(&[256, 256]); + r.ram.put(RETURN_PAGE, &[0xaa; 0x4010]); + read_texels(&mut b, &mut r, &big); + assert_eq!(r.ram.word(RETURN_PAGE), 1, "the completion is posted so the client does not wait"); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 0x4008), vec![0xaa; 0x4008], "no image is written"); + let mut huge = GREY_READ; + huge[1..3].copy_from_slice(&[u32::MAX, u32::MAX]); + read_texels(&mut b, &mut r, &huge); + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[0x1c0, 0x9000, 8192, 8192, 0, 0]); + read_texels(&mut b, &mut r, &huge); + assert!(b.contexts.select(Some(0)).x.feed_finish(&[0x100, 7]).is_none(), "the bracket is refused"); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("3 (1 in a bracket)") && text.contains("3 too big"), "{text}"); + } + + /// The pack record's skips are the guest's, and they lengthen what the packer builds: a small read under a + /// huge skip is oversize, not an allocation that aborts the host, and is answered like any other oversize read. + #[test] + fn a_texel_read_under_a_huge_pack_skip_is_refused_not_allocated() { + let (mut b, mut r) = (backend(), Rig::new()); + load_grey_level(&mut b, &mut r); + let store = |b: &mut KonaBackend, r: &mut Rig, name: u32, value: u32| { + r.send(b, Job::Command { grammar: Grammar::Gl, selector: SET_PIXEL_STORE_PARAMETER_INTEGER_FORM, args: &[name, value], packed: 0, handled: false }); + }; + let mut wide = GREY_READ; + wide[1..3].copy_from_slice(&[8000, 1]); + for (name, value) in [(0xd03, u32::MAX), (0xd04, u32::MAX)] { + store(&mut b, &mut r, name, value); + r.ram.put(RETURN_PAGE, &[0xaa; 0x4010]); + read_texels(&mut b, &mut r, &wide); + assert_eq!(r.ram.word(RETURN_PAGE), 1, "the completion is posted so the client does not wait"); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 0x4008), vec![0xaa; 0x4008], "no image is written"); + store(&mut b, &mut r, name, 0); + } + // In a bracket the same read is refused, not built. + store(&mut b, &mut r, 0xd03, u32::MAX); + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[0x1c0, 0x9000, 16000, 16000, 0, 0]); + read_texels(&mut b, &mut r, &wide); + assert!(b.contexts.select(Some(0)).x.feed_finish(&[0x100, 7]).is_none(), "the bracket is refused"); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("3 too big"), "{text}"); + // Without the skip the same read is answered. + store(&mut b, &mut r, 0xd03, 0); + read_texels(&mut b, &mut r, &wide); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("texel read-backs 4 (1 in a bracket)") && text.contains("3 too big"), "{text}"); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 4), [0, 0, 4, 4], "16000 bytes fit the page, and the first texels are the level's"); + } + + /// The texel cap is its own cap: a read of one-byte texels, 2048 by 1024, packs to 2 MiB, well under the byte + /// cap, but would build 2^21 colours. It is refused in a bracket (where the byte cap does not stop it). + #[test] + fn a_texel_read_past_the_texel_cap_is_refused_though_its_bytes_are_few() { + let (mut b, mut r) = (backend(), Rig::new()); + load_grey_level(&mut b, &mut r); + let mut read = GREY_READ; + read[1..3].copy_from_slice(&[2048, 1024]); + read[3..5].copy_from_slice(&[0, 1]); + read[10..12].copy_from_slice(&[2047, 1023]); + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[0x1c0, 0x9000, 1 << 21, 1 << 21, 0, 0]); + read_texels(&mut b, &mut r, &read); + assert!(b.contexts.select(Some(0)).x.feed_finish(&[0x100, 7]).is_none(), "the bracket is refused"); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("1 too big") && text.contains("0 for a level not loaded"), "{text}"); + // Half the texels (the cap itself) is answered: the level is not that big, so its texels are zeros. + read[2] = 512; + read[11] = 511; + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[0x1c0, 0x9000, 1 << 20, 1 << 20, 0, 0]); + read_texels(&mut b, &mut r, &read); + let done = b.contexts.select(Some(0)).x.feed_finish(&[0x100, 7]).expect("a read at the cap is answered"); + assert_eq!(done.data.len(), 1 << 20); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("1 too big"), "{text}"); + } + + /// In a bracket, a read of a format or type the packer does not know refuses the bracket, as it has + /// nothing to describe. + #[test] + fn an_unsupported_texel_read_in_a_bracket_refuses_it() { + let (mut b, mut r) = (backend(), Rig::new()); + load_grey_level(&mut b, &mut r); + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[0x1c0, 0x9000, 8192, 8192, 0, 0]); + let mut unknown = GREY_READ; + unknown[3] = 0x77; + read_texels(&mut b, &mut r, &unknown); + assert!(b.contexts.select(Some(0)).x.feed_finish(&[0x100, 7]).is_none()); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("1 for a format or type the packer does not know"), "{text}"); + } + + /// Inside a select-feed bracket the read goes to the bracket's host buffer and not the return page. + #[test] + fn a_bracketed_texel_read_back_leaves_the_return_page_alone() { + let (mut b, mut r) = (backend(), Rig::new()); + load_grey_level(&mut b, &mut r); + // A bracket for the whole read: 64 rows of 128 bytes in one line. + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[0x1c0, 0x9000, 8192, 8192, 0, 0]); + r.send(&mut b, Job::Command { grammar: Grammar::Gl, selector: QUERY_TEXTURE_IMAGE, args: &GREY_READ, packed: 0, handled: false }); + assert_eq!(r.ram.word(RETURN_PAGE), 0, "the bracket's finish returns it, by output DMA"); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("texel read-backs 1 (1 in a bracket)"), "{text}"); + let c = b.contexts.select(Some(0)); + let done = c.x.feed_finish(&[0x100, 7]).expect("the bracket holds the read"); + assert_eq!((done.data.len(), done.line, done.echo), (8192, 8192, 7)); + assert_eq!(&done.data[..4], &[0, 0, 0x04, 0x04], "texel 0 is 0 and texel 1 is 4 x 257"); + } + + /// A bracketed texel read over 0x3ff0 bytes (the 512 x 256 level Quake reads back) is delivered to the range the + /// start token describes: its device address from the map page and the offset (plus 0x9000), the padded image + /// as one line of the full length with no stride. Nothing goes to the return page, and the finish posts event + /// 0x100 with the slot, no bytes remaining and no overflow. + #[test] + fn a_bracketed_texel_read_is_delivered_by_output_dma_to_the_described_range() { + use crate::dev::kona::defs::{MAPRAM_KIND_16K, MAPRAM_PAGE_SHIFT}; + let (mut b, mut r) = (backend(), Rig::new()); + let host = 0x40_0000u64; + let dev = (8u64 << MAPRAM_PAGE_SHIFT) + 0x100; + for page in 0..18u64 { + r.core.map_ram[8 + page as usize] = MAPRAM_KIND_16K | ((host + (page << MAPRAM_PAGE_SHIFT)) >> 12) as u32; + } + let mut header = [0u32; 20]; + header[..5].copy_from_slice(&[0x0de1, 512, 256, 5, 1]); + header[9] = 0x30; + header[13] = 511; + header[14] = 255; + header[16] = 0x89; + header[17] = 0x2200; + header[18] = 0x400; + let grey = |x: u32, y: u32| (x + 2 * y) % 256; + let words: Vec = (0..512 * 256).map(|i| { let g = grey(i % 512, i / 512); g << 24 | g << 16 | g << 8 | 0xff }).collect(); + assert_eq!(r.send(&mut b, Job::Command { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &header, packed: 0, handled: false }), Reply::Data(512 * 256)); + r.send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &header, words: &words }); + // The read: 512 x 256 luminance in unsigned shorts is 262144 bytes, far over the page. + let len = 512 * 256 * 2u32; + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[(dev >> 12) as u32, 0x9000 | (dev & 0xfff) as u32, len, len, 0, 0]); + let read = [0x0de1, 512, 256, 6, 3, 0xd, 0, 0, 0, 0, 511, 255, 0, 0x0385_0030, 0x89, 0x44, 0x400, 0]; + r.ram.put(RETURN_PAGE, &[0; 16]); + read_texels(&mut b, &mut r, &read); + assert_eq!(r.ram.bytes(RETURN_PAGE, 16), [0; 16], "no header and no completion word on the return page"); + assert_eq!(r.ram.bytes(host + 0x100, 4), [0; 4], "and nothing is written until the finish"); + r.x(&mut b, FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE, &[0x100, 7]); + let image = r.ram.bytes(host + 0x100, len as usize); + let texel = |x: usize, y: usize| u16::from_be_bytes([image[2 * (512 * y + x)], image[2 * (512 * y + x) + 1]]); + for (x, y) in [(0, 0), (1, 0), (0, 1), (511, 255), (300, 200), (127, 255)] { + assert_eq!(texel(x, y) as u32, grey(x as u32, y as u32) * 257, "texel ({x}, {y})"); + } + assert_eq!(r.ram.bytes(host + 0xff, 1), [0], "the byte before the range is untouched"); + assert_eq!(r.ram.bytes(host + 0x100 + len as u64, 4), [0; 4], "and the one after"); + assert_eq!([r.ram.word(REASON_PAGE), r.ram.word(REASON_PAGE + 4), r.ram.word(REASON_PAGE + 8), r.ram.word(REASON_PAGE + 12)], [0x100, 7, 0, 0], "event 0x100: the slot, nothing remaining, no overflow"); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("texel read-backs 1 (1 in a bracket)"), "{text}"); + // A descriptor whose length is not the image's is refused, and nothing is posted. + r.ram.put(REASON_PAGE, &[0; 16]); + r.x(&mut b, START_SELECT_FEEDBACK_RETURN_DMA, &[(dev >> 12) as u32, 0x9000 | (dev & 0xfff) as u32, len - 4, len - 4, 0, 0]); + read_texels(&mut b, &mut r, &read); + r.x(&mut b, FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE, &[0x100, 8]); + assert_eq!(r.ram.word(REASON_PAGE), 0); + assert!(run(&mut b, &["status"]).1.contains("read brackets 1 refused")); + } + + /// A DAC cursor load is decoded and held by channel, and draws nothing until channel 0's control register enables it; a load of another length, or + /// for a channel the table does not give, is refused and counted. + #[test] + fn a_dac_cursor_load_is_held_by_channel() { + let mut b = backend(); + let mut load = [0u32; 263]; + load[0] = 3; + load[1] = 0x3ff; + send(&mut b, Job::Video { command: VIDEO_DAC_CURSOR_LOAD, data: &load }); + send(&mut b, Job::Video { command: VIDEO_DAC_CURSOR_LOAD, data: &[0; 5] }); + let mut stray = load; + stray[0] = 0x1234; + send(&mut b, Job::Video { command: VIDEO_DAC_CURSOR_LOAD, data: &stray }); + // Channel 8 is the first value past the table's 0 to 7. + stray[0] = 8; + send(&mut b, Job::Video { command: VIDEO_DAC_CURSOR_LOAD, data: &stray }); + let status = run(&mut b, &["status"]).1; + assert!(status.contains("DAC cursor 4 loads, 3 refused; held for channels [3]; channel 0's is not drawn"), "{status}"); + assert!(status.contains("control value for the owning channel: 0x5 = X11 cursor, enabled, not interlaced"), "{status}"); + assert_eq!(b.tables.lock().dac().cursors().next().unwrap().foreground, [0x3ff, 0, 0]); + assert_eq!(crate::dev::kona::tables::VIDEO.get(VIDEO_DAC_CURSOR_LOAD as usize).copied().flatten(), Some(263)); + } + + /// A video setter reaches the display generator's channels, a Get changes nothing, and a reset + /// puts the power-up state back. + #[test] + fn video_setters_reach_the_channels_and_a_reset_restores_them() { + let mut b = backend(); + send(&mut b, Job::Video { command: VIDEO_CURSOR_PRIORITY, data: &[2, 9] }); + send(&mut b, Job::Video { command: VIDEO_GET_BOARD_CONFIG, data: &[0] }); + assert_eq!(b.channels.channel[2].cursor_priority, 9); + assert_eq!(b.channels.channel[0], display::channels::Channels::new(2).channel[0]); + // `kona status` says which channels hold blanking. + assert!(run(&mut b, &["status"]).1.contains("video channels enabled [0], blanked [] (channel 0's, and all while temporarily blanked, are drawn black), frame 1280 x 1024\n")); + send(&mut b, Job::Video { command: VIDEO_CHANNEL_BLANKING, data: &[3, 1] }); + send(&mut b, Job::Video { command: VIDEO_TEMP_BLANK, data: &[1] }); + assert!(run(&mut b, &["status"]).1.contains("blanked [3] (channel 0's, and all while temporarily blanked, are drawn black), frame 1280 x 1024, all blanked\n")); + b.reset(); + assert_eq!(b.channels.channel[2].cursor_priority, 0); + } + + /// The channels are part of the saved small state: what the video setters left comes back from a snapshot + /// into a fresh backend, and a snapshot of the restored backend is the same bytes. + #[test] + fn the_video_channels_are_saved_and_restored_with_the_backend() { + use crate::dev::kona::stream::{Reader, Writer}; + let mut b = backend(); + send(&mut b, Job::Video { command: VIDEO_CURSOR_PRIORITY, data: &[2, 9] }); + send(&mut b, Job::Video { command: VIDEO_CHANNEL_BLANKING, data: &[4, 1] }); + send(&mut b, Job::Video { command: VIDEO_TEMP_BLANK, data: &[1] }); + send(&mut b, Job::Video { command: VIDEO_RESIZE, data: &[1, 10, 500, 640, 480, 1, 1, 0] }); + let mut w = Writer::default(); + b.visit_small(&mut w).unwrap(); + let mut back = backend(); + assert_ne!(back.channels, b.channels); + let mut r = Reader::new(&w.out); + back.visit_small(&mut r).unwrap(); + r.finish().unwrap(); + assert_eq!(back.channels, b.channels); + assert_eq!((back.channels.channel[2].cursor_priority, back.channels.channel[4].blanked, back.channels.blanked, back.channels.channel[1].input), (9, true, true, [10, 500, 640, 480])); + let mut again = Writer::default(); + back.visit_small(&mut again).unwrap(); + assert_eq!(again.out, w.out); + } + + /// The loader releasing the firmware runs the stored combination, or the built-in one when there is none: the + /// job reaches the channels without any recording. After it channel 0 runs 1280x1024_60.vfo, channel 1 holds + /// its format blanked, and the Gets say so. + #[test] + fn starting_the_firmware_runs_the_built_in_combination_on_the_channels() { + let (mut b, mut r) = (backend(), Rig::new()); + let ram = r.ram.clone(); + let words = |at: u64, n: usize| ram.bytes(at, 4 * n).chunks(4).map(|c| u32::from_be_bytes(c.try_into().unwrap())).collect::>(); + r.send(&mut b, Job::Video { command: VIDEO_GET_CHANNEL_CONFIG, data: &[0] }); + assert_eq!(words(RETURN_PAGE + 8 + 4, 5)[1], 0, "before the start channel 0 holds no named format"); + r.send(&mut b, Job::FirmwareStart { combination: &[] }); + r.send(&mut b, Job::Video { command: VIDEO_GET_CHANNEL_CONFIG, data: &[0] }); + let w = words(RETURN_PAGE + 8, 21); + assert_eq!(w[0], 7, "the reply id"); + assert_eq!(&w[2..6], &[u32::from_be_bytes(*b"1280"), u32::from_be_bytes(*b"x102"), u32::from_be_bytes(*b"4_60"), u32::from_be_bytes(*b".vfo")]); + r.send(&mut b, Job::Video { command: VIDEO_GET_CHANNEL_BLANKING, data: &[1] }); + assert_eq!(words(RETURN_PAGE + 8, 3), [9, 1, 10], "channel 1 is blanked"); + r.send(&mut b, Job::Video { command: VIDEO_GET_CHANNEL_BLANKING, data: &[0] }); + assert_eq!(words(RETURN_PAGE + 8, 3), [9, 0, 9], "and channel 0 is not"); + } + + /// A Get posts its reply on the return page from byte 8 and the ready word with it (`post_return` + /// writes the data first and the ready word last); a setter posts nothing. + #[test] + fn a_video_get_posts_its_reply_and_the_ready_word() { + let (mut b, mut r) = (backend(), Rig::new()); + r.send(&mut b, Job::Video { command: VIDEO_CURSOR_PRIORITY, data: &[0, 1] }); + assert_eq!(r.ram.bytes(RETURN_PAGE, 36), [0; 36], "a setter has no reply"); + r.send(&mut b, Job::Video { command: VIDEO_GET_BOARD_CONFIG, data: &[0] }); + let words = |v: &[u32]| v.iter().flat_map(|w| w.to_be_bytes()).collect::>(); + assert_eq!(r.ram.bytes(RETURN_PAGE, 4), words(&[1])); + assert_eq!(r.ram.bytes(RETURN_PAGE + 8, 28), words(&[6, 1, 0xff, 0, 0, 0, 0x106])); + // With no return page the reply is dropped, logged, and the board carries on. + r.ram.put(RETURN_PAGE, &[0; 36]); + r.core.return_page = None; + r.send(&mut b, Job::Video { command: VIDEO_GET_BOARD_CONFIG, data: &[0] }); + assert_eq!(r.ram.bytes(RETURN_PAGE, 36), [0; 36]); + } + + /// A 64 x 64 window at the screen's corner, double buffered, with a viewport that + /// maps clip coordinates -1 to 1 onto it; draws go to colour word 2. The lit pixels + /// of that word are read back with `lit_pixels`. + fn gl_window() -> KonaBackend { + let mut b = KonaBackend::new(KonaConfig::default()); + command(&mut b, Grammar::Kernel, WINDOW_CLIP_SETUP, &[0, 0, 64, 64, 0, 0, 64, 64, 0, 13, 0]); + command(&mut b, Grammar::Gl, CONFIGURE_FRAME_BUFFER_LAYOUT, &[0, 8, 0, 1, 0, 0, 24, 0, 0, 0]); + command(&mut b, Grammar::Gl, SET_VIEWPORT, &[0, 0, 64, 64]); + command(&mut b, Grammar::Gl, SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, &[1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits()]); + b + } + + /// Window coordinate to clip coordinate for `gl_window`'s 64-pixel viewport. + fn ndc(w: f32) -> u32 { + (w / 32.0 - 1.0).to_bits() + } + + fn vertices(b: &mut KonaBackend, mode: u32, points: &[(f32, f32)]) { + command(b, Grammar::Gl, mode, &[0]); + for &(x, y) in points { + command(b, Grammar::Gl, SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, &[ndc(x), ndc(y), 0.0f32.to_bits()]); + } + command(b, Grammar::Gl, END_OPEN_PRIMITIVE, &[0]); + } + + fn lit_pixels(b: &mut KonaBackend) -> Vec<(u32, u32)> { + command(b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + (0..64).flat_map(|y| (0..64u32).map(move |x| (x, y))).filter(|&(x, y)| b.store.read(x, y, 2) != 0).collect() + } + + /// A texture through the board: a 2 x 2 image loaded inline, bound by its short bind and + /// record, and drawn with the replace environment, shows its texels at nearest and + /// blends them at linear; with the texture off the draw is the vertex colour. + #[test] + fn a_loaded_and_bound_texture_shows_on_a_triangle() { + use crate::dev::kona::defs::gl::TEXTURE_2D; + let mut b = gl_window(); + let mut hdr = [0u32; 20]; + hdr[..4].copy_from_slice(&[0x0de1, 2, 2, 5]); + hdr[4] = 1; + hdr[9] = 0x30; + hdr[13] = 1; + hdr[14] = 1; + hdr[16] = 0x11; + hdr[17] = 0x2200; + hdr[18] = 0x1000; + let image = [0xff00_00ffu32, 0x00ff_00ff, 0x0000_ffff, 0xffff_ffff]; + assert_eq!(command(&mut b, Grammar::Gl, DRAW_TEXELS_INTO_TEXTURE_MEMORY, &hdr), Reply::Data(4)); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &hdr, words: &image }); + command(&mut b, Grammar::Gl, TEXTURE_LOAD_BY_IDENTIFIER, &[0]); + let mut rec = [0u32; 22]; + rec[0] = 0x44; + rec[1] = 0x1010; + rec[6] = 0x1000; + rec[7] = 0xffff_ffff; + let bind = |b: &mut KonaBackend, mag: u32| { + command(b, Grammar::Gl, BIND_TEXTURE_CURRENT_FORM, &[0xffff_ffff, 2 << 12, mag << 5, 0x0011, 1]); + command(b, Grammar::Gl, BIND_2D_MIP_MAPPED_TEXTURE, &rec); + }; + bind(&mut b, 0); + command(&mut b, Grammar::Gl, SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM, &[0x2200, (0x1e01 as f32).to_bits(), 0, 0, 0]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[TEXTURE_2D]); + let quad = |b: &mut KonaBackend| { + command(b, Grammar::Gl, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[0]); + for (x, y, s, t) in [(0.0, 0.0, 0.0, 0.0), (64.0, 0.0, 1.0, 0.0), (0.0, 64.0, 0.0, 1.0), (64.0, 0.0, 1.0, 0.0), (64.0, 64.0, 1.0, 1.0), (0.0, 64.0, 0.0, 1.0)] { + command(b, Grammar::Gl, SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT, &[(s as f32).to_bits(), (t as f32).to_bits()]); + command(b, Grammar::Gl, SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, &[ndc(x), ndc(y), 0.0f32.to_bits()]); + } + command(b, Grammar::Gl, END_OPEN_PRIMITIVE, &[0]); + }; + quad(&mut b); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + let at = |b: &KonaBackend, x, y| b.store.read(x, y, 2); + assert_eq!([at(&b, 10, 10), at(&b, 50, 10), at(&b, 10, 50), at(&b, 50, 50)], [0xffff_0000, 0xff00_ff00, 0xff00_00ff, 0xffff_ffff], "nearest: each quadrant its texel"); + // Linear magnification blends the four at the middle. + bind(&mut b, 1); + quad(&mut b); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + let mid = at(&b, 32, 32); + assert!([mid >> 16 & 0xff, mid >> 8 & 0xff, mid & 0xff].iter().all(|&c| (100..160).contains(&c)), "{mid:#x}: red, green, blue and white average to grey"); + // Off, the draw is the vertex colour (white). + command(&mut b, Grammar::Gl, DISABLE_CAPABILITY, &[TEXTURE_2D]); + quad(&mut b); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + assert_eq!(at(&b, 10, 10), 0xffff_ffff); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("1 level loads") && text.contains("2 binds (0 refused)"), "{text}"); + } + + /// A volume through the board: two 2 x 2 slices loaded one command each, bound by the short bind and the + /// single-level volume record, and drawn with the replace environment under the 3D enable. The texture + /// coordinate's r picks the slice; with the 3D enable off, the draw is the vertex colour. + #[test] + fn a_loaded_and_bound_volume_shows_the_slice_r_names() { + use crate::dev::kona::defs::gl::TEXTURE_3D; + let mut b = gl_window(); + for (z, texel) in [(0u32, 0xff00_00ffu32), (1, 0x0000_ffff)] { + let mut hdr = [0u32; 20]; + hdr[..4].copy_from_slice(&[0x806f, 2, 2, 5]); + hdr[4] = 1; + hdr[9] = 0x30; + hdr[13] = 1; + hdr[14] = 1; + hdr[15] = z; + hdr[16] = 0x111; + hdr[17] = 0x2200; + hdr[18] = 0x1000; + assert_eq!(command(&mut b, Grammar::Gl, DRAW_TEXELS_INTO_TEXTURE_MEMORY, &hdr), Reply::Data(4)); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &hdr, words: &[texel; 4] }); + } + command(&mut b, Grammar::Gl, TEXTURE_LOAD_BY_IDENTIFIER, &[0]); + command(&mut b, Grammar::Gl, BIND_TEXTURE_CURRENT_FORM, &[0xffff_ffff, 3 << 28 | 2 << 12, 0, 0x0111, 1]); + command(&mut b, Grammar::Gl, BIND_3D_TEXTURE, &[0x44, 0x111, 0x1000, 0]); + command(&mut b, Grammar::Gl, SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM, &[0x2200, (0x1e01 as f32).to_bits(), 0, 0, 0]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[TEXTURE_3D]); + let quad = |b: &mut KonaBackend, r: f32, x0: f32| { + command(b, Grammar::Gl, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[0]); + for (x, y) in [(0.0, 0.0), (32.0, 0.0), (0.0, 32.0), (32.0, 0.0), (32.0, 32.0), (0.0, 32.0)] { + command(b, Grammar::Gl, SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT, &[(x / 32.0f32).to_bits(), (y / 32.0f32).to_bits(), r.to_bits()]); + command(b, Grammar::Gl, SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, &[ndc(x + x0), ndc(y), 0.0f32.to_bits()]); + } + command(b, Grammar::Gl, END_OPEN_PRIMITIVE, &[0]); + }; + quad(&mut b, 0.25, 0.0); + quad(&mut b, 0.75, 32.0); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + let at = |b: &KonaBackend, x, y| b.store.read(x, y, 2); + assert_eq!([at(&b, 8, 8), at(&b, 24, 24), at(&b, 40, 8), at(&b, 56, 24)], [0xffff_0000, 0xffff_0000, 0xff00_00ff, 0xff00_00ff], "slice 0 where r is a quarter, slice 1 at three quarters"); + command(&mut b, Grammar::Gl, DISABLE_CAPABILITY, &[TEXTURE_3D]); + quad(&mut b, 0.25, 0.0); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + assert_eq!(at(&b, 8, 8), 0xffff_ffff); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("1 binds (0 refused)") && text.contains("2 volume slice loads, 1 volume binds"), "{text}"); + } + + /// A detail bind (token 0x17d) and a filter-coefficient table (token 0x17e and its 33 words) are kept and counted, + /// and neither refuses a bind nor draws a flat colour: the base texture stays what is bound. + #[test] + fn a_detail_bind_and_a_filter_table_are_kept_and_change_nothing_drawn() { + let mut b = gl_window(); + command(&mut b, Grammar::Gl, BIND_DETAIL_TEXTURE, &[0xffff_ffff, 3, 2, 0x2, 0x400, 0x400]); + command(&mut b, Grammar::Gl, LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE, &[1]); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE, args: &[1], words: &[0; 33] }); + assert_eq!((b.tex_counts.detail_binds, b.tex_counts.filter_tables), (1, 1)); + assert_eq!((b.tex_counts.unmodelled, b.tex_counts.bind_refused), (0, 0)); + assert!(run(&mut b, &["status"]).1.contains("detail 1 binds, 1 filter tables")); + } + + /// A perspective-divided textured quad samples with the divided coordinate. The right edge is twice as far (w = 2) as + /// the left, so the screen's middle column is a third of the way across the texture, not half: it shows the first texel + /// of a 2 x 2 image at nearest, where the affine coordinate would show the second. + #[test] + fn a_perspective_textured_quad_samples_with_the_divided_coordinate() { + use crate::dev::kona::defs::gl::TEXTURE_2D; + let mut b = gl_window(); + let mut hdr = [0u32; 20]; + hdr[..4].copy_from_slice(&[0x0de1, 2, 2, 5]); + hdr[4] = 1; + hdr[9] = 0x30; + hdr[13] = 1; + hdr[14] = 1; + hdr[16] = 0x11; + hdr[17] = 0x2200; + hdr[18] = 0x1000; + let image = [0xff00_00ffu32, 0x00ff_00ff, 0x0000_ffff, 0xffff_ffff]; + command(&mut b, Grammar::Gl, DRAW_TEXELS_INTO_TEXTURE_MEMORY, &hdr); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_TEXELS_INTO_TEXTURE_MEMORY, args: &hdr, words: &image }); + command(&mut b, Grammar::Gl, TEXTURE_LOAD_BY_IDENTIFIER, &[0]); + let mut rec = [0u32; 22]; + rec[0] = 0x44; + rec[1] = 0x1010; + rec[6] = 0x1000; + rec[7] = 0xffff_ffff; + command(&mut b, Grammar::Gl, BIND_TEXTURE_CURRENT_FORM, &[0xffff_ffff, 2 << 12, 0, 0x0011, 1]); + command(&mut b, Grammar::Gl, BIND_2D_MIP_MAPPED_TEXTURE, &rec); + command(&mut b, Grammar::Gl, SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM, &[0x2200, (0x1e01 as f32).to_bits(), 0, 0, 0]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[TEXTURE_2D]); + // Clip coordinates: x = +-1 times w, so the screen's x is 0 and 64 whatever w is. + let corner = |b: &mut KonaBackend, xn: f32, yn: f32, w: f32, s: f32| { + command(b, Grammar::Gl, SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT, &[s.to_bits(), 0.25f32.to_bits()]); + command(b, Grammar::Gl, SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT, &[(xn * w).to_bits(), (yn * w).to_bits(), 0.0f32.to_bits(), w.to_bits()]); + }; + command(&mut b, Grammar::Gl, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[0]); + for (xn, yn, w, s) in [(-1.0, -0.5, 1.0, 0.0), (1.0, -0.5, 2.0, 1.0), (1.0, 0.5, 2.0, 1.0), (-1.0, -0.5, 1.0, 0.0), (1.0, 0.5, 2.0, 1.0), (-1.0, 0.5, 1.0, 0.0)] { + corner(&mut b, xn, yn, w, s); + } + command(&mut b, Grammar::Gl, END_OPEN_PRIMITIVE, &[0]); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + let at = |b: &KonaBackend, x| b.store.read(x, 32, 2); + // The middle is a third across (red); the affine half would be green. Near the right edge, s nears 1 (green). + assert_eq!(at(&b, 32), 0xffff_0000, "the divided coordinate: a third of the way across"); + assert_eq!(at(&b, 8), 0xffff_0000); + assert_eq!(at(&b, 60), 0xff00_ff00, "near the far edge"); + // The coordinate is half at two thirds of the screen: s = 0.5 t / (1 - 0.5 t), not at the middle. + let first_green = (0..64).find(|&x| at(&b, x) == 0xff00_ff00).expect("some green"); + assert!((40..46).contains(&first_green), "the texel edge is at {first_green}, two thirds of the screen, not the middle"); + } + + /// A point of one pixel lights exactly the pixel its vertex is in, even on its + /// edge; one larger than `POINT_PIXEL_MAX` is a disc of about its area. + #[test] + fn a_point_lights_one_pixel_or_a_disc() { + for (x, y, want) in [(10.5, 10.5, (10, 10)), (20.0, 30.0, (20, 30)), (7.9, 3.0, (7, 3))] { + let mut b = gl_window(); + vertices(&mut b, BEGIN_PRIMITIVE_POINTS, &[(x, y)]); + assert_eq!(lit_pixels(&mut b), [want], "a point at ({x}, {y})"); + } + let mut b = gl_window(); + command(&mut b, Grammar::Gl, SET_POINT_SIZE, &[3.0f32.to_bits(), 0]); + vertices(&mut b, BEGIN_PRIMITIVE_POINTS, &[(10.5, 10.5)]); + assert_eq!(lit_pixels(&mut b), [(10, 10)], "size 3 is still one pixel"); + let mut b = gl_window(); + command(&mut b, Grammar::Gl, SET_POINT_SIZE, &[8.0f32.to_bits(), 0]); + vertices(&mut b, BEGIN_PRIMITIVE_POINTS, &[(32.0, 32.0)]); + let lit = lit_pixels(&mut b); + assert!((44..=56).contains(&lit.len()), "a disc of diameter 8 holds about 50 pixels: {}", lit.len()); + assert!(lit.iter().all(|&(x, y)| (27..=36).contains(&x) && (27..=36).contains(&y))); + } + + /// A line of width 1 lights one row, from just past its first end to its last pixel + /// (the fill rule leaves the left edge out); a wide line lights its width. + #[test] + fn a_line_lights_its_width() { + let mut b = gl_window(); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 20.5), (40.5, 20.5)]); + let lit = lit_pixels(&mut b); + assert!(lit.iter().all(|&(_, y)| y == 20), "one row"); + assert_eq!((lit.first().map(|p| p.0), lit.last().map(|p| p.0), lit.len()), (Some(11), Some(40), 30)); + let mut b = gl_window(); + command(&mut b, Grammar::Gl, SET_LINE_WIDTH, &[3.0f32.to_bits(), 0]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 20.5), (40.5, 20.5)]); + let rows: std::collections::BTreeSet = lit_pixels(&mut b).iter().map(|p| p.1).collect(); + assert_eq!(rows, [19, 20, 21].into(), "width 3"); + } + + /// The alpha a pixel was written with: smoothing scales it, and with no blending the colour is + /// the vertex's, so alpha reads the coverage back. + fn alpha_at(b: &KonaBackend, x: u32, y: u32) -> u32 { + b.store.read(x, y, 2) >> 24 + } + + /// A smoothed line of width 1 centred on a row is that row at full coverage, with each end a half + /// pixel in (its end passes through the end pixels' centres); moved half a pixel up it shares two + /// rows evenly. + #[test] + fn a_smoothed_line_ramps_its_ends_and_shares_a_row_boundary() { + use crate::dev::kona::defs::gl::LINE_SMOOTH; + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[LINE_SMOOTH]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 20.5), (40.5, 20.5)]); + let lit = lit_pixels(&mut b); + assert!(lit.iter().all(|&(_, y)| y == 20), "one row: {lit:?}"); + assert_eq!((lit.first().map(|p| p.0), lit.last().map(|p| p.0), lit.len()), (Some(10), Some(40), 31), "the end pixels, half covered, are drawn too"); + assert_eq!([alpha_at(&b, 10, 20), alpha_at(&b, 11, 20), alpha_at(&b, 25, 20), alpha_at(&b, 39, 20), alpha_at(&b, 40, 20)], [128, 255, 255, 255, 128]); + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[LINE_SMOOTH]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 21.0), (40.5, 21.0)]); + lit_pixels(&mut b); + assert_eq!([alpha_at(&b, 25, 19), alpha_at(&b, 25, 20), alpha_at(&b, 25, 21), alpha_at(&b, 25, 22)], [0, 128, 128, 0]); + // Off, the same line is a row of whole pixels from the second. + let mut b = gl_window(); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 21.0), (40.5, 21.0)]); + let lit = lit_pixels(&mut b); + assert!(lit.iter().all(|&(_, y)| y == 21) && lit.iter().all(|&(x, y)| alpha_at(&b, x, y) == 255)); + } + + /// A diagonal smoothed line through pixel centres covers its own pixel by a little under a whole and + /// its two neighbours across by about a quarter each: a strip one pixel wide at 45 degrees takes + /// 0.914 of its pixel and 0.25 of each neighbour, 1.414 in all, the line's width times the length + /// of a diagonal step. Set-up snaps the rectangle's corners to eighths of a pixel, which widens it + /// a little, so the figures here are a little above those. + #[test] + fn a_smoothed_diagonal_line_shares_its_width_across_the_pixels_it_touches() { + use crate::dev::kona::defs::gl::LINE_SMOOTH; + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[LINE_SMOOTH]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 10.5), (40.5, 40.5)]); + lit_pixels(&mut b); + let a = |dx: u32, dy: u32| alpha_at(&b, dx, dy); + assert!((235..=250).contains(&a(25, 25)), "{}", a(25, 25)); + assert_eq!((a(26, 25), a(25, 26)), (a(24, 25), a(25, 24)), "the neighbours either side are alike"); + assert!((60..=80).contains(&a(26, 25)), "{}", a(26, 25)); + let sum = (a(25, 25) + a(26, 25) + a(24, 25)) as f32 / 255.0; + assert!((1.35..=1.6).contains(&sum), "{sum}"); + assert_eq!(a(27, 25), 0, "and none past them"); + } + + /// A smoothed polygon ramps its outer edges and not the seam of the fan it is drawn as: a square + /// from pixel centre to pixel centre covers its edge pixels by half and its corners by a quarter, + /// and the pixels along its diagonal are whole. + #[test] + fn a_smoothed_polygon_ramps_its_outline_and_not_its_seam() { + use crate::dev::kona::defs::gl::POLYGON_SMOOTH; + let square = [(10.5, 10.5), (40.5, 10.5), (40.5, 40.5), (10.5, 40.5)]; + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[POLYGON_SMOOTH]); + vertices(&mut b, BEGIN_PRIMITIVE_POLYGON, &square); + lit_pixels(&mut b); + let a = |x, y| alpha_at(&b, x, y); + assert_eq!([a(10, 25), a(25, 10), a(40, 25), a(25, 40)], [128; 4], "edge pixels, half covered"); + assert_eq!([a(10, 10), a(40, 10), a(40, 40), a(10, 40)], [64; 4], "corners, a quarter"); + assert_eq!([a(11, 11), a(25, 25), a(39, 39), a(20, 30)], [255; 4], "inside, seam included, whole"); + assert_eq!([a(9, 25), a(41, 25), a(25, 9), a(25, 41)], [0; 4], "and nothing past the ramp"); + // Off, the same polygon is whole pixels only. + let mut b = gl_window(); + vertices(&mut b, BEGIN_PRIMITIVE_POLYGON, &square); + let lit = lit_pixels(&mut b); + assert!(lit.iter().all(|&(x, y)| alpha_at(&b, x, y) == 255)); + // A triangle's slanted edge covers its pixels in part, and the pixels a plain triangle lights + // keep at least half a pixel's cover on their outer edge. + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[POLYGON_SMOOTH]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[(10.0, 10.0), (50.0, 10.0), (10.0, 50.0)]); + lit_pixels(&mut b); + let mid = alpha_at(&b, 30, 29); + assert!((1..255).contains(&mid), "a pixel on the hypotenuse is partly covered: {mid}"); + } + + /// A smoothed point covers the share of each pixel its disc does: the pixel about a size-1 point's + /// centre about pi / 4, a size-3 point's pixels symmetric about it and summing to the disc's area. + #[test] + fn a_smoothed_point_covers_by_its_disc() { + use crate::dev::kona::defs::gl::POINT_SMOOTH; + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[POINT_SMOOTH]); + vertices(&mut b, BEGIN_PRIMITIVE_POINTS, &[(20.5, 20.5)]); + let lit = lit_pixels(&mut b); + assert_eq!(lit, [(20, 20)]); + // pi / 4 of full, from an 8 x 8 grid of sample positions: within a few percent. + assert!((195..=212).contains(&alpha_at(&b, 20, 20)), "{}", alpha_at(&b, 20, 20)); + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[POINT_SMOOTH]); + command(&mut b, Grammar::Gl, SET_POINT_SIZE, &[3.0f32.to_bits(), 0]); + vertices(&mut b, BEGIN_PRIMITIVE_POINTS, &[(20.5, 20.5)]); + lit_pixels(&mut b); + let a = |dx: i32, dy: i32| alpha_at(&b, (20 + dx) as u32, (20 + dy) as u32); + assert_eq!(a(0, 0), 255); + assert!(a(-1, 0) == a(1, 0) && a(0, -1) == a(0, 1) && a(1, 0) == a(0, 1), "the four edge neighbours alike"); + assert!(a(1, 1) == a(-1, -1) && a(1, 1) < a(1, 0), "the corners, alike, are less than the edges"); + let sum: u32 = (-2..=2).flat_map(|dy| (-2..=2).map(move |dx| (dx, dy))).map(|(dx, dy)| a(dx, dy)).sum(); + let area = std::f32::consts::PI * 1.5 * 1.5; + assert!((sum as f32 / 255.0 - area).abs() < 0.2, "coverage sums to {} against the disc's {area}", sum as f32 / 255.0); + } + + /// The line stipple indexes its 16-bit pattern by the distance along the line over + /// the repeat count; the polygon stipple, 32 x 32 bits, by window position. + #[test] + fn stipples_keep_the_pixels_their_patterns_name() { + use crate::dev::kona::defs::gl::{LINE_STIPPLE, POLYGON_STIPPLE}; + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[LINE_STIPPLE]); + command(&mut b, Grammar::Gl, SET_LINE_STIPPLE, &[1, 0x00ff]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 20.5), (40.5, 20.5)]); + let xs: Vec = lit_pixels(&mut b).iter().map(|p| p.0).collect(); + let want: Vec = (11..=40).filter(|x| (x - 10) % 16 < 8).collect(); + assert_eq!(xs, want, "the low eight bits of the pattern: eight pixels on, eight off"); + // A repeat of 2 stretches each bit over two pixels. + let mut b = gl_window(); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[LINE_STIPPLE]); + command(&mut b, Grammar::Gl, SET_LINE_STIPPLE, &[2, 0x00ff]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 20.5), (50.5, 20.5)]); + let xs: Vec = lit_pixels(&mut b).iter().map(|p| p.0).collect(); + let want: Vec = (11..=50).filter(|x| (x - 10) / 2 % 16 < 8).collect(); + assert_eq!(xs, want); + // Polygon stipple: a checkerboard, row 0 starting set at the leftmost column. + let mut b = gl_window(); + let mut rows = [0xaaaa_aaaau32; 32]; + rows.iter_mut().skip(1).step_by(2).for_each(|r| *r = 0x5555_5555); + assert_eq!(command(&mut b, Grammar::Gl, LOAD_POLYGON_STIPPLE_PATTERN, &[0, 0, 0]), Reply::Data(32)); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: LOAD_POLYGON_STIPPLE_PATTERN, args: &[0, 0, 0], words: &rows }); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[POLYGON_STIPPLE]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[(0.5, 0.5), (60.5, 0.5), (0.5, 60.5)]); + let lit = lit_pixels(&mut b); + assert!(lit.len() > 500 && lit.iter().all(|&(x, y)| (x + y) % 2 == 0), "a checkerboard of {} pixels", lit.len()); + // With the stipple off the same triangle is solid. + let mut b = gl_window(); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: LOAD_POLYGON_STIPPLE_PATTERN, args: &[0, 0, 0], words: &rows }); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[(0.5, 0.5), (60.5, 0.5), (0.5, 60.5)]); + assert!(lit_pixels(&mut b).iter().any(|&(x, y)| (x + y) % 2 == 1)); + } + + /// A GL triangle, through the board: the kernel places the window and the + /// buffer configuration lays the planes out; the triangle goes to the back + /// buffer's word, the display shows nothing of it until a swap names the + /// window's id with the buffer bit, and then shows it as direct colour. + #[test] + fn a_gl_triangle_draws_into_the_back_buffer_and_shows_after_the_swap() { + use crate::dev::kona::back::display::compose; + use crate::dev::kona::back::store::DID_PLANE; + let mut b = KonaBackend::new(KonaConfig::default()); + let f = |v: f32| v.to_bits(); + // Window at (10, 20), 50 x 40, display id 13, not partly covered. + command(&mut b, Grammar::Kernel, WINDOW_CLIP_SETUP, &[10, 20, 50, 40, 10, 20, 50, 40, 0, 13, 0]); + command(&mut b, Grammar::Gl, 1, &[0, 8, 0, 1, 0, 0, 24, 0, 0, 0]); + command(&mut b, Grammar::Gl, SET_VIEWPORT, &[0, 0, 50, 40]); + command(&mut b, Grammar::Gl, SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, &[f(1.0), f(0.0), f(0.0), f(1.0)]); + command(&mut b, Grammar::Gl, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[0]); + for (x, y) in [(-1.0, -1.0), (1.0, -1.0), (0.0, 1.0)] { + command(&mut b, Grammar::Gl, SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, &[f(x), f(y), f(0.0)]); + } + command(&mut b, Grammar::Gl, END_OPEN_PRIMITIVE, &[0]); + // A job that touches pixels waits for the drawing first. + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + let red = |w: usize| (0..1024).flat_map(|y| (0..1280u32).map(move |x| (x, y))).filter(|&(x, y)| b.store.read(x, y, w) == 0xffff_0000).count(); + assert!(red(1) == 0, "the front buffer is untouched"); + let back = red(2); + assert!(back > 900 && back <= 1000, "about half of 50 x 40 pixels: {back}"); + assert!(b.store.read(30, 30, 2) == 0xffff_0000 && b.store.read(5, 30, 2) == 0, "inside the window and outside it"); + // The display: id 13 is direct colour, showing the back buffer once swapped. + b.store.write(30, 30, DID_PLANE, 13 << 2); + command(&mut b, Grammar::Kernel, DISPLAY_MODE_BYTE, &[0, 13, 4]); + let shown = |b: &KonaBackend| { + let mut out = vec![0u32; 1280 * 1024]; + compose(&b.store, 1, &b.map.lock(), &b.tables.lock().snapshot(), &mut out, 1280); + out[(1023 - 30) * 1280 + 30] + }; + assert_eq!(shown(&b), 0xff00_0000, "the front buffer is empty"); + command(&mut b, Grammar::Kernel, DISPLAY_MODE_BYTE, &[0x3c0001, 13, 1]); + assert_eq!(shown(&b), 0xff00_00ff, "red, as 0xAABBGGRR, after the swap"); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("raster stage 1 triangles"), "{text}"); + } + + /// Each row of the drain table: what reads or writes the store drains the + /// GL stage, and what does not leaves it alone. Counted, so the test does not + /// depend on a worker being slow enough to lose a race. + #[test] + fn the_drain_table_row_by_row() { + let mut b = KonaBackend::new(test_cfg().with_pool(2, 32)); + b.tiles(); + let drains = |b: &KonaBackend| b.tiles.as_ref().map_or(0, |t| t.stats().drains); + let mut expect = 0; + // Rows that do not drain. + let quiet = |b: &mut KonaBackend, what: &str, job: Job<'_>| { + send(b, job); + assert_eq!(drains(b), expect, "{what} left the workers alone"); + }; + quiet(&mut b, "GL state", Job::Command { grammar: Grammar::Gl, selector: SET_VIEWPORT, args: &[0, 0, 50, 40], packed: 0, handled: false }); + quiet(&mut b, "GL colour", Job::Command { grammar: Grammar::Gl, selector: SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, args: &[1.0f32.to_bits(); 4], packed: 0, handled: false }); + quiet(&mut b, "GL begin", Job::Command { grammar: Grammar::Gl, selector: BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, args: &[0], packed: 0, handled: false }); + quiet(&mut b, "GL end", Job::Command { grammar: Grammar::Gl, selector: END_OPEN_PRIMITIVE, args: &[0], packed: 0, handled: false }); + quiet(&mut b, "a GL clear", Job::Command { grammar: Grammar::Gl, selector: CLEAR_SELECTED_BUFFERS, args: &[0], packed: 0, handled: false }); + quiet(&mut b, "an X state load", Job::Command { grammar: Grammar::Kernel, selector: x::state::X_POSITION, args: &[0, 0, 8, 8], packed: 0, handled: false }); + quiet(&mut b, "an X data token", Job::Command { grammar: Grammar::Kernel, selector: x::fill::X_DATA2, args: &[0, 0], packed: 0, handled: false }); + quiet(&mut b, "a display-table load", Job::Command { grammar: Grammar::Kernel, selector: DISPLAY_MODE_BYTE, args: &[0, 13, 4], packed: 0, handled: false }); + quiet(&mut b, "an indirect store", Job::Indirect { selector: XMAP_CURSOR_MODE, value: 0 }); + quiet(&mut b, "a context event", Job::Context(super::super::job::ContextEvent::Activated { slot: 1, fresh: true })); + quiet(&mut b, "dropped words", Job::Dropped { word: 7 }); + // Rows that drain, each once. + let mut loud = |b: &mut KonaBackend, what: &str, job: Job<'_>| { + send(b, job); + expect += 1; + assert_eq!(drains(b), expect, "{what} drains"); + }; + loud(&mut b, "a textport command", Job::Command { grammar: Grammar::Textport, selector: TEXTPORT_FILL_BOX, args: &[0, 0, 3, 3], packed: 0, handled: false }); + loud(&mut b, "an X box", Job::Command { grammar: Grammar::Kernel, selector: x::fill::X_BOX, args: &[0, 2 << 16 | 2], packed: 0, handled: false }); + loud(&mut b, "an X pixel finish", Job::Command { grammar: Grammar::Kernel, selector: x::pixel::X_PIXEL_FINISH, args: &[0], packed: 0, handled: false }); + loud(&mut b, "a swap", Job::Command { grammar: Grammar::Kernel, selector: DISPLAY_MODE_BYTE, args: &[0x3c0001, 13, 1], packed: 0, handled: false }); + loud(&mut b, "a region swap-out", Job::Command { grammar: Grammar::Kernel, selector: PIPE_MEMORY_SWAP_OUT, args: &[0; 8], packed: 0, handled: false }); + loud(&mut b, "a retrace request", Job::Retrace { tag: 1 }); + loud(&mut b, "a GL finish", Job::Command { grammar: Grammar::Gl, selector: FINISH_DRAIN_PIPE_AND_SIGNAL_COMPLETION, args: &[0], packed: 0, handled: false }); + b.quiesce(); + assert_eq!(drains(&b), expect + 1, "a quiesce drains"); + assert_eq!(b.tiles.as_ref().map_or(0, |t| t.stats().busy_drains), 0, "no worker had anything queued"); + } + + /// The region commands are counted with the bytes the swaps name, and the + /// front end still moves the bytes. + #[test] + fn region_commands_are_counted() { + let (mut b, mut r) = (backend(), Rig::new()); + r.x(&mut b, PIPE_MEMORY_RESERVE, &[2, 9, 0, 1]); + r.x(&mut b, PIPE_MEMORY_COPY_DOWN, &[2, 0, 0, 0]); + r.x(&mut b, PIPE_MEMORY_SWAP_OUT, &[3, 0, 4, 0x400, 1, 0, 0x9000, 0x102]); + r.x(&mut b, PIPE_MEMORY_SWAP_IN, &[3, 0, 2, 0x400, 1, 0]); + let text = run(&mut b, &["status"]).1; + assert!(text.contains("1 placed, 1 copied down, 1 swapped out (4096 bytes), 1 swapped in (2048 bytes)"), "{text}"); + } + + /// A bracketed read whose host buffer is not mapped writes nothing, posts no + /// completion and is counted, as the recorder's model does. + #[test] + fn a_bracketed_read_into_an_unmapped_buffer_posts_nothing() { + use crate::traits::{Device, BUS_OK}; + let (host, kona) = board(); + let w = |v: u32| assert_eq!(kona.test_write(0xc10000, 4, v as u64), BUS_OK); + let words = |ws: &[u32]| ws.iter().for_each(|&v| w(v)); + let phys = |a: u64| 0x1000_0000 | (a >> 12) as u32; + words(&[0x1f1, phys(REASON_PAGE), 0, 0x4000]); + words(&[0x1fd, phys(0x50_0000), 0, 0x10000]); + words(&[0x1f2, phys(RETURN_PAGE), 0, 0x4000]); + words(&[0x129]); + words(&INDEX_GC); + words(&[0x154, 0, 2 << 16 | 1]); + words(&[0x136, 2, 1, 0, 8, 2, 1, 0]); + // Device page 0x1c1 has no map RAM entry. + let dev = 0x1c1u64 << 14; + words(&[0x1f5, (dev >> 12) as u32, 0x9000 | (dev & 0xfff) as u32, 2, 2, 0, 0]); + words(&[0x138, 0, 0]); + words(&[0x1f6, 0x100, 0x77]); + assert_eq!(host.word(REASON_PAGE), 0, "no completion"); + let text = monitor(&kona, &["status"]).1; + assert!(text.contains("read brackets 0 refused, 1 with no completion posted"), "{text}"); + kona.stop(); + } + + /// A read inside a select-feed bracket goes to the host buffer at the + /// bracket's finish, which then posts its completion with the second word + /// the finish carried. Through the board's own command port. + #[test] + fn a_bracketed_read_reaches_the_host_buffer() { + use crate::traits::{Device, BUS_OK}; + let (host, kona) = board(); + let w = |v: u32| assert_eq!(kona.test_write(0xc10000, 4, v as u64), BUS_OK); + let words = |ws: &[u32]| ws.iter().for_each(|&v| w(v)); + let phys = |a: u64| 0x1000_0000 | (a >> 12) as u32; + words(&[0x1f1, phys(REASON_PAGE), 0, 0x4000]); + words(&[0x1fd, phys(0x50_0000), 0, 0x10000]); + words(&[0x1f2, phys(RETURN_PAGE), 0, 0x4000]); + // Device page 0x1c0 maps the host buffer. + let buf = 0x70_0000u64; + assert_eq!(kona.test_write(0x100000 + 4 * 0x1c0, 4, 0x2000_0000 | (buf >> 12) as u64), BUS_OK); + let dev = 0x1c0u64 << 14; + words(&[0x129]); + words(&INDEX_GC); + words(&[0x154, 0, 2 << 16 | 1]); + words(&[0x136, 2, 1, 0, 8, 2, 1, 0]); + words(&[0x1f5, (dev >> 12) as u32, 0x9000 | (dev & 0xfff) as u32, 2, 2, 0, 0]); + words(&[0x138, 0, 0]); + assert_eq!(host.word(RETURN_PAGE), 0, "inside a bracket the bytes wait"); + words(&[0x1f6, 0x100, 0x77]); + assert_eq!(host.bytes(buf, 2), [0x5a, 0x5a]); + assert_eq!([host.word(REASON_PAGE), host.word(REASON_PAGE + 4), host.word(REASON_PAGE + 8)], [0x100, 0x77, 0]); + let text = monitor(&kona, &["status"]).1; + assert!(text.contains("1 reads"), "{text}"); + kona.stop(); + } + + /// While someone holds the pipe's state, the pixel commands still answer + /// and a wait for the state gives up instead of hanging. + #[test] + fn pixel_reads_do_not_wait_for_the_state() { + use crate::traits::Device; + let (_host, kona) = board(); + kona.with_backend(|_| { + let out = Capture::default(); + kona.execute_command("kona", &["hash"], Box::new(out.clone())).unwrap(); + assert_eq!(out.text().trim().len(), 16); + kona.execute_command("kona", &["pix", "1", "1"], Box::new(out.clone())).unwrap(); + assert!(out.text().contains("word arrays allocated")); + }); + } + + /// A command that reads the back end's state gives up waiting for it and + /// says so: the answer is "board busy", and the state is its again after. + #[test] + fn a_state_command_answers_busy_when_the_state_stays_out() { + use crate::traits::Device; + let cfg = test_cfg(); + let host = TestHost::new(); + let kona = Kona::new(cfg, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + kona.shared.monitor_wait.store(0, Relaxed); + kona.with_backend(|_| { + let out = Capture::default(); + assert_eq!(kona.execute_command("kona", &["status"], Box::new(out.clone())), Err("kona: board busy".into())); + assert!(kona.shared.pipe.try_lock().is_none(), "the state is out"); + }); + assert!(kona.shared.pipe.try_lock().is_some()); + let out = Capture::default(); + kona.execute_command("kona", &["status"], Box::new(out.clone())).unwrap(); + assert!(out.text().contains("raster managers")); + } + + /// A stop from the main thread leaves the renderer to it; any other stop + /// has the display thread release it as its last act. + #[test] + fn a_stop_from_the_main_thread_leaves_the_renderer_alone() { + use crate::dev::ng1::rex3::Renderer; + use crate::traits::Device; + use std::sync::atomic::AtomicU32; + struct Spy(Arc); + impl Renderer for Spy { + fn present( + &mut self, + _: &mut crate::disp::Rex3Screen, + _: &mut crate::debug_overlay::DebugOverlay, + _: &mut crate::disp::StatusBar, + _: &mut crate::disp::StatusBarTexture, + _: &crate::disp::BarStats, + _: bool, + _: Option<&[u32]>, + _: Option<&[u32]>, + ) { + } + + fn stop(&mut self) { + self.0.fetch_add(1, Relaxed); + } + } + let run = |name: &str| { + let stops = Arc::new(AtomicU32::new(0)); + let spy = stops.clone(); + std::thread::Builder::new() + .name(name.into()) + .spawn(move || { + let cfg = test_cfg(); + let host = TestHost::new(); + let kona = Kona::new(cfg, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + *kona.with_backend(|b| b.display()).renderer_slot().lock() = Some(Box::new(Spy(spy))); + kona.start(); + kona.stop(); + }) + .unwrap() + .join() + .unwrap(); + stops.load(Relaxed) + }; + assert_eq!(run("main"), 0, "the main thread releases it"); + assert_eq!(run("other"), 1, "the display thread releases it"); + } + + /// The machine may take the window before or after wiring in its status + /// source: it is the same window. + #[test] + fn the_window_is_the_same_before_and_after_the_status_is_wired() { + let cfg = test_cfg(); + let backend = KonaBackend::new(cfg); + let before = backend.display(); + backend.set_status(crate::disp::StatusSource::new(Arc::default(), Arc::default())); + assert!(Arc::ptr_eq(&before, &backend.display())); + } + + #[test] + fn pix_shows_the_allocated_words_of_a_pixel() { + let mut b = backend(); + let (known, text) = run(&mut b, &["pix", "3", "4"]); + assert!(known && text.contains("0 word arrays allocated"), "{text}"); + b.store.write(3, 4, 2, 0xabcd); + let text = run(&mut b, &["pix", "3", "4"]).1; + assert!(text.contains("word 2 0x0000abcd") && text.contains("1 word arrays"), "{text}"); + assert!(run(&mut b, &["pix", "5000", "0"]).1.contains("outside")); + assert!(!run(&mut b, &["pix", "x", "0"]).0); + // A reset zeroes the store in place. + b.reset(); + assert!(run(&mut b, &["pix", "3", "4"]).1.contains("word 2 0x00000000")); + } + + #[test] + fn attach_and_stop_hold_and_release_the_handle() { + use crate::traits::Device; + let (_host, kona) = board(); + assert!(monitor(&kona, &["status"]).1.contains("detached")); + kona.start(); + assert!(monitor(&kona, &["status"]).1.contains("display handle attached")); + kona.stop(); + assert!(monitor(&kona, &["status"]).1.contains("detached")); + } + + // ---- GL pixel operations ---- + + /// `gl_window` with a window-sized orthographic projection, so a raster position is a window pixel. + fn pixel_window() -> KonaBackend { + let mut b = gl_window(); + command(&mut b, Grammar::Gl, SELECT_CURRENT_MATRIX_STACK, &[crate::dev::kona::defs::gl::PROJECTION]); + let m = crate::dev::gl::math::ortho(0.0, 64.0, 0.0, 64.0, -1.0, 1.0); + command(&mut b, Grammar::Gl, LOAD_MATRIX_16_FLOATS, &m.map(f32::to_bits)); + b + } + + fn at(b: &mut KonaBackend, x: i32, y: i32) { + command(b, Grammar::Gl, SET_RASTER_POSITION_2_COMPONENTS_32_BIT_INTEGER, &[x as u32, y as u32]); + } + + /// A pixel draw: the header, then the image words the back end asked for. + fn draw_pixels(b: &mut KonaBackend, w: u32, h: u32, format: u32, ty: u32, image: &[u32]) { + let hdr = [w, h, format, ty, 0, 0, 0, 0]; + assert_eq!(command(b, Grammar::Gl, DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, &hdr), if image.is_empty() { Reply::Done } else { Reply::Data(image.len() as u32) }); + if !image.is_empty() { + send(b, Job::Data { grammar: Grammar::Gl, selector: DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, args: &hdr, words: image }); + } + } + + fn colour_at(b: &mut KonaBackend, x: u32, y: u32) -> u32 { + command(b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + b.store.read(x, y, 2) + } + + /// A 2 x 2 RGBA image of bytes lands with its lower-left pixel at the raster position; zoom + /// replicates each pixel; a raster position outside the clip volume draws nothing. + #[test] + fn draw_pixels_lands_at_the_raster_position_and_zooms() { + use crate::dev::kona::defs::pixel::*; + let mut b = pixel_window(); + at(&mut b, 10, 20); + draw_pixels(&mut b, 2, 2, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0xff00_00ff, 0x00ff_00ff, 0x0000_ffff, 0xffff_ffff]); + assert_eq!([colour_at(&mut b, 10, 20), colour_at(&mut b, 11, 20), colour_at(&mut b, 10, 21), colour_at(&mut b, 11, 21)], [0xffff_0000, 0xff00_ff00, 0xff00_00ff, 0xffff_ffff]); + assert_eq!(colour_at(&mut b, 12, 20), 0, "nothing past the image"); + command(&mut b, Grammar::Gl, SET_PIXEL_ZOOM_FACTORS, &[2.0f32.to_bits(), 3.0f32.to_bits()]); + at(&mut b, 30, 30); + draw_pixels(&mut b, 1, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0x1020_30ff]); + let lit = (28..40u32).flat_map(|y| (28..40u32).map(move |x| (x, y))).filter(|&(x, y)| colour_at(&mut b, x, y) == 0xff10_2030).collect::>(); + assert_eq!(lit, vec![(30, 30), (31, 30), (30, 31), (31, 31), (30, 32), (31, 32)], "two wide and three high"); + at(&mut b, 100, 100); + draw_pixels(&mut b, 1, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0xffff_ffff]); + assert_eq!(colour_at(&mut b, 63, 63), 0); + assert!(run(&mut b, &["gl"]).1.contains("1 with no valid raster position")); + } + + /// The pixel-store tokens set the unpack record that sizes the next draw; the transfer scale + /// and bias, and a pixel map under its flag, change what is drawn. + #[test] + fn unpack_state_transfer_and_maps_shape_a_draw() { + use crate::dev::kona::defs::pixel::*; + let mut b = pixel_window(); + at(&mut b, 5, 5); + // 3 x 1 RGB bytes: nine bytes are three words, with alignment 4 or 1. + let hdr = [3, 1, FORMAT_RGB, TYPE_UNSIGNED_BYTE, 0, 0, 0, 0]; + assert_eq!(command(&mut b, Grammar::Gl, DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, &hdr), Reply::Data(3)); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, args: &hdr, words: &[0, 0, 0] }); + // Rows of 3 x 3 bytes at alignment 4 pad to 12 bytes, so two rows are six words. + let hdr2 = [3, 2, FORMAT_RGB, TYPE_UNSIGNED_BYTE, 0, 0, 0, 0]; + assert_eq!(command(&mut b, Grammar::Gl, DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, &hdr2), Reply::Data(6)); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, args: &hdr2, words: &[0; 6] }); + command(&mut b, Grammar::Gl, SET_PIXEL_STORE_PARAMETER_INTEGER_FORM, &[UNPACK_BASE + 5, 1]); + assert_eq!(command(&mut b, Grammar::Gl, DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, &hdr2), Reply::Data(5), "alignment 1 packs the rows"); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, args: &hdr2, words: &[0; 5] }); + // A red scale of 0.5. + command(&mut b, Grammar::Gl, SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM, &[RED_SCALE, 0.5f32.to_bits()]); + at(&mut b, 20, 20); + draw_pixels(&mut b, 1, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0xff00_00ff]); + let c = colour_at(&mut b, 20, 20); + assert!((0x7f..=0x80).contains(&(c >> 16 & 0xff)), "{c:#x}"); + // A red-to-red map of two entries, loaded as a header and its values, flips red once its flag is set. + command(&mut b, Grammar::Gl, SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM, &[RED_SCALE, 1.0f32.to_bits()]); + let hdr = [MAP_RED_TO_RED, 2, 1.0f32.to_bits(), 0]; + assert_eq!(command(&mut b, Grammar::Gl, LOAD_PIXEL_MAP_FLOAT_VALUES, &hdr), Reply::Data(2)); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: LOAD_PIXEL_MAP_FLOAT_VALUES, args: &hdr, words: &[1.0f32.to_bits(), 0.0f32.to_bits()] }); + command(&mut b, Grammar::Gl, SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM, &[MAP_COLOUR, 1]); + at(&mut b, 22, 20); + draw_pixels(&mut b, 1, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0xff00_00ff]); + assert_eq!(colour_at(&mut b, 22, 20) >> 16 & 0xff, 0); + } + + /// A bitmap lights its set bits in the raster colour, most significant bit leftmost, and moves the + /// position by its advance. The small form's word count includes the byte count, as the recording's does. + #[test] + fn a_small_bitmap_draws_and_advances() { + let mut b = pixel_window(); + command(&mut b, Grammar::Gl, SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, &[1.0f32.to_bits(), 0, 0, 1.0f32.to_bits()]); + at(&mut b, 8, 8); + // Two rows of eight: 1010 0000 and 0000 0001. Origin (-1, 0): the glyph sits one pixel right; move 8 (origin plus move is 7). + let args = [8, 2, (-1.0f32).to_bits(), 0, 7.0f32.to_bits(), 0, 2, 2]; + assert_eq!(command(&mut b, Grammar::Gl, DRAW_BITMAP_FROM_DISPLAY_LIST, &args), Reply::Data(1)); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_BITMAP_FROM_DISPLAY_LIST, args: &args, words: &[0xa001_0000] }); + let lit = lit_pixels(&mut b); + // Bytes 0xa0 (bottom row) and 0x01 (row above): the bottom row is first, at y = 8. + assert_eq!(lit, vec![(9, 8), (11, 8), (16, 9)]); + // The advance moved the position to x = 16: the next bitmap starts there. + let one = [8, 1, 0, 0, 8.0f32.to_bits(), 0, 2, 1]; + command(&mut b, Grammar::Gl, DRAW_BITMAP_FROM_DISPLAY_LIST, &one); + send(&mut b, Job::Data { grammar: Grammar::Gl, selector: DRAW_BITMAP_FROM_DISPLAY_LIST, args: &one, words: &[0x8000_0000] }); + assert!(lit_pixels(&mut b).contains(&(16, 8))); + // An empty bitmap (a large one with no rows) moves the position and draws nothing. + let empty = [0, 0, 0, 0, 0, 0.0f32.to_bits(), 0.0f32.to_bits(), 5.0f32.to_bits(), 0.0f32.to_bits()]; + assert_eq!(command(&mut b, Grammar::Gl, DRAW_BITMAP, &empty), Reply::Done); + assert!(run(&mut b, &["gl"]).1.contains("3 bitmaps"), "{}", run(&mut b, &["gl"]).1); + } + + /// A copy takes the read buffer's rectangle to the raster position; a read packs it for the host; + /// depth and stencil copies and reads carry their planes. + #[test] + fn copy_and_read_pixels() { + use crate::dev::kona::defs::pixel::*; + let mut b = pixel_window(); + at(&mut b, 4, 4); + draw_pixels(&mut b, 2, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0x1122_33ff, 0x4455_66ff]); + // The read buffer is the back one the draw went to. + command(&mut b, Grammar::Gl, SELECT_READ_BUFFER, &[crate::dev::kona::defs::gl::BACK]); + command(&mut b, Grammar::Gl, SELECT_DRAW_BUFFER, &[crate::dev::kona::defs::gl::BACK]); + at(&mut b, 30, 30); + command(&mut b, Grammar::Gl, COPY_PIXEL_RECTANGLE_WITHIN_FRAME_BUFFER, &[4, 4, 2, 1, 0, COPY_COLOUR, 0]); + assert_eq!([colour_at(&mut b, 30, 30), colour_at(&mut b, 31, 30)], [0xff11_2233, 0xff44_5566]); + // The read's reply is the packed bytes. + let screen = b.gl.screen(); + let ds = b.contexts.select(None).gl.draw_state(screen); + b.arrays(&ds); + let c = b.contexts.select(None); + let env = gl::pixels::Env { store: &b.store, ds }; + let out = gl::pixels::plane_command(&mut c.gl, &mut b.pix_counts, &env, READ_PIXEL_RECTANGLE_BACK_TO_HOST, &[30, 30, 2, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, 0]); + assert_eq!(out, gl::pixels::Out::Reply(vec![0x11, 0x22, 0x33, 0xff, 0x44, 0x55, 0x66, 0xff])); + let out = gl::pixels::plane_command(&mut c.gl, &mut b.pix_counts, &env, READ_PIXEL_RECTANGLE_BACK_TO_HOST, &[30, 30, 1, 1, FORMAT_RED, TYPE_UNSIGNED_SHORT, 2]); + assert_eq!(out, gl::pixels::Out::Reply(vec![0, 0, 0x11, 0x11]), "the destination's byte offset leads"); + // The overlap query: the copy's source and its destination meet when the rectangles do. + let out = gl::pixels::plane_command(&mut c.gl, &mut b.pix_counts, &env, ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION, &[29, 29, 4, 4]); + assert_eq!(out, gl::pixels::Out::Reply(vec![0, 0, 0, 4])); + let out = gl::pixels::plane_command(&mut c.gl, &mut b.pix_counts, &env, ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION, &[0, 0, 4, 4]); + assert_eq!(out, gl::pixels::Out::Reply(vec![0, 0, 0, 0])); + } + + /// Stencil and depth images draw into their planes: a stencil image directly under the write + /// mask, a depth image as fragments at its depths. + #[test] + fn stencil_and_depth_draws() { + use crate::dev::kona::defs::pixel::*; + let mut b = pixel_window(); + command(&mut b, Grammar::Gl, CONFIGURE_FRAME_BUFFER_LAYOUT, &[0, 8, 0, 1, 0, 0, 24, 8, 0, 0]); + at(&mut b, 6, 6); + draw_pixels(&mut b, 2, 1, FORMAT_STENCIL, TYPE_UNSIGNED_BYTE, &[0x0507_0000]); + let (stencil, depth) = { + let l = b.contexts.select(None).gl.layout.clone().expect("a layout"); + (l.stencil.start as usize, l.depth.start as usize) + }; + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + let read = |b: &KonaBackend, w: usize, x: u32| b.store.read(x, 6, w); + assert_eq!([read(&b, stencil, 6), read(&b, stencil, 7)], [5, 7]); + // A depth image (half and full) under the always-pass depth test writes the depth plane. + command(&mut b, Grammar::Gl, SET_DEPTH_TEST, &[crate::dev::kona::defs::gl::ALWAYS]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[crate::dev::kona::defs::gl::DEPTH_TEST]); + at(&mut b, 6, 8); + draw_pixels(&mut b, 2, 1, FORMAT_DEPTH, TYPE_UNSIGNED_SHORT, &[0x8000_ffff]); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + let z = |x: u32| b.store.read(x, 8, depth); + assert!(z(6).abs_diff(0x8000_0000) < 0x0001_0000, "{:#x}", z(6)); + assert!(z(7) > 0xfffe_0000, "{:#x}", z(7)); + } + + /// Accumulation: load the colour buffer scaled, add, multiply, and return to the draw buffer. + #[test] + fn accumulation_loads_scales_and_returns() { + use crate::dev::kona::defs::pixel::*; + let mut b = pixel_window(); + command(&mut b, Grammar::Gl, CONFIGURE_FRAME_BUFFER_LAYOUT, &[0, 8, 0, 1, 0, 0, 24, 0, 16, 0]); + at(&mut b, 3, 3); + draw_pixels(&mut b, 1, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0x8040_00ff]); + command(&mut b, Grammar::Gl, SELECT_READ_BUFFER, &[crate::dev::kona::defs::gl::BACK]); + let acc = |b: &mut KonaBackend, op: u32, v: f32| { + command(b, Grammar::Gl, ACCUMULATION_BUFFER_OPERATION, &[op, v.to_bits()]); + }; + acc(&mut b, ACCUM_LOAD, 0.5); + acc(&mut b, ACCUM, 0.5); + acc(&mut b, ACCUM_MULT, 0.5); + acc(&mut b, ACCUM_ADD, 0.25); + // Clear the colour, then return the accumulated colour: 0.5 * c + 0.25. + command(&mut b, Grammar::Gl, SET_COLOUR_CLEAR_VALUE, &[0, 0, 0, 0]); + command(&mut b, Grammar::Gl, CLEAR_SELECTED_BUFFERS, &[crate::dev::kona::defs::gl::COLOR_BUFFER_BIT]); + acc(&mut b, ACCUM_RETURN, 1.0); + let c = colour_at(&mut b, 3, 3); + // red 0x80 -> 128/255 * 0.5 + 0.25 = 0.502 -> 0x80; green 0x40 -> 0.376 -> 0x60; blue 0 -> 0.25 -> 0x40 + assert_eq!([c >> 16 & 0xff, c >> 8 & 0xff, c & 0xff], [0x80, 0x60, 0x40], "{c:#x}"); + // Clearing the accumulation buffer takes the clear value. + command(&mut b, Grammar::Gl, SET_ACCUMULATION_BUFFER_CLEAR_VALUE, &[0.5f32.to_bits(), 0.5f32.to_bits(), 0.5f32.to_bits(), 1.0f32.to_bits()]); + command(&mut b, Grammar::Gl, CLEAR_SELECTED_BUFFERS, &[crate::dev::kona::defs::gl::ACCUM_BUFFER_BIT]); + acc(&mut b, ACCUM_RETURN, 1.0); + let c = colour_at(&mut b, 3, 3); + assert!(c >> 16 & 0xff >= 0x7f && c >> 16 & 0xff <= 0x80, "{c:#x}"); + assert!(run(&mut b, &["gl"]).1.contains("6 accumulations"), "{}", run(&mut b, &["gl"]).1); + } + + + /// A value of exactly 1.0 runs as two half passes: loading, accumulating and adding it lands where + /// the single pass would, and multiplying by it changes nothing. + #[test] + fn accumulation_by_exactly_one_is_two_half_passes() { + use crate::dev::kona::defs::pixel::*; + let mut b = pixel_window(); + command(&mut b, Grammar::Gl, CONFIGURE_FRAME_BUFFER_LAYOUT, &[0, 8, 0, 1, 0, 0, 24, 0, 16, 0]); + at(&mut b, 3, 3); + draw_pixels(&mut b, 1, 1, FORMAT_RGBA, TYPE_UNSIGNED_BYTE, &[0x8040_00ff]); + command(&mut b, Grammar::Gl, SELECT_READ_BUFFER, &[crate::dev::kona::defs::gl::BACK]); + let acc = |b: &mut KonaBackend, op: u32, v: f32| { + command(b, Grammar::Gl, ACCUMULATION_BUFFER_OPERATION, &[op, v.to_bits()]); + }; + let back = |b: &mut KonaBackend| { + command(b, Grammar::Gl, SET_COLOUR_CLEAR_VALUE, &[0, 0, 0, 0]); + command(b, Grammar::Gl, CLEAR_SELECTED_BUFFERS, &[crate::dev::kona::defs::gl::COLOR_BUFFER_BIT]); + acc(b, ACCUM_RETURN, 1.0); + let c = colour_at(b, 3, 3); + [c >> 16 & 0xff, c >> 8 & 0xff, c & 0xff] + }; + acc(&mut b, ACCUM_LOAD, 1.0); + assert_eq!(back(&mut b), [0x80, 0x40, 0], "a load of 1.0 is the colour"); + acc(&mut b, ACCUM_MULT, 1.0); + assert_eq!(back(&mut b), [0x80, 0x40, 0], "multiplying by 1.0 changes nothing"); + acc(&mut b, ACCUM, 1.0); + acc(&mut b, ACCUM_MULT, 0.5); + assert_eq!(back(&mut b), [0x80, 0x40, 0], "accumulating 1.0 twice the colour, halved"); + acc(&mut b, ACCUM_ADD, 1.0); + assert_eq!(back(&mut b), [0xff, 0xff, 0xff], "an add of 1.0 saturates a return"); + } + + // ---- Queries, select and feedback ---- + + /// A feedback pass takes what survives clipping and culling as GL's records and draws nothing; a + /// pass-through marker is a record of its own; a culled polygon is not fed back. + #[test] + fn a_feedback_pass_collects_records_and_draws_nothing() { + use crate::dev::kona::back::gl::query::*; + let mut b = gl_window(); + b.contexts.select(None).gl.feed.start(256); + command(&mut b, Grammar::Gl, SELECT_FEEDBACK_RECORD_TYPE, &[FEEDBACK_3D]); + command(&mut b, Grammar::Gl, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[FEEDBACK]); + command(&mut b, Grammar::Gl, INSERT_FEEDBACK_PASS_THROUGH_MARKER, &[7.0f32.to_bits()]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[(8.0, 8.0), (40.0, 8.0), (8.0, 40.0)]); + vertices(&mut b, BEGIN_PRIMITIVE_POINTS, &[(20.0, 30.0)]); + // The back-face cull: this winding is clockwise. + command(&mut b, Grammar::Gl, SET_FACE_CULLING, &[crate::dev::kona::defs::gl::BACK]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[crate::dev::kona::defs::gl::CULL_FACE]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[(8.0, 8.0), (8.0, 40.0), (40.0, 8.0)]); + command(&mut b, Grammar::Gl, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[RENDER]); + let (words, overflow) = b.contexts.select(None).gl.feed.take(); + assert!(!overflow); + let f = |v: f32| v.to_bits(); + // Pass-through; a polygon of three vertices, x y z each (z is half the depth range for z = 0, as the + // formatter's scales leave it); a point. The culled triangle is absent. + let half = 0.5 * 1_048_575.0 / 1_048_576.0; + let want = [PASS_THROUGH, f(7.0), POLYGON, 3, f(8.0), f(8.0), f(half), f(40.0), f(8.0), f(half), f(8.0), f(40.0), f(half), POINT, f(20.0), f(30.0), f(half)]; + assert_eq!(words, want); + assert!(lit_pixels(&mut b).is_empty(), "feedback draws nothing"); + assert!(run(&mut b, &["gl"]).1.contains("1 passes")); + } + + /// A select pass writes a hit per name change: the marker, the name count, the nearest and farthest depth, the names. + #[test] + fn a_select_pass_records_hits_by_name() { + use crate::dev::kona::back::gl::query::*; + let mut b = gl_window(); + b.contexts.select(None).gl.feed.start(256); + command(&mut b, Grammar::Gl, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[SELECT]); + command(&mut b, Grammar::Gl, CLEAR_SELECTION_NAME_STACK, &[0]); + command(&mut b, Grammar::Gl, PUSH_SELECTION_NAME, &[5]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[(8.0, 8.0), (40.0, 8.0), (8.0, 40.0)]); + command(&mut b, Grammar::Gl, REPLACE_TOP_SELECTION_NAME, &[6]); + // Beyond the far plane (z = 2): no hit. + command(&mut b, Grammar::Gl, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[0]); + for (x, y) in [(8.0, 8.0), (40.0, 8.0), (8.0, 40.0)] { + command(&mut b, Grammar::Gl, SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, &[ndc(x), ndc(y), 2.0f32.to_bits()]); + } + command(&mut b, Grammar::Gl, END_OPEN_PRIMITIVE, &[0]); + command(&mut b, Grammar::Gl, REPLACE_TOP_SELECTION_NAME, &[7]); + vertices(&mut b, BEGIN_PRIMITIVE_POINTS, &[(10.0, 10.0)]); + command(&mut b, Grammar::Gl, SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, &[RENDER]); + let (words, _) = b.contexts.select(None).gl.feed.take(); + let z = (0.5f32 * 4_294_967_040.0) as u32; + assert_eq!(words, vec![1, 1, z, z, 5, 1, 1, z, z, 7]); + assert!(lit_pixels(&mut b).is_empty()); + } + + /// The query tokens reach the context's state: the model-view matrix and the error come back, an + /// unknown generic name is counted. + #[test] + fn queries_are_answered_from_the_contexts_state() { + let mut b = gl_window(); + command(&mut b, Grammar::Gl, SELECT_CURRENT_MATRIX_STACK, &[crate::dev::kona::defs::gl::MODELVIEW]); + command(&mut b, Grammar::Gl, TRANSLATE_FLOATS, &[2.0f32.to_bits(), 3.0f32.to_bits(), 4.0f32.to_bits()]); + let c = b.contexts.select(None); + let r = gl::query::answer(&mut c.gl, &mut b.query_counts, QUERY_STATE_GENERIC, &[0xba6]); + assert_eq!(r.len(), 16); + assert_eq!([f32::from_bits(r[12]), f32::from_bits(r[13]), f32::from_bits(r[14]), f32::from_bits(r[15])], [2.0, 3.0, 4.0, 1.0]); + // A query token through the job path counts (no return page here, so nothing is posted). + command(&mut b, Grammar::Gl, QUERY_STATE_GENERIC, &[0x9999]); + command(&mut b, Grammar::Gl, QUERY_HISTOGRAM, &[0]); + let text = run(&mut b, &["gl"]).1; + assert!(text.contains("3 answered") && text.contains("0x9999 x 1") && text.contains("0x104 x 1"), "{text}"); + } + + + // ---- Multisample ---- + + /// `gl_window` reconfigured with `samples` samples and a 24-bit depth buffer. + fn ms_window(samples: u32) -> KonaBackend { + let mut b = gl_window(); + command(&mut b, Grammar::Gl, CONFIGURE_FRAME_BUFFER_LAYOUT, &[0, 8, 0, 1, 0, samples, 24, 0, 0, 0]); + b + } + + /// A rectangle of two triangles from (2, 2) to (`right`, 20). + fn rectangle(b: &mut KonaBackend, right: f32) { + vertices(b, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[(2.0, 2.0), (right, 2.0), (right, 20.0), (2.0, 2.0), (right, 20.0), (2.0, 20.0)]); + } + + fn column(b: &mut KonaBackend, x: u32, y: u32) -> u32 { + command(b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + b.store.read(x, y, 2) + } + + /// In a multisampled window a smoothed line takes coverage (smoothing wins for lines) while a smoothed + /// polygon multisamples (multisampling wins for polygons). + #[test] + fn a_multisampled_window_smooths_lines_and_multisamples_polygons() { + use crate::dev::kona::defs::gl::{LINE_SMOOTH, POLYGON_SMOOTH}; + let mut b = ms_window(4); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[LINE_SMOOTH]); + vertices(&mut b, BEGIN_PRIMITIVE_INDEPENDENT_LINES, &[(10.5, 21.0), (40.5, 21.0)]); + command(&mut b, Grammar::Kernel, WINDOW_SYSTEM_PIXEL_PATH_FINISH, &[0]); + assert_eq!([alpha_at(&b, 25, 19), alpha_at(&b, 25, 20), alpha_at(&b, 25, 21), alpha_at(&b, 25, 22)], [0, 128, 128, 0], "half coverage a row either side"); + let mut b = ms_window(4); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[POLYGON_SMOOTH]); + rectangle(&mut b, 10.75); + assert_eq!([column(&mut b, 9, 10), column(&mut b, 10, 10), column(&mut b, 11, 10)], [0xffff_ffff, 0xff80_8080, 0], "the multisampled edge, not a ramp"); + } + + /// The right edge of a rectangle at 10.75 (10.25 once the half pixel comes off) falls between samples of its + /// column: the 4-sample pattern's samples at 0 and 2 eighths across are in (one on the edge, which a right edge + /// keeps), those at 4 and 6 out, so half the pixel is white; with 8 samples (0, 6, 5, 1, 2, 7, 3, 4 eighths across) + /// three are in. A column left of it is whole and one right of it empty; the shared diagonal of the two triangles + /// leaves no seam. + #[test] + fn a_four_and_an_eight_sample_edge_share_the_pixel_by_samples() { + let mut b = ms_window(4); + rectangle(&mut b, 10.75); + assert_eq!([column(&mut b, 9, 10), column(&mut b, 10, 10), column(&mut b, 11, 10)], [0xffff_ffff, 0xff80_8080, 0]); + // Every pixel the diagonal crosses is whole: no seam between the triangles. + let seams: Vec<_> = (3..10).flat_map(|x| (3..19).map(move |y| (x, y))).filter(|&(x, y)| column(&mut b, x, y) != 0xffff_ffff).collect(); + assert!(seams.is_empty(), "{seams:?}"); + let mut b = ms_window(8); + rectangle(&mut b, 10.75); + // Three of eight samples: (3 * 255 + 4) / 8 = 96. + assert_eq!([column(&mut b, 9, 10), column(&mut b, 10, 10), column(&mut b, 11, 10)], [0xffff_ffff, 0xff60_6060, 0]); + let seams: Vec<_> = (3..10).flat_map(|x| (3..19).map(move |y| (x, y))).filter(|&(x, y)| column(&mut b, x, y) != 0xffff_ffff).collect(); + assert!(seams.is_empty(), "{seams:?}"); + } + + /// With the enable off, or no multisample buffer, the same edge is the single-sample one: the pixel is whole or empty. + #[test] + fn without_multisampling_an_edge_is_whole_or_empty() { + let mut b = ms_window(4); + command(&mut b, Grammar::Gl, DISABLE_CAPABILITY, &[crate::dev::kona::defs::gl::MULTISAMPLE]); + rectangle(&mut b, 10.875); + // The sample at the pixel's corner: 10.0 < 10.375, inside. + assert_eq!([column(&mut b, 10, 10), column(&mut b, 11, 10)], [0xffff_ffff, 0]); + let mut b = ms_window(0); + rectangle(&mut b, 10.875); + assert_eq!([column(&mut b, 10, 10), column(&mut b, 11, 10)], [0xffff_ffff, 0]); + } + + /// Depth is kept a sample: a nearer rectangle and a farther one that share a pixel's edge each win the samples + /// they cover, and a farther fragment does not overwrite a nearer sample. A clear sets every sample. + #[test] + fn depth_and_clears_are_per_sample() { + use crate::dev::kona::defs::gl::*; + let mut b = ms_window(4); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[DEPTH_TEST]); + command(&mut b, Grammar::Gl, SET_DEPTH_TEST, &[LESS]); + command(&mut b, Grammar::Gl, SET_COLOUR_CLEAR_VALUE, &[0, 0, 0.5f32.to_bits(), 1.0f32.to_bits()]); + command(&mut b, Grammar::Gl, CLEAR_SELECTED_BUFFERS, &[COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT]); + assert_eq!(column(&mut b, 5, 5), 0xff00_0080, "a clear sets the samples and the word"); + // A white rectangle at z = 0 (nearer), then a red one behind it at z = 0.5 that reaches further right. + let rect = |b: &mut KonaBackend, right: f32, z: f32| { + command(b, Grammar::Gl, BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, &[0]); + for (x, y) in [(2.0, 2.0), (right, 2.0), (right, 20.0), (2.0, 2.0), (right, 20.0), (2.0, 20.0)] { + command(b, Grammar::Gl, SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, &[ndc(x), ndc(y), z.to_bits()]); + } + command(b, Grammar::Gl, END_OPEN_PRIMITIVE, &[0]); + }; + rect(&mut b, 10.875, 0.0); + command(&mut b, Grammar::Gl, SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, &[1.0f32.to_bits(), 0, 0, 1.0f32.to_bits()]); + rect(&mut b, 14.875, 0.5); + // Column 5 stays white (the red is behind); column 10 has two white samples and two red ones that the + // far rectangle won where the near one is not (samples 2 and 3): mean red (2 * 255 + 2 * 255 + 2) / 4 = 255, + // green and blue (2 * 255 + 2) / 4 = 128; column 12 is red. + assert_eq!(column(&mut b, 5, 10), 0xffff_ffff); + assert_eq!(column(&mut b, 10, 10) & 0xffffff, 0xff8080); + assert_eq!(column(&mut b, 12, 10) & 0xffffff, 0xff0000); + } + + /// The sample mask keeps the samples its coverage word names, by pixel column: the half-coverage mask on 4 + /// samples is 0x33333333, nibble 3 (samples 0 and 1) a column: half the pixel. + #[test] + fn the_sample_mask_and_alpha_to_mask_thin_the_coverage() { + use crate::dev::kona::defs::gl::*; + let mut b = ms_window(4); + command(&mut b, Grammar::Gl, SET_MULTISAMPLE_COVERAGE_MASK, &[0.5f32.to_bits(), 0]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[SAMPLE_MASK]); + rectangle(&mut b, 10.875); + // Mask 0.5 selects the 15-16 entry: 0x33333333, samples 0 and 1 of every pixel: half of a whole pixel. + assert_eq!(column(&mut b, 5, 10), 0xff80_8080); + // Inverted, the other half. + let mut b = ms_window(4); + command(&mut b, Grammar::Gl, SET_MULTISAMPLE_COVERAGE_MASK, &[0.5f32.to_bits(), 1]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[SAMPLE_MASK]); + rectangle(&mut b, 10.875); + assert_eq!(column(&mut b, 5, 10), 0xff80_8080); + // Alpha to mask with alpha 0.5 lets half the samples through; alpha to one then makes the fragment opaque. + let mut b = ms_window(4); + command(&mut b, Grammar::Gl, SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, &[1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits(), 0.5f32.to_bits()]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[SAMPLE_ALPHA_TO_MASK]); + command(&mut b, Grammar::Gl, ENABLE_CAPABILITY, &[SAMPLE_ALPHA_TO_ONE]); + rectangle(&mut b, 10.875); + assert_eq!(column(&mut b, 5, 10), 0xff80_8080); + // A bad pattern is an invalid-enumerant error; a good one is taken. + command(&mut b, Grammar::Gl, SET_MULTISAMPLE_PATTERN, &[0x1234]); + assert_eq!(b.contexts.select(None).gl.error, 0x500); + command(&mut b, Grammar::Gl, SET_MULTISAMPLE_PATTERN, &[0x80a3]); + } + +} + +#[cfg(test)] +mod region_tests { + use super::*; + + #[test] + fn region_commands_are_counted_with_their_sizes() { + let mut r = RegionCounts::default(); + r.note(PIPE_MEMORY_RESERVE, &[2, 9, 0, 1]); + r.note(PIPE_MEMORY_COPY_DOWN, &[2, 0, 0, 0]); + r.note(PIPE_MEMORY_SWAP_OUT, &[3, 0, 4, 0x400, 1, 0x20, 0x9000, 0x102]); + r.note(PIPE_MEMORY_SWAP_IN, &[3, 0x10, 2, 0x400, 1, 0x20]); + let seen = r.clone(); + r.note(PIPE_MEMORY_RESERVE + 0x400, &[]); + r.note(0x154, &[]); + assert_eq!(r, seen, "a command that is no region command counts nothing"); + assert_eq!(r, RegionCounts { placed: 1, copy_downs: 1, swapped_out: 1, swapped_in: 1, bytes_out: 4096, bytes_in: 2048 }); + } +} diff --git a/src/dev/kona/back/raster/fog.rs b/src/dev/kona/back/raster/fog.rs new file mode 100644 index 000000000..c0aaa3117 --- /dev/null +++ b/src/dev/kona/back/raster/fog.rs @@ -0,0 +1,208 @@ +//! Fog, per fragment, on plain data. +//! +//! What is known of the pipe: it keeps a fog enable and +//! the mode, density, start, end and colour parameters (generic state rows 0xb60 to 0xb66, established +//! as rows), the fog parameter setter takes five words (established), and the raster side has fog +//! control, colour and depth-offset registers and a fog function table (shadow words 82 to 85 and +//! the table setter, all inferred). The pipe evaluates the function on the eye distance recovered +//! from the window depth, and builds a table of 12-bit unsigned entries indexed by that depth plus +//! an offset (the entry width is established; the depth indexing is inferred). The model keeps the +//! function and the 12-bit entry and approximates the table: it evaluates the function at each +//! fragment's eye distance, interpolated perspective-correctly, instead of at depth steps, then +//! rounds the factor to 12 bits (`ENTRY_ONE` is 1.0; the hardware's conversion constant is +//! unknown). The blend's rounding, the fog index of the colour-index mode and the order of fog +//! among the fragment operations (after the texture environment and before the alpha test, +//! OpenGL's order) are unknown for the hardware and the model's own. +//! +//! The factor is OpenGL's published one, clamped to 0..1: linear `(end - c) / (end - start)`, +//! `exp(-density c)` and `exp(-(density c)^2)`; the fragment's red, green and blue become +//! `f C + (1 - f) Cf` and alpha is untouched. `exp` is not the host's: `exp_neg` is single-precision +//! multiplies and adds only, so a result is the same on every host (this file is held to the same +//! rule as `pixel`). + +use crate::dev::kona::back::raster::DrawState; + +/// Which curve a draw's fog follows. A draw is run by a function specialised on it once, so a +/// fragment of a draw with fog off or of one curve does not test the others. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Curve { + Linear, + Exp, + Exp2, +} + +/// Fog as a draw sees it, from the context's parameters when the draw state is built. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Fog { + pub curve: Curve, + /// Linear: the end distance; the exponential curves: the density. + pub a: f32, + /// Linear: 1 / (end - start), or a very large step when they are equal (the equation divides + /// by zero there; the model takes the limit, 1 before the end and 0 from it); unused otherwise. + pub b: f32, + /// The colour in 13-bit units (8191 is full intensity). + pub colour: [f32; 3], +} + +impl Fog { + /// What a draw with fog off holds in place of fog, for the specialised loop that never reads it. + pub const OFF: Fog = Fog { curve: Curve::Exp, a: 0.0, b: 0.0, colour: [0.0; 3] }; + + /// The fog of `fog`'s parameters, or None with fog off. + pub fn new(on: bool, mode: u32, density: f32, start: f32, end: f32, colour: [f32; 3]) -> Option { + use crate::dev::kona::defs::gl; + on.then(|| { + let curve = match mode { + gl::LINEAR => Curve::Linear, + gl::EXP2 => Curve::Exp2, + _ => Curve::Exp, + }; + let (a, b) = match curve { + Curve::Linear => (end, if end == start { 1.0e30 } else { 1.0 / (end - start) }), + _ => (density, 0.0), + }; + Fog { curve, a, b, colour: colour.map(|c| c.clamp(0.0, 1.0) * 8191.0) } + }) + } +} + +/// `exp(-x)` for `x` >= 0, from multiplies and adds alone. `x` is split as `k ln 2 + r`; `e^-r` for `r` +/// under `ln 2` is a degree-9 Taylor polynomial (relative error about 1e-7, far below the 13-bit +/// steps the factor is spent on), and 2^-k is built from its exponent. Past 12 the result is under 6e-6 and +/// is taken as 0. +#[inline(always)] +pub fn exp_neg(x: f32) -> f32 { + const LN2: f32 = 0.693_147_2; + const LOG2E: f32 = 1.442_695; + if !(x < 12.0f32) { + return 0.0; + } + let x = x.max(0.0f32); + let k = (x * LOG2E) as u32; + let r = x - k as f32 * LN2; + // 1 - r + r^2/2 - ... - r^9/362880, Horner's form. + let mut p = -1.0f32 / 362880.0; + for c in [1.0f32 / 40320.0, -1.0 / 5040.0, 1.0 / 720.0, -1.0 / 120.0, 1.0 / 24.0, -1.0 / 6.0, 0.5, -1.0, 1.0] { + p = p * r + c; + } + p * f32::from_bits((127 - k) << 23) +} + +impl Fog { + /// The factor for fog coordinate `c`, 1 (no fog) to 0 (all fog). + #[inline(always)] + pub fn factor(&self, c: f32) -> f32 { + let f = match CURVE { + LINEAR => (self.a - c) * self.b, + EXP => exp_neg(self.a * c), + _ => { + let u = self.a * c; + exp_neg(u * u) + } + }; + // A table entry is 12 bits (approximated: nearest, 1.0 as all ones). + ((f.clamp(0.0f32, 1.0f32) * ENTRY_ONE + 0.5f32) as u32) as f32 / ENTRY_ONE + } + + /// Fragment colour `src` (13-bit red, green, blue, alpha) with factor `f` applied. + #[inline(always)] + pub fn apply(&self, src: [u32; 4], f: f32) -> [u32; 4] { + let g = 1.0f32 - f; + let m = |k: usize| (f * src[k] as f32 + g * self.colour[k] + 0.5f32) as u32; + [m(0), m(1), m(2), src[3]] + } +} + +/// All ones in a 12-bit fog table entry. +const ENTRY_ONE: f32 = 4095.0; + +/// The curve numbers `Fog::factor` is specialised on, and what `curve` names. +pub const LINEAR: u8 = 1; +pub const EXP: u8 = 2; +pub const EXP2: u8 = 3; + +/// The curve number of a draw's fog, 0 with fog off. +pub fn curve_of(ds: &DrawState) -> u8 { + match ds.fog.map(|f| f.curve) { + None => 0, + Some(Curve::Linear) => LINEAR, + Some(Curve::Exp) => EXP, + Some(Curve::Exp2) => EXP2, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::defs::gl; + + #[test] + fn exp_neg_follows_the_exponential_to_a_part_in_five_million() { + let mut worst = 0.0f64; + let mut x = 0.0f32; + while x < 12.0 { + let want = (-(x as f64)).exp(); + worst = worst.max(((exp_neg(x) as f64 - want) / want).abs()); + x += 0.0137; + } + assert!(worst < 1e-6, "worst relative error {worst}"); + assert_eq!(exp_neg(0.0), 1.0); + assert_eq!(exp_neg(12.0), 0.0); + assert_eq!(exp_neg(f32::NAN), 0.0, "not a number is all fog, not a panic"); + assert_eq!(exp_neg(-3.0), 1.0, "a negative argument is no fog"); + } + + fn fog(mode: u32) -> Fog { + Fog::new(true, mode, 0.5, 2.0, 6.0, [1.0, 0.5, 0.0]).expect("on") + } + + #[test] + fn off_is_none_and_the_curves_are_the_published_ones() { + assert!(Fog::new(false, gl::EXP, 1.0, 0.0, 1.0, [0.0; 3]).is_none()); + let l = fog(gl::LINEAR); + assert_eq!([l.factor::(2.0), l.factor::(6.0), l.factor::(9.0), l.factor::(0.0)], [1.0, 0.0, 0.0, 1.0]); + assert_eq!(l.factor::(4.0), 2048.0 / ENTRY_ONE, "halfway is entry 2048 of 4095"); + let e = fog(gl::EXP); + // The factor is a 12-bit entry, so it lands within half a step of the curve. + let step = 0.5 / ENTRY_ONE + 1e-6; + assert!((e.factor::(2.0) - (-1.0f32).exp()).abs() < step); + let e2 = fog(gl::EXP2); + assert!((e2.factor::(2.0) - (-1.0f32).exp()).abs() < step, "exp(-(0.5 * 2)^2) = exp(-1)"); + assert!((e2.factor::(4.0) - (-4.0f32).exp()).abs() < step); + } + + #[test] + fn the_factor_is_a_twelve_bit_entry() { + let l = fog(gl::LINEAR); + for c in [2.0f32, 2.001, 3.3333, 4.5, 5.99] { + let f = l.factor::(c); + assert_eq!(f * ENTRY_ONE, (f * ENTRY_ONE).round(), "{c}: {f}"); + } + assert_eq!(l.factor::(2.0), 1.0, "no fog is all ones"); + } + + #[test] + fn a_linear_fog_with_start_at_end_steps_at_the_end() { + let f = Fog::new(true, gl::LINEAR, 1.0, 3.0, 3.0, [0.0; 3]).expect("on"); + assert_eq!([f.factor::(2.999), f.factor::(3.0), f.factor::(3.001)], [1.0, 0.0, 0.0]); + } + + #[test] + fn the_blend_moves_red_green_and_blue_toward_the_fog_colour_and_leaves_alpha() { + let f = fog(gl::LINEAR); + let c = [8191, 0, 4000, 1234]; + assert_eq!(f.apply(c, 1.0), c); + assert_eq!(f.apply(c, 0.0), [8191, 4096, 0, 1234], "all fog: the fog colour, 0.5 rounded to 4096"); + assert_eq!(f.apply([0, 8191, 0, 7], 0.5), [4096, 6143, 0, 7]); + } + + #[test] + fn the_draw_names_its_curve() { + let mut ds = DrawState::default(); + assert_eq!(curve_of(&ds), 0); + for (mode, want) in [(gl::LINEAR, LINEAR), (gl::EXP, EXP), (gl::EXP2, EXP2)] { + ds.fog = Fog::new(true, mode, 1.0, 0.0, 1.0, [0.0; 3]); + assert_eq!(curve_of(&ds), want); + } + } +} diff --git a/src/dev/kona/back/raster/mod.rs b/src/dev/kona/back/raster/mod.rs new file mode 100644 index 000000000..15553777b --- /dev/null +++ b/src/dev/kona/back/raster/mod.rs @@ -0,0 +1,519 @@ +//! Drawing into the pixel store. +//! +//! The integer primitives the textport and the X server use: points, boxes +//! and bitmaps written into one word position of the store, clipped to the +//! managed area. The store is bottom-origin, y up, and the textport's +//! coordinates are the board's own, written as given. The X server's run down +//! from the top of the screen, so its fills mirror the row (`x::fill`); both were +//! settled by comparing the store rendered from the recordings with their +//! screen captures. The display then flips the rows once. +//! +//! # The fill rule +//! +//! For a box with corners `x1 < x2`, `y1 < y2` the board lights columns +//! `x1 + 1` to `x2` and rows `y1 + 1` to `y2`: the left column and bottom row of +//! a whole-pixel box are left out (established per the hardware documents for boxes sent +//! as four unchanged corners; the grade of that rests on a ruling still to +//! come). `lit` is the one place that rule lives, so a ruling changes a function and its grade. +//! +//! # The GL stage, and what it leaves out +//! +//! `setup` and `pixel` draw the GL client's triangles on the tile scheduler +//! (`back::tiles`): the 1/8-pixel set-up, the fill rule, colour, depth, the +//! scissor and polygon offset; then, per fragment, window-ID ownership, the +//! polygon and line stipples, the alpha test, stencil, depth, blend, the logic +//! operation and the write masks, and, for a bound 1D or 2D texture, sampling +//! with a level of detail taken from the 2 x 2 quad. A multisampled window runs +//! the same tests per sample (`multisample`). Points and lines reach set-up as +//! triangles (`gl`: a point is a square or a disc, a line a rectangle about its +//! segment). What the stage does not do yet, each a gap and not a decision: +//! +//! - nothing for the board's fog table or its depth-offset register: fog is +//! applied per fragment from the interpolated eye distance (`fog`); +//! - depth is the 32.16 fixed-point plane of `pixel`, clamped to the 32-bit +//! fraction, not the board's float or compressed depth forms; +//! - the order of the fragment operations, the blend and dither arithmetic and +//! the line and point shapes are the model's own, not read from the board. + +pub mod fog; +pub mod multisample; +pub mod ops; +pub mod pixel; +pub mod texel; +pub mod setup; +pub mod smooth; +use super::store::{PixelStore, WordArray}; +use super::tiles::Tiles; +use ops::{BlendEq, BlendFactor, Compare, LogicOp, Ownership, StencilOp}; + +/// A rectangle in pixels, far corner exclusive: the only rectangle type. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Rect { + pub x0: i32, + pub y0: i32, + pub x1: i32, + pub y1: i32, +} + +impl Rect { + pub fn is_empty(&self) -> bool { + self.x0 >= self.x1 || self.y0 >= self.y1 + } + + pub fn intersect(&self, o: &Rect) -> Rect { + Rect { x0: self.x0.max(o.x0), y0: self.y0.max(o.y0), x1: self.x1.min(o.x1), y1: self.y1.min(o.y1) } + } +} + +/// A vertex handed to set-up: relative to the window origin, at the vertex +/// bus's precision. `x` and `y` are on a 1/256-pixel grid (the bus carries 20 +/// bits, unsigned; the model keeps the sign, since a window origin can be +/// negative and set-up works in integers), `z` is a 32-bit fraction of the +/// depth range, `c` four 13-bit components (8191 is full intensity) and `t` +/// two sets of the texture words `s/w, t/w, r/w, q/w`, each linear in screen +/// space (zero when untextured). That a textured vertex carries a fourth word +/// is inferred, and so is its form (1/w, q/w or 1/q): the model uses q/w, +/// which is 1/w for the usual q of 1 and keeps the divide right when q is not. +/// +/// Positions are rounded onto the 1/256 grid once, to the nearest with ties to even (`vertex::grid`). +/// The half-pixel GL offset is set-up's conversion and is not applied here. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct WVertex { + pub x: i32, + pub y: i32, + pub z: u32, + pub c: [u16; 4], + pub t: [[f32; 4]; 2], + /// How far along its line the vertex is, in pixels (zero for a triangle's). + pub d: f32, + /// The fog coordinate over w, and 1 / w: both linear in screen space, so the fragment's + /// coordinate is their quotient (zero when fog is off). + pub fog: [f32; 2], +} + +/// A glClear, over `rect` (the draw state's clip rectangle; the viewport does +/// not apply). Each plane the request names is cleared under its write mask. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ClearReq { + pub rect: Rect, + /// The colour word to write, `0xAARRGGBB` from the clear colour, or None. + pub colour: Option, + /// The depth fraction to write, or None. + pub depth: Option, + /// The stencil value to write, or None. + pub stencil: Option, + /// Which of red, green, blue and alpha the clear writes, whether depth is + /// written, and which stencil bits. + pub colour_mask: [bool; 4], + pub depth_mask: bool, + pub stencil_mask: u8, +} + +impl Default for ClearReq { + fn default() -> Self { + ClearReq { rect: Rect::default(), colour: None, depth: None, stencil: None, colour_mask: [true; 4], depth_mask: true, stencil_mask: 0xff } + } +} + +/// The alpha test: a fragment passes when its alpha, in 13-bit units, compares +/// with `reference` (`func.passes(alpha, reference)`). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct AlphaTest { + pub enabled: bool, + pub func: Compare, + pub reference: u32, +} + +/// The stencil test and its operations: the test compares `reference & read_mask` +/// with the stored value under `read_mask`; each operation's result is written +/// under `write_mask`. One state serves front and back faces: a model limit, since +/// the tokens name no face and no recording sends them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct StencilState { + pub enabled: bool, + pub func: Compare, + pub reference: u8, + pub read_mask: u8, + pub write_mask: u8, + pub fail: StencilOp, + pub zfail: StencilOp, + pub zpass: StencilOp, +} + +impl Default for StencilState { + fn default() -> Self { + StencilState { enabled: false, func: Compare::Always, reference: 0, read_mask: 0xff, write_mask: 0xff, fail: StencilOp::Keep, zfail: StencilOp::Keep, zpass: StencilOp::Keep } + } +} + +/// Blending: `src * src_factor + dst * dst_factor` under `eq` (add is all there is), with +/// the constant colour in 13-bit units. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BlendState { + pub enabled: bool, + pub src: BlendFactor, + pub dst: BlendFactor, + pub eq: BlendEq, + pub constant: [u32; 4], +} + +impl Default for BlendState { + fn default() -> Self { + BlendState { enabled: false, src: BlendFactor::One, dst: BlendFactor::Zero, eq: BlendEq::Add, constant: [0; 4] } + } +} + +/// What a draw runs under, as plain data: it is copied into the scheduler's +/// draw-state table and read there by the workers. Built from the context's GL +/// state when that state changes. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct DrawState { + /// The window's origin in plane pixels; a vertex is relative to it. + pub origin: [i32; 2], + /// Where a draw can write: the window cut to the scissor when it is on and + /// to the screen. + pub clip: Rect, + /// The viewport in plane pixels. A primitive is cut to it as well. + pub viewport: Rect, + /// The colour words of the buffers drawn to, and how many. + pub colour: [u32; 2], + pub colours: u8, + /// The depth word and the bits of depth in it; 0 bits: no depth plane. + pub depth_word: u32, + pub depth_bits: u32, + pub depth_test: bool, + pub depth_func: Compare, + /// The depth plane is written by a passing fragment. + pub depth_mask: bool, + /// The stencil word, and its bits (8 or 0: none). + pub stencil_word: u32, + pub stencil_bits: u32, + pub stencil: StencilState, + pub alpha: AlphaTest, + pub blend: BlendState, + /// A logic operation applied to the colour written and the buffer's, or None. + pub logic: Option, + /// Which of red, green, blue and alpha a draw writes. + pub colour_mask: [bool; 4], + /// Dither is carried and applied as off: no source gives the matrix. + pub dither: bool, + /// Polygon offset, added to the depth plane in set-up: `factor` times the + /// larger depth slope plus `units` times the smallest depth step. + pub offset: Option<[f32; 2]>, + /// The window-ID test, when the window's enable is set. A fragment whose pixel + /// fails it is dropped. X drawing does not take it (approximated: the X server paints + /// the display-ID plane itself, and no source says the pipe tests its fills). + pub ownership: Option, + /// The line stipple: the 16-bit pattern and the repeat count of each bit. + pub line_stipple: Option<(u16, u32)>, + /// The polygon stipple: 32 rows of 32 bits, the most significant bit leftmost. + pub polygon_stipple: Option<[u32; 32]>, + /// The bound texture and its environment, when texturing is on. + pub texture: Option, + /// The samples of a multisampled window, when multisampling is on. + pub samples: Option, + /// Fog, when it is on. + pub fog: Option, + /// Which primitives are smoothed. + pub smooth: Smooth, +} + +/// The smoothing switches a draw runs under. A multisampled window smooths lines only (see `smooth`). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Smooth { + pub points: bool, + pub lines: bool, + pub polygons: bool, +} + +impl Default for DrawState { + fn default() -> Self { + DrawState { + origin: [0; 2], + clip: Rect::default(), + viewport: Rect::default(), + colour: [0; 2], + colours: 0, + depth_word: 0, + depth_bits: 0, + depth_test: false, + depth_func: Compare::Always, + depth_mask: true, + stencil_word: 0, + stencil_bits: 0, + stencil: StencilState::default(), + alpha: AlphaTest::default(), + blend: BlendState::default(), + logic: None, + colour_mask: [true; 4], + dither: false, + offset: None, + ownership: None, + line_stipple: None, + polygon_stipple: None, + texture: None, + samples: None, + fog: None, + smooth: Smooth::default(), + } + } +} + +/// What the GL path made in one job, for `KonaBackend` to flush into set-up: +/// triangles and clears, and the draw state they run under. +#[derive(Default)] +pub struct Prims { + pub tris: Vec<[WVertex; 3]>, + /// Triangles that make up lines: the rectangle about a segment, each vertex + /// carrying `d`, so the line stipple can index its pattern. + pub lines: Vec<[WVertex; 3]>, + /// The smoothed polygons and lines, each as a run of `aa_verts`. + pub aa: Vec, + pub aa_verts: Vec, + /// The smoothed points. + pub discs: Vec, + pub clears: Vec, + /// The state the above run under; set whenever either is not empty. + pub state: Option, +} + +/// A smoothed polygon or line: `n` convex vertices of `Prims::aa_verts` from `start`, drawn as a fan +/// from the first. A line is the rectangle about its segment, with its distance along the line in +/// each vertex, and takes the line stipple. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SmoothPoly { + pub start: u32, + pub n: u32, + pub line: bool, +} + +/// A smoothed point: the square about its disc as four corners (counter-clockwise), and the disc's +/// centre and radius in pixels, in GL's window coordinates. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SmoothPoint { + pub quad: [WVertex; 4], + pub x: f32, + pub y: f32, + pub r: f32, +} + +impl Prims { + /// Empty, keeping the capacity. + pub fn clear(&mut self) { + self.tris.clear(); + self.lines.clear(); + self.discs.clear(); + self.aa.clear(); + self.aa_verts.clear(); + self.clears.clear(); + self.state = None; + } +} + +/// The coordinates a box from `lo` to `hi` lights along one axis, as a +/// half-open range: the left column and bottom row are left out and the right +/// column and top row lit (the rule the hardware documents give for the board). +/// Empty when `hi < lo`. +pub fn lit(lo: i64, hi: i64) -> std::ops::Range { + lo + 1..hi + 1 +} + +/// A surface onto word `word` of the store, after every queued raster job has +/// run: whatever reads or writes the store's pixels comes through here, so the +/// tile workers never race it. The drain lives in the way to the pixels, not in a +/// list of the jobs that need it. +pub fn surface<'a>(tiles: &mut Option, store: &'a PixelStore, word: usize) -> Surface<'a> { + if let Some(t) = tiles { + // A poisoned scheduler is reported by `Tiles::is_poisoned`, which the + // board reads; the drain itself still ends. + let _ = t.drain(); + } + Surface::new(store, word) +} + +/// A word position of the store to draw into, and where drawing stops. +pub struct Surface<'a> { + store: &'a PixelStore, + array: WordArray<'a>, +} + +impl<'a> Surface<'a> { + /// Draw into word `word` of the store's pixels. Allocates the array: this + /// is the draw's set-up, and resolves it once for every call after. + pub fn new(store: &'a PixelStore, word: usize) -> Surface<'a> { + store.ensure(word); + Surface { store, array: store.array(word).expect("allocated by the set-up") } + } + + /// A surface onto another word position of the same store, allocated + /// now: the display-ID and overlay planes a draw reaches by its target. + pub fn sibling(&self, word: usize) -> Surface<'a> { + Surface::new(self.store, word) + } + + /// The managed area's size. + pub fn size(&self) -> (u32, u32) { + (self.store.width(), self.store.height()) + } + + /// The word array drawn into. + pub fn array(&self) -> WordArray<'a> { + self.array + } + + /// Set the pixel (`x`, `y`) to `value`; false if it is off the managed area. + pub fn plot(&self, x: i64, y: i64, value: u32) -> bool { + let (w, h) = self.size(); + if x < 0 || y < 0 || x >= w as i64 || y >= h as i64 { + return false; + } + // SAFETY: the pixel is inside the managed area, which the array covers. + unsafe { self.array.store(y as usize * self.array.pitch() + x as usize, value) }; + true + } + + /// Fill the box with corners (`x1`, `y1`) and (`x2`, `y2`) by the fill rule (`lit`), + /// clipped to the managed area. A box whose far corner is below or left of + /// its near one draws nothing (what a reversed box does is unknown). + pub fn fill_box(&self, x1: i64, y1: i64, x2: i64, y2: i64, value: u32) { + let (xs, ys) = (lit(x1, x2), lit(y1, y2)); + let xs = xs.start.max(0)..xs.end.min(self.store.width() as i64); + let ys = ys.start.max(0)..ys.end.min(self.store.height() as i64); + let pitch = self.array.pitch(); + for y in ys { + let row = y as usize * pitch; + for x in xs.clone() { + // SAFETY: both ranges are clipped to the managed area. + unsafe { self.array.store(row + x as usize, value) }; + } + } + } + + /// Set the pixels of a bitmap whose bits are set, most significant bit + /// first, row-major, `width` bits a row, with its row 0 at `y0` and its + /// column 0 at `x0`. Rows and columns off the managed area are skipped + /// without being looked at, and a clear word is skipped whole. + pub fn bitmap(&self, x0: i64, y0: i64, width: u32, height: u32, bits: &[u32], value: u32) { + let (w, h) = (self.store.width() as i64, self.store.height() as i64); + let pitch = self.array.pitch(); + // The columns of the bitmap that land on the store. + let (lo, hi) = ((-x0).max(0), (w - x0).min(width as i64)); + for r in 0..height as u64 { + let y = y0 + r as i64; + if y < 0 || y >= h || lo >= hi { + continue; + } + let (row_bit, row_store) = (r * width as u64, y as usize * pitch); + let (mut bit, end) = (row_bit + lo as u64, row_bit + hi as u64); + while bit < end { + let word = bits.get((bit / 32) as usize).copied().unwrap_or(0); + if word == 0 { + bit = (bit / 32 + 1) * 32; + continue; + } + if word >> (31 - bit % 32) & 1 != 0 { + // SAFETY: the column is inside `lo..hi`, clipped to the store. + unsafe { self.array.store(row_store + (x0 + (bit - row_bit) as i64) as usize, value) }; + } + bit += 1; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lit(s: &PixelStore, w: usize) -> Vec<(u32, u32)> { + let mut v = Vec::new(); + for y in 0..s.height() { + for x in 0..s.width() { + if s.read(x, y, w) != 0 { + v.push((x, y)); + } + } + } + v + } + + /// The four-corner packet of the hardware documents: x1 < x2, y1 < y2, no + /// fraction, lights columns x1+1..=x2 and rows y1+1..=y2. + #[test] + fn a_box_lights_columns_and_rows_after_the_near_corner() { + let s = PixelStore::new(16, 16); + Surface::new(&s, 1).fill_box(2, 3, 5, 6, 7); + let got = lit(&s, 1); + assert_eq!(got.len(), 3 * 3); + assert!(got.iter().all(|&(x, y)| (3..=5).contains(&x) && (4..=6).contains(&y))); + assert_eq!(s.read(2, 4, 1), 0, "the left column is out"); + assert_eq!(s.read(3, 3, 1), 0, "the bottom row is out"); + assert_eq!(s.read(5, 6, 1), 7, "the right column and top row are in"); + } + + /// Adjacent boxes that share an edge meet with neither a gap nor an + /// overlap, which is why the rule leaves the near edge out. + #[test] + fn boxes_sharing_an_edge_neither_gap_nor_overlap() { + let s = PixelStore::new(8, 12); + let d = Surface::new(&s, 0); + d.fill_box(0, 0, 7, 5, 1); + d.fill_box(0, 5, 7, 10, 2); + for y in 1..=10u32 { + for x in 1..=7u32 { + assert_eq!(s.read(x, y, 0), if y <= 5 { 1 } else { 2 }, "({x},{y})"); + } + } + } + + /// The rule leaves out the left column and bottom row, and a reversed box is empty. + #[test] + fn the_rule_omits_the_left_and_bottom_edges() { + assert_eq!(super::lit(2, 5), 3..6); + assert!(super::lit(5, 2).is_empty()); + } + + /// A box at the left or bottom edge of the screen leaves column or row 0 + /// unlit; a box past the far edges is clipped; a reversed box draws nothing. + #[test] + fn boxes_are_clipped_and_reversed_ones_draw_nothing() { + let s = PixelStore::new(8, 8); + let d = Surface::new(&s, 0); + d.fill_box(0, 0, 100, 100, 1); + assert_eq!(s.read(0, 4, 0), 0); + assert_eq!(s.read(4, 0, 0), 0); + assert_eq!(lit(&s, 0).len(), 7 * 7); + let t = PixelStore::new(8, 8); + Surface::new(&t, 0).fill_box(5, 5, 2, 2, 1); + Surface::new(&t, 0).fill_box(-50, -50, -10, -10, 1); + assert!(lit(&t, 0).is_empty()); + } + + #[test] + fn plot_refuses_off_screen_points() { + let s = PixelStore::new(4, 4); + let d = Surface::new(&s, 0); + assert!(d.plot(3, 3, 9)); + assert!(!d.plot(4, 0, 9) && !d.plot(0, 4, 9) && !d.plot(-1, 0, 9) && !d.plot(i64::MAX, 0, 9)); + assert_eq!(lit(&s, 0), [(3, 3)]); + } + + /// Bits go most significant first, row-major, and a missing word reads as + /// clear. + #[test] + fn a_bitmap_sets_its_set_bits() { + let s = PixelStore::new(16, 16); + // 5 x 3: row 0 = 10101, row 1 = 01010, row 2 = 11111. + let bits = [0b10101_01010_11111u32 << 17]; + Surface::new(&s, 2).bitmap(4, 8, 5, 3, &bits, 3); + assert_eq!(lit(&s, 2).len(), 3 + 2 + 5); + assert_eq!(s.read(4, 8, 2), 3); + assert_eq!(s.read(5, 8, 2), 0); + assert_eq!(s.read(5, 9, 2), 3); + assert_eq!(s.read(8, 10, 2), 3); + // A short payload: the missing words read as clear. + let wide = PixelStore::new(32, 4); + Surface::new(&wide, 4).bitmap(0, 0, 32, 2, &[u32::MAX], 1); + assert_eq!(lit(&wide, 4).len(), 32); + } +} diff --git a/src/dev/kona/back/raster/multisample.rs b/src/dev/kona/back/raster/multisample.rs new file mode 100644 index 000000000..07218ba4b --- /dev/null +++ b/src/dev/kona/back/raster/multisample.rs @@ -0,0 +1,250 @@ +//! Multisample: the sample patterns, the sample mask's coverage words, and the state a context keeps. +//! +//! The definitions table's section on multisampling gives the sample count (a layout choice: +//! 4 and 8 are covered), the three enables and the sample-mask enable, the pattern token's +//! seven enumerants with a pair of location words for each count, and the 33 coverage words +//! each count's sample mask indexes. Sample locations are on an eighth-of-a-pixel grid, a +//! three-bit x and a three-bit y; the first location register holds samples 0 to 3 and the +//! second 4 to 7. The table grades the location words and the coverage words established +//! and their reading (the positions, one nibble or byte a pixel) inferred. +//! +//! What the table leaves unknown, and this model decides, each stated here and counted in `RasterCounts` (multisampled draws, with alpha to mask, with a sample mask): +//! +//! - A location is measured from the pixel's integer corner, the same corner the single-sample +//! path's one sample sits at (the table does not say whether the origin is a corner or the +//! centre). Edges and ties are as set-up has them, with the sample moved. +//! - The samples live in the layout's own words, whose sizes say how: a multisampled window +//! reserves 3 words a 4 samples for colour (24 bits a sample, red, green and blue; alpha is +//! not kept), 3 words a 4 for 24-bit depth (S/2 at 16 bits) and 1 a 4 for 8-bit stencil, in place +//! of the plain depth and stencil words. The model packs a sample's value at `k` times its +//! width from the run's first bit, across word boundaries (the table gives no layout in memory; +//! the sizes are the only evidence, so the reading is inferred). A fragment is shaded once, +//! at the pixel's corner, and run through the stencil and depth tests and the colour operations +//! once a covered sample, against that sample's values, with the depth at the sample's own +//! position. The pixel's colour word is then the mean of its samples, so two triangles that +//! share an edge cover a straddling pixel between them without a seam. The word's alpha is the +//! last fragment's (samples keep none). +//! The pipe has no resolve step: a read, a copy or a draw-buffer selection uses the colour and +//! auxiliary banks and never reads the sample count, so a colour read most likely takes the +//! maintained per-pixel colour (inferred), which is the mean word here. What a depth or +//! stencil read returns, and how accumulation and a blended copy use the samples, is not +//! readable from software (unknown). +//! - A clear sets every sample (the firmware has no multisample branch in a clear; which sample +//! banks the hardware reaches is unknown). +//! - The sample mask's coverage word picks a pixel's nibble (4 samples) or byte (8) by the +//! pixel's x modulo the number of them, and the bits are samples 0 up. The invert flag is a plain +//! complement (established). That the word is ANDed with the primitive's own coverage is +//! inferred from the table's shape and the vendor test's pairing. +//! - The board's registers for all this, the control word (0x1c03), the primitive sampling word +//! (0x0806) and the depth clear word (0x1c09), are in `defs::multisample`. The model draws from +//! its context state and not from those words, so the optimisation the firmware turns off under +//! blending, a logic operation, a colour mask and a masked clear (values established, meaning +//! inferred) has no effect on a pixel here. +//! - A mask value becomes a coverage index by rounding its product with 32 (the table says +//! the float-to-index rounding is not recorded). +//! - Alpha to mask covers the first `round(alpha * samples)` samples (the table has no rule), +//! and alpha to one sets the fragment's alpha to one afterwards. +//! - Lines and points go through the same triangles, so they are multisampled the same way +//! (the table has no rule for them). + +use crate::dev::kona::stream::Stream; + +/// The sample patterns, 0x80a1 to 0x80a7, as a pair of location words for 4 samples and one for 8. +const PATTERN_FIRST: u32 = 0x80a1; +const WORDS4: [[u32; 2]; 7] = [[0xd94880; 2], [0xf92940; 2], [0xb19f80; 2], [0x4eaf40; 2], [0x793a40; 2], [0xaddc80; 2], [0xdad6c0; 2]]; +const WORDS8: [[u32; 2]; 7] = [[0x655f80, 0xb0b9f2], [0xf77380, 0x913a99], [0xf32840, 0xad774e], [0x2d1fc0, 0x8b476e], [0x8ab240, 0x7fe535], [0x3b3e40, 0xb6251f], [0x5ede40, 0x2e27b4]]; + +/// The coverage word for a sample-mask index `n` (0 to 32). +const COVER4: [u32; 17] = [0, 0x11, 0x11000011, 0x11001111, 0x11111111, 0x11111133, 0x33111133, 0x33113333, 0x33333333, 0x33333377, 0x77333377, 0x77337777, 0x77777777, 0x777777ff, 0xff7777ff, 0xff77ffff, 0xffffffff]; +const COVER8: [u32; 33] = [ + 0x00000000, 0x00000001, 0x01000001, 0x01000101, 0x01010101, 0x01010111, 0x11010111, 0x11011111, 0x11111111, 0x11111113, 0x13111113, 0x13111313, 0x13131313, 0x13131333, 0x33131333, 0x33133333, 0x33333333, 0x33333337, 0x37333337, 0x37333737, 0x37373737, 0x37373777, 0x77373777, 0x77377777, 0x77777777, 0x7777777f, + 0x7f77777f, 0x7f777f7f, 0x7f7f7f7f, 0x7f7f7fff, 0xff7f7fff, 0xff7fffff, 0xffffffff, +]; + +/// What a context keeps of multisampling. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Multisample { + /// The sample count of the window's layout: 0 (none), 4 or 8. + pub samples: u32, + pub enable: bool, + pub alpha_to_mask: bool, + pub alpha_to_one: bool, + pub mask_on: bool, + pub pattern: u32, + pub mask_value: f32, + pub mask_invert: bool, +} + +impl Default for Multisample { + /// What a buffer configuration leaves: the pattern one-pass, the mask 1.0, the alpha enables off, + /// and multisample enabled whatever the count. + fn default() -> Self { + Multisample { samples: 0, enable: true, alpha_to_mask: false, alpha_to_one: false, mask_on: false, pattern: PATTERN_FIRST, mask_value: 1.0, mask_invert: false } + } +} + +/// What a draw runs under: the count, the sample locations, the coverage word of the mask and the +/// alpha options. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Samples { + pub count: u8, + pub at: [[u8; 2]; 8], + /// The sample mask's coverage word, or all ones with the mask off. + pub cover: u32, + pub alpha_to_mask: bool, + pub alpha_to_one: bool, + /// The layout's words that hold the samples' colour, depth and stencil: the first word and how many. + pub colour: (u32, u32), + pub depth: (u32, u32), + pub stencil: (u32, u32), +} + +impl Multisample { + /// A new buffer configuration: the count, and everything else as the start-up values. + pub fn configure(&mut self, samples: u32) { + *self = Multisample { samples: if matches!(samples, 4 | 8) { samples } else { 0 }, ..Multisample::default() }; + } + + /// The pattern token: false (an invalid enumerant) outside 0x80a1 to 0x80a7. + pub fn set_pattern(&mut self, pattern: u32) -> bool { + let ok = (PATTERN_FIRST..PATTERN_FIRST + 7).contains(&pattern); + if ok { + self.pattern = pattern; + } + ok + } + + /// The coverage index of the mask value, rounded, and its word with the invert flag. + pub fn coverage_word(&self) -> u32 { + let n = (self.mask_value.clamp(0.0, 1.0) * 32.0 + 0.5) as usize; + let w = if self.samples == 8 { COVER8[n.min(32)] } else { COVER4[(n.min(32) + 1) / 2] }; + if self.mask_invert { !w } else { w } + } + + /// The draw's sample state, or None when multisampling does not apply: no multisample buffer, or + /// the enable is off. + pub fn draw(&self, colour: (u32, u32), depth: (u32, u32), stencil: (u32, u32)) -> Option { + if self.samples == 0 || !self.enable { + return None; + } + let words = if self.samples == 8 { WORDS8 } else { WORDS4 }[(self.pattern - PATTERN_FIRST) as usize]; + let mut at = [[0u8; 2]; 8]; + for (n, loc) in at.iter_mut().enumerate().take(self.samples as usize) { + let (w, shift) = (words[n / 4], 6 * (n % 4)); + *loc = [(w >> shift & 7) as u8, (w >> (shift + 3) & 7) as u8]; + } + Some(Samples { count: self.samples as u8, at, cover: if self.mask_on { self.coverage_word() } else { !0 }, alpha_to_mask: self.alpha_to_mask, alpha_to_one: self.alpha_to_one, colour, depth, stencil }) + } + + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { samples: _, enable: _, alpha_to_mask: _, alpha_to_one: _, mask_on: _, pattern: _, mask_value: _, mask_invert: _ } = self; + s.u32(&mut self.samples); + for b in [&mut self.enable, &mut self.alpha_to_mask, &mut self.alpha_to_one, &mut self.mask_on, &mut self.mask_invert] { + s.bool(b); + } + s.u32(&mut self.pattern); + s.f32(&mut self.mask_value); + // A pattern read from a damaged snapshot would index past the tables. + if !(PATTERN_FIRST..PATTERN_FIRST + 7).contains(&self.pattern) { + self.pattern = PATTERN_FIRST; + } + } +} + +impl Samples { + /// The samples of pixel column `x` a mask allows: the nibble or byte its column picks. + pub fn allowed(&self, x: i32) -> u32 { + if self.count == 8 { self.cover >> (8 * (x & 3)) & 0xff } else { self.cover >> (4 * (x & 7)) & 0xf } + } + + /// The samples alpha lets through: the first `round(alpha * count)` of them. `alpha` is 13 bits. + pub fn alpha_samples(&self, alpha: u32) -> u32 { + let k = (alpha.min(8191) * self.count as u32 + 4095) / 8191; + (1u32 << k) - 1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The table's worked words give the table's positions: pattern one-pass at 4 samples is (0,0) (2,4) (4,2) (6,6), + /// at 8 samples (0,0) (6,7) (5,2) (1,3) then (2,6) (7,4) (3,1) (4,5). + #[test] + fn the_location_words_decode_to_the_tables_positions() { + let mut m = Multisample::default(); + m.configure(4); + let s = m.draw((1, 3), (4, 3), (7, 1)).expect("multisampled"); + assert_eq!(&s.at[..4], &[[0, 0], [2, 4], [4, 2], [6, 6]]); + m.configure(8); + let s = m.draw((1, 3), (4, 3), (7, 1)).expect("multisampled"); + assert_eq!(s.at, [[0, 0], [6, 7], [5, 2], [1, 3], [2, 6], [7, 4], [3, 1], [4, 5]]); + assert!(m.set_pattern(0x80a7)); + assert_eq!(&m.draw((1, 3), (4, 3), (7, 1)).unwrap().at[..4], &[[0, 0], [1, 7], [5, 5], [7, 2]]); + assert!(!m.set_pattern(0x80a8)); + m.configure(4); + assert!(m.set_pattern(0x80a2)); + assert_eq!(&m.draw((1, 3), (4, 3), (7, 1)).unwrap().at[..4], &[[0, 0], [5, 4], [2, 2], [6, 7]]); + } + + /// A configuration with no multisample buffer, or any other count, draws without samples; the enable turns them off. + #[test] + fn only_a_multisampled_layout_with_the_enable_on_has_samples() { + let mut m = Multisample::default(); + assert!(m.draw((1, 3), (4, 3), (7, 1)).is_none()); + m.configure(2); + assert_eq!(m.samples, 0); + m.configure(4); + assert!(m.draw((1, 3), (4, 3), (7, 1)).is_some()); + m.enable = false; + assert!(m.draw((1, 3), (4, 3), (7, 1)).is_none()); + } + + /// The mask value indexes the coverage tables (rounded), the invert flag complements, and a pixel's column picks its share. + #[test] + fn the_sample_mask_picks_a_coverage_word_and_a_share_per_column() { + let mut m = Multisample::default(); + m.configure(4); + m.mask_on = true; + m.mask_value = 0.0; + assert_eq!(m.coverage_word(), 0); + m.mask_value = 0.5; + assert_eq!(m.coverage_word(), 0x33333333, "index 16 of 32 is the table's 15-16 entry"); + m.mask_invert = true; + assert_eq!(m.coverage_word(), !0x33333333); + m.configure(8); + m.mask_on = true; + m.mask_value = 1.0; + assert_eq!(m.coverage_word(), 0xffffffff); + m.mask_value = 0.5; + let s = m.draw((1, 3), (4, 3), (7, 1)).unwrap(); + assert_eq!(s.cover, 0x33333333); + assert_eq!([s.allowed(0), s.allowed(3), s.allowed(4)], [0x33, 0x33, 0x33]); + // The first sample of an index-1 word only. + m.mask_value = 1.0 / 32.0; + let s = m.draw((1, 3), (4, 3), (7, 1)).unwrap(); + assert_eq!([s.allowed(0), s.allowed(1)], [1, 0]); + assert_eq!(s.alpha_samples(8191), 0xff); + assert_eq!(s.alpha_samples(0), 0); + assert_eq!(s.alpha_samples(4096).count_ones(), 4); + } + + #[test] + fn the_state_round_trips_through_a_stream() { + use crate::dev::kona::stream::{Reader, Writer}; + let mut m = Multisample::default(); + m.configure(8); + m.mask_on = true; + m.mask_value = 0.25; + m.mask_invert = true; + m.set_pattern(0x80a5); + let mut w = Writer::default(); + m.visit(&mut w); + let mut n = Multisample::default(); + let mut r = Reader::new(&w.out); + n.visit(&mut r); + assert!(r.finish().is_ok()); + assert_eq!(n, m); + } +} diff --git a/src/dev/kona/back/raster/ops.rs b/src/dev/kona/back/raster/ops.rs new file mode 100644 index 000000000..2b3323ced --- /dev/null +++ b/src/dev/kona/back/raster/ops.rs @@ -0,0 +1,445 @@ +//! Pure functions on typed values for the per-pixel module: comparisons and colour +//! packing. No state, no store access: each is a function of its arguments, so a +//! GPU kernel could reproduce it. + +use crate::dev::kona::defs::{gl, WINDOW_ID_GROUP_FIRST, WINDOW_ID_GROUP_LAST, WINDOW_ID_ORDINARY_LAST, WINDOW_ID_POPUP}; + +/// A GL comparison function, typed when the draw state is built so nothing per +/// pixel matches an enumerant. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Compare { + Never, + Less, + Equal, + Lequal, + Greater, + Notequal, + Gequal, + #[default] + Always, +} + +impl Compare { + /// The function GL names, or None for a value that is not one. + pub fn from_gl(v: u32) -> Option { + Some(match v { + gl::NEVER => Compare::Never, + gl::LESS => Compare::Less, + gl::EQUAL => Compare::Equal, + gl::LEQUAL => Compare::Lequal, + gl::GREATER => Compare::Greater, + gl::NOTEQUAL => Compare::Notequal, + gl::GEQUAL => Compare::Gequal, + gl::ALWAYS => Compare::Always, + _ => return None, + }) + } + + /// Whether `new` passes against `old`. + #[inline(always)] + pub fn passes(self, new: u32, old: u32) -> bool { + match self { + Compare::Never => false, + Compare::Less => new < old, + Compare::Equal => new == old, + Compare::Lequal => new <= old, + Compare::Greater => new > old, + Compare::Notequal => new != old, + Compare::Gequal => new >= old, + Compare::Always => true, + } + } +} + +/// What a stencil test does to the stored value. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum StencilOp { + #[default] + Keep, + Zero, + Replace, + Incr, + Decr, + Invert, +} + +impl StencilOp { + pub fn from_gl(v: u32) -> Option { + Some(match v { + gl::KEEP => StencilOp::Keep, + gl::ZERO => StencilOp::Zero, + gl::REPLACE => StencilOp::Replace, + gl::INCR => StencilOp::Incr, + gl::DECR => StencilOp::Decr, + gl::INVERT => StencilOp::Invert, + _ => return None, + }) + } + + /// The new value of stored `s`, with reference `r`; increment and decrement + /// saturate at the 8-bit range. + #[inline(always)] + pub fn apply(self, s: u8, r: u8) -> u8 { + match self { + StencilOp::Keep => s, + StencilOp::Zero => 0, + StencilOp::Replace => r, + StencilOp::Incr => s.saturating_add(1), + StencilOp::Decr => s.saturating_sub(1), + StencilOp::Invert => !s, + } + } +} + +/// A blend factor, by what it multiplies by. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum BlendFactor { + Zero, + #[default] + One, + SrcColour, + OneMinusSrcColour, + SrcAlpha, + OneMinusSrcAlpha, + DstAlpha, + OneMinusDstAlpha, + DstColour, + OneMinusDstColour, + SrcAlphaSaturate, + ConstantColour, + OneMinusConstantColour, + ConstantAlpha, + OneMinusConstantAlpha, +} + +/// The full-intensity value of a 13-bit component. +pub const FULL: u32 = 8191; + +impl BlendFactor { + pub fn from_gl(v: u32) -> Option { + Some(match v { + gl::ZERO => BlendFactor::Zero, + gl::ONE => BlendFactor::One, + gl::SRC_COLOR => BlendFactor::SrcColour, + gl::ONE_MINUS_SRC_COLOR => BlendFactor::OneMinusSrcColour, + gl::SRC_ALPHA => BlendFactor::SrcAlpha, + gl::ONE_MINUS_SRC_ALPHA => BlendFactor::OneMinusSrcAlpha, + gl::DST_ALPHA => BlendFactor::DstAlpha, + gl::ONE_MINUS_DST_ALPHA => BlendFactor::OneMinusDstAlpha, + gl::DST_COLOR => BlendFactor::DstColour, + gl::ONE_MINUS_DST_COLOR => BlendFactor::OneMinusDstColour, + gl::SRC_ALPHA_SATURATE => BlendFactor::SrcAlphaSaturate, + gl::CONSTANT_COLOR => BlendFactor::ConstantColour, + gl::ONE_MINUS_CONSTANT_COLOR => BlendFactor::OneMinusConstantColour, + gl::CONSTANT_ALPHA => BlendFactor::ConstantAlpha, + gl::ONE_MINUS_CONSTANT_ALPHA => BlendFactor::OneMinusConstantAlpha, + _ => return None, + }) + } + + /// The factor's weight on each component (red, green, blue, alpha), in 13-bit + /// units, for source `s`, destination `d` and constant `k`. + #[inline(always)] + pub fn weights(self, s: [u32; 4], d: [u32; 4], k: [u32; 4]) -> [u32; 4] { + let one_minus = |v: [u32; 4]| v.map(|c| FULL - c); + match self { + BlendFactor::Zero => [0; 4], + BlendFactor::One => [FULL; 4], + BlendFactor::SrcColour => s, + BlendFactor::OneMinusSrcColour => one_minus(s), + BlendFactor::SrcAlpha => [s[3]; 4], + BlendFactor::OneMinusSrcAlpha => [FULL - s[3]; 4], + BlendFactor::DstAlpha => [d[3]; 4], + BlendFactor::OneMinusDstAlpha => [FULL - d[3]; 4], + BlendFactor::DstColour => d, + BlendFactor::OneMinusDstColour => one_minus(d), + BlendFactor::SrcAlphaSaturate => { + let f = s[3].min(FULL - d[3]); + [f, f, f, FULL] + } + BlendFactor::ConstantColour => k, + BlendFactor::OneMinusConstantColour => one_minus(k), + BlendFactor::ConstantAlpha => [k[3]; 4], + BlendFactor::OneMinusConstantAlpha => [FULL - k[3]; 4], + } + } +} + +/// `src * sf + dst * df` on each of four 13-bit components. The rounding (to +/// nearest), the clamp and the 13-bit units are the model's own: no source gives them. +#[inline(always)] +pub fn blend(src: [u32; 4], dst: [u32; 4], sf: BlendFactor, df: BlendFactor, eq: BlendEq, k: [u32; 4]) -> [u32; 4] { + let BlendEq::Add = eq; + let (sw, dw) = (sf.weights(src, dst, k), df.weights(src, dst, k)); + std::array::from_fn(|i| ((src[i] * sw[i] + dst[i] * dw[i] + FULL / 2) / FULL).min(FULL)) +} + +/// The blend equation. Only add is modelled: the board's equation token has no +/// layout read, so this is a model limit, and the type is here so a second +/// equation is a variant and not a flag. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum BlendEq { + #[default] + Add, +} + +/// A logic operation: GL's sixteen, in the order the X server numbers them. Shared +/// with the X path, which numbers them 0 to 15 and sends anything above as a copy. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum LogicOp { + Clear, + And, + AndReverse, + #[default] + Copy, + AndInverted, + Noop, + Xor, + Or, + Nor, + Equiv, + Invert, + OrReverse, + CopyInverted, + OrInverted, + Nand, + Set, +} + +impl LogicOp { + /// The operation GL names, or None for a value that is not one. + pub fn from_gl(v: u32) -> Option { + v.checked_sub(gl::LOGIC_OP_CLEAR).filter(|&n| n < 16).map(LogicOp::from_index) + } + + /// The operation the X server's number `n` names; above 15 is a copy. + #[inline(always)] + pub fn from_index(n: u32) -> LogicOp { + match n { + 0 => LogicOp::Clear, + 1 => LogicOp::And, + 2 => LogicOp::AndReverse, + 3 => LogicOp::Copy, + 4 => LogicOp::AndInverted, + 5 => LogicOp::Noop, + 6 => LogicOp::Xor, + 7 => LogicOp::Or, + 8 => LogicOp::Nor, + 9 => LogicOp::Equiv, + 10 => LogicOp::Invert, + 11 => LogicOp::OrReverse, + 12 => LogicOp::CopyInverted, + 13 => LogicOp::OrInverted, + 14 => LogicOp::Nand, + 15 => LogicOp::Set, + _ => LogicOp::Copy, + } + } + + /// The operation on a source `s` and destination `d`. + #[inline(always)] + pub fn apply(self, s: u32, d: u32) -> u32 { + match self { + LogicOp::Clear => 0, + LogicOp::And => s & d, + LogicOp::AndReverse => s & !d, + LogicOp::Copy => s, + LogicOp::AndInverted => !s & d, + LogicOp::Noop => d, + LogicOp::Xor => s ^ d, + LogicOp::Or => s | d, + LogicOp::Nor => !(s | d), + LogicOp::Equiv => !(s ^ d), + LogicOp::Invert => !d, + LogicOp::OrReverse => s | !d, + LogicOp::CopyInverted => !s, + LogicOp::OrInverted => !s | d, + LogicOp::Nand => !(s & d), + LogicOp::Set => !0, + } + } +} + +/// The window-ID test the board's register holds: the reference ID and the mask of the bits +/// compared. A fragment passes when `((plane ^ reference) & mask) == 0` (the polarity is inferred +/// from the values: every pair the encoder and the kernel make has its ID bits inside its mask +/// bits, and the other polarity would compare no window's ID at all). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Ownership { + pub reference: u32, + pub mask: u32, +} + +impl Ownership { + /// What the board encodes the window-placement request's word 9 as, and whether + /// that word was one of the ranges it names (anything else is encoded as the + /// ordinary pair 0x3f, 0x3f and counted by the caller). + pub fn encode(word9: i32) -> (Ownership, bool) { + match word9 { + 0..=WINDOW_ID_ORDINARY_LAST => (Ownership { reference: word9 as u32, mask: 0x3f }, true), + WINDOW_ID_POPUP => (Ownership { reference: 0x800, mask: 0x800 }, true), + WINDOW_ID_GROUP_FIRST..=WINDOW_ID_GROUP_LAST => (Ownership { reference: ((word9 - WINDOW_ID_GROUP_FIRST) as u32) << 6, mask: 0x7c0 }, true), + // The kernel's null clip sends -1 with the test on, to make a pipe own nothing (established): the + // board encodes it as the ordinary pair, which no pixel the shipped software paints matches. + -1 => (Ownership { reference: 0x3f, mask: 0x3f }, true), + _ => (Ownership { reference: 0x3f, mask: 0x3f }, false), + } + } + + /// The window-ID test register's value for this ownership with the enable set. + #[cfg(test)] + pub fn register(&self) -> u32 { + use crate::dev::kona::defs::WINDOW_ID_TEST as T; + T::MASK.put(T::REFERENCE_ID.put(T::ENABLE.put(0, 1), self.reference), self.mask) + } + + /// Whether a pixel whose compared plane value is `plane` is the window's. + #[inline(always)] + pub fn owns(&self, plane: u32) -> bool { + (plane ^ self.reference) & self.mask == 0 + } +} + +/// The 12-bit value the ownership test compares for a pixel: the painted value shifted right +/// by 2 (inferred). The X server paints main window ID n as `n << 2` and an overlay or popup group g +/// as `g << 8`, so the compared value holds the main ID in bits 5:0 and the group in bits 10:6 (the +/// ID-plane word of the store holds the painted value, both fields). +/// +/// One reading is the model's: the encoder compares a popup window at bit 11 (reference and mask +/// 0x800), which the painter never sets, since it paints the popup as group 31. Taking a group +/// field of all ones as the popup (the table's leading reading, inferred) sets bit 11 for it. An +/// overlay window of group 31 and a popup window then own the same pixels, as the painter gives them +/// the same pattern. +#[inline(always)] +pub fn ownership_plane(painted: u32) -> u32 { + (painted >> 2 & 0xfff) | ((painted >> 8 & 0x1f == 0x1f) as u32) << 11 +} + +/// A plane word `0xAARRGGBB` as four 13-bit components (red, green, blue, alpha). +#[inline(always)] +pub fn unpack_colour(w: u32) -> [u32; 4] { + let c = |shift: u32| ((w >> shift & 0xff) * FULL + 127) / 255; + [c(16), c(8), c(0), c(24)] +} + +/// A colour component of 13 bits as 8, rounded. +#[inline(always)] +pub fn to8(c13: u32) -> u32 { + (c13 * 255 + 4095) / 8191 +} + +/// Red, green, blue and alpha of 13 bits each as the plane word `0xAARRGGBB`. +#[inline(always)] +pub fn pack_colour(c: [u32; 4]) -> u32 { + to8(c[3]) << 24 | to8(c[0]) << 16 | to8(c[1]) << 8 | to8(c[2]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn comparisons_follow_gl_and_an_unknown_function_passes() { + let c = |f: u32| Compare::from_gl(f).unwrap(); + assert!(c(gl::LESS).passes(1, 2) && !c(gl::LESS).passes(2, 2)); + assert!(c(gl::LEQUAL).passes(2, 2) && c(gl::EQUAL).passes(3, 3) && !c(gl::NEVER).passes(0, 0)); + assert!(c(gl::GREATER).passes(3, 2) && c(gl::GEQUAL).passes(2, 2) && c(gl::NOTEQUAL).passes(1, 2)); + assert!(c(gl::ALWAYS).passes(0, 9)); + assert_eq!(Compare::from_gl(0x1234), None); + } + + #[test] + fn colour_packs_to_eight_bits_a_component_rounded() { + assert_eq!(pack_colour([8191, 0, 0, 8191]), 0xffff_0000); + assert_eq!(pack_colour([0, 8191, 4096, 0]), 0x00_00_ff_80); + assert_eq!(to8(0), 0); + } + + #[test] + fn stencil_operations_saturate_at_the_eight_bit_range() { + assert_eq!((StencilOp::Incr.apply(255, 9), StencilOp::Incr.apply(3, 9)), (255, 4)); + assert_eq!((StencilOp::Decr.apply(0, 9), StencilOp::Decr.apply(3, 9)), (0, 2)); + assert_eq!((StencilOp::Replace.apply(3, 9), StencilOp::Zero.apply(3, 9), StencilOp::Keep.apply(3, 9), StencilOp::Invert.apply(0x0f, 0)), (9, 0, 3, 0xf0)); + assert_eq!(StencilOp::from_gl(0x1e02), Some(StencilOp::Incr)); + assert_eq!(StencilOp::from_gl(0x1234), None); + } + + /// Source-over: (alpha, 1 - alpha) gives the weighted average, rounded; (one, zero) + /// returns the source; (zero, one) the destination; additive saturates. + #[test] + fn blending_weighs_source_and_destination() { + let (s, d, k) = ([FULL, 0, 4096, 4096], [0, FULL, 0, FULL], [0; 4]); + assert_eq!(blend(s, d, BlendFactor::One, BlendFactor::Zero, BlendEq::Add, k), s); + assert_eq!(blend(s, d, BlendFactor::Zero, BlendFactor::One, BlendEq::Add, k), d); + let over = blend(s, d, BlendFactor::SrcAlpha, BlendFactor::OneMinusSrcAlpha, BlendEq::Add, k); + assert_eq!(over, [4096, 4095, 2048, 6143], "{over:?}: alpha 4096 of 8191 is just over half"); + assert_eq!(blend(s, s, BlendFactor::One, BlendFactor::One, BlendEq::Add, k), [FULL, 0, FULL, FULL], "additive saturates"); + let constant = blend(s, d, BlendFactor::ConstantColour, BlendFactor::OneMinusConstantColour, BlendEq::Add, [FULL, 0, FULL, 0]); + assert_eq!(constant[..3], [FULL, FULL, 4096], "a constant of (1, 0, 1) takes source red and blue, destination green"); + // Saturate weighs by min(source alpha, 1 - destination alpha): none left of an opaque destination. + assert_eq!(blend(s, d, BlendFactor::SrcAlphaSaturate, BlendFactor::Zero, BlendEq::Add, k)[0], 0); + assert_eq!(blend(s, [0; 4], BlendFactor::SrcAlphaSaturate, BlendFactor::Zero, BlendEq::Add, k)[0], (FULL * 4096 + FULL / 2) / FULL); + } + + #[test] + fn the_stored_word_widens_to_thirteen_bits_and_packs_back_exactly() { + for v in 0..=255u32 { + let w = v << 24 | v << 16 | (255 - v) << 8 | v; + assert_eq!(pack_colour(unpack_colour(w).map(|c| c)), w & 0xffff_ffff, "{v}"); + } + assert_eq!(unpack_colour(0xff00_ff00), [0, FULL, 0, FULL]); + } + + /// The sixteen operations by their truth tables over (source, destination), in the + /// order the X server numbers them and GL names them. + #[test] + fn logic_operations_follow_their_truth_tables() { + const TRUTH: [u32; 16] = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]; + let (s, d) = (0x963c_a569u32, 0x5a81_c30fu32); + for (op, t) in TRUTH.iter().enumerate() { + let want = (0..32).fold(0u32, |acc, bit| { + let index = ((s >> bit) & 1) << 1 | (d >> bit) & 1; + acc | ((t >> index) & 1) << bit + }); + assert_eq!(LogicOp::from_index(op as u32).apply(s, d), want, "operation {op}"); + } + assert_eq!(LogicOp::from_index(u32::MAX).apply(s, d), s); + assert_eq!(LogicOp::from_index(3).apply(0xf0, 0x0f), 0xf0); + assert_eq!(LogicOp::from_index(6).apply(0xf0, 0xff), 0x0f); + assert_eq!(LogicOp::from_index(16).apply(5, 6), 5, "above 15 is a copy"); + assert_eq!(LogicOp::from_gl(gl::LOGIC_OP_CLEAR + 6), Some(LogicOp::Xor)); + assert_eq!(LogicOp::from_gl(0x1510), None); + } + + /// The encoder's worked values: the recorded word 9 of 0xe and 0xf give the + /// registers 0x7e01d and 0x7e01f; the overlay context's 0x44 is group 4 and + /// gives reference 0x100 with mask 0x7c0; the popup and the all-ones case. + #[test] + fn word_nine_encodes_as_the_board_does() { + assert_eq!(Ownership::encode(0xe).0.register(), 0x7e01d); + assert_eq!(Ownership::encode(0xf).0.register(), 0x7e01f); + assert_eq!(Ownership::encode(0x44).0, Ownership { reference: 0x100, mask: 0x7c0 }); + assert_eq!(Ownership::encode(0x3f).0, Ownership { reference: 0x800, mask: 0x800 }); + assert_eq!(Ownership::encode(-1), (Ownership { reference: 0x3f, mask: 0x3f }, true), "the null clip: the ordinary pair, not counted odd"); + assert_eq!(Ownership::encode(0x60), (Ownership { reference: 0x3f, mask: 0x3f }, false), "outside every range: the ordinary pair, flagged"); + assert_eq!(Ownership::encode(-5).1, false); + } + + /// An ordinary window owns the pixels painted with its ID and no other; a group window owns its + /// group whatever the main field holds; the popup owns a group of all ones; all ones, the null clip, owns + /// a pixel only where the main field is 0x3f, which the painter never writes. + #[test] + fn ownership_compares_the_bits_the_mask_names() { + let own = |w: i32, painted: u32| Ownership::encode(w).0.owns(ownership_plane(painted)); + // Painted values: main ID n is n << 2, an overlay group g is g << 8. + assert!(own(5, 5 << 2) && !own(5, 6 << 2)); + assert!(own(5, 5 << 2 | 2 << 8), "the overlay group beside the main ID does not matter to an ordinary window"); + assert!(own(0x44, 4 << 8) && own(0x44, 37 << 2 | 4 << 8), "group 4 whatever the main ID"); + assert!(!own(0x44, 5 << 8) && !own(0x44, 37 << 2), "no group is not group 4"); + assert!(own(0x40, 0), "group 0 is the pixels with no overlay painted"); + assert!(own(0x3f, 0x1f << 8) && !own(0x3f, 5 << 2), "the popup owns the group of all ones"); + assert!(own(0x5f, 0x1f << 8), "and so does an overlay window of group 31"); + assert!(!(0..0x3f).any(|n| own(-1, n << 2)), "no main ID the painter writes is 0x3f"); + assert!(own(-1, 0x3f << 2), "a pixel whose main field is 0x3f is the only one the null clip owns"); + } +} diff --git a/src/dev/kona/back/raster/pixel.rs b/src/dev/kona/back/raster/pixel.rs new file mode 100644 index 000000000..d4e3e2deb --- /dev/null +++ b/src/dev/kona/back/raster/pixel.rs @@ -0,0 +1,610 @@ +//! The per-pixel module: what runs for every pixel of a triangle, a clear or a +//! tile. +//! +//! Integers and single-precision floats only: the edge functions and the depth +//! plane (32.16 fixed point) are exact `i64`, and the colour planes `f32` +//! evaluated as `at_origin + dx * i + dy * j` with separate multiplies and adds +//! (no fused operation), so the result is the same on every host, and a GPU +//! kernel could reproduce it. A test in `golden` holds this file to that. The +//! one write is `WordArray::store`. +//! +//! The order of a fragment's operations is OpenGL's (alpha test, stencil, depth, blend, +//! logic operation, colour mask) with the ownership test first; the board's order is +//! unknown beyond the alpha test coming before the raster engines, so the position of +//! ownership and of both stipples is the model's own. Blend and then logic operation +//! both apply when both are on (the plan's order; OpenGL lets the logic operation +//! replace blending in RGBA). The 13-bit and 8-bit conversions are the model's own. +//! +//! A job is walked in 2 x 2 quads aligned to even screen coordinates, clipped +//! to the job's rectangle and the tile (the walk a texture's level of detail +//! will need). A tile owner calls `run` with its tile as `clip`; the walk is +//! clipped to it and keeps the window's own pixel grid, so a tiled run is +//! bit-exact with an inline one. There is one per-fragment path, which reads the +//! draw state and branches on the operations that are on, and the loop is +//! specialised once for each draw on the features the draw uses (see `run_tri`), +//! so a draw does not pay for those it does not use; other speed work is deferred. + +use super::setup::{Tri, TriJob}; +use crate::dev::kona::back::tiles::RasterJob; +use super::texel::{environment, sample, sample3, sample_enhanced, TexDesc}; +use super::{DrawState, Rect}; +use crate::dev::kona::back::store::texture::TextureStore; +use crate::dev::kona::back::store::{PixelStore, WordArray, DID_PLANE}; + +use super::ops::{blend, ownership_plane, pack_colour, to8, unpack_colour}; + +/// An attribute plane's value at a sample, clamped to `0..=max` and rounded. +#[inline(always)] +fn at(t: &Tri, k: usize, i: f32, j: f32, max: f32) -> u32 { + let v = t.at_origin[k] + t.dx[k] * i + t.dy[k] * j; + (v.clamp(0.0f32, max) + 0.5f32) as u32 +} + +/// The depth plane's value at a sample: the 32.16 plane's integer part, clamped to +/// the fraction's range. +#[inline(always)] +fn depth_at(t: &Tri, i: i64, j: i64) -> u32 { + // Wrapping: when the true value fits `i64` the wrapped sum is exact, and a sliver far + // outside the screen, whose slope can near 2^51, cannot panic a development build. + let v = t.z.at.wrapping_add(t.z.dx.wrapping_mul(i)).wrapping_add(t.z.dy.wrapping_mul(j)); + (v >> 16).clamp(0, DEPTH_MAX as i64) as u32 +} + +/// The depth plane's value at sample offset `at` (eighths of a pixel) from pixel (`i`, `j`). +#[inline(always)] +fn depth_at_sample(t: &Tri, i: i64, j: i64, at: [u8; 2]) -> u32 { + let v = t.z.at.wrapping_add(t.z.dx.wrapping_mul(i)).wrapping_add(t.z.dy.wrapping_mul(j)).wrapping_add((t.z.dx.wrapping_mul(at[0] as i64)).wrapping_add(t.z.dy.wrapping_mul(at[1] as i64)) / 8); + (v >> 16).clamp(0, DEPTH_MAX as i64) as u32 +} + +/// The depth fraction's largest value, `0xffffff00`. +const DEPTH_MAX: u32 = 4_294_967_040; + +/// Write `new` to a colour word over `old` under a colour mask. +#[inline(always)] +fn masked(old: u32, new: u32, mask: [bool; 4]) -> u32 { + let keep = (!mask[0] as u32 * 0xff) << 16 | (!mask[1] as u32 * 0xff) << 8 | (!mask[2] as u32 * 0xff) | (!mask[3] as u32 * 0xff) << 24; + (new & !keep) | (old & keep) +} + +/// The planes a fragment is tested against and written to, resolved once per job. +pub struct Planes<'a> { + depth: Option>, + stencil: Option>, + colour: [Option>; 2], + /// A multisampled window's sample storage; its depth and stencil are here, not above. + samples: Option>, +} + +/// The words a multisampled window keeps a sample in, a sample's value at a bit offset in the +/// run of words (the layout's sizes are 24 bits a sample of colour, 24 or 16 of depth and 8 of stencil). +struct SampleArrays<'a> { + colour: Vec>, + depth: Vec>, + stencil: Vec>, +} + +/// The bits of sample `k` in a run of words at `index`: `width` bits from bit `k * stride`. +#[inline(always)] +fn sample_get(words: &[WordArray<'_>], index: usize, k: usize, stride: u32, width: u32) -> u32 { + let bit = k * stride as usize; + let (w, sh) = (bit / 32, (bit % 32) as u32); + // SAFETY: the pixel is inside the screen, which every array covers. + let lo = unsafe { words[w].load(index) } as u64; + // SAFETY: as above, and a value that crosses a word has one after it. + let hi = if sh + width > 32 { (unsafe { words[w + 1].load(index) } as u64) << 32 } else { 0 }; + ((lo | hi) >> sh) as u32 & ((1u64 << width) - 1) as u32 +} + +#[inline(always)] +fn sample_set(words: &[WordArray<'_>], index: usize, k: usize, stride: u32, width: u32, v: u32) { + let bit = k * stride as usize; + let (w, sh) = (bit / 32, (bit % 32) as u32); + let mask = ((1u64 << width) - 1) << sh; + let put = |a: &WordArray<'_>, mask: u32, v: u32| { + // SAFETY: the pixel is inside the screen, which every array covers. + unsafe { a.store(index, (a.load(index) & !mask) | (v & mask)) }; + }; + put(&words[w], mask as u32, ((v as u64) << sh) as u32); + if sh + width > 32 { + put(&words[w + 1], (mask >> 32) as u32, ((v as u64) << sh >> 32) as u32); + } +} + +impl<'a> Planes<'a> { + pub fn new(store: &'a PixelStore, ds: &DrawState) -> Planes<'a> { + let colours = &ds.colour[..ds.colours as usize]; + let run = |(first, n): (u32, u32)| (first..first + n).map(|w| store.array(w as usize).expect("allocated by set-up")).collect::>(); + let samples = ds.samples.as_ref().map(|m| SampleArrays { colour: run(m.colour), depth: run(m.depth), stencil: run(m.stencil) }); + let plain = samples.is_none(); + Planes { + depth: (plain && ds.depth_bits != 0).then(|| store.array(ds.depth_word as usize).expect("allocated by set-up")), + stencil: (plain && ds.stencil_bits != 0 && ds.stencil.enabled).then(|| store.array(ds.stencil_word as usize).expect("allocated by set-up")), + colour: [colours.first().map(|&w| store.array(w as usize).expect("allocated")), colours.get(1).map(|&w| store.array(w as usize).expect("allocated"))], + samples, + } + } + + /// One fragment at plane index `index`, through the tests and writes that come after + /// ownership and the stipples: alpha, stencil, depth, blend, logic operation, colour mask. + #[inline(always)] + pub fn shade(&self, ds: &DrawState, index: usize, src: [u32; 4], z: u32) { + let all = [true; 4]; + let (depth, arrays) = (&self.depth, &self.colour); + let test = ds.depth_test && depth.is_some(); + let reads_dst = ds.blend.enabled || ds.logic.is_some() || ds.colour_mask != all; + let st = &ds.stencil; + if ds.alpha.enabled && !ds.alpha.func.passes(src[3], ds.alpha.reference) { + return; + } + // The stencil test, then the depth test; each outcome has its own operation. + let mut stored = 0u8; + if let Some(sa) = &self.stencil { + // SAFETY: the pixel is inside the screen, which the array covers. + stored = unsafe { sa.load(index) } as u8; + let pass = st.func.passes((st.reference & st.read_mask) as u32, (stored & st.read_mask) as u32); + if !pass { + let v = st.fail.apply(stored, st.reference); + // SAFETY: as above. + unsafe { sa.store(index, ((stored & !st.write_mask) | (v & st.write_mask)) as u32) }; + return; + } + } + // SAFETY: as above. + let zpass = !test || ds.depth_func.passes(z, unsafe { depth.as_ref().expect("test needs depth").load(index) }); + if let Some(sa) = &self.stencil { + let v = if zpass { st.zpass } else { st.zfail }.apply(stored, st.reference); + // SAFETY: as above. + unsafe { sa.store(index, ((stored & !st.write_mask) | (v & st.write_mask)) as u32) }; + } + if !zpass { + return; + } + if let (Some(d), true, true) = (depth, ds.depth_test, ds.depth_mask) { + // SAFETY: as above. + unsafe { d.store(index, z) }; + } + for a in arrays.iter().flatten() { + // SAFETY: as above. + let old = if reads_dst { unsafe { a.load(index) } } else { 0 }; + // SAFETY: as above. + unsafe { a.store(index, colour_out(ds, src, old)) }; + } + } + + /// One fragment of a multisampled window, once for each sample of `covered`: the stencil and depth tests + /// and the colour operations run against that sample's own values; `z_at` is the fragment's depth at a + /// sample (a fraction of `u32::MAX`). The pixel's colour word is then the mean of its samples, so + /// samples two triangles cover between them add up to a whole pixel. A pixel is tested with the alpha + /// once, before its samples (alpha is a fragment's, not a sample's). + pub fn shade_samples(&self, ds: &DrawState, index: usize, src: [u32; 4], z_at: impl Fn(usize) -> u32, covered: u32) { + let (Some(ms), Some(sa)) = (&ds.samples, &self.samples) else { return }; + if ds.alpha.enabled && !ds.alpha.func.passes(src[3], ds.alpha.reference) { + return; + } + let count = ms.count as usize; + let (cstride, dwidth) = (ms.colour.1 * 32 / ms.count as u32, if ms.depth.1 == 0 { 0 } else { ms.depth.1 * 32 / ms.count as u32 }); + let st = &ds.stencil; + let stencil_on = ds.stencil.enabled && ds.stencil_bits != 0 && !sa.stencil.is_empty(); + let test = ds.depth_test && !sa.depth.is_empty(); + // Blending reads the destination's alpha from the main word, which samples do not keep. + let main = self.colour.iter().flatten().next(); + let mut live = 0u32; + for k in 0..count { + if covered >> k & 1 == 0 { + continue; + } + let mut stored = 0u8; + if stencil_on { + stored = sample_get(&sa.stencil, index, k, 8, 8) as u8; + if !st.func.passes((st.reference & st.read_mask) as u32, (stored & st.read_mask) as u32) { + let v = st.fail.apply(stored, st.reference); + sample_set(&sa.stencil, index, k, 8, 8, ((stored & !st.write_mask) | (v & st.write_mask)) as u32); + continue; + } + } + let z = z_at(k) >> (32 - dwidth.clamp(1, 32)); + let zpass = !test || ds.depth_func.passes(z, sample_get(&sa.depth, index, k, dwidth, dwidth)); + if stencil_on { + let v = if zpass { st.zpass } else { st.zfail }.apply(stored, st.reference); + sample_set(&sa.stencil, index, k, 8, 8, ((stored & !st.write_mask) | (v & st.write_mask)) as u32); + } + if !zpass { + continue; + } + if test && ds.depth_mask { + sample_set(&sa.depth, index, k, dwidth, dwidth, z); + } + live |= 1 << k; + } + if live == 0 { + return; + } + // SAFETY (loads and stores of the main word): the pixel is inside the screen. + let alpha = main.map_or(0, |a| unsafe { a.load(index) } >> 24 & 0xff); + let mut sum = [0u32; 3]; + for k in 0..count { + let mut rgb = sample_get(&sa.colour, index, k, cstride, 24); + if live >> k & 1 != 0 { + let out = colour_out(ds, src, alpha << 24 | rgb); + rgb = out & 0xff_ffff; + sample_set(&sa.colour, index, k, cstride, 24, rgb); + } + sum[0] += rgb >> 16 & 0xff; + sum[1] += rgb >> 8 & 0xff; + sum[2] += rgb & 0xff; + } + // Samples keep no alpha: the word's is the last fragment's, so a shared edge leaves no seam in it. + let new_alpha = to8(src[3]); + let mean = |v: u32| (v + count as u32 / 2) / count as u32; + let word = new_alpha << 24 | mean(sum[0]) << 16 | mean(sum[1]) << 8 | mean(sum[2]); + for a in self.colour.iter().flatten() { + // SAFETY: as above. + unsafe { a.store(index, word) }; + } + } +} + +impl Planes<'_> { + /// Sample 0's depth at `index` as a fraction of `u32::MAX`, in a multisampled window (reads take one sample). + pub fn sample_depth(&self, ds: &DrawState, index: usize) -> Option { + let (ms, sa) = (ds.samples.as_ref()?, self.samples.as_ref()?); + let w = ms.depth.1 * 32 / ms.count as u32; + (!sa.depth.is_empty()).then(|| sample_get(&sa.depth, index, 0, w, w) << (32 - w.clamp(1, 32))) + } + + /// Sample 0's stencil at `index`, in a multisampled window. + pub fn sample_stencil(&self, index: usize) -> Option { + let sa = self.samples.as_ref()?; + (!sa.stencil.is_empty()).then(|| sample_get(&sa.stencil, index, 0, 8, 8)) + } + + /// A stencil image's value into every sample of the pixel under the write mask, in a multisampled window. + pub fn set_stencil(&self, ds: &DrawState, index: usize, value: u8, mask: u8) { + let (Some(ms), Some(sa)) = (&ds.samples, &self.samples) else { return }; + for k in 0..ms.count as usize { + let old = sample_get(&sa.stencil, index, k, 8, 8); + sample_set(&sa.stencil, index, k, 8, 8, (old & !mask as u32) | (value & mask) as u32); + } + } + + /// A clear of the samples of the pixel at `index`: colour under the colour mask, depth under the + /// depth mask, stencil under its mask. + fn clear_samples(&self, ds: &DrawState, index: usize, q: &super::ClearReq) { + let (Some(ms), Some(sa)) = (&ds.samples, &self.samples) else { return }; + let count = ms.count as usize; + let (cstride, dwidth) = (ms.colour.1 * 32 / ms.count as u32, if ms.depth.1 == 0 { 0 } else { ms.depth.1 * 32 / ms.count as u32 }); + for k in 0..count { + if let Some(word) = q.colour { + let old = sample_get(&sa.colour, index, k, cstride, 24); + sample_set(&sa.colour, index, k, cstride, 24, masked(old, word, q.colour_mask) & 0xff_ffff); + } + if let (Some(z), true, true) = (q.depth, q.depth_mask, !sa.depth.is_empty()) { + sample_set(&sa.depth, index, k, dwidth, dwidth, z >> (32 - dwidth.clamp(1, 32))); + } + if let (Some(v), false) = (q.stencil, sa.stencil.is_empty()) { + let old = sample_get(&sa.stencil, index, k, 8, 8); + sample_set(&sa.stencil, index, k, 8, 8, (old & !(q.stencil_mask as u32)) | (v & q.stencil_mask) as u32); + } + } + } +} + +/// The colour a fragment writes over `old`: blend, then the logic operation, then the colour mask. +#[inline(always)] +fn colour_out(ds: &DrawState, src: [u32; 4], old: u32) -> u32 { + let mut c = src; + if ds.blend.enabled { + c = blend(src, unpack_colour(old), ds.blend.src, ds.blend.dst, ds.blend.eq, ds.blend.constant); + } + let mut word = pack_colour(c); + if let Some(op) = ds.logic { + word = op.apply(word, old); + } + if ds.colour_mask != [true; 4] { + word = masked(old, word, ds.colour_mask); + } + word +} + +/// The samples of pixel (`x`, `y`) inside the triangle, as bits: each at its offset in eighths from the +/// pixel's corner, on the edges' own grid. +#[inline(always)] +fn sample_mask(t: &Tri, ms: &super::multisample::Samples, x: i64, y: i64) -> u32 { + let mut mask = 0; + for k in 0..ms.count as usize { + let [ox, oy] = ms.at[k].map(|v| v as i64); + let inside = (0..3).all(|e| { + // The edge at the corner, moved by the sample's offset (a and b are multiples of 8). + let v = t.a[e] * x + t.b[e] * y + t.c[e] + (t.a[e] * ox + t.b[e] * oy) / 8; + v > 0 || (v == 0 && t.tie[e]) + }); + mask |= (inside as u32) << k; + } + mask +} + +/// Run `job`, under draw state `ds`, over the pixels of `clip` it covers. +pub fn run(store: &PixelStore, job: &RasterJob, ds: &DrawState, clip: Rect) { + let r = job.rect().intersect(&clip); + if r.is_empty() { + return; + } + let colours = &ds.colour[..ds.colours as usize]; + let pitch = store.width() as usize; + let all = [true; 4]; + // Whether the window owns the pixel at `index` (the ID plane's bits 7:2 are the main ID). + let did = ds.ownership.is_some().then(|| store.array(DID_PLANE)).flatten(); + let owns = |index: usize| match &ds.ownership { + None => true, + // SAFETY: the pixel is inside the rectangle, inside the screen, which the array covers. + Some(o) => o.owns(ownership_plane(did.as_ref().map_or(0, |d| unsafe { d.load(index) }))), + }; + match job { + RasterJob::Clear(c) => { + let q = &c.req; + if let (Some(word), true) = (q.colour, !colours.is_empty()) { + for &w in colours { + let a = store.array(w as usize).expect("allocated by set-up"); + for y in r.y0..r.y1 { + for x in r.x0..r.x1 { + let i = y as usize * pitch + x as usize; + if !owns(i) { + continue; + } + // SAFETY: the rectangle is inside the screen, which the array covers. + unsafe { a.store(i, if q.colour_mask == all { word } else { masked(a.load(i), word, q.colour_mask) }) }; + } + } + } + } + // A multisampled window clears its samples; its depth and stencil words are packed samples, not planes. + if ds.samples.is_some() { + let planes = Planes::new(store, ds); + for y in r.y0..r.y1 { + for x in r.x0..r.x1 { + let i = y as usize * pitch + x as usize; + if owns(i) { + planes.clear_samples(ds, i, q); + } + } + } + } + if let (Some(z), true, true, true) = (q.depth, ds.depth_bits != 0, q.depth_mask, ds.samples.is_none()) { + let a = store.array(ds.depth_word as usize).expect("allocated by set-up"); + for y in r.y0..r.y1 { + for x in r.x0..r.x1 { + let i = y as usize * pitch + x as usize; + if owns(i) { + // SAFETY: as above. + unsafe { a.store(i, z) }; + } + } + } + } + if let (Some(v), true, true) = (q.stencil, ds.stencil_bits != 0, ds.samples.is_none()) { + let a = store.array(ds.stencil_word as usize).expect("allocated by set-up"); + for y in r.y0..r.y1 { + for x in r.x0..r.x1 { + let i = y as usize * pitch + x as usize; + if owns(i) { + // SAFETY: as above. + unsafe { a.store(i, (a.load(i) & !(q.stencil_mask as u32)) | (v & q.stencil_mask) as u32) }; + } + } + } + } + } + RasterJob::Tri(job) => { + // One function per fog curve, per smoothed or plain triangle and per kind of bound + // texture, chosen here once for the job: a triangle without fog, smoothing, a volume + // texture or a detail or sharpen combine runs the loop with no code for any of them in it. + let aa = job.tri.cover != super::smooth::Cover::None; + let tex = match &ds.texture { + Some(d) if d.volume => Kind::Volume, + Some(d) if !matches!(d.combine, super::texel::Combine::None) => Kind::Enhanced, + _ => Kind::Plain, + }; + macro_rules! go { + ($fog:expr) => { + match (aa, tex) { + (false, Kind::Plain) => run_tri::<{ $fog }, false, Plain>(store, job, ds, r, pitch, &owns), + (true, Kind::Plain) => run_tri::<{ $fog }, true, Plain>(store, job, ds, r, pitch, &owns), + (false, Kind::Volume) => run_tri::<{ $fog }, false, Volume>(store, job, ds, r, pitch, &owns), + (true, Kind::Volume) => run_tri::<{ $fog }, true, Volume>(store, job, ds, r, pitch, &owns), + (false, Kind::Enhanced) => run_tri::<{ $fog }, false, Enhanced>(store, job, ds, r, pitch, &owns), + (true, Kind::Enhanced) => run_tri::<{ $fog }, true, Enhanced>(store, job, ds, r, pitch, &owns), + } + }; + } + match super::fog::curve_of(ds) { + 0 => go!(0), + super::fog::LINEAR => go!(super::fog::LINEAR), + super::fog::EXP => go!(super::fog::EXP), + _ => go!(super::fog::EXP2), + } + } + } +} + +/// How a draw's bound texture is sampled, chosen once for a job. +#[derive(Clone, Copy)] +enum Kind { + Plain, + Volume, + Enhanced, +} + +/// The sampler `run_tri` is built over: a plain 2D texture (or none), a volume, which takes a third +/// coordinate and a depth in the level of detail, or a 2D texture with a detail or sharpen combine. +/// A type for each kind, so the choice is made at compile time and no kind is a catch-all. +trait Sampler { + /// Whether the draw takes the r coordinate. + const VOLUME: bool; + fn sample(store: &TextureStore, d: &TexDesc, s: f32, t: f32, r: f32, lod: f32) -> [u32; 4]; +} + +struct Plain; +struct Volume; +struct Enhanced; + +impl Sampler for Plain { + const VOLUME: bool = false; + #[inline(always)] + fn sample(store: &TextureStore, d: &TexDesc, s: f32, t: f32, _r: f32, lod: f32) -> [u32; 4] { + sample(store, d, s, t, lod) + } +} + +impl Sampler for Volume { + const VOLUME: bool = true; + #[inline(always)] + fn sample(store: &TextureStore, d: &TexDesc, s: f32, t: f32, r: f32, lod: f32) -> [u32; 4] { + sample3(store, d, s, t, r, lod) + } +} + +impl Sampler for Enhanced { + const VOLUME: bool = false; + #[inline(always)] + fn sample(store: &TextureStore, d: &TexDesc, s: f32, t: f32, _r: f32, lod: f32) -> [u32; 4] { + sample_enhanced(store, d, s, t, lod) + } +} + +/// The triangle `job`, in the pixels of `r`, with fog of curve `FOG` (0: none; `fog::LINEAR`, +/// `fog::EXP`, `fog::EXP2`), with coverage (`AA`) when it is a triangle of a smoothed primitive, and +/// sampling a texture with `S`. +fn run_tri(store: &PixelStore, job: &TriJob, ds: &DrawState, r: Rect, pitch: usize, owns: &impl Fn(usize) -> bool) { + let t = &job.tri; + let planes = Planes::new(store, ds); + // Quads aligned to even screen coordinates, clipped to the rectangle: the + // walk a derivative (texture level of detail) needs. A quad with no + // covered sample is skipped whole; the four samples are otherwise taken + // in order, so a fragment's result does not depend on the others. + let (qx0, qy0) = (r.x0 & !1, r.y0 & !1); + let tex = store.texture(); + // Only read with fog on (`FOG` != 0); the default stands in otherwise and is never used. + let fog = ds.fog.unwrap_or(super::fog::Fog::OFF); + for qy in (qy0..r.y1).step_by(2) { + for qx in (qx0..r.x1).step_by(2) { + // The texture coordinates at a sample, and the level of detail from the + // quad's differences: log2 of the larger of the s and t steps in texels of + // level 0, times the LOD scale (16 times the factor). A volume's third + // coordinate, r, is taken only for a volume (`S::VOLUME`): a 2D draw has no code for it, + // not even its divide. + let volume = S::VOLUME; + let coord = |x: i32, y: i32| -> (f32, f32, f32) { + let (i, j) = ((x - t.origin[0]) as f32, (y - t.origin[1]) as f32); + let w = |k: usize| t.tex[k][0] + t.tex[k][1] * i + t.tex[k][2] * j; + let q = w(3); + let r = if volume { w(2) } else { 0.0f32 }; + if q.abs() > 1e-30f32 { (w(0) / q, w(1) / q, if volume { r / q } else { 0.0f32 }) } else { (w(0), w(1), r) } + }; + let lod = ds.texture.as_ref().map(|d| { + let (s0, t0, r0) = coord(qx, qy); + let (sx, tx, rx) = coord(qx + 1, qy); + let (sy, ty, ry) = coord(qx, qy + 1); + let l0 = d.levels[0]; + let (fw, fh) = (l0.w.max(1) as f32, l0.h.max(1) as f32); + let mut rho = ((sx - s0) * fw).abs().max(((sy - s0) * fw).abs()).max(((tx - t0) * fh).abs()).max(((ty - t0) * fh).abs()); + let mut scale = d.lod_scale[0].max(1) as f32; + if volume { + // The depth step in texels of level 0 joins the largest, under its own scale; how the + // pipe combines the three steps is not read (approximated). + let fd = l0.d.max(1) as f32; + let rr = ((rx - r0) * fd).abs().max(((ry - r0) * fd).abs()); + let rs = d.lod_scale[2].max(1) as f32; + if rr * rs > rho * scale { + (rho, scale) = (rr, rs); + } + } + (rho * scale / 16.0f32).max(1e-6f32).log2() + }); + for (dy, dx) in [(0, 0), (0, 1), (1, 0), (1, 1)] { + let (x, y) = (qx + dx, qy + dy); + if x < r.x0 || x >= r.x1 || y < r.y0 || y >= r.y1 { + continue; + } + // The samples of the pixel inside the triangle: one, at the corner, without multisampling. + // A smoothed triangle (never in a multisampled window) takes the share of the pixel it + // covers instead, and draws the pixel when that is not nothing. + let mut cov = 1.0f32; + let covered = if AA { + cov = super::smooth::coverage(t, x, y); + // A line's and a point's coverage reaches the fragment as an 8-bit alpha. + if job.line || matches!(t.cover, super::smooth::Cover::Disc { .. }) { + cov = super::smooth::quantise(cov); + } + // In a multisampled window a smoothed line writes every sample of the pixel. + if cov > 0.0f32 { ds.samples.as_ref().map_or(1, |m| (1u32 << m.count as u32) - 1) } else { 0 } + } else { + match &ds.samples { + None => (0..3).all(|k| { + let e = t.a[k] * x as i64 + t.b[k] * y as i64 + t.c[k]; + e > 0 || (e == 0 && t.tie[k]) + }) as u32, + Some(ms) => sample_mask(t, ms, x as i64, y as i64), + } + }; + if covered == 0 { + continue; + } + let (i, j) = ((x - t.origin[0]) as f32, (y - t.origin[1]) as f32); + let index = y as usize * pitch + x as usize; + if !owns(index) { + continue; + } + // Stipple: a line's by its distance along the line, a polygon's by its window position. + if job.line { + if let Some((pattern, repeat)) = ds.line_stipple { + let d = t.line_d[0] + t.line_d[1] * i + t.line_d[2] * j; + if pattern >> (((d.max(0.0f32) as u32) / repeat.max(1)) & 15) & 1 == 0 { + continue; + } + } + } else if let Some(rows) = &ds.polygon_stipple { + let (col, row) = ((x - ds.origin[0]) & 31, (y - ds.origin[1]) & 31); + if rows[row as usize] >> (31 - col) & 1 == 0 { + continue; + } + } + let mut src = [at(t, 0, i, j, 8191.0f32), at(t, 1, i, j, 8191.0f32), at(t, 2, i, j, 8191.0f32), at(t, 3, i, j, 8191.0f32)]; + if let (Some(d), Some(lod)) = (&ds.texture, lod) { + let (s, tt, r) = coord(x, y); + let texel = S::sample(tex, d, s, tt, r, lod); + src = environment(d, src, texel); + } + // Fog follows the texture environment and comes before the alpha test: OpenGL's + // order, the model's choice. The coordinate is the plane of coordinate / w over the plane of 1 / w. + if FOG != 0 { + let (n, q) = (t.fog[0][0] + t.fog[0][1] * i + t.fog[0][2] * j, t.fog[1][0] + t.fog[1][1] * i + t.fog[1][2] * j); + let c = if q.abs() > 1e-30f32 { n / q } else { n }; + src = fog.apply(src, fog.factor::(c)); + } + // The share covered scales alpha, after fog and before the alpha test. + if AA { + src[3] = (src[3] as f32 * cov + 0.5f32) as u32; + } + let (i, j) = ((x - t.origin[0]) as i64, (y - t.origin[1]) as i64); + match &ds.samples { + None => planes.shade(ds, index, src, depth_at(t, i, j)), + // A multisampled fragment keeps the samples the sample mask and alpha-to-mask leave it, + // shaded once and written to each, at the depth of the sample. + Some(ms) => { + let mut m = covered & ms.allowed(x); + if ms.alpha_to_mask { + m &= ms.alpha_samples(src[3]); + } + if ms.alpha_to_one { + src[3] = 8191; + } + if m != 0 { + planes.shade_samples(ds, index, src, |k| depth_at_sample(t, i, j, ms.at[k]), m); + } + } + } + } + } + } +} diff --git a/src/dev/kona/back/raster/setup.rs b/src/dev/kona/back/raster/setup.rs new file mode 100644 index 000000000..bb2fb72cc --- /dev/null +++ b/src/dev/kona/back/raster/setup.rs @@ -0,0 +1,370 @@ +//! Primitive set-up: a triangle of window-relative vertices into a `TriJob`. +//! +//! This is the one place the raster stage uses `f64`, once per triangle, to +//! solve the attribute planes; what it hands the per-pixel module (`pixel`) is +//! integers and `f32` (a test holds `pixel.rs` to that). +//! +//! Vertices arrive on the 1/256-pixel grid, rounded once by the GL path as the engine's subtract of +//! 49152.0 rounds them (`vertex::grid`). +//! Set-up takes the GL half-pixel offset off x and y (so a GL +//! unit square lights the pixel GL specifies), then snaps to the +//! 1/8 grid, nearest with ties up: every primitive is computed to 8 x 8 +//! sub-pixel accuracy (established, published). The window's origin is added +//! after, in whole pixels. +//! +//! Edges are exact integers on that grid. A sample sits at the pixel's integer +//! corner (sample 0 at offset (0, 0), inferred; a multisampled window's samples are +//! at their own offsets from that corner, see `multisample`); one exactly on a left or +//! bottom edge is out, one on a right or top edge is in (inferred, from the +//! whole-pixel rectangle the textport sends). Every sample falls on one side of +//! an edge two triangles share, so shared edges are watertight. A triangle of +//! no area draws nothing. +//! +//! There is no clip in window space: a triangle is cut only by the job's +//! rectangle, the box of its sample points cut to the clip rectangle and the +//! viewport. The attribute planes are valued at that rectangle's corner, not at +//! the triangle's own box, so a triangle far outside the screen keeps its +//! slopes and its colour where it crosses the screen's edge. + +use super::smooth::{Cover, MAX_EDGES}; +use super::{DrawState, Rect, WVertex}; + +/// A triangle after set-up: three edge functions on the 1/8-pixel grid, exact +/// in integers, and a plane for each attribute in single precision, valued +/// at the pixel `origin`, the corner of the job's rectangle. All the `f64` of +/// set-up is spent making these. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Tri { + /// `e(x, y) = a * x + b * y + c` at the sample of pixel (x, y); a sample + /// is inside when `e > 0`, or `e == 0` and `tie` (a right or top edge). + pub a: [i64; 3], + pub b: [i64; 3], + pub c: [i64; 3], + pub tie: [bool; 3], + pub origin: [i32; 2], + /// Red, green, blue and alpha in 0..=8191: + /// `value = at_origin + dx * (x - origin.x) + dy * (y - origin.y)`. + pub at_origin: [f32; 4], + pub dx: [f32; 4], + pub dy: [f32; 4], + /// Depth, a fraction of `u32::MAX` in 32.16 fixed point, on the same plane form + /// in integers: the compare is exact whatever the host. + pub z: Plane, + /// For a line's triangle, the distance along the line in pixels, as the colour + /// planes are: `at`, `dx`, `dy`. + pub line_d: [f32; 3], + /// The texture words `s/w, t/w, r/w, q/w`, each `at`, `dx`, `dy` as the colour planes are. + pub tex: [[f32; 3]; 4], + /// The fog coordinate over w, then 1 / w, as planes the same way. + pub fog: [[f32; 3]; 2], + /// What covers the triangle's pixels besides the triangle, for a smoothed primitive. + pub cover: Cover, +} + +/// An integer attribute plane: `at + dx * i + dy * j` at the pixel `i, j` from +/// the job's origin. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Plane { + pub at: i64, + pub dx: i64, + pub dy: i64, +} + +/// A triangle to draw inside `rect`, under draw state `state` (an entry of the +/// scheduler's table). +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct TriJob { + pub tri: Tri, + /// A triangle of a line: the line stipple applies to it, and the polygon stipple does not. + pub line: bool, + /// Where the triangle can write: its box of sample points cut to the clip + /// rectangle and the viewport. + pub rect: Rect, + pub state: u16, +} + +/// Why a triangle has no set-up. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Skip { + /// Its three vertices are on a line (after snapping to the 1/8 grid). + NoArea, + /// It has area, but no pixel's sample falls inside its box. + NoSample, + /// It has samples, but none inside the clip rectangle and the viewport. + Outside, +} + +/// One vertex on the 1/8 grid with its attributes. +#[derive(Clone, Copy, Debug)] +struct V { + x: i64, + y: i64, + attr: [f64; 12], +} + +/// How a triangle's pixels are chosen and weighted. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Shape<'a> { + /// One sample at each pixel's corner against the three edges: the plain rule. + Plain, + /// A triangle of the fan a smoothed polygon or line is drawn as (`smooth`): every pixel the + /// polygon reaches, weighted by the share of it the polygon covers. `inner[k]` says the edge from + /// the triangle's vertex `k` is an inner edge of the fan. + Smooth { inner: [bool; 3], outline: &'a Outline }, + /// A triangle of the square about a smoothed point of radius `r` pixels at (`x`, `y`) in the + /// window's pixel coordinates, GL's: the pixel centres are half a pixel on. + Disc { x: f32, y: f32, r: f32 }, +} + +/// A smoothed polygon's edges, each as the point it starts at and its unit normal pointing inside, +/// in pixels of the sample plane, from the vertices as set-up snaps them. An edge of no length (clipping +/// can leave two vertices alike) is left out. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Outline { + n: usize, + e: [[f64; 4]; MAX_EDGES], +} + +impl Outline { + /// The outline of the polygon `v`, or None for one smoothing does not take: under three vertices, + /// over `MAX_EDGES` edges or of no area. + pub fn new(v: &[WVertex], ds: &DrawState) -> Option { + if v.len() < 3 || v.len() > MAX_EDGES { + return None; + } + let p: Vec = v.iter().map(|w| vertex(w, ds.origin)).collect(); + let area2: i64 = (0..p.len()).map(|k| p[k].x * p[(k + 1) % p.len()].y - p[(k + 1) % p.len()].x * p[k].y).sum(); + if area2 == 0 { + return None; + } + // The normal to the left of an edge points inside a counter-clockwise polygon. + let turn = if area2 > 0 { 1.0 } else { -1.0 }; + let mut o = Outline { n: 0, e: [[0.0; 4]; MAX_EDGES] }; + for k in 0..p.len() { + let (s, e) = (p[k], p[(k + 1) % p.len()]); + let (dx, dy) = ((e.x - s.x) as f64, (e.y - s.y) as f64); + let len = dx.hypot(dy); + if len > 0.0 { + o.e[o.n] = [s.x as f64 / 8.0, s.y as f64 / 8.0, -dy / len * turn, dx / len * turn]; + o.n += 1; + } + } + Some(o) + } +} + +fn vertex(v: &WVertex, origin: [i32; 2]) -> V { + // 1/256 to 1/8: take the half pixel off (128 of 256), then round to the + // nearest eighth, ties up (adding half of 32 and shifting floors). + let snap = |c: i32, o: i32| ((c as i64 - 128 + 16) >> 5) + o as i64 * 8; + V { + x: snap(v.x, origin[0]), + y: snap(v.y, origin[1]), + attr: [v.c[0] as f64, v.c[1] as f64, v.c[2] as f64, v.c[3] as f64, v.z as f64, v.d as f64, v.t[0][0] as f64, v.t[0][1] as f64, v.t[0][2] as f64, v.t[0][3] as f64, v.fog[0] as f64, v.fog[1] as f64], + } +} + +/// The set-up of triangle `v` under draw state `ds` (entry `state` of the +/// scheduler's table). `Skip` says why there is none: no area, no pixel +/// sample in its box, or none inside the clip rectangle and the viewport. +#[cfg(test)] +pub fn triangle(v: &[WVertex; 3], ds: &DrawState, state: u16) -> Result { + shaped(v, ds, state, Shape::Plain) +} + +/// `triangle`, chosen and weighted by `shape`. +pub fn shaped(v: &[WVertex; 3], ds: &DrawState, state: u16, shape: Shape<'_>) -> Result { + let mut p = [vertex(&v[0], ds.origin), vertex(&v[1], ds.origin), vertex(&v[2], ds.origin)]; + let area2 = (p[1].x - p[0].x) * (p[2].y - p[0].y) - (p[2].x - p[0].x) * (p[1].y - p[0].y); + if area2 == 0 { + return Err(Skip::NoArea); + } + // Whether the edge that starts at each vertex is an inner edge, in the order the edges are taken. + let mut inner = match shape { + Shape::Smooth { inner, .. } => inner, + _ => [false; 3], + }; + // Counter-clockwise, so the inside is on the left of every edge. + if area2 < 0 { + p.swap(1, 2); + // The edges are the same three, run the other way: the one that started at the second vertex + // now starts at the third. + inner = [inner[2], inner[1], inner[0]]; + } + // The box of integer sample points (multiples of 8) the triangle spans. + let lo = |f: fn(&V) -> i64| p.iter().map(f).min().expect("three"); + let hi = |f: fn(&V) -> i64| p.iter().map(f).max().expect("three"); + let ceil8 = |v: i64| (v + 7) >> 3; + let floor8 = |v: i64| v >> 3; + // With samples inside the pixel, a pixel whose corner is left of the triangle can still be covered. + let first = |v: i64| if ds.samples.is_some() { floor8(v) } else { ceil8(v) }; + // A smoothed triangle reaches a pixel whose sample is outside it: by less than a pixel. + let pad = matches!(shape, Shape::Smooth { .. }) as i64; + let (x0, x1) = (first(lo(|v| v.x)) - pad, floor8(hi(|v| v.x)) + 1 + pad); + let (y0, y1) = (first(lo(|v| v.y)) - pad, floor8(hi(|v| v.y)) + 1 + pad); + if x0 >= x1 || y0 >= y1 { + return Err(Skip::NoSample); + } + // The guard band keeps positions inside `i32` (nine viewport widths), so + // the clamp only guards an absurd viewport. + let clamp = |v: i64| v.clamp(i32::MIN as i64, i32::MAX as i64) as i32; + let bounds = Rect { x0: clamp(x0), y0: clamp(y0), x1: clamp(x1), y1: clamp(y1) }; + let rect = bounds.intersect(&ds.clip).intersect(&ds.viewport); + if rect.is_empty() { + return Err(Skip::Outside); + } + let mut t = Tri { origin: [rect.x0, rect.y0], ..Tri::default() }; + for i in 0..3 { + let (s, e) = (p[i], p[(i + 1) % 3]); + let (dx, dy) = (e.x - s.x, e.y - s.y); + // e(sx, sy) = dx * (sy - s.y) - dy * (sx - s.x) at sx = 8x, sy = 8y. + t.a[i] = -dy * 8; + t.b[i] = dx * 8; + t.c[i] = dy * s.x - dx * s.y; + t.tie[i] = dy > 0 || (dy == 0 && dx < 0); + } + t.cover = match shape { + Shape::Plain => Cover::None, + Shape::Smooth { outline, .. } => { + // Each edge as a plane from the job's first pixel: its normal and the distance there. + let (ox, oy) = (rect.x0 as f64, rect.y0 as f64); + let mut plane = [[0.0f32; 3]; MAX_EDGES]; + for (pl, [sx, sy, nx, ny]) in plane.iter_mut().zip(&outline.e[..outline.n]) { + *pl = [*nx as f32, *ny as f32, (nx * (ox - sx) + ny * (oy - sy)) as f32]; + } + Cover::Edges { inner, n: outline.n as u8, plane } + } + // Sample-plane coordinates: GL's pixel centres are half a pixel on, and the window's origin is added. + Shape::Disc { x, y, r } => Cover::Disc { x: x - 0.5 + ds.origin[0] as f32, y: y - 0.5 + ds.origin[1] as f32, r }, + }; + // Attribute planes, in pixel units, from the vertices' own positions, + // valued at the rectangle's corner. + let px = |v: &V| (v.x as f64 / 8.0, v.y as f64 / 8.0); + let ((ax, ay), (bx, by), (cx, cy)) = (px(&p[0]), px(&p[1]), px(&p[2])); + let det = (bx - ax) * (cy - ay) - (cx - ax) * (by - ay); + for k in 0..12 { + let (db, dc) = (p[1].attr[k] - p[0].attr[k], p[2].attr[k] - p[0].attr[k]); + let gx = (db * (cy - ay) - dc * (by - ay)) / det; + let gy = (dc * (bx - ax) - db * (cx - ax)) / det; + let at = p[0].attr[k] + gx * (rect.x0 as f64 - ax) + gy * (rect.y0 as f64 - ay); + if k < 4 { + t.dx[k] = gx as f32; + t.dy[k] = gy as f32; + t.at_origin[k] = at as f32; + } else if k == 5 { + t.line_d = [at as f32, gx as f32, gy as f32]; + } else if k > 9 { + t.fog[k - 10] = [at as f32, gx as f32, gy as f32]; + } else if k > 5 { + t.tex[k - 6] = [at as f32, gx as f32, gy as f32]; + } else { + // 16 fraction bits; the depth fraction is under 2^32, so the plane's value fits `i64`; its + // slope, times a pixel offset, can exceed it for a sliver at the clamp, which the + // per-pixel module's wrapping arithmetic tolerates. + let fx = |v: f64| (v * 65536.0).round().clamp(-(1i64 << 62) as f64, (1i64 << 62) as f64) as i64; + // Polygon offset: factor times the larger depth slope, plus units times the + // smallest step of the depth the planes hold (a 24-bit depth in the top of + // the 32-bit fraction, so 256). The formula is Iris's own. + let off = ds.offset.map_or(0.0, |[factor, units]| factor as f64 * gx.abs().max(gy.abs()) + units as f64 * 256.0); + t.z = Plane { at: fx(at + off), dx: fx(gx), dy: fx(gy) }; + } + } + Ok(TriJob { tri: t, line: false, rect, state }) +} + +#[cfg(test)] +mod tests { + use super::*; + + pub fn vtx(x: f32, y: f32, c: [u16; 4], z: u32) -> WVertex { + WVertex { x: (x * 256.0) as i32, y: (y * 256.0) as i32, z, c, t: [[0.0; 4]; 2], d: 0.0, fog: [0.0; 2] } + } + + const BIG: Rect = Rect { x0: -4096, y0: -4096, x1: 4096, y1: 4096 }; + + fn ds(origin: [i32; 2], clip: Rect, viewport: Rect) -> DrawState { + DrawState { origin, clip, viewport, ..DrawState::default() } + } + + fn tri3(a: WVertex, b: WVertex, c: WVertex, d: &DrawState) -> Result { + triangle(&[a, b, c], d, 7) + } + + #[test] + fn a_triangle_of_no_area_has_no_setup() { + let (c, d) = ([0; 4], ds([0, 0], BIG, BIG)); + assert_eq!(tri3(vtx(1.0, 1.0, c, 0), vtx(2.0, 2.0, c, 0), vtx(3.0, 3.0, c, 0), &d).err(), Some(Skip::NoArea)); + // A sliver between two sample columns has area and no sample. + assert_eq!(tri3(vtx(1.625, 1.5, c, 0), vtx(1.875, 1.5, c, 0), vtx(1.625, 5.5, c, 0), &d).err(), Some(Skip::NoSample)); + // A triangle with samples wholly outside the clip rectangle or the viewport. + let off = ds([0, 0], Rect { x0: 20, y0: 20, x1: 30, y1: 30 }, BIG); + assert_eq!(tri3(vtx(1.5, 1.5, c, 0), vtx(5.5, 1.5, c, 0), vtx(1.5, 5.5, c, 0), &off).err(), Some(Skip::Outside)); + let off = ds([0, 0], BIG, Rect { x0: 20, y0: 20, x1: 30, y1: 30 }); + assert_eq!(tri3(vtx(1.5, 1.5, c, 0), vtx(5.5, 1.5, c, 0), vtx(1.5, 5.5, c, 0), &off).err(), Some(Skip::Outside)); + } + + /// The half pixel comes off, then the grid is eighths with ties up: a vertex + /// at 1.5 in GL window coordinates is pixel 1.0, 8 on the grid. + #[test] + fn vertices_lose_half_a_pixel_and_snap_to_eighths_ties_up() { + let c = [0; 4]; + let t = tri3(vtx(1.5, 1.5, c, 0), vtx(5.5, 1.5, c, 0), vtx(1.5, 5.5, c, 0), &ds([0, 0], BIG, BIG)).unwrap(); + assert_eq!(t.rect, Rect { x0: 1, y0: 1, x1: 6, y1: 6 }, "samples at 1..=5 in each axis"); + assert_eq!(t.tri.origin, [1, 1]); + assert_eq!(t.state, 7); + let v = vertex(&vtx(0.5 + 1.0 / 16.0, 0.0, c, 0), [0, 0]); + assert_eq!(v.x, 1, "a sixteenth is half an eighth: ties go up"); + assert_eq!(vertex(&vtx(0.5 + 1.0 / 16.0 - 1.0 / 256.0, 0.0, c, 0), [0, 0]).x, 0); + } + + #[test] + fn the_planes_are_valued_at_the_origin_and_slope_across_the_triangle() { + let (r, g) = ([8191, 0, 0, 8191], [0, 8191, 0, 8191]); + let t = tri3(vtx(0.5, 0.5, r, 0), vtx(8.5, 0.5, g, 0), vtx(0.5, 8.5, [0, 0, 8191, 8191], 0x8000_0000), &ds([0, 0], BIG, BIG)).unwrap().tri; + assert_eq!(t.origin, [0, 0]); + assert_eq!(t.at_origin[0], 8191.0); + assert!((t.dx[0] + 8191.0 / 8.0).abs() < 1e-2 && (t.dy[0] + 8191.0 / 8.0).abs() < 1e-2, "red falls to zero along both edges"); + assert!((t.dx[1] - 8191.0 / 8.0).abs() < 1e-2); + assert!((t.z.dy as f64 / 65536.0 - 2147483648.0 / 8.0).abs() < 1.0); + } + + /// Polygon offset moves the depth plane's value, not its slope: factor times the + /// larger slope plus units steps of 256. + #[test] + fn polygon_offset_adds_slope_and_units_to_the_depth_plane() { + let c = [0; 4]; + let v = |x: f32, y: f32, z: u32| vtx(x, y, c, z); + let tri = [v(0.5, 0.5, 0), v(8.5, 0.5, 0x0800_0000), v(0.5, 8.5, 0)]; + let plain = triangle(&tri, &ds([0, 0], BIG, BIG), 0).unwrap().tri.z; + let off = triangle(&tri, &DrawState { offset: Some([2.0, 3.0]), ..ds([0, 0], BIG, BIG) }, 0).unwrap().tri.z; + assert_eq!((off.dx, off.dy), (plain.dx, plain.dy), "the slope is unchanged"); + let slope = plain.dx.abs().max(plain.dy.abs()) as f64 / 65536.0; + let want = ((2.0 * slope + 3.0 * 256.0) * 65536.0).round() as i64; + assert!((off.at - plain.at - want).abs() <= 2, "{} against {want}", off.at - plain.at); + } + + #[test] + fn the_window_origin_moves_the_triangle() { + let c = [0; 4]; + let t = tri3(vtx(0.5, 0.5, c, 0), vtx(4.5, 0.5, c, 0), vtx(0.5, 4.5, c, 0), &ds([100, 50], BIG, BIG)).unwrap(); + assert_eq!(t.rect, Rect { x0: 100, y0: 50, x1: 105, y1: 55 }); + } + + /// The job's rectangle is the triangle's box cut to the clip rectangle and + /// the viewport, and the planes are valued at its corner. + #[test] + fn a_triangle_far_off_screen_keeps_its_colour_at_the_screen_edge() { + let (r, g, b) = ([8191, 0, 0, 8191], [0, 8191, 0, 8191], [0, 0, 8191, 8191]); + // Red at (-4000, -4000), green at (4000, -4000), blue at (-4000, 4000): at (x, y) the red + // weight is 1 - (x + 4000) / 8000 - (y + 4000) / 8000, and green and blue the two terms. + let screen = Rect { x0: 0, y0: 0, x1: 64, y1: 64 }; + let t = tri3(vtx(-4000.0, -4000.0, r, 0), vtx(4000.0, -4000.0, g, 0), vtx(-4000.0, 4000.0, b, 0), &ds([0, 0], screen, screen)).unwrap(); + assert_eq!(t.rect, screen); + assert_eq!(t.tri.origin, [0, 0]); + // The half pixel comes off the vertices, so pixel (0, 0) sits at 4000.5 / 8000 along each axis. + let (wg, wb) = (4000.5 / 8000.0, 4000.5 / 8000.0); + let want = |w: f64| (w * 8191.0) as f32; + let at = t.tri.at_origin; + assert!((at[1] - want(wg)).abs() < 2.0 && (at[2] - want(wb)).abs() < 2.0 && (at[0] - want(1.0 - wg - wb)).abs() < 2.0, "{at:?}"); + } +} diff --git a/src/dev/kona/back/raster/smooth.rs b/src/dev/kona/back/raster/smooth.rs new file mode 100644 index 000000000..d50ae2388 --- /dev/null +++ b/src/dev/kona/back/raster/smooth.rs @@ -0,0 +1,241 @@ +//! Smoothed (antialiased) primitives: the coverage of a pixel by a point, a line or a polygon. +//! +//! What is known of the pipe: it has a polygon-smoothing class and a line-smoothing class, with the +//! sampling-word values it writes for each (established), and the capabilities are the public smooth +//! enables, whose state rows the generic state query lists (established as rows). Multisampling wins +//! over polygon smoothing, and smoothing wins over multisampling for lines. What is unknown is the +//! rule itself: the rasterisation rules for multisampled lines, points and polygons beyond the +//! three sampling selects (tie rules, multisampled line and polygon coverage), and how the coverage +//! reaches the fragment. +//! +//! Everything below is therefore an approximation of the model's own, kept in this one file so a rule +//! the hardware turns out to follow replaces it here and nowhere else: +//! +//! - **Coverage scales alpha.** A smoothed fragment's alpha is multiplied by the fraction of the +//! pixel's square the primitive covers, after texture and fog and before the alpha test, as OpenGL's +//! published definition has it. Whether the board does that, or feeds a coverage word to its image +//! engines as it does for sample masks, is unknown. +//! - **A polygon edge, a line side or a line end is a half-plane.** The pixel's share of one is the +//! exact area of the unit square on that side of a straight line (`half_plane`); a pixel at a +//! corner takes the product of the shares of all the primitive's edges, which is exact for an +//! axis-aligned corner and close otherwise. A line thinner than a pixel is overstated by the +//! product. A polygon of more than `MAX_EDGES` edges (after clipping) is not smoothed. +//! - **A point is a disc,** its coverage the share of an 8 x 8 grid of sample positions in the pixel +//! that lie inside it (`DISC_GRID`), so it takes 65 levels. +//! - **A pixel is its unit square.** Coverage is measured over the square centred on the pixel's +//! position in GL's window coordinates (the plain rule's sample sits at the square's corner in +//! the board's own frame; the set-up half-pixel conversion makes the two agree for a GL unit +//! square). Every pixel with any coverage is drawn, so a smoothed primitive reaches up to one pixel +//! past where the plain rule stops. +//! - **Inner edges are not smoothed.** A polygon is drawn as a fan of triangles; only the edges of the +//! polygon are ramps, and the fan's inner edges keep the unsmoothed centre test, so a pixel on one +//! belongs to exactly one triangle and the seam does not blend twice. Each triangle weighs its +//! pixels by every edge of the polygon, so a pixel at a corner the fan splits is covered as the +//! corner covers it. +//! - **A multisampled window:** smoothing wins for lines and points there and multisampling for +//! polygons (the sampling word's values and the point handler's order of tests, established), so a +//! smoothed line or point still takes coverage, written to every sample of the pixel with alpha +//! scaled, and a polygon is multisampled (`DrawState::smooth`). +//! - **Lines and points deliver coverage as alpha in 8 bits** (the pipe loads its line coverage +//! ramps with 8-bit values scaled by 255.0: established for the ramps, inferred for their use as +//! alpha), so their coverage is rounded to a multiple of 1/255 (`quantise`). The ramps are indexed +//! by distance in 32 steps; the exact area is the model's, not that. A smoothed polygon's +//! coverage is computed in the pixel generator and its rule is unknown, so it stays the exact +//! area (approximated). The alpha-to-mask fade table (nine entries, entry n with n bits set) +//! is written at start-up; how alpha indexes it is unknown and the model does not use it. +//! +//! What would settle them: a capture of a smoothed line, point and polygon on the board (or the +//! coverage tables and the alpha-to-mask fade table, which are unread) +//! giving the per-pixel result for a known primitive. +//! +//! This file is held to the per-pixel module's rule: single-precision multiplies and adds, no doubles, +//! so a result is the same on every host. + +use super::setup::Tri; + +/// What covers a triangle's pixels besides the triangle itself. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum Cover { + /// Nothing: the plain rule. + #[default] + None, + /// A triangle of a smoothed polygon or line. `inner[k]` says edge `k` of the triangle is an inner + /// edge of the fan, which keeps the centre test; the triangle's other edges are the polygon's + /// and ramp. The primitive's `n` edges are each a plane `[nx, ny, d]`, its unit normal pointing + /// inside and the distance of the job's first pixel, so a pixel `(i, j)` from there is `nx i + ny j + d` + /// inside the edge. + Edges { inner: [bool; 3], n: u8, plane: [[f32; 3]; MAX_EDGES] }, + /// A triangle of the square about a smoothed point: the disc's centre and radius in pixels, in + /// the plane of the sample positions. + Disc { x: f32, y: f32, r: f32 }, +} + +/// The most edges a smoothed polygon can have and be smoothed. +pub const MAX_EDGES: usize = 8; + +/// Sample positions a side of a disc's pixel is cut into. +const DISC_GRID: u32 = 8; + +/// A line's or point's coverage as the 8-bit alpha the ramps deliver. +#[inline(always)] +pub fn quantise(cov: f32) -> f32 { + ((cov * 255.0f32 + 0.5f32) as u32) as f32 / 255.0f32 +} + +/// The share of the unit square about the origin on the inside of a line, as an exact area. The +/// line's unit normal points inside, `(nx, ny)`, and the origin is `s` pixels inside it (negative: +/// outside). The projection of the square on the normal is a trapezoid of half-width +/// `(|nx| + |ny|) / 2`, flat over `||nx| - |ny|| / 2` and rising linearly outside that, whose +/// integral is the answer. +#[inline(always)] +pub fn half_plane(nx: f32, ny: f32, s: f32) -> f32 { + let (ax, ay) = (nx.abs(), ny.abs()); + let (a, b) = if ax >= ay { (ax, ay) } else { (ay, ax) }; + let (w, h) = ((a + b) * 0.5f32, (a - b) * 0.5f32); + let t = -s; + let below = if t <= -w { + 0.0f32 + } else if t >= w { + 1.0f32 + } else if t < -h { + // The rising part: only when b > 0, since otherwise -w == -h. + let u = t + w; + u * u / (2.0f32 * a * b) + } else if t <= h { + b / (2.0f32 * a) + (t + h) / a + } else { + let u = w - t; + 1.0f32 - u * u / (2.0f32 * a * b) + }; + 1.0f32 - below +} + +/// The share of the pixel (`x`, `y`) a disc of radius `r` about (`cx`, `cy`) covers. Whole and empty +/// pixels are told by distance without sampling. +#[inline(always)] +fn disc(cx: f32, cy: f32, r: f32, x: i32, y: i32) -> f32 { + // The farthest a pixel's corner is from its centre. + const REACH: f32 = 0.7072f32; + let (dx, dy) = (x as f32 - cx, y as f32 - cy); + let d2 = dx * dx + dy * dy; + if r > REACH && d2 <= (r - REACH) * (r - REACH) { + return 1.0f32; + } + if d2 >= (r + REACH) * (r + REACH) { + return 0.0f32; + } + let step = 1.0f32 / DISC_GRID as f32; + let (x0, y0) = (dx - 0.5f32 + step * 0.5f32, dy - 0.5f32 + step * 0.5f32); + let mut n = 0u32; + for j in 0..DISC_GRID { + let py = y0 + step * j as f32; + for i in 0..DISC_GRID { + let px = x0 + step * i as f32; + n += (px * px + py * py <= r * r) as u32; + } + } + n as f32 / (DISC_GRID * DISC_GRID) as f32 +} + +/// The share of pixel (`x`, `y`) the triangle `t` covers, 0 when the pixel is not drawn at all. With +/// `Cover::None` it is the plain rule's one or none. +#[inline(always)] +pub fn coverage(t: &Tri, x: i32, y: i32) -> f32 { + match t.cover { + Cover::None => (0..3).all(|k| inside(t, k, x, y)) as u32 as f32, + Cover::Disc { x: cx, y: cy, r } => { + // The square about the disc holds the pixels the disc can touch; the triangle's own + // centre test shares them between its two halves. + if !(0..3).all(|k| inside(t, k, x, y)) { + return 0.0f32; + } + disc(cx, cy, r, x, y) + } + Cover::Edges { inner, n, plane } => { + for k in 0..3 { + if inner[k] && !inside(t, k, x, y) { + return 0.0f32; + } + } + let (i, j) = ((x - t.origin[0]) as f32, (y - t.origin[1]) as f32); + let mut c = 1.0f32; + for [nx, ny, d] in &plane[..n as usize] { + let s = nx * i + ny * j + d; + // Farther inside than a pixel's corner is from its centre: whole. + if s >= 0.7072f32 { + continue; + } + c *= half_plane(*nx, *ny, s); + if c <= 0.0f32 { + return 0.0f32; + } + } + c + } + } +} + +/// Whether the sample of pixel (`x`, `y`) is on the inside of edge `k` of `t`, a tie going to a +/// right or top edge. +#[inline(always)] +fn inside(t: &Tri, k: usize, x: i32, y: i32) -> bool { + let e = t.a[k] * x as i64 + t.b[k] * y as i64 + t.c[k]; + e > 0 || (e == 0 && t.tie[k]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_axis_aligned_edge_covers_the_distance_it_cuts() { + for (s, want) in [(-1.0f32, 0.0f32), (-0.5, 0.0), (-0.25, 0.25), (0.0, 0.5), (0.25, 0.75), (0.5, 1.0), (3.0, 1.0)] { + assert_eq!(half_plane(1.0, 0.0, s), want, "x normal at {s}"); + assert_eq!(half_plane(0.0, -1.0, s), want, "y normal at {s}"); + } + } + + #[test] + fn a_slanted_edge_through_the_centre_covers_half_and_the_two_sides_make_a_whole() { + let n = std::f32::consts::FRAC_1_SQRT_2; + assert!((half_plane(n, n, 0.0) - 0.5).abs() < 1e-6); + for (nx, ny) in [(0.6f32, 0.8f32), (n, -n), (0.2, 0.9798), (-0.9, 0.4359)] { + for s in [-0.6f32, -0.3, -0.1, 0.0, 0.05, 0.2, 0.45, 0.7] { + let (p, q) = (half_plane(nx, ny, s), half_plane(-nx, -ny, -s)); + assert!((p + q - 1.0).abs() < 1e-5, "n ({nx}, {ny}) s {s}: {p} + {q}"); + assert!((0.0..=1.0).contains(&p)); + } + } + // Against the area counted on a fine grid of the square. + let area = |s: f32| { + // Brute force over a fine grid of the square. + let n = 400; + let mut c = 0; + for j in 0..n { + for i in 0..n { + let (x, y) = ((i as f32 + 0.5) / n as f32 - 0.5, (j as f32 + 0.5) / n as f32 - 0.5); + c += (0.6 * x + 0.8 * y + s >= 0.0) as u32; + } + } + c as f32 / (n * n) as f32 + }; + for s in [-0.6f32, -0.35, -0.1, 0.0, 0.2, 0.5] { + assert!((half_plane(0.6, 0.8, s) - area(s)).abs() < 2e-3, "s {s}: {} against {}", half_plane(0.6, 0.8, s), area(s)); + } + } + + #[test] + fn a_disc_covers_by_its_share_of_the_pixel() { + // Centred on the pixel, radius one half: the disc inscribed in the square, pi / 4. + let c = disc(5.0, 5.0, 0.5, 5, 5); + assert!((c - std::f32::consts::FRAC_PI_4).abs() < 0.04, "{c}"); + // Far inside is whole, far outside is nothing, and a pixel whose square touches the rim is partial. + assert_eq!(disc(5.0, 5.0, 6.0, 5, 6), 1.0); + assert_eq!(disc(5.0, 5.0, 2.0, 9, 5), 0.0); + let rim = disc(5.0, 5.0, 2.0, 7, 5); + assert!(rim > 0.0 && rim < 1.0, "{rim}"); + // By symmetry the four pixels about a centre on a pixel corner share it equally. + let q = [disc(5.5, 5.5, 1.0, 5, 5), disc(5.5, 5.5, 1.0, 6, 5), disc(5.5, 5.5, 1.0, 5, 6), disc(5.5, 5.5, 1.0, 6, 6)]; + assert!(q.iter().all(|&v| v == q[0]) && (q[0] * 4.0 - std::f32::consts::PI).abs() < 0.15, "{q:?}"); + } +} diff --git a/src/dev/kona/back/raster/texel.rs b/src/dev/kona/back/raster/texel.rs new file mode 100644 index 000000000..acc5939d8 --- /dev/null +++ b/src/dev/kona/back/raster/texel.rs @@ -0,0 +1,704 @@ +//! Texture sampling and the texture environment, on plain data. +//! +//! A `TexDesc` is a bound texture as a draw sees it: the levels' places in texture +//! memory, the filters, the wraps and the environment. It holds no pointer, so it +//! sits in `DrawState` and in the scheduler's table. Sampling is nearest or bilinear +//! within a level, nearest or linear between levels; the weights are 8-bit (the +//! model's own). `s = (s/w) / (q/w)` per fragment, so texture is +//! perspective-correct while colour is linear in screen space (both inferred). + +use super::ops::{unpack_colour, FULL}; +use crate::dev::kona::back::store::texture::{Plane, TextureStore}; + +/// Levels a texture can hold: a 4096-texel side is 13 halvings. +pub const MAX_LEVELS: usize = 14; + +/// One level: where its texels sit in texture memory (word `off` of plane `plane`, rows `pitch` apart, +/// starting `ox`, `oy` into the physical rectangle) and its logical size. A volume's level +/// is `d` slices, `slice` texels apart; a 2D level is one slice (`d` zero). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TexLevel { + pub plane: u16, + pub off: u32, + pub pitch: u16, + pub ox: u16, + pub oy: u16, + pub w: u16, + pub h: u16, + pub d: u16, + pub slice: u32, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Filter { + #[default] + Nearest, + Linear, + /// Mip-mapped: the filter within a level, then between the two levels. + Mip { within_linear: bool, between_linear: bool }, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Wrap { + #[default] + Repeat, + /// Clamp (to the edge texel: the model's choice for every non-repeat mode). + Clamp, +} + +/// How the texel combines with the fragment. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum EnvMode { + #[default] + Modulate, + Decal, + Blend, + Replace, +} + +impl EnvMode { + /// The mode a texture-environment mode number names. + pub fn from_gl(v: u32) -> Option { + Some(match v { + 0x2100 => EnvMode::Modulate, + 0x2101 => EnvMode::Decal, + 0x0be2 => EnvMode::Blend, + 0x1e01 | 0x8062 => EnvMode::Replace, + _ => return None, + }) + } +} + +impl Filter { + fn bits(self) -> u32 { + match self { + Filter::Nearest => 0, + Filter::Linear => 1, + Filter::Mip { within_linear, between_linear } => 2 | (within_linear as u32) << 2 | (between_linear as u32) << 3, + } + } + + fn from_bits(b: u32) -> Filter { + match b & 3 { + 0 => Filter::Nearest, + 1 => Filter::Linear, + _ => Filter::Mip { within_linear: b >> 2 & 1 != 0, between_linear: b >> 3 & 1 != 0 }, + } + } +} + +/// The curve nodes a filter table holds: 64 samples at x = log2(i / 64) (the detail and sharpen +/// curve, established). +pub const CURVE_NODES: usize = 64; + +/// The magnification combine a bound 2D texture asks for (mode-word codes 3, 4 and 5). +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum Combine { + #[default] + None, + Detail(Detail), + Sharpen(Sharpen), +} + +/// A detail combine: the detail image (one level) added to or modulating the magnified base. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Detail { + /// Modulate (mode-word code 4) and not add (code 3). + pub modulate: bool, + /// The detail level code, bits 23:21 of the mode word: the negated public value, 0 to 4 (the library rejects + /// others; established). The detail image repeats 2^scale times as often as the base across the base + /// (approximated). + pub scale: u8, + /// The magnification channels, bits 27:26 of the mode word: 0 all, 1 colour only, 2 alpha only, 3 level 0. + pub channels: u8, + pub image: TexLevel, + /// The weight at each curve node, the public weight (the stored curve is doubled for detail). + pub weights: [f32; CURVE_NODES], +} + +/// A sharpen combine: the base pushed away from the next coarser level by the weight. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Sharpen { + pub channels: u8, + /// The weight at each curve node, the public weight (the stored curve is negated for sharpen). + pub weights: [f32; CURVE_NODES], +} + +/// The weights of the client's default curve, points (-4, 1) and (0, 0): linear in the level of detail from 1 at +/// -4 and below to 0 at 0 (established as the default points; the straight line between them is approximated). +pub fn default_weights() -> [f32; CURVE_NODES] { + std::array::from_fn(|i| if i == 0 { 1.0f32 } else { (-(i as f32 / 64.0f32).log2() / 4.0f32).clamp(0.0f32, 1.0f32) }) +} + +/// The 64 samples of a spline-form filter table: four ten-bit codes to two words, each a signed mantissa shifted +/// left two in bits 9:2 and an exponent in bits 1:0, value = mantissa x 2^exponent / 128 (established as the encoding a GL client +/// writes; word 33 is padding). None for a word that carries bits the code does not (the last word of a pair is 8 bits). +/// A GL client makes these codes (established): n = trunc(v x 512 + 2) >> 2, clamped to -1024..1023, then halved +/// until it is in -128..127 with the halvings counted, 0 to 3; the code is (n & 0xff) << 2 | count. A value a client +/// writes therefore reaches no further than 127 x 8 / 128. +pub fn spline_samples(words: &[u32; 33]) -> Option<[f32; CURVE_NODES]> { + let mut out = [0.0f32; CURVE_NODES]; + for i in 0..16 { + let (first, last) = (words[2 * i], words[2 * i + 1]); + if last & !0xff != 0 { + return None; + } + let codes = [first & 0x3ff, first >> 10 & 0x3ff, first >> 20 & 0x3ff, first >> 30 | last << 2]; + for (j, c) in codes.into_iter().enumerate() { + out[4 * i + j] = (c >> 2) as u8 as i8 as f32 * (1u32 << (c & 3)) as f32 / 128.0f32; + } + } + Some(out) +} + +impl Combine { + fn visit(&mut self, s: &mut S) { + let (mut kind, mut modulate, mut scale, mut channels) = (0u32, false, 0u8, 0u8); + let (mut image, mut weights) = (TexLevel::default(), [0.0f32; CURVE_NODES]); + match self { + Combine::None => {} + Combine::Detail(d) => (kind, modulate, scale, channels, image, weights) = (1, d.modulate, d.scale, d.channels, d.image, d.weights), + Combine::Sharpen(p) => (kind, channels, weights) = (2, p.channels, p.weights), + } + s.u32(&mut kind); + s.bool(&mut modulate); + s.u8(&mut scale); + s.u8(&mut channels); + for v in [&mut image.pitch, &mut image.ox, &mut image.oy, &mut image.w, &mut image.h] { + s.u16(v); + } + s.u32(&mut image.off); + for w in weights.iter_mut() { + let mut b = w.to_bits(); + s.u32(&mut b); + *w = f32::from_bits(b); + } + if s.loading() { + *self = match kind { + 1 => Combine::Detail(Detail { modulate, scale, channels, image, weights }), + 2 => Combine::Sharpen(Sharpen { channels, weights }), + _ => Combine::None, + }; + } + } +} + +/// A bound texture and its environment. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct TexDesc { + pub levels: [TexLevel; MAX_LEVELS], + pub count: u8, + pub min: Filter, + pub mag: Filter, + /// The wraps of s, t and r (r only for a volume). + pub wrap: [Wrap; 3], + /// The LOD scales of s, t and r, 16 times the factor (r only for a volume). + pub lod_scale: [u8; 3], + /// A volume (3D) texture: its levels have depth and the sampler takes the r coordinate. + pub volume: bool, + /// What a magnified fragment is combined with (none for a minified one). + pub combine: Combine, + /// The format class: 1 alpha, 2 luminance or RGB, 3 luminance-alpha or RGBA, 4 intensity. + pub class: u8, + pub env: EnvMode, + /// The environment colour in 13-bit units. + pub env_colour: [u32; 4], +} + +impl TexDesc { + /// A bound texture, as the stream module reads and writes it. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { levels: _, count: _, min: _, mag: _, wrap: _, lod_scale: _, volume: _, combine: _, class: _, env: _, env_colour: _ } = self; + for l in self.levels.iter_mut() { + s.u32(&mut l.off); + for v in [&mut l.pitch, &mut l.ox, &mut l.oy, &mut l.w, &mut l.h, &mut l.d] { + s.u16(v); + } + s.u32(&mut l.slice); + } + s.u8(&mut self.count); + for f in [&mut self.min, &mut self.mag] { + let mut b = f.bits(); + s.u32(&mut b); + *f = Filter::from_bits(b); + } + for w in self.wrap.iter_mut() { + let mut b = (*w == Wrap::Clamp) as u32; + s.u32(&mut b); + *w = if b != 0 { Wrap::Clamp } else { Wrap::Repeat }; + } + for v in self.lod_scale.iter_mut() { + s.u8(v); + } + s.bool(&mut self.volume); + self.combine.visit(s); + s.u8(&mut self.class); + let mut e = match self.env { + EnvMode::Modulate => 0, + EnvMode::Decal => 1, + EnvMode::Blend => 2, + EnvMode::Replace => 3, + }; + s.u32(&mut e); + self.env = [EnvMode::Modulate, EnvMode::Decal, EnvMode::Blend, EnvMode::Replace][(e & 3) as usize]; + s.u32s(&mut self.env_colour); + } +} + +/// A coordinate wrapped into `0..n`. +#[inline(always)] +fn wrap(i: i32, n: i32, w: Wrap) -> i32 { + match w { + Wrap::Repeat => i.rem_euclid(n), + Wrap::Clamp => i.clamp(0, n - 1), + } +} + +/// Texel `x`, `y` of level `l`, `mem` being the level's plane (`TextureStore::plane`, taken once for a sample). +#[inline(always)] +fn texel(mem: &Plane, l: &TexLevel, x: i32, y: i32) -> u32 { + mem.get(l.off + (l.oy as u32 + y as u32) * l.pitch as u32 + l.ox as u32 + x as u32) +} + +/// A texel's bytes (r, g, b, a) of the word `0xAARRGGBB`. +#[inline(always)] +fn bytes(w: u32) -> [u32; 4] { + [w >> 16 & 0xff, w >> 8 & 0xff, w & 0xff, w >> 24] +} + +/// A level sampled at `s`, `t` in 0..1 repeating: nearest, or bilinear with 8-bit weights. +fn level(store: &TextureStore, l: &TexLevel, s: f32, t: f32, linear: bool, wr: [Wrap; 3]) -> [u32; 4] { + let mem = store.plane(l.plane); + let (w, h) = (l.w.max(1) as i32, l.h.max(1) as i32); + let (u, v) = (s * w as f32, t * h as f32); + if !linear { + return bytes(texel(mem, l, wrap(u.floor() as i32, w, wr[0]), wrap(v.floor() as i32, h, wr[1]))); + } + let (u, v) = (u - 0.5, v - 0.5); + let (i0, j0) = (u.floor(), v.floor()); + let (fu, fv) = (((u - i0) * 256.0) as u32, ((v - j0) * 256.0) as u32); + let (i0, j0) = (i0 as i32, j0 as i32); + let t00 = bytes(texel(mem, l, wrap(i0, w, wr[0]), wrap(j0, h, wr[1]))); + let t10 = bytes(texel(mem, l, wrap(i0 + 1, w, wr[0]), wrap(j0, h, wr[1]))); + let t01 = bytes(texel(mem, l, wrap(i0, w, wr[0]), wrap(j0 + 1, h, wr[1]))); + let t11 = bytes(texel(mem, l, wrap(i0 + 1, w, wr[0]), wrap(j0 + 1, h, wr[1]))); + std::array::from_fn(|k| { + let top = t00[k] * (256 - fu) + t10[k] * fu; + let bot = t01[k] * (256 - fu) + t11[k] * fu; + (top * (256 - fv) + bot * fv + 32768) >> 16 + }) +} + +/// The texel colour (r, g, b, a in 8 bits) at `s`, `t` with level of detail `lod`. +pub fn sample(store: &TextureStore, d: &TexDesc, s: f32, t: f32, lod: f32) -> [u32; 4] { + let top = (d.count as usize).saturating_sub(1); + let minify = lod > 0.0; + match if minify { d.min } else { d.mag } { + Filter::Nearest => level(store, &d.levels[0], s, t, false, d.wrap), + Filter::Linear => level(store, &d.levels[0], s, t, true, d.wrap), + Filter::Mip { within_linear, between_linear } => { + let lod = lod.clamp(0.0, top as f32); + let l0 = lod.floor() as usize; + if !between_linear || l0 >= top { + let l = if between_linear { l0 } else { (lod + 0.5) as usize }.min(top); + return level(store, &d.levels[l], s, t, within_linear, d.wrap); + } + let (a, b) = (level(store, &d.levels[l0], s, t, within_linear, d.wrap), level(store, &d.levels[l0 + 1], s, t, within_linear, d.wrap)); + let f = ((lod - l0 as f32) * 256.0) as u32; + std::array::from_fn(|k| (a[k] * (256 - f) + b[k] * f + 128) >> 8) + } + } +} + +#[inline(always)] +fn texel3(mem: &Plane, l: &TexLevel, x: i32, y: i32, z: i32) -> u32 { + mem.get(l.off + z as u32 * l.slice + (l.oy as u32 + y as u32) * l.pitch as u32 + l.ox as u32 + x as u32) +} + +/// A volume level sampled at `s`, `t`, `r`: nearest, or trilinear with 8-bit weights (a bilinear +/// blend in each of the two slices the r coordinate falls between, then a blend of the two). The +/// weights are the model's own: the pipe's filter arithmetic is not read (approximated). +fn level3(store: &TextureStore, l: &TexLevel, s: f32, t: f32, r: f32, linear: bool, wr: [Wrap; 3]) -> [u32; 4] { + let mem = store.plane(l.plane); + let (w, h, dp) = (l.w.max(1) as i32, l.h.max(1) as i32, l.d.max(1) as i32); + let (u, v, q) = (s * w as f32, t * h as f32, r * dp as f32); + if !linear { + return bytes(texel3(mem, l, wrap(u.floor() as i32, w, wr[0]), wrap(v.floor() as i32, h, wr[1]), wrap(q.floor() as i32, dp, wr[2]))); + } + let (u, v, q) = (u - 0.5, v - 0.5, q - 0.5); + let (i0, j0, k0) = (u.floor(), v.floor(), q.floor()); + let (fu, fv, fz) = (((u - i0) * 256.0) as u32, ((v - j0) * 256.0) as u32, ((q - k0) * 256.0) as u32); + let (i0, j0, k0) = (i0 as i32, j0 as i32, k0 as i32); + let (xs, ys) = ([wrap(i0, w, wr[0]), wrap(i0 + 1, w, wr[0])], [wrap(j0, h, wr[1]), wrap(j0 + 1, h, wr[1])]); + // One slice's bilinear blend, 16 fraction bits. + let slice = |z: i32| -> [u32; 4] { + let t00 = bytes(texel3(mem, l, xs[0], ys[0], z)); + let t10 = bytes(texel3(mem, l, xs[1], ys[0], z)); + let t01 = bytes(texel3(mem, l, xs[0], ys[1], z)); + let t11 = bytes(texel3(mem, l, xs[1], ys[1], z)); + std::array::from_fn(|k| { + let top = t00[k] * (256 - fu) + t10[k] * fu; + let bot = t01[k] * (256 - fu) + t11[k] * fu; + top * (256 - fv) + bot * fv + }) + }; + let (a, b) = (slice(wrap(k0, dp, wr[2])), slice(wrap(k0 + 1, dp, wr[2]))); + std::array::from_fn(|k| (a[k] * (256 - fz) + b[k] * fz + (1 << 23)) >> 24) +} + +/// The texel colour at `s`, `t`, `r` with level of detail `lod`, for a volume texture: `sample`'s +/// level selection and blending over `level3`. +pub fn sample3(store: &TextureStore, d: &TexDesc, s: f32, t: f32, r: f32, lod: f32) -> [u32; 4] { + let top = (d.count as usize).saturating_sub(1); + match if lod > 0.0 { d.min } else { d.mag } { + Filter::Nearest => level3(store, &d.levels[0], s, t, r, false, d.wrap), + Filter::Linear => level3(store, &d.levels[0], s, t, r, true, d.wrap), + Filter::Mip { within_linear, between_linear } => { + let lod = lod.clamp(0.0, top as f32); + let l0 = lod.floor() as usize; + if !between_linear || l0 >= top { + let l = if between_linear { l0 } else { (lod + 0.5) as usize }.min(top); + return level3(store, &d.levels[l], s, t, r, within_linear, d.wrap); + } + let (a, b) = (level3(store, &d.levels[l0], s, t, r, within_linear, d.wrap), level3(store, &d.levels[l0 + 1], s, t, r, within_linear, d.wrap)); + let f = ((lod - l0 as f32) * 256.0) as u32; + std::array::from_fn(|k| (a[k] * (256 - f) + b[k] * f + 128) >> 8) + } + } +} + +/// The curve's weight at a level of detail at or below zero: the node at 64 x 2^lod, blended linearly with the next. +/// Indexing by 64 x 2^lod is inferred; the blend between nodes is approximated (no source says +/// whether the hardware interpolates or takes the nearest node). +#[inline(always)] +fn curve_weight(w: &[f32; CURVE_NODES], lod: f32) -> f32 { + let at = (lod.exp2() * 64.0f32).min(63.0f32); + let n = at as usize; + let f = at - n as f32; + w[n] * (1.0f32 - f) + w[(n + 1).min(CURVE_NODES - 1)] * f +} + +/// Whether a channel (0 to 3: red, green, blue, alpha) is changed under the magnification channel field. Code 3 +/// ("level 0") is not read and changes every channel (approximated). +#[inline(always)] +fn channel_on(channels: u8, k: usize) -> bool { + match channels { + 1 => k < 3, + 2 => k == 3, + _ => true, + } +} + +/// The texel colour at `s`, `t` for a 2D texture that has a detail or sharpen combine, with level of detail `lod`. +/// A minified fragment, which the combine does not touch, is sampled as `sample` does. A magnified one is the base +/// level sampled bilinearly, then: +/// +/// Detail: the detail image sampled bilinearly, repeating, at the base's level-0 texel coordinate times 2^|L|, L the +/// detail level (the code is the negated public value), wrapped to the detail size: the same position as the base's +/// coordinate times the size ratio times 2^code, which is what this takes. Then `base + K (d - 1/2)` for add and +/// `base (1 + K (d - 1/2))` for modulate, clamped, K the stored doubled curve at node 64 x 2^lod, which is the +/// public weight times two (the position and the forms are inferred). The widths, rounding, the half-texel offset +/// and the clamp point are unknown (approximated: single-precision arithmetic). +/// +/// Sharpen: `A + K (B - A)` with K the stored curve, which is the public weight negated, A the base sampled bilinearly +/// and B the next coarser level (the form and the sign are inferred; the sign of the stored curve is established). The +/// widths and rounding are unknown (approximated). +/// +/// Chosen once per draw (`pixel::run_tri`): a plain texture uses `sample`, which carries none of this. +pub fn sample_enhanced(store: &TextureStore, d: &TexDesc, s: f32, t: f32, lod: f32) -> [u32; 4] { + if lod > 0.0f32 { + return sample(store, d, s, t, lod); + } + let base = level(store, &d.levels[0], s, t, true, d.wrap); + let unit = |v: u32| v as f32 / 255.0f32; + let out = |v: f32| (v.clamp(0.0f32, 1.0f32) * 255.0f32 + 0.5f32) as u32; + match &d.combine { + Combine::None => base, + Combine::Detail(c) => { + let (b0, img) = (&d.levels[0], &c.image); + let k = (1u32 << c.scale.min(15)) as f32; + let (ds, dt) = (s * (b0.w.max(1) as f32 / img.w.max(1) as f32) * k, t * (b0.h.max(1) as f32 / img.h.max(1) as f32) * k); + let other = level(store, img, ds, dt, true, [Wrap::Repeat; 3]); + let w = curve_weight(&c.weights, lod); + std::array::from_fn(|k| { + if !channel_on(c.channels, k) { + return base[k]; + } + let (b, signed) = (unit(base[k]), w * (2.0f32 * unit(other[k]) - 1.0f32)); + out(if c.modulate { b * (1.0f32 + signed) } else { b + signed }) + }) + } + Combine::Sharpen(c) => { + if d.count < 2 { + return base; + } + let coarse = level(store, &d.levels[1], s, t, true, d.wrap); + let w = curve_weight(&c.weights, lod); + std::array::from_fn(|k| if channel_on(c.channels, k) { out(unit(base[k]) + w * (unit(base[k]) - unit(coarse[k]))) } else { base[k] }) + } + } +} + +/// `a * b` of two 13-bit values. +#[inline(always)] +fn mul(a: u32, b: u32) -> u32 { + (a * b + FULL / 2) / FULL +} + +/// The fragment colour `f` (13-bit r, g, b, a) after the texture environment, with the +/// texel `t` (8-bit r, g, b, a from `sample`). Class 1 is alpha only (no colour), 2 +/// luminance or RGB (no alpha), 3 luminance-alpha or RGBA, 4 intensity (the one +/// component stands for colour and alpha). Decal is defined for classes 2 and 3 +/// only; the others modulate. The arithmetic and its rounding are the model's own. +pub fn environment(d: &TexDesc, f: [u32; 4], t: [u32; 4]) -> [u32; 4] { + let tc = unpack_colour(t[3] << 24 | t[0] << 16 | t[1] << 8 | t[2]); + let (ct, at) = ([tc[0], tc[1], tc[2]], match d.class { + 1 | 3 => tc[3], + 4 => tc[0], + _ => FULL, + }); + let k = d.env_colour; + let mode = if d.env == EnvMode::Decal && !matches!(d.class, 2 | 3) { EnvMode::Modulate } else { d.env }; + let mut out = match (mode, d.class) { + (EnvMode::Modulate, 1) => [f[0], f[1], f[2], mul(f[3], at)], + (EnvMode::Modulate, _) => [mul(f[0], ct[0]), mul(f[1], ct[1]), mul(f[2], ct[2]), mul(f[3], at)], + (EnvMode::Decal, 2) => [ct[0], ct[1], ct[2], f[3]], + (EnvMode::Decal, _) => [mul(f[0], FULL - at) + mul(ct[0], at), mul(f[1], FULL - at) + mul(ct[1], at), mul(f[2], FULL - at) + mul(ct[2], at), f[3]], + (EnvMode::Blend, 1) => [f[0], f[1], f[2], mul(f[3], at)], + (EnvMode::Blend, c) => { + let a = if c == 4 { mul(f[3], FULL - at) + mul(k[3], at) } else if c == 2 { f[3] } else { mul(f[3], at) }; + [mul(f[0], FULL - ct[0]) + mul(k[0], ct[0]), mul(f[1], FULL - ct[1]) + mul(k[1], ct[1]), mul(f[2], FULL - ct[2]) + mul(k[2], ct[2]), a] + } + (EnvMode::Replace, 1) => [f[0], f[1], f[2], at], + (EnvMode::Replace, 2) => [ct[0], ct[1], ct[2], f[3]], + (EnvMode::Replace, _) => [ct[0], ct[1], ct[2], at], + }; + out.iter_mut().for_each(|c| *c = (*c).min(FULL)); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A 2 x 2 texture at offset 0: red, green / blue, white, opaque. + fn two() -> (TextureStore, TexDesc) { + let t = TextureStore::default(); + t.clear(0, 0, 4); + for (i, w) in [0xffff_0000u32, 0xff00_ff00, 0xff00_00ff, 0xffff_ffff].into_iter().enumerate() { + t.set(0, i as u32, w); + } + let mut d = TexDesc { count: 1, class: 3, ..Default::default() }; + d.levels[0] = TexLevel { off: 0, pitch: 2, ox: 0, oy: 0, w: 2, h: 2, ..Default::default() }; + (t, d) + } + + #[test] + fn nearest_picks_the_texel_and_wraps() { + let (t, d) = two(); + assert_eq!(sample(&t, &d, 0.25, 0.25, 0.0), [255, 0, 0, 255]); + assert_eq!(sample(&t, &d, 0.75, 0.25, 0.0), [0, 255, 0, 255]); + assert_eq!(sample(&t, &d, 1.25, 0.75, 0.0), [0, 0, 255, 255], "repeat: 1.25 is 0.25"); + let c = TexDesc { wrap: [Wrap::Clamp; 3], ..d }; + assert_eq!(sample(&t, &c, 1.25, 0.25, 0.0), [0, 255, 0, 255], "clamp to the edge texel"); + assert_eq!(sample(&t, &c, -3.0, 5.0, 0.0), [0, 0, 255, 255]); + } + + #[test] + fn bilinear_blends_the_four_texels_at_a_corner() { + let (t, d) = two(); + let l = TexDesc { mag: Filter::Linear, wrap: [Wrap::Clamp; 3], ..d }; + let mid = sample(&t, &l, 0.5, 0.5, 0.0); + assert_eq!(mid, [128, 128, 128, 255], "{mid:?}: the mean of red, green, blue and white"); + assert_eq!(sample(&t, &l, 0.25, 0.25, 0.0), [255, 0, 0, 255], "at a texel's centre, that texel"); + } + + #[test] + fn a_mip_filter_picks_and_blends_levels() { + let t = TextureStore::default(); + t.clear(0, 0, 5); + t.set(0, 0, 0xffff_0000); + t.set(0, 1, 0xffff_0000); + t.set(0, 2, 0xffff_0000); + t.set(0, 3, 0xffff_0000); + t.set(0, 4, 0xff00_00ff); + let mut d = TexDesc { count: 2, class: 3, ..Default::default() }; + d.levels[0] = TexLevel { off: 0, pitch: 2, w: 2, h: 2, ..Default::default() }; + d.levels[1] = TexLevel { off: 4, pitch: 1, w: 1, h: 1, ..Default::default() }; + d.min = Filter::Mip { within_linear: false, between_linear: false }; + assert_eq!(sample(&t, &d, 0.1, 0.1, 0.4), [255, 0, 0, 255], "nearest level: level 0"); + assert_eq!(sample(&t, &d, 0.1, 0.1, 0.6), [0, 0, 255, 255], "level 1"); + d.min = Filter::Mip { within_linear: false, between_linear: true }; + assert_eq!(sample(&t, &d, 0.1, 0.1, 0.5), [128, 0, 128, 255], "halfway between the two"); + assert_eq!(sample(&t, &d, 0.1, 0.1, 9.0), [0, 0, 255, 255], "past the last level: the last"); + assert_eq!(sample(&t, &d, 0.1, 0.1, 0.0), [255, 0, 0, 255], "magnified: the magnification filter on level 0"); + } + + /// A 2 x 2 x 2 volume at offset 0: slice 0 is red, slice 1 is blue (opaque), each slice 4 texels. + fn volume() -> (TextureStore, TexDesc) { + let t = TextureStore::default(); + t.clear(0, 0, 8 + 2); + for i in 0..4 { + t.set(0, i, 0xffff_0000); + t.set(0, 4 + i, 0xff00_00ff); + } + let mut d = TexDesc { count: 1, class: 3, volume: true, wrap: [Wrap::Clamp, Wrap::Clamp, Wrap::Repeat], ..Default::default() }; + d.levels[0] = TexLevel { off: 0, pitch: 2, w: 2, h: 2, d: 2, slice: 4, ..Default::default() }; + (t, d) + } + + #[test] + fn a_volume_picks_its_slice_by_r_and_wraps_or_clamps_it() { + let (t, d) = volume(); + assert_eq!(sample3(&t, &d, 0.25, 0.25, 0.25, 0.0), [255, 0, 0, 255], "r in the first half: slice 0"); + assert_eq!(sample3(&t, &d, 0.25, 0.25, 0.75, 0.0), [0, 0, 255, 255], "slice 1"); + assert_eq!(sample3(&t, &d, 0.25, 0.25, 1.25, 0.0), [255, 0, 0, 255], "repeat: 1.25 is 0.25"); + let c = TexDesc { wrap: [Wrap::Clamp; 3], ..d }; + assert_eq!(sample3(&t, &c, 0.25, 0.25, 1.25, 0.0), [0, 0, 255, 255], "clamp: the last slice"); + assert_eq!(sample3(&t, &c, 0.25, 0.25, -2.0, 0.0), [255, 0, 0, 255], "clamp: the first"); + } + + #[test] + fn trilinear_blends_the_two_slices_around_r() { + let (t, d) = volume(); + let l = TexDesc { mag: Filter::Linear, ..d }; + assert_eq!(sample3(&t, &l, 0.25, 0.25, 0.5, 0.0), [128, 0, 128, 255], "halfway between the slice centres"); + assert_eq!(sample3(&t, &l, 0.25, 0.25, 0.25, 0.0), [255, 0, 0, 255], "at a slice centre, that slice"); + // Repeating r blends the last slice into the first past the end. + assert_eq!(sample3(&t, &l, 0.25, 0.25, 1.0, 0.0), [128, 0, 128, 255], "wrapped between slice 1 and slice 0"); + let c = TexDesc { wrap: [Wrap::Clamp; 3], ..l }; + assert_eq!(sample3(&t, &c, 0.25, 0.25, 1.0, 0.0), [0, 0, 255, 255], "clamped past the end: the last slice"); + } + + #[test] + fn a_volume_mip_chain_halves_every_axis() { + let (t, mut d) = volume(); + t.set(0, 8, 0xff00_ff00); + d.count = 2; + d.levels[1] = TexLevel { off: 8, pitch: 1, w: 1, h: 1, d: 1, slice: 1, ..Default::default() }; + d.min = Filter::Mip { within_linear: false, between_linear: false }; + assert_eq!(sample3(&t, &d, 0.25, 0.25, 0.75, 0.4), [0, 0, 255, 255], "level 0"); + assert_eq!(sample3(&t, &d, 0.25, 0.25, 0.75, 0.6), [0, 255, 0, 255], "level 1: one texel whatever r is"); + d.min = Filter::Mip { within_linear: true, between_linear: true }; + assert_eq!(sample3(&t, &d, 0.25, 0.25, 0.75, 0.5), [0, 128, 128, 255], "halfway between the levels, level 0 at r = 0.75 is the slice 1 centre, blue"); + } + + /// Four codes to two words: a signed mantissa shifted left two and an exponent. Value = mantissa x 2^exponent / 128. + #[test] + fn a_spline_table_decodes_to_sixty_four_samples() { + let mut w = [0u32; 33]; + // Codes: 64 << 2 | 0 (0.5), 0xc0 << 2 | 1 (-64 x 2 / 128 = -1), 1 << 2 | 3 (8/128), 127 << 2 | 0 (127/128). + let codes = [64u32 << 2, 0xc0 << 2 | 1, 1 << 2 | 3, 127 << 2]; + w[0] = codes[0] | codes[1] << 10 | codes[2] << 20 | (codes[3] & 3) << 30; + w[1] = codes[3] >> 2; + let v = spline_samples(&w).unwrap(); + assert_eq!(&v[..5], &[0.5, -1.0, 0.0625, 127.0 / 128.0, 0.0]); + w[1] |= 0x100; + assert!(spline_samples(&w).is_none(), "the second word of a pair is 8 bits"); + } + + /// Four stored hardware test vectors decode to the default curve: the doubled 2.0 below + /// x = -4 is code 0x102, the knee 0x1d5, x = -2 is 0x101, x = -1 is 0x100 and the last sample 0x004. + #[test] + fn the_hardware_vectors_decode_to_the_default_curve() { + let at = |c: u32| { + let mut w = [0u32; 33]; + w[0] = c; + spline_samples(&w).unwrap()[0] + }; + assert_eq!([at(0x102), at(0x101), at(0x100), at(0x004)], [2.0, 1.0, 0.5, 1.0 / 128.0]); + assert_eq!(at(0x1d5), 117.0 * 2.0 / 128.0); + assert_eq!(at(0x1ff), 127.0 * 8.0 / 128.0, "the top of the range, 7.9375"); + assert_eq!(at(0x201), -2.0, "-2.0 codes as 0x201"); + } + + /// The default curve is 1 at the finest and falls to 0 at a level of detail of zero. + #[test] + fn the_default_curve_runs_from_one_to_zero() { + let w = default_weights(); + assert_eq!((w[0], w[63] < 0.01), (1.0, true)); + assert!((w[16] - 0.5).abs() < 1e-6, "node 16 is lod -2"); + } + + /// A 2 x 2 base of mid grey and an 8-wide detail of alternating black and white: a magnified fragment moves off + /// the grey by the weight in the direction of its detail texel (add) or by that fraction of itself (modulate), a + /// minified one does not, and the channel field restricts it. + #[test] + fn a_detail_combine_adds_or_modulates_on_magnification_only() { + let t = TextureStore::default(); + t.clear(0, 0, 4 + 8); + for i in 0..4 { + t.set(0, i, 0xff80_8080); + } + for i in 0..8 { + t.set(0, 4 + i, if i % 2 == 0 { 0xff00_0000 } else { 0xffff_ffff }); + } + let weights = [0.25f32; CURVE_NODES]; + let mut d = TexDesc { count: 1, class: 3, mag: Filter::Linear, wrap: [Wrap::Clamp; 3], ..Default::default() }; + d.levels[0] = TexLevel { off: 0, pitch: 2, w: 2, h: 2, ..Default::default() }; + let image = TexLevel { off: 4, pitch: 8, w: 8, h: 1, ..Default::default() }; + let det = |modulate, channels| Combine::Detail(Detail { modulate, scale: 0, channels, image, weights }); + // The detail coordinate is the base's times 2/8: s = 0.25 is the centre of detail texel 0 (black), 0.75 of texel 1 (white). + d.combine = det(false, 0); + assert_eq!(sample_enhanced(&t, &d, 0.25, 0.5, -1.0)[..3], [64, 64, 64], "0x80 less a weight of a quarter"); + assert_eq!(sample_enhanced(&t, &d, 0.75, 0.5, -1.0)[..3], [192, 192, 192], "white detail: plus a quarter"); + assert_eq!(sample_enhanced(&t, &d, 0.25, 0.5, 1.0), sample(&t, &d, 0.25, 0.5, 1.0), "minified: the base alone"); + d.combine = det(true, 0); + assert_eq!(sample_enhanced(&t, &d, 0.25, 0.5, -1.0)[..3], [96, 96, 96], "modulate: 0x80 x (1 - 1/4)"); + assert_eq!(sample_enhanced(&t, &d, 0.75, 0.5, -1.0)[..3], [160, 160, 160], "0x80 x (1 + 1/4)"); + d.combine = det(false, 2); + assert_eq!(sample_enhanced(&t, &d, 0.25, 0.5, -1.0)[..3], [128, 128, 128], "alpha only: the colour is the base's"); + d.combine = det(false, 1); + let c = sample_enhanced(&t, &d, 0.25, 0.5, -1.0); + assert_eq!((c[..3].to_vec(), c[3]), (vec![64, 64, 64], 255), "colour only; alpha is the base's"); + // The curve is read at 64 x 2^lod, so lod -1 is node 32, and a zero node is the base. + let mut w = [0.0f32; CURVE_NODES]; + w[32] = 0.125; + d.combine = Combine::Detail(Detail { modulate: false, scale: 0, channels: 0, image, weights: w }); + assert_eq!(sample_enhanced(&t, &d, 0.25, 0.5, -1.0)[..3], [96, 96, 96], "node 32 at lod -1: an eighth"); + assert_eq!(sample_enhanced(&t, &d, 0.25, 0.5, -2.0)[..3], [128, 128, 128], "node 16: zero"); + // A scale of one repeats the detail twice as often: the detail coordinate is 2/8 x 2 x s, so s = 0.125 is texel 0's centre. + d.combine = Combine::Detail(Detail { modulate: false, scale: 1, channels: 0, image, weights }); + assert_eq!(sample_enhanced(&t, &d, 0.125, 0.5, -1.0)[..3], [64, 64, 64]); + } + + /// A sharpen combine pushes the magnified base away from the next coarser level, and does nothing with one level. + #[test] + fn a_sharpen_combine_pushes_away_from_the_coarser_level() { + let t = TextureStore::default(); + t.clear(0, 0, 5); + for i in 0..4 { + t.set(0, i, 0xff80_8080); + } + t.set(0, 4, 0xff40_4040); + let mut d = TexDesc { count: 2, class: 3, mag: Filter::Linear, wrap: [Wrap::Clamp; 3], combine: Combine::Sharpen(Sharpen { channels: 0, weights: [0.5; CURVE_NODES] }), ..Default::default() }; + d.levels[0] = TexLevel { off: 0, pitch: 2, w: 2, h: 2, ..Default::default() }; + d.levels[1] = TexLevel { off: 4, pitch: 1, w: 1, h: 1, ..Default::default() }; + let c = sample_enhanced(&t, &d, 0.25, 0.25, -1.0); + assert_eq!(c[..3], [160, 160, 160], "0x80 + 1/2 (0x80 - 0x40)"); + d.count = 1; + assert_eq!(sample_enhanced(&t, &d, 0.25, 0.25, -1.0)[..3], [128, 128, 128], "no coarser level: the base"); + } + + fn env(mode: EnvMode, class: u8, f: [u32; 4], t: [u32; 4]) -> [u32; 4] { + environment(&TexDesc { env: mode, class, env_colour: [FULL, 0, 0, FULL], ..Default::default() }, f, t) + } + + #[test] + fn the_environment_combines_texel_and_fragment_by_mode_and_class() { + let half = [4096, 4096, 4096, 4096]; + let white = [255, 255, 255, 255]; + assert_eq!(env(EnvMode::Modulate, 3, half, white), half, "modulate by white leaves the fragment"); + assert_eq!(env(EnvMode::Modulate, 3, [FULL; 4], [0, 255, 0, 128])[..3], [0, FULL, 0]); + assert_eq!(env(EnvMode::Replace, 2, half, [255, 0, 0, 7]), [FULL, 0, 0, 4096], "RGB replace keeps the fragment's alpha"); + assert_eq!(env(EnvMode::Replace, 3, half, [255, 0, 0, 255]), [FULL, 0, 0, FULL]); + assert_eq!(env(EnvMode::Decal, 3, [0, FULL, 0, FULL], [255, 0, 0, 255]), [FULL, 0, 0, FULL], "opaque RGBA texel decals over the fragment"); + assert_eq!(env(EnvMode::Decal, 3, [0, FULL, 0, FULL], [255, 0, 0, 0]), [0, FULL, 0, FULL], "a transparent texel leaves it"); + assert_eq!(env(EnvMode::Decal, 1, half, white), env(EnvMode::Modulate, 1, half, white), "decal of an alpha texture modulates"); + assert_eq!(env(EnvMode::Blend, 2, [0, 0, 0, FULL], [255, 255, 255, 255])[..3], [FULL, 0, 0], "a white texel takes the environment colour"); + assert_eq!(env(EnvMode::Modulate, 1, [FULL, 0, 0, FULL], [0, 0, 0, 128])[3], mul(FULL, unpack_colour(128 << 24)[3]), "alpha texture scales alpha only"); + let i = unpack_colour(0xff80_8080)[0]; + assert_eq!(env(EnvMode::Modulate, 4, [FULL; 4], [128, 128, 128, 255]), [i; 4], "intensity scales colour and alpha"); + } +} diff --git a/src/dev/kona/back/replay.rs b/src/dev/kona/back/replay.rs new file mode 100644 index 000000000..cf055f10a --- /dev/null +++ b/src/dev/kona/back/replay.rs @@ -0,0 +1,1699 @@ +//! Replay of a recorded command stream into a board. +//! +//! A recording is what a guest sent a pipe and what the pipe answered, made by +//! another model of the same board (the file is `KONAREC`, a NUL, a `u32` +//! version, then tagged little-endian records; QEMU's KONA manual page, +//! "Recording the stream", defines it). Replaying it into a board with +//! A null back end checks the front end and the file format before anything +//! draws: the board is built from the `C` record, each `W` is a bus store and +//! each `R` a bus load, in order, retrying while the bus is busy; the board's +//! reads of guest memory are served from the `D` records and its writes are +//! compared with the `O` records. +//! +//! The recorder is another model, not a hardware measurement, so a match +//! shows the front end behaves as that model does and nothing more. What a +//! back end adds (pixel reads, query replies, image DMA, timing) a front end +//! on a null back end cannot reproduce; the report lists those apart. +//! +//! `KONA_SHOT_DIR`, when set, keeps the frame the desktop test composes there as +//! `irix-desktop-ours.png`, to look at beside the capture. +//! +//! The recordings are not in the repository. `KONA_RECORDINGS` names the +//! directory that holds them; without it, or when it is not readable (as in a +//! build container without the storage), the tests that need them skip, with a +//! line on stderr that says so. + +use crate::dev::kona::tests::{ScratchDir, TestHost}; +use super::super::defs::{ACTIVITY, CAUSE_VIDEO, CREDIT, INT_CAUSE, PROBE}; +use super::super::ident::{KonaConfig, Product}; +use super::super::Kona; +use super::census::Census; +use crate::traits::{Resettable, BUS_BUSY, BUS_OK}; +use parking_lot::Mutex; +use std::collections::BTreeMap; +use std::fmt::Write; +use std::io::Read; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; +use std::sync::Arc; + +/// One record of a recording, after the board's `C` record. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum Rec { + Store { size: u8, off: u32, val: u64 }, + Load { size: u8, off: u32, val: u64 }, + /// Guest memory the pipe read. + DmaRead { src: u8, addr: u64, bytes: Vec }, + /// Guest memory the pipe wrote. + DmaWrite { src: u8, addr: u64, bytes: Vec }, + Interrupt { kind: u8, vector: u8, delivered: u8 }, + Marker { kind: u32, arg: u32 }, +} + +/// The board a recording was made on. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) struct Board { + pub part: u32, + pub revision: u32, + pub rm_count: u32, + pub texture_mb: u32, + pub product: String, +} + +#[derive(Debug)] +pub(super) struct Recording { + pub board: Board, + pub records: Vec, + /// The record count the end record gave: None for a file cut off. + pub end: Option, +} + +/// One thing in a recording: the board, a record, or the end with its count. +pub(super) enum Item { + Board(Board), + Rec(Rec), + End(u64), +} + +/// A recording read record by record, so a file too big for memory can be +/// replayed. +pub(super) struct Stream { + r: R, + /// Bytes read so far, for messages. + at: u64, + seen_board: bool, + ended: bool, +} + +impl Stream { + pub fn new(mut r: R) -> Result, String> { + let mut head = [0u8; 12]; + r.read_exact(&mut head).map_err(|_| "not a KONAREC file".to_string())?; + if &head[..8] != b"KONAREC\0" { + return Err("not a KONAREC file".into()); + } + let version = u32::from_le_bytes(head[8..12].try_into().unwrap()); + if version != 1 { + return Err(format!("format version {version}, not 1")); + } + Ok(Stream { r, at: 12, seen_board: false, ended: false }) + } + + fn fill(&mut self, buf: &mut [u8]) -> Result<(), String> { + self.r.read_exact(buf).map_err(|_| format!("record cut off at byte {}", self.at))?; + self.at += buf.len() as u64; + Ok(()) + } + + fn u8(&mut self) -> Result { + let mut b = [0u8; 1]; + self.fill(&mut b)?; + Ok(b[0]) + } + + fn u32(&mut self) -> Result { + let mut b = [0u8; 4]; + self.fill(&mut b)?; + Ok(u32::from_le_bytes(b)) + } + + fn u64(&mut self) -> Result { + let mut b = [0u8; 8]; + self.fill(&mut b)?; + Ok(u64::from_le_bytes(b)) + } + + fn bytes(&mut self, n: usize) -> Result, String> { + let mut v = vec![0u8; n]; + self.fill(&mut v)?; + Ok(v) + } + + /// The next item; None at the end of the file between records. + pub fn next(&mut self) -> Result, String> { + let tag_at = self.at; + let mut t = [0u8; 1]; + let n = self.r.read(&mut t).map_err(|e| e.to_string())?; + if n == 0 { + return Ok(None); + } + self.at += 1; + if self.ended { + return Err(format!("data after the end record, at byte {tag_at}")); + } + let tag = t[0]; + if tag != b'C' && !self.seen_board { + return Err("the board record is not first".into()); + } + Ok(Some(match tag { + b'C' => { + let (part, revision, rm_count, texture_mb) = (self.u32()?, self.u32()?, self.u32()?, self.u32()?); + let n = self.u8()? as usize; + let product = String::from_utf8_lossy(&self.bytes(n)?).into_owned(); + if std::mem::replace(&mut self.seen_board, true) { + return Err(format!("second board record at byte {tag_at}")); + } + Item::Board(Board { part, revision, rm_count, texture_mb, product }) + } + b'W' | b'R' => { + let (size, off, val) = (self.u8()?, self.u32()?, self.u64()?); + Item::Rec(if tag == b'W' { Rec::Store { size, off, val } } else { Rec::Load { size, off, val } }) + } + b'D' | b'O' => { + let (src, addr, n) = (self.u8()?, self.u64()?, self.u32()? as usize); + let bytes = self.bytes(n)?; + Item::Rec(if tag == b'D' { Rec::DmaRead { src, addr, bytes } } else { Rec::DmaWrite { src, addr, bytes } }) + } + b'I' => Item::Rec(Rec::Interrupt { kind: self.u8()?, vector: self.u8()?, delivered: self.u8()? }), + b'M' => Item::Rec(Rec::Marker { kind: self.u32()?, arg: self.u32()? }), + b'E' => { + self.ended = true; + Item::End(self.u64()?) + } + _ => return Err(format!("unknown record tag {tag:#04x} at byte {tag_at}")), + })) + } +} + +impl Recording { + /// Parse a file. A file with no end record parses, with `end` None and + /// whole records only: the recorder flushes at record boundaries, but the + /// last record of a killed run may be partial and is an error. + pub fn parse(bytes: &[u8]) -> Result { + let mut stream = Stream::new(bytes)?; + let mut board = None; + let mut records = Vec::new(); + let mut end = None; + while let Some(item) = stream.next()? { + match item { + Item::Board(b) => board = Some(b), + Item::Rec(r) => records.push(r), + Item::End(n) => end = Some(n), + } + } + let board = board.ok_or("no board record")?; + if let Some(n) = end { + // The count is of the records before the end, the board's included. + if n != records.len() as u64 + 1 { + return Err(format!("end record counts {n} records, the file has {}", records.len() + 1)); + } + } + Ok(Recording { board, records, end }) + } + + /// The `KonaConfig` of the recorded board: the product its record names, + /// with the texture size that product has. + pub fn config(&self) -> Result { + let product = Product::from_name(&self.board.product).map_err(|e| format!("recorded {e}"))?; + let want = product.spec().texture_mb(); + if self.board.texture_mb != want { + return Err(format!("product {:?} has {want} MB of texture memory, the recording says {}", self.board.product, self.board.texture_mb)); + } + KonaConfig::new(product, self.board.rm_count) + } +} + +/// The directory the recordings are in, if the environment names one that +/// exists. Prints why a test is skipping when not. +pub(super) fn recordings_dir() -> Option { + match std::env::var_os("KONA_RECORDINGS") { + Some(d) if std::path::Path::new(&d).is_dir() => Some(d.into()), + Some(d) => { + skip_note(&format!("KONA_RECORDINGS={d:?} is not a readable directory here")); + None + } + None => { + skip_note("KONA_RECORDINGS is not set"); + None + } + } +} + +/// One line saying a replay test is skipping, written to stderr directly so the +/// test harness's capture does not hide it. A build container that cannot see +/// the recordings' storage skips this way: run the built test binary on the +/// host with `KONA_RECORDINGS` set to run them. +fn skip_note(why: &str) { + use std::io::Write; + let _ = writeln!(std::io::stderr(), "kona replay test skipped: {why}; the recordings are not readable from here (run the test binary on a host that has them)"); +} + +/// Builds the machine side of a replay: a test host that serves the board's reads +/// from the recording's `D` records and keeps its writes with the record index. +fn replay_host(rec: &Recording, at: Arc) -> Arc { + let reads = rec.records.iter().filter_map(|r| match r { + Rec::DmaRead { src, addr, bytes } => Some((*src, *addr, bytes.clone())), + _ => None, + }); + TestHost::replaying(reads.collect(), at) +} + +/// A load whose answer depends on timing the replay does not reproduce. +/// Returns the mask of the bits that are compared: zero skips the load. +fn load_mask(off: u32) -> u64 { + match off { + // The FIFO's fill level and the watchdog latch follow the thread's + // pace, which a replay without the thread does not have. + CREDIT | ACTIVITY => 0, + // The video bit rises at a retrace, which the recorder times against + // its own clock. + INT_CAUSE => !(CAUSE_VIDEO as u64), + _ => !0, + } +} + +/// What a replay found. +#[derive(Default, Debug)] +pub(super) struct Report { + pub stores: u64, + pub loads: u64, + pub loads_compared: u64, + /// Loads that differed: (offset, expected, got) to (count, record index + /// of the first). + pub load_mismatches: BTreeMap<(u32, u64, u64), (u64, usize)>, + pub resets: u64, + /// The board's failure, if it stopped taking commands. + pub failure: Option, + /// Recorded interrupts of the geometry-engine kind, and those the board sent. + pub ge_expected: Vec, + pub ge_sent: Vec, + pub general_expected: u64, + pub general_sent: u64, + /// Retrace completions the recording marked, and the tags the board's + /// back end was asked for. + pub retrace_expected: Vec, + pub retrace_asked: Vec, + /// Swap markers: only a back end that draws can produce them. + pub swap_markers: u64, + pub reads_matched: u64, + pub reads_skipped_over: u64, + pub reads_unmatched: u64, + pub reads_unmatched_at: Vec<(u64, usize)>, + /// Recorded reads no read of the board used, by source. + pub reads_unused: BTreeMap, + /// Bytes the board wrote that the recording has the same (run, bytes) for, + /// bytes it wrote that the recording does not have, and recorded bytes it + /// never wrote, each by source of the recorded write where known. + pub writes_same: u64, + pub writes_extra: Vec<(u64, usize)>, + pub writes_extra_bytes: u64, + pub writes_missing: Vec<(u8, u64, usize)>, + pub writes_missing_bytes: u64, + pub writes_missing_by_src: BTreeMap, + /// Writes in order: board runs found in the recorded runs in order. + pub runs_in_order: u64, + pub runs_board: u64, + /// The video Gets' replies the recording shows (a reply id, the data its size takes and the + /// sum), and how many of them the board wrote, in order; the recorded ones it did not. + pub video_replies: (u64, u64), + pub video_replies_lost: Vec<(u64, Vec)>, + /// The first record index a load differed at. + pub first_mismatch: Option, + /// The record index of the 64th dropped word in a row (the run of data the + /// front end could not size began a little before), where a replay into a null back end stops being exact (the back + /// end answers `Reply::Data` for those; a null one does not). + pub exact_until: usize, + /// Records in the recording, after the board's. + pub records: usize, + /// Before that index: runs of bytes the board wrote that the recording's + /// writes also hold, in order, out of all the board wrote; how many runs + /// the recording has (it has more: what only a back end writes); and the + /// first board runs it lacks. + pub exact_runs: (u64, u64), + pub exact_runs_recorded: u64, + pub exact_runs_lost: Vec<(u64, usize)>, +} + +/// Merge consecutive writes that touch end to start into runs. +fn runs(writes: impl IntoIterator)>) -> Vec<(u64, Vec)> { + let mut out: Vec<(u64, Vec)> = Vec::new(); + for (a, d) in writes { + match out.last_mut() { + Some((la, ld)) if *la + ld.len() as u64 == a => ld.extend_from_slice(&d), + _ => out.push((a, d)), + } + } + out +} + +/// Whether a run of written bytes is a video Get's reply: a reply id below 32, the data its size +/// in the server's table takes, and the id plus the data as the last word. +fn is_video_reply(d: &[u8]) -> bool { + let word = |i: usize| u32::from_be_bytes(d[4 * i..4 * i + 4].try_into().unwrap()); + if d.len() < 12 || d.len() % 4 != 0 { + return false; + } + let id = word(0) as usize; + let size = crate::dev::kona::tables::VIDEO_REPLY.iter().flatten().find(|(i, _)| *i as usize == id).map(|(_, b)| *b as usize); + let last = d.len() / 4 - 1; + size == Some(d.len() - 8) && word(last) == (0..last).fold(0u32, |s, i| s.wrapping_add(word(i))) +} + +/// What a replay's board has behind its census tap: a back end that answers every +/// job as nothing (retrace requests complete at once; DMA rows are unclaimed), or the real one. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum Behind { + Null, + Real, +} + +impl Behind { + /// Put the census tap on `kona`'s back end, and the null double in place of its work if asked. + fn install(self, kona: &Kona, census: &Arc>, at: &Arc) { + kona.with_backend(|b| { + b.tap = Some((census.clone(), at.clone())); + if self == Behind::Null { + b.hook = Some(super::null_hook()); + } + }); + } +} + +/// Replay `rec` into a board with a null back end, through the census tap. +pub(super) fn replay(rec: &Recording) -> Result<(Report, Census), String> { + replay_into(rec, Behind::Null).map(|(rep, census, _)| (rep, census)) +} + +/// As `replay`, with `behind` it, under the census tap; the board is returned so +/// a test can look at what the back end made. +pub(super) fn replay_into(rec: &Recording, behind: Behind) -> Result<(Report, Census, Arc), String> { + let cfg = rec.config()?; + let at = Arc::new(AtomicUsize::new(0)); + let host = replay_host(rec, at.clone()); + let census = Arc::new(Mutex::new(Census::default())); + // The probe register's value is the board's configuration, which the + // recording leaves out; its first answer is the value the recorder's + // board had. + let probe = rec.records.iter().find_map(|r| match r { + Rec::Load { off, val, .. } if *off == PROBE => Some(*val as u32), + _ => None, + }); + let kona: Arc = Kona::new(KonaConfig { probe: probe.unwrap_or(0), ..cfg }, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None)?; + behind.install(&kona, &census, &at); + let mut rep = Report::default(); + let busy_limit = 50_000_000u32; + for (index, r) in rec.records.iter().enumerate() { + at.store(index, Relaxed); + match *r { + Rec::Store { size, off, val } => { + rep.stores += 1; + let mut n = 0; + while kona.bus_write(off as u64, size as u32, val) == BUS_BUSY { + n += 1; + if n > busy_limit { + return Err(format!("store at {off:#x} stayed busy")); + } + kona.run_stores(); + std::thread::yield_now(); + } + kona.run_stores(); + } + Rec::Load { size, off, val } => { + rep.loads += 1; + let mut n = 0; + let got = loop { + let (status, data) = kona.bus_read(off as u64, size as u32); + if status == BUS_OK { + break data; + } + n += 1; + if status != BUS_BUSY || n > busy_limit { + return Err(format!("load at {off:#x} answered {status:#x}")); + } + std::thread::yield_now(); + }; + let mask = load_mask(off); + if mask != 0 { + rep.loads_compared += 1; + if got & mask != val & mask { + rep.load_mismatches.entry((off, val & mask, got & mask)).or_insert((0, index)).0 += 1; + rep.first_mismatch.get_or_insert(index); + } + } + } + Rec::Marker { kind: 1, .. } => { + rep.resets += 1; + kona.power_on(); + } + Rec::Marker { kind: 2, .. } => rep.swap_markers += 1, + Rec::Marker { kind: 3, arg } => rep.retrace_expected.push(arg), + Rec::Marker { .. } | Rec::DmaRead { .. } | Rec::DmaWrite { .. } => {} + Rec::Interrupt { kind: 1, vector, .. } => rep.ge_expected.push(vector), + Rec::Interrupt { .. } => rep.general_expected += 1, + } + } + rep.failure = kona.failure(); + + // Interrupts. The recording names a kind and a vector; a packet carries + // only the vector, so the geometry-engine vector is told from the + // general one by the vectors the recording used for each. + let general: Vec = rec + .records + .iter() + .filter_map(|r| if let Rec::Interrupt { kind: 0, vector, .. } = r { Some(*vector) } else { None }) + .collect(); + for vector in host.sent().iter().filter(|p| p.level).map(|p| p.vector) { + if rep.ge_expected.contains(&vector) && !general.contains(&vector) { + rep.ge_sent.push(vector); + } else { + rep.general_sent += 1; + } + } + rep.retrace_asked = census.lock().retraces.clone(); + + // Reads. + { + let r = host.recorded().reads.lock(); + rep.reads_matched = r.matched; + rep.reads_skipped_over = r.skipped_over; + rep.reads_unmatched = r.unmatched; + rep.reads_unmatched_at = r.unmatched_at.clone(); + for rec in r.recs.iter().filter(|x| x.served == 0) { + let e = rep.reads_unused.entry(rec.src).or_default(); + e.0 += 1; + e.1 += rec.bytes.len() as u64; + } + } + + // Writes: as byte maps, so chunking does not matter, and as runs, so + // order does. + let recorded: Vec<(u8, u64, Vec)> = rec + .records + .iter() + .filter_map(|r| if let Rec::DmaWrite { src, addr, bytes } = r { Some((*src, *addr, bytes.clone())) } else { None }) + .collect(); + let board_all = host.recorded().writes.lock().clone(); + let board: Vec<(u64, Vec)> = board_all.iter().map(|(_, a, d)| (*a, d.clone())).collect(); + let mut rec_final: BTreeMap = BTreeMap::new(); + for (src, a, d) in &recorded { + for (i, b) in d.iter().enumerate() { + rec_final.insert(a + i as u64, (*b, *src)); + } + } + let mut board_final: BTreeMap = BTreeMap::new(); + for (a, d) in &board { + for (i, b) in d.iter().enumerate() { + board_final.insert(a + i as u64, *b); + } + } + let mut extra: Vec = Vec::new(); + for (a, b) in &board_final { + match rec_final.get(a) { + Some((rb, _)) if rb == b => rep.writes_same += 1, + _ => extra.push(*a), + } + } + rep.writes_extra_bytes = extra.len() as u64; + for run in extra.chunk_by(|a, b| *b == *a + 1) { + if rep.writes_extra.len() < 8 { + rep.writes_extra.push((run[0], run.len())); + } + } + let mut missing: Vec<(u64, u8)> = Vec::new(); + for (a, (b, src)) in &rec_final { + if board_final.get(a) != Some(b) { + missing.push((*a, *src)); + } + } + rep.writes_missing_bytes = missing.len() as u64; + for run in missing.chunk_by(|a, b| b.0 == a.0 + 1 && b.1 == a.1) { + let e = rep.writes_missing_by_src.entry(run[0].1).or_default(); + e.0 += 1; + e.1 += run.len() as u64; + if rep.writes_missing.len() < 8 { + rep.writes_missing.push((run[0].1, run[0].0, run.len())); + } + } + let rec_runs = runs(recorded.iter().map(|(_, a, d)| (*a, d.clone()))); + let board_runs = runs(board); + let mut at = 0; + for run in &board_runs { + rep.runs_board += 1; + if let Some(i) = rec_runs[at..].iter().position(|x| x == run) { + at += i + 1; + rep.runs_in_order += 1; + } + } + + let mut at = 0; + for run in rec_runs.iter().filter(|(_, d)| is_video_reply(d)) { + rep.video_replies.0 += 1; + match board_runs[at..].iter().position(|x| x == run) { + Some(i) => { + at += i + 1; + rep.video_replies.1 += 1; + } + None => rep.video_replies_lost.push(run.clone()), + } + } + + let census = std::mem::take(&mut *census.lock()); + rep.records = rec.records.len(); + rep.exact_until = census.first_long_drop.unwrap_or(rep.records); + let before = |at: usize| at < rep.exact_until; + let exact_board = runs(board_all.iter().filter(|(at, ..)| before(*at)).map(|(_, a, d)| (*a, d.clone()))); + let exact_rec = runs(rec.records.iter().enumerate().filter(|(at, _)| before(*at)).filter_map(|(_, r)| match r { + Rec::DmaWrite { addr, bytes, .. } => Some((*addr, bytes.clone())), + _ => None, + })); + let mut at = 0; + for run in &exact_board { + rep.exact_runs.1 += 1; + match exact_rec[at..].iter().position(|x| x == run) { + Some(i) => { + at += i + 1; + rep.exact_runs.0 += 1; + } + None if rep.exact_runs_lost.len() < 8 => rep.exact_runs_lost.push((run.0, run.1.len())), + None => {} + } + } + rep.exact_runs_recorded = exact_rec.len() as u64; + Ok((rep, census, kona)) +} + +impl Report { + pub fn render(&self) -> String { + let mut o = String::new(); + let _ = writeln!(o, "stores {} loads {} ({} compared) resets {}", self.stores, self.loads, self.loads_compared, self.resets); + let _ = writeln!(o, "board failure: {:?}", self.failure); + if self.exact_until >= self.records { + let _ = writeln!(o, "exact over the whole stream; first load mismatch at {:?}", self.first_mismatch); + } else { + let _ = writeln!( + o, + "exact until record {}, where the {}th dropped word in a row came (a data run the front end could not size, begun a little before); first load mismatch at {:?}", + self.exact_until, + super::census::LONG_DROP, + self.first_mismatch + ); + } + let _ = writeln!( + o, + "writes before that: {} of the board's {} runs are in the recording's {} in order; not found: {:x?}", + self.exact_runs.0, self.exact_runs.1, self.exact_runs_recorded, self.exact_runs_lost + ); + let _ = writeln!(o, "load mismatches: {}", self.load_mismatches.len()); + for ((off, want, got), (n, first)) in &self.load_mismatches { + let _ = writeln!(o, " {off:#08x} wanted {want:#x}, got {got:#x}: {n} times, first at record {first}"); + } + let _ = writeln!( + o, + "geometry-engine interrupts: recorded {:?}, sent {:?}; general: recorded {}, sent {}", + self.ge_expected, self.ge_sent, self.general_expected, self.general_sent + ); + let _ = writeln!(o, "retraces: recorded {:?}, asked of the back end {:?}; swap markers {}", self.retrace_expected, self.retrace_asked, self.swap_markers); + let _ = writeln!( + o, + "board reads: {} served from records ({} skipped over earlier ones), {} bytes unmatched {:?}", + self.reads_matched, self.reads_skipped_over, self.reads_unmatched, self.reads_unmatched_at + ); + let _ = writeln!(o, "recorded reads never used, by source: {:?}", self.reads_unused); + let _ = writeln!( + o, + "board writes: {} bytes equal to the recording, {} bytes not in it {:?}, {} recorded bytes the board did not write {:?}", + self.writes_same, self.writes_extra_bytes, self.writes_extra, self.writes_missing_bytes, self.writes_missing + ); + let _ = writeln!(o, "recorded bytes not written, by source (runs, bytes): {:?}", self.writes_missing_by_src); + let _ = writeln!(o, "board write runs found in the recorded runs, in order: {} of {}", self.runs_in_order, self.runs_board); + let _ = writeln!(o, "video Get replies the recording shows, written by the board in order: {} of {}; lost: {:x?}", self.video_replies.1, self.video_replies.0, self.video_replies_lost); + o + } +} + +/// Replay a recording into a board whose back end is a `KonaBackend`, loads +/// and stores only, and return the monitor's `status` and `hash` at the end, +/// after `limit` records or all of them. The recording's resets are performed +/// as in `replay`. +pub(super) fn replay_drawing(rec: &Recording, limit: Option) -> Result<(String, String), String> { + let kona = replay_board(rec, limit)?; + Ok((monitor(&kona, &["status"])?, monitor(&kona, &["hash"])?.trim().to_string())) +} + +/// A monitor command of a board's back end, as text. +pub(super) fn monitor(kona: &Kona, args: &[&str]) -> Result { + let mut v = Vec::new(); + kona.with_backend(|b| b.monitor(args, &mut v)).map_err(|e| e.to_string())?; + String::from_utf8(v).map_err(|e| e.to_string()) +} + +/// A board with a `KonaBackend` after `limit` records (all of them if None) of +/// `rec`, loads and stores only. +pub(super) fn replay_board(rec: &Recording, limit: Option) -> Result, String> { + replay_board_with_display(rec, limit).map(|(kona, _)| kona) +} + +/// As `replay_board`, with the window the board shows the guest. +pub(super) fn replay_board_with_display(rec: &Recording, limit: Option) -> Result<(Arc, Arc), String> { + let kona = new_board(rec)?; + let display = kona.with_backend(|b| b.display()); + play(&kona, rec.records.iter().take(limit.unwrap_or(usize::MAX))); + Ok((kona, display)) +} + +/// A board with a `KonaBackend` as the recording's recorder had it, with the recording's +/// reads to serve and no record played. +pub(super) fn new_board(rec: &Recording) -> Result, String> { + let cfg = rec.config()?; + let at = Arc::new(AtomicUsize::new(0)); + let host = replay_host(rec, at); + let probe = rec.records.iter().find_map(|r| match r { + Rec::Load { off, val, .. } if *off == PROBE => Some(*val as u32), + _ => None, + }); + Kona::new(KonaConfig { probe: probe.unwrap_or(0), ..cfg }, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None) +} + +/// Play loads, stores and resets of a recording into `kona`. +pub(super) fn play<'a>(kona: &Kona, records: impl Iterator) { + for r in records { + match *r { + Rec::Store { size, off, val } => { + while kona.bus_write(off as u64, size as u32, val) == BUS_BUSY { + std::thread::yield_now(); + } + kona.run_stores(); + } + Rec::Load { size, off, .. } => { + while kona.bus_read(off as u64, size as u32).0 == BUS_BUSY { + std::thread::yield_now(); + } + } + Rec::Marker { kind: 1, .. } => kona.power_on(), + _ => {} + } + } +} + +/// The GL demo recording played into a second board loaded from a snapshot of the first at each of `splits` +/// (record numbers; the snapshot is taken at the first record from there on where the stream is between +/// commands) and driven from then on with it. At each of `checks` (record numbers; the first record from +/// there on, once every snapshot is taken, where the stream is between commands) every second board is compared with the first: the chunks +/// it saves (the store, the texture arena and the small state) and its hash. Returns the record each snapshot +/// was taken at, and for each check the record it was made at and the boards that differed. +pub(super) fn gl_resume(path: &std::path::Path, splits: &[usize], checks: &[usize]) -> Result<(Vec, Vec<(usize, Vec)>), String> { + let open = || -> Result>, String> { + let f = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?; + Stream::new(std::io::BufReader::with_capacity(1 << 20, f)) + }; + let (mut board, mut reads, mut probe) = (None, Vec::new(), None); + let mut stream = open()?; + while let Some(item) = stream.next()? { + match item { + Item::Board(b) => board = Some(b), + Item::Rec(Rec::DmaRead { src, addr, bytes }) => reads.push((src, addr, bytes)), + Item::Rec(Rec::Load { off, val, .. }) if off == PROBE && probe.is_none() => probe = Some(val as u32), + _ => {} + } + } + let cfg = Recording { board: board.ok_or("no board record")?, records: Vec::new(), end: None }.config()?; + let make = || -> Result<(Arc, Arc), String> { + let host = TestHost::replaying(reads.clone(), Arc::new(AtomicUsize::new(0))); + Ok((Kona::new(KonaConfig { probe: probe.unwrap_or(0), ..cfg.clone() }, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None)?, host)) + }; + let (a, a_host) = make()?; + let saves = ScratchDir::new("gl-resume"); + let cs = crate::chunk_store::ChunkStore::new(saves.path()); + let mut seconds: Vec> = Vec::new(); + let (mut taken, mut done) = (Vec::new(), Vec::new()); + let mut stream = open()?; + let mut index = 0usize; + while let Some(item) = stream.next()? { + let Item::Rec(r) = item else { continue }; + index += 1; + play(&a, std::iter::once(&r)); + for b in &seconds { + play(b, std::iter::once(&r)); + } + for (k, &split) in splits.iter().enumerate() { + // A snapshot is refused between the words of a command; the next record that is not. + if index >= split && taken.len() == k { + if let Ok(saved) = a.save_chunks(&cs) { + let (b, b_host) = make()?; + b.load_chunks(&cs, &saved)?; + // The second board's memory reads continue where the first's have got to. + b_host.recorded().continue_from(a_host.recorded()); + taken.push(index); + seconds.push(b); + } + } + } + if let Some(&at) = checks.get(done.len()).filter(|&&at| index >= at && taken.len() == splits.len()) { + if let Ok(ours) = a.save_chunks(&cs) { + let hash = monitor(&a, &["hash"])?; + let mut differ = Vec::new(); + for (k, b) in seconds.iter().enumerate() { + if b.save_chunks(&cs).map_err(|e| e.to_string())? != ours || monitor(b, &["hash"])? != hash { + differ.push(k); + } + } + done.push((index.max(at), differ)); + } + } + } + Ok((taken, done)) +} + +/// What a stretch of a stream between two user marks sent. +pub(super) struct Span { + /// The mark the span starts at (0 for the start of the file). + pub mark: u32, + /// Records in it, swap markers and retrace-completion markers it holds. + pub records: u64, + pub swaps: u64, + pub retraces: u64, + pub census: Census, + /// The GL path's counts for the span (`kona gl`), when a drawing back end ran. + pub gl: String, + /// The store's hash at the swaps `GL_GOLDEN_SWAPS` names in a GL demo's span, + /// and the pixels in the windmill tower's box that are near red: swap, hash, count. + pub frames: Vec<(u64, String, u32)>, +} + +/// The swaps of each GL demo's span at which the replay takes a hash and a shot. +const GL_GOLDEN_SWAPS: [u64; 3] = [51, 301, 901]; + +/// The windmill tower's box in screen pixels (x0, y0, x1, y1), where the capture +/// shows a red tower. +const TOWER_BOX: (u32, u32, u32, u32) = (299, 279, 407, 510); + +/// How many pixels of the box are near red in the frame the display would show. +fn tower_pixels(kona: &Kona) -> u32 { + let (w, frame) = kona.with_backend(|b| (b.store.width() as usize, b.pixels().frame())); + let (x0, y0, x1, y1) = TOWER_BOX; + let mut n = 0; + for y in y0 as usize..y1 as usize { + for x in x0 as usize..x1 as usize { + let p = frame[y * w + x]; + let (r, g, b) = (p & 0xff, p >> 8 & 0xff, p >> 16 & 0xff); + n += (r > 150 && g < 80 && b < 80) as u32; + } + } + n +} + +/// Replay a recording too big to hold in memory into a board with a census +/// tap, cutting a census at every user mark. Loads are not compared (the +/// stream is past the point where a replay into a null back end stays exact); +/// the point is which tokens the stream sends. Reads the file twice: the +/// board's DMA reads first, since they are recorded after the store that +/// causes them. +pub(super) fn census_spans(path: &std::path::Path) -> Result, String> { + census_spans_into(path, Behind::Null) +} + +/// As `census_spans`, with `behind` it, under the census tap; its `gl` monitor +/// text is taken at each mark and cleared. +pub(super) fn census_spans_into(path: &std::path::Path, behind: Behind) -> Result, String> { + census_spans_replies(path, behind, None) +} + +/// The query replies the recording shows the board wrote (the data at byte 8 of the return page, +/// at least 52 bytes) and those the board under replay wrote, in order. +#[derive(Default)] +pub(super) struct Replies { + pub recorded: Vec>, + pub board: Vec>, +} + +/// As `census_spans_into`, also collecting the query replies if asked. +pub(super) fn census_spans_replies(path: &std::path::Path, behind: Behind, mut replies: Option<&mut Replies>) -> Result, String> { + // KONA_SHOT_DIR: keep the composed frame at some swaps of each span. + let shots = std::env::var_os("KONA_SHOT_DIR").map(std::path::PathBuf::from); + let open = || -> Result>, String> { + let f = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?; + Stream::new(std::io::BufReader::with_capacity(1 << 20, f)) + }; + let mut board = None; + let mut reads = Vec::new(); + let mut probe = None; + let mut stream = open()?; + while let Some(item) = stream.next()? { + match item { + Item::Board(b) => board = Some(b), + Item::Rec(Rec::DmaRead { src, addr, bytes }) => reads.push((src, addr, bytes)), + Item::Rec(Rec::DmaWrite { bytes, .. }) if bytes.len() >= 52 => { + if let Some(r) = replies.as_deref_mut() { + r.recorded.push(bytes); + } + } + Item::Rec(Rec::Load { off, val, .. }) if off == PROBE && probe.is_none() => probe = Some(val as u32), + _ => {} + } + } + let board = board.ok_or("no board record")?; + let cfg = Recording { board, records: Vec::new(), end: None }.config()?; + let at = Arc::new(AtomicUsize::new(0)); + let host = TestHost::replaying(reads, at.clone()); + let census = Arc::new(Mutex::new(Census::default())); + let kona = Kona::new(KonaConfig { probe: probe.unwrap_or(0), ..cfg }, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None)?; + behind.install(&kona, &census, &at); + let _ = monitor(&kona, &["gl", "census"]); + let mut spans = Vec::new(); + let mut frames = Vec::new(); + let (mut mark, mut records, mut swaps, mut retraces) = (0u32, 0u64, 0u64, 0u64); + let mut stream = open()?; + let mut index = 0usize; + let progress = std::env::var("KONA_PROGRESS").ok().and_then(|v| v.parse::().ok()).filter(|&n| n > 0); + // KONA_STOP_AT=N ends the replay after N records, to compare two builds at the same point. + let stop_at = std::env::var("KONA_STOP_AT").ok().and_then(|v| v.parse::().ok()); + while let Some(item) = stream.next()? { + let Item::Rec(r) = item else { continue }; + at.store(index, Relaxed); + index += 1; + records += 1; + // KONA_PROGRESS=N prints the texture counts every N records, to see how far a long replay got. + if progress.is_some_and(|n| index % n == 0) { + let line = monitor(&kona, &["gl"]).unwrap_or_default().lines().find(|l| l.starts_with("textures")).map(str::to_string).unwrap_or_default(); + eprintln!("progress: record {index}, mark {mark}, {swaps} swaps: {line}"); + } + if stop_at.is_some_and(|n| index >= n) { + break; + } + match r { + Rec::Store { size, off, val } => { + let mut n = 0u32; + while kona.bus_write(off as u64, size as u32, val) == BUS_BUSY { + n += 1; + if n > 50_000_000 { + return Err(format!("store at {off:#x} stayed busy")); + } + kona.run_stores(); + std::thread::yield_now(); + } + kona.run_stores(); + } + Rec::Load { size, off, .. } => { + let mut n = 0u32; + while kona.bus_read(off as u64, size as u32).0 == BUS_BUSY { + n += 1; + if n > 50_000_000 { + return Err(format!("load at {off:#x} stayed busy")); + } + std::thread::yield_now(); + } + } + Rec::Marker { kind: 1, .. } => kona.power_on(), + Rec::Marker { kind: 2, .. } => { + swaps += 1; + // The tex_cube span has two swaps in all: both are kept. + if (GL_GOLDEN_SWAPS.contains(&swaps) && [210, 220].contains(&mark)) || (mark == 230 && swaps <= 2) { + let hash = monitor(&kona, &["hash"]).unwrap_or_default().trim().to_string(); + frames.push((swaps, hash, tower_pixels(&kona))); + } + if let (Some(dir), true) = (&shots, ([51, 301, 901].contains(&swaps) && [210, 220].contains(&mark)) || (mark == 230 && swaps <= 2)) { + let file = dir.join(format!("gl-mark{mark}-swap{swaps}.png")); + let _ = monitor(&kona, &["shot", file.to_str().unwrap_or("x.png")]); + } + } + Rec::Marker { kind: 3, .. } => retraces += 1, + Rec::Marker { kind: 0x100, arg } => { + let done = std::mem::take(&mut *census.lock()); + let gl = monitor(&kona, &["gl"]).unwrap_or_default(); + let _ = monitor(&kona, &["gl", "clear"]); + spans.push(Span { mark, records, swaps, retraces, census: done, gl, frames: std::mem::take(&mut frames) }); + (mark, records, swaps, retraces) = (arg, 0, 0, 0); + } + _ => {} + } + } + let gl = monitor(&kona, &["gl"]).unwrap_or_default(); + spans.push(Span { mark, records, swaps, retraces, census: std::mem::take(&mut *census.lock()), gl, frames }); + if let Some(r) = replies { + // The board writes the count at byte 4 and the data at byte 8 in one write; the recorder's + // model wrote the data alone, so the count's four bytes are cut. + // The host keeps each bus write as it came; adjacent ones in order make a run. + let mut runs: Vec<(u64, Vec)> = Vec::new(); + for (_, addr, d) in host.recorded().writes.lock().iter() { + match runs.last_mut() { + Some((a, run)) if *a + run.len() as u64 == *addr => run.extend_from_slice(d), + _ => runs.push((*addr, d.clone())), + } + } + // A reply is the count word then that many words; the previous reply's completion word, when it is + // adjacent, leads the run. + let word = |d: &[u8], i: usize| u32::from_be_bytes(d[4 * i..4 * i + 4].try_into().unwrap()) as usize; + r.board = runs + .into_iter() + .filter_map(|(_, d)| match d.len() { + n if n >= 12 && n % 4 == 0 && word(&d, 0) * 4 + 4 == n => Some(d[4..].to_vec()), + n if n >= 16 && n % 4 == 0 && word(&d, 1) * 4 + 8 == n => Some(d[8..].to_vec()), + _ => None, + }) + .filter(|d| d.len() >= 52) + .collect(); + } + Ok(spans) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A recording's bytes, built record by record. + #[derive(Default)] + struct Builder(Vec); + + impl Builder { + fn new(rm: u32, product: &str) -> Builder { + let mut b = Builder(b"KONAREC\0".to_vec()); + b.0.extend(1u32.to_le_bytes()); + b.0.push(b'C'); + for v in [0xc102u32, 0, rm, 64] { + b.0.extend(v.to_le_bytes()); + } + b.0.push(product.len() as u8); + b.0.extend(product.as_bytes()); + b + } + + fn rw(mut self, tag: u8, size: u8, off: u32, val: u64) -> Self { + self.0.push(tag); + self.0.push(size); + self.0.extend(off.to_le_bytes()); + self.0.extend(val.to_le_bytes()); + self + } + + fn mem(mut self, tag: u8, src: u8, addr: u64, bytes: &[u8]) -> Self { + self.0.push(tag); + self.0.push(src); + self.0.extend(addr.to_le_bytes()); + self.0.extend((bytes.len() as u32).to_le_bytes()); + self.0.extend(bytes); + self + } + + fn marker(mut self, kind: u32, arg: u32) -> Self { + self.0.push(b'M'); + self.0.extend(kind.to_le_bytes()); + self.0.extend(arg.to_le_bytes()); + self + } + + fn end(mut self, count: u64) -> Vec { + self.0.push(b'E'); + self.0.extend(count.to_le_bytes()); + self.0 + } + } + + /// The store after the PROM recording (see `the_prom_recording_draws_into_the_store`). + const PROM_STORE_HASH: &str = "48d707b87081b952"; + + /// The records of the IRIX recording the front end frames exactly (see + /// `the_irix_recording_replays`). + const IRIX_EXACT_RECORDS: usize = 1_415_538; + /// The GL demo recording's store hash at the swaps its shots are taken at, with + /// the count of near-red pixels in the windmill tower's box (mark, swap, hash, + /// tower pixels). Pinned from this model's own output on 2026-11-08, after the + /// hand-off stopped cutting in window space (which is what made the ivview tower + /// draw: the capture shows about 11,700 red pixels, the model drew 54 before). Re-pinned + /// when depth became a 32.16 integer plane: the stored depth words round differently, + /// so the hashes moved, and the frames moved by three windmill pixels where exact depth + /// ties now resolve differently (one at swap 51, two at swap 901); Atlantis is identical. + /// Re-pinned again when lines, points and the polygon stipple began to draw: the + /// windmill's sails are stippled, so its hashes and tower counts moved (the capture shows + /// about 11,700); the Atlantis hashes did not. Re-pinned a third time when textures began to draw (and + /// the matrix double forms made tex_cube's cube draw): the Atlantis and windmill hashes + /// moved and the windmill's tower counts did not; the tex_cube span, two swaps, is pinned + /// from this run, and its second frame is within 24 levels of the hardware capture + /// everywhere but 16 pixels. Re-pinned a fourth time when bitmaps began to draw: the tex_cube + /// span's 19 bitmaps (its "Loading texture" label, 460 fragments) changed both its swaps, + /// where the second frame still matches the capture to the same 16 pixels; the Atlantis and + /// windmill hashes did not move. Re-pinned a fifth time when multisampling began to draw: the + /// Atlantis window is multisampled (four samples, 16-bit depth), so its span's hashes moved, and + /// the later spans' with them, since the store keeps the sample words of the earlier window. The + /// windmill tower counts and the tex_cube frame's distance from the capture (the same sixteen + /// pixels) did not move. Re-pinned a sixth time when a small texture level took the origin and size the + /// client's copies give it, instead of the box of every copy: the tex_cube frame's second swap moved in 216 + /// pixels, none by more than 8 levels, and its distance from the capture went from 16 to 17 pixels past 24 + /// levels; the other swaps did not move. Re-pinned a seventh time when window positions began to round to + /// the nearest 1/256 with ties to even, as the engine's subtract of 49152.0 does, instead of truncating + /// toward minus infinity: every hash moved (every vertex can land a step away), and the frames moved by 14 + /// pixels at Atlantis's last pin, 94 at the windmill's second and none at the cube's; the windmill tower + /// counts moved by four pixels at two pins; no frame's distance from its capture changed by more than + /// three pixels. Re-pinned an eighth time when texture-coordinate generation began to run: the demos + /// enable generation for s and t (eye linear, the start-up mode) around their textured draws, so + /// generated coordinates now replace the current ones and every hash moved; the windmill tower + /// counts did not. They + /// guard the GL path; they are not hardware truth. A span's hash covers the store as the earlier + /// spans left it (the sample words of a multisampled window stay), so a change to one demo moves the + /// pins of the demos after it. Only the windmill span has a tower, so the other counts are zero. + const GL_GOLDEN: [(u32, u64, &str, u32); 8] = [ + (210, 51, "314e04178c634396", 0), + (210, 301, "060ac500cc3c9b2c", 0), + (210, 901, "26e030b433aaa6e8", 0), + (220, 51, "d968e2f0c8716891", 13711), + (220, 301, "c238b10a810661ff", 11622), + (220, 901, "1fc80fbf204ae224", 11629), + (230, 1, "15c23026e7755afa", 0), + (230, 2, "44e73640bb50bcf6", 0), + ]; + const IRIX_STORE_HASH: &str = "3a39733d18360884"; + const IRIX_FULL_STORE_HASH: &str = "ba6c19868b1d31f9"; + + /// The share of pixels the PROM's composed frame may differ from its capture by: + /// none. It measures zero, so any difference is a regression to look at. + const PROM_FRAME_TOLERANCE: f64 = 0.0; + + const PORT: u32 = 0xc10000; + + #[test] + fn the_parser_reads_every_record_kind() { + let mut b = Builder::new(2, "ir2") + .rw(b'W', 4, PORT, 7) + .rw(b'R', 8, 0x800240, 0x1122_3344_5566_7788) + .mem(b'D', 3, 0x1000, &[1, 2, 3]) + .mem(b'O', 1, 0x2000, &[9]); + b.0.extend([b'I', 1, 0x12, 1]); + let b = b.marker(1, 0).end(7); + let r = Recording::parse(&b).unwrap(); + assert_eq!(r.board, Board { part: 0xc102, revision: 0, rm_count: 2, texture_mb: 64, product: "ir2".into() }); + assert_eq!(r.end, Some(7)); + assert_eq!( + r.records, + [ + Rec::Store { size: 4, off: PORT, val: 7 }, + Rec::Load { size: 8, off: 0x800240, val: 0x1122_3344_5566_7788 }, + Rec::DmaRead { src: 3, addr: 0x1000, bytes: vec![1, 2, 3] }, + Rec::DmaWrite { src: 1, addr: 0x2000, bytes: vec![9] }, + Rec::Interrupt { kind: 1, vector: 0x12, delivered: 1 }, + Rec::Marker { kind: 1, arg: 0 }, + ] + ); + assert!(r.config().is_ok()); + } + + #[test] + fn the_parser_refuses_what_it_cannot_trust() { + assert!(Recording::parse(b"ODSYREC\0\x01\0\0\0").is_err()); + let ok = Builder::new(2, "ir2").rw(b'W', 4, PORT, 7).end(2); + assert!(Recording::parse(&ok).is_ok()); + // A wrong count, a cut-off record, an unknown tag, a second version. + assert!(Recording::parse(&Builder::new(2, "ir2").rw(b'W', 4, PORT, 7).end(3)).unwrap_err().contains("counts 3")); + // The end record cut in half is a cut-off record, not a short file. + assert!(Recording::parse(&ok[..ok.len() - 4]).unwrap_err().contains("cut off")); + let cut = Builder::new(2, "ir2").rw(b'W', 4, PORT, 7).0; + assert!(Recording::parse(&cut[..cut.len() - 3]).unwrap_err().contains("cut off")); + let mut bad = Builder::new(2, "ir2").0; + bad.push(b'Z'); + assert!(Recording::parse(&bad).unwrap_err().contains("unknown record tag")); + let mut v2 = Builder::new(2, "ir2").0; + v2[8] = 2; + assert!(Recording::parse(&v2).unwrap_err().contains("version 2")); + // A file with no end record parses, and says so. + assert_eq!(Recording::parse(&cut).unwrap().end, None); + // Data after the end record, a record before the board, a second board. + let mut after = ok.clone(); + after.push(b'W'); + assert!(Recording::parse(&after).unwrap_err().contains("after the end record")); + let mut early = b"KONAREC\0\x01\0\0\0".to_vec(); + early.extend([b'W', 4]); + assert!(Recording::parse(&early).unwrap_err().contains("not first")); + let board = Builder::new(2, "ir2").0; + let mut twice = board.clone(); + twice.extend(&board[12..]); + assert!(Recording::parse(&twice).unwrap_err().contains("second board")); + assert!(Recording::parse(&board[..12]).unwrap_err().contains("no board record")); + // The InfiniteReality4 is not modelled. + let e = Recording::parse(&Builder::new(2, "ir4").0).unwrap().config().unwrap_err(); + assert!(e.contains("ir4") && e.contains("ir2e"), "{e}"); + } + + /// A recording's product picks the pipe, and its texture size has to be + /// the product's. + #[test] + fn a_recording_names_any_modelled_product() { + let board = |product: &str, texture_mb| Recording { + board: Board { part: 0xc102, revision: 0, rm_count: 1, texture_mb, product: product.into() }, + records: Vec::new(), + end: None, + }; + for (product, mb) in [("ir2", 64), ("ir2e", 64), ("ir3", 256)] { + let cfg = board(product, mb).config().unwrap(); + assert_eq!(cfg.spec().name, product); + assert_eq!(cfg.texture_bytes_per_rm(), (mb as u64) << 20); + } + let e = board("ir3", 64).config().unwrap_err(); + assert!(e.contains("256"), "{e}"); + } + + /// The textport probe and a colour select, a mailbox read the reply + /// answers, a retrace request and a reset marker, through the board. + #[test] + fn a_small_stream_replays_and_is_compared() { + let b = Builder::new(2, "ir2") + .rw(b'W', 4, PORT, 1) + .rw(b'W', 4, PORT, 9) + .rw(b'W', 4, PORT, 0) + .rw(b'R', 4, 0x800240, 0x1234abcd) + .rw(b'W', 4, PORT, 0x1e7) + .rw(b'W', 4, PORT, 0x42) + .rw(b'R', 4, 0x800248, 0x42) + .marker(3, 0x42) + .marker(1, 0) + .rw(b'R', 4, 0x800248, 0) + .end(11); + let rec = Recording::parse(&b).unwrap(); + let (rep, census) = replay(&rec).unwrap(); + assert_eq!(rep.loads_compared, 3, "{}", rep.render()); + assert!(rep.load_mismatches.is_empty(), "{}", rep.render()); + assert_eq!(rep.resets, 1); + assert_eq!(rep.retrace_asked, [0x42]); + assert_eq!(rep.retrace_asked, rep.retrace_expected); + assert_eq!(census.retraces, [0x42]); + assert!(rep.failure.is_none()); + } + + /// A load that differs from the recorded answer is reported, one that + /// depends on timing is not compared. + #[test] + fn a_wrong_answer_is_reported_and_a_timing_load_is_not() { + let b = Builder::new(2, "ir2") + .rw(b'R', 4, 0x04, 0xdead_beef) + .rw(b'R', 4, CREDIT, 0x7fe) + .rw(b'R', 4, ACTIVITY, 4) + .end(4); + let (rep, ..) = replay(&Recording::parse(&b).unwrap()).unwrap(); + assert_eq!(rep.loads, 3); + assert_eq!(rep.loads_compared, 1); + assert_eq!(rep.load_mismatches.keys().map(|k| k.0).collect::>(), [0x04]); + } + + /// The board's reads are served from the recorded ones in order, a read + /// no record covers is counted, and what the board writes is compared + /// with the recorded writes. + #[test] + fn dma_is_served_and_compared() { + let host = replay_host(&Recording::parse(&Builder::new(2, "ir2").mem(b'D', 3, 0x1000, &[1, 2, 3, 4]).mem(b'D', 3, 0x2000, &[5, 6]).end(3)).unwrap(), Arc::default()); + let mut a = [0u8; 2]; + host.recorded().read_into(0x1000, &mut a); + assert_eq!(a, [1, 2]); + let mut a = [0u8; 2]; + host.recorded().read_into(0x1002, &mut a); + assert_eq!(a, [3, 4]); + let mut a = [0xffu8; 2]; + host.recorded().read_into(0x9000, &mut a); + assert_eq!(a, [0, 0]); + let r = host.recorded().reads.lock(); + assert_eq!((r.matched, r.unmatched, r.cursor), (2, 2, 1)); + } + + /// The recordings, when KONA_RECORDINGS names them. The PROM stream uses + /// the register aperture only; the IRIX stream adds DMA, contexts and + /// the GL grammar. + fn replay_file(name: &str) -> Option<(Recording, Report, Census)> { + let dir = recordings_dir()?; + let path = dir.join(name); + if !path.exists() { + eprintln!("skipping: {} is not there", path.display()); + return None; + } + let rec = Recording::parse(&std::fs::read(&path).unwrap()).unwrap(); + let (rep, census) = replay(&rec).unwrap(); + eprintln!("{name}\n{}", rep.render()); + eprintln!("{}", census.render()); + if let Some(before) = &census.before_long_drop { + eprintln!("\n=== the same census up to record {}, where the front end could no longer size the stream ===\n{}", rep.exact_until, before.render()); + } + Some((rec, rep, census)) + } + + /// The PROM stream drawn into the store, to its end. The hash is the + /// store after the last record: a regression net for the drawing, not + /// evidence of the pixels, which the display step compares with + /// the capture. Reset markers clear the store, so this is the last + /// screen the console drew. + #[test] + fn the_prom_recording_draws_into_the_store() { + let Some(dir) = recordings_dir() else { return }; + let path = dir.join("onyx2-deskside-prom.kona.rec"); + if !path.exists() { + eprintln!("skipping: {} is not there", path.display()); + return; + } + let rec = Recording::parse(&std::fs::read(&path).unwrap()).unwrap(); + let (status, hash) = replay_drawing(&rec, None).unwrap(); + eprintln!("{status}\nstore hash {hash}"); + assert!(status.contains("1 word arrays"), "{status}"); + assert_eq!(hash, PROM_STORE_HASH, "{status}"); + // The PROM draws no GL, so no drain ever finds work queued. + assert!(status.contains("0 with work queued"), "{status}"); + } + + /// The IRIX stream drawn into the store as far as the front end frames it + /// exactly with no back end behind it: the X server's state, boxes, + /// rectangles, spans, points, stipples and the pixel writes and copies of + /// the greeter's start. The hash is a regression net, not evidence of the + /// pixels. The whole stream, which only a back end can frame, is + /// `the_irix_recording_replays_into_the_back_end`. + #[test] + fn the_irix_recording_draws_into_the_store_over_its_exact_window() { + let Some(dir) = recordings_dir() else { return }; + let path = dir.join("onyx2-deskside-irix-4dwm.kona.rec"); + if !path.exists() { + eprintln!("skipping: {} is not there", path.display()); + return; + } + let rec = Recording::parse(&std::fs::read(&path).unwrap()).unwrap(); + let (status, hash) = replay_drawing(&rec, Some(IRIX_EXACT_RECORDS)).unwrap(); + eprintln!("{status}\nstore hash {hash}"); + assert_eq!(hash, IRIX_STORE_HASH, "{status}"); + } + + /// A snapshot taken part way through a stream, loaded into a fresh board, which then plays the + /// rest, ends with the store and the chunks the uninterrupted board ends with. Chunks of a + /// saved board that is loaded and saved again are identical. + fn resumed_from_a_snapshot(name: &str, splits: &[usize]) { + let Some(dir) = recordings_dir() else { return }; + let path = dir.join(name); + if !path.exists() { + eprintln!("skipping: {} is not there", path.display()); + return; + } + let rec = Recording::parse(&std::fs::read(&path).unwrap()).unwrap(); + let whole = replay_board(&rec, None).unwrap(); + let want = monitor(&whole, &["hash"]).unwrap(); + for &split in splits { + let mut split = split.min(rec.records.len()); + let saves = ScratchDir::new(&format!("replay-snap-{split}")); + let cs = crate::chunk_store::ChunkStore::new(saves.path()); + // A snapshot is refused in the middle of a command; take the next record that is not. + let (first, saved) = loop { + let first = replay_board(&rec, Some(split)).unwrap(); + match first.save_chunks(&cs) { + Ok(saved) => break (first, saved), + Err(e) if e.to_string().contains("middle of a command") && split < rec.records.len() => split += 1, + Err(e) => panic!("{name} at {split}: {e}"), + } + }; + let second = new_board(&rec).unwrap(); + second.load_chunks(&cs, &saved).unwrap(); + let again = second.save_chunks(&cs).unwrap(); + assert_eq!(saved, again, "{name} at {split}: save, load, save changed the chunks"); + assert_eq!(monitor(&first, &["hash"]).unwrap(), monitor(&second, &["hash"]).unwrap(), "{name} at {split}: the store differs after the load"); + play(&second, rec.records.iter().skip(split)); + assert_eq!(monitor(&second, &["hash"]).unwrap(), want, "{name}: resumed at {split} ends with another store"); + eprintln!("{name}: resumed at {split} of {} records, {} chunk lists", rec.records.len(), saved.len()); + } + } + + #[test] + fn the_prom_replay_resumed_from_a_snapshot_reaches_the_same_frame() { + resumed_from_a_snapshot("onyx2-deskside-prom.kona.rec", &[1000, 20_000, 40_000]); + } + + #[test] + fn the_irix_replay_resumed_from_a_snapshot_reaches_the_same_store() { + resumed_from_a_snapshot("onyx2-deskside-irix-4dwm.kona.rec", &[IRIX_EXACT_RECORDS / 2, IRIX_EXACT_RECORDS]); + } + + /// A snapshot taken part way through each of the three GL demos, loaded into a fresh board that is then driven with + /// the first, ends each demo with the same chunks (the store, the texture arena, the small state: + /// contexts, texture level maps, pixel, feedback and multisample state) and the same hash. The splits are + /// inside atlantis (a multisampled window with a texture), ivview and tex_cube. Slow: the file is replayed once + /// with up to four boards. + #[test] + fn the_gl_replay_resumed_from_snapshots_ends_the_same() { + let Some(path) = std::env::var_os("KONA_GL_RECORDING").map(std::path::PathBuf::from) else { + eprintln!("skipping: set KONA_GL_RECORDING to the GL demo recording"); + return; + }; + // The boards are compared once all three exist (tex_cube's), at its start, at its end and at the recording's. + let (taken, checks) = gl_resume(&path, &[33_586_058 + 1_000_000, 65_508_173 + 500_000, 71_263_133 + 10_000], &[71_273_200, 71_283_374, 71_292_000]).unwrap(); + eprintln!("snapshots at {taken:?}; checks {checks:?}"); + assert_eq!(taken.len(), 3); + assert_eq!(checks.len(), 3, "every check was made"); + assert!(checks.iter().all(|(_, differ)| differ.is_empty()), "boards that ended differently: {checks:?}"); + } + + + /// The GL demo recording's query replies: what the recorder's board wrote for the current-value, generic-state (the + /// matrices the demos read back every frame) and line-limit queries, against what this board writes. The recorder + /// is a model of the same board, so agreement is a behaviour match and not a claim about the hardware. + #[test] + fn the_gl_demo_query_replies_match_the_recording() { + let Some(path) = std::env::var_os("KONA_GL_RECORDING").map(std::path::PathBuf::from) else { + eprintln!("skipping: set KONA_GL_RECORDING to the GL demo recording"); + return; + }; + let mut replies = Replies::default(); + census_spans_replies(&path, Behind::Real, Some(&mut replies)).unwrap(); + // The replies of the three query kinds, by size: current values (52 bytes), a matrix (64) and the line limits (128). + let by_len = |v: &Vec>, n: usize| v.iter().filter(|r| r.len() == n).cloned().collect::>(); + let mut report = Vec::new(); + for (n, kind) in [(52, "current values"), (64, "matrices"), (128, "line limits")] { + let (a, b) = (by_len(&replies.recorded, n), by_len(&replies.board, n)); + let same = a.iter().zip(&b).filter(|(x, y)| x == y).count(); + let first = a.iter().zip(&b).position(|(x, y)| x != y); + eprintln!("{kind}: {} recorded, {} written by this board, {same} identical, first difference at {first:?}", a.len(), b.len()); + if let Some(i) = first { + eprintln!(" recorded {:08x?}\n board {:08x?}", a[i].chunks(4).map(|c| u32::from_be_bytes(c.try_into().unwrap())).collect::>(), b[i].chunks(4).map(|c| u32::from_be_bytes(c.try_into().unwrap())).collect::>()); + } + report.push((n, a.len(), b.len(), same)); + } + // The line limits are the recorder's own values and match exactly. The current-value replies, now in the + // table's established layout (colour, index, normal, texture coordinates, edge flag), match word for word + // except the edge flag: the recorder writes 0 where the default is set and this board writes the float 1.0, which + // is what the client needs for a set flag (it truncates the word to an integer). The table grades the flag's + // exact bit pattern inferred, so that one word is not held, and the other twelve are. The + // matrices are held to within float rounding. + let (a, b) = (by_len(&replies.recorded, 52), by_len(&replies.board, 52)); + let body = |r: &Vec| r[..48].to_vec(); + let differ = a.iter().zip(&b).filter(|(x, y)| body(x) != body(y)).count(); + eprintln!("current values, edge flag aside: {differ} differ"); + assert_eq!(differ, 0, "current-value replies that differ outside the edge flag"); + assert_eq!(report[1].1, report[1].2, "a matrix reply for every one recorded"); + // The matrices are built by each side's own float arithmetic from the same commands, so they agree to the + // last bit or two of the largest element, not always exactly. + let (a, b) = (by_len(&replies.recorded, 64), by_len(&replies.board, 64)); + let fl = |v: &Vec| v.chunks(4).map(|c| f32::from_bits(u32::from_be_bytes(c.try_into().unwrap()))).collect::>(); + let worst = a.iter().zip(&b).map(|(x, y)| { + let (x, y) = (fl(x), fl(y)); + let scale = x.iter().fold(0f32, |m, v| m.max(v.abs())).max(1e-30); + x.iter().zip(&y).map(|(p, q)| (p - q).abs() / scale).fold(0f32, f32::max) + }).fold(0f32, f32::max); + eprintln!("worst matrix difference, relative to the matrix's largest element: {worst:e}"); + assert!(worst < 2e-7, "{worst:e}"); + assert_eq!(report[2].3, report[2].1, "line-limit replies that differ"); + assert_eq!(report[0].1, report[0].2, "a current-value reply for every one recorded"); + } + + /// The frame the PROM stream composes, against the recorded screen + /// capture. Both renderers are approximations of one board, so the check is + /// a tolerance, set from the first measurement: the share of pixels whose + /// colour differs by more than a few levels in any channel stays under + /// `PROM_FRAME_TOLERANCE`, zero (the first measurement found no pixel differing; + /// the recorder is the same model, so that agreement is not hardware + /// evidence). It catches a layout, placement or colour-map + /// disagreement to look into; it is not a claim about the hardware's pixels. + #[test] + fn the_prom_frame_matches_its_capture_within_a_tolerance() { + let Some(dir) = recordings_dir() else { return }; + let (rec_path, png_path) = (dir.join("onyx2-deskside-prom.kona.rec"), dir.join("onyx2-deskside-prom-graphics-console.png")); + if !rec_path.exists() || !png_path.exists() { + eprintln!("skipping: the PROM recording or its capture is not in {}", dir.display()); + return; + } + let rec = Recording::parse(&std::fs::read(&rec_path).unwrap()).unwrap(); + let kona = replay_board(&rec, None).unwrap(); + let shots = ScratchDir::new("prom-shot"); + let shot = shots.join("shot.png"); + let said = monitor(&kona, &["shot", shot.to_str().unwrap()]).unwrap(); + assert!(said.contains("1280 x 1024"), "{said}"); + let (ours, theirs) = (decode_png(&shot), decode_png(&png_path)); + assert_eq!((ours.0, ours.1), (theirs.0, theirs.1)); + let bad = ours.2.chunks_exact(3).zip(theirs.2.chunks_exact(3)).filter(|(a, b)| a.iter().zip(b.iter()).any(|(x, y)| x.abs_diff(*y) > 8)).count(); + let share = bad as f64 / (ours.0 * ours.1) as f64; + eprintln!("{bad} of {} pixels differ by more than 8 levels ({:.4})", ours.0 * ours.1, share); + assert!(share <= PROM_FRAME_TOLERANCE, "{share}"); + } + + /// The window the board shows the guest returns the composed PROM frame: + /// once the board runs, a frame request is answered at a retrace with the + /// frame in the screen's buffer, which matches the capture as the shot does. + #[test] + fn a_frame_request_on_the_running_board_returns_the_prom_frame() { + use crate::gfx_display::GfxDisplay; + use crate::traits::Device; + let Some(dir) = recordings_dir() else { return }; + let (rec_path, png_path) = (dir.join("onyx2-deskside-prom.kona.rec"), dir.join("onyx2-deskside-prom-graphics-console.png")); + if !rec_path.exists() || !png_path.exists() { + eprintln!("skipping: the PROM recording or its capture is not in {}", dir.display()); + return; + } + let rec = Recording::parse(&std::fs::read(&rec_path).unwrap()).unwrap(); + let (kona, display) = replay_board_with_display(&rec, None).unwrap(); + kona.start(); + display.request_frame(); + let end = std::time::Instant::now() + std::time::Duration::from_secs(30); + while display.frame_pending() && std::time::Instant::now() < end { + std::thread::yield_now(); + } + assert!(!display.frame_pending(), "the request was answered at a retrace"); + let theirs = decode_png(&png_path); + let screen = display.screen().lock(); + assert_eq!((screen.width, screen.height), (theirs.0, theirs.1)); + let mut bad = 0; + for y in 0..theirs.1 { + for x in 0..theirs.0 { + let p = screen.rgba[y * crate::disp::FB_STRIDE + x]; + let ours = [p as u8, (p >> 8) as u8, (p >> 16) as u8]; + let at = (y * theirs.0 + x) * 3; + bad += ours.iter().zip(&theirs.2[at..at + 3]).any(|(a, b)| a.abs_diff(*b) > 8) as usize; + } + } + drop(screen); + kona.stop(); + let share = bad as f64 / (theirs.0 * theirs.1) as f64; + eprintln!("{bad} pixels differ ({share:.4})"); + assert!(share <= PROM_FRAME_TOLERANCE, "{share}"); + } + + /// The IRIX recording replayed whole into the drawing back end, with the + /// board's board built from its `C` record. + fn replay_irix_into_the_back_end() -> Option<(Report, Census, Arc)> { + let dir = recordings_dir()?; + let path = dir.join("onyx2-deskside-irix-4dwm.kona.rec"); + if !path.exists() { + eprintln!("skipping: {} is not there", path.display()); + return None; + } + let rec = Recording::parse(&std::fs::read(&path).unwrap()).unwrap(); + Some(replay_into(&rec, Behind::Real).unwrap()) + } + + /// The video Get replies the IRIX recording holds. + const VIDEO_REPLIES_RECORDED: u64 = 63; + + /// The whole IRIX stream, which the front end can now frame to its end: the + /// back end sizes the pixel writes' rows, so no word is dropped. What the + /// recording shows the board wrote to guest memory is what the back end + /// writes, byte for byte: the pixel reads' rows (about 250 KB of them, by + /// DMA into host buffers) come out of this store the same as out of the + /// recorder's, and the two completions that follow them match. Both models + /// are approximations of one board, so that agreement is a behaviour match + /// and not a statement about the hardware's pixels. + #[test] + fn the_irix_recording_replays_into_the_back_end() { + let Some((rep, census, kona)) = replay_irix_into_the_back_end() else { return }; + eprintln!("{}", rep.render()); + let status = monitor(&kona, &["status"]).unwrap(); + eprintln!("{status}"); + assert!(rep.failure.is_none(), "{}", rep.render()); + assert!(rep.load_mismatches.is_empty(), "{}", rep.render()); + assert_eq!(rep.exact_until, rep.records, "no run of dropped words: the stream frames whole"); + assert_eq!(census.dropped, 0); + assert_eq!(rep.ge_sent, rep.ge_expected, "{}", rep.render()); + assert_eq!((rep.writes_extra_bytes, rep.writes_missing_bytes), (0, 0), "{}", rep.render()); + assert_eq!(rep.writes_same, 251_920, "{}", rep.render()); + // The display generator's Gets, which the stream's start-up sends and the server waits on: every + // reply the recording shows, written the same, in order. + assert_eq!(rep.video_replies, (VIDEO_REPLIES_RECORDED, VIDEO_REPLIES_RECORDED), "{}", rep.render()); + assert_eq!(rep.runs_in_order, rep.runs_board, "{}", rep.render()); + // Every pixel command the stream sends is drawn, none refused. + assert!(status.contains("448 setups, 692 writes (6 by DMA), 113 copies, 63 reads, 272 finishes"), "{status}"); + assert!(status.contains("refused 0 context, 0 target, 0 destination, 0 window copies, 0 reads answered with zeros (0 frame-buffer word)"), "{status}"); + assert!(status.contains("0 short or left owing, 0 with no write waiting"), "{status}"); + // No GL is queued while the desktop draws, so no drain ever found work waiting. + assert!(status.contains("0 with work queued"), "{status}"); + // The desktop's pointer is the XMAP glyph; the stream never loads or enables a DAC cursor. + assert!(status.contains("DAC cursor 0 loads, 0 refused; held for channels []; channel 0's is not drawn"), "{status}"); + let hash = monitor(&kona, &["hash"]).unwrap().trim().to_string(); + eprintln!("store hash {hash}"); + assert_eq!(hash, IRIX_FULL_STORE_HASH, "{status}"); + } + + /// The frame the board composes at the end of the IRIX stream, against the + /// recorded capture of the desktop. The desktop is index windows through the + /// X colour map and the gamma RAM, composed from what the stream loaded and + /// drew. Both renderers are approximations of one board and the capture is the + /// recorder's own, so this agreement shows the composition reads the tables as + /// the recorder's model does and says nothing about the hardware's pixels. + /// + /// Every pixel is within one level of the capture, and no pixel is further. The only + /// remaining cause is the recorder's widening of the 10-bit colour-map entry to the + /// 12-bit gamma index: it rounds the entry to 8 bits first and widens that, where the + /// pipe's own rule replicates the top two bits (graded inferred: the only rule the + /// board's gamma addressing is known to fit), which with the identity gamma RAM is the + /// entry's top eight bits. The 12-bit gamma output reaches the 8-bit DAC inputs by dropping + /// the low four bits, as the recorder does (established on the link from the colour-map + /// chips to the video-output chips, by the board's wiring). With the widening the recorder's + /// way too, the frame matched the capture exactly (measured on 2026-11-10: 0 of 1310720 + /// pixels differ). Measured with the truncation in: 644505 of 1310720 pixels differ, all by + /// one level (rounding the entry and dropping its low two bits part at about every other + /// entry). The glyph cursor is enabled at the end of the stream, but neither this shot nor + /// the capture shows one, so none is compared. + #[test] + fn the_irix_desktop_frame_matches_its_capture_within_a_tolerance() { + let Some(dir) = recordings_dir() else { return }; + let png_path = dir.join("onyx2-deskside-irix-4dwm-desktop.png"); + if !png_path.exists() { + eprintln!("skipping: {} is not there", png_path.display()); + return; + } + let Some((_, _, kona)) = replay_irix_into_the_back_end() else { return }; + let shots = ScratchDir::new("irix-shot"); + let shot = shots.join("shot.png"); + monitor(&kona, &["shot", shot.to_str().unwrap()]).unwrap(); + let (ours, theirs) = (decode_png(&shot), decode_png(&png_path)); + // KONA_SHOT_DIR keeps the frame, to look at beside the capture. + if let Some(dir) = std::env::var_os("KONA_SHOT_DIR") { + let _ = std::fs::copy(&shot, std::path::Path::new(&dir).join("irix-desktop-ours.png")); + } + assert_eq!((ours.0, ours.1), (theirs.0, theirs.1)); + let widest = ours.2.iter().zip(&theirs.2).map(|(a, b)| a.abs_diff(*b)).max().unwrap(); + let off_by_one = ours.2.chunks_exact(3).zip(theirs.2.chunks_exact(3)).filter(|(a, b)| a != b).count(); + let bad = ours.2.chunks_exact(3).zip(theirs.2.chunks_exact(3)).filter(|(a, b)| a.iter().zip(b.iter()).any(|(x, y)| x.abs_diff(*y) > 1)).count(); + eprintln!("{off_by_one} of {} pixels differ, the widest by {widest} level, {bad} by more than one", ours.0 * ours.1); + assert_eq!(bad, 0, "pixels further than one level from the capture"); + } + + /// A PNG's width, height and RGB bytes. + fn decode_png(path: &std::path::Path) -> (usize, usize, Vec) { + let dec = png::Decoder::new(std::io::BufReader::new(std::fs::File::open(path).unwrap())); + let mut r = dec.read_info().unwrap(); + let mut buf = vec![0; r.output_buffer_size().expect("a PNG that fits memory")]; + let info = r.next_frame(&mut buf).unwrap(); + let bytes = &buf[..info.buffer_size()]; + let (w, h) = (info.width as usize, info.height as usize); + let rgb = match info.color_type { + png::ColorType::Rgb => bytes.to_vec(), + png::ColorType::Rgba => bytes.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]).collect(), + other => panic!("unexpected PNG colour type {other:?}"), + }; + (w, h, rgb) + } + + #[test] + fn the_prom_recording_replays() { + let Some((rec, rep, ..)) = replay_file("onyx2-deskside-prom.kona.rec") else { return }; + assert_eq!(rec.end, Some(rec.records.len() as u64 + 1)); + assert!(rep.failure.is_none(), "{}", rep.render()); + assert!(rep.load_mismatches.is_empty(), "{}", rep.render()); + assert_eq!(rep.ge_expected, rep.ge_sent); + assert_eq!(rep.exact_runs, (0, 0)); + } + + #[test] + fn the_irix_recording_replays() { + let Some((rec, rep, census)) = replay_file("onyx2-deskside-irix-4dwm.kona.rec") else { return }; + assert_eq!(rec.end, Some(rec.records.len() as u64 + 1)); + assert!(rep.failure.is_none(), "{}", rep.render()); + // Exact as far as the front end can frame the stream alone: every + // deterministic load, and every byte the board wrote to guest memory, + // matches the recording before the first run of data only a back end + // can size (measured as the 64th dropped word in a row). Past it the stream misframes, as it must on a null back end. + // The window is pinned: a front-end change that makes the stream + // misframe sooner shrinks what is checked, and must fail here. + assert_eq!(rep.exact_until, 1_415_538, "{}", rep.render()); + assert_eq!(rep.exact_runs, (4, 4), "{}", rep.render()); + assert_eq!(rep.ge_sent, rep.ge_expected, "{}", rep.render()); + assert!(rep.first_mismatch.is_none_or(|at| at >= rep.exact_until), "{}", rep.render()); + assert!(rep.exact_runs.1 > 0 && rep.exact_runs.0 == rep.exact_runs.1, "{}", rep.render()); + // The recording has more: the data a pixel read's reply carries, which + // only a back end posts. + assert!(rep.exact_runs.1 < rep.exact_runs_recorded, "{}", rep.render()); + // Video interrupts depend on timing, so only that some came is + // checked: the back end completes no retrace here, so none is sent. + assert!(rep.general_expected > 0); + assert!(census.jobs > 0); + } + + /// The GL demo recording, when KONA_GL_RECORDING names it (a gigabyte: a + /// minute or two in a debug build). Prints each demo's span; asserts that + /// the marks are there and that GL commands reached the back end. + #[test] + fn the_gl_demo_recording_is_censused() { + let Some(path) = std::env::var_os("KONA_GL_RECORDING").map(std::path::PathBuf::from) else { + eprintln!("skipping: set KONA_GL_RECORDING to the GL demo recording"); + return; + }; + let spans = census_spans(&path).unwrap(); + let marks: Vec = spans.iter().map(|s| s.mark).collect(); + for m in [210, 220, 230] { + assert!(marks.contains(&m), "mark {m} not in {marks:?}"); + } + for sp in spans.iter().filter(|s| [210, 220, 230].contains(&s.mark)) { + eprintln!("=== span from mark {}: {} records, {} swap markers, {} retrace markers ===", sp.mark, sp.records, sp.swaps, sp.retraces); + eprintln!("{}", sp.census.render()); + eprintln!("{}", sp.census.render_families()); + assert!(sp.census.jobs > 0); + } + } + + /// The GL demo recording through the GL path with the census on: where its + /// vertices fall against the window, which texture-coordinate forms it + /// sends, what the path does not take. Prints each demo's span. The + /// recording is a gigabyte (a few minutes in a debug build). + #[test] + fn the_gl_demo_recording_is_censused_through_the_gl_path() { + let Some(path) = std::env::var_os("KONA_GL_RECORDING").map(std::path::PathBuf::from) else { + eprintln!("skipping: set KONA_GL_RECORDING to the GL demo recording"); + return; + }; + let cfg = KonaConfig::default(); + let spans = census_spans_into(&path, Behind::Real).unwrap(); + for sp in spans.iter().filter(|s| [210, 220, 230].contains(&s.mark)) { + eprintln!("=== GL path, span from mark {}: {} records ===\n{}", sp.mark, sp.records, sp.gl); + assert!(sp.gl.contains("vertex census"), "{}", sp.gl); + for (swap, hash, tower) in &sp.frames { + eprintln!("golden mark {} swap {swap}: store hash {hash}, tower pixels {tower}", sp.mark); + } + } + let got: Vec<(u32, u64, &str, u32)> = spans.iter().filter(|s| [210, 220, 230].contains(&s.mark)).flat_map(|s| s.frames.iter().map(move |(w, h, t)| (s.mark, *w, h.as_str(), *t))).collect(); + assert_eq!(got, GL_GOLDEN.to_vec(), "the GL demos' store hashes at the shot swaps moved: a change to the GL path or the pixel module, or a fix; look at the shots"); + } + + /// The Quake recording (SGI Quake GL, then TyrQuake) through the GL path: how many texture loads + /// and binds each span's replay refuses. Texture memory is one pool the games load into and reload + /// within; a level reloaded at the same place overwrites it and a retired level's space is free again, + /// so a long run must not fill the product's memory (`back::store::texture`). The recording is four + /// gigabytes (several minutes in a release build). + #[test] + fn the_quake_recording_keeps_texture_memory_in_bounds() { + let Some(path) = std::env::var_os("KONA_QUAKE_RECORDING").map(std::path::PathBuf::from) else { + eprintln!("skipping: set KONA_QUAKE_RECORDING to the Quake recording"); + return; + }; + let spans = census_spans_into(&path, Behind::Real).unwrap(); + // The "textures" line reads "... {E} past memory), {F} finishes, {G} binds ({H} of volumes, {I} refused) ...". + let past_of = |text: &str| -> u64 { text.lines().find(|l| l.starts_with("textures")).and_then(|l| l.split(" past memory").next()).and_then(|l| l.split_whitespace().last()).and_then(|t| t.parse().ok()).unwrap_or(0) }; + let binds_refused_of = |text: &str| -> u64 { text.lines().find(|l| l.starts_with("textures")).and_then(|l| l.split(" of volumes, ").nth(1)).and_then(|l| l.split_whitespace().next()).and_then(|t| t.parse().ok()).unwrap_or(0) }; + let (mut past, mut refused_binds) = (0u64, 0u64); + for sp in &spans { + let (p, b) = (past_of(&sp.gl), binds_refused_of(&sp.gl)); + eprintln!("quake span from mark {}: {} records, {p} loads past texture memory, {b} binds refused", sp.mark, sp.records); + for l in sp.gl.lines().filter(|l| l.starts_with("textures") || l.starts_with("texture memory")) { + eprintln!(" {l}"); + } + past += p; + refused_binds += b; + } + eprintln!("quake total: {past} loads past texture memory, {refused_binds} refused binds"); + assert_eq!(past, 0, "a load was refused as past texture memory"); + assert_eq!(refused_binds, 0, "a bind was refused"); + } + + /// A list pull is recorded by its offset in the pool, but the board reads it at + /// the host addresses its map RAM gives: the replay serves it in order, and a + /// read nothing covers is still answered with zeros and counted. + #[test] + fn a_list_pull_is_served_from_the_record_keyed_by_pool_offset() { + let at = Arc::new(AtomicUsize::new(0)); + let reads = vec![(2, 0x1000, vec![1, 2, 3, 4]), (4, 0x80c, vec![9, 8, 7, 6, 5, 4, 3, 2]), (4, 0xc0c, vec![1; 8])]; + let host = TestHost::replaying(reads, at); + let mut word = [0u8; 4]; + host.recorded().read_into(0x1000, &mut word); + assert_eq!(word, [1, 2, 3, 4]); + let mut pull = [0u8; 8]; + host.recorded().read_into(0x1_03f2_480c, &mut pull); + assert_eq!(pull, [9, 8, 7, 6, 5, 4, 3, 2]); + host.recorded().read_into(0x055f_c00c, &mut pull); + assert_eq!(pull, [1; 8]); + let mut none = [7u8; 8]; + host.recorded().read_into(0x7777, &mut none); + assert_eq!(none, [0; 8]); + assert_eq!(host.recorded().reads.lock().unmatched, 8); + } +} diff --git a/src/dev/kona/back/snapshot.rs b/src/dev/kona/back/snapshot.rs new file mode 100644 index 000000000..b0020acfc --- /dev/null +++ b/src/dev/kona/back/snapshot.rs @@ -0,0 +1,372 @@ +//! The back end's bulk state in the chunk store, for a snapshot. +//! +//! Every allocated word array of the pixel store (the colour, depth, stencil and +//! the other words of a pixel's record, and the display-ID, overlay and popup +//! planes beside them) and the cursor's glyph memory are written as 64 KiB chunks +//! of big-endian words. A chunk of all zeros is not written: its place in the +//! list holds `ZERO`, so a mostly empty array costs a list of markers. An +//! allocated array that is all zero still gets its entry, so a restored store +//! allocates what the saved one had (the store's hash tells an unallocated array +//! from a zero one). +//! +//! Not saved here: the tile ring and the draw-state table (the board is +//! quiesced, so both are empty), the composed frame and the baked display tables +//! (rebuilt from the tables as loaded). The small state (the pipe's registers, +//! the display tables, the contexts) is the machine's `Saveable` part, which +//! this module does not cover. A snapshot loads only into the build that saved it. + +use super::display::tables::Tables; +use super::store::texture::{Page, PAGE_WORDS, PLANES}; +use super::store::{PixelStore, POSITIONS}; +use crate::chunk_store::{ChunkHash, ChunkStore, CHUNK_SIZE}; +use serde::{Deserialize, Serialize}; +use std::io; + +/// The hash a zero chunk's place holds; the store never holds a chunk by it. +pub const ZERO: ChunkHash = [0; 32]; + +/// One saved memory: its name and its chunks, in order. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct KonaChunk { + pub name: String, + pub hashes: Vec, +} + +const WORDS: usize = CHUNK_SIZE / 4; + +/// Bytes as chunks, with a zero chunk's place holding `ZERO`. +pub fn put_bytes(cs: &ChunkStore, bytes: &[u8]) -> io::Result> { + bytes.chunks(CHUNK_SIZE).map(|c| if c.iter().all(|&b| b == 0) { Ok(ZERO) } else { cs.put(c) }).collect() +} + +/// What `put_bytes` wrote, `len` bytes of it. +pub fn get_bytes(cs: &ChunkStore, hashes: &[ChunkHash], len: usize) -> Result, String> { + let mut out = Vec::with_capacity(len); + for (i, h) in hashes.iter().enumerate() { + let want = (len.saturating_sub(i * CHUNK_SIZE)).min(CHUNK_SIZE); + if *h == ZERO { + out.resize(out.len() + want, 0); + } else { + let b = cs.get(h).map_err(|e| format!("chunk {i}: {e}"))?; + if b.len() != want { + return Err(format!("chunk {i} holds {} bytes, not {want}", b.len())); + } + out.extend_from_slice(&b); + } + } + if out.len() != len { + return Err(format!("{} bytes saved, {len} expected", out.len())); + } + Ok(out) +} + +fn put(cs: &ChunkStore, words: &[u32]) -> io::Result> { + let mut bytes = Vec::with_capacity(CHUNK_SIZE); + words + .chunks(WORDS) + .map(|c| { + if c.iter().all(|&w| w == 0) { + return Ok(ZERO); + } + bytes.clear(); + c.iter().for_each(|w| bytes.extend_from_slice(&w.to_be_bytes())); + cs.put(&bytes) + }) + .collect() +} + +fn get(cs: &ChunkStore, hashes: &[ChunkHash], len: usize) -> Result, String> { + // A manifest is data: more or fewer chunks than the array needs is refused before any is read. + if hashes.len() != len.div_ceil(WORDS) { + return Err(format!("{} chunks for {len} words", hashes.len())); + } + let mut words = Vec::with_capacity(len); + for (i, h) in hashes.iter().enumerate() { + let want = (len - i * WORDS).min(WORDS); + if *h == ZERO { + words.resize(words.len() + want, 0); + continue; + } + let bytes = cs.get(h).map_err(|e| format!("chunk {i}: {e}"))?; + if bytes.len() != want * 4 { + return Err(format!("chunk {i} holds {} bytes, not {}", bytes.len(), want * 4)); + } + words.extend(bytes.chunks_exact(4).map(|c| u32::from_be_bytes([c[0], c[1], c[2], c[3]]))); + } + if words.len() != len { + return Err(format!("{} words saved, {len} expected", words.len())); + } + Ok(words) +} + +/// Save the store's allocated arrays and the cursor memory. +pub fn save(store: &PixelStore, tables: &Tables, cs: &ChunkStore) -> io::Result> { + let mut out = Vec::new(); + for w in store.allocated() { + let a = store.array(w).expect("allocated"); + // SAFETY: `i` is below the array's length. + let words: Vec = (0..a.len()).map(|i| unsafe { a.load(i) }).collect(); + out.push(KonaChunk { name: format!("pixel-{w}"), hashes: put(cs, &words)? }); + } + out.push(KonaChunk { name: "cursor-ram".into(), hashes: put(cs, tables.cursor_words())? }); + // Texture memory: each plane that holds pages, by its number and the length of its page table (both in the + // name), one chunk to a page and a zero marker where the table has no page. A page is one chunk long. + let tex = store.texture(); + for (plane, pages) in tex.used() { + let hashes = (0..pages) + .map(|pg| match tex.page(plane, pg) { + Some(p) => Ok(put(cs, p)?.remove(0)), + None => Ok(ZERO), + }) + .collect::>>()?; + out.push(KonaChunk { name: format!("texture-g{plane}-{pages}"), hashes }); + } + Ok(out) +} + +/// A snapshot's bulk state read into memory and checked, ready to apply. +pub struct Prepared { + data: Vec<(usize, Vec)>, + cursor: Vec, + /// Texture memory's planes, by number. + planes: Vec<(u16, Vec>>)>, +} + +/// Read and check every chunk `save` wrote for a store and tables of the same configuration, and +/// change nothing. +pub fn prepare(store: &PixelStore, tables: &Tables, cs: &ChunkStore, list: &[KonaChunk]) -> Result { + let mut arrays = Vec::new(); + let mut cursor = None; + let mut texture = Vec::new(); + for c in list { + if let Some((p, n)) = c.name.strip_prefix("texture-g").and_then(|r| r.split_once('-')).and_then(|(p, n)| Some((p.parse::().ok()?, n.parse::().ok()?))) { + if p as usize >= PLANES || texture.iter().any(|&(q, _, _)| q == p) { + return Err(format!("chunk {} names no texture plane or repeats one", c.name)); + } + if n > store.texture().plane_pages() || c.hashes.len() != n { + return Err(format!("{}: the saved plane is past a bank of one raster manager's memory", c.name)); + } + texture.push((p, n, c)); + continue; + } + match c.name.strip_prefix("pixel-").map(str::parse::) { + Some(Ok(w)) if w < POSITIONS => arrays.push((w, c)), + Some(_) => return Err(format!("chunk {} names no word position", c.name)), + None if c.name == "cursor-ram" => cursor = Some(c), + None => return Err(format!("unknown chunk {}", c.name)), + } + } + let entries = store.width() as usize * store.height() as usize; + let mut data = Vec::new(); + for (w, c) in arrays { + data.push((w, get(cs, &c.hashes, entries).map_err(|e| format!("{}: {e}", c.name))?)); + } + let cursor = match cursor { + Some(c) => get(cs, &c.hashes, tables.cursor_words().len()).map_err(|e| format!("cursor-ram: {e}"))?, + None => vec![0; tables.cursor_words().len()], + }; + let mut planes = Vec::new(); + let mut held = 0; + for (p, n, c) in texture { + let mut pages = Vec::with_capacity(n); + for (i, h) in c.hashes.iter().enumerate() { + if *h == ZERO { + pages.push(None); + continue; + } + let words = get(cs, std::slice::from_ref(h), PAGE_WORDS).map_err(|e| format!("{} page {i}: {e}", c.name))?; + pages.push(Some(Box::::try_from(words.into_boxed_slice()).map_err(|_| "a page is not PAGE_WORDS long".to_string())?)); + held += 1; + } + planes.push((p, pages)); + } + if held > store.texture().cap_pages() { + return Err(format!("{held} texture pages saved, past the {} the product's memory holds", store.texture().cap_pages())); + } + Ok(Prepared { data, cursor, planes }) +} + +/// Put what `prepare` read into the store and tables: every array the snapshot names is allocated +/// and filled, and the others are zeroed, so the store reads as it did when saved. +pub fn apply(store: &PixelStore, tables: &mut Tables, p: Prepared) -> Result<(), String> { + store.reset(); + for (plane, pages) in p.planes { + if !store.texture().set_pages(plane, pages) { + return Err(format!("texture plane {plane}: refused")); + } + } + for (w, words) in p.data { + store.ensure(w); + let a = store.array(w).ok_or("no array after ensure")?; + // SAFETY: the array holds `entries` words. + words.iter().enumerate().for_each(|(i, &v)| unsafe { a.store(i, v) }); + } + tables.load_cursor_words(&p.cursor); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::back::store::{DID_PLANE, POPUP_PLANE}; + use crate::dev::kona::back::KonaBackend; + use crate::dev::kona::KonaConfig; + use crate::dev::kona::tests::ScratchDir; + + /// A chunk store's directory that goes with the test, however it ends. + fn dir(tag: &str) -> ScratchDir { + ScratchDir::new(&format!("snap-{tag}")) + } + + fn board() -> KonaBackend { + KonaBackend::new(KonaConfig { managed: (200, 100), ..KonaConfig::default() }) + } + + /// A board with a few arrays drawn on: colour words, the ID plane, and a zero array. + fn drawn() -> KonaBackend { + let b = board(); + for (w, v) in [(1usize, 0xff12_3456u32), (2, 0xff65_4321)] { + b.store.write(7, 9, w, v); + b.store.write(199, 99, w, v ^ 1); + } + b.store.write(3, 3, DID_PLANE, 13 << 2); + b.store.ensure(POPUP_PLANE); + b + } + + /// Save, load into a fresh board, save again: the store reads the same, the + /// arrays allocated are the same (an allocated zero array too), and the second save + /// writes the same chunk lists. + #[test] + fn a_saved_store_restores_into_a_fresh_board_and_saves_the_same() { + let d = dir("round"); + let cs = ChunkStore::new(d.path()); + let mut a = drawn(); + let first = a.save_chunks(&cs).unwrap(); + let mut b = board(); + b.load_chunks(&cs, &first).unwrap(); + assert_eq!(b.store.checksum(), a.store.checksum()); + assert_eq!(b.store.allocated(), a.store.allocated()); + assert_eq!(b.store.read(7, 9, 1), 0xff12_3456); + assert_eq!(b.save_chunks(&cs).unwrap(), first, "save, load, save"); + } + + /// A chunk of zeros is not written, and its place holds the zero hash. + #[test] + fn zero_chunks_are_not_stored() { + let d = dir("zero"); + let cs = ChunkStore::new(d.path()); + let mut a = drawn(); + let list = a.save_chunks(&cs).unwrap(); + let popup = list.iter().find(|c| c.name == format!("pixel-{POPUP_PLANE}")).unwrap(); + assert!(popup.hashes.iter().all(|h| *h == ZERO), "an all-zero array is all zero markers"); + assert!(!cs.has(&ZERO)); + let colour = list.iter().find(|c| c.name == "pixel-1").unwrap(); + assert!(colour.hashes.iter().any(|h| *h != ZERO) && colour.hashes.iter().all(|h| *h == ZERO || cs.has(h))); + } + + /// A missing chunk is refused and the board is left as it was; so is a name no + /// store position has. + #[test] + fn a_bad_snapshot_leaves_the_board_alone() { + let d = dir("bad"); + let cs = ChunkStore::new(d.path()); + let mut a = drawn(); + let list = a.save_chunks(&cs).unwrap(); + let mut b = board(); + b.store.write(1, 1, 1, 77); + let before = b.store.checksum(); + let gone = list.iter().find(|c| c.name == "pixel-1").unwrap().hashes.iter().find(|h| **h != ZERO).copied().unwrap(); + std::fs::remove_file(cs.path_for(&gone)).unwrap(); + assert!(b.load_chunks(&cs, &list).is_err()); + assert_eq!(b.store.checksum(), before); + let odd = [KonaChunk { name: "pixel-9999".into(), hashes: vec![] }]; + assert!(b.load_chunks(&cs, &odd).unwrap_err().contains("no word position")); + assert!(b.load_chunks(&cs, &[KonaChunk { name: "nonsense".into(), hashes: vec![] }]).is_err()); + } + + /// The cursor memory comes back with its registers: the glyph the host sees is the same. + #[test] + fn the_cursor_memory_is_restored() { + let d = dir("cursor"); + let cs = ChunkStore::new(d.path()); + let mut a = board(); + { + let mut t = a.tables.lock(); + let mut glyph = vec![0u32; 260]; + glyph[0] = 0x8000_0000; + t.load_cursor(260, &glyph); + t.indirect(0x101, 260); + t.indirect(0x102, 34 | 33 << 13); + t.indirect(0x103, 2); + } + let list = a.save_chunks(&cs).unwrap(); + let mut b = board(); + { + // The registers are the small state, saved elsewhere; set as a restore would. + let mut t = b.tables.lock(); + t.indirect(0x101, 260); + t.indirect(0x102, 34 | 33 << 13); + t.indirect(0x103, 2); + } + b.load_chunks(&cs, &list).unwrap(); + assert_eq!(b.tables.lock().snapshot().hw_cursor(), a.tables.lock().snapshot().hw_cursor()); + assert!(b.tables.lock().snapshot().hw_cursor().is_some()); + } + + #[test] + fn texture_memory_is_restored_by_page() { + let d = dir("texture"); + let cs = ChunkStore::new(d.path()); + let mut a = board(); + let t = a.store.texture(); + assert!(t.set(2, 0, 0xff01_0203) && t.set(2, 69_999, 0xff0a_0b0c) && t.set(5, 17, 7)); + let list = a.save_chunks(&cs).unwrap(); + let named = |n: &str| list.iter().find(|c| c.name == n).map(|c| c.hashes.iter().filter(|h| **h != ZERO).count()); + assert_eq!((named("texture-g2-5"), named("texture-g5-1")), (Some(2), Some(1)), "a chunk to a held page, a marker to the rest"); + let mut b = board(); + b.load_chunks(&cs, &list).unwrap(); + let t = b.store.texture(); + assert_eq!((t.used(), t.get(2, 0), t.get(2, 69_999), t.get(5, 17), t.resident_pages()), (vec![(2, 5), (5, 1)], 0xff01_0203, 0xff0a_0b0c, 7, 3)); + assert_eq!(b.save_chunks(&cs).unwrap(), list); + } + + /// A plane that is not one, is repeated or is longer than a bank, a table of the wrong length, pages past the + /// ceiling, and the older layouts' chunks, are refused. + #[test] + fn texture_planes_from_a_snapshot_are_checked() { + let d = dir("texture-planes"); + let cs = ChunkStore::new(d.path()); + let mut a = board(); + assert!(a.store.texture().set(1, 0, 1)); + let list = a.save_chunks(&cs).unwrap(); + let h = put(&cs, &[0; PAGE_WORDS]).unwrap(); + let with = |name: &str, n: usize| -> Vec { list.iter().cloned().chain([KonaChunk { name: name.into(), hashes: h.iter().cycle().take(n).copied().collect() }]).collect() }; + let past = format!("texture-g3-{}", a.store.texture().plane_pages() + 1); + for (name, n) in [("texture-g1-1", 1), (&format!("texture-g{PLANES}-1"), 1), ("texture-g3-2", 1), (past.as_str(), a.store.texture().plane_pages() + 1), ("texture-g9-999999999999", 1), ("texture-p3-8", 1), ("texture-8", 1)] { + assert!(board().load_chunks(&cs, &with(name, n)).is_err(), "{name}"); + } + assert!(board().load_chunks(&cs, &with("texture-g3-1", 1)).is_ok()); + } + + /// The manifest names every chunk the board saved, so the garbage collector keeps + /// them, and not the zero marker. + #[test] + fn the_manifest_keeps_the_boards_chunks_alive() { + use crate::snapshot::ChunksManifest; + let d = dir("gc"); + let cs = ChunkStore::new(d.path()); + let mut a = drawn(); + let list = a.save_chunks(&cs).unwrap(); + let orphan = cs.put(b"nothing refers to this").unwrap(); + let m = ChunksManifest { kona_chunks: list.clone(), ..Default::default() }; + let live: std::collections::HashSet<_> = m.referenced_hashes().copied().collect(); + assert!(!live.contains(&ZERO) && live.len() >= 2); + cs.gc(&live).unwrap(); + assert!(list.iter().flat_map(|c| &c.hashes).all(|h| *h == ZERO || cs.has(h)), "the board's chunks survive"); + assert!(!cs.has(&orphan), "an unreferenced one does not"); + // And the manifest round-trips through postcard as the registry reads it. + let bytes = postcard::to_allocvec(&m).unwrap(); + assert_eq!(postcard::from_bytes::(&bytes).unwrap().kona_chunks, list); + } +} diff --git a/src/dev/kona/back/store/layout.rs b/src/dev/kona/back/store/layout.rs new file mode 100644 index 000000000..79387a57d --- /dev/null +++ b/src/dev/kona/back/store/layout.rs @@ -0,0 +1,376 @@ +//! Where a window's planes sit inside a pixel's words. +//! +//! The buffer-configuration command carries ten words and no raster manager +//! count: level, colour size, index size, double buffer, stereo, sample +//! count, depth bits, stencil bits, accumulation bits and auxiliary buffers. +//! From them the planes follow one another in a pixel's record in this order: +//! colour buffers, depth, 8-bit stencil, multisample colour, accumulation. +//! Sizes are in words; `S` is the sample count. The layout is the same for 1, +//! 2 and 4 raster managers. +//! +//! Grades. The order, the word counts per plane and the worked check below +//! are established from the pipe's firmware as software expectation; that the +//! counts are 32-bit words and that word 0 is the overlay level's colour word +//! are inferred. The unit of the window offset and what sets it beyond its +//! start-up value are unknown; here it is a plain word count that moves every +//! plane's base together. +//! +//! Two readings are the model's own, graded inferred. A sample count of 1 +//! means multisampling is off, the same as 0 (the documents speak of "no +//! multisampling" and of `S` samples, and 1, 4 or 8 are the published +//! counts). A colour-index visual carries no colour size, so it reads as +//! under 12 bits and takes one word per buffer. +//! +//! What the hardware documents do not cover is refused with a reason, not +//! guessed: a depth width other than 16, 24 or 32 bits once rounded up to a +//! multiple of 8, a sample count other than 0, 1, 4 or 8, a stencil width +//! other than 0, 1 or 8, a colour size above 11 bits other than 12 or 16, a +//! level below 0, an auxiliary buffer, and an accumulation buffer on a 16-bit +//! colour size (its words depend on a firmware mode value nothing defines). + +use std::ops::Range; + +/// The first colour word of a level-0 layout at window offset 0. The textport, +/// the X path, the GL path's draw target and the display's compose all use this +/// word and the one after it (the second buffer of a double-buffered window), so +/// that what is drawn is what is shown. +pub const MAIN_COLOUR_WORD: u32 = 1; + +/// The ten words of the command. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BufferConfig { + /// 0 for the main planes, above 0 an overlay. + pub level: i32, + /// Bits per colour component; 0 for a colour-index visual. + pub colour_bits: u32, + /// Bits of colour index; 0 for an RGB visual. + pub index_bits: u32, + pub double_buffer: bool, + pub stereo: bool, + pub samples: u32, + pub depth_bits: u32, + pub stencil_bits: u32, + pub accum_bits: u32, + pub aux: u32, +} + +impl BufferConfig { + pub const WORDS: usize = 10; + + pub fn from_words(w: &[u32; Self::WORDS]) -> BufferConfig { + BufferConfig { + level: w[0] as i32, + colour_bits: w[1], + index_bits: w[2], + double_buffer: w[3] != 0, + stereo: w[4] != 0, + samples: w[5], + depth_bits: w[6], + stencil_bits: w[7], + accum_bits: w[8], + aux: w[9], + } + } +} + +/// Why a configuration has no layout. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// A level below 0. + Underlay, + /// A sample count other than 0, 1, 4 or 8. + Samples, + /// A depth width that rounds up to something other than 16, 24 or 32 bits. + DepthWidth, + /// A stencil width other than 0, 1 or 8. + StencilWidth, + /// An auxiliary buffer: the hardware documents give it no words. + Auxiliary, + /// An accumulation buffer on a 16-bit colour size. + Accumulation16, + /// A colour size of 13 to 15 bits, or above 16. + ColourWidth, + /// More words than the host's budget gives a pixel. + OverBudget, +} + +impl Refusal { + pub const ALL: [Refusal; 8] = [ + Refusal::Underlay, + Refusal::Samples, + Refusal::DepthWidth, + Refusal::StencilWidth, + Refusal::Auxiliary, + Refusal::Accumulation16, + Refusal::ColourWidth, + Refusal::OverBudget, + ]; +} + +// The most words a pixel's record may take, from the host's own budget +// arithmetic. +// +// The host driver, not the board, picks a window's per-pixel class: it counts +// the managed area's pixels (the width rounded up to a multiple of 160, times +// the height), divides a budget of 32-bit words by that count and maps the +// quotient to a class of 64, 32, 16, 8 or 4 words. The budget is +// `rm_count * 5 * 2^22` words on an InfiniteReality. That the budget counts +// 32-bit words is inferred; the worked case agrees with it: one raster manager +// at 1280 x 1024 gives 20,971,520 words over 1,310,720 pixels, 16 words, the +// published 512 bits per pixel. The back end does not report a class (the guest +// derives it from the raster manager count and the managed size); the bound only +// limits the model: a layout that needs more words than it allows is refused. + +/// The widths the host rounds the managed area's width up to. +const WIDTH_UNIT: u32 = 160; + +/// The classes, in words per pixel, largest first. +const CLASSES: [u32; 5] = [64, 32, 16, 8, 4]; + +/// The budget in 32-bit words for `rm_count` raster managers. +fn budget_words(rm_count: u32) -> u64 { + (5u64 << 22) * rm_count as u64 +} + +/// The managed area's pixels as the host counts them. +fn managed_pixels(width: u32, height: u32) -> u64 { + width.div_ceil(WIDTH_UNIT) as u64 * WIDTH_UNIT as u64 * height as u64 +} + +/// The words a pixel's record may take, or 0 when the budget gives less than the +/// smallest class (which the hardware documents do not cover). +pub fn record_words(rm_count: u32, width: u32, height: u32) -> u32 { + let pixels = managed_pixels(width, height); + if pixels == 0 { + return 0; + } + let quotient = budget_words(rm_count) / pixels; + CLASSES.into_iter().find(|&c| quotient >= c as u64).unwrap_or(0) +} + +/// A window's planes as word ranges in a pixel's record. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Layout { + pub colour: Range, + pub depth: Range, + pub stencil: Range, + pub multisample: Range, + pub accumulation: Range, + /// The last word the window uses. + pub last: u32, +} + +impl Layout { + /// Words a pixel's record needs, counting word 0. + pub fn words(&self) -> u32 { + self.last + 1 + } + + /// The layout of `cfg` for a window at `window_offset` words, or why not. + /// `bound` is the most words a pixel may take (`record_words`): + /// a layout needing more is refused. + pub fn new(cfg: &BufferConfig, window_offset: u32, bound: u32) -> Result { + if cfg.level < 0 { + return Err(Refusal::Underlay); + } + if cfg.aux != 0 { + return Err(Refusal::Auxiliary); + } + if cfg.colour_bits >= 12 && !matches!(cfg.colour_bits, 12 | 16) { + return Err(Refusal::ColourWidth); + } + let s = cfg.samples; + if !matches!(s, 0 | 1 | 4 | 8) { + return Err(Refusal::Samples); + } + let multisampled = s > 1; + let depth = if cfg.depth_bits == 0 { 0 } else { cfg.depth_bits.div_ceil(8) * 8 }; + if !matches!(depth, 0 | 16 | 24 | 32) { + return Err(Refusal::DepthWidth); + } + if !matches!(cfg.stencil_bits, 0 | 1 | 8) { + return Err(Refusal::StencilWidth); + } + + // Colour: one word per buffer under 12 bits or at exactly 16, one and a + // half, rounded up, at 12 (other sizes are refused above). Index + // visuals carry no component size and so take one word. + let buffers = 1 + cfg.double_buffer as u32 + 2 * cfg.stereo as u32; + let wide = cfg.colour_bits == 12; + let colour_words = if wide { (buffers * 3).div_ceil(2) } else { buffers }; + let colour_start = if cfg.level == 0 { window_offset + MAIN_COLOUR_WORD } else { 0 }; + let colour = colour_start..colour_start + colour_words; + + // Depth: none without depth; one word without multisampling; with it + // S/2 for 16 bits and 3S/4 for 24 or 32. A 1-bit stencil with no depth + // takes one word. + let depth_words = match (depth, multisampled) { + (0, _) => (cfg.stencil_bits == 1) as u32, + (_, false) => 1, + (16, true) => s / 2, + (_, true) => 3 * s / 4, + }; + let depth_range = colour.end..colour.end + depth_words; + + // 8-bit stencil: max(1, S/4) words. A 1-bit stencil shares the depth word. + let stencil_words = if cfg.stencil_bits == 8 { (s / 4).max(1) } else { 0 }; + let stencil = depth_range.end..depth_range.end + stencil_words; + + // Multisample colour: 3S/4 words under 12 bits or at 16, 3S/2 at 12. + let ms_words = match (multisampled, wide) { + (false, _) => 0, + (true, false) => 3 * s / 4, + (true, true) => 3 * s / 2, + }; + let multisample = stencil.end..stencil.end + ms_words; + + // Accumulation: none without, none in colour-index mode; else 3 words + // under 12 bits, 4 at 12; 16 bits is 2 or 4 by a mode the hardware documents do + // not define. + let accum_words = if cfg.accum_bits == 0 || cfg.index_bits != 0 { + 0 + } else if cfg.colour_bits == 16 { + return Err(Refusal::Accumulation16); + } else if cfg.colour_bits < 12 { + 3 + } else { + 4 + }; + let accumulation = multisample.end..multisample.end + accum_words; + + let last = accumulation.end - 1; + let layout = Layout { colour, depth: depth_range, stencil, multisample, accumulation, last }; + if layout.words() > bound { + return Err(Refusal::OverBudget); + } + Ok(layout) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The worked case: one raster manager, 1280 x 1024, 16 words, 512 bits. + #[test] + fn one_raster_manager_at_1280_by_1024_is_sixteen_words() { + assert_eq!(budget_words(1), 20_971_520); + assert_eq!(managed_pixels(1280, 1024), 1_310_720); + assert_eq!(record_words(1, 1280, 1024), 16); + assert_eq!(record_words(2, 1280, 1024), 32); + assert_eq!(record_words(4, 1280, 1024), 64); + } + + /// The width rounds up to a multiple of 160 before the division, and a + /// quotient between two classes falls to the smaller. + #[test] + fn the_width_rounds_up_and_a_quotient_takes_the_lower_class() { + assert_eq!(managed_pixels(1, 1), 160); + assert_eq!(managed_pixels(161, 1), 320); + assert_eq!(managed_pixels(1920, 1080), 1920 * 1080); + assert_eq!(managed_pixels(1921, 1080), 2080 * 1080); + // 2048 rounds up to 2080: 20,971,520 / 4,259,840 = 4.9, the 4-word class. + assert_eq!(record_words(1, 2048, 2048), 4); + // 1920 x 1200: 20,971,520 / 2,304,000 = 9, so the 8-word class. + assert_eq!(record_words(1, 1920, 1200), 8); + assert_eq!(record_words(1, 0, 1024), 0); + } + + fn cfg(w: [u32; 10]) -> BufferConfig { + BufferConfig::from_words(&w) + } + + /// The hardware document's consistency check: an RGB10 double-buffered pixel with 4 + /// samples, 24 bits of depth, 8 of stencil and accumulation takes colour 2 + /// words (1 and 2), depth 3, stencil 1, multisample colour 3 and + /// accumulation 3, and ends at word 12: 13 words counting word 0. + #[test] + fn the_worked_check_ends_at_word_12() { + let l = Layout::new(&cfg([0, 10, 0, 1, 0, 4, 24, 8, 25, 0]), 0, 16).unwrap(); + assert_eq!(l.colour, 1..3); + assert_eq!(l.depth, 3..6); + assert_eq!(l.stencil, 6..7); + assert_eq!(l.multisample, 7..10); + assert_eq!(l.accumulation, 10..13); + assert_eq!(l.last, 12); + assert_eq!(l.words(), 13); + } + + /// Colour: one word per buffer under 12 bits and at 16, one and a half + /// (rounded up) otherwise; buffers are 1, 2, 3 or 4. + #[test] + fn colour_words_follow_the_size_and_the_buffer_count() { + let words = |rgb, double, stereo| Layout::new(&cfg([0, rgb, 0, double, stereo, 0, 0, 0, 0, 0]), 0, 64).unwrap().colour.len(); + assert_eq!([words(8, 0, 0), words(8, 1, 0), words(8, 0, 1), words(8, 1, 1)], [1, 2, 3, 4]); + assert_eq!([words(16, 0, 0), words(16, 1, 1)], [1, 4]); + assert_eq!([words(12, 0, 0), words(12, 1, 0), words(12, 0, 1), words(12, 1, 1)], [2, 3, 5, 6]); + assert_eq!(words(0, 0, 0), 1, "an index visual takes one word a buffer"); + } + + /// A visual of the kind a posted capture gives: 12-bit colour, double + /// buffered, 23 bits of depth (24 once rounded), 1-bit stencil. + #[test] + fn a_one_bit_stencil_shares_the_depth_word() { + let l = Layout::new(&cfg([0, 12, 0, 1, 0, 0, 23, 1, 0, 0]), 0, 16).unwrap(); + assert_eq!((l.colour.clone(), l.depth.clone(), l.stencil.clone()), (1..4, 4..5, 5..5)); + assert_eq!(l.words(), 5); + // With no depth, the 1-bit stencil takes the word. + let l = Layout::new(&cfg([0, 8, 0, 0, 0, 0, 0, 1, 0, 0]), 0, 16).unwrap(); + assert_eq!((l.colour.clone(), l.depth.clone()), (1..2, 2..3)); + } + + #[test] + fn multisample_words_follow_the_sample_count() { + let l4 = Layout::new(&cfg([0, 8, 0, 0, 0, 4, 16, 8, 0, 0]), 0, 64).unwrap(); + assert_eq!((l4.depth.len(), l4.stencil.len(), l4.multisample.len()), (2, 1, 3)); + let l8 = Layout::new(&cfg([0, 8, 0, 0, 0, 8, 24, 8, 0, 0]), 0, 64).unwrap(); + assert_eq!((l8.depth.len(), l8.stencil.len(), l8.multisample.len()), (6, 2, 6)); + let l12 = Layout::new(&cfg([0, 12, 0, 0, 0, 4, 24, 0, 0, 0]), 0, 64).unwrap(); + assert_eq!(l12.multisample.len(), 6); + } + + /// Accumulation: 3 words under 12 bits, 4 at 12, none in index mode. + #[test] + fn accumulation_words_follow_the_colour_size() { + let acc = |rgb, index| Layout::new(&cfg([0, rgb, index, 0, 0, 0, 0, 0, 25, 0]), 0, 64).unwrap().accumulation.len(); + assert_eq!([acc(8, 0), acc(12, 0), acc(0, 8)], [3, 4, 0]); + } + + /// The window offset moves every plane's base together; above level 0 the + /// colour planes start at word 0. + #[test] + fn the_window_offset_moves_the_planes_together() { + let c = cfg([0, 8, 0, 1, 0, 0, 24, 8, 0, 0]); + let a = Layout::new(&c, 0, 64).unwrap(); + let b = Layout::new(&c, 5, 64).unwrap(); + assert_eq!(b.colour.start, a.colour.start + 5); + assert_eq!((b.depth.start, b.stencil.start, b.last), (a.depth.start + 5, a.stencil.start + 5, a.last + 5)); + let overlay = Layout::new(&cfg([1, 0, 8, 0, 0, 0, 0, 0, 0, 0]), 0, 64).unwrap(); + assert_eq!(overlay.colour, 0..1); + assert_eq!(overlay.words(), 1); + } + + #[test] + fn what_the_documents_do_not_cover_is_refused() { + let r = |w: [u32; 10]| Layout::new(&cfg(w), 0, 64).unwrap_err(); + assert_eq!(r([u32::MAX, 8, 0, 0, 0, 0, 0, 0, 0, 0]), Refusal::Underlay); + assert_eq!(r([0, 8, 0, 0, 0, 2, 0, 0, 0, 0]), Refusal::Samples); + assert_eq!(r([0, 8, 0, 0, 0, 0, 8, 0, 0, 0]), Refusal::DepthWidth); + assert_eq!(r([0, 8, 0, 0, 0, 0, 0, 4, 0, 0]), Refusal::StencilWidth); + assert_eq!(r([0, 8, 0, 0, 0, 0, 0, 0, 0, 1]), Refusal::Auxiliary); + assert_eq!(r([0, 16, 0, 0, 0, 0, 0, 0, 25, 0]), Refusal::Accumulation16); + for bits in [13, 15, 17, 32] { + assert_eq!(r([0, bits, 0, 0, 0, 0, 0, 0, 0, 0]), Refusal::ColourWidth, "{bits}"); + } + } + + /// The host's budget bounds the record: the worked check needs 13 words, + /// which the 16-word class holds and the 8-word class does not. + #[test] + fn a_layout_over_the_budget_is_refused() { + let c = cfg([0, 10, 0, 1, 0, 4, 24, 8, 25, 0]); + assert!(Layout::new(&c, 0, 16).is_ok()); + assert_eq!(Layout::new(&c, 0, 12), Err(Refusal::OverBudget)); + assert_eq!(Layout::new(&c, 0, 0), Err(Refusal::OverBudget)); + } +} diff --git a/src/dev/kona/back/store/mod.rs b/src/dev/kona/back/store/mod.rs new file mode 100644 index 000000000..7ef619a46 --- /dev/null +++ b/src/dev/kona/back/store/mod.rs @@ -0,0 +1,434 @@ +//! The pixel store: one array for the whole screen, held word by word. +//! +//! A pixel owns a record of up to 64 32-bit words (the planes of its window, +//! laid out by `layout`). The store keeps one two-dimensional array of `u32` +//! per word position, one entry per pixel of the managed area, in rows of the +//! managed area's pitch. A fill that touches one or two words of a record +//! then writes contiguous memory, and a window that uses 13 of the 16 words +//! of its class allocates 13 arrays, not 16. +//! +//! The physical address function from pixel and word to memory is unknown and +//! nothing a guest sees depends on it, so it stays behind `word_slot`. Cold +//! paths (read-back, snapshots, region swaps, monitors) use `word_slot`; a +//! draw resolves each array's base pointer and pitch once when it is set up +//! and its kernels index from those. +//! +//! # Allocation and publication +//! +//! An array is allocated whole, never grown, zero-filled, on the raster side +//! at a draw's or clear's set-up and before any tile worker sees the job: the +//! set-up allocates every array the job's planes read or write that is still +//! null (a depth test with depth writes off still reads depth). Each array is +//! published through a fixed slot, an `AtomicPtr` per word position, stored +//! once with release ordering and never changed until the store is dropped. A +//! reset zeroes the arrays in place and frees nothing. A worker loads its base +//! pointers when its job's state is resolved, so a store on the pixel path +//! checks nothing. The display loads a word position once per row run with +//! acquire ordering and treats null as a run of zeros. Because a pointer never +//! moves, a display read that races a worker's write sees the old or the new +//! value, as the lock-free display accepts. +//! +//! The elements are `AtomicU32` and accessed relaxed: a racing read and write +//! is then defined, and costs a plain load or store. + +pub mod layout; +pub mod texture; + +use std::alloc::{alloc_zeroed, dealloc, Layout as MemLayout}; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering::{Acquire, Relaxed, Release}}; + +/// Words in a pixel's record: the largest class. +pub const MAX_WORDS: usize = 64; + +/// Planes held beside the pixel record, each in a position past it: the +/// display-ID plane, whose 13-bit words name a pixel's window for the scan-out, +/// and the three planes the X server draws that are not colour (overlay 0, +/// overlay 1 and popup). Where the board keeps them is not known and nothing a +/// guest sees depends on it; they take positions of their own so a draw +/// resolves them as it does a colour word. +pub const DID_PLANE: usize = MAX_WORDS; +pub const OVERLAY0_PLANE: usize = MAX_WORDS + 1; +pub const OVERLAY1_PLANE: usize = MAX_WORDS + 2; +pub const POPUP_PLANE: usize = MAX_WORDS + 3; +/// Word positions in all: the record's and the planes beside it. +pub const POSITIONS: usize = MAX_WORDS + 4; + +/// One word position's array as a draw resolves it: its base, its length and +/// the pitch of a row. It borrows the store, so it cannot outlive it, and the +/// array never moves while the store lives. +#[derive(Clone, Copy)] +pub struct WordArray<'a> { + base: *const AtomicU32, + len: usize, + pitch: usize, + _store: PhantomData<&'a PixelStore>, +} + +// SAFETY: the base points into an array the store owns until it drops, and +// `'a` keeps the store borrowed; the elements are atomics. +unsafe impl Send for WordArray<'_> {} +unsafe impl Sync for WordArray<'_> {} + +impl WordArray<'_> { + /// The word of the pixel at `index` (row * pitch + column). + /// + /// # Safety + /// + /// `index` must be less than `len()`. A draw meets this by clipping to the + /// managed area before it indexes, so the pixel path carries no check; a + /// debug build asserts it. + #[inline] + pub unsafe fn load(&self, index: usize) -> u32 { + debug_assert!(index < self.len, "pixel index {index} past an array of {}", self.len); + // SAFETY: `index` is in range by the caller's contract, and the array + // is live for `'a`. + unsafe { (*self.base.add(index)).load(Relaxed) } + } + + /// Store `value` in the word of the pixel at `index`. + /// + /// # Safety + /// + /// As `load`: `index` must be less than `len()`. + #[inline] + pub unsafe fn store(&self, index: usize, value: u32) { + debug_assert!(index < self.len, "pixel index {index} past an array of {}", self.len); + // SAFETY: as `load`. + unsafe { (*self.base.add(index)).store(value, Relaxed) } + } + + /// Entries in the array: pitch times rows. + pub fn len(&self) -> usize { + self.len + } + + pub fn pitch(&self) -> usize { + self.pitch + } +} + +pub struct PixelStore { + width: usize, + height: usize, + slots: [AtomicPtr; POSITIONS], + /// Texture memory, which a draw reads beside the planes. + tex: texture::TextureStore, +} + +impl PixelStore { + /// A store for a managed area of `width` x `height` pixels. Nothing is + /// allocated until a word position is first written. + #[cfg(test)] + pub fn new(width: u32, height: u32) -> PixelStore { + Self::with_texture(width, height, Default::default()) + } + + /// A store whose texture memory is `tex`: a product's own size. + pub fn with_texture(width: u32, height: u32, tex: texture::TextureStore) -> PixelStore { + PixelStore { width: width as usize, height: height as usize, slots: std::array::from_fn(|_| AtomicPtr::new(std::ptr::null_mut())), tex } + } + + /// Texture memory. + pub fn texture(&self) -> &texture::TextureStore { + &self.tex + } + + pub fn width(&self) -> u32 { + self.width as u32 + } + + pub fn height(&self) -> u32 { + self.height as u32 + } + + /// Entries in one word position's array. + fn entries(&self) -> usize { + self.width * self.height + } + + /// Where the word `w` of pixel (`x`, `y`) lives: the word position and the + /// index in its array. The one definition of the address function; None + /// outside the managed area or past the record. + pub fn word_slot(&self, x: u32, y: u32, w: usize) -> Option<(usize, usize)> { + (w < POSITIONS && (x as usize) < self.width && (y as usize) < self.height).then(|| (w, y as usize * self.width + x as usize)) + } + + /// Allocate word position `w`'s array if it is still null, zero-filled, and + /// publish it. The set-up step of a draw or clear calls this for every word + /// its planes touch, before any worker runs. Safe from more than one + /// thread: the loser of a race frees its array. + pub fn ensure(&self, w: usize) { + assert!(w < POSITIONS); + if !self.slots[w].load(Acquire).is_null() { + return; + } + let n = self.entries(); + if n == 0 { + return; + } + let layout = MemLayout::array::(n).expect("the managed area fits memory"); + // SAFETY: the layout has a nonzero size; an all-zero bit pattern is a + // valid `AtomicU32`. + let p = unsafe { alloc_zeroed(layout) } as *mut AtomicU32; + assert!(!p.is_null(), "pixel store allocation of {} bytes failed", layout.size()); + if self.slots[w].compare_exchange(std::ptr::null_mut(), p, Release, Acquire).is_err() { + // SAFETY: allocated just above with this layout and never shared. + unsafe { dealloc(p as *mut u8, layout) }; + } + } + + /// The array at word position `w`, for a draw that resolves its state once; + /// None while it is unallocated (every entry reads zero). + pub fn array(&self, w: usize) -> Option> { + let base = self.slots[w].load(Acquire); + (!base.is_null()).then_some(WordArray { base, len: self.entries(), pitch: self.width, _store: PhantomData }) + } + + /// Word `w` of pixel (`x`, `y`): zero while its array is unallocated or + /// the pixel is outside the managed area. A cold path. + pub fn read(&self, x: u32, y: u32, w: usize) -> u32 { + let Some((w, i)) = self.word_slot(x, y, w) else { return 0 }; + // SAFETY: `word_slot` bounds the index to the managed area. + self.array(w).map_or(0, |a| unsafe { a.load(i) }) + } + + #[cfg(test)] + /// Write word `w` of pixel (`x`, `y`), allocating its array. False outside + /// the managed area or past the record. A cold path. + pub fn write(&self, x: u32, y: u32, w: usize, value: u32) -> bool { + let Some((w, i)) = self.word_slot(x, y, w) else { return false }; + self.ensure(w); + // SAFETY: `word_slot` bounds the index to the managed area. + unsafe { self.array(w).expect("ensured").store(i, value) }; + true + } + + /// The word positions that have an array. + pub fn allocated(&self) -> Vec { + (0..POSITIONS).filter(|&w| !self.slots[w].load(Acquire).is_null()).collect() + } + + /// A hash of the store's contents, to compare a store with a recorded or + /// remembered one: FNV-1a over each allocated word position's index and + /// entries. An unallocated position and an all-zero one differ, so replays + /// that allocate the same arrays compare equal. + pub fn checksum(&self) -> u64 { + const PRIME: u64 = 0x0000_0100_0000_01b3; + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + let mut mix = |v: u64| h = (h ^ v).wrapping_mul(PRIME); + for w in self.allocated() { + mix(w as u64 | 1 << 32); + let a = self.array(w).expect("allocated"); + // SAFETY: `i` is below `len()`. + (0..a.len()).for_each(|i| mix(unsafe { a.load(i) } as u64)); + } + h + } + + /// Zero every allocated array in place. Frees nothing and moves nothing, + /// so a worker's or the display's pointer stays good. Each entry is a + /// relaxed atomic store rather than a `memset`, which may write a word in + /// pieces: a reader racing a reset must see the old value or zero, never a + /// torn one. It is a cold path. + pub fn reset(&self) { + self.tex.reset(); + for w in self.allocated() { + let a = self.array(w).expect("allocated"); + // SAFETY: the array has `a.len()` entries and is live for `&self`. + let all = unsafe { std::slice::from_raw_parts(a.base, a.len()) }; + all.iter().for_each(|e| e.store(0, Relaxed)); + } + } +} + +impl Drop for PixelStore { + fn drop(&mut self) { + let n = self.entries(); + for s in &self.slots { + let p = s.load(Relaxed); + if !p.is_null() { + // SAFETY: allocated in `ensure` with this layout and freed once, + // here; `&mut self` means no draw or reader holds it. + unsafe { dealloc(p as *mut u8, MemLayout::array::(n).expect("sized when allocated")) }; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicBool, AtomicU64}; + use std::sync::Arc; + + #[test] + fn nothing_is_allocated_until_a_word_is_written() { + let s = PixelStore::new(64, 16); + assert!(s.allocated().is_empty()); + assert_eq!(s.read(3, 4, 0), 0); + assert!(s.allocated().is_empty(), "a read allocates nothing"); + assert!(s.write(3, 4, 5, 0xdead_beef)); + assert_eq!(s.allocated(), [5]); + assert_eq!(s.read(3, 4, 5), 0xdead_beef); + assert_eq!(s.read(4, 4, 5), 0); + assert_eq!(s.read(3, 4, 6), 0); + } + + #[test] + fn word_slot_is_row_major_by_word_and_bounded() { + let s = PixelStore::new(10, 4); + assert_eq!(s.word_slot(0, 0, 0), Some((0, 0))); + assert_eq!(s.word_slot(3, 2, 7), Some((7, 23))); + assert_eq!(s.word_slot(10, 0, 0), None); + assert_eq!(s.word_slot(0, 4, 0), None); + assert_eq!(s.word_slot(0, 0, POSITIONS), None); + assert!(!s.write(10, 0, 0, 1)); + assert_eq!(s.read(10, 0, 0), 0); + } + + /// A draw resolves the base once and indexes from it; a clear is + /// contiguous in one array. + #[test] + fn a_resolved_array_reads_and_writes_by_index() { + let s = PixelStore::new(8, 8); + for w in [1, 2] { + s.ensure(w); + } + let a = s.array(1).unwrap(); + assert_eq!(a.pitch(), 8); + for i in 0..64 { + // SAFETY: i < 64 = len. + unsafe { a.store(i, i as u32 + 1) }; + } + assert_eq!(s.read(2, 3, 1), 3 * 8 + 2 + 1); + assert_eq!(s.read(2, 3, 2), 0); + assert!(s.array(3).is_none()); + } + + /// The pointer of an array never moves: a second `ensure` keeps it, and a + /// reset zeroes in place. + #[test] + fn an_array_never_moves_and_reset_zeroes_in_place() { + let s = PixelStore::new(4, 4); + s.ensure(9); + let before = s.array(9).unwrap().base; + s.write(1, 1, 9, 7); + s.ensure(9); + assert_eq!(s.array(9).unwrap().base, before); + s.reset(); + assert_eq!(s.array(9).unwrap().base, before); + assert_eq!(s.read(1, 1, 9), 0); + assert_eq!(s.allocated(), [9]); + } + + /// A reader racing the first write sees zero or the value, never anything + /// else, whatever the stage of the allocation. The reader runs until it + /// has seen the value, with a bound on the iterations. A smoke check: the + /// release and acquire pairing is argued in the module doc, and the test + /// runs on whatever the host's memory model allows. + #[test] + fn a_reader_racing_a_first_write_sees_zero_or_the_value() { + const VALUE: u32 = 0x1234_5678; + for _ in 0..200 { + let s = Arc::new(PixelStore::new(256, 64)); + let go = Arc::new(AtomicBool::new(false)); + let reader = { + let (s, go) = (s.clone(), go.clone()); + std::thread::spawn(move || { + while !go.load(Relaxed) { + std::hint::spin_loop(); + } + let mut seen = false; + for _ in 0..50_000_000u32 { + let v = s.read(200, 40, 3); + assert!(v == 0 || v == VALUE, "read {v:#x}"); + if v == VALUE { + seen = true; + break; + } + std::thread::yield_now(); + } + assert!(seen, "the value never showed"); + }) + }; + go.store(true, Relaxed); + assert!(s.write(200, 40, 3, VALUE)); + reader.join().unwrap(); + } + } + + /// A reset zeroes in place while a reader runs: the reader sees the value + /// or zero, never a torn word, and its pointer stays good. + #[test] + fn a_reset_racing_a_reader_leaves_zero_or_the_value() { + const VALUE: u32 = 0xa5a5_5a5a; + let s = Arc::new(PixelStore::new(128, 128)); + for x in 0..128 { + s.write(x, 7, 2, VALUE); + } + let stop = Arc::new(AtomicBool::new(false)); + let passes = Arc::new(AtomicU64::new(0)); + let reader = { + let (s, stop, passes) = (s.clone(), stop.clone(), passes.clone()); + std::thread::spawn(move || { + while !stop.load(Relaxed) { + for x in 0..128 { + let v = s.read(x, 7, 2); + assert!(v == 0 || v == VALUE, "read {v:#x}"); + } + passes.fetch_add(1, Relaxed); + } + }) + }; + for _ in 0..200 { + s.reset(); + for x in 0..128 { + s.write(x, 7, 2, VALUE); + } + } + // The writer can finish before the reader thread is scheduled: let it make one pass before it is stopped. + while passes.load(Relaxed) == 0 && !reader.is_finished() { + std::thread::yield_now(); + } + stop.store(true, Relaxed); + reader.join().unwrap(); + assert!(passes.load(Relaxed) > 0); + } + + #[test] + fn the_checksum_follows_contents_and_allocation() { + let a = PixelStore::new(8, 8); + let b = PixelStore::new(8, 8); + assert_eq!(a.checksum(), b.checksum()); + a.write(1, 1, 3, 5); + assert_ne!(a.checksum(), b.checksum()); + b.write(1, 1, 3, 5); + assert_eq!(a.checksum(), b.checksum()); + b.write(2, 2, 3, 0); + assert_eq!(a.checksum(), b.checksum(), "writing a zero changes nothing"); + b.ensure(9); + assert_ne!(a.checksum(), b.checksum(), "an allocated array of zeros counts"); + a.reset(); + assert_ne!(a.checksum(), PixelStore::new(8, 8).checksum()); + } + + /// Two threads allocating the same word position end with one array. + #[test] + fn two_threads_ensuring_one_word_share_one_array() { + for _ in 0..100 { + let s = Arc::new(PixelStore::new(32, 32)); + let ts: Vec<_> = (0..4) + .map(|_| { + let s = s.clone(); + std::thread::spawn(move || { + s.ensure(0); + s.array(0).unwrap().base as usize + }) + }) + .collect(); + let bases: Vec<_> = ts.into_iter().map(|t| t.join().unwrap()).collect(); + assert!(bases.windows(2).all(|p| p[0] == p[1])); + } + } +} diff --git a/src/dev/kona/back/store/texture.rs b/src/dev/kona/back/store/texture.rs new file mode 100644 index 000000000..a496881ec --- /dev/null +++ b/src/dev/kona/back/store/texture.rs @@ -0,0 +1,444 @@ +//! Texture memory: texels held as the model chooses, one 32-bit word each, at the address +//! a load names. +//! +//! The board's texture memory is not guest-visible except through loads and +//! binds, and the order and width of a texel inside it is not known for any +//! format, so the model keeps its own layout: one word per texel, `0xAARRGGBB`, +//! 8 bits a component, rows of a level contiguous at the level's pitch. A format +//! narrower than that (5-5-5-1, 4-4-4-4) is reduced to its width at load, so the +//! texel shows what the board would hold. +//! +//! The pipe has no allocator of its own: a load writes where its address words say, a level +//! reloaded at the same place overwrites it, and one a later load overlaps is gone +//! (established). A level is addressed by its bank and an address in units of 256 bits, relative +//! to the block the kernel gave its owner. The store follows that: a level at address `a` of a +//! bank starts at word `a * UNIT_WORDS` of that bank's plane, so there is nothing to place and +//! nothing to fragment, and a load is refused only when its address, with the level's extent, +//! lies outside the bank (`contains`) or would take the store past its ceiling (`fits`). Both +//! refusals are the model's own: the hardware has nothing to refuse with, and a client that goes +//! past its block is the kernel's to prevent. A unit holds sixteen 16-bit lanes, so `UNIT_WORDS` +//! words is room for the most texels a unit can carry, and levels whose units do not overlap +//! never share a word. +//! +//! A plane is one bank of one owner's block: the owner's two banks, and two more for its detail +//! images, which the model keeps apart from the base levels (inferred). The block offsets the +//! kernel writes to the global-offset registers on a reserve are not modelled, so every context +//! has planes of its own, each as large as a bank, where the board has one pool the owners' blocks +//! share. That is approximated: contexts neither contend for memory nor alias, so a texture +//! loaded through one member of a share group (contexts the kernel lets see the same physical +//! block) is not visible to a bind from another member. +//! +//! A plane is sparse: it is a table of fixed pages of `PAGE_WORDS` words (64 KiB), and a page +//! exists once a nonzero word is written into it, so host memory follows what was written, not +//! how far the highest address reaches; a word in a page that does not exist reads as zero. The +//! pages held across every plane may not pass `cap_pages`, the most texels the product's texture +//! memory can carry (a lane is the least a texel takes, and the model keeps a word for it), which +//! is what the real pool cannot exceed whichever owners hold it. Per context planes would +//! otherwise let a guest that loads at high addresses in many contexts take gigabytes of host memory. +//! +//! The producer (`kona-hip`) writes only after the tiles are drained, and a draw's workers only +//! read, so a plane may change under `UnsafeCell` without a lock: no reader runs while it changes. + +use crate::dev::kona::defs::CONTEXT_SLOTS; +use std::cell::UnsafeCell; + +/// Words of a plane a unit of 256 bits (sixteen 16-bit lanes) addresses: the most texels it can carry. +pub const UNIT_WORDS: u32 = 16; +/// Bytes a unit of 256 bits holds. +const UNIT_BYTES: u64 = 32; +/// The planes a context owns: its two banks and its detail images' two. +pub const PLANES_PER_CONTEXT: usize = 4; +/// Every context slot and the no-context state have their planes. +pub const PLANES: usize = (CONTEXT_SLOTS + 1) * PLANES_PER_CONTEXT; +/// Words of a page: 64 KiB, which is one chunk of a snapshot. +pub const PAGE_WORDS: usize = 1 << 14; +const PAGE_SHIFT: u32 = 14; + +/// A page of a plane. +pub type Page = [u32; PAGE_WORDS]; + +fn new_page() -> Box { + // A zeroed vector is a lazily mapped allocation, so the page costs host memory when it is written. + vec![0u32; PAGE_WORDS].into_boxed_slice().try_into().expect("a page is PAGE_WORDS long") +} + +/// One plane: its pages by index, a missing page being zero. Trailing missing pages are not kept. +#[derive(Default)] +pub struct Plane { + pages: Vec>>, +} + +impl Plane { + /// Word `at`; 0 where no page is. + #[inline(always)] + pub fn get(&self, at: u32) -> u32 { + let at = at as usize; + match self.pages.get(at >> PAGE_SHIFT) { + Some(Some(p)) => p[at & (PAGE_WORDS - 1)], + _ => 0, + } + } + + fn held(&self) -> usize { + self.pages.iter().flatten().count() + } + + fn has(&self, page: usize) -> bool { + matches!(self.pages.get(page), Some(Some(_))) + } + + fn trim(&mut self) { + while matches!(self.pages.last(), Some(None)) { + self.pages.pop(); + } + } +} + +static NO_PLANE: Plane = Plane { pages: Vec::new() }; + +#[derive(Default)] +struct Planes { + planes: Vec, + /// Pages held across every plane, and the most there have been since power-on. + resident: usize, + peak: usize, +} + +pub struct TextureStore { + inner: UnsafeCell, + /// Units of 256 bits one bank of one raster manager's texture memory holds. + bank_units: u32, + /// Pages the planes together may hold. + cap_pages: usize, +} + +impl Default for TextureStore { + /// A 64 MB raster manager's texture memory: the InfiniteReality2's, which a store has unless its + /// product says otherwise. + fn default() -> Self { + Self::with_capacity_bytes(64 << 20) + } +} + +// SAFETY: see the module doc: writes happen only while no worker runs (the producer +// drains first), and reads never overlap them. +unsafe impl Sync for TextureStore {} + +impl TextureStore { + /// A store for a raster manager whose texture memory is `bytes` long, in two banks. + pub fn with_capacity_bytes(bytes: u64) -> Self { + let bank_units = (bytes / 2 / UNIT_BYTES).min(u32::MAX as u64 / UNIT_WORDS as u64) as u32; + // A lane of two bytes is the least a texel takes, and a word stands for it. A pool is at least a page + // to each bank, so a toy store can hold both. + let cap_pages = ((bytes / 2).div_ceil(PAGE_WORDS as u64)).max(2) as usize; + TextureStore { inner: UnsafeCell::new(Planes::default()), bank_units, cap_pages } + } + + /// Units of 256 bits one bank holds. + #[cfg(test)] + pub fn bank_units(&self) -> u32 { + self.bank_units + } + + fn bank_words(&self) -> usize { + self.bank_units as usize * UNIT_WORDS as usize + } + + /// Pages the planes together may hold: the product's texture memory, a word to a 16-bit lane. + pub fn cap_pages(&self) -> usize { + self.cap_pages + } + + /// Whether `units` units from `address` lie inside a bank. + pub fn contains(&self, address: u32, units: u64) -> bool { + address as u64 + units <= self.bank_units as u64 + } + + /// Whether writing `rows` runs of `run` words, `stride` apart from word `start` of plane `plane`, would + /// leave the store within its ceiling. Pages already held cost nothing, and a run is taken to need + /// its pages though a zero word allocates none, so a yes is a guarantee. Rows must not overlap. + pub fn fits(&self, plane: u16, start: u64, run: u64, stride: u64, rows: u64) -> bool { + // SAFETY: as for `plane`. + let st = unsafe { &*self.inner.get() }; + let pl = st.planes.get(plane as usize).unwrap_or(&NO_PLANE); + let (bank, mut missing, mut next) = (self.bank_words() as u64, 0usize, 0usize); + for r in 0..rows { + let first = start + r * stride; + if run == 0 || first >= bank { + break; + } + let last = (first + run - 1).min(bank - 1); + let (p0, p1) = ((first >> PAGE_SHIFT) as usize, (last >> PAGE_SHIFT) as usize); + for pg in p0.max(next)..=p1 { + missing += !pl.has(pg) as usize; + } + next = next.max(p1 + 1); + if st.resident + missing > self.cap_pages { + return false; + } + } + st.resident + missing <= self.cap_pages + } + + /// Zero the `n` words of plane `plane` from `from`. A page the range covers is given back, a page it + /// part covers is zeroed in the range, and none is made. A range past the bank is cut at its end + /// (`contains` is the caller's check). Call only with the tiles drained. + pub fn clear(&self, plane: u16, from: u32, n: usize) { + // SAFETY: no reader runs (the caller drained the tiles). + let st = unsafe { &mut *self.inner.get() }; + let end = (from as usize + n).min(self.bank_words()); + let from = from as usize; + let Some(pl) = st.planes.get_mut(plane as usize).filter(|_| from < end) else { return }; + let mut freed = 0; + for pg in from >> PAGE_SHIFT..=(end - 1) >> PAGE_SHIFT { + let (lo, hi) = (pg << PAGE_SHIFT, (pg + 1) << PAGE_SHIFT); + let Some(slot) = pl.pages.get_mut(pg) else { break }; + if from <= lo && hi <= end { + freed += slot.take().is_some() as usize; + } else if let Some(p) = slot { + p[from.max(lo) - lo..end.min(hi) - lo].fill(0); + } + } + pl.trim(); + st.resident -= freed; + } + + /// Give plane `plane` back: its words are gone and its memory is returned. Call only with the tiles drained. + pub fn drop_plane(&self, plane: u16) { + // SAFETY: as for `clear`. + let st = unsafe { &mut *self.inner.get() }; + if let Some(pl) = st.planes.get_mut(plane as usize) { + st.resident -= pl.held(); + pl.pages = Vec::new(); + } + } + + /// Write word `at` of plane `plane`. False when the word is not written: past the bank, or the page it + /// needs would pass the ceiling. A zero into a page that is not held writes nothing and is true. Call only + /// with the tiles drained. + pub fn set(&self, plane: u16, at: u32, v: u32) -> bool { + // SAFETY: as for `clear`. + let st = unsafe { &mut *self.inner.get() }; + if plane as usize >= PLANES || at as usize >= self.bank_words() { + return false; + } + let (pg, i) = (at as usize >> PAGE_SHIFT, at as usize & (PAGE_WORDS - 1)); + if st.planes.len() <= plane as usize { + st.planes.resize_with(PLANES, Plane::default); + } + let pl = &mut st.planes[plane as usize]; + if !pl.has(pg) { + if v == 0 { + return true; + } + if st.resident >= self.cap_pages { + return false; + } + if pl.pages.len() <= pg { + pl.pages.resize_with(pg + 1, || None); + } + pl.pages[pg] = Some(new_page()); + st.resident += 1; + st.peak = st.peak.max(st.resident); + } + if let Some(Some(p)) = pl.pages.get_mut(pg) { + p[i] = v; + } + true + } + + /// Word `at` of plane `plane`; 0 where nothing has been written. + #[inline(always)] + pub fn get(&self, plane: u16, at: u32) -> u32 { + self.plane(plane).get(at) + } + + /// Plane `plane`, so a loop over one level's texels reaches them without a lookup of the plane each. + #[inline(always)] + pub fn plane(&self, plane: u16) -> &Plane { + // SAFETY: readers run while nothing writes. + unsafe { &*self.inner.get() }.planes.get(plane as usize).unwrap_or(&NO_PLANE) + } + + /// The planes that hold pages, in order, with the length of each one's page table. + pub fn used(&self) -> Vec<(u16, usize)> { + // SAFETY: as for `plane`. + unsafe { &*self.inner.get() }.planes.iter().enumerate().filter(|(_, d)| d.held() > 0).map(|(i, d)| (i as u16, d.pages.len())).collect() + } + + /// Page `page` of plane `plane`, if it is held. + pub fn page(&self, plane: u16, page: usize) -> Option<&Page> { + self.plane(plane).pages.get(page)?.as_deref() + } + + /// Pages a plane can have: those a bank's words fill. + pub fn plane_pages(&self) -> usize { + self.bank_words().div_ceil(PAGE_WORDS) + } + + /// Take `pages` as plane `plane`, as a snapshot saved them. False, and nothing changed, for a plane the + /// store has not, a table longer than a bank, or pages that would take the store past its ceiling. Call + /// only with the tiles drained. + pub fn set_pages(&self, plane: u16, pages: Vec>>) -> bool { + // SAFETY: as for `clear`. + let st = unsafe { &mut *self.inner.get() }; + if plane as usize >= PLANES || pages.len() > self.plane_pages() { + return false; + } + if st.planes.len() <= plane as usize { + st.planes.resize_with(PLANES, Plane::default); + } + let mut pl = Plane { pages }; + pl.trim(); + let (old, new) = (st.planes[plane as usize].held(), pl.held()); + if st.resident - old + new > self.cap_pages { + return false; + } + st.resident = st.resident - old + new; + st.peak = st.peak.max(st.resident); + st.planes[plane as usize] = pl; + true + } + + /// Pages held across all planes. + pub fn resident_pages(&self) -> usize { + // SAFETY: as for `plane`. + unsafe { &*self.inner.get() }.resident + } + + /// The most pages held at once since power-on. + pub fn peak_pages(&self) -> usize { + // SAFETY: as for `plane`. + unsafe { &*self.inner.get() }.peak + } + + /// Forget every image (power-on). Call only with the tiles drained. + pub fn reset(&self) { + // SAFETY: as for `clear`. + let st = unsafe { &mut *self.inner.get() }; + st.planes.clear(); + st.resident = 0; + st.peak = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_word_is_where_its_address_puts_it_zeroed_and_bounded() { + let t = TextureStore::default(); + assert!(t.set(0, 17, 0xdead_beef) && t.set(1, 3, 7)); + assert_eq!((t.resident_pages(), t.used()), (2, vec![(0, 1), (1, 1)])); + assert_eq!((t.get(0, 17), t.get(0, 16), t.get(0, 99), t.get(1, 3), t.get(7, 0)), (0xdead_beef, 0, 0, 7, 0)); + assert!(t.set(0, 20, 0) && t.set(5, 4, 0), "a zero into a page that is not held writes nothing"); + assert_eq!(t.resident_pages(), 2); + assert!(!t.set(0, t.bank_units() * UNIT_WORDS, 1), "past the bank"); + t.clear(0, 17, 1); + assert_eq!(t.get(0, 17), 0, "cleared again"); + t.drop_plane(0); + assert_eq!((t.used(), t.resident_pages()), (vec![(1, 1)], 1)); + t.reset(); + assert_eq!((t.resident_pages(), t.peak_pages()), (0, 0)); + } + + /// A page is made by the first nonzero word in it: a single word at the top of the bank holds one page, not a bank. + #[test] + fn a_word_at_the_top_of_a_bank_holds_one_page() { + let t = TextureStore::default(); + let top = t.bank_units() * UNIT_WORDS - 1; + assert!(t.set(3, top, 9)); + assert_eq!((t.resident_pages(), t.get(3, top), t.get(3, top - 1)), (1, 9, 0)); + assert!(t.page(3, t.plane_pages() - 1).is_some() && t.page(3, 0).is_none()); + t.clear(3, 0, usize::MAX / 2); + assert_eq!(t.resident_pages(), 0, "a clear over the plane gives the pages back"); + } + + /// A clear gives back only the pages it covers and zeroes the rest in range, making none. + #[test] + fn a_clear_covers_whole_pages_and_part_pages() { + let t = TextureStore::default(); + let w = PAGE_WORDS as u32; + for pg in 0..3 { + assert!(t.set(0, pg * w + 5, 1)); + } + t.clear(0, 4, PAGE_WORDS + 3); + assert_eq!((t.resident_pages(), t.get(0, 5), t.get(0, w + 5), t.get(0, 2 * w + 5)), (3, 0, 0, 1), "pages 0 and 1 are only part covered"); + t.clear(0, 0, 2 * PAGE_WORDS); + assert_eq!((t.resident_pages(), t.used()), (1, vec![(0, 3)])); + t.clear(1, 0, 10); + assert_eq!(t.resident_pages(), 1, "a clear makes nothing"); + } + + /// The pages held across all planes stay within the ceiling, `fits` says so beforehand and `set` refuses past it. + #[test] + fn the_planes_together_stay_within_the_ceiling() { + // 4 MiB of texture memory: 2 Mi words of lanes, 128 pages; a bank is 64 pages. + let t = TextureStore::with_capacity_bytes(4 << 20); + assert_eq!((t.cap_pages(), t.plane_pages()), (128, 64)); + let w = PAGE_WORDS as u64; + for (plane, pages) in [(0u16, 64u32), (1, 63)] { + for pg in 0..pages { + assert!(t.set(plane, pg * PAGE_WORDS as u32, 1), "plane {plane} page {pg}"); + } + } + assert_eq!(t.resident_pages(), 127); + assert!(t.fits(2, 0, 1, 1, 1) && !t.fits(2, 0, 2 * w, 2 * w, 1) && t.fits(0, 0, 64 * w, 64 * w, 1), "pages held cost nothing"); + assert!(t.set(2, 0, 1) && !t.set(2, PAGE_WORDS as u32, 1), "the 129th page is refused"); + assert!(t.set(2, 5, 2), "a page already held takes more words"); + assert!(!t.fits(1, 63 * w, 1, 1, 1)); + assert_eq!(t.peak_pages(), 128); + t.drop_plane(0); + assert!(t.fits(2, w, 1, 1, 1) && t.set(2, PAGE_WORDS as u32, 1)); + } + + /// Rows that share a page cost it once. + #[test] + fn rows_that_share_a_page_cost_it_once() { + // A bank of 3 pages and a pool of 6, with 4 held. + let t = TextureStore::with_capacity_bytes(12 * PAGE_WORDS as u64); + assert_eq!((t.plane_pages(), t.cap_pages()), (3, 6)); + for (plane, pages) in [(1u16, 3u32), (2, 1)] { + for pg in 0..pages { + assert!(t.set(plane, pg * PAGE_WORDS as u32, 1)); + } + } + let w = PAGE_WORDS as u64; + // 64 rows of 4 words, 256 apart, are all in page 0. + assert!(t.fits(0, 0, 4, 256, 64)); + assert!(t.fits(0, 0, 1, w, 2) && !t.fits(0, 0, 1, w, 3)); + } + + /// Snapshots set a plane whole and refuse one past a bank or the ceiling. + #[test] + fn a_plane_from_a_snapshot_is_checked() { + let t = TextureStore::with_capacity_bytes(4 << 20); + let page = || { + let mut p = new_page(); + p[2] = 3; + Some(p) + }; + assert!(t.set_pages(3, vec![None, page()])); + assert_eq!((t.used(), t.get(3, PAGE_WORDS as u32 + 2), t.resident_pages()), (vec![(3, 2)], 3, 1)); + assert!(!t.set_pages(3, (0..t.plane_pages() + 1).map(|_| None).collect()), "a table past the bank"); + assert!(!t.set_pages(PLANES as u16, vec![page()]), "no such plane"); + assert!(t.set_pages(4, (0..64).map(|_| page()).collect()) && t.set_pages(5, (0..63).map(|_| page()).collect())); + assert!(!t.set_pages(6, vec![page(), page()]), "past the ceiling"); + assert!(t.set_pages(4, vec![]) && t.set_pages(5, vec![])); + assert_eq!((t.used(), t.resident_pages()), (vec![(3, 2)], 1), "refused: unchanged"); + assert!(t.set_pages(3, vec![]) && t.used().is_empty()); + } + + /// A product with more texture memory holds addresses a smaller one refuses. + #[test] + fn the_capacity_is_the_products() { + let small = TextureStore::default(); + let big = TextureStore::with_capacity_bytes(256 << 20); + assert_eq!((small.bank_units(), big.bank_units()), (1 << 20, 4 << 20)); + assert!(!small.contains(1 << 20, 1) && big.contains(1 << 20, 1)); + assert!(!big.contains(4 << 20, 1)); + assert_eq!((small.cap_pages(), big.cap_pages()), (2048, 8192)); + assert_eq!(TextureStore::with_capacity_bytes(64).cap_pages(), 2, "at least a page to a bank"); + } +} diff --git a/src/dev/kona/back/switch_tests.rs b/src/dev/kona/back/switch_tests.rs new file mode 100644 index 000000000..d77a5fa77 --- /dev/null +++ b/src/dev/kona/back/switch_tests.rs @@ -0,0 +1,181 @@ +//! Texture loads across a context switch, through the whole board: the +//! command stream, the contexts the pipe keeps and the real back end. +//! +//! Two clients share the pipe and the kernel switches between them in the +//! middle of their loads. Each load's rows and its identifier belong to the +//! context that sent them, whatever the other one has in flight. + +use super::gl::texture::ReadHeader; +use crate::dev::kona::tests::{Fixture, KICK}; + +const REASON_PAGE: u64 = 0x30_0000; +const SAVE_A: u64 = 0x50_0000; +const SAVE_B: u64 = 0x51_0000; +const RETURN_A: u64 = 0x52_0000; +const RETURN_B: u64 = 0x53_0000; +/// Both clients use the same load identifier and the same level address. +const IDENTIFIER: u32 = 5; +const BOTH: u32 = 0x3_0000; + +/// A 2 x 2 RGBA level, one per client, so a texel read back says whose it is. +const TEXELS_A: [u32; 4] = [0xff00_00ff, 0x00ff_00ff, 0x0000_ffff, 0xffff_ffff]; +const TEXELS_B: [u32; 4] = [0x1122_33ff, 0x4455_66ff, 0x7788_99ff, 0xaabb_ccff]; + +/// The header of an inline 2 x 2 load of level address 0x1000; word 6 asks for the rows by DMA. +fn header(dma: bool) -> [u32; 20] { + let mut h = [0u32; 20]; + h[..5].copy_from_slice(&[0x0de1, 2, 2, 5, 1]); + h[6] = dma as u32; + h[9] = 0x30; + h[13] = 1; + h[14] = 1; + h[16] = 0x11; + h[17] = 0x2200; + h[18] = 0x1000; + h +} + +impl Fixture { + fn phys(addr: u64) -> u32 { + 0x1000_0000 | (addr >> 12) as u32 + } + + /// A first activation with its return page, as the kernel does it, then back to user commands. + fn activate_new(&self, save: u64, ret: u64) { + self.w(KICK, BOTH); + self.cmd(&[0x1fd, Self::phys(save), 0, 0x10000]); + self.return_page(ret); + self.w(KICK, 0); + } + + /// Switch the current context out and the saved one back, as the kernel does it. + fn switch_out(&self, index: u32) { + self.w(KICK, BOTH); + self.cmd(&[0x1fc, index]); + self.w(KICK, 0); + } + + fn restore(&self, save: u64) { + self.w(KICK, BOTH); + self.cmd(&[0x1f9, 0x4b4f_4e41, Self::phys(save), 0, 0xeded_eded]); + self.w(KICK, 0); + } + + /// The load's header and its first two texel words. + fn load_start(&self, texels: &[u32; 4]) { + self.cmd(&[0x174]); + self.cmd(&header(false)); + self.cmd(&texels[..2]); + } + + /// The rest of the rows, which complete the load. + fn load_rows(&self, texels: &[u32; 4]) { + self.cmd(&texels[2..]); + } + + fn load_publish(&self) { + self.cmd(&[0x176, IDENTIFIER]); + } + + /// The level context `slot` holds at the test address, as colour. + fn level(&self, slot: usize) -> Option> { + let mut a = [0u32; 18]; + a[..5].copy_from_slice(&[0x0de1, 2, 2, 5, 1]); + a[14] = 0x11; + a[16] = 0x1000; + let h = ReadHeader::parse(&a).unwrap(); + self.kona.with_backend(|b| b.contexts.select(Some(slot)).tex.read_level(b.store.texture(), &h)) + } + + fn expect_level(&self, slot: usize, texels: &[u32; 4]) { + let want: Vec<[f32; 4]> = texels.iter().map(|t| t.to_be_bytes().map(|c| c as f32 / 255.0)).collect(); + assert_eq!(self.level(slot), Some(want), "context {slot}'s level"); + } + + fn texture_loads(&self) -> u64 { + self.kona.with_backend(|b| b.tex_counts.loads) + } +} + +fn started() -> Fixture { + let f = Fixture::new(); + f.reason_page(REASON_PAGE); + f +} + +/// Two contexts load a level each, same address, different texels, and the +/// kernel switches between them in the middle of both loads with rows still +/// owed. Each load completes on its own context. +#[test] +fn loads_interleave_across_switches() { + let f = started(); + f.activate_new(SAVE_A, RETURN_A); + f.load_start(&TEXELS_A); + f.switch_out(1); + f.activate_new(SAVE_B, RETURN_B); + f.load_start(&TEXELS_B); + f.switch_out(2); + f.restore(SAVE_A); + f.load_rows(&TEXELS_A); + f.switch_out(1); + f.restore(SAVE_B); + f.load_rows(&TEXELS_B); + f.load_publish(); + f.switch_out(2); + f.restore(SAVE_A); + f.load_publish(); + f.expect_level(0, &TEXELS_A); + f.expect_level(1, &TEXELS_B); + assert_eq!(f.texture_loads(), 2); +} + +/// A's load is complete and waits for its identifier when A is switched out; +/// B is in the middle of a load, rows still owed, when A returns. A's +/// identifier finds A's level, and B's rows still finish B's own load. +#[test] +fn a_finished_load_survives_another_contexts_open_load() { + let f = started(); + f.activate_new(SAVE_A, RETURN_A); + f.load_start(&TEXELS_A); + f.load_rows(&TEXELS_A); + f.switch_out(1); + f.activate_new(SAVE_B, RETURN_B); + f.load_start(&TEXELS_B); + f.switch_out(2); + f.restore(SAVE_A); + f.load_publish(); + f.expect_level(0, &TEXELS_A); + f.switch_out(1); + f.restore(SAVE_B); + f.load_rows(&TEXELS_B); + f.load_publish(); + f.expect_level(0, &TEXELS_A); + f.expect_level(1, &TEXELS_B); +} + +/// A's load takes its rows by DMA and is switched out before the packet; B +/// loads a level of its own meanwhile. A's packet, when A is back, fills A's +/// level and leaves B's alone. +#[test] +fn a_dma_load_switched_out_before_its_packet_beside_another_context() { + let f = started(); + let source = 0x3c_0000u64; + let rows: Vec = (0..16u8).map(|k| 0x10 + k).collect(); + f.host.put(source, &rows); + f.activate_new(SAVE_A, RETURN_A); + f.cmd(&[0x174]); + f.cmd(&header(true)); + f.switch_out(1); + f.activate_new(SAVE_B, RETURN_B); + f.load_start(&TEXELS_B); + f.load_rows(&TEXELS_B); + f.switch_out(2); + f.restore(SAVE_A); + f.w(0x80007c, 0x1eb); + for w in [Fixture::phys(source), 0x9000 | (source & 0xfff) as u32, 16, 16, 0, 0x100] { + f.w(0x80007c, w); + } + f.w(KICK, 0); + f.expect_level(0, &[0x1011_1213, 0x1415_1617, 0x1819_1a1b, 0x1c1d_1e1f]); + f.expect_level(1, &TEXELS_B); +} diff --git a/src/dev/kona/back/textport.rs b/src/dev/kona/back/textport.rs new file mode 100644 index 000000000..ade4c281e --- /dev/null +++ b/src/dev/kona/back/textport.rs @@ -0,0 +1,208 @@ +//! The textport: the PROM's text console, drawn before any window exists. +//! +//! Commands, after the textport's sync word: 2 sets the draw colour, 4 the +//! raster position, 5 fills a box, 6 plots a point and a bitmap blit draws a +//! glyph or icon at the raster position, and 3 loads the textport's colour map +//! (an index and three 10-bit components). 1, 7 and 8 are framed and counted, +//! with no drawing role known. 9 and 0xc are answered by the front end. +//! +//! Grades. The command meanings are software expectations; a textport pixel is +//! a colour index (inferred from command 2 taking an index, 0 to 255). The +//! pixels go to the colour word of a single-buffered level-0 window, word 1 of +//! the record: no buffer configuration precedes the console, so this default is +//! the model's. A box lights columns and rows by the fill rule. A bitmap's +//! header is width, height, x origin, y origin, x advance and y advance, the +//! last four signed 16-bit; pixel (column c, row r) lands at (raster x - x +//! origin + c, raster y - y origin + r), row 0 lowest, and the raster position +//! then advances by the advances. That placement is approximated: the model's +//! reading, matched to the glyph routines that send it, not confirmed. + +use crate::dev::kona::back::display::ColourMap; +use crate::dev::kona::defs::*; +use crate::dev::kona::back::raster::Surface; +use parking_lot::Mutex; +use std::sync::Arc; + +/// The word of a pixel's record the textport draws into: the main colour word of +/// the planes' layout, which the display composes and the GL path draws. +pub const COLOUR_WORD: usize = crate::dev::kona::back::store::layout::MAIN_COLOUR_WORD as usize; + +#[derive(Debug, Default)] +pub struct Textport { + colour: u32, + raster: (i32, i32), + /// Shared with the display, which copies it once a frame. + colour_map: Arc>, + /// Commands that drew, and those accepted without effect. + pub drawn: u64, + pub unmodelled: u64, +} + +impl Textport { + /// A textport drawing with `map` as its colour map. + pub fn with_map(map: Arc>) -> Textport { + Textport { colour_map: map, ..Textport::default() } + } + + pub fn colour(&self) -> u32 { + self.colour + } + + pub fn raster(&self) -> (i32, i32) { + self.raster + } + + #[cfg(test)] + /// The colour map a textport pixel (an index) is resolved through at + /// scan-out. + pub fn colour_map(&self) -> ColourMap { + self.colour_map.lock().clone() + } + + /// The colour, the raster position and the colour map, as the stream module reads and + /// writes them. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { colour: _, raster: _, colour_map: _, drawn: _, unmodelled: _ } = self; + s.u32(&mut self.colour); + s.i32(&mut self.raster.0); + s.i32(&mut self.raster.1); + self.colour_map.lock().visit(s); + } + + /// Power-on state. + pub fn reset(&mut self) { + let map = self.colour_map.clone(); + map.lock().reset(); + *self = Textport::with_map(map); + } + + /// A textport command. False if `selector` is not one. + /// `surface` makes the surface a paint draws on, only when one does. + pub fn command<'a>(&mut self, surface: impl FnOnce() -> Surface<'a>, selector: u32, args: &[u32]) -> bool { + let a = |i: usize| args.get(i).copied().unwrap_or(0); + match selector { + TEXTPORT_SET_COLOUR => self.colour = a(0) & 0xff, + TEXTPORT_SET_RASTER_POSITION => self.raster = (a(0) as i32, a(1) as i32), + TEXTPORT_FILL_BOX => { + surface().fill_box(a(0) as i32 as i64, a(1) as i32 as i64, a(2) as i32 as i64, a(3) as i32 as i64, self.colour); + self.drawn += 1; + } + TEXTPORT_PLOT_POINT => { + surface().plot(a(0) as i32 as i64, a(1) as i32 as i64, self.colour); + self.drawn += 1; + } + TEXTPORT_LOAD_COLOUR_MAP => self.colour_map.lock().load(args), + TEXTPORT_UNMODELLED_1 | TEXTPORT_UNMODELLED_7 | TEXTPORT_UNMODELLED_8 => self.unmodelled += 1, + _ => return false, + } + true + } + + /// A bitmap blit: six header words, then the bits. + pub fn bitmap(&mut self, surface: &Surface<'_>, header: &[u32], payload: &[u32]) { + let [w, h, xo, yo, xa, ya] = header else { return }; + let (xo, yo, xa, ya) = (*xo as i16 as i64, *yo as i16 as i64, *xa as i16 as i64, *ya as i16 as i64); + let (rx, ry) = (self.raster.0 as i64, self.raster.1 as i64); + surface.bitmap(rx - xo, ry - yo, *w, *h, payload, self.colour); + // The position advances with wrapping 32-bit arithmetic: its range is + // unknown. + self.raster = ((rx + xa) as i32, (ry + ya) as i32); + self.drawn += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::back::store::PixelStore; + + + /// What the tests draw on: the store's colour word, with nothing queued. + fn sf<'a>(s: &'a PixelStore) -> impl FnOnce() -> Surface<'a> { + move || Surface::new(s, COLOUR_WORD) + } + + fn tp() -> (Textport, PixelStore) { + (Textport::default(), PixelStore::new(32, 32)) + } + + #[test] + fn colour_then_a_box_draws_the_index() { + let (mut t, s) = tp(); + assert!(t.command(sf(&s), 2, &[0x1ff])); + assert_eq!(t.colour(), 0xff, "an index is a byte"); + assert!(t.command(sf(&s), 5, &[1, 1, 4, 3])); + assert_eq!(s.read(2, 2, COLOUR_WORD), 0xff); + assert_eq!(s.read(1, 2, COLOUR_WORD), 0); + assert_eq!(s.read(4, 3, COLOUR_WORD), 0xff); + assert_eq!(s.allocated(), [COLOUR_WORD]); + } + + /// A colour-map load reaches the map the display resolves through. + #[test] + fn a_colour_map_load_sets_an_entry() { + let (mut t, s) = tp(); + assert!(t.command(sf(&s), 3, &[7, 1020, 512, 0])); + assert_eq!(t.colour_map().get(7), 0xff00_80ff); + assert_eq!(t.unmodelled, 0); + } + + #[test] + fn a_point_and_the_raster_position() { + let (mut t, s) = tp(); + t.command(sf(&s), 2, &[9]); + t.command(sf(&s), 6, &[7, 8]); + t.command(sf(&s), 4, &[10, 20]); + assert_eq!(s.read(7, 8, COLOUR_WORD), 9); + assert_eq!(t.raster(), (10, 20)); + assert_eq!(t.drawn, 1, "the position draws nothing"); + } + + /// A box with negative corners (sent as 32-bit words) is clipped. + #[test] + fn corners_are_signed() { + let (mut t, s) = tp(); + t.command(sf(&s), 2, &[1]); + t.command(sf(&s), 5, &[-5i32 as u32, -5i32 as u32, 2, 2]); + assert_eq!(s.read(0, 0, COLOUR_WORD), 1); + assert_eq!(s.read(2, 2, COLOUR_WORD), 1); + assert_eq!(s.read(3, 3, COLOUR_WORD), 0); + } + + /// A glyph lands at the raster position minus its origin, and the position + /// advances by the advance. + #[test] + fn a_bitmap_lands_relative_to_the_raster_position_and_advances_it() { + let (mut t, s) = tp(); + t.command(sf(&s), 2, &[5]); + t.command(sf(&s), 4, &[10, 10]); + // 2 x 2, origin (-1, 0): column 0 at x = 11; advance (3, 0). + t.bitmap(&Surface::new(&s, COLOUR_WORD), &[2, 2, -1i32 as u32, 0, 3, 0], &[0b10_01u32 << 28]); + assert_eq!(s.read(11, 10, COLOUR_WORD), 5); + assert_eq!(s.read(12, 10, COLOUR_WORD), 0); + assert_eq!(s.read(12, 11, COLOUR_WORD), 5); + assert_eq!(s.read(11, 11, COLOUR_WORD), 0); + assert_eq!(t.raster(), (13, 10)); + // A header of the wrong length is ignored. + let before = (t.colour(), t.raster(), t.drawn, t.unmodelled); + t.bitmap(&Surface::new(&s, COLOUR_WORD), &[1, 1], &[0]); + assert_eq!((t.colour(), t.raster(), t.drawn, t.unmodelled), before); + } + + #[test] + fn commands_with_no_drawing_role_are_counted_and_others_refused() { + let (mut t, s) = tp(); + for sel in [1, 7, 8] { + assert!(t.command(sf(&s), sel, &[0, 0, 0, 0])); + } + assert_eq!(t.unmodelled, 3); + assert!(!t.command(sf(&s), 9, &[])); + assert!(!t.command(sf(&s), 0xc, &[])); + assert!(s.allocated().is_empty()); + t.command(sf(&s), 3, &[1, 400, 0, 0]); + t.reset(); + assert_eq!((t.colour(), t.raster(), t.drawn, t.unmodelled), (0, (0, 0), 0, 0)); + assert_eq!(t.colour_map(), ColourMap::default(), "a reset clears the shared map too"); + } +} diff --git a/src/dev/kona/back/tiles.rs b/src/dev/kona/back/tiles.rs new file mode 100644 index 000000000..db1005411 --- /dev/null +++ b/src/dev/kona/back/tiles.rs @@ -0,0 +1,765 @@ +//! KONA's tiles: the screen cut into square tiles, one owner each, and the +//! scheduler that gives each tile's owner the raster jobs that touch it. +//! +//! The producer (`kona-hip`, through the raster stage) writes each job into the +//! next slot of a ring and appends the slot's index to the queue of every tile +//! the job's bounding box touches; the slot counts +//! those tiles. A tile with work goes on a ready queue. A worker takes a tile +//! from it and owns the tile until its queue is empty, running each command +//! clipped to the tile's rectangle, in the order the jobs were written. +//! The producer retires slots whose count has reached zero, in order, and +//! writes them again only then. +//! +//! The rules (they are the screen-tile rules VPro's fragment pipeline follows; +//! nothing here is VPro's code, and VPro is untouched): +//! +//! - One owner per tile is the only ordering. A command must go to every tile +//! it can touch, so its bounding box must contain every pixel it writes: a +//! box too small lets a pixel be written out of order. The scheduler cannot +//! check this; the pixel module's tests do. +//! - The pixel module clips to the tile it is given and keeps the window's own +//! grid (the caller's quad grid, in the raster stage), so a tiled run is +//! bit-exact with an inline one: `threads == 0` runs each job +//! immediately over the whole screen, which is the inline run to compare with. +//! - A slot is not written again while any tile queue can name it. A tile's +//! queue holds a slot's index until a worker takes it, and the slot's count +//! drops only after the worker has finished running it, so a count of zero +//! means no queue and no worker has the slot. +//! - The slot is the job ring: it holds a `RasterJob` for good, plain data the +//! producer fills in place (`submit_with`), so a draw allocates nothing. A job +//! names the draw state it runs under by an index into the draw-state table +//! (`state`); an entry is not written again while any job in flight names it. +//! - A work budget bounds what is in flight: the producer waits, retiring as it +//! goes, while the commands' estimated costs (pixels) exceed it, and resumes +//! at half. +//! - A panic in a worker poisons the scheduler: the slot still retires, so a +//! drain finishes, and every later `submit_with` and `drain` says so. +//! +//! Tiles are a plain grid from the screen's origin. The wrapping 4096-pixel +//! grid VPro's multi-window planes use is not needed: the managed area is at +//! most 4096 wide. + +use super::raster::pixel; +use super::raster::setup::TriJob; +use super::raster::{ClearReq, DrawState, Rect}; +use super::super::handle::DisplayHandle; +use super::store::PixelStore; +use crossbeam_queue::ArrayQueue; +use parking_lot::{Condvar, Mutex}; +use std::cell::UnsafeCell; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering::*}; +use std::sync::Arc; +use std::thread::JoinHandle; + +/// A clear, under draw state `state`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ClearJob { + pub req: ClearReq, + pub state: u16, +} + +/// What a slot of the ring holds: plain data with no pointers, naming its draw +/// state by index. `RasterJob::default` is the empty one. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum RasterJob { + Tri(TriJob), + Clear(ClearJob), +} + +impl Default for RasterJob { + fn default() -> Self { + RasterJob::Clear(ClearJob::default()) + } +} + +impl RasterJob { + /// Where the command can write. + pub fn rect(&self) -> Rect { + match self { + RasterJob::Tri(t) => t.rect, + RasterJob::Clear(c) => c.req.rect, + } + } + + /// The draw-state table entry it runs under. + pub fn state(&self) -> u16 { + match self { + RasterJob::Tri(t) => t.state, + RasterJob::Clear(c) => c.state, + } + } + + /// An estimate of the work in pixels, for the tiles' budget. + pub fn cost(&self) -> u64 { + let r = self.rect(); + (r.x1 - r.x0).max(0) as u64 * (r.y1 - r.y0).max(0) as u64 + } +} + +/// A tile size or ring size that is not a power of two (or a ring under two slots). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Invalid { + Tile(u32), + Ring(usize), +} + +/// A panic in a worker poisoned the tiles. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Poisoned; + +/// What the tiles counted. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TileStats { + pub submitted: u64, + pub retired: u64, + /// Jobs run by a tile owner, summed over tiles. + pub tile_runs: u64, + /// The most cost in flight at once, and times the producer waited for + /// the ring or the budget. + pub peak_cost: u64, + pub waits: u64, + /// Times something that touches the store asked for every job to finish, + /// and those that found jobs still queued, which is what a drain costs. + pub drains: u64, + pub busy_drains: u64, +} + +struct Slot { + job: UnsafeCell, + /// Tiles that have yet to finish running the job. + pending: AtomicU32, +} + +// SAFETY: the producer alone writes `job`, and only while `pending` is zero and +// no tile queue holds the slot's index (the slot has retired, or is new); +// workers only read it, and only between taking the index from a queue and +// decrementing `pending`. `pending` is the hand-over: Release by the worker +// after its last read, Acquire by the producer before it writes again. +unsafe impl Sync for Slot {} + +/// One entry of the draw-state table: written by the producer while no job in +/// flight names it, read by the workers while one does. +struct StateCell(UnsafeCell); + +// SAFETY: as for `Slot`: the producer writes an entry only when its count of +// jobs in flight, which it keeps itself and decrements after the slot's +// Acquire load of `pending`, is zero; workers read it only while a job that +// names it is between a tile queue and its `pending` decrement. +unsafe impl Sync for StateCell {} + +/// Entries in the draw-state table. +const STATES: usize = 64; + +struct Tile { + /// The slots to run, in order: a ring the producer appends to at `tail` + /// and the owner takes from at `head`. It has a place for every slot, and a + /// slot is named at most once, so it never overflows. + queue: Box<[AtomicU32]>, + head: AtomicU32, + tail: AtomicU32, + /// On the ready queue, or owned by a worker. + scheduled: AtomicBool, + rect: Rect, +} + +impl Tile { + /// Producer: append a slot's index. + fn push(&self, slot: u32) { + let t = self.tail.load(Relaxed); + self.queue[t as usize & (self.queue.len() - 1)].store(slot, Relaxed); + self.tail.store(t.wrapping_add(1), Release); + } + + /// Owner: take the next slot's index. + fn pop(&self) -> Option { + let h = self.head.load(Relaxed); + if h == self.tail.load(Acquire) { + return None; + } + let slot = self.queue[h as usize & (self.queue.len() - 1)].load(Relaxed); + self.head.store(h.wrapping_add(1), Relaxed); + Some(slot) + } + + fn is_empty(&self) -> bool { + self.head.load(Relaxed) == self.tail.load(Acquire) + } +} + +struct Shared { + store: Arc, + slots: Vec, + states: Vec, + tiles: Vec, + ready: ArrayQueue, + stop: AtomicBool, + poisoned: AtomicBool, + sleepers: AtomicUsize, + gate: Mutex<()>, + wake: Condvar, + tile_runs: AtomicU64, +} + +impl Shared { + /// Wake a sleeping worker, if one sleeps. + fn notify(&self) { + if self.sleepers.load(SeqCst) > 0 { + let _g = self.gate.lock(); + self.wake.notify_one(); + } + } + + fn run_tile(&self, t: &Tile) { + loop { + while let Some(s) = t.pop() { + let slot = &self.slots[s as usize]; + // SAFETY: the slot's index came off a tile queue, so its count is + // above zero until this tile decrements it below. + let job = unsafe { &*slot.job.get() }; + // SAFETY: the entry the job names stays as it is until the job retires. + let ds = unsafe { &*self.states[job.state() as usize].0.get() }; + if catch_unwind(AssertUnwindSafe(|| pixel::run(&self.store, job, ds, t.rect))).is_err() { + self.poisoned.store(true, SeqCst); + } + self.tile_runs.fetch_add(1, Relaxed); + slot.pending.fetch_sub(1, Release); + } + t.scheduled.store(false, SeqCst); + // A job pushed after the last pop and before the store above saw + // the tile scheduled and left it to us. + if t.is_empty() || t.scheduled.swap(true, SeqCst) { + return; + } + } + } + + fn worker(&self) { + let mut idle = 0u32; + loop { + if let Some(t) = self.ready.pop() { + idle = 0; + self.run_tile(&self.tiles[t as usize]); + continue; + } + if self.stop.load(SeqCst) { + return; + } + idle += 1; + if idle < 200 { + std::hint::spin_loop(); + continue; + } + self.sleepers.fetch_add(1, SeqCst); + { + let mut g = self.gate.lock(); + if self.ready.is_empty() && !self.stop.load(SeqCst) { + self.wake.wait(&mut g); + } + } + self.sleepers.fetch_sub(1, SeqCst); + idle = 0; + } + } +} + +pub struct Tiles { + shared: Arc, + workers: Vec>, + screen: Rect, + tile: i32, + cols: i32, + mask: u64, + /// Producer-only: slots written, slots retired, each slot's cost and + /// draw-state entry, the cost in flight, the budget and whether the + /// producer is waiting for half of it. + head: u64, + tail: u64, + costs: Vec, + slot_state: Vec, + /// Jobs in flight naming each draw-state entry, and one more for the entry + /// the producer last set (`held`), which the next `state` call can reuse. + refs: Vec, + held: Option, + inflight: u64, + budget: u64, + throttled: bool, + /// With no workers, the job to run at once. + scratch: RasterJob, + stats: TileStats, + /// Told the producer is working while it waits, so the driver's watchdog + /// does not take a long drain for a dead pipe. + handle: Option, +} + +impl Tiles { + /// Tiles for a `store`'s screen cut into `tile`-pixel squares (a power of + /// two), `threads` workers (0: inline), `ring` slots (a power of two) and a + /// budget in pixels of cost. + pub fn new(store: Arc, tile: u32, threads: usize, ring: usize, budget: u64, handle: Option) -> Result { + if !tile.is_power_of_two() { + return Err(Invalid::Tile(tile)); + } + if !ring.is_power_of_two() || ring < 2 { + return Err(Invalid::Ring(ring)); + } + let (w, h) = (store.width() as i32, store.height() as i32); + let t = tile as i32; + let (cols, rows) = ((w + t - 1) / t, (h + t - 1) / t); + let tiles: Vec = (0..rows * cols) + .map(|i| { + let (x0, y0) = ((i % cols) * t, (i / cols) * t); + Tile { + queue: (0..ring).map(|_| AtomicU32::new(0)).collect(), + head: AtomicU32::new(0), + tail: AtomicU32::new(0), + scheduled: AtomicBool::new(false), + rect: Rect { x0, y0, x1: (x0 + t).min(w), y1: (y0 + t).min(h) }, + } + }) + .collect(); + let ready = ArrayQueue::new(tiles.len().max(1)); + let slots = (0..ring).map(|_| Slot { job: UnsafeCell::new(RasterJob::default()), pending: AtomicU32::new(0) }).collect(); + let states = (0..STATES).map(|_| StateCell(UnsafeCell::new(DrawState::default()))).collect(); + let shared = Arc::new(Shared { store, slots, states, tiles, ready, stop: AtomicBool::new(false), poisoned: AtomicBool::new(false), sleepers: AtomicUsize::new(0), gate: Mutex::new(()), wake: Condvar::new(), tile_runs: AtomicU64::new(0) }); + let workers = (0..threads) + .map(|n| { + let s = shared.clone(); + std::thread::Builder::new().name(format!("kona-tile-{n}")).spawn(move || s.worker()).expect("a thread") + }) + .collect(); + Ok(Tiles { shared, workers, screen: Rect { x0: 0, y0: 0, x1: w, y1: h }, tile: t, cols, mask: ring as u64 - 1, head: 0, tail: 0, costs: vec![0; ring], slot_state: vec![0; ring], refs: vec![0; STATES], held: None, inflight: 0, budget, throttled: false, scratch: RasterJob::default(), stats: TileStats::default(), handle }) + } + + pub fn stats(&self) -> TileStats { + TileStats { retired: self.tail, tile_runs: self.shared.tile_runs.load(Relaxed), ..self.stats } + } + + /// Workers asleep on the wake condition, for the tests of that path. + #[cfg(test)] + fn sleeping(&self) -> usize { + self.shared.sleepers.load(SeqCst) + } + + pub fn is_poisoned(&self) -> bool { + self.shared.poisoned.load(SeqCst) + } + + /// The producer is waiting on the workers: the driver's watchdog is told. + fn working(&self) { + if let Some(h) = &self.handle { + h.activity(); + } + } + + /// Retire the slots whose tiles are all done, in order. + fn retire(&mut self) { + while self.tail < self.head { + let i = (self.tail & self.mask) as usize; + if self.shared.slots[i].pending.load(Acquire) != 0 { + break; + } + self.inflight -= self.costs[i]; + self.refs[self.slot_state[i] as usize] -= 1; + self.tail += 1; + } + } + + /// Enter `ds` in the draw-state table and return its index, for the jobs + /// that follow to name. The entry last returned is reused when it is the + /// same state. Waits, retiring as it goes, while no entry is free. + pub fn state(&mut self, ds: DrawState) -> u16 { + if let Some(h) = self.held { + // SAFETY: the producer set the entry and nobody writes it but the producer. + if unsafe { *self.shared.states[h as usize].0.get() } == ds { + return h; + } + self.refs[h as usize] -= 1; + self.held = None; + } + let i = loop { + self.retire(); + match self.refs.iter().position(|&r| r == 0) { + Some(i) => break i, + None => { + self.stats.waits += 1; + self.working(); + std::thread::yield_now(); + } + } + }; + // SAFETY: no job in flight names the entry, so no worker reads it. + unsafe { *self.shared.states[i].0.get() = ds }; + self.refs[i] = 1; + self.held = Some(i as u16); + i as u16 + } + + /// Hand `job` to every tile its rectangle touches. Waits, retiring as it + /// goes, while the ring is full or the budget is spent. The draw state it + /// names must have come from `state` and not been replaced since. + pub fn submit(&mut self, job: RasterJob) -> Result<(), Poisoned> { + if self.is_poisoned() { + return Err(Poisoned); + } + self.stats.submitted += 1; + if self.workers.is_empty() { + self.scratch = job; + let r = job.rect().intersect(&self.screen); + if !r.is_empty() { + // SAFETY: the entry is the producer's and nothing else reads it. + let ds = unsafe { &*self.shared.states[job.state() as usize].0.get() }; + pixel::run(&self.shared.store, &self.scratch, ds, self.screen); + } + self.tail = self.stats.submitted; + return Ok(()); + } + self.retire(); + let mut waited = false; + while self.head - self.tail > self.mask || (self.inflight > 0 && self.over_budget()) { + waited = true; + self.working(); + std::thread::yield_now(); + self.retire(); + } + self.stats.waits += waited as u64; + let i = (self.head & self.mask) as usize; + let slot = &self.shared.slots[i]; + // SAFETY: the slot has retired (or never ran): its count is zero, so no + // tile queue names it and no worker reads it. + unsafe { *slot.job.get() = job }; + self.refs[job.state() as usize] += 1; + self.slot_state[i] = job.state(); + let (bounds, cost) = (job.rect().intersect(&self.screen), job.cost()); + self.head += 1; + self.costs[i] = cost; + self.inflight += cost; + self.stats.peak_cost = self.stats.peak_cost.max(self.inflight); + if bounds.is_empty() { + slot.pending.store(0, Relaxed); + self.retire(); + return Ok(()); + } + let (tx0, tx1) = (bounds.x0 / self.tile, (bounds.x1 - 1) / self.tile); + let (ty0, ty1) = (bounds.y0 / self.tile, (bounds.y1 - 1) / self.tile); + slot.pending.store(((tx1 - tx0 + 1) * (ty1 - ty0 + 1)) as u32, Relaxed); + for ty in ty0..=ty1 { + for tx in tx0..=tx1 { + let id = (ty * self.cols + tx) as u32; + let t = &self.shared.tiles[id as usize]; + t.push(i as u32); + if !t.scheduled.swap(true, SeqCst) { + // The ready queue holds each tile at most once and has room for + // every tile, so this cannot fail. + assert!(self.shared.ready.push(id).is_ok(), "the ready queue is full"); + self.shared.notify(); + } + } + } + Ok(()) + } + + /// Over the budget; once over, stay waiting until half of it is free. + fn over_budget(&mut self) -> bool { + if self.inflight > self.budget { + self.throttled = true; + } else if self.inflight <= self.budget / 2 { + self.throttled = false; + } + self.throttled + } + + /// Jobs submitted and not yet retired. + pub fn queued(&mut self) -> u64 { + self.retire(); + // Inline, nothing is ever queued: the tail follows the submissions and the head does not. + self.head.saturating_sub(self.tail) + } + + /// Wait until every job submitted has been run and retired. + pub fn drain(&mut self) -> Result<(), Poisoned> { + self.stats.drains += 1; + self.stats.busy_drains += (self.queued() > 0) as u64; + while self.tail < self.head { + self.working(); + self.retire(); + std::thread::yield_now(); + } + if self.is_poisoned() { + Err(Poisoned) + } else { + Ok(()) + } + } +} + +impl Drop for Tiles { + fn drop(&mut self) { + let _ = self.drain(); + self.shared.stop.store(true, SeqCst); + { + let _g = self.shared.gate.lock(); + self.shared.wake.notify_all(); + } + for w in self.workers.drain(..) { + let _ = w.join(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const W: u32 = 100; + const H: u32 = 70; + + /// The draw state of a draw into word 1 over the whole screen. + fn draw_state() -> DrawState { + DrawState { clip: Rect { x0: 0, y0: 0, x1: W as i32, y1: H as i32 }, colour: [1, 0], colours: 1, ..DrawState::default() } + } + + /// Tiles with the draw state of the above entered. + fn tiles(store: Arc, tile: u32, threads: usize, ring: usize, budget: u64) -> (Tiles, u16) { + let mut t = Tiles::new(store, tile, threads, ring, budget, None).unwrap(); + let s = t.state(draw_state()); + (t, s) + } + + /// A clear of `r` to `colour`, as a job under draw state `state`. + fn clear(r: Rect, colour: u32, state: u16) -> RasterJob { + RasterJob::Clear(ClearJob { req: ClearReq { rect: r, colour: Some(colour), ..Default::default() }, state }) + } + + fn store() -> Arc { + let s = Arc::new(PixelStore::new(W, H)); + s.ensure(1); + s + } + + /// The inline hash of `scene(400)`: a regression net that moves only if the + /// scene or the clear does. + const INLINE_400: u64 = 0xe30a_20fc_d4c9_4467; + + /// An order-sensitive scene of overlapping clears. + fn scene(n: usize) -> Vec<(Rect, u32)> { + let mut seed = 0x1234_5678u64; + let mut next = move |m: i32| { + seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + ((seed >> 33) % m as u64) as i32 + }; + (0..n) + .map(|k| { + let (x0, y0) = (next(110) - 10, next(80) - 10); + (Rect { x0, y0, x1: x0 + next(60) + 1, y1: y0 + next(40) + 1 }, 0xff00_0000 | k as u32) + }) + .collect() + } + + fn run(scene: &[(Rect, u32)], threads: usize, tile: u32, ring: usize, budget: u64) -> (u64, TileStats) { + let store = store(); + let (mut t, s) = tiles(store.clone(), tile, threads, ring, budget); + for (r, c) in scene { + t.submit(clear(*r, *c, s)).unwrap(); + } + t.drain().unwrap(); + let stats = t.stats(); + drop(t); + (store.checksum(), stats) + } + + /// A tiled run is bit-exact with the inline one at every worker count, tile + /// size and ring size, and the scene is order sensitive, so a lost ordering shows. + #[test] + fn a_tiled_run_is_bit_exact_with_the_inline_one() { + let s = scene(400); + let (inline, st) = run(&s, 0, 32, 64, 1 << 20); + assert_eq!(st.submitted, 400); + assert_eq!(inline, INLINE_400, "the pinned inline hash"); + for threads in [1, 2, 4] { + for tile in [8, 16, 32, 64] { + for ring in [4, 64] { + let (h, st) = run(&s, threads, tile, ring, 1 << 14); + assert_eq!(h, inline, "{threads} workers, tile {tile}, ring {ring}"); + assert_eq!(st.retired, 400); + } + } + } + let mut r = s.clone(); + r.reverse(); + assert_ne!(run(&r, 0, 32, 64, 1 << 20).0, inline, "the scene depends on its order"); + } + + /// A job reaches every tile its rectangle touches and no pixel outside it + /// (cut at the screen's edge) is written. + #[test] + fn a_job_reaches_every_tile_it_touches() { + let boxes = [Rect { x0: 0, y0: 0, x1: 1, y1: 1 }, Rect { x0: 15, y0: 15, x1: 17, y1: 17 }, Rect { x0: 10, y0: 60, x1: 100, y1: 70 }, Rect { x0: -50, y0: -50, x1: 500, y1: 500 }, Rect { x0: 32, y0: 0, x1: 48, y1: 16 }, Rect { x0: 200, y0: 0, x1: 300, y1: 10 }]; + for b in boxes { + let store = store(); + let (mut t, s) = tiles(store.clone(), 16, 3, 8, 1 << 20); + t.submit(clear(b, 0xff11_2233, s)).unwrap(); + t.drain().unwrap(); + for y in 0..H as i32 { + for x in 0..W as i32 { + let inside = x >= b.x0 && x < b.x1 && y >= b.y0 && y < b.y1; + assert_eq!(store.read(x as u32, y as u32, 1) != 0, inside, "{b:?} at ({x}, {y})"); + } + } + } + } + + /// Jobs on one tile run in submission order and a slot is not written again under + /// a worker: a two-slot ring and a thousand overlapping jobs leave the last one's colour. + #[test] + fn tiles_run_in_order_and_a_slot_is_not_recycled_under_a_worker() { + let store = store(); + let (mut t, s) = tiles(store.clone(), 16, 2, 2, 1 << 20); + let all = Rect { x0: 0, y0: 0, x1: W as i32, y1: H as i32 }; + for k in 0..1000u32 { + t.submit(clear(all, 0xff00_0000 | k, s)).unwrap(); + } + t.drain().unwrap(); + assert_eq!(t.stats().retired, 1000); + assert!((0..H).all(|y| (0..W).all(|x| store.read(x, y, 1) == 0xff00_0000 | 999))); + } + + /// The cost in flight stays near the budget: the producer waits for work to retire. + #[test] + fn the_budget_bounds_what_is_in_flight() { + let store = store(); + let (mut t, s) = tiles(store, 16, 2, 64, 3000); + let all = Rect { x0: 0, y0: 0, x1: W as i32, y1: H as i32 }; + for k in 0..200u32 { + t.submit(clear(all, k, s)).unwrap(); + } + t.drain().unwrap(); + let st = t.stats(); + assert!(st.peak_cost <= 3000 + (W * H) as u64, "{st:?}"); + assert!(st.waits > 0, "{st:?}"); + } + + /// A job wholly off the screen retires at once, with no tile named. + #[test] + fn a_job_off_the_screen_retires_at_once() { + let store = store(); + let (mut t, s) = tiles(store, 16, 2, 4, 1 << 20); + t.submit(clear(Rect { x0: 500, y0: 500, x1: 600, y1: 600 }, 1, s)).unwrap(); + assert_eq!((t.stats().tile_runs, t.queued()), (0, 0)); + } + + /// A panic in a worker poisons the tiles: the slot still retires, a drain ends and + /// later submissions say so. (A job that names a colour word set-up never allocated + /// panics in the pixel module.) + #[test] + fn a_panicking_job_poisons_the_tiles() { + let store = Arc::new(PixelStore::new(W, H)); + let (mut t, s) = tiles(store, 16, 2, 4, 1 << 20); + t.submit(clear(Rect { x0: 0, y0: 0, x1: 10, y1: 10 }, 1, s)).unwrap(); + assert_eq!(t.drain(), Err(Poisoned)); + assert!(t.is_poisoned()); + assert_eq!(t.submit(RasterJob::default()), Err(Poisoned)); + } + + /// Dropping runs what is queued and joins the workers. + #[test] + fn dropping_runs_what_is_queued_and_joins() { + let store = store(); + let (mut t, s) = tiles(store.clone(), 16, 2, 16, 1 << 20); + let all = Rect { x0: 0, y0: 0, x1: W as i32, y1: H as i32 }; + for k in 0..10u32 { + t.submit(clear(all, 0xff00_0000 | k, s)).unwrap(); + } + drop(t); + assert_eq!(store.read(5, 5, 1), 0xff00_0009); + } + + /// A pool that has gone to sleep is woken by a job. + #[test] + fn a_sleeping_pool_is_woken_by_a_job() { + let store = store(); + let (mut t, s) = tiles(store.clone(), 16, 2, 4, 1 << 20); + let end = std::time::Instant::now() + std::time::Duration::from_secs(10); + while t.sleeping() < 2 && std::time::Instant::now() < end { + std::thread::sleep(std::time::Duration::from_millis(5)); + } + assert_eq!(t.sleeping(), 2, "both workers went to sleep"); + t.submit(clear(Rect { x0: 0, y0: 0, x1: 20, y1: 20 }, 0xff12_3456, s)).unwrap(); + t.drain().unwrap(); + assert_eq!(store.read(3, 3, 1), 0xff12_3456); + } + + /// A retired slot lets go of the draw state its job named, and the same + /// state entered again is the same entry. + #[test] + fn a_retired_slot_lets_go_of_its_draw_state() { + let store = store(); + for threads in [0, 2] { + let (mut t, s) = tiles(store.clone(), 16, threads, 4, 1 << 20); + assert_eq!(t.state(draw_state()), s, "{threads} workers: unchanged state, same entry"); + t.submit(clear(Rect { x0: 0, y0: 0, x1: 20, y1: 20 }, 1, s)).unwrap(); + let other = t.state(DrawState { colours: 0, ..draw_state() }); + t.drain().unwrap(); + // Only the entry set last is held: the job's hold went when it retired. + assert_eq!((t.refs.iter().sum::(), t.refs[other as usize]), (1, 1), "{threads} workers"); + } + } + + /// A draw-state table far smaller than the states used makes the producer + /// wait for entries, and the result is the inline one: a state is not + /// overwritten while a job in flight names it. + #[test] + fn more_states_than_entries_wait_for_free_ones() { + let run = |threads: usize| { + let store = store(); + (2..=3).for_each(|w| store.ensure(w)); + let mut t = Tiles::new(store.clone(), 8, threads, 256, 1 << 20, None).unwrap(); + for k in 0..(4 * STATES as i32) { + // Each state names one of three colour words, and no two in a row are alike. + let s = t.state(DrawState { colour: [1 + (k % 3) as u32, k as u32 + 100], ..draw_state() }); + t.submit(clear(Rect { x0: 0, y0: 0, x1: W as i32, y1: H as i32 }, 0xff00_0000 | k as u32, s)).unwrap(); + } + t.drain().unwrap(); + drop(t); + store.checksum() + }; + assert_eq!(run(0), run(3)); + } + + /// A producer that waits on the workers tells the driver's watchdog the pipe + /// is working: for a full ring, a spent budget, a full state table and a drain. + #[test] + fn a_producer_that_waits_feeds_the_watchdog() { + use std::sync::atomic::Ordering::Relaxed; + let f = crate::dev::kona::tests::Fixture::new(); + let host = f.kona.shared.host.clone(); + let handle = DisplayHandle::new(&host); + let all = Rect { x0: 0, y0: 0, x1: W as i32, y1: H as i32 }; + // A two-slot ring: the third job has to wait for a worker. + let mut t = Tiles::new(store(), 16, 1, 2, 1 << 20, Some(handle.clone())).unwrap(); + let s = t.state(draw_state()); + host.activity.store(false, Relaxed); + for k in 0..64u32 { + t.submit(clear(all, k, s)).unwrap(); + } + t.drain().unwrap(); + assert!(t.stats().waits > 0 && host.activity.load(Relaxed), "{:?}", t.stats()); + // A budget below one job's cost makes the next submission wait. + host.activity.store(false, Relaxed); + let mut t = Tiles::new(store(), 16, 1, 64, 10, Some(handle)).unwrap(); + let s = t.state(draw_state()); + for k in 0..8u32 { + t.submit(clear(all, k, s)).unwrap(); + } + t.drain().unwrap(); + assert!(host.activity.load(Relaxed)); + } + + #[test] + fn a_bad_tile_or_ring_size_is_refused() { + let store = store(); + assert_eq!(Tiles::new(store.clone(), 24, 1, 4, 1, None).err(), Some(Invalid::Tile(24))); + assert_eq!(Tiles::new(store.clone(), 16, 1, 3, 1, None).err(), Some(Invalid::Ring(3))); + assert_eq!(Tiles::new(store, 16, 1, 1, 1, None).err(), Some(Invalid::Ring(1))); + } +} diff --git a/src/dev/kona/back/x/fill.rs b/src/dev/kona/back/x/fill.rs new file mode 100644 index 000000000..239721635 --- /dev/null +++ b/src/dev/kona/back/x/fill.rs @@ -0,0 +1,1132 @@ +//! The X server's fills: boxes and rectangles, and the data tokens that carry +//! their words. +//! +//! A box (0x154, `x_box`; 0x155 with six) is two words, `x1 << 16 | y1` and +//! `x2 << 16 | y2` with signed halves, far corner exclusive, both corners added +//! to the drawable's position like a rectangle's origin and a point. That the pipe +//! adds the position to a box is inferred: the senders in the recordings zero the +//! position before 18 of the 19 boxes, so they cannot tell a position-relative box +//! from an absolute one, and of the boxes that follow a nonzero origin there are +//! four (three of 0x154 and one of 0x155, against 201 rectangles of 0x152 and 4 of +//! 0x153), which the rectangles' reading and the points' make the likelier. A +//! rectangle (0x152, 0x153) is `x << 16 | y` and `width << 16 | height`, the X +//! protocol's, relative to the drawable the position names. When a request has +//! more boxes than a token holds, the leading words arrive as data tokens +//! (0x141 to 0x14b) and are kept until the box or rectangle token that ends +//! them, which draws them all. +//! +//! Spans (0x156, 0x157, 0x158 with one, two or six) are a count `n`, `n` points +//! `x << 16 | y` and `n` widths: each a one-pixel-high run of `width` pixels from +//! its point, absolute (no position added). Points (0x14c, 0x14d, 0x14e with +//! one, six or twenty-four) are single pixels `x << 16 | y`, added to the +//! drawable's position like a rectangle's origin and a relative point's start, and like boxes +//! they take the words of a waiting data run first. The senders do not zero the position: the +//! X server loads the drawable's screen x and y, and 386 of the 1192 position loads in the desktop +//! recording are nonzero. The recorded position loads are established; that the pipe adds the +//! position to a point is inferred. A span token whose count +//! is not what its length implies is dropped, counted. Every fill is masked by the +//! loaded stipple (see `stipple`). +//! +//! Background points (0x15f, 0x160, 0x161 with one, six or twenty-four) are +//! plain points in the context's background colour (graphics-context words 2 +//! and 3). They take a waiting data run first and go through the clip, +//! scissor, write mask and logic operation, but not the stipple: the stipple +//! masks the fills of the shape tokens above, and a background point is +//! drawn whole. The token rows are established; that the colour is the +//! context's background, from words 2 and 3 of the context load, is inferred, +//! and so is the stipple not applying (no source states either). Ported from sgi_kona_x_points in hw/misc/sgi-kona.c in +//! jamesbraid/qemu-sgi-origami. +//! +//! Relative points (0x14f, 0x150, 0x151 with one, six or twenty-four) are +//! points whose words are steps: each is relative to the previous point, the +//! first to the drawable's origin (`x << 16 | y`, signed halves), and the walk +//! runs on through a waiting data run and the burst's words and starts again +//! at the origin with the next token. The token rows are established; the +//! anchor rules are inferred, and no source states them. Like absolute points +//! they go through the stipple. +//! +//! Segments and polylines (0x159 to 0x15e) are in `line`. A waiting data run is +//! dropped, counted, when one of them arrives. +//! +//! X shapes draw the pixels the request names: they do not go through the +//! triangle sample rule (approximated; the architecture says X is a different +//! stream onto the same raster state, not that the board applies that rule to +//! it). A fill is clipped to the screen, the clip list and the scissor, takes +//! the context's colour through its write mask and logic operation, and is +//! refused, counted, if the context is not decoded, targets planes the model +//! does not yet hold, or finds a destination word wider than the target. The +//! planes not held are the overlays, the popup planes and the display-ID plane, +//! which the display step takes, and the two-buffer RGB form (configuration 4, +//! buffer selector 3), which the model's source draws into both buffers while +//! this model has one colour word and refuses it rather than draw half. A +//! request is checked whole before any pixel is written, so a refusal draws +//! nothing. That a data run is the next box's or rectangle's words, and that a +//! run of odd length is dropped, are inferred from the senders. +//! +//! The model carries no per-pixel tag saying what a word means (index or +//! direct); which colour map resolves a word is the display's to settle. +//! +//! Y: the X server's coordinates run down from the top of the screen, the +//! textport's up from the bottom. The store is bottom-origin, so a fill's row is +//! `height - 1 - y` (established by comparing the store rendered from the +//! recorded greeter with its capture, whose toolchest and icons land at the top +//! only this way). Clip, scissor and stipple use the X coordinates. +//! +//! # Cost +//! +//! A request is planned once: the context is decoded, the bounds, clip +//! rectangles and stipple are looked up, and the word array is resolved. Each +//! box is then clipped to rectangles up front and drawn a row of spans at a +//! time, with the stipple stepped by a column counter and the logic operation a +//! constant of the loop (`rows`), so nothing in the pixel loop looks anything up +//! or branches on the state. A solid, unmasked, clip-free fill is a plain run of +//! stores. + +use crate::dev::kona::defs::*; +use super::gc::{Gc, Target}; +use crate::dev::kona::back::raster::ops::LogicOp; +use super::state::XState; +use crate::dev::kona::back::raster::Surface; +use crate::dev::kona::back::store::{WordArray, DID_PLANE, OVERLAY0_PLANE, OVERLAY1_PLANE, POPUP_PLANE}; + +/// The two-word data token (tests). +#[cfg(test)] +pub const X_DATA2: u32 = WINDOW_SYSTEM_2_WORD_DATA_PRELOAD; +pub const X_RECT: u32 = FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT; +pub const X_RECT6: u32 = FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT_BURST_OF_6; +pub const X_BOX: u32 = FILL_BOXES_TWO_CORNERS; +pub const X_BOX6: u32 = FILL_BOXES_TWO_CORNERS_BURST_OF_6; +pub const X_POINT: u32 = DRAW_ABSOLUTE_POINTS; +pub const X_POINT6: u32 = DRAW_ABSOLUTE_POINTS_BURST_OF_6; +pub const X_POINT24: u32 = DRAW_ABSOLUTE_POINTS_BURST_OF_24; +pub const X_RPOINT: u32 = DRAW_RELATIVE_POINTS; +pub const X_RPOINT6: u32 = DRAW_RELATIVE_POINTS_BURST_OF_6; +pub const X_RPOINT24: u32 = DRAW_RELATIVE_POINTS_BURST_OF_24; +pub const X_BGPOINT: u32 = DRAW_POINTS_IN_BACKGROUND_COLOUR; +pub const X_BGPOINT6: u32 = DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_6; +pub const X_BGPOINT24: u32 = DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_24; +pub const X_SPANS: u32 = FILL_SPANS; +pub const X_SPANS2: u32 = FILL_SPANS_BURST_OF_2; +pub const X_SPANS6: u32 = FILL_SPANS_BURST_OF_6; +/// The data tokens, 0x141 to 0x14b: how many words each carries. +const DATA: [(u32, usize); 8] = [(WINDOW_SYSTEM_1_WORD_DATA_PRELOAD, 1), (WINDOW_SYSTEM_2_WORD_DATA_PRELOAD, 2), (WINDOW_SYSTEM_3_WORD_DATA_PRELOAD, 3), (WINDOW_SYSTEM_4_WORD_DATA_PRELOAD, 4), (WINDOW_SYSTEM_5_WORD_DATA_PRELOAD, 5), (WINDOW_SYSTEM_6_WORD_DATA_PRELOAD, 6), (WINDOW_SYSTEM_8_WORD_DATA_PRELOAD, 8), (WINDOW_SYSTEM_10_WORD_DATA_PRELOAD, 10)]; +/// The most words a data run may hold. +const RUN_WORDS: usize = 32; + +/// What fills did. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct FillCounts { + /// Fills that drew at least a pixel's worth of box. + pub drawn: u64, + /// Requests refused because the context did not decode (or was opaque with + /// no stipple), because it targets planes not held, or because a + /// destination word did not fit the target. + pub bad_context: u64, + pub other_target: u64, + pub bad_destination: u64, + /// Tokens whose word count is not what they carry: a span token whose + /// count disagrees with its length, a data token of the wrong size. + pub malformed: u64, +} + +/// Whether `selector` is a data token, and its word count. +pub fn data_words(selector: u32) -> Option { + DATA.iter().find(|(s, _)| *s == selector).map(|&(_, n)| n) +} + +/// Whether `selector` is a token that writes pixels when it arrives: a box, +/// rectangle, point (absolute, relative or background) or span. The data tokens only collect words for one. +pub fn paints(selector: u32) -> bool { + super::line::is_line(selector) || matches!(selector, X_RECT | X_RECT6 | X_BOX | X_BOX6 | X_POINT | X_POINT6 | X_POINT24 | X_BGPOINT | X_BGPOINT6 | X_BGPOINT24 | X_RPOINT | X_RPOINT6 | X_RPOINT24 | X_SPANS | X_SPANS2 | X_SPANS6) +} + +/// Whether `selector` is a token `draw_command` takes. +pub fn is_draw(selector: u32) -> bool { + keeps_run(selector) || super::line::is_line(selector) || matches!(selector, X_SPANS | X_SPANS2 | X_SPANS6) +} + +/// A token that keeps a waiting data run: the data tokens, boxes, rectangles, +/// points and the four polylines, whose points a run precedes. +pub fn keeps_run(selector: u32) -> bool { + data_words(selector).is_some() || super::line::is_polyline(selector) || matches!(selector, X_RECT | X_RECT6 | X_BOX | X_BOX6 | X_POINT | X_POINT6 | X_POINT24 | X_BGPOINT | X_BGPOINT6 | X_BGPOINT24 | X_RPOINT | X_RPOINT6 | X_RPOINT24) +} + +/// Why a request was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Refused { + Context, + Target, + Destination, +} + +/// A box, far corner exclusive, in the X server's coordinates. +pub(super) type Box4 = (i64, i64, i64, i64); + +/// The stipple pattern of a request: its rows, size and the point it is +/// measured from. +#[derive(Clone, Copy, Debug)] +pub struct StipplePattern { + pub rows: [u32; 32], + pub width: i64, + pub height: i64, + pub origin: (i64, i64), +} + +/// Everything about a request, looked up once. +pub(super) struct Plan<'a> { + pub(super) gc: Gc, + pub(super) array: WordArray<'a>, + pub(super) pitch: usize, + pub(super) height: i64, + pub(super) bounds: super::state::Bounds, + /// The clip rectangles when there are several (one is the bounds). + clips: &'a [[i32; 4]], + pub(super) stipple: Option, + pub(super) needs: bool, +} + +impl<'a> Plan<'a> { + pub(super) fn new(x: &'a XState, surface: &Surface<'a>) -> Result>, Refused> { + let stipple = x.stipple_pattern(); + // An opaque or tiled context carries both colours and needs a stipple. + let gc = match Gc::decode_for_fill(&x.gc) { + Ok(gc) if stipple.is_some() || !gc.opaque_or_tiled => gc, + _ => return Err(Refused::Context), + }; + Plan::with_gc(x, surface, gc, stipple) + } + + /// The plan for background points: the context's background colour as + /// the colour, and no stipple. + fn background(x: &'a XState, surface: &Surface<'a>) -> Result>, Refused> { + let Ok(mut gc) = Gc::decode_for_fill(&x.gc) else { return Err(Refused::Context) }; + gc.foreground = gc.background; + Plan::with_gc(x, surface, gc, None) + } + + /// The plan for a request drawn under `gc`, which the caller decoded: + /// refused if it targets planes the model does not hold. Inlined, so the + /// fills' own plan is the code it was before this had a second caller. + #[inline(always)] + pub(super) fn with_gc(x: &'a XState, surface: &Surface<'a>, gc: Gc, stipple: Option) -> Result>, Refused> { + let two_buffers = gc.target == Target::MainRgb8 && gc.raw[GC_FRAMEBUFFER_CONFIG] == 4 && gc.first_selector == 3; + if two_buffers || gc.target == Target::OverlayBoth { + return Err(Refused::Target); + } + if gc.is_noop() { + return Ok(None); + } + let (width, height) = surface.size(); + // The colour word, or the plane the target names. + let array = match gc.target { + Target::MainCi8 | Target::MainIndex16 | Target::MainRgb8 => surface.array(), + Target::Did => surface.sibling(DID_PLANE).array(), + Target::Overlay0 => surface.sibling(OVERLAY0_PLANE).array(), + Target::Overlay1 => surface.sibling(OVERLAY1_PLANE).array(), + Target::Popup => surface.sibling(POPUP_PLANE).array(), + Target::OverlayBoth => unreachable!("refused above"), + }; + let needs = gc.needs_destination(); + let clips = if x.clip_rects().len() > 1 { x.clip_rects() } else { &[] }; + Ok(Some(Plan { gc, pitch: array.pitch(), array, height: height as i64, bounds: x.bounds(width, height), clips, stipple, needs })) + } + + /// `b` clipped to the bounds; None if nothing is left. + pub(super) fn clip(&self, b: Box4) -> Option { + let (x1, y1, x2, y2) = (b.0.max(self.bounds.x1), b.1.max(self.bounds.y1), b.2.min(self.bounds.x2), b.3.min(self.bounds.y2)); + (x1 < x2 && y1 < y2).then_some((x1, y1, x2, y2)) + } + + /// The runs of row `y` between `x1` and `x2` that the clip list lets + /// through, sorted and not overlapping (a pixel in two rectangles is + /// painted once: a logic operation twice would undo itself). + pub(super) fn row_spans(&self, y: i64, x1: i64, x2: i64, out: &mut Vec<(i64, i64)>) { + out.clear(); + if self.clips.is_empty() { + out.push((x1, x2)); + return; + } + for c in self.clips.iter().filter(|c| (c[1] as i64..c[3] as i64).contains(&y)) { + let (a, b) = (x1.max(c[0] as i64), x2.min(c[2] as i64)); + if a < b { + out.push((a, b)); + } + } + out.sort_unstable(); + let mut merged = 0; + for i in 1..out.len() { + if out[i].0 <= out[merged].1 { + out[merged].1 = out[merged].1.max(out[i].1); + } else { + merged += 1; + out[merged] = out[i]; + } + } + out.truncate(if out.is_empty() { 0 } else { merged + 1 }); + } + + /// The stipple's row for `y`, and the column its first pixel at `x` falls + /// in: bits of the row, the pattern width and the starting column. + pub(super) fn stipple_start(&self, p: &StipplePattern, y: i64, x: i64) -> (u32, i64, i64) { + let row = p.rows[(y - p.origin.1).rem_euclid(p.height) as usize]; + (row, p.width, (x - p.origin.0).rem_euclid(p.width)) + } + + /// Whether every pixel this box would change holds a word that fits the + /// target: only a fill that reads its destination can fail. + fn destination_fits(&self, b: Box4, spans: &mut Vec<(i64, i64)>) -> bool { + let Some((x1, y1, x2, y2)) = self.clip(b) else { return true }; + for y in y1..y2 { + self.row_spans(y, x1, x2, spans); + let base = (self.height - 1 - y) as usize * self.pitch; + for &(a, c) in spans.iter() { + let (mut bits, mut width, mut col) = (0, 1, 0); + if let Some(p) = &self.stipple { + (bits, width, col) = self.stipple_start(p, y, a); + } + for x in a..c { + let set = self.stipple.is_none() || bits & (0x8000_0000 >> col) != 0; + col += 1; + if col == width { + col = 0; + } + if !set && !self.gc.opaque_or_tiled { + continue; + } + // SAFETY: the box is inside the bounds, which are inside the managed area. + if unsafe { self.array.load(base + x as usize) } > self.gc.width_mask { + return false; + } + } + } + } + true + } +} + +/// The word written for `source` over `destination`, the operation a +/// constant of the caller. +#[inline(always)] +pub(super) fn apply(gc: &Gc, source: u32, destination: u32) -> u32 { + ((LogicOp::from_index(GX).apply(source, destination) & gc.mask) | (destination & !gc.mask)) & gc.width_mask +} + +/// Draw the clipped box `b`, a row of spans at a time. `NEEDS`: the write +/// reads the destination; `STIP`: a stipple masks it; `GX`: the logic +/// operation, used only when the destination is read. +fn rows(p: &Plan<'_>, b: Box4, spans: &mut Vec<(i64, i64)>) { + let (x1, y1, x2, y2) = b; + let gc = &p.gc; + // With no destination read the colour alone makes each word, so the two + // results are made once. + let (fg, bg) = if NEEDS { (gc.foreground, gc.background) } else { (gc.apply(gc.foreground, 0), gc.apply(gc.background, 0)) }; + for y in y1..y2 { + p.row_spans(y, x1, x2, spans); + let base = (p.height - 1 - y) as usize * p.pitch; + for &(a, c) in spans.iter() { + if !NEEDS && !STIP { + for x in a..c { + // SAFETY: the box is inside the bounds, which are inside the managed area. + unsafe { p.array.store(base + x as usize, fg) }; + } + continue; + } + let (mut bits, mut width, mut col) = (0, 1, 0); + if STIP { + if let Some(s) = &p.stipple { + (bits, width, col) = p.stipple_start(s, y, a); + } + } + for x in a..c { + let set = !STIP || bits & (0x8000_0000 >> col) != 0; + col += 1; + if col == width { + col = 0; + } + if !set && !gc.opaque_or_tiled { + continue; + } + let i = base + x as usize; + let value = if NEEDS { + // SAFETY: as above. + let old = unsafe { p.array.load(i) }; + apply::(gc, if set { fg } else { bg }, old) + } else if set { + fg + } else { + bg + }; + // SAFETY: as above. + unsafe { p.array.store(i, value) }; + } + } + } +} + +/// Draw `b` with the loop that fits the request: chosen once, not per pixel. +fn draw(p: &Plan<'_>, b: Box4, spans: &mut Vec<(i64, i64)>) { + macro_rules! by_op { + ($stip:literal, $($gx:literal),*) => { + match p.gc.gx as u32 { + $($gx => rows::<$gx, true, $stip>(p, b, spans),)* + _ => { + debug_assert!(false, "a decoded context's logic operation is 0 to 15"); + rows::<3, true, $stip>(p, b, spans) + } + } + }; + } + let stip = p.stipple.is_some(); + match (p.needs, stip) { + (false, false) => rows::<0, false, false>(p, b, spans), + (false, true) => rows::<0, false, true>(p, b, spans), + (true, false) => by_op!(false, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), + (true, true) => by_op!(true, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), + } +} + +/// What a request did: boxes drawn, and why it was refused if it was. +struct Outcome { + drawn: u64, + refused: Option, +} + +impl XState { + /// The stipple pattern a fill uses, if one with a pattern is loaded. + pub(super) fn stipple_pattern(&self) -> Option { + let (rows, width, height, origin) = self.stipple.loaded_pattern()?; + let (ox, oy) = self.origin(); + Some(StipplePattern { rows, width, height, origin: (ox + origin.0, oy + origin.1) }) + } + + /// Draw `boxes` under the context: planned once, every box checked before + /// any is drawn when the write reads its destination. + fn request(&self, surface: &Surface<'_>, boxes: impl Iterator + Clone, background: bool, spans: &mut Vec<(i64, i64)>) -> Outcome { + let plan = match if background { Plan::background(self, surface) } else { Plan::new(self, surface) } { + Ok(Some(plan)) => plan, + Ok(None) => return Outcome { drawn: 0, refused: None }, + Err(why) => return Outcome { drawn: 0, refused: Some(why) }, + }; + if plan.needs && !boxes.clone().all(|b| plan.destination_fits(b, spans)) { + return Outcome { drawn: 0, refused: Some(Refused::Destination) }; + } + let mut drawn = 0; + for b in boxes { + if let Some(b) = plan.clip(b) { + draw(&plan, b, spans); + drawn += 1; + } + } + Outcome { drawn, refused: None } + } + + /// Run `boxes` and count the outcome. The spans buffer is kept between + /// requests, so a request allocates nothing. + pub(super) fn run_request(&mut self, surface: &Surface<'_>, boxes: impl Iterator + Clone, background: bool) { + let mut spans = std::mem::take(&mut self.spans); + let out = self.request(surface, boxes, background, &mut spans); + self.spans = spans; + self.fills.drawn += out.drawn; + match out.refused { + Some(Refused::Context) => self.fills.bad_context += 1, + Some(Refused::Target) => self.fills.other_target += 1, + Some(Refused::Destination) => self.fills.bad_destination += 1, + None => {} + } + } + + /// A data token, which only collects words for the paint that follows. False if + /// `selector` is not one. + pub fn collect_command(&mut self, selector: u32, args: &[u32]) -> bool { + if let Some(n) = data_words(selector) { + if args.len() != n { + self.fills.malformed += 1; + } else if self.run.len() + n > RUN_WORDS { + self.run.clear(); + self.runs_dropped += 1; + } else { + self.run.extend_from_slice(args); + } + return true; + } + false + } + + /// A data token, a box, a rectangle, points or spans. False if + /// `selector` is none of them. + pub fn draw_command(&mut self, surface: &Surface<'_>, selector: u32, args: &[u32]) -> bool { + if self.collect_command(selector, args) { + return true; + } + match selector { + X_BOX | X_BOX6 => self.boxes(surface, false, args), + X_RECT | X_RECT6 => self.boxes(surface, true, args), + X_POINT | X_POINT6 | X_POINT24 => self.points(surface, args, false), + X_BGPOINT | X_BGPOINT6 | X_BGPOINT24 => self.points(surface, args, true), + X_RPOINT | X_RPOINT6 | X_RPOINT24 => self.relative_points(surface, args), + X_SPANS | X_SPANS2 | X_SPANS6 => self.spans(surface, args), + t if super::line::is_line(t) => self.lines(surface, t, args), + _ => return false, + } + true + } + + /// The waiting run's words, then the token's, as boxes or rectangles. + fn boxes(&mut self, surface: &Surface<'_>, rects: bool, args: &[u32]) { + if self.run.len() % 2 != 0 { + self.run.clear(); + self.runs_dropped += 1; + } + let (ox, oy) = self.origin(); + let words = self.run.iter().chain(args).copied(); + let pairs = words.clone().step_by(2).zip(words.skip(1).step_by(2)); + let boxes = pairs.map(move |(w0, w1)| { + let (x, y) = ((w0 >> 16) as u16 as i16 as i64, w0 as u16 as i16 as i64); + if rects { + (x + ox, y + oy, x + ox + (w1 >> 16) as i64, y + oy + (w1 & 0xffff) as i64) + } else { + (x + ox, y + oy, ((w1 >> 16) as u16 as i16 as i64) + ox, (w1 as u16 as i16 as i64) + oy) + } + }); + // The boxes go in a buffer the state keeps; the run is cleared after, + // keeping its capacity. + let mut buf = std::mem::take(&mut self.boxes); + buf.clear(); + buf.extend(boxes); + self.run.clear(); + self.run_request(surface, buf.iter().copied(), false); + self.boxes = buf; + } + + /// Points: the waiting run's words, then the token's, one pixel each, in + /// the foreground colour or (`background`) the background colour, each + /// at the drawable's position plus its own. + fn points(&mut self, surface: &Surface<'_>, args: &[u32], background: bool) { + let (ox, oy) = self.origin(); + let mut buf = std::mem::take(&mut self.boxes); + buf.clear(); + buf.extend(self.run.iter().chain(args).map(|&w| { + let (x, y) = (((w >> 16) as u16 as i16 as i64) + ox, (w as u16 as i16 as i64) + oy); + (x, y, x + 1, y + 1) + })); + self.run.clear(); + self.run_request(surface, buf.iter().copied(), background); + self.boxes = buf; + } + + /// Relative points: each word is a step from the previous point, the + /// first from the drawable's origin, and the walk runs through the + /// waiting run's words and then the token's. The next token starts again + /// at the origin. + fn relative_points(&mut self, surface: &Surface<'_>, args: &[u32]) { + let (mut x, mut y) = self.origin(); + let mut buf = std::mem::take(&mut self.boxes); + buf.clear(); + buf.extend(self.run.iter().chain(args).map(|&w| { + x += (w >> 16) as u16 as i16 as i64; + y += w as u16 as i16 as i64; + (x, y, x + 1, y + 1) + })); + self.run.clear(); + self.run_request(surface, buf.iter().copied(), false); + self.boxes = buf; + } + + /// Spans: a count, that many points, that many widths. + fn spans(&mut self, surface: &Surface<'_>, args: &[u32]) { + let n = args.len().saturating_sub(1) / 2; + if args.is_empty() || args[0] as usize != n { + self.fills.malformed += 1; + return; + } + let mut buf = std::mem::take(&mut self.boxes); + buf.clear(); + buf.extend((0..n).map(|i| { + let w = args[1 + i]; + let (x, y) = ((w >> 16) as u16 as i16 as i64, w as u16 as i16 as i64); + (x, y, x + args[1 + n + i] as i64, y + 1) + })); + self.run_request(surface, buf.iter().copied(), false); + self.boxes = buf; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::back::store::PixelStore; + + /// An index-16 context: foreground 0x5a00, background 0xa500, full mask. + const GC: [u32; 13] = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + + fn rig() -> (XState, PixelStore) { + let mut x = XState::default(); + x.command(0x129, &GC); + (x, PixelStore::new(32, 32)) + } + + /// The word at X coordinates (`x`, `y`): the store is bottom-origin. + fn at(s: &PixelStore, x: u32, y: u32) -> u32 { + s.read(x, 31 - y, 1) + } + + fn lit(s: &PixelStore) -> usize { + (0..32).flat_map(|y| (0..32).map(move |x| (x, y))).filter(|&(x, y)| s.read(x, y, 1) != 0).count() + } + + fn pack(a: i32, b: i32) -> u32 { + (a as u32) << 16 | (b as u32 & 0xffff) + } + + /// A box has an exclusive far corner: it draws the pixels the request names, + /// not by the textport's fill rule. + #[test] + fn a_box_draws_the_pixels_it_names() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + assert!(x.draw_command(&d, X_BOX, &[pack(2, 3), pack(5, 7)])); + assert_eq!(lit(&s), 3 * 4); + assert_eq!(at(&s, 2, 3), 0x5a00); + assert_eq!(at(&s, 4, 6), 0x5a00); + assert_eq!(at(&s, 5, 3), 0, "the far corner is exclusive"); + assert_eq!(x.fills.drawn, 1); + } + + /// A box takes the drawable's position at both corners, as a rectangle's origin and a point do, + /// in the token and in its six-box burst, and a position loaded back to zero takes it away. + #[test] + fn the_position_is_added_to_both_corners_of_a_box() { + let mut x = XState::default(); + x.command(0x129, &GC); + // Tall and wide enough for position 100,50 and a box beyond it. + let s = PixelStore::new(128, 64); + let d = Surface::new(&s, 1); + let at = |x: u32, y: u32| s.read(x, 63 - y, 1); + let lit = || (0..64).flat_map(|y| (0..128).map(move |x| (x, y))).filter(|&(x, y)| s.read(x, y, 1) != 0).count(); + x.command(0x12a, &[100, 50, 100, 100]); + x.draw_command(&d, X_BOX, &[pack(2, 3), pack(5, 7)]); + assert_eq!(lit(), 3 * 4); + assert_eq!((at(102, 53), at(104, 56), at(105, 53), at(102, 57), at(2, 3)), (0x5a00, 0x5a00, 0, 0, 0)); + // Six boxes of one pixel, two columns apart. + let mut six = [0u32; 12]; + for i in 0..6 { + six[2 * i] = pack(10 + 2 * i as i32, 0); + six[2 * i + 1] = pack(11 + 2 * i as i32, 1); + } + x.draw_command(&d, X_BOX6, &six); + assert_eq!(lit(), 3 * 4 + 6); + assert!((0..6).all(|i| at(110 + 2 * i, 50) == 0x5a00) && at(10, 0) == 0); + // Back at position zero a box is where its words put it. + x.command(0x12a, &[0, 0, 100, 100]); + x.draw_command(&d, X_BOX, &[pack(20, 20), pack(22, 22)]); + assert_eq!((lit(), at(20, 20), at(21, 21), at(22, 22)), (3 * 4 + 6 + 4, 0x5a00, 0x5a00, 0)); + } + + /// A rectangle is relative to the drawable's position, with a size. + #[test] + fn a_rectangle_is_relative_to_the_position() { + let (mut x, s) = rig(); + x.command(0x12a, &[10, 20, 100, 100]); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_RECT, &[pack(1, 2), 4 << 16 | 3]); + assert_eq!(lit(&s), 12); + assert_eq!(at(&s, 11, 22), 0x5a00); + assert_eq!(at(&s, 14, 24), 0x5a00); + assert_eq!(at(&s, 15, 22), 0); + } + + /// Data tokens carry the leading boxes; the box token draws them all, and + /// an odd run is dropped. + #[test] + fn data_tokens_feed_the_next_box() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, 0x144, &[pack(0, 0), pack(2, 2), pack(10, 10), pack(12, 12)]); + x.draw_command(&d, X_BOX, &[pack(20, 20), pack(22, 22)]); + assert_eq!(lit(&s), 4 + 4 + 4); + assert_eq!(x.fills.drawn, 3); + // Three words are not boxes: the run is dropped, the token's own box draws. + x.draw_command(&d, 0x143, &[pack(1, 1), pack(2, 2), pack(3, 3)]); + x.draw_command(&d, X_BOX, &[pack(25, 25), pack(26, 26)]); + assert_eq!(x.runs_dropped, 1); + assert_eq!(at(&s, 1, 1), 0x5a00, "the first run's box 0,0-2,2 covers it"); + assert_eq!(at(&s, 25, 25), 0x5a00); + assert_eq!(at(&s, 3, 3), 0); + // A run that grows past its bound is dropped. + for _ in 0..4 { + x.draw_command(&d, 0x149, &[0; 8]); + } + x.draw_command(&d, 0x141, &[0]); + assert_eq!(x.runs_dropped, 2); + } + + #[test] + fn a_fill_clips_to_the_screen_the_clip_and_the_scissor() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_BOX, &[pack(-10, -10), pack(4, 4)]); + assert_eq!(lit(&s), 16); + x.command(0x12b, &[1, 10, 10, 20, 20]); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(31, 31)]); + assert_eq!(at(&s, 10, 10), 0x5a00); + assert_eq!(at(&s, 9, 10), 0); + assert_eq!(at(&s, 20, 15), 0); + x.command(0x12d, &[12, 12, 2, 2]); + let before = lit(&s); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(1, 1)]); + assert_eq!(lit(&s), before, "nothing in the scissor"); + } + + /// With several clip rectangles a pixel between them stays clear. + #[test] + fn a_clip_list_masks_the_fill() { + let (mut x, s) = rig(); + x.command(0x145, &[2, 0, 0, 8]); + x.command(0x12b, &[8, 20, 20, 30, 30]); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(31, 31)]); + assert_eq!(lit(&s), 64 + 100); + assert_eq!(at(&s, 15, 15), 0); + } + + /// A write mask keeps the destination's other bits. + #[test] + fn a_write_mask_and_operation_apply_to_the_destination() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(2, 2)]); + let mut gc = GC; + gc[0] = 0xff000; // foreground 0xff00 + gc[4] = 0xf0000f; // mask: only the low nibble of the high byte? index mask 0xf000 + x.command(0x129, &gc); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(1, 1)]); + let g = x.gc().unwrap(); + assert_eq!(at(&s, 0, 0), g.apply(g.foreground, 0x5a00)); + assert_eq!(at(&s, 1, 1), 0x5a00); + // The no-op operation changes nothing. + let mut gc = GC; + gc[9] = 10; + x.command(0x129, &gc); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(2, 2)]); + assert_eq!(at(&s, 0, 0), g.apply(g.foreground, 0x5a00)); + } + + /// What cannot be drawn is refused whole and counted: an undecoded + /// context, a target held elsewhere, a destination wider than the target, + /// an opaque context with no stipple. + #[test] + fn what_cannot_be_drawn_is_refused_and_counted() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + let mut bad = GC; + bad[9] = 1; + x.command(0x129, &bad); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(4, 4)]); + assert_eq!((x.fills.bad_context, lit(&s)), (1, 0)); + // The two-buffer RGB form draws two colour buffers at once, which the model has one word for. + let two_buffers = [0, 0, 0, 0, 0xff_ffff, 0xff_ffff, 4, 3, 1, 6, 0, 0, 0]; + x.command(0x129, &two_buffers); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(4, 4)]); + assert_eq!((x.fills.other_target, lit(&s)), (1, 0)); + // An opaque stippled context with no stipple loaded. + let mut opaque = GC; + opaque[4] |= 0x8000_0000; + x.command(0x129, &opaque); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(4, 4)]); + assert_eq!((x.fills.bad_context, lit(&s)), (2, 0)); + // A destination wider than the 16-bit target (a direct pixel there), + // with a mask that needs the destination. + let mut masked = GC; + masked[4] = 0xf0053f; + x.command(0x129, &masked); + s.write(10, 31 - 10, 1, 0x12_3456); + x.draw_command(&d, X_BOX, &[pack(8, 8), pack(12, 12)]); + assert_eq!((x.fills.bad_destination, at(&s, 8, 8)), (1, 0), "nothing of the request drew"); + // One bad box in a request stops the whole request: the first box is + // fine, the second reaches the bad destination. + x.draw_command(&d, 0x142, &[pack(0, 0), pack(2, 2)]); + x.draw_command(&d, X_BOX, &[pack(9, 9), pack(11, 11)]); + assert_eq!(x.fills.bad_destination, 2); + assert_eq!(at(&s, 0, 0), 0, "the first box of the refused request did not draw"); + } + + /// The display-ID plane is drawn by its own context into a plane of its + /// own: only the bits the mask names change, and the colour word does not. + #[test] + fn the_display_id_plane_takes_its_own_fills() { + use crate::dev::kona::back::store::DID_PLANE; + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + // The main ID field, bits 7:2, under mask 0xfc, written as ID 0x2b. + let id = |value: u32| [0, value << 9 & 0x3ffde00, 0, 0, 0, 0xfc000, 8, 1, 0, 6, 0, 1, 1]; + s.write(1, 31 - 1, DID_PLANE, 0x1f03); + x.command(0x129, &id(0)); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(2, 2)]); + assert_eq!(s.read(1, 31 - 1, DID_PLANE), 0x1f03, "ID 0 into bits 7:2 leaves them zero"); + s.write(1, 31 - 1, DID_PLANE, 0x1fff); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(2, 2)]); + assert_eq!(s.read(1, 31 - 1, DID_PLANE), 0x1f03, "the ID bits cleared, the overlay field kept"); + assert_eq!(lit(&s), 0, "the colour word is untouched"); + assert_eq!(x.fills.other_target, 0); + } + + /// Spans are absolute one-pixel-high runs; a count that disagrees with the + /// length drops the token. + #[test] + fn spans_draw_runs_and_a_bad_count_is_dropped() { + let (mut x, s) = rig(); + x.command(0x12a, &[100, 100, 5, 5]); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SPANS2, &[2, pack(1, 1), pack(4, 3), 5, 2]); + assert_eq!(lit(&s), 7); + assert_eq!(at(&s, 1, 1), 0x5a00); + assert_eq!(at(&s, 5, 1), 0x5a00); + assert_eq!(at(&s, 6, 1), 0); + assert_eq!(at(&s, 5, 3), 0x5a00); + x.draw_command(&d, X_SPANS, &[2, pack(0, 9), 3]); + assert_eq!((x.fills.malformed, lit(&s)), (1, 7)); + // A zero width draws nothing; spans end a waiting data run. + x.draw_command(&d, 0x141, &[pack(20, 20)]); + // (the back end offers every kernel command to `command` first, which + // ends the run) + assert!(!x.command(X_SPANS, &[])); + x.draw_command(&d, X_SPANS, &[1, pack(0, 9), 0]); + assert_eq!((x.runs_dropped, lit(&s)), (1, 7)); + } + + /// Points take a waiting run's words first, and are single pixels. + #[test] + fn points_draw_single_pixels_including_a_waiting_run() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, 0x142, &[pack(1, 1), pack(2, 2)]); + x.draw_command(&d, X_POINT, &[pack(3, 3)]); + assert_eq!(lit(&s), 3); + x.draw_command(&d, X_POINT6, &[pack(10, 1), pack(11, 1), pack(12, 1), pack(13, 1), pack(14, 1), pack(-1, 1)]); + assert_eq!(lit(&s), 3 + 5); + } + + /// An absolute point is at the drawable's position plus its own coordinates, in the run and in the token, + /// foreground or background. + #[test] + fn the_position_is_added_to_absolute_points() { + let (mut x, s) = rig(); + x.command(0x12a, &[10, 20, 100, 100]); + let d = Surface::new(&s, 1); + x.draw_command(&d, 0x141, &[pack(1, 1)]); + x.draw_command(&d, X_POINT, &[pack(2, 2)]); + assert_eq!((lit(&s), at(&s, 11, 21), at(&s, 12, 22), at(&s, 1, 1)), (2, 0x5a00, 0x5a00, 0)); + x.draw_command(&d, X_BGPOINT, &[pack(-1, -1)]); + assert_eq!(at(&s, 9, 19), 0xa500); + } + + /// Relative points step from the previous point, the first from the + /// drawable's origin, through a waiting run and a burst, and start again + /// at the origin with the next token. + #[test] + fn relative_points_step_from_the_previous_point() { + let (mut x, s) = rig(); + x.command(0x12a, &[10, 20, 100, 100]); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_RPOINT, &[pack(1, 2)]); + assert_eq!((lit(&s), at(&s, 11, 22)), (1, 0x5a00)); + // The run's words come first, and the burst continues the walk. + x.draw_command(&d, 0x142, &[pack(0, 0), pack(2, 0)]); + x.draw_command(&d, X_RPOINT6, &[pack(0, 1), pack(-1, 0), pack(0, 0), pack(5, 1), pack(0, 0), pack(0, 0)]); + let want = [(10, 20), (12, 20), (12, 21), (11, 21), (11, 21), (16, 22), (16, 22), (16, 22)]; + for &(px, py) in &want { + assert_eq!(at(&s, px, py), 0x5a00, "({px}, {py})"); + } + assert_eq!(lit(&s), 1 + 5, "a pixel named twice is one pixel"); + // A step of -1 in both axes from the origin goes up and left of it. + x.draw_command(&d, X_RPOINT, &[pack(-1, -1)]); + assert_eq!(at(&s, 9, 19), 0x5a00); + } + + /// Background points are points in the context's background colour, and + /// take a waiting run's words like the foreground ones. + #[test] + fn background_points_draw_in_the_background_colour() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, 0x141, &[pack(1, 1)]); + x.draw_command(&d, X_BGPOINT, &[pack(2, 2)]); + assert_eq!((lit(&s), at(&s, 1, 1), at(&s, 2, 2)), (2, 0xa500, 0xa500)); + x.draw_command(&d, X_BGPOINT6, &[pack(10, 1), pack(11, 1), pack(12, 1), pack(13, 1), pack(14, 1), pack(-1, 1)]); + assert_eq!((lit(&s), at(&s, 14, 1)), (2 + 5, 0xa500)); + let words: Vec = (0..24).map(|i| pack(i, 20)).collect(); + x.draw_command(&d, X_BGPOINT24, &words); + assert_eq!((lit(&s), at(&s, 23, 20)), (2 + 5 + 24, 0xa500)); + // The foreground token still draws the foreground. + x.draw_command(&d, X_POINT, &[pack(5, 5)]); + assert_eq!(at(&s, 5, 5), 0x5a00); + } + + /// A background point is not masked by the stipple: it draws where the + /// pattern is clear, and a plain point in the same place is masked. + #[test] + fn background_points_ignore_the_stipple() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + // An 8x1 pattern of alternating columns, the even ones set. + x.command(0x13d, &[1, 8, 1]); + x.stipple.rows(&[0xaaaa_aaaa]); + x.command(0x13c, &[1]); + x.draw_command(&d, X_BGPOINT6, &[pack(0, 0), pack(1, 0), pack(2, 0), pack(3, 0), pack(4, 0), pack(5, 0)]); + assert_eq!((0..6).map(|c| at(&s, c, 0)).collect::>(), [0xa500; 6]); + x.draw_command(&d, X_POINT6, &[pack(0, 1), pack(1, 1), pack(2, 1), pack(3, 1), pack(4, 1), pack(5, 1)]); + assert_eq!((0..6).map(|c| at(&s, c, 1)).collect::>(), [0x5a00, 0, 0x5a00, 0, 0x5a00, 0]); + } + + /// The two-buffer RGB form (configuration 4, buffer selector 3) is + /// refused, not drawn into one buffer. + #[test] + fn the_two_buffer_rgb_form_is_refused() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + let rgb = [0, 0, 0, 0, 0xffffff, 0xffffff, 4, 3, 1, 6, 0, 0, 0]; + x.command(0x129, &rgb); + assert!(Gc::decode_for_fill(&rgb).is_ok()); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(4, 4)]); + assert_eq!((x.fills.other_target, lit(&s)), (1, 0)); + // The same form with selector 1 draws. + let mut one = rgb; + one[7] = 1; + x.command(0x129, &one); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(4, 4)]); + assert_eq!(x.fills.other_target, 1); + } + + /// A pixel in two overlapping clip rectangles is painted once: under xor a + /// second painting would undo the first. + #[test] + fn overlapping_clip_rectangles_paint_a_pixel_once() { + let (mut x, s) = rig(); + let mut gc = GC; + gc[9] = 12; // xor + x.command(0x129, &gc); + x.command(0x145, &[2, 0, 0, 20]); + x.command(0x12b, &[10, 10, 0, 30, 10]); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(32, 10)]); + let v = x.gc().unwrap().foreground; + for px in [0, 15, 19, 25, 29] { + assert_eq!(at(&s, px, 5), v, "x {px}"); + } + assert_eq!(at(&s, 30, 5), 0); + } + + /// A data token with the wrong word count is counted, not taken. + #[test] + fn a_data_token_of_the_wrong_size_is_counted() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, 0x142, &[1]); + x.draw_command(&d, 0x141, &[1, 2]); + assert_eq!((x.fills.malformed, x.runs_dropped), (2, 0)); + x.draw_command(&d, X_BOX, &[pack(0, 0), pack(1, 1)]); + assert_eq!(x.fills.drawn, 1, "the box alone"); + } + + /// The optimised fills against a per-pixel reference that works from what + /// this test wrote, not from the code under test: its own table of the 16 + /// logic operations, the raw words it chose for the colours and the mask, the + /// clip rectangles, scissor, stipple rows, origin and position it sent, and + /// the clip rule stated once below. Nothing on the expected-value path calls + /// `XState`, `Gc` or `Stipple`; only the loads and the draw go through them. + /// + /// The generator keeps most requests on the screen (clip rectangles + /// overlapping it, few empty masks), and the test counts, from the + /// reference, the requests that change a pixel and asserts at least half of + /// them do, and that every operation that writes changed something. + /// + /// The clip rule, as the model this was taken from has it (inferred there, + /// not a hardware measurement): drawing is limited to the screen; to the box + /// from the smallest x1 and y1 to the largest x2 and y2 of the clip + /// rectangles as sent (a reversed rectangle contributes its corners as given, + /// so it can make that box empty); when more than one rectangle was sent, to + /// pixels inside at least one (x1 <= x < x2 and y1 <= y < y2, so a reversed + /// rectangle holds none); and to the scissor box (x1, y1, width, height) when + /// one was loaded after the clip. A stipple bit is the row at `(y - oy) mod + /// height`, bit `(x - ox) mod width` from the most significant, where the + /// origin is the position plus the entry's origin. + #[test] + fn fills_match_a_per_pixel_reference() { + const W: i64 = 64; + const H: i64 = 48; + // The 16 operations in the GL numbering, written out here. + let ops: [fn(u32, u32) -> u32; 16] = [ + |_, _| 0, + |s, d| s & d, + |s, d| s & !d, + |s, _| s, + |s, d| !s & d, + |_, d| d, + |s, d| s ^ d, + |s, d| s | d, + |s, d| !(s | d), + |s, d| !(s ^ d), + |_, d| !d, + |s, d| s | !d, + |s, _| !s, + |s, d| !s | d, + |s, d| !(s & d), + |_, _| u32::MAX, + ]; + let mut seed = 0x9e37_79b9_7f4a_7c15u64; + let mut rnd = move |n: u32| { + seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + ((seed >> 33) as u32) % n + }; + let store = PixelStore::new(W as u32, H as u32); + let surface = Surface::new(&store, 1); + // The reference's own copy of the screen, by X row (y down), and the store + // is bottom-origin: store row = H - 1 - y. + let mut model = vec![0u32; (W * H) as usize]; + for y in 0..H { + for x in 0..W { + let v = rnd(0x1_0000); + model[(y * W + x) as usize] = v; + store.write(x as u32, (H - 1 - y) as u32, 1, v); + } + } + // A rectangle mostly over the screen; now and then reversed. + let rect = |rnd: &mut dyn FnMut(u32) -> u32| -> [i64; 4] { + let (x1, y1) = (rnd(70) as i64 - 8, rnd(54) as i64 - 6); + let (w, h) = (6 + rnd(60) as i64, 6 + rnd(44) as i64); + if rnd(12) == 0 { [x1 + w, y1 + h, x1, y1] } else { [x1, y1, x1 + w, y1 + h] } + }; + let (mut changing, mut by_op) = (0u32, [0u32; 16]); + const REQUESTS: u32 = 600; + for request in 0..REQUESTS { + let mut x = XState::default(); + // Stipple: none, or one of several sizes at a random origin. + let stippled = rnd(3) != 0; + let (mut rows, mut sw, mut sh, mut so) = (Vec::new(), 1i64, 1i64, (0i64, 0i64)); + if stippled { + let sizes = [1u32, 2, 3, 5, 8, 17, 32]; + let (w, h) = (sizes[rnd(7) as usize], sizes[rnd(7) as usize]); + x.command(0x13d, &[1, w, h]); + rows = (0..h).map(|_| rnd(u32::MAX)).collect(); + x.stipple.rows(&rows); + so = (rnd(80) as i64 - 40, rnd(80) as i64 - 40); + x.command(0x13e, &[1, so.0 as i32 as u32, so.1 as i32 as u32]); + x.command(0x13c, &[1]); + (sw, sh) = (w as i64, h as i64); + } + let pos = (rnd(40) as i64 - 20, rnd(40) as i64 - 20); + x.command(0x12a, &[pos.0 as i32 as u32, pos.1 as i32 as u32, 64, 48]); + // The context: the words are built from the values chosen here. + let gx = rnd(16); + let mask = match rnd(5) { + 0 | 1 => 0xffff, + _ => 1 + rnd(0xffff), + }; + let (fg, bg) = (rnd(0x1_0000), rnd(0x1_0000)); + let opaque = stippled && rnd(2) == 0; + let word = |v: u32| (v & 0xfff0) << 4 | (v & 0xf) << 3; + let mut gc = [0, 0x1ffe, 0, 0x1ffe, 0, 0, 0x180c, 0x40001, 1, 2 * gx, 5, 1, 1]; + gc[0] = word(fg); + gc[2] = word(bg); + gc[4] = (mask >> 4) & 0xfff | (mask & 0xf) << 20 | if opaque { 0x8000_0000 } else { 0 }; + x.command(0x129, &gc); + // The clip: none, one rectangle, or several that overlap. + let mut clips: Vec<[i64; 4]> = Vec::new(); + match rnd(3) { + 0 => {} + 1 => { + clips.push(rect(&mut rnd)); + let r = clips[0]; + x.command(0x12b, &[1, r[0] as i32 as u32, r[1] as i32 as u32, r[2] as i32 as u32, r[3] as i32 as u32]); + } + _ => { + let n = 2 + rnd(3) as usize; + let mut flat = vec![n as u32]; + for _ in 0..n { + let r = rect(&mut rnd); + clips.push(r); + flat.extend(r.iter().map(|&v| v as i32 as u32)); + } + let (lead, last) = flat.split_at(flat.len() - 5); + for c in lead.chunks(4) { + x.command(0x145, c); + } + x.command(0x12b, last); + } + } + let scissor = (rnd(4) == 0).then(|| [rnd(50) as i64 - 10, rnd(36) as i64 - 8, 10 + rnd(60) as i64, 8 + rnd(40) as i64]); + if let Some(s) = scissor { + x.command(0x12d, &[s[0] as i32 as u32, s[1] as i32 as u32, s[2] as u32, s[3] as u32]); + } + // `b` is where the box lands: it is sent relative to the position, which both corners take. + let b = rect(&mut rnd); + x.draw_command(&surface, X_BOX, &[pack((b[0] - pos.0) as i32, (b[1] - pos.1) as i32), pack((b[2] - pos.0) as i32, (b[3] - pos.1) as i32)]); + + // The reference. + let mut limit = [0i64, 0, W, H]; + if !clips.is_empty() { + limit = [ + limit[0].max(clips.iter().map(|c| c[0]).min().unwrap()), + limit[1].max(clips.iter().map(|c| c[1]).min().unwrap()), + limit[2].min(clips.iter().map(|c| c[2]).max().unwrap()), + limit[3].min(clips.iter().map(|c| c[3]).max().unwrap()), + ]; + } + if let Some(s) = scissor { + limit = [limit[0].max(s[0]), limit[1].max(s[1]), limit[2].min(s[0] + s[2]), limit[3].min(s[1] + s[3])]; + } + let writes = mask != 0 && gx != 5; + let mut changed = false; + if writes { + for py in 0..H { + for px in 0..W { + let inside = |r: &[i64; 4]| px >= r[0] && px < r[2] && py >= r[1] && py < r[3]; + if !inside(&b) || !inside(&limit) || (clips.len() > 1 && !clips.iter().any(inside)) { + continue; + } + let set = !stippled || rows[(py - (pos.1 + so.1)).rem_euclid(sh) as usize] & (0x8000_0000u32 >> (px - (pos.0 + so.0)).rem_euclid(sw)) != 0; + if !set && !opaque { + continue; + } + let at = (py * W + px) as usize; + let old = model[at]; + let new = ((ops[gx as usize](if set { fg } else { bg }, old) & mask) | (old & !mask)) & 0xffff; + changed |= new != old; + model[at] = new; + } + } + } + for py in 0..H { + for px in 0..W { + let (a, r) = (store.read(px as u32, (H - 1 - py) as u32, 1), model[(py * W + px) as usize]); + assert_eq!(a, r, "request {request}: pixel ({px}, {py}) after gx {gx}, mask {mask:#x}, box {b:?}, clips {clips:?}, scissor {scissor:?}, stipple {stippled} (opaque {opaque})"); + } + } + if changed { + changing += 1; + by_op[gx as usize] += 1; + } + } + assert!(changing >= REQUESTS / 2, "only {changing} of {REQUESTS} requests changed a pixel"); + for (gx, n) in by_op.iter().enumerate() { + // The no-op writes nothing; the others must have changed pixels. + assert!(gx == 5 || *n >= 5, "operation {gx} changed pixels in only {n} requests"); + } + } + + #[test] + fn the_run_rule_names_the_tokens_that_keep_a_run() { + for t in [0x141, 0x142, 0x143, 0x144, 0x146, 0x147, 0x149, 0x14b, X_BOX, X_RECT, X_BOX6, X_RECT6] { + assert!(keeps_run(t), "{t:#x}"); + } + for t in [X_POINT, X_POINT6, X_POINT24, X_BGPOINT, X_BGPOINT6, X_BGPOINT24] { + assert!(keeps_run(t) && paints(t) && is_draw(t), "{t:#x}"); + } + assert!(!keeps_run(0x129) && !keeps_run(0x145) && !keeps_run(crate::dev::kona::back::x::line::X_SEGMENT)); + for t in [crate::dev::kona::back::x::line::X_LINE, crate::dev::kona::back::x::line::X_LINE6, crate::dev::kona::back::x::line::X_RLINE, crate::dev::kona::back::x::line::X_RLINE6] { + assert!(keeps_run(t) && paints(t) && is_draw(t), "{t:#x}"); + } + assert_eq!(data_words(0x14b), Some(10)); + assert_eq!(data_words(0x145), None); + } +} diff --git a/src/dev/kona/back/x/gc.rs b/src/dev/kona/back/x/gc.rs new file mode 100644 index 000000000..82784101f --- /dev/null +++ b/src/dev/kona/back/x/gc.rs @@ -0,0 +1,638 @@ +//! The X server's graphics context, as the thirteen words it loads. +//! +//! The words say what the next fills draw: a colour, a background colour, a +//! write mask, a logic operation, and a target (which planes). Only some forms +//! are understood. A form that is not is reported with why, and a draw using +//! it is refused and counted, never guessed. +//! +//! Ported from hw/display/sgi/x-gc.c in jamesbraid/qemu-sgi-origami. Its +//! grades carry over: the field layouts are software expectations read from +//! the senders. What the ported model marks inferred stays inferred here: that the second +//! buffer selector, when it equals the first, is the same selection; that the +//! overlay-both form (config 0x28, selector 3) is the geometry GC loader's +//! form for an 8-bit overlay window drawn into both overlay buffers; and that +//! the 30-bit TrueColor form is told apart by format 9. The canonical colour +//! subsets (8-bit index, 8-bit-per-component RGB) are model subsets, not +//! visual identities. + +use crate::dev::kona::back::raster::ops::LogicOp; +use crate::dev::kona::defs::*; + +/// Which planes a context draws into. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Target { + /// An 8-bit colour index in the main planes. + MainCi8, + /// The native 16-bit main-plane index. + MainIndex16, + /// 24-bit RGB in the main planes. + MainRgb8, + Overlay0, + Overlay1, + Popup, + /// The display-ID plane. + Did, + /// Both overlay buffers at once, the same 8-bit index in each. + OverlayBoth, +} + +/// Why a context was not decoded. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Status { + BadFields, + BadColour, + BadMask, + MixedDid, + /// A form whose meaning is not known. + Deferred, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Gc { + pub raw: [u32; GC_WORDS], + pub target: Target, + pub foreground: u32, + pub background: u32, + pub mask: u32, + pub width_mask: u32, + /// The logic operation, 0 to 15 in the GL numbering (clear, and, and + /// reverse, copy, ...). + pub gx: u8, + pub first_selector: u8, + pub second_selector: u8, + /// Word 4's bit 31: the X server's intent of an opaque stippled fill (fill style 3) or a tiled + /// fill with a pipe tile, the only difference on the wire between an opaque and a transparent + /// stipple (inferred). Whether the board honours it is unknown: the engine forwards the word + /// whole and the chip probably applies the 0x00ffffff mask to it, which would leave the bit no + /// effect. The model keys an opaque fill on it, which is an approximation that contradicts that + /// mask; a raster-side observation of where a recording's opaque pixels come from would settle + /// it. + pub opaque_or_tiled: bool, +} + +fn index_value(high: u32) -> u32 { + (high >> 4) & 0xfff0 | (high >> 3) & 0xf +} + +fn index_mask(word: u32) -> u32 { + (word & 0xfff) << 4 | (word >> 20) & 0xf +} + +/// Whether a 12-bit colour field is a widened component: an 8-bit one +/// `b << 4 | b >> 4` for 24-bit TrueColor, a 10-bit one `c << 2 | c >> 8` for +/// 30-bit TrueColor, as the X server's context builder widens them. +fn canonical_component(component: u32, rgb30: bool) -> bool { + if rgb30 { + let c = component >> 2; + component == (c << 2 | c >> 8) + } else { + let byte = component >> 4; + component == (byte << 4 | byte >> 4) + } +} + +/// The model keeps eight bits a component: a 30-bit component's low two bits +/// are dropped. +fn canonical_rgb(colour: u64, rgb30: bool) -> Option { + const FIELDS: u64 = 0xfff << 40 | 0xfff << 27 | 0xfff << 14 | 0x1ffe; + let alpha = (colour >> 1) & 0xfff; + if colour & !FIELDS != 0 || (alpha != 0 && alpha != 0xfff) { + return None; + } + let mut out = 0; + for k in 1..=3u32 { + let component = ((colour >> (13 * k + 1)) & 0xfff) as u32; + if !canonical_component(component, rgb30) { + return None; + } + out |= (component >> 4) << (8 * (k - 1)); + } + Some(out) +} + +fn decode_did(w: &[u32; GC_WORDS], mut gc: Gc) -> Result { + let (high, low) = (w[GC_FOREGROUND_HIGH] != 0 || w[GC_WRITE_MASK_FIRST] != 0, w[GC_FOREGROUND_LOW] != 0 || w[GC_WRITE_MASK_SECOND] != 0); + gc.target = Target::Did; + gc.width_mask = 0xffff; + if w[GC_FRAMEBUFFER_CONFIG] != 8 || w[GC_OPERATION] != 1 || w[GC_FRAMEBUFFER_BASES] != 0 || w[GC_BLEND_AND_LOGIC] != 6 || w[GC_PIXEL_PACK_CODE] != 0 || w[GC_TEXTURE_FILTER_COLOUR_FLAG] != 1 || w[GC_TEXTURE_FILTER_PIXEL_FLAG] != 1 || w[GC_BACKGROUND_HIGH] != 0 || w[GC_BACKGROUND_LOW] != 0 { + return Err(Status::BadFields); + } + if high && low { + return Err(Status::MixedDid); + } + if w[GC_FOREGROUND_HIGH] & !0xfff78 != 0 || w[GC_FOREGROUND_LOW] & !0x3ffde00 != 0 { + return Err(Status::BadColour); + } + if w[GC_WRITE_MASK_FIRST] & !0xf0_0fff != 0 || w[GC_WRITE_MASK_SECOND] & !0xfff_00f != 0 { + return Err(Status::BadMask); + } + if high { + gc.foreground = index_value(w[GC_FOREGROUND_HIGH]); + gc.mask = index_mask(w[GC_WRITE_MASK_FIRST]); + } else { + gc.foreground = (w[GC_FOREGROUND_LOW] >> 9) & 0xf | (w[GC_FOREGROUND_LOW] >> 10) & 0xfff0; + gc.mask = (w[GC_WRITE_MASK_SECOND] >> 12) & 0xfff | (w[GC_WRITE_MASK_SECOND] & 0xf) << 12; + } + Ok(gc) +} + +impl Gc { + /// Decode the words the way the X fills do: the native 16-bit index form + /// for configurations 0xc and 0x180c, the 8-bit form otherwise. + pub fn decode_for_fill(w: &[u32; GC_WORDS]) -> Result { + if w[GC_FRAMEBUFFER_CONFIG] == 0xc || w[GC_FRAMEBUFFER_CONFIG] == 0x180c { + Gc::decode_main_index(w) + } else { + Gc::decode(w) + } + } + + pub fn decode(w: &[u32; GC_WORDS]) -> Result { + decode(w, false) + } + + /// Keep the full native main-plane index and mask. + pub fn decode_main_index(w: &[u32; GC_WORDS]) -> Result { + let gc = decode(w, true)?; + if gc.target == Target::MainIndex16 { Ok(gc) } else { Err(Status::BadFields) } + } + + /// The write that changes nothing: no bits enabled, or the no-op operation. + pub fn is_noop(&self) -> bool { + self.mask == 0 || self.gx == 5 + } + + /// Whether a write depends on the pixel it replaces. + pub fn needs_destination(&self) -> bool { + if self.is_noop() { + return false; + } + self.mask != self.width_mask || !matches!(self.gx, 0 | 3 | 12 | 15) + } + + /// The word written for `source` over `destination`. + pub fn apply(&self, source: u32, destination: u32) -> u32 { + ((LogicOp::from_index(self.gx as u32).apply(source, destination) & self.mask) | (destination & !self.mask)) & self.width_mask + } +} + +fn decode(w: &[u32; GC_WORDS], native_index: bool) -> Result { + let plane = w[GC_FRAMEBUFFER_CONFIG] & 0x3c; + let mask_word = w[GC_WRITE_MASK_FIRST] & !0x8000_0000; + // The X server's context builder copies the buffer selector in op bits + // 1:0 into bits 3:2 for a format with bit 0x80000. What the second copy + // selects is unknown: a copy equal to the first reads as the same + // selection (inferred), any other value is deferred. + let op = if (w[GC_OPERATION] >> 2) & 3 == w[GC_OPERATION] & 3 { w[GC_OPERATION] & !0xc } else { w[GC_OPERATION] }; + let residual = op & !0xfc_0003; + let geometry = w[GC_PIXEL_PACK_CODE] == 0; + let rgb30 = w[GC_FRAMEBUFFER_CONFIG] == 0 && (w[GC_PIXEL_PACK_CODE] == 0 || w[GC_PIXEL_PACK_CODE] == 9); + + if w[GC_BLEND_AND_LOGIC] > 30 || w[GC_BLEND_AND_LOGIC] & 1 != 0 { + return Err(Status::BadFields); + } + let mut gc = Gc { + raw: *w, + target: Target::MainCi8, + foreground: 0, + background: 0, + mask: 0, + width_mask: 0, + gx: (w[GC_BLEND_AND_LOGIC] / 2) as u8, + first_selector: (w[GC_OPERATION] & 3) as u8, + second_selector: ((w[GC_OPERATION] >> 18) & 0x3f) as u8, + opaque_or_tiled: w[GC_WRITE_MASK_FIRST] & 0x8000_0000 != 0, + }; + if w[GC_FRAMEBUFFER_CONFIG] == 8 { + return decode_did(w, gc); + } + // Config 0x28 with buffer selector 3 is the geometry GC loader's form for + // an 8-bit overlay window drawn into both overlay buffers (inferred); a + // 15-bit overlay format also gets 0x28 whatever its selector, and those + // forms stay deferred. + if (plane == 0x28 && !(w[GC_FRAMEBUFFER_CONFIG] == 0x28 && op == 3)) || (w[GC_FRAMEBUFFER_CONFIG] == 0x1008 && w[GC_OPERATION] == 0x10001 && w[GC_PIXEL_PACK_CODE] == 5) { + return Err(Status::Deferred); + } + match w[GC_FRAMEBUFFER_CONFIG] { + 0 => { + // 30-bit TrueColor: config 0 from every builder, told apart by + // format 9, or 0 from the geometry builder (inferred). + if !rgb30 { + return Err(Status::BadFields); + } + gc.target = Target::MainRgb8; + gc.width_mask = 0xff_ffff; + } + 0xc | 0x180c => { + gc.target = if native_index { Target::MainIndex16 } else { Target::MainCi8 }; + gc.width_mask = if native_index { 0xffff } else { 0xff }; + } + 4 | 0x2804 | 0x3804 => { + gc.target = Target::MainRgb8; + gc.width_mask = 0xff_ffff; + } + 0x2c | 0x182c | 0x102c => { + gc.target = Target::Overlay0; + gc.width_mask = 0xffff; + } + 0x30 | 0x1030 => return Err(Status::Deferred), + 0x1830 => { + gc.target = Target::Overlay1; + gc.width_mask = 0xffff; + } + 0x28 => { + gc.target = Target::OverlayBoth; + gc.width_mask = 0xff; + } + 0x20 | 0x1820 | 0x1020 => { + gc.target = Target::Popup; + gc.width_mask = 3; + } + _ => return Err(Status::BadFields), + } + if op & 4 != 0 { + return Err(Status::Deferred); + } + if !rgb30 && geometry != (w[GC_FRAMEBUFFER_CONFIG] < 0x1000) { + return Err(Status::BadFields); + } + if matches!(gc.target, Target::MainCi8 | Target::MainIndex16 | Target::MainRgb8) { + let rgb = gc.target == Target::MainRgb8; + let tail = if rgb { 0 } else { 1 }; + if w[GC_FRAMEBUFFER_BASES] != 1 || w[GC_TEXTURE_FILTER_COLOUR_FLAG] != tail || w[GC_TEXTURE_FILTER_PIXEL_FLAG] != tail { + return Err(Status::BadFields); + } + if geometry { + if op != 1 && !(rgb && w[GC_FRAMEBUFFER_CONFIG] == 4 && op == 3) { + return Err(Status::BadFields); + } + } else if rgb30 { + // Selector 2 has no config form to show it in: deferred. + if op != 0x64001 { + return Err(Status::Deferred); + } + } else if w[GC_PIXEL_PACK_CODE] != if rgb { 8 } else { 5 } + || !(1..=2).contains(&gc.first_selector) + || !(1..=2).contains(&gc.second_selector) + || residual != if rgb { 0x2_0000 } else { 0 } + || (rgb && ((w[GC_FRAMEBUFFER_CONFIG] == 0x3804) != (gc.second_selector == 2))) + { + return Err(Status::BadFields); + } + } else if w[GC_FRAMEBUFFER_BASES] != 0 + || w[GC_TEXTURE_FILTER_COLOUR_FLAG] != 1 + || w[GC_TEXTURE_FILTER_PIXEL_FLAG] != 1 + || (!geometry && w[GC_PIXEL_PACK_CODE] != 5) + || op != match gc.target { + Target::Overlay1 => 2, + Target::OverlayBoth => 3, + _ => 1, + } + { + return Err(Status::BadFields); + } + + if gc.target == Target::MainRgb8 { + let (red, green, blue) = (mask_word & 0xfff, (mask_word >> 12) & 0xfff, w[GC_WRITE_MASK_SECOND] & 0xfff); + let (Some(fg), Some(bg)) = (canonical_rgb((w[GC_FOREGROUND_HIGH] as u64) << 32 | w[GC_FOREGROUND_LOW] as u64, rgb30), canonical_rgb((w[GC_BACKGROUND_HIGH] as u64) << 32 | w[GC_BACKGROUND_LOW] as u64, rgb30)) else { + return Err(Status::BadColour); + }; + gc.foreground = fg; + gc.background = bg; + if mask_word & !0xff_ffff != 0 + || w[GC_WRITE_MASK_SECOND] & !0xff_ffff != 0 + || w[GC_WRITE_MASK_SECOND] & 0xfff000 != 0xfff000 + || !canonical_component(red, rgb30) + || !canonical_component(green, rgb30) + || !canonical_component(blue, rgb30) + { + return Err(Status::BadMask); + } + gc.mask = (red >> 4) << 16 | (green >> 4) << 8 | (blue >> 4); + } else { + if w[GC_FOREGROUND_HIGH] & !0xfff78 != 0 || w[GC_BACKGROUND_HIGH] & !0xfff78 != 0 || (w[GC_FOREGROUND_LOW] != 0 && w[GC_FOREGROUND_LOW] != 0x1ffe) || (w[GC_BACKGROUND_LOW] != 0 && w[GC_BACKGROUND_LOW] != 0x1ffe) { + return Err(Status::BadColour); + } + if mask_word & !0xf0_0fff != 0 || w[GC_WRITE_MASK_SECOND] != 0 { + return Err(Status::BadMask); + } + gc.foreground = index_value(w[GC_FOREGROUND_HIGH]); + gc.background = index_value(w[GC_BACKGROUND_HIGH]); + gc.mask = index_mask(mask_word); + match gc.target { + Target::MainCi8 => { + if (gc.foreground | gc.background) & 0xff != 0 { + return Err(Status::BadColour); + } + if gc.mask & 0xff != 0xff { + return Err(Status::BadMask); + } + gc.foreground >>= 8; + gc.background >>= 8; + gc.mask >>= 8; + } + Target::OverlayBoth => { + // The two bytes are one index repeated; keep one of them. + if gc.foreground >> 8 != gc.foreground & 0xff || gc.background >> 8 != gc.background & 0xff { + return Err(Status::BadColour); + } + if gc.mask >> 8 != gc.mask & 0xff { + return Err(Status::BadMask); + } + gc.foreground &= 0xff; + gc.background &= 0xff; + gc.mask &= 0xff; + } + Target::Popup => { + if mask_word & !0x30_0000 != 0 { + return Err(Status::BadMask); + } + gc.foreground &= 3; + gc.background &= 3; + gc.mask = (mask_word >> 20) & 3; + } + _ => {} + } + } + Ok(gc) +} + +#[cfg(test)] +mod tests { + use super::*; + + const CI: [u32; 13] = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + const RGB: [u32; 13] = [0, 0, 0, 0, 0xffffff, 0xffffff, 0x2804, 0x60001, 1, 6, 8, 0, 0]; + const DID: [u32; 13] = [0, 0, 0, 0, 0, 0xfc000, 8, 1, 0, 6, 0, 1, 1]; + + fn ok(w: &[u32; 13]) -> Gc { + Gc::decode(w).unwrap() + } + + /// The engine applies a mask to each of words 4 to 12 before the raster register sees it, so a form + /// the decoder takes has no bit outside its word's mask. + #[test] + fn the_forms_the_decoder_takes_fit_the_engines_masks() { + for w in [CI, RGB, DID] { + ok(&w); + for (k, mask) in GC_WORD_MASKS.iter().enumerate() { + assert_eq!(w[GC_WRITE_MASK_FIRST + k] & !mask, 0, "word {} of {w:x?}", GC_WRITE_MASK_FIRST + k); + } + } + } + + #[test] + fn an_index_context_decodes() { + let gc = ok(&CI); + assert_eq!((gc.target, gc.foreground, gc.background, gc.mask), (Target::MainCi8, 0x5a, 0xa5, 0xff)); + assert_eq!(gc.raw, CI); + let mut w = CI; + w[GC_WRITE_MASK_FIRST] = 0xf0053f; // logical mask 0x53, alignment-fill bits 0xff + let gc = ok(&w); + assert_eq!(gc.mask, 0x53); + assert_eq!(gc.apply(0x5a, 0xa5), 0xf6); + w[GC_WRITE_MASK_FIRST] = 0xf0000f; + assert!(ok(&w).is_noop()); + w[GC_WRITE_MASK_FIRST] = CI[4] ^ 1; + assert_eq!(Gc::decode(&w), Err(Status::BadMask)); + w[GC_WRITE_MASK_FIRST] = CI[4]; + w[GC_FOREGROUND_HIGH] |= 0x100; + assert_eq!(Gc::decode(&w), Err(Status::BadColour)); + } + + #[test] + fn an_rgb_context_decodes_every_widened_byte() { + let mut w = RGB; + for b in 0..256u64 { + let component = b << 4 | b >> 4; + let colour = component << 40 | component << 27 | component << 14 | 0x1ffe; + w[GC_FOREGROUND_HIGH] = (colour >> 32) as u32; + w[GC_FOREGROUND_LOW] = colour as u32; + w[GC_WRITE_MASK_FIRST] = (component | component << 12) as u32; + w[GC_WRITE_MASK_SECOND] = 0xfff000 | component as u32; + let gc = ok(&w); + assert_eq!((gc.foreground, gc.mask), (b as u32 * 0x010101, b as u32 * 0x010101)); + } + let mut w = RGB; + for m in 0..4096u32 { + let byte = m >> 4; + let canonical = m == (byte << 4 | byte >> 4); + w[GC_WRITE_MASK_FIRST] = 0xfff000 | m; + assert_eq!(Gc::decode(&w).map(|_| ()), if canonical { Ok(()) } else { Err(Status::BadMask) }, "{m:#x}"); + } + let mut w = RGB; + w[GC_FOREGROUND_HIGH] = 0xff000; + assert_eq!(Gc::decode(&w), Err(Status::BadColour)); + w[GC_FOREGROUND_HIGH] = 0; + w[GC_BACKGROUND_HIGH] = 0xff000; + assert_eq!(Gc::decode(&w), Err(Status::BadColour)); + } + + #[test] + fn a_display_id_context_decodes_both_halves() { + for v in (0..=0xffffu32).step_by(7).chain([0xffff]) { + let mut w = DID; + w[GC_FOREGROUND_HIGH] = (v & 15) << 3 | (v & 0xfff0) << 4; + w[GC_WRITE_MASK_FIRST] = (v >> 4) & 0xfff | (v & 15) << 20; + w[GC_WRITE_MASK_SECOND] = 0; + let gc = ok(&w); + assert_eq!((gc.target, gc.foreground, gc.mask), (Target::Did, v, v)); + w[GC_FOREGROUND_HIGH] = 0; + w[GC_WRITE_MASK_FIRST] = 0; + w[GC_FOREGROUND_LOW] = (v & 15) << 9 | (v & 0xfff0) << 10; + w[GC_WRITE_MASK_SECOND] = (v >> 12) & 15 | (v & 0xfff) << 12; + let gc = ok(&w); + assert_eq!((gc.foreground, gc.mask), (v, v)); + } + let mut w = DID; + assert_eq!(ok(&w).apply(0, 0xa5c3), 0xa503); + w[GC_WRITE_MASK_FIRST] = 0x300a5c; + assert_eq!(Gc::decode(&w), Err(Status::MixedDid)); + w[GC_WRITE_MASK_SECOND] = 0; + w[GC_FOREGROUND_HIGH] = 0x80; + assert_eq!(Gc::decode(&w), Err(Status::BadColour)); + let mut w = DID; + w[GC_BLEND_AND_LOGIC] = 10; + assert_eq!(Gc::decode(&w), Err(Status::BadFields)); + } + + #[test] + fn the_ancillary_plane_sets_decode_to_their_targets() { + for (cfg, target) in [ + (0x182c, Target::Overlay0), + (0x2c, Target::Overlay0), + (0x102c, Target::Overlay0), + (0x1830, Target::Overlay1), + (0x1820, Target::Popup), + (0x20, Target::Popup), + (0x1020, Target::Popup), + ] { + let mut w = CI; + w[GC_FOREGROUND_HIGH] = 0x78; + w[GC_BACKGROUND_HIGH] = 0x38; + w[GC_WRITE_MASK_FIRST] = if target == Target::Popup { 0x300000 } else { 0xf00000 }; + w[GC_FRAMEBUFFER_CONFIG] = cfg; + w[GC_OPERATION] = if cfg & 0x3c == 0x30 { 2 } else { 1 }; + w[GC_FRAMEBUFFER_BASES] = 0; + if cfg < 0x1000 { + w[GC_PIXEL_PACK_CODE] = 0; + } + let gc = ok(&w); + let (fg, m) = if target == Target::Popup { (3, 3) } else { (15, 15) }; + assert_eq!((gc.target, gc.foreground, gc.mask), (target, fg, m), "{cfg:#x}"); + } + let mut w = CI; + w[GC_FRAMEBUFFER_CONFIG] = 0xc; + w[GC_OPERATION] = 1; + w[GC_PIXEL_PACK_CODE] = 0; + assert_eq!(ok(&w).target, Target::MainCi8); + w[GC_FOREGROUND_HIGH] = 0x93000; + assert_eq!(ok(&w).target, Target::MainCi8); + w[GC_WRITE_MASK_FIRST] |= 0x8000_0000; + assert!(ok(&w).opaque_or_tiled); + let mut w = RGB; + w[GC_OPERATION] = 0x7c001; + assert_eq!(Gc::decode(&w), Err(Status::BadFields)); + w[GC_FRAMEBUFFER_CONFIG] = 0; + w[GC_OPERATION] = 0x64001; + w[GC_PIXEL_PACK_CODE] = 9; + assert_eq!(ok(&w).target, Target::MainRgb8); + w[GC_OPERATION] = 0xa4001; + assert_eq!(Gc::decode(&w), Err(Status::Deferred)); + assert_eq!(Gc::decode(&[0; 13]), Err(Status::BadFields)); + } + + /// The 30-bit packing: each 10-bit component to `c << 2 | c >> 8`; the + /// model keeps the top eight bits. + #[test] + fn a_thirty_bit_context_keeps_eight_bits_a_component() { + fn words(pixel: u32) -> (u32, u32) { + let mut colour = 0x1ffeu64; + for k in 1..=3u32 { + let c = (pixel >> (10 * (k - 1))) & 0x3ff; + colour |= ((c << 2 | c >> 8) as u64) << (13 * k + 1); + } + ((colour >> 32) as u32, colour as u32) + } + let mut w = [0, 0, 0, 0, 0xffffff, 0xffffff, 0, 0x64001, 1, 6, 9, 0, 0]; + (w[GC_FOREGROUND_HIGH], w[GC_FOREGROUND_LOW]) = words(0x123 << 20 | 0x2c7 << 10 | 0x0a5); + (w[GC_BACKGROUND_HIGH], w[GC_BACKGROUND_LOW]) = words(0x3ff << 20 | 0x001 << 10 | 0x200); + let gc = ok(&w); + assert_eq!((gc.target, gc.foreground, gc.background, gc.mask), (Target::MainRgb8, 0x48b129, 0xff0080, 0xffffff)); + w[GC_OPERATION] = 1; + w[GC_PIXEL_PACK_CODE] = 0; + assert_eq!(ok(&w).foreground, 0x48b129); + w[GC_FOREGROUND_LOW] ^= 1 << 14; + assert_eq!(Gc::decode(&w), Err(Status::BadColour)); + w[GC_FOREGROUND_LOW] ^= 1 << 14; + w[GC_PIXEL_PACK_CODE] = 5; + assert_eq!(Gc::decode(&w), Err(Status::BadFields)); + } + + #[test] + fn a_repeated_buffer_selector_reads_as_the_same_selection() { + let mut w = RGB; + w[GC_OPERATION] = 0x60005; + let gc = ok(&w); + assert_eq!((gc.target, gc.first_selector), (Target::MainRgb8, 1)); + w[GC_FRAMEBUFFER_CONFIG] = 4; + w[GC_OPERATION] = 5; + w[GC_PIXEL_PACK_CODE] = 0; + assert!(Gc::decode(&w).is_ok()); + w[GC_OPERATION] = 6; + assert_eq!(Gc::decode(&w), Err(Status::Deferred)); + let mut w = CI; + w[GC_OPERATION] = 0x40005; + assert!(Gc::decode(&w).is_ok()); + w[GC_OPERATION] = 0x40006; + assert_eq!(Gc::decode(&w), Err(Status::Deferred)); + } + + #[test] + fn masks_and_operations_apply_as_the_gc_says() { + for gx in 0..16u32 { + let mut w = CI; + w[GC_BLEND_AND_LOGIC] = 2 * gx; + w[GC_WRITE_MASK_FIRST] = 0xf0053f; + let gc = ok(&w); + assert_eq!(gc.gx as u32, gx); + assert_eq!(gc.apply(0x195, 0x26a), (LogicOp::from_index(gx).apply(0x95, 0x6a) & 0x53) | (0x6a & 0xac)); + assert_eq!(gc.is_noop(), gx == 5); + assert_eq!(gc.needs_destination(), gx != 5); + } + let mut w = CI; + for gx in 0..16u32 { + w[GC_BLEND_AND_LOGIC] = 2 * gx; + assert_eq!(ok(&w).needs_destination(), !matches!(gx, 0 | 3 | 5 | 12 | 15)); + } + for bl in (1..32).step_by(2) { + w[GC_BLEND_AND_LOGIC] = bl; + assert_eq!(Gc::decode(&w), Err(Status::BadFields)); + } + w[GC_BLEND_AND_LOGIC] = 32; + assert_eq!(Gc::decode(&w), Err(Status::BadFields)); + } + + #[test] + fn reserved_bits_refuse() { + let mut w = CI; + w[GC_FRAMEBUFFER_CONFIG] = 0x1820; + w[GC_OPERATION] = 1; + w[GC_FRAMEBUFFER_BASES] = 0; + for bit in 22..=23 { + w[GC_WRITE_MASK_FIRST] = 0x300000 | 1 << bit; + assert_eq!(Gc::decode(&w), Err(Status::BadMask)); + } + for pair in [0, 2] { + for gap in [0, 13, 26, 39, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63] { + let mut w = RGB; + let v = 1u64 << gap; + w[pair] = (v >> 32) as u32; + w[pair + 1] = v as u32; + assert_eq!(Gc::decode(&w), Err(Status::BadColour), "pair {pair} gap {gap}"); + } + let mut w = RGB; + w[pair + 1] = 2; + assert_eq!(Gc::decode(&w), Err(Status::BadColour)); + w[pair + 1] = 0x1ffe; + assert!(Gc::decode(&w).is_ok()); + } + let mut w = CI; + (w[GC_FRAMEBUFFER_CONFIG], w[GC_OPERATION], w[GC_FRAMEBUFFER_BASES], w[GC_PIXEL_PACK_CODE]) = (0x30, 2, 0, 0); + assert_eq!(Gc::decode(&w), Err(Status::Deferred)); + (w[GC_FRAMEBUFFER_CONFIG], w[GC_PIXEL_PACK_CODE]) = (0x1030, 5); + assert_eq!(Gc::decode(&w), Err(Status::Deferred)); + } + + #[test] + fn the_overlay_layer_clear_decodes_and_its_siblings_defer() { + let mut w = [0, 0x1ffe, 0, 0x1ffe, 0xf00fff, 0, 0x28, 3, 0, 6, 0, 1, 1]; + let gc = ok(&w); + assert_eq!((gc.target, gc.foreground, gc.mask, gc.width_mask), (Target::OverlayBoth, 0, 0xff, 0xff)); + w[GC_FOREGROUND_HIGH] = 0x5a5 << 8 | 0xa << 3; + assert_eq!(ok(&w).foreground, 0x5a); + w[GC_FOREGROUND_HIGH] = 0x5a000; + assert_eq!(Gc::decode(&w), Err(Status::BadColour)); + w[GC_FOREGROUND_HIGH] = 0; + w[GC_WRITE_MASK_FIRST] = 0xfff; + assert_eq!(Gc::decode(&w), Err(Status::BadMask)); + w[GC_WRITE_MASK_FIRST] = 0xf00fff; + w[GC_OPERATION] = 1; + assert_eq!(Gc::decode(&w), Err(Status::Deferred)); + (w[GC_FRAMEBUFFER_CONFIG], w[GC_PIXEL_PACK_CODE], w[GC_OPERATION]) = (0x1828, 5, 3); + assert_eq!(Gc::decode(&w), Err(Status::Deferred)); + } + + /// The fills read configurations 0xc and 0x180c as the native 16-bit index. + #[test] + fn a_fill_reads_the_native_index_form_for_its_two_configurations() { + let mut w = CI; + w[GC_FRAMEBUFFER_CONFIG] = 0xc; + w[GC_OPERATION] = 1; + w[GC_PIXEL_PACK_CODE] = 0; + assert_eq!(Gc::decode_for_fill(&w).unwrap().target, Target::MainIndex16); + assert_eq!(Gc::decode_main_index(&CI).unwrap().target, Target::MainIndex16); + assert_eq!(Gc::decode_main_index(&RGB), Err(Status::BadFields)); + assert_eq!(Gc::decode_for_fill(&RGB).unwrap().target, Target::MainRgb8); + } +} diff --git a/src/dev/kona/back/x/line.rs b/src/dev/kona/back/x/line.rs new file mode 100644 index 000000000..cc212142a --- /dev/null +++ b/src/dev/kona/back/x/line.rs @@ -0,0 +1,434 @@ +//! The X server's lines: segments and polylines. +//! +//! Segments (0x159 with one, 0x15a with six) are independent pairs of points, +//! `x << 16 | y` with signed halves, each point added to the drawable's position. +//! Polylines (0x15b with one word, 0x15c with seven) join their points in order, a +//! longer one arriving as n - 1 words of data preload and the one-word token for its last +//! point (the sender counts are established, the pipe's consumption is inferred); +//! relative polylines (0x15d with two, 0x15e with seven) take each point as a step from the +//! previous one, the first from the drawable's origin, as relative points do. The senders +//! do not zero the position (386 of the 1192 position loads in the desktop recording are +//! nonzero); the position loads are established and the add to every vertex, absolute +//! and relative, is inferred. +//! The token rows and their word counts are established except for the burst +//! of six segments (0x15a), which is inferred. What the segment token (0x159) +//! draws is unknown and what the polyline tokens draw is inferred; the framing of +//! the two polyline bursts is the shipped X server's wire contract (seven words), +//! established. +//! Lines draw under the context as it stands, through the clip, scissor, +//! stipple, write mask and logic operation, as a request checked whole before +//! any pixel is written. A polyline takes the points of a waiting data run +//! before its own, as the X server sends a long one (the sender counts are established, the +//! pipe's consumption inferred); a segment token +//! does not, and ends a run. A relative polyline starts from the drawable's +//! origin with every command, as relative points do (established). +//! +//! Which pixels a line lights is unknown at the device level: the engine's +//! line drawer is known only to operation counts, so the tie and rounding rule, whether the last end point is lit, whether a +//! segment of no length lights a pixel and whether a vertex shared by two +//! segments is lit once are all unread. The evidence once offered for a +//! one-pixel segment of no length is a line of width zero, not a segment of +//! no length. What the model does is therefore a labelled approximation, not a +//! reading: a segment draws both end points, a segment of no length draws one +//! pixel, a vertex shared by two segments of a polyline is drawn once, and a +//! polyline is not closed unless the caller's last point is its first (that +//! it has no implicit close is inferred). The line is the integer line through +//! pixel centres, and where it falls exactly between two pixels of the minor +//! axis it takes the one nearer the end point. A real pipe's choice would show in a short line of odd slope drawn +//! by the X server and read back, or in the pipe's line routine if it were +//! read. Also not known: whether the last vertex of a polyline is drawn or is +//! left to the next command that shares it (the model draws it), whether a +//! closing vertex that is the first point is drawn twice (the model does). +//! +//! Ported from sgi_kona_x_lines in hw/misc/sgi-kona.c in +//! jamesbraid/qemu-sgi-origami, which draws the same shapes by an error term +//! and a coordinate wrap; this walks the major axis and keeps the minor axis's +//! remainder. +//! +//! # Cost +//! +//! 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 outside the screen are not +//! made. The pixel loops and the destination check are the fills' own. + +use crate::dev::kona::defs::*; +use super::fill::Box4; +use super::state::XState; +use crate::dev::kona::back::raster::Surface; + +pub const X_SEGMENT: u32 = DRAW_LINE_SEGMENTS; +pub const X_SEGMENT6: u32 = DRAW_LINE_SEGMENTS_BURST_OF_6; +pub const X_LINE: u32 = DRAW_CONNECTED_POLYLINE; +pub const X_LINE6: u32 = DRAW_CONNECTED_POLYLINE_BURST_OF_6; +pub const X_RLINE: u32 = DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS; +pub const X_RLINE6: u32 = DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS_BURST_OF_6; + +/// Whether `selector` is a segment or polyline token. +pub fn is_line(selector: u32) -> bool { + matches!(selector, X_SEGMENT | X_SEGMENT6 | X_LINE | X_LINE6 | X_RLINE | X_RLINE6) +} + +/// Whether `selector` is one of the four polyline tokens (absolute or relative, one segment or a burst). +pub fn is_polyline(selector: u32) -> bool { + matches!(selector, X_LINE | X_LINE6 | X_RLINE | X_RLINE6) +} + +fn point(w: u32) -> (i64, i64) { + ((w >> 16) as u16 as i16 as i64, w as u16 as i16 as i64) +} + +/// Add the pixel (`x`, `y`) to `boxes`, widening the last box when it is a +/// row or column the pixel continues, and not at all when it is off screen. +fn plot(boxes: &mut Vec, bounds: &super::state::Bounds, x: i64, y: i64) { + if x < bounds.x1 || x >= bounds.x2 || y < bounds.y1 || y >= bounds.y2 { + return; + } + if let Some(last) = boxes.last_mut() { + let column = last.0 == x && last.2 == x + 1; + let row = last.1 == y && last.3 == y + 1; + if column && y == last.3 { + last.3 += 1; + return; + } + if column && y + 1 == last.1 { + last.1 = y; + return; + } + if row && x == last.2 { + last.2 += 1; + return; + } + if row && x + 1 == last.0 { + last.0 = x; + return; + } + } + boxes.push((x, y, x + 1, y + 1)); +} + +/// The pixels of the line from `a` to `b`, in order from the one after `a` +/// (and `a` too when `first`). The major axis steps once a pixel; the minor +/// axis takes `floor((n + 2 i m) / 2n)` steps by pixel `i`, with `n` and `m` +/// the major and minor extents, which rounds an exact half toward `b`. +fn walk(boxes: &mut Vec, bounds: &super::state::Bounds, a: (i64, i64), b: (i64, i64), first: bool) { + let (dx, dy) = (b.0 - a.0, b.1 - a.1); + let (n, m) = (dx.abs().max(dy.abs()), dx.abs().min(dy.abs())); + if first { + plot(boxes, bounds, a.0, a.1); + } + let x_major = dx.abs() >= dy.abs(); + let (major_step, minor_step) = if x_major { (dx.signum(), dy.signum()) } else { (dy.signum(), dx.signum()) }; + let (mut major, mut minor, mut acc) = (0i64, 0i64, n); + for _ in 0..n { + major += major_step; + acc += 2 * m; + if acc >= 2 * n { + acc -= 2 * n; + minor += minor_step; + } + if x_major { + plot(boxes, bounds, a.0 + major, a.1 + minor); + } else { + plot(boxes, bounds, a.0 + minor, a.1 + major); + } + } +} + +impl XState { + /// A segment or polyline token. The words of a waiting data run are not + /// part of a line: the arrival of this token has already dropped them. + pub(super) fn lines(&mut self, surface: &Surface<'_>, selector: u32, args: &[u32]) { + let (width, height) = surface.size(); + let bounds = self.bounds(width, height); + let mut boxes = std::mem::take(&mut self.boxes); + boxes.clear(); + if matches!(selector, X_SEGMENT | X_SEGMENT6) { + let (ox, oy) = self.origin(); + for pair in args.chunks_exact(2) { + // Both ends, and a single pixel when they are the same point. + let (a, b) = (point(pair[0]), point(pair[1])); + walk(&mut boxes, &bounds, (a.0 + ox, a.1 + oy), (b.0 + ox, b.1 + oy), true); + } + } else { + let relative = matches!(selector, X_RLINE | X_RLINE6); + // A relative polyline restarts at the drawable's origin with each command, as relative + // points do (established); an absolute one adds the origin to each point. + let (ox, oy) = self.origin(); + let (mut sx, mut sy) = if relative { (ox, oy) } else { (0, 0) }; + let mut previous = None; + // The points of a waiting data run come first, then the token's: the X server sends all + // but the last of a long polyline as a run (inferred from the sender). + let mut run = std::mem::take(&mut self.run); + for &w in run.iter().chain(args) { + let (px, py) = point(w); + let p = if relative { + sx += px; + sy += py; + (sx, sy) + } else { + (px + ox, py + oy) + }; + // The first point, then each segment without its start pixel, + // which the segment before it has drawn. + match previous { + None => plot(&mut boxes, &bounds, p.0, p.1), + Some(q) => walk(&mut boxes, &bounds, q, p, false), + } + previous = Some(p); + } + run.clear(); + self.run = run; + } + self.run_request(surface, boxes.iter().copied(), false); + self.boxes = boxes; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::back::store::PixelStore; + + /// An index-16 context: foreground 0x5a00, full mask. + const GC: [u32; 13] = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + + fn rig() -> (XState, PixelStore) { + let mut x = XState::default(); + x.command(0x129, &GC); + (x, PixelStore::new(32, 32)) + } + + fn pack(a: i32, b: i32) -> u32 { + (a as u32) << 16 | (b as u32 & 0xffff) + } + + /// The lit pixels, as X coordinates, in row order. + fn lit(s: &PixelStore) -> Vec<(u32, u32)> { + (0..32).flat_map(|y| (0..32).map(move |x| (x, y))).filter(|&(x, y)| s.read(x, 31 - y, 1) != 0).collect() + } + + #[test] + fn a_segment_draws_both_ends_and_runs_along_a_row() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + assert!(x.draw_command(&d, X_SEGMENT, &[pack(2, 3), pack(6, 3)])); + assert_eq!(lit(&s), [(2, 3), (3, 3), (4, 3), (5, 3), (6, 3)]); + // Backwards is the same pixels. + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(6, 3), pack(2, 3)]); + assert_eq!(lit(&s), [(2, 3), (3, 3), (4, 3), (5, 3), (6, 3)]); + } + + #[test] + fn a_segment_of_no_length_is_one_pixel() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(7, 9), pack(7, 9)]); + assert_eq!(lit(&s), [(7, 9)]); + } + + /// A diagonal lights its diagonal; where a line is exactly half a pixel + /// along the minor axis it takes the pixel on the end point's side + /// (approximated). + #[test] + fn diagonal_and_sloped_lines_follow_the_pixel_centres() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(1, 1), pack(4, 4)]); + assert_eq!(lit(&s), [(1, 1), (2, 2), (3, 3), (4, 4)]); + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + // Four columns over two rows: the line is half a pixel along the + // minor axis at the first column and at the third, and each exact half + // goes to the end point's side. + x.draw_command(&d, X_SEGMENT, &[pack(0, 0), pack(4, 2)]); + assert_eq!(lit(&s), [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2)]); + // A steep line is the same shape on the other axis. + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(0, 0), pack(2, 4)]); + assert_eq!(lit(&s), [(0, 0), (1, 1), (1, 2), (2, 3), (2, 4)]); + } + + /// Each segment of a burst is its own, and segments that meet draw the + /// shared pixel for each. + #[test] + fn a_burst_draws_six_independent_segments() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + let mut w = Vec::new(); + for i in 0..6 { + w.extend([pack(0, 2 * i), pack(3, 2 * i)]); + } + x.draw_command(&d, X_SEGMENT6, &w); + assert_eq!(lit(&s).len(), 6 * 4); + // Two segments through one pixel under XOR clear it. + let (mut x, s) = rig(); + let mut gc = GC; + gc[9] = 12; // logic operation 6, xor + x.command(0x129, &gc); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(0, 5), pack(4, 5)]); + x.draw_command(&d, X_SEGMENT, &[pack(4, 5), pack(4, 8)]); + assert!(!lit(&s).contains(&(4, 5)), "the meeting pixel was drawn by both"); + assert_eq!(lit(&s).len(), 4 + 3); + } + + /// A polyline draws a vertex shared by two of its segments once, even + /// where the write would undo itself, and does not close itself. + #[test] + fn a_polyline_draws_its_interior_vertices_once_and_does_not_close() { + let (mut x, s) = rig(); + let mut gc = GC; + gc[9] = 12; + x.command(0x129, &gc); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_LINE6, &[pack(0, 0), pack(4, 0), pack(4, 4), pack(0, 4), pack(0, 1), pack(0, 1), pack(0, 1)]); + let got = lit(&s); + assert!(got.contains(&(4, 0)) && got.contains(&(4, 4)) && got.contains(&(0, 4)), "{got:?}"); + assert!(got.contains(&(0, 0)), "the start"); + // Row 0 (5), column 4 (4 more), row 4 (4 more), column 0 up from row 4 to row 1 (3 more). + assert_eq!(got.len(), 5 + 4 + 4 + 3); + // The last point of the line is (0, 1): (0, 0) is lit only as the start. + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_LINE, &[pack(3, 3), pack(8, 3)]); + assert_eq!(lit(&s).len(), 6); + } + + /// A polyline of two to five points arrives as one to four words of data preload and the one-word token + /// (the sender counts), and a single word alone is a single pixel. + #[test] + fn a_polyline_of_two_to_five_points_is_a_run_and_the_one_word_token() { + for points in 2..=5 { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + let run: Vec = (0..points - 1).map(|i| pack(2 * i, 4)).collect(); + x.draw_command(&d, 0x140 + run.len() as u32, &run); + x.draw_command(&d, X_LINE, &[pack(2 * (points - 1), 4)]); + let want: Vec<(u32, u32)> = (0..=2 * (points as u32 - 1)).map(|c| (c, 4)).collect(); + assert_eq!(lit(&s), want, "{points} points"); + assert_eq!(x.runs_dropped, 0); + } + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_LINE, &[pack(5, 6)]); + assert_eq!(lit(&s), [(5, 6)]); + } + + /// The drawable's position is added to every vertex of a segment and of an absolute polyline, with or + /// without a data run, as it is to a relative polyline's start. + #[test] + fn the_position_is_added_to_every_vertex() { + let (mut x, s) = rig(); + x.command(0x12a, &[10, 20, 100, 100]); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(1, 1), pack(3, 1)]); + assert_eq!(lit(&s), [(11, 21), (12, 21), (13, 21)]); + x.draw_command(&d, X_SEGMENT6, &[pack(0, 2), pack(1, 2), pack(0, 3), pack(1, 3), pack(0, 4), pack(1, 4), pack(0, 5), pack(1, 5), pack(0, 6), pack(1, 6), pack(0, 7), pack(1, 7)]); + assert_eq!(lit(&s).len(), 3 + 12); + assert!(lit(&s).contains(&(11, 27))); + let (mut x, s) = rig(); + x.command(0x12a, &[10, 20, 100, 100]); + let d = Surface::new(&s, 1); + x.draw_command(&d, 0x141, &[pack(0, 0)]); + x.draw_command(&d, X_LINE, &[pack(2, 0)]); + assert_eq!(lit(&s), [(10, 20), (11, 20), (12, 20)]); + x.draw_command(&d, X_LINE6, &[pack(0, 3), pack(2, 3), pack(2, 3), pack(2, 3), pack(2, 3), pack(2, 3), pack(2, 3)]); + assert_eq!(lit(&s).len(), 3 + 3); + assert!(lit(&s).contains(&(10, 23)) && lit(&s).contains(&(12, 23))); + } + + /// Relative polylines step from the previous point, the first from the + /// drawable's origin. + #[test] + fn a_relative_polyline_steps_from_the_origin_and_the_previous_point() { + let (mut x, s) = rig(); + x.command(0x12a, &[10, 20, 100, 100]); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_RLINE, &[pack(1, 1), pack(3, 0)]); + assert_eq!(lit(&s), [(11, 21), (12, 21), (13, 21), (14, 21)]); + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_RLINE6, &[pack(2, 2), pack(2, 0), pack(0, 2), pack(-2, 0), pack(0, 0), pack(0, 0), pack(0, 0)]); + // From (2, 2) right to (4, 2), down to (4, 4), left to (2, 4). + assert_eq!(lit(&s).len(), 3 + 2 + 2); + } + + /// Pixels off the screen are not drawn, and a line cut by the clip draws + /// the part inside. + #[test] + fn a_line_is_clipped_to_the_screen_and_the_clip_list() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(-5, 2), pack(3, 2)]); + assert_eq!(lit(&s), [(0, 2), (1, 2), (2, 2), (3, 2)]); + x.command(0x12b, &[1, 10, 0, 12, 32]); + x.draw_command(&d, X_SEGMENT, &[pack(8, 6), pack(14, 6)]); + assert_eq!(lit(&s).iter().filter(|p| p.1 == 6).count(), 2); + x.draw_command(&d, X_SEGMENT, &[pack(-30000, -30000), pack(30000, 30000)]); + } + + /// A line whose destination does not fit the target draws nothing, as + /// every other fill is checked before it is drawn. + #[test] + fn a_line_over_a_wide_destination_is_refused_whole() { + let (mut x, s) = rig(); + let mut masked = GC; + masked[4] = 0xf0053f; + x.command(0x129, &masked); + s.write(4, 31 - 3, 1, 0x12_3456); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_SEGMENT, &[pack(0, 3), pack(8, 3)]); + assert_eq!((x.fills.bad_destination, s.read(0, 31 - 3, 1)), (1, 0)); + } + + /// A segment token ends a waiting data run, which no segment uses. + #[test] + fn a_segment_token_drops_a_waiting_run() { + let (mut x, _s) = rig(); + x.command(0x141, &[pack(1, 1)]); + x.collect_command(0x141, &[pack(1, 1)]); + assert!(!x.command(X_SEGMENT, &[])); + assert_eq!(x.runs_dropped, 1); + } + + /// A polyline takes the points of a waiting run before its own, as the X server sends a long one + /// (inferred), and the run is spent; a relative polyline starts at the origin with every command. + #[test] + fn a_polyline_joins_a_waiting_data_run() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + for p in [pack(2, 2), pack(6, 2)] { + x.command(0x141, &[p]); + x.collect_command(0x141, &[p]); + } + x.end_runs(X_LINE); + assert_eq!(x.runs_dropped, 0, "a polyline keeps the run"); + x.draw_command(&d, X_LINE, &[pack(6, 5), pack(2, 5)]); + assert_eq!(lit(&s).len(), 5 + 3 + 4, "(2,2)-(6,2), then down to (6,5), then to (2,5), shared vertices once"); + assert!(lit(&s).contains(&(2, 2)) && lit(&s).contains(&(2, 5))); + // Spent: a second polyline starts from its own first point. + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.command(0x141, &[pack(2, 2)]); + x.collect_command(0x141, &[pack(2, 2)]); + x.draw_command(&d, X_LINE, &[pack(4, 4), pack(4, 6)]); + x.draw_command(&d, X_LINE, &[pack(9, 9), pack(9, 9)]); + assert!(!lit(&s).contains(&(2, 2)) || lit(&s).contains(&(3, 3)), "the first run joined the first polyline only"); + assert_eq!(lit(&s).iter().filter(|p| p.0 == 9).count(), 1); + } + + /// A relative polyline's first point is a step from the drawable's origin, every command. + #[test] + fn a_relative_polyline_restarts_at_the_origin_each_command() { + let (mut x, s) = rig(); + let d = Surface::new(&s, 1); + x.draw_command(&d, X_RLINE, &[pack(3, 3), pack(2, 0)]); + x.draw_command(&d, X_RLINE, &[pack(3, 3), pack(0, 2)]); + let l = lit(&s); + assert!(l.contains(&(3, 3)) && l.contains(&(5, 3)) && l.contains(&(3, 5))); + assert!(!l.contains(&(6, 6)), "the second command did not continue from the first's end"); + } +} diff --git a/src/dev/kona/back/x/mod.rs b/src/dev/kona/back/x/mod.rs new file mode 100644 index 000000000..5ece203da --- /dev/null +++ b/src/dev/kona/back/x/mod.rs @@ -0,0 +1,12 @@ +//! The X server's integer path, drawing inline. +//! +//! `state` and `gc`: the graphics context and the drawing state loaded around +//! it; `fill`: fills and the stipple they use (`stipple` is its cache); +//! `line`: segments and polylines; `pixel`: pixel writes, reads and copies. + +pub mod fill; +pub mod gc; +pub mod line; +pub mod pixel; +pub mod state; +pub mod stipple; diff --git a/src/dev/kona/back/x/pixel.rs b/src/dev/kona/back/x/pixel.rs new file mode 100644 index 000000000..9e8d00627 --- /dev/null +++ b/src/dev/kona/back/x/pixel.rs @@ -0,0 +1,1285 @@ +//! The X server's pixel transfers: setup, write, read, copy and finish. +//! +//! - 0x136 (`x_pixel_setup`) seven words that the next transfer reads: mode, +//! source packing, destination packing, shift, width, height and pad. The +//! packing and shift words are table entries whose meaning to the pipe is +//! unknown; this model reads the forms the X server is known to send and +//! refuses the rest, counted. +//! - 0x139 (`x_pixel_write`) the position of an image whose rows follow, either +//! behind the command as a data run the back end sizes, or, with the DMA bit +//! of the mode word set, in a DMA packet that arrives next. Rows run down from +//! the position, each `ceil((pad + bits * width) / 32)` words, pixels packed +//! most significant bit first (inferred from the senders' packing tables). +//! - 0x13a (`x_pixel_finish`) ends the write. +//! - 0x137 (`x_pixel_copy`) moves a rectangle inside the store. +//! - 0x138 (`x_pixel_read`) returns a rectangle on the return page. +//! +//! Grades. The word layouts, the pixel widths a source packing names and the +//! forms of context each transfer accepts are software expectations from the +//! senders, ported from hw/display/sgi/x-gc.c and hw/misc/sgi-kona.c in +//! jamesbraid/qemu-sgi-origami. What a clear bit of a one-bit image paints, +//! and the conversion of a packed pixel to the plane's word, are inferred +//! there and here. +//! +//! Approximated: the model applies each row as it arrives, where the model +//! this was taken from stages a batch of writes and commits it at the finish. +//! The two agree because a logic operation acts on each bit alone, so applying +//! operations in order gives the staged result. The one difference is that +//! this model checks the destination before any pixel is written only for +//! the write in hand, not for the whole batch. +//! +//! Refused, counted and drawn nowhere: the window-copy pair (source and +//! destination packing 3), a write or copy aimed at the overlay, popup or +//! display-ID planes (fills draw those; transfers do not) and the two-buffer RGB +//! form, and a frame-buffer word read, which tells an index from a direct colour by a tag +//! this store does not keep. A read that is refused still completes, with the +//! whole usable page zeroed: the X server waits on the page's first word with +//! no timeout, and what the pipe answers to a read it cannot perform is +//! unknown (approximated, as the model this was taken from does). +//! +//! # Cost +//! +//! A write is planned once, as a fill is. The pixel loop is chosen at compile +//! time by the packing kind (packed, one-bit, opaque one-bit), whether a +//! stipple masks it and the logic operation, and does an integer window of two +//! words, a shift and a store per pixel; the row's words are copied once with +//! a zero after them, so the window never reads past the row. What it still +//! does per pixel is two bounds-checked word loads and, for a one-bit image, a +//! skip of each clear bit, which is data and cannot be hoisted. A copy checks +//! its source (and, for a write that reads its destination, its destination) +//! in loops of their own and then stores under a logic operation that is a +//! constant of the row loop. A read hoists the packing out of the pixel loop: +//! one loop for each of one, two and four bytes. Only `WordArray::store` writes +//! a pixel. + +use crate::dev::kona::defs::*; +use super::gc::{Gc, Status, Target}; +use super::state::XState; +use super::fill::{apply, Plan, Refused}; +use crate::dev::kona::back::raster::Surface; + +pub const X_PIXEL_SETUP: u32 = WINDOW_SYSTEM_PIXEL_PATH_SET_UP; +pub const X_PIXEL_COPY: u32 = WINDOW_SYSTEM_PIXEL_COPY; +pub const X_PIXEL_READ: u32 = WINDOW_SYSTEM_PIXEL_READ; +pub const X_PIXEL_WRITE: u32 = WINDOW_SYSTEM_PIXEL_WRITE; +pub const X_PIXEL_FINISH: u32 = WINDOW_SYSTEM_PIXEL_PATH_FINISH; + +/// The setup words in send order. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Setup { + pub mode: u32, + pub src: u32, + pub dst: u32, + pub shift: u32, + pub width: u32, + pub height: u32, + pub pad: u32, +} + +impl Setup { + /// The mode word's bit that sends a write's rows by DMA. + pub const MODE_DMA: u32 = PIXEL_SETUP_DIRECT_WRITE_FLAG; + + fn from_words(w: &[u32; PIXEL_SETUP_WORDS]) -> Setup { + Setup { + mode: w[PIXEL_SETUP_TRANSFER_KIND], + src: w[PIXEL_SETUP_OUTPUT_MODULE], + dst: w[PIXEL_SETUP_CONVERSION_OR_PACKER], + shift: w[PIXEL_SETUP_ENGINE_VALUE], + width: w[PIXEL_SETUP_WIDTH], + height: w[PIXEL_SETUP_HEIGHT], + pad: w[PIXEL_SETUP_DISTRIBUTOR_VALUE], + } + } + + /// Bits a source pixel takes, or None for a packing this model does not + /// know. 4 and 5 are the one-bit forms. + fn bits(&self) -> Option { + match self.src { + 0 => Some(8), + 1 => Some(16), + 2 => Some(32), + 4 | 5 => Some(1), + _ => None, + } + } + + /// Words a row takes, and words an image takes; None past what the model + /// accepts or for an unknown packing. + fn words(&self) -> Option<(usize, usize)> { + let bits = self.bits()? as u64; + let stride = (self.pad as u64 + bits * self.width as u64).div_ceil(32); + let all = stride.checked_mul(self.height as u64)?; + (all <= MAX_WRITE_WORDS as u64).then_some((stride as usize, all as usize)) + } +} + +/// The most words one write may carry: 64 MiB, the model's own bound. +pub const MAX_WRITE_WORDS: usize = 16 << 20; + +/// The largest reply a read can post: the return page less its first two words. +const READ_LIMIT: usize = 0x4000 - 8; + +/// A write that has announced its rows and waits for them. +#[derive(Clone, Copy, Debug)] +struct Pending { + x: i32, + y: i32, + words: usize, + dma: bool, +} + +/// What the pixel path did, for `kona status`. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PixelCounts { + pub setups: u64, + pub writes: u64, + pub dma_writes: u64, + pub copies: u64, + pub reads: u64, + pub finishes: u64, + /// Writes and copies refused: the context or setup was not understood, + /// the target has no store, a destination word did not fit, or the + /// window-copy pair. + pub bad_context: u64, + pub other_target: u64, + pub bad_destination: u64, + pub window_copies: u64, + /// Reads answered with zeros, and of them frame-buffer word reads. + pub reads_refused: u64, + pub fb_word_reads: u64, + /// Writes whose rows did not arrive whole, rows that arrived with none + /// waiting, and writes that ended with rows still owed. + pub short: u64, + pub unexpected: u64, + /// Read brackets that finished refused or without a read, and those whose + /// host buffer could not be written or whose reason page was missing, which + /// post no completion. + pub bracket_refused: u64, + pub bracket_unposted: u64, +} + +/// How a transfer ended. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Verdict { + /// Drawn, or nothing to draw. + Done, + /// Drawn and a copy: counted as one. + Copied, + Context, + Target, + Destination, +} + +/// A transfer's state, one per context. +#[derive(Clone, Debug, Default)] +pub struct Pixel { + setup: Option, + pending: Option, + read_dma: Option, + pub counts: PixelCounts, + /// Scratch for a copy's source row, kept between copies. + row: Vec, +} + +impl Pixel { + /// Whether a write waits for its rows or a read bracket is open, neither of which a + /// snapshot carries. + pub fn busy(&self) -> bool { + self.pending.is_some() || self.read_dma.is_some() + } + + /// The set-up, as the stream module reads and writes it (the counters are not saved). + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { setup: _, pending: _, read_dma: _, counts: _, row: _ } = self; + s.opt(&mut self.setup, |s, v| { + for w in [&mut v.mode, &mut v.src, &mut v.dst, &mut v.shift, &mut v.width, &mut v.height, &mut v.pad] { + s.u32(w); + } + }); + if s.loading() { + self.pending = None; + self.read_dma = None; + } + } + + fn count(&mut self, v: Verdict) { + match v { + Verdict::Done => {} + Verdict::Copied => self.counts.copies += 1, + Verdict::Context => self.counts.bad_context += 1, + Verdict::Target => self.counts.other_target += 1, + Verdict::Destination => self.counts.bad_destination += 1, + } + } +} + +/// A read bracket the kernel opened with select-feed start: where the host +/// buffer is and how its lines lie, and, once the read inside it has run, the +/// bytes waiting for the bracket's finish. +#[derive(Clone, Debug)] +struct ReadDma { + descriptor: [u32; 6], + refused: bool, + header: [u32; 7], + data: Option>, + /// The bytes are a texel read-back's, whose descriptor length is the packed image's own (`feed_texels`). + texels: bool, +} + +impl ReadDma { + /// The host buffer: device address, line pitch and line count, for the + /// descriptor forms the X server sends (address tagged 0x9000 in the low + /// word, a byte count that is a whole number of lines, a non-negative gap). + fn buffer(&self) -> Option<(u64, u64, u32)> { + let a = &self.descriptor; + if a[1] & !0xfff != 0x9000 || a[2] == 0 || a[2] > 64 << 20 || a[3] == 0 || a[3] > a[2] || a[2] % a[3] != 0 || (a[4] as i32) < 0 || a[5] != 0 { + return None; + } + let io = (a[0] as u64) << 12 | (a[1] & 0xfff) as u64; + let lines = a[2] / a[3]; + let pitch = a[3] as u64 + a[4] as u64; + let span = (lines as u64 - 1) * pitch + a[3] as u64; + (span <= 64 << 20).then_some((io, pitch, lines)) + } + + /// Whether the descriptor fits the read `header` names: the rows laid + /// end to end, or one line, with the bytes a row of the read takes. + fn fits(&self, h: &[u32; 7]) -> bool { + if !matches!(h[4], 1 | 2 | 4) || h[5] != 0 || h[6] != 0 { + return false; + } + let row = h[2] as u64 * h[4] as u64; + if row == 0 || (h[2] as i32) <= 0 || (h[3] as i32) <= 0 || h[2] > 8192 || h[3] > 8192 || self.buffer().is_none() { + return false; + } + let total = row * h[3] as u64; + self.descriptor[2] as u64 == total && ((self.descriptor[3] as u64 == total && self.descriptor[4] == 0) || self.descriptor[3] as u64 == row) + } +} + +/// What a finished read bracket writes to the host and posts. +#[derive(Debug, PartialEq, Eq)] +pub struct FeedDone { + /// Device address of the first line, the line pitch and the bytes of a line. + pub io: u64, + pub pitch: u64, + pub line: usize, + pub data: Vec, + /// The second word of the finish, which the completion repeats. + pub echo: u32, +} + +/// What a pixel command asks of the back end next. +#[derive(Debug, PartialEq, Eq)] +pub enum Next { + Nothing, + /// The write's rows follow as this many data words. + Words(u32), + /// The write's rows arrive in a DMA packet. + Dma, + /// Post these bytes on the return page. + Return(Vec), +} + +/// Whether `selector` is one of the pixel transfer's commands. +pub fn is_pixel(selector: u32) -> bool { + matches!(selector, X_PIXEL_SETUP | X_PIXEL_COPY | X_PIXEL_READ | X_PIXEL_WRITE | X_PIXEL_FINISH) +} + +/// The kinds of source the pixel loop tells apart. +const PACKED: u8 = 0; +const BITMAP: u8 = 1; +const OPAQUE: u8 = 2; + +/// How a source pixel becomes the word a plane takes. +#[derive(Clone, Copy)] +struct Conv { + fg: u32, + bg: u32, + shift: u32, + mask: u32, + bits: u32, + pad: u32, +} + +/// Where an image lands and how its rows lie. +#[derive(Clone, Copy)] +struct Geo { + x: i64, + y: i64, + width: i64, + height: i64, + stride: usize, +} + +/// The context the write draws under, from the context words and the setup: +/// the forms the X server sends for images and glyphs. Ported from the +/// write-decode of the model named above; every branch keeps its conditions. +fn decode_write(raw: &[u32; GC_WORDS], s: &Setup) -> Result { + let mut raw = *raw; + let bad = Status::Deferred; + if s.mode & !Setup::MODE_DMA != 0 { + return Err(bad); + } + let bitmap = s.src == 4 || s.src == 5; + match s.src { + 1 => { + if s.dst != 1 || s.shift != 4 || raw[GC_PIXEL_PACK_CODE] != 5 { + return Err(bad); + } + let gc = Gc::decode_main_index(&raw)?; + return if gc.opaque_or_tiled { Err(bad) } else { Ok(gc) }; + } + 2 => { + if s.dst != 2 || s.shift != 8 || raw[GC_PIXEL_PACK_CODE] != 8 { + return Err(bad); + } + let gc = Gc::decode(&raw)?; + return if gc.target == Target::MainRgb8 && !gc.opaque_or_tiled { Ok(gc) } else { Err(bad) }; + } + _ => {} + } + // A glyph into a 24-bit or 30-bit TrueColor window: the colour bytes ride + // in words 0 and 1 widened to twelve bits; rewrite them into the form the + // solid fills use, alpha all ones. + if s.src == 4 && (raw[GC_FRAMEBUFFER_CONFIG] == 0x2804 || raw[GC_FRAMEBUFFER_CONFIG] == 0x3804 || (raw[GC_FRAMEBUFFER_CONFIG] == 0 && raw[GC_OPERATION] == 0x64001)) { + if s.dst != 2 || s.shift != 0 || (raw[GC_FOREGROUND_HIGH] & !0xf) != 0xfff0 || raw[GC_BACKGROUND_HIGH] != 0 || raw[GC_BACKGROUND_LOW] != 0 || raw[GC_PIXEL_PACK_CODE] != 0 { + return Err(bad); + } + let rgb30 = raw[GC_FRAMEBUFFER_CONFIG] == 0; + let red = (raw[GC_FOREGROUND_LOW] & 0xfff) as u64; + let green = ((raw[GC_FOREGROUND_LOW] >> 12) & 0xfff) as u64; + let blue = ((raw[GC_FOREGROUND_HIGH] & 0xf) as u64) << 8 | (raw[GC_FOREGROUND_LOW] >> 24) as u64; + let colour = red << 40 | green << 27 | blue << 14 | 0x1ffe; + raw[GC_FOREGROUND_HIGH] = (colour >> 32) as u32; + raw[GC_FOREGROUND_LOW] = colour as u32; + raw[GC_PIXEL_PACK_CODE] = if rgb30 { 9 } else { 8 }; + let gc = Gc::decode(&raw)?; + return if gc.target == Target::MainRgb8 && !gc.opaque_or_tiled { Ok(gc) } else { Err(bad) }; + } + // An 8-bit image into the popup planes. + if s.src == 0 && raw[GC_FRAMEBUFFER_CONFIG] == 0x1020 { + if s.dst != 1 || s.shift != 0 { + return Err(bad); + } + let gc = Gc::decode(&raw)?; + return if gc.target == Target::Popup && !gc.opaque_or_tiled { Ok(gc) } else { Err(bad) }; + } + if s.dst != 1 || s.shift != if bitmap { 0 } else { 8 } || (!bitmap && s.src != 0) { + return Err(bad); + } + // The one-bit image sender sends the glyph form's words with the geometry + // configuration; a glyph into the popup planes uses configuration 0x1820. + let geometry = s.src == 4 && raw[GC_FRAMEBUFFER_CONFIG] == 0xc; + let popup = s.src == 4 && raw[GC_FRAMEBUFFER_CONFIG] == 0x1820; + if bitmap { + if (raw[GC_FRAMEBUFFER_CONFIG] != 0x180c && !geometry && !popup) || raw[GC_PIXEL_PACK_CODE] != if s.src == 4 { 0 } else { 5 } { + return Err(bad); + } + if s.src == 4 { + if raw[GC_FOREGROUND_HIGH] != 0xfff0 || raw[GC_FOREGROUND_LOW] & !0xf0_0fff != 0 || raw[GC_BACKGROUND_HIGH] != 0 || raw[GC_BACKGROUND_LOW] != 0 { + return Err(bad); + } + let value = ((raw[GC_FOREGROUND_LOW] & 0xfff) << 4) | (raw[GC_FOREGROUND_LOW] >> 20); + raw[GC_FOREGROUND_HIGH] = ((value & 0xfff0) << 4) | ((value & 15) << 3); + raw[GC_BACKGROUND_HIGH] = 0; + } else { + if (raw[GC_FOREGROUND_HIGH] | raw[GC_BACKGROUND_HIGH]) & !0xffff != 0 || raw[GC_FOREGROUND_LOW] != 0 || raw[GC_BACKGROUND_LOW] != 0 { + return Err(bad); + } + raw[GC_FOREGROUND_HIGH] = ((raw[GC_FOREGROUND_HIGH] & 0xfff0) << 4) | ((raw[GC_FOREGROUND_HIGH] & 15) << 3); + raw[GC_BACKGROUND_HIGH] = ((raw[GC_BACKGROUND_HIGH] & 0xfff0) << 4) | ((raw[GC_BACKGROUND_HIGH] & 15) << 3); + } + raw[GC_FOREGROUND_LOW] = 0; + raw[GC_BACKGROUND_LOW] = 0; + raw[GC_PIXEL_PACK_CODE] = if geometry { 0 } else { 5 }; + } + if popup { + let gc = Gc::decode(&raw)?; + return if gc.target == Target::Popup && !gc.opaque_or_tiled { Ok(gc) } else { Err(bad) }; + } + let gc = Gc::decode_main_index(&raw)?; + if gc.opaque_or_tiled { Err(bad) } else { Ok(gc) } +} + +/// Walk an image: call `emit(index, source)` for every pixel that lands on +/// the store, in row order, with the store index it lands on and the word its +/// bits convert to. A clear bit of a one-bit image (`BITMAP`) emits nothing. +fn walk(p: &Plan<'_>, c: &Conv, g: &Geo, words: &[u32], spans: &mut Vec<(i64, i64)>, pad: &mut Vec, mut emit: impl FnMut(usize, u32)) { + let mask = if c.bits == 32 { u32::MAX as u64 } else { (1u64 << c.bits) - 1 }; + let (xa, xb) = (g.x.max(p.bounds.x1), (g.x + g.width).min(p.bounds.x2)); + for r in 0..g.height { + let y = g.y + r; + if y < p.bounds.y1 || y >= p.bounds.y2 || xa >= xb { + continue; + } + // The row's words and a zero one after, so a pixel that ends in the last + // word has a word to read its low bits from without a test. + pad.clear(); + pad.extend_from_slice(&words[r as usize * g.stride..(r as usize + 1) * g.stride]); + pad.push(0); + let row = &pad[..]; + p.row_spans(y, xa, xb, spans); + let base = (p.height - 1 - y) as usize * p.pitch; + for &(a, b) in spans.iter() { + let (mut sbits, mut swidth, mut col) = (0, 1, 0); + if STIP { + if let Some(s) = &p.stipple { + (sbits, swidth, col) = p.stipple_start(s, y, a); + } + } + for x in a..b { + let set = !STIP || sbits & (0x8000_0000 >> col) != 0; + col += 1; + if col == swidth { + col = 0; + } + if !set { + continue; + } + let bit = c.pad as u64 + c.bits as u64 * (x - g.x) as u64; + let (w, o) = ((bit / 32) as usize, (bit % 32) as u32); + // The pixel's bits sit in this word and, if it runs over, the next. + let window = (row[w] as u64) << 32 | row[w + 1] as u64; + let v = ((window >> (64 - o - c.bits)) & mask) as u32; + let src = match KIND { + PACKED => (v << c.shift) & c.mask, + BITMAP => { + if v == 0 { + continue; + } + c.fg + } + _ => { + if v != 0 { + c.fg + } else { + c.bg + } + } + }; + emit(base + x as usize, src); + } + } + } +} + +/// `walk` with the kind and stipple chosen once. +fn walk_kind(p: &Plan<'_>, kind: u8, c: &Conv, g: &Geo, words: &[u32], spans: &mut Vec<(i64, i64)>, pad: &mut Vec, emit: impl FnMut(usize, u32)) { + let stip = p.stipple.is_some(); + match (kind, stip) { + (PACKED, false) => walk::(p, c, g, words, spans, pad, emit), + (PACKED, true) => walk::(p, c, g, words, spans, pad, emit), + (BITMAP, false) => walk::(p, c, g, words, spans, pad, emit), + (BITMAP, true) => walk::(p, c, g, words, spans, pad, emit), + (_, false) => walk::(p, c, g, words, spans, pad, emit), + (_, true) => walk::(p, c, g, words, spans, pad, emit), + } +} + +/// Draw an image under the logic operation, a constant of the loop. +fn draw_image(p: &Plan<'_>, kind: u8, c: &Conv, g: &Geo, words: &[u32], spans: &mut Vec<(i64, i64)>, pad: &mut Vec) { + let gc = &p.gc; + walk_kind(p, kind, c, g, words, spans, pad, |i, src| { + let old = if NEEDS { + // SAFETY: the walk clips to the bounds, which are inside the managed area. + unsafe { p.array.load(i) } + } else { + 0 + }; + // SAFETY: as above. + unsafe { p.array.store(i, apply::(gc, src, old)) }; + }); +} + +fn draw_by_op(p: &Plan<'_>, kind: u8, c: &Conv, g: &Geo, words: &[u32], spans: &mut Vec<(i64, i64)>, pad: &mut Vec) { + macro_rules! by_op { + ($needs:literal, $($gx:literal),*) => { + match p.gc.gx as u32 { + $($gx => draw_image::<$gx, $needs>(p, kind, c, g, words, spans, pad),)* + _ => { + debug_assert!(false, "a decoded context's logic operation is 0 to 15"); + draw_image::<3, true>(p, kind, c, g, words, spans, pad) + } + } + }; + } + if p.needs { + by_op!(true, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) + } else { + // Only the operations that ignore the destination get here. + by_op!(false, 0, 3, 12, 15) + } +} + +/// Store one row of source words under the logic operation, a constant of the +/// loop: `row[0]` lands at column `x0`, the spans are the columns the clip lets +/// through, `dbase` the destination row's first index. +fn copy_row(plan: &Plan<'_>, row: &[u32], spans: &[(i64, i64)], dbase: usize, x0: i64) { + for &(a, c) in spans { + for x in a..c { + let d = dbase + x as usize; + // SAFETY: the spans are inside the bounds, which are inside the managed area. + unsafe { plan.array.store(d, apply::(&plan.gc, row[(x - x0) as usize], plan.array.load(d))) }; + } + } +} + +fn copy_by_op(plan: &Plan<'_>, row: &[u32], spans: &[(i64, i64)], dbase: usize, x0: i64) { + macro_rules! by_op { + ($($gx:literal),*) => { + match plan.gc.gx as u32 { + $($gx => copy_row::<$gx>(plan, row, spans, dbase, x0),)* + _ => { + debug_assert!(false, "a decoded context's logic operation is 0 to 15"); + copy_row::<3>(plan, row, spans, dbase, x0) + } + } + }; + } + by_op!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) +} + +/// Copy `s.width` x `s.height` pixels from (`sx`, `sy`) to (`dx`, `dy`) under +/// the plan's context, clipped. `row` holds one source row at a time. +fn copy_rows(plan: &Plan<'_>, s: &Setup, [sx, sy, dx, dy]: [i64; 4], spans: &mut Vec<(i64, i64)>, row: &mut Vec) -> Verdict { + let (w, h) = (s.width as i64, s.height as i64); + let array = plan.array; + let pitch = plan.pitch; + let top = plan.height - 1; + let width = pitch as i64; + let b = plan.bounds; + let (i0, i1) = ((-sx).max(0).max(b.x1 - dx), w.min(width - sx).min(b.x2 - dx)); + let (j0, j1) = ((-sy).max(0).max(b.y1 - dy), h.min(plan.height - sy).min(b.y2 - dy)); + if i0 >= i1 || j0 >= j1 { + return Verdict::Done; + } + let mask = plan.gc.width_mask; + // Every source word, and every destination word a write reads, must fit. + for j in j0..j1 { + let sbase = (top - (sy + j)) as usize * pitch; + for i in i0..i1 { + // SAFETY: the rectangle is inside the managed area; i0..i1 and j0..j1 clip it. + if unsafe { array.load(sbase + (sx + i) as usize) } > mask { + return Verdict::Destination; + } + } + } + if plan.needs { + for j in j0..j1 { + let dbase = (top - (dy + j)) as usize * pitch; + for i in i0..i1 { + // SAFETY: as above. + if unsafe { array.load(dbase + (dx + i) as usize) } > mask { + return Verdict::Destination; + } + } + } + } + // Rows run away from where an overlapping destination lies. + for k in j0..j1 { + let j = if dy > sy { j1 - 1 - (k - j0) } else { k }; + let (sbase, dbase) = ((top - (sy + j)) as usize * pitch, (top - (dy + j)) as usize * pitch); + row.clear(); + // SAFETY: as above. + row.extend((i0..i1).map(|i| unsafe { array.load(sbase + (sx + i) as usize) })); + plan.row_spans(dy + j, dx + i0, dx + i1, spans); + copy_by_op(plan, row, spans, dbase, dx + i0); + } + Verdict::Copied +} + +impl XState { + /// A pixel command. `surface` is the plane drawn into. + /// `answered`: a context is current and a return page is registered, so a + /// read has somewhere to reply. + pub fn pixel_command(&mut self, surface: &Surface<'_>, selector: u32, args: &[u32], answered: bool) -> Next { + match selector { + X_PIXEL_SETUP => { + if let Ok(w) = <&[u32; PIXEL_SETUP_WORDS]>::try_from(args) { + self.pixel.setup = Some(Setup::from_words(w)); + self.pixel.counts.setups += 1; + } + Next::Nothing + } + X_PIXEL_WRITE => self.pixel_write(args), + X_PIXEL_READ => self.pixel_read(surface, args, answered), + X_PIXEL_COPY => { + self.pixel_copy(surface, args); + Next::Nothing + } + X_PIXEL_FINISH => { + self.pixel.counts.finishes += 1; + if self.pixel.pending.take().is_some() { + self.pixel.counts.short += 1; + } + Next::Nothing + } + _ => Next::Nothing, + } + } + + /// 0x139: where the image goes, and how its rows will come. + fn pixel_write(&mut self, args: &[u32]) -> Next { + let Ok(&[x, y]) = <&[u32; 2]>::try_from(args) else { + self.pixel.counts.bad_context += 1; + return Next::Nothing; + }; + self.pixel.pending = None; + let Some(setup) = self.pixel.setup else { + self.pixel.counts.bad_context += 1; + return Next::Nothing; + }; + let Some((_, words)) = setup.words() else { + self.pixel.counts.bad_context += 1; + return Next::Nothing; + }; + if words == 0 { + return Next::Nothing; + } + let dma = setup.mode & Setup::MODE_DMA != 0; + self.pixel.pending = Some(Pending { x: x as i32, y: y as i32, words, dma }); + if dma { + Next::Dma + } else { + Next::Words(words as u32) + } + } + + /// Whether a write waits for its rows by DMA, and how many words it owes. + pub fn dma_write_words(&self) -> Option { + self.pixel.pending.filter(|p| p.dma).map(|p| p.words) + } + + /// A DMA packet that carried no write's rows. Counted. + pub fn pixel_unexpected(&mut self) { + self.pixel.counts.unexpected += 1; + } + + /// The rows of the open write, as the words of the data run or the DMA + /// packet. A count that is not what the setup announced draws nothing. + pub fn pixel_rows(&mut self, surface: &Surface<'_>, words: &[u32]) { + let Some(p) = self.pixel.pending.take() else { + self.pixel.counts.unexpected += 1; + return; + }; + if words.len() != p.words { + self.pixel.counts.short += 1; + return; + } + self.pixel.counts.writes += 1; + self.pixel.counts.dma_writes += p.dma as u64; + let Some(setup) = self.pixel.setup else { return }; + let Some((stride, _)) = setup.words() else { return }; + let gc = match decode_write(&self.gc, &setup) { + Ok(gc) if gc.target != Target::Popup => gc, + Ok(_) => { + self.pixel.counts.other_target += 1; + return; + } + Err(_) => { + self.pixel.counts.bad_context += 1; + return; + } + }; + let stipple = self.stipple_pattern(); + let (kind, bits) = match setup.src { + 4 => (BITMAP, 1), + 5 => (OPAQUE, 1), + _ => (PACKED, setup.bits().unwrap_or(8)), + }; + let conv = Conv { + fg: gc.foreground, + bg: gc.background, + shift: if gc.target == Target::MainRgb8 { 0 } else { setup.shift }, + mask: if gc.target == Target::MainRgb8 { 0xff_ffff } else { 0xffff }, + bits, + pad: setup.pad, + }; + let geo = Geo { x: p.x as i64, y: p.y as i64, width: setup.width as i64, height: setup.height as i64, stride }; + let mut spans = std::mem::take(&mut self.spans); + let mut pad = std::mem::take(&mut self.pixel.row); + let verdict = match Plan::with_gc(self, surface, gc, stipple) { + Ok(Some(plan)) => { + let fits = !plan.needs || { + let mut ok = true; + let mask = plan.gc.width_mask; + walk_kind(&plan, kind, &conv, &geo, words, &mut spans, &mut pad, |i, _| { + // SAFETY: the walk clips to the bounds, which are inside the managed area. + ok &= unsafe { plan.array.load(i) } <= mask; + }); + ok + }; + if fits { + draw_by_op(&plan, kind, &conv, &geo, words, &mut spans, &mut pad); + Verdict::Done + } else { + Verdict::Destination + } + } + Ok(None) => Verdict::Done, + Err(Refused::Target) => Verdict::Target, + Err(_) => Verdict::Context, + }; + self.spans = spans; + self.pixel.row = pad; + self.pixel.count(verdict); + } + + /// 0x138: a rectangle of the main plane, as the packing the setup names. + /// Inside a read bracket the bytes wait for its finish; otherwise they go + /// on the return page. A read that cannot be answered from the store is + /// answered with zeros all the same, if there is somewhere to answer. + fn pixel_read(&mut self, surface: &Surface<'_>, args: &[u32], answered: bool) -> Next { + self.pixel.counts.reads += 1; + // A bracket's buffer takes up to 64 MiB; the return page, what is left of it. + let limit = if self.pixel.read_dma.is_some() { 64 << 20 } else { READ_LIMIT }; + let data = self.read_payload(surface, args, limit).ok(); + if data.is_none() { + self.pixel.counts.reads_refused += 1; + } + let Some(mut dma) = self.pixel.read_dma.take() else { + return Next::Return(data.unwrap_or_else(|| vec![0; READ_LIMIT])); + }; + let setup = self.pixel.setup.unwrap_or_default(); + let (x, y) = (args.first().copied().unwrap_or(0), args.get(1).copied().unwrap_or(0)); + let header = [x, y, setup.width, setup.height, [1, 2, 4].get(setup.dst as usize).copied().unwrap_or(0), 0, 0]; + if dma.refused || dma.data.is_some() || !dma.fits(&header) { + dma.refused = true; + dma.data = None; + } else if let Some(d) = data { + dma.header = header; + dma.data = Some(d); + } else if answered { + // The descriptor fixes the byte count: a read the store cannot answer completes as zeros. + dma.header = header; + dma.data = Some(vec![0; dma.descriptor[2] as usize]); + } else { + dma.refused = true; + } + self.pixel.read_dma = Some(dma); + Next::Nothing + } + + /// Whether a select-feed bracket is open. + pub fn bracket_open(&self) -> bool { + self.pixel.read_dma.is_some() + } + + /// A texel read-back's bytes (token 0x052) for the open bracket: the packed image, `rows` rows of + /// `row` bytes. The client's descriptor names the image's padded length as one line with no stride + /// (the table), and the bracket's finish writes the bytes there from its first byte. A descriptor of + /// `rows` lines of `row` is taken too. Any other length refuses the bracket, counted, as an X read's + /// would be. False when no bracket is open. + pub fn feed_texels(&mut self, row: u32, rows: u32, mut data: Vec) -> bool { + let Some(mut dma) = self.pixel.read_dma.take() else { return false }; + let header = [0, 0, row, rows, 1, 0, 0]; + let d = dma.descriptor; + if dma.refused || dma.data.is_some() { + dma.refused = true; + dma.data = None; + } else if d[2] as usize == data.len() && d[3] == d[2] && d[4] == 0 && dma.buffer().is_some() { + dma.texels = true; + dma.data = Some(data); + } else if !dma.fits(&header) { + dma.refused = true; + dma.data = None; + } else { + data.resize(row as usize * rows as usize, 0); + dma.header = header; + dma.data = Some(data); + } + self.pixel.read_dma = Some(dma); + true + } + + /// Select-feed start (0x1f5): a bracket whose read will be written to a + /// host buffer. `owned`: a context is current. + pub fn feed_start(&mut self, args: &[u32], owned: bool) { + let Ok(&descriptor) = <&[u32; 6]>::try_from(args) else { return }; + let mut dma = ReadDma { descriptor, refused: !owned, header: [0; 7], data: None, texels: false }; + dma.refused |= dma.buffer().is_none(); + self.pixel.read_dma = Some(dma); + } + + /// Select-feed finish (0x1f6): the read's bytes for the host buffer and + /// the completion to post, or None, counted, for a bracket that is not a + /// whole read of a kind this model takes (a feedback or selection pass's + /// bracket holds none). + pub fn feed_finish(&mut self, args: &[u32]) -> Option { + let dma = self.pixel.read_dma.take()?; + let &[mark, echo] = <&[u32; 2]>::try_from(args).ok()?; + if dma.refused || mark != 0x100 || !(dma.texels || dma.fits(&dma.header)) { + self.pixel.counts.bracket_refused += 1; + return None; + } + let (io, pitch, _) = dma.buffer()?; + let data = dma.data?; + Some(FeedDone { io, pitch, line: dma.descriptor[3] as usize, data, echo }) + } + + /// A select-feed bracket's host buffer, taking the bracket: the device address of the first + /// line, the line pitch, the bytes in a line and in all. None for a bracket the descriptor + /// does not describe a buffer for. A feedback or select pass writes there. + pub fn feed_buffer(&mut self) -> Option<(u64, u64, usize, usize)> { + let dma = self.pixel.read_dma.take()?; + if dma.refused { + return None; + } + let (io, pitch, _) = dma.buffer()?; + Some((io, pitch, dma.descriptor[3] as usize, dma.descriptor[2] as usize)) + } + + /// The bytes of the rectangle at `args`, or why the store cannot answer + /// it: the setup or context is not a form the X server's reads send, the + /// rectangle is off the screen or too big for the page, or a source word + /// is not a main-plane colour of the width the packing takes. + fn read_payload(&mut self, surface: &Surface<'_>, args: &[u32], limit: usize) -> Result, &'static str> { + let &[x, y] = <&[u32; 2]>::try_from(args).map_err(|_| "not two words")?; + let s = self.pixel.setup.ok_or("no setup loaded")?; + let (x, y) = (x as i32 as i64, y as i32 as i64); + let bytes: usize = match (s.dst, s.shift as i32) { + (0, 8) => 1, + (1, 12) => 2, + (2, -8) | (2, 0) => 4, + _ => 0, + }; + if bytes == 4 && s.shift == 0 { + // Told apart by a per-pixel tag this store does not keep. + self.pixel.counts.fb_word_reads += 1; + return Err("a frame-buffer word read needs a pixel tag"); + } + let rgb = bytes == 4; + let (w6, rest) = (self.gc[6], &self.gc[7..]); + let gc_ok = if rgb { w6 == 0x2804 && rest == [0x60001, 1, 6, 8, 0, 0] } else { (w6 == 0x180c && rest == [0x40001, 1, 6, 5, 1, 1]) || (w6 == 0xc && rest == [1, 1, 6, 0, 1, 1]) }; + if bytes == 0 { + return Err("unknown packing"); + } + if !gc_ok { + return Err("not a main-plane read context"); + } + if s.mode != 2 || s.src != if rgb { 2 } else { 1 } || s.pad != 0 { + return Err("not a read setup"); + } + let (width, height) = (s.width as i32 as i64, s.height as i32 as i64); + let (sw, sh) = surface.size(); + if width <= 0 || height <= 0 || x < 0 || y < 0 || x + width > sw as i64 || y + height > sh as i64 { + return Err("rectangle outside the main plane"); + } + let pitch = width as usize * bytes; + if pitch > limit || height as usize > limit / pitch { + return Err("payload exceeds the room for it"); + } + let array = surface.array(); + let limit = if rgb { 0xff_ffff } else { 0xffff }; + let at = |c: i64, r: i64| (sh as i64 - 1 - y - r) as usize * array.pitch() + (x + c) as usize; + // Every source is checked before any of the reply is made. + for r in 0..height { + for c in 0..width { + // SAFETY: the rectangle is inside the managed area. + if unsafe { array.load(at(c, r)) } > limit { + return Err("source is not main-plane colour"); + } + } + } + let mut data = vec![0u8; pitch * height as usize]; + macro_rules! rows { + ($b:literal, $pack:expr) => { + for r in 0..height { + let base = (sh as i64 - 1 - y - r) as usize * array.pitch() + x as usize; + let out = &mut data[r as usize * pitch..(r as usize + 1) * pitch]; + for (c, px) in out.chunks_exact_mut($b).enumerate() { + // SAFETY: the rectangle is inside the managed area. + let word = unsafe { array.load(base + c) }; + $pack(word, px); + } + } + }; + } + match bytes { + 4 => rows!(4, |word: u32, px: &mut [u8]| px.copy_from_slice(&word.to_be_bytes())), + 2 => rows!(2, |word: u32, px: &mut [u8]| px.copy_from_slice(&(((word >> 4) & 0xfff) as u16).to_be_bytes())), + _ => rows!(1, |word: u32, px: &mut [u8]| px[0] = (word >> 8) as u8), + } + Ok(data) + } + + /// 0x137: copy a rectangle inside the store under the context's logic + /// operation, clipped to the clip list and scissor. Every source word is + /// checked first, and rows run in the order that keeps an overlapping + /// source whole. + fn pixel_copy(&mut self, surface: &Surface<'_>, args: &[u32]) { + let Ok(&[sx, sy, dx, dy]) = <&[u32; 4]>::try_from(args) else { + self.pixel.counts.bad_context += 1; + return; + }; + let Some(s) = self.pixel.setup else { + self.pixel.counts.bad_context += 1; + return; + }; + if s.src == 3 && s.dst == 3 { + self.pixel.counts.window_copies += 1; + return; + } + let gc = match Gc::decode_for_fill(&self.gc) { + Ok(gc) => gc, + Err(_) => { + self.pixel.counts.bad_context += 1; + return; + } + }; + let rgb = gc.target == Target::MainRgb8; + let form_ok = if rgb { gc.raw[GC_PIXEL_PACK_CODE] == 8 || gc.raw[GC_PIXEL_PACK_CODE] == 9 } else { gc.target == Target::MainIndex16 && gc.raw[GC_PIXEL_PACK_CODE] == 5 }; + let pack = if rgb { 2 } else { 1 }; + if !form_ok || s.mode != 1 || s.src != pack || s.dst != pack || s.shift != 0 || s.pad != 0 { + self.pixel.counts.bad_context += 1; + return; + } + if gc.is_noop() { + return; + } + let (mut spans, mut row) = (std::mem::take(&mut self.spans), std::mem::take(&mut self.pixel.row)); + let verdict = match Plan::with_gc(self, surface, gc, None) { + Ok(Some(plan)) => copy_rows(&plan, &s, [sx as i32 as i64, sy as i32 as i64, dx as i32 as i64, dy as i32 as i64], &mut spans, &mut row), + Ok(None) => Verdict::Done, + Err(Refused::Target) => Verdict::Target, + Err(_) => Verdict::Context, + }; + self.spans = spans; + self.pixel.row = row; + self.pixel.count(verdict); + } + + /// A read bracket's bytes could not be written, or there was no reason + /// page: no completion was posted. + pub fn bracket_unposted(&mut self) { + self.pixel.counts.bracket_unposted += 1; + } + + pub fn pixel_counts(&self) -> &PixelCounts { + &self.pixel.counts + } + + /// A destroyed window ends the transfer open on it. + pub fn pixel_release(&mut self) { + if self.pixel.pending.take().is_some() { + self.pixel.counts.short += 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::back::store::PixelStore; + + /// An index context, native 16-bit index, full mask. + const INDEX: [u32; 13] = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + /// 24-bit RGB, copy, full mask. + const RGB: [u32; 13] = [0, 0, 0, 0, 0xff_ffff, 0xff_ffff, 0x2804, 0x60001, 1, 6, 8, 0, 0]; + /// A glyph colour, index 0x1234, in the one-bit senders' form. + const GLYPH: [u32; 13] = [0xfff0, 0x123 | 4 << 20, 0, 0, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 0, 1, 1]; + /// An opaque one-bit image: foreground index 0x1230, background 0x10. + const OPAQUE_GC: [u32; 13] = [0x1230, 0, 0x10, 0, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + + fn rig(gc: &[u32; 13]) -> (XState, PixelStore) { + let mut x = XState::default(); + x.command(0x129, gc); + (x, PixelStore::new(32, 32)) + } + + /// The word at X coordinates (`x`, `y`): the store is bottom-origin. + fn at(s: &PixelStore, x: u32, y: u32) -> u32 { + s.read(x, 31 - y, 1) + } + + fn row(s: &PixelStore, y: u32, n: u32) -> Vec { + (0..n).map(|c| at(s, c, y)).collect() + } + + fn cmd(x: &mut XState, s: &PixelStore, selector: u32, args: &[u32]) -> Next { + x.pixel_command(&Surface::new(s, 1), selector, args, true) + } + + /// Set up, open a write at `pos` and send its rows as the data run. + fn write(x: &mut XState, s: &PixelStore, w: [u32; 7], pos: (u32, u32), rows: &[u32]) -> Next { + cmd(x, s, X_PIXEL_SETUP, &w); + let next = cmd(x, s, X_PIXEL_WRITE, &[pos.0, pos.1]); + if let Next::Words(n) = next { + assert_eq!(n as usize, rows.len(), "the run the back end asks for"); + x.pixel_rows(&Surface::new(s, 1), rows); + } + next + } + + /// An 8-bit image lands shifted into the index word, its first row at the + /// position and each row below the last; pixels pack most significant first. + #[test] + fn an_eight_bit_image_lands_shifted_top_row_first() { + let (mut x, s) = rig(&INDEX); + assert_eq!(write(&mut x, &s, [0, 0, 1, 8, 3, 2, 0], (4, 2), &[0x0102_0300, 0x0405_0600]), Next::Words(2)); + assert_eq!([at(&s, 4, 2), at(&s, 5, 2), at(&s, 6, 2)], [0x100, 0x200, 0x300]); + assert_eq!([at(&s, 4, 3), at(&s, 5, 3), at(&s, 6, 3)], [0x400, 0x500, 0x600]); + assert_eq!(at(&s, 7, 2), 0); + assert_eq!(x.pixel_counts().writes, 1); + } + + /// A pad of bits starts the row's first pixel that far in, and a pixel may + /// run over into the next word of the row. + #[test] + fn a_row_starts_after_its_pad_and_pixels_cross_words() { + let (mut x, s) = rig(&INDEX); + write(&mut x, &s, [0, 0, 1, 8, 2, 1, 8], (0, 0), &[0x0011_2200]); + assert_eq!(row(&s, 0, 2), [0x1100, 0x2200]); + // Ten bits of pad, then 0xab 0xcd 0xef: the last pixel ends in the second word. + let next = write(&mut x, &s, [0, 0, 1, 8, 3, 1, 10], (0, 1), &[0x002a_f37b, 0xc000_0000]); + assert_eq!(next, Next::Words(2)); + assert_eq!(row(&s, 1, 3), [0xab00, 0xcd00, 0xef00]); + } + + /// A 16-bit image takes the setup's shift of 4, a 32-bit one into an RGB + /// window keeps the low 24 bits. + #[test] + fn sixteen_and_thirty_two_bit_images_convert_to_the_plane_word() { + let (mut x, s) = rig(&INDEX); + write(&mut x, &s, [0, 1, 1, 4, 2, 1, 0], (0, 0), &[0x1234_0056]); + assert_eq!(row(&s, 0, 2), [0x2340, 0x0560]); + let (mut x, s) = rig(&RGB); + write(&mut x, &s, [0, 2, 2, 8, 2, 1, 0], (3, 3), &[0x01ff_8040, 0xaa11_2233]); + assert_eq!([at(&s, 3, 3), at(&s, 4, 3)], [0xff_8040, 0x11_2233]); + } + + /// A one-bit image paints the context's colour where a bit is set and + /// leaves the rest; the opaque form paints its background there too. + #[test] + fn one_bit_images_paint_the_foreground_and_the_opaque_form_the_background() { + let (mut x, s) = rig(&GLYPH); + s.write(1, 31, 1, 0x7777); + write(&mut x, &s, [0, 4, 1, 0, 5, 2, 0], (0, 0), &[0xb000_0000, 0x4000_0000]); + assert_eq!(row(&s, 0, 5), [0x1234, 0x7777, 0x1234, 0x1234, 0], "a clear bit leaves its pixel"); + assert_eq!(row(&s, 1, 5), [0, 0x1234, 0, 0, 0]); + let (mut x, s) = rig(&OPAQUE_GC); + write(&mut x, &s, [0, 5, 1, 0, 3, 1, 0], (0, 0), &[0xa000_0000]); + assert_eq!(row(&s, 0, 3), [0x1230, 0x10, 0x1230]); + } + + /// The clip list and the scissor narrow a write, and a loaded stipple + /// masks it. + #[test] + fn a_write_is_clipped_scissored_and_stippled() { + let (mut x, s) = rig(&INDEX); + x.command(0x12b, &[1, 2, 0, 4, 1]); + write(&mut x, &s, [0, 0, 1, 8, 4, 2, 0], (0, 0), &[0x0102_0304, 0x0506_0708]); + assert_eq!(row(&s, 0, 4), [0, 0, 0x300, 0x400], "the clip is x 2 to 3, y 0"); + assert_eq!(row(&s, 1, 4), [0; 4]); + x.command(0x12d, &[3, 0, 1, 1]); + write(&mut x, &s, [0, 0, 1, 8, 4, 1, 0], (0, 0), &[0x0a0b_0c0d]); + assert_eq!(row(&s, 0, 4), [0, 0, 0x300, 0xd00], "the scissor leaves column 3 only"); + // A 2 x 1 stipple, left bit set: every other column of a fresh clip. + x.command(0x12b, &[1, 0, 0, 8, 8]); + x.command(0x13d, &[1, 2, 1]); + x.stipple.rows(&[0x8000_0000]); + x.command(0x13c, &[1]); + write(&mut x, &s, [0, 0, 1, 8, 4, 1, 0], (0, 4), &[0x0102_0304]); + assert_eq!(row(&s, 4, 4), [0x100, 0, 0x300, 0]); + } + + /// A write under a partial mask or a logic operation combines with the + /// pixel it covers; a destination word too wide for the target refuses the + /// whole write. + #[test] + fn a_masked_write_combines_and_a_wide_destination_refuses_it() { + // Mask 0x00ff: the high byte of the destination stays. + let mut gc = INDEX; + gc[4] = 0xf0000f; + let (mut x, s) = rig(&gc); + s.write(0, 31, 1, 0xabcd); + write(&mut x, &s, [0, 0, 1, 8, 1, 1, 0], (0, 0), &[0x0100_0000]); + assert_eq!(at(&s, 0, 0), 0xab00); + // Pixel 1 holds a word wider than the 16-bit target: nothing is drawn. + s.write(1, 31, 1, 0x1_0000); + write(&mut x, &s, [0, 0, 1, 8, 2, 1, 0], (0, 0), &[0x0202_0000]); + assert_eq!(row(&s, 0, 2), [0xab00, 0x1_0000]); + assert_eq!(x.pixel_counts().bad_destination, 1); + // XOR (operation 6) with the full mask reads the destination too. + let mut gc = INDEX; + gc[9] = 12; + let (mut x, s) = rig(&gc); + s.write(0, 31, 1, 0x0f0f); + write(&mut x, &s, [0, 0, 1, 8, 1, 1, 0], (0, 0), &[0xff00_0000]); + assert_eq!(at(&s, 0, 0), 0xff00 ^ 0x0f0f); + } + + /// A write whose rows do not number what the setup announced draws nothing + /// and is counted; so is one with no setup, a packing that is not known and + /// a target with no store (the popup planes). + #[test] + fn writes_that_cannot_be_drawn_are_counted() { + let (mut x, s) = rig(&INDEX); + assert_eq!(cmd(&mut x, &s, X_PIXEL_WRITE, &[0, 0]), Next::Nothing, "no setup"); + cmd(&mut x, &s, X_PIXEL_SETUP, &[0, 0, 1, 8, 2, 1, 0]); + cmd(&mut x, &s, X_PIXEL_WRITE, &[0, 0]); + x.pixel_rows(&Surface::new(&s, 1), &[1, 2]); + assert_eq!(at(&s, 0, 0), 0); + let c = x.pixel_counts().clone(); + assert_eq!((c.bad_context, c.short, c.writes), (1, 1, 0)); + assert_eq!(write(&mut x, &s, [0, 3, 1, 8, 2, 1, 0], (0, 0), &[1]), Next::Nothing, "packing 3 has no framing"); + assert_eq!(x.pixel_counts().bad_context, 2); + let mut popup = INDEX; + popup[0] = 0x78; + popup[2] = 0x38; + popup[4] = 0x30_0000; + popup[6] = 0x1020; + popup[7] = 1; + popup[8] = 0; + let (mut x, s) = rig(&popup); + write(&mut x, &s, [0, 0, 1, 0, 1, 1, 0], (0, 0), &[0x0100_0000]); + assert_eq!((x.pixel_counts().other_target, at(&s, 0, 0)), (1, 0), "{:?}", x.pixel_counts()); + // A rows packet with no write waiting is counted too. + x.pixel_rows(&Surface::new(&s, 1), &[0]); + assert_eq!(x.pixel_counts().unexpected, 1); + } + + /// The DMA bit of the mode word sends the rows by DMA: the write waits for + /// them, a finish ends it, and a destroyed window drops it. + #[test] + fn a_dma_write_waits_for_its_rows() { + let (mut x, s) = rig(&INDEX); + assert_eq!(write(&mut x, &s, [0x10, 0, 1, 8, 3, 2, 0], (1, 1), &[]), Next::Dma); + assert_eq!(x.dma_write_words(), Some(2)); + x.pixel_rows(&Surface::new(&s, 1), &[0x0102_0300, 0x0405_0600]); + assert_eq!((at(&s, 1, 1), at(&s, 3, 2)), (0x100, 0x600)); + assert_eq!((x.dma_write_words(), x.pixel_counts().dma_writes), (None, 1)); + // An open write ended by the finish, or by its window going, was short. + write(&mut x, &s, [0x10, 0, 1, 8, 1, 1, 0], (0, 0), &[]); + cmd(&mut x, &s, X_PIXEL_FINISH, &[0]); + write(&mut x, &s, [0x10, 0, 1, 8, 1, 1, 0], (0, 0), &[]); + x.pixel_release(); + assert_eq!((x.pixel_counts().short, x.pixel_counts().finishes), (2, 1)); + assert_eq!(x.dma_write_words(), None); + } + + /// A copy moves a rectangle, and an overlapping one moves as if the source + /// were held apart, whichever way it runs. + #[test] + fn a_copy_handles_an_overlap_in_either_direction() { + let (mut x, s) = rig(&INDEX); + for c in 0..8 { + for r in 0..4 { + s.write(c, 31 - r, 1, (c + 10 * r) as u32); + } + } + cmd(&mut x, &s, X_PIXEL_SETUP, &[1, 1, 1, 0, 6, 1, 0]); + // Right by two, within the row. + cmd(&mut x, &s, X_PIXEL_COPY, &[0, 0, 2, 0]); + assert_eq!(row(&s, 0, 8), [0, 1, 0, 1, 2, 3, 4, 5]); + // Two rows down by one, over the row the second reads. + cmd(&mut x, &s, X_PIXEL_SETUP, &[1, 1, 1, 0, 3, 2, 0]); + cmd(&mut x, &s, X_PIXEL_COPY, &[0, 0, 0, 1]); + assert_eq!(row(&s, 1, 3), [0, 1, 0]); + assert_eq!(row(&s, 2, 3), [10, 11, 12], "row 1 was read before it was written over"); + // And two rows up by one. + cmd(&mut x, &s, X_PIXEL_COPY, &[0, 1, 0, 0]); + assert_eq!(row(&s, 0, 3), [0, 1, 0]); + assert_eq!(row(&s, 1, 3), [10, 11, 12]); + assert_eq!(x.pixel_counts().copies, 3); + } + + /// A copy is clipped, refuses the window-copy pair and a source word too + /// wide for the target, and an RGB window copies as 24-bit words. + #[test] + fn a_copy_is_clipped_and_refuses_what_it_cannot_draw() { + let (mut x, s) = rig(&INDEX); + s.write(0, 31, 1, 7); + s.write(1, 31, 1, 8); + cmd(&mut x, &s, X_PIXEL_SETUP, &[1, 1, 1, 0, 2, 1, 0]); + x.command(0x12b, &[1, 11, 0, 12, 1]); + cmd(&mut x, &s, X_PIXEL_COPY, &[0, 0, 10, 0]); + assert_eq!(row(&s, 0, 12)[10..], [0, 8], "only x 11 is inside the clip"); + x.command(0x12b, &[1, 0, 0, 32, 32]); + s.write(0, 31, 1, 0x1_0000); + cmd(&mut x, &s, X_PIXEL_COPY, &[0, 0, 4, 0]); + assert_eq!((at(&s, 4, 0), x.pixel_counts().bad_destination), (0, 1)); + cmd(&mut x, &s, X_PIXEL_SETUP, &[1, 3, 3, 0, 2, 1, 0]); + cmd(&mut x, &s, X_PIXEL_COPY, &[0, 0, 4, 0]); + assert_eq!(x.pixel_counts().window_copies, 1); + let (mut x, s) = rig(&RGB); + s.write(0, 31, 1, 0xab_cdef); + cmd(&mut x, &s, X_PIXEL_SETUP, &[1, 2, 2, 0, 1, 1, 0]); + cmd(&mut x, &s, X_PIXEL_COPY, &[0, 0, 5, 5]); + assert_eq!(at(&s, 5, 5), 0xab_cdef); + } + + /// A read packs the words as the setup asks, and a read that cannot be + /// answered from the store completes with the whole usable page of zeros. + #[test] + fn a_read_packs_the_plane_words_and_a_refused_one_answers_zeros() { + let (mut x, s) = rig(&INDEX); + // The native read context. + let mut gc = [0u32; 13]; + gc[6..].copy_from_slice(&[0x180c, 0x40001, 1, 6, 5, 1, 1]); + x.command(0x129, &gc); + s.write(2, 31 - 3, 1, 0xabcd); + s.write(3, 31 - 3, 1, 0x1234); + cmd(&mut x, &s, X_PIXEL_SETUP, &[2, 1, 0, 8, 2, 1, 0]); + assert_eq!(cmd(&mut x, &s, X_PIXEL_READ, &[2, 3]), Next::Return(vec![0xab, 0x12]), "the high byte of each index"); + cmd(&mut x, &s, X_PIXEL_SETUP, &[2, 1, 1, 12, 2, 1, 0]); + assert_eq!(cmd(&mut x, &s, X_PIXEL_READ, &[2, 3]), Next::Return(vec![0x0a, 0xbc, 0x01, 0x23]), "twelve bits each"); + // 24-bit RGB words. + let mut gc = [0u32; 13]; + gc[6..].copy_from_slice(&[0x2804, 0x60001, 1, 6, 8, 0, 0]); + x.command(0x129, &gc); + s.write(0, 31, 1, 0x00_0102_03); + cmd(&mut x, &s, X_PIXEL_SETUP, &[2, 2, 2, (-8i32) as u32, 1, 1, 0]); + assert_eq!(cmd(&mut x, &s, X_PIXEL_READ, &[0, 0]), Next::Return(vec![0, 1, 2, 3])); + assert_eq!(x.pixel_counts().reads_refused, 0); + // A frame-buffer word read has no tag to answer from: zeros, counted. + cmd(&mut x, &s, X_PIXEL_SETUP, &[2, 2, 2, 0, 1, 1, 0]); + assert_eq!(cmd(&mut x, &s, X_PIXEL_READ, &[0, 0]), Next::Return(vec![0; READ_LIMIT])); + // So is a rectangle off the screen, and a source too wide for the packing. + cmd(&mut x, &s, X_PIXEL_SETUP, &[2, 2, 2, (-8i32) as u32, 2, 1, 0]); + assert_eq!(cmd(&mut x, &s, X_PIXEL_READ, &[31, 0]), Next::Return(vec![0; READ_LIMIT])); + s.write(0, 31, 1, 0x100_0000); + cmd(&mut x, &s, X_PIXEL_SETUP, &[2, 2, 2, (-8i32) as u32, 1, 1, 0]); + assert_eq!(cmd(&mut x, &s, X_PIXEL_READ, &[0, 0]), Next::Return(vec![0; READ_LIMIT])); + let c = x.pixel_counts(); + assert_eq!((c.reads, c.reads_refused, c.fb_word_reads), (6, 3, 1)); + } + + /// A read inside a bracket waits for the finish, which hands back its + /// lines for the host buffer; a bracket whose descriptor or read does not + /// fit is refused and writes nothing. + #[test] + fn a_read_bracket_hands_back_its_lines_at_the_finish() { + let mut gc = [0u32; 13]; + gc[6..].copy_from_slice(&[0x2804, 0x60001, 1, 6, 8, 0, 0]); + let (mut x, s) = rig(&gc); + s.write(0, 31, 1, 0x11); + s.write(1, 31, 1, 0x22); + s.write(0, 30, 1, 0x33); + s.write(1, 30, 1, 0x44); + cmd(&mut x, &s, X_PIXEL_SETUP, &[2, 2, 2, (-8i32) as u32, 2, 2, 0]); + // Two lines of eight bytes, a gap of four between them. + x.feed_start(&[0x1c0, 0x9000 | 0x10, 16, 8, 4, 0], true); + assert_eq!(cmd(&mut x, &s, X_PIXEL_READ, &[0, 0]), Next::Nothing); + let done = x.feed_finish(&[0x100, 0x55]).expect("a finished read"); + assert_eq!((done.io, done.pitch, done.line, done.echo), (0x1c0 << 12 | 0x10, 12, 8, 0x55)); + assert_eq!(done.data, [0, 0, 0, 0x11, 0, 0, 0, 0x22, 0, 0, 0, 0x33, 0, 0, 0, 0x44]); + // One line the size of the whole read is the same bytes. + x.feed_start(&[0x1c0, 0x9000, 16, 16, 0, 0], true); + cmd(&mut x, &s, X_PIXEL_READ, &[0, 0]); + assert_eq!(x.feed_finish(&[0x100, 0]).map(|d| (d.line, d.data.len())), Some((16, 16))); + // A count that is not the read's, a bad tag, no context and a finish that is not 0x100 refuse. + for (desc, owned, mark) in [([0x1c0, 0x9000, 20, 20, 0, 0], true, 0x100), ([0x1c0, 0x8000, 16, 16, 0, 0], true, 0x100), ([0x1c0, 0x9000, 16, 16, 0, 0], false, 0x100), ([0x1c0, 0x9000, 16, 16, 0, 0], true, 0x101)] { + x.feed_start(&desc, owned); + cmd(&mut x, &s, X_PIXEL_READ, &[0, 0]); + assert_eq!(x.feed_finish(&[mark, 0]), None, "{desc:x?} {owned} {mark:#x}"); + } + assert_eq!(x.pixel_counts().bracket_refused, 4); + // A bracket with no read in it has nothing to write (a feedback pass's). + x.feed_start(&[0x1c0, 0x9000, 16, 16, 0, 0], true); + assert_eq!(x.feed_finish(&[0x100, 0]), None); + } +} diff --git a/src/dev/kona/back/x/state.rs b/src/dev/kona/back/x/state.rs new file mode 100644 index 000000000..6803d559e --- /dev/null +++ b/src/dev/kona/back/x/state.rs @@ -0,0 +1,361 @@ +//! The X server's drawing state: graphics context, position, clip and +//! scissor. +//! +//! The X server sends integer drawing in its own command stream, onto the same +//! raster state the GL path uses. These commands load the state that fills +//! then draw under: +//! +//! - 0x129 (`x_gc`) the thirteen words of the graphics context, decoded by +//! `gc` when a fill needs them; +//! - 0x12a (`x_position`) the drawable's x, y, width and height, the origin of +//! rectangle coordinates; +//! - 0x145 (`x_bdata4`) the leading words of a clip list longer than one +//! rectangle, kept until the 0x12b (`x_clip`) that ends it: a count `n` and +//! `n` rectangles `x1, y1, x2, y2`; +//! - 0x12d (`x_scissor`) a clip box (x1, y1, width, height); +//! - 0x13c, 0x13d, 0x13e the stipple cache (see `stipple`). +//! +//! Grades. The word layouts are software expectations from the senders. That +//! each clip rectangle is absolute with an exclusive far corner, and that +//! drawing clips to their union, is inferred. A list whose length does not +//! match its count, or a count of 0, leaves drawing clipped only to the +//! screen, since what the pipe makes of one is unknown. The scissor is applied +//! as a second rectangle with the clip and cleared at the next clip load: that +//! the pipe does either is inferred and how long it lasts is unknown. The +//! model keeps at most 64 clip rectangles, its own bound. + +use crate::dev::kona::defs::*; +use super::gc::{Gc, Status}; + +pub const X_GC: u32 = WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD; +pub const X_POSITION: u32 = WINDOW_SYSTEM_DRAWABLE_ORIGIN_AND_SIZE_LOAD; +pub const X_CLIP: u32 = WINDOW_SYSTEM_CLIP_LOAD; +pub const X_SCISSOR: u32 = WINDOW_SYSTEM_SCISSOR_LOAD; +pub const X_BDATA4: u32 = WINDOW_SYSTEM_FOUR_WORD_DATA_PRELOAD_BYTE_FORM; + +/// Clip rectangles the model keeps. +pub const CLIP_MAX_RECTS: usize = 64; +const CLIP_WORDS: usize = 5; +const BDATA4_WORDS: usize = 4; +const CLIP_LIST_WORDS: usize = 1 + 4 * CLIP_MAX_RECTS; + +/// The rectangle drawing lands in, far corner exclusive. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Bounds { + pub x1: i64, + pub y1: i64, + pub x2: i64, + pub y2: i64, +} + +#[derive(Clone, Debug)] +pub struct XState { + pub(super) gc: [u32; GC_WORDS], + position: [u32; 4], + clip: Vec<[i32; 4]>, + scissor: Option<[u32; 4]>, + /// Words of a clip list's leading data tokens, waiting for the clip. + clip_run: Vec, + /// Words of data tokens waiting for the box, rectangle or point that uses + /// them. + pub(super) run: Vec, + /// Buffers kept between requests, so a fill allocates nothing: the boxes + /// of a request and the spans of a row. + pub(super) boxes: Vec, + pub(super) spans: Vec<(i64, i64)>, + pub fills: super::fill::FillCounts, + pub stipple: super::stipple::Stipple, + pub(super) pixel: super::pixel::Pixel, + /// Clip lists refused (a count that does not match, or too long) and runs + /// dropped by another command. + pub clips_refused: u64, + pub runs_dropped: u64, + /// State commands taken: zero for a copy that was only ever selected. + pub loads: u64, +} + +impl Default for XState { + fn default() -> Self { + XState { gc: [0; GC_WORDS], position: [0; 4], clip: Vec::new(), scissor: None, clip_run: Vec::new(), run: Vec::new(), boxes: Vec::new(), spans: Vec::new(), fills: Default::default(), stipple: Default::default(), pixel: Default::default(), clips_refused: 0, runs_dropped: 0, loads: 0 } + } +} + +impl XState { + /// Whether a pixel transfer is part way, which a snapshot does not carry. + pub fn busy(&self) -> bool { + self.pixel.busy() + } + + /// The graphics context, position, clip, scissor, waiting data, stipple cache and + /// pixel set-up, as the stream module reads and writes them. Counters, the scratch + /// buffers and the refused counts are not saved. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { gc: _, position: _, clip: _, scissor: _, clip_run: _, run: _, boxes: _, spans: _, fills: _, stipple: _, pixel: _, clips_refused: _, runs_dropped: _, loads: _ } = self; + s.u32s(&mut self.gc); + s.u32s(&mut self.position); + let n = s.len(self.clip.len()); + if s.loading() { + self.clip.clear(); + self.clip.resize(n, [0; 4]); + } + for r in self.clip.iter_mut() { + s.i32s(r); + } + s.opt(&mut self.scissor, |s, v| s.u32s(v)); + s.vec_u32(&mut self.clip_run); + s.vec_u32(&mut self.run); + self.stipple.visit(s); + self.pixel.visit(s); + s.u64(&mut self.loads); + } + + #[cfg(test)] + /// Power-on state. + pub fn reset(&mut self) { + *self = XState::default(); + } + + /// The graphics context as last loaded, decoded for a fill. + pub fn gc(&self) -> Result { + Gc::decode_for_fill(&self.gc) + } + + #[cfg(test)] + pub fn gc_words(&self) -> &[u32; GC_WORDS] { + &self.gc + } + + /// The drawable's x and y, the origin of rectangle coordinates. + pub fn origin(&self) -> (i64, i64) { + (self.position[0] as i32 as i64, self.position[1] as i32 as i64) + } + + pub fn clip_rects(&self) -> &[[i32; 4]] { + &self.clip + } + + pub fn scissor(&self) -> Option<[u32; 4]> { + self.scissor + } + + /// The screen of `width` x `height`, narrowed to the bounds of the clip + /// list's rectangles when one is loaded and to the scissor when one is. + pub fn bounds(&self, width: u32, height: u32) -> Bounds { + let mut b = Bounds { x1: 0, y1: 0, x2: width as i64, y2: height as i64 }; + if !self.clip.is_empty() { + let (mut x1, mut y1, mut x2, mut y2) = (i64::MAX, i64::MAX, i64::MIN, i64::MIN); + for c in &self.clip { + x1 = x1.min(c[0] as i64); + y1 = y1.min(c[1] as i64); + x2 = x2.max(c[2] as i64); + y2 = y2.max(c[3] as i64); + } + b = Bounds { x1: b.x1.max(x1), y1: b.y1.max(y1), x2: b.x2.min(x2), y2: b.y2.min(y2) }; + } + if let Some(s) = self.scissor { + let (x1, y1) = (s[0] as i32 as i64, s[1] as i32 as i64); + b = Bounds { x1: b.x1.max(x1), y1: b.y1.max(y1), x2: b.x2.min(x1 + s[2] as i64), y2: b.y2.min(y1 + s[3] as i64) }; + } + b + } + + #[cfg(test)] + /// Whether (`x`, `y`), inside `bounds`, is in one of the clip list's + /// rectangles. One rectangle (or none) is the bounds themselves. + pub fn clip_contains(&self, x: i64, y: i64) -> bool { + self.clip.len() <= 1 || self.clip.iter().any(|c| x >= c[0] as i64 && y >= c[1] as i64 && x < c[2] as i64 && y < c[3] as i64) + } + + /// Every kernel command ends a waiting clip list's words except the two + /// that belong to it, and a waiting data run's except the tokens that use + /// or extend it. Called for every kernel-grammar command before it runs. + /// + /// The model this was taken from keeps a held run across words another + /// context sends (the run belongs to the context that began it; inferred + /// there). This model drops it: the context-ownership rule is not modelled, + /// to be revisited with the context-switch step. + pub fn end_runs(&mut self, selector: u32) { + if selector != X_BDATA4 && selector != X_CLIP && !self.clip_run.is_empty() { + self.clip_run.clear(); + self.runs_dropped += 1; + } + if !super::fill::keeps_run(selector) && !self.run.is_empty() { + self.run.clear(); + self.runs_dropped += 1; + } + } + + /// An X state command. False if `selector` is not one of them. + pub fn command(&mut self, selector: u32, args: &[u32]) -> bool { + self.end_runs(selector); + match selector { + X_GC => { + if let Ok(w) = <[u32; GC_WORDS]>::try_from(args) { + self.gc = w; + } + } + X_POSITION => { + if let Ok(w) = <[u32; 4]>::try_from(args) { + self.position = w; + } + } + X_BDATA4 => { + if self.clip_run.len() + BDATA4_WORDS + CLIP_WORDS > CLIP_LIST_WORDS { + self.clip_run.clear(); + self.runs_dropped += 1; + } else if args.len() == BDATA4_WORDS { + self.clip_run.extend_from_slice(args); + } + } + X_CLIP => self.load_clip(args), + super::stipple::X_LOAD_STIPPLE => self.stipple.load(args), + super::stipple::X_STORE_STIPPLE => { + self.stipple.begin_store(args); + } + super::stipple::X_ROTATE_STIPPLE => self.stipple.rotate(args), + X_SCISSOR => { + if let Ok(w) = <[u32; 4]>::try_from(args) { + self.scissor = Some(w); + } + } + _ => return false, + } + self.loads += 1; + true + } + + fn load_clip(&mut self, args: &[u32]) { + let mut list = std::mem::take(&mut self.clip_run); + self.scissor = None; + if args.len() != CLIP_WORDS { + self.clips_refused += 1; + self.clip.clear(); + return; + } + list.extend_from_slice(args); + let count = list[0] as usize; + if count == 0 || list.len() != 1 + 4 * count || count > CLIP_MAX_RECTS { + self.clips_refused += 1; + self.clip.clear(); + return; + } + self.clip = list[1..].chunks_exact(4).map(|r| [r[0] as i32, r[1] as i32, r[2] as i32, r[3] as i32]).collect(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn clip(x: &mut XState, rects: &[[i32; 4]]) { + let flat: Vec = std::iter::once(rects.len() as u32).chain(rects.iter().flatten().map(|v| *v as u32)).collect(); + // The first rectangle's words ride in the clip token; the rest go + // ahead of it as bdata4 tokens. + let (lead, last) = flat.split_at(flat.len() - CLIP_WORDS); + // bdata4 carries words in fours, from the list's first word. + for c in lead.chunks(BDATA4_WORDS) { + assert_eq!(c.len(), BDATA4_WORDS, "the test builds lists the senders would"); + x.command(X_BDATA4, c); + } + x.command(X_CLIP, last); + } + + #[test] + fn a_one_rectangle_clip_narrows_the_screen() { + let mut x = XState::default(); + clip(&mut x, &[[10, 20, 110, 220]]); + assert_eq!(x.bounds(1280, 1024), Bounds { x1: 10, y1: 20, x2: 110, y2: 220 }); + assert!(x.clip_contains(0, 0), "one rectangle is the bounds themselves"); + } + + /// Two rectangles: the bounds are their union's box and a pixel must be in + /// one of them. The first four words ride in a bdata4 token. + #[test] + fn a_longer_list_arrives_through_bdata4() { + let mut x = XState::default(); + // count 2: 1 + 8 = 9 words: bdata4 takes four, a second four, clip the last five? 9 = 4 + 5. + x.command(X_BDATA4, &[2, 0, 0, 10]); + x.command(X_CLIP, &[10, 50, 50, 60, 60]); + assert_eq!(x.clip_rects(), [[0, 0, 10, 10], [50, 50, 60, 60]]); + assert_eq!(x.bounds(100, 100), Bounds { x1: 0, y1: 0, x2: 60, y2: 60 }); + assert!(x.clip_contains(5, 5) && x.clip_contains(55, 55)); + assert!(!x.clip_contains(30, 30), "between the rectangles"); + } + + /// A count that does not match the words, or zero, leaves the screen. + #[test] + fn a_clip_list_that_does_not_match_its_count_clips_to_the_screen() { + let mut x = XState::default(); + x.command(X_CLIP, &[1, 5, 5, 9, 9]); + assert_eq!(x.clip_rects().len(), 1); + x.command(X_CLIP, &[0, 0, 0, 0, 0]); + assert!(x.clip_rects().is_empty()); + assert_eq!(x.bounds(8, 8), Bounds { x1: 0, y1: 0, x2: 8, y2: 8 }); + x.command(X_CLIP, &[3, 0, 0, 1, 1]); + assert!(x.clip_rects().is_empty()); + assert_eq!(x.clips_refused, 2); + } + + #[test] + fn a_scissor_narrows_the_bounds_until_the_next_clip() { + let mut x = XState::default(); + x.command(X_SCISSOR, &[100, 200, 30, 40]); + assert_eq!(x.bounds(1280, 1024), Bounds { x1: 100, y1: 200, x2: 130, y2: 240 }); + x.command(X_CLIP, &[1, 0, 0, 1280, 1024]); + assert_eq!(x.scissor(), None); + x.command(X_SCISSOR, &[0, 0, 0, 0]); + let b = x.bounds(16, 16); + assert!(b.x1 >= b.x2, "a scissor of no width clips everything"); + } + + /// Another command ends a waiting clip list; the clip and bdata4 do not. + #[test] + fn another_command_ends_a_waiting_clip_list() { + let mut x = XState::default(); + x.command(X_BDATA4, &[2, 0, 0, 10]); + x.command(X_BDATA4, &[10, 1, 1, 2]); + assert_eq!(x.runs_dropped, 0); + x.command(X_POSITION, &[1, 2, 3, 4]); + assert_eq!(x.runs_dropped, 1); + x.command(X_CLIP, &[1, 0, 0, 4, 4]); + assert_eq!(x.clip_rects(), [[0, 0, 4, 4]], "the dropped words are not in the list"); + } + + /// The run is bounded: a list past 64 rectangles is dropped. + #[test] + fn a_clip_list_is_bounded() { + let mut x = XState::default(); + for _ in 0..CLIP_LIST_WORDS / BDATA4_WORDS + 1 { + x.command(X_BDATA4, &[0; 4]); + } + assert_eq!(x.runs_dropped, 1); + } + + /// A clip token of the wrong length is refused, not indexed. + #[test] + fn a_clip_of_the_wrong_length_is_refused() { + let mut x = XState::default(); + x.command(X_CLIP, &[]); + x.command(X_CLIP, &[1, 2]); + assert_eq!(x.clips_refused, 2); + assert!(x.clip_rects().is_empty()); + } + + #[test] + fn state_loads_keep_their_words() { + let mut x = XState::default(); + let gc = [0x5a000, 0x1ffe, 0xa5000, 0x1ffe, 0xf00fff, 0, 0x180c, 0x40001, 1, 6, 5, 1, 1]; + assert!(x.command(X_GC, &gc)); + assert_eq!(x.gc_words(), &gc); + assert_eq!(x.gc().unwrap().foreground, 0x5a00); + x.command(X_POSITION, &[(-3i32) as u32, 7, 100, 50]); + assert_eq!(x.origin(), (-3, 7)); + assert!(!x.command(0x1, &[])); + // A load of the wrong length is ignored. + x.command(X_GC, &gc[..5]); + assert_eq!(x.gc_words(), &gc); + x.reset(); + assert_eq!(x.origin(), (0, 0)); + } +} diff --git a/src/dev/kona/back/x/stipple.rs b/src/dev/kona/back/x/stipple.rs new file mode 100644 index 000000000..d7ed42cad --- /dev/null +++ b/src/dev/kona/back/x/stipple.rs @@ -0,0 +1,271 @@ +//! The X server's stipple cache. +//! +//! Stippled fills are masked by a pattern the X server stores once and then +//! names: +//! +//! - 0x13d (`x_store_stipple`): an entry, the pattern's width and height, then +//! `height` rows, one word each, most significant bit leftmost (the rows come +//! behind the command, as a data run the back end asks for); +//! - 0x13e (`x_rotate_stipple`): an entry, then the pattern origin x, y; +//! - 0x13c (`x_load_stipple`): an entry whose pattern masks the box, rectangle, +//! span and point fills that follow, or all ones to end the masking. +//! +//! Grades. The command meanings and the row order are software expectations +//! from the senders; the bit order (most significant bit leftmost) is inferred +//! from a traced I-beam. The cache's size, 32 patterns of up to 32 by 32, is +//! the model's choice: a store outside it is refused, its rows consumed, and +//! the entry left holding no pattern. A store announcing more than 4096 rows +//! is this model's own bound: it is refused and its rows are not asked for. A store replaces the entry's pattern and +//! keeps its origin; whether the pipe does either is unknown. The pattern is +//! measured from the entry's origin plus the drawable's position, both +//! inferred, and bits past the width are ignored. A clear bit leaves its pixel +//! unchanged unless the context is opaque (inferred; no stippled fill in the +//! recorded streams shows which a real pipe does). With no stipple loaded, or +//! one holding no pattern, every pixel is painted. + +use crate::dev::kona::defs::*; + +pub const X_LOAD_STIPPLE: u32 = LOAD_WINDOW_SYSTEM_STIPPLE; +pub const X_STORE_STIPPLE: u32 = STORE_WINDOW_SYSTEM_STIPPLE; +pub const X_ROTATE_STIPPLE: u32 = ROTATE_WINDOW_SYSTEM_STIPPLE; + +/// Entries in the cache, and the largest pattern side. +pub const ENTRIES: usize = 32; +pub const MAX_SIDE: u32 = 32; +const NONE: u32 = u32::MAX; +/// The most rows a store may announce before it is refused. +const ROWS_MAX: u32 = 4096; + +#[derive(Clone, Debug)] +pub struct Stipple { + width: [u32; ENTRIES], + height: [u32; ENTRIES], + origin: [(i32, i32); ENTRIES], + rows: [[u32; MAX_SIDE as usize]; ENTRIES], + loaded: u32, + /// A store waiting for its rows: entry, width, height. + pending: Option<(u32, u32, u32)>, + /// Stores and loads refused (outside the cache, or an entry with no + /// pattern). + pub refused: u64, +} + +impl Default for Stipple { + fn default() -> Self { + Stipple { width: [0; ENTRIES], height: [0; ENTRIES], origin: [(0, 0); ENTRIES], rows: [[0; 32]; ENTRIES], loaded: NONE, pending: None, refused: 0 } + } +} + +fn fits(entry: u32, width: u32, height: u32) -> bool { + (entry as usize) < ENTRIES && (1..=MAX_SIDE).contains(&width) && (1..=MAX_SIDE).contains(&height) +} + +impl Stipple { + /// The cache, as the stream module reads and writes it (the counter is not saved). + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { width: _, height: _, origin: _, rows: _, loaded: _, pending: _, refused: _ } = self; + s.u32s(&mut self.width); + s.u32s(&mut self.height); + for o in self.origin.iter_mut() { + s.i32(&mut o.0); + s.i32(&mut o.1); + } + for r in self.rows.iter_mut() { + s.u32s(r); + } + s.u32(&mut self.loaded); + s.opt(&mut self.pending, |s, p| { + s.u32(&mut p.0); + s.u32(&mut p.1); + s.u32(&mut p.2); + }); + } + + /// A store's header: the rows to expect behind it. The entry loses its + /// pattern until they arrive. + pub fn begin_store(&mut self, args: &[u32]) -> u32 { + let [entry, width, height] = *args else { return 0 }; + if (entry as usize) < ENTRIES { + self.width[entry as usize] = 0; + self.height[entry as usize] = 0; + } + // The rows are asked for as one data run: a count past this model's + // own bound is refused whole rather than swallow the stream. A store + // refused for its size still has its rows consumed, up to that bound. + if !fits(entry, width, height) || height > ROWS_MAX { + self.refused += 1; + } + if height > ROWS_MAX { + self.pending = None; + return 0; + } + self.pending = (height != 0).then_some((entry, width, height)); + if height == 0 { + // No rows: the store is complete and holds nothing. + return 0; + } + height + } + + /// The rows behind a store. + pub fn rows(&mut self, words: &[u32]) { + let Some((entry, width, height)) = self.pending.take() else { return }; + if fits(entry, width, height) && words.len() == height as usize { + let e = entry as usize; + self.rows[e][..words.len()].copy_from_slice(words); + self.width[e] = width; + self.height[e] = height; + } + } + + /// Rows still owed to a store. + pub fn pending_rows(&self) -> u32 { + self.pending.map_or(0, |(_, _, h)| h) + } + + pub fn rotate(&mut self, args: &[u32]) { + let [entry, x, y] = *args else { return }; + if (entry as usize) < ENTRIES { + self.origin[entry as usize] = (x as i32, y as i32); + } else { + self.refused += 1; + } + } + + pub fn load(&mut self, args: &[u32]) { + let [entry] = *args else { return }; + self.loaded = if entry == NONE { + NONE + } else if (entry as usize) >= ENTRIES { + self.refused += 1; + NONE + } else { + if self.width[entry as usize] == 0 { + self.refused += 1; + } + entry + }; + } + + /// Whether a stipple with a pattern is loaded. + pub fn is_loaded(&self) -> bool { + (self.loaded as usize) < ENTRIES && self.width[self.loaded as usize] != 0 + } + + /// The loaded pattern: its rows, width, height and origin, if one with a + /// pattern is loaded. + pub fn loaded_pattern(&self) -> Option<([u32; 32], i64, i64, (i64, i64))> { + if !self.is_loaded() { + return None; + } + let e = self.loaded as usize; + Some((self.rows[e], self.width[e] as i64, self.height[e] as i64, (self.origin[e].0 as i64, self.origin[e].1 as i64))) + } + + #[cfg(test)] + /// Whether the loaded stipple lets a fill paint (`x`, `y`), the drawable + /// at (`ox`, `oy`). + pub fn bit(&self, x: i64, y: i64, ox: i64, oy: i64) -> bool { + if !self.is_loaded() { + return true; + } + let e = self.loaded as usize; + let (w, h) = (self.width[e] as i64, self.height[e] as i64); + let (px, py) = (ox + self.origin[e].0 as i64, oy + self.origin[e].1 as i64); + let row = self.rows[e][((y - py).rem_euclid(h)) as usize]; + row & (0x8000_0000u32 >> (x - px).rem_euclid(w)) != 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn stored(s: &mut Stipple, entry: u32, w: u32, rows: &[u32]) { + assert_eq!(s.begin_store(&[entry, w, rows.len() as u32]), rows.len() as u32); + s.rows(rows); + } + + /// A 3 x 2 pattern, most significant bit leftmost, tiles from the origin. + #[test] + fn a_stored_pattern_tiles_from_its_origin() { + let mut s = Stipple::default(); + stored(&mut s, 5, 3, &[0b101 << 29, 0b010 << 29]); + assert!(!s.is_loaded()); + assert!(s.bit(0, 0, 0, 0), "nothing loaded: every pixel paints"); + s.load(&[5]); + assert!(s.is_loaded()); + let row = |y| (0..6).map(|x| s.bit(x, y, 0, 0)).collect::>(); + assert_eq!(row(0), [true, false, true, true, false, true]); + assert_eq!(row(1), [false, true, false, false, true, false]); + assert_eq!(row(2), row(0), "rows repeat by the height"); + // Negative coordinates wrap the same way. + assert_eq!(s.bit(-1, 0, 0, 0), s.bit(2, 0, 0, 0)); + // The entry's origin and the drawable's position both shift it. + s.rotate(&[5, 1, 1]); + // Pattern bit (0, 0) is set and (1, 0) is clear. + assert!(s.bit(1, 1, 0, 0) && !s.bit(2, 1, 0, 0)); + assert!(s.bit(4, 3, 3, 2) && !s.bit(5, 3, 3, 2)); + } + + #[test] + fn load_none_or_a_bad_entry_ends_the_masking() { + let mut s = Stipple::default(); + stored(&mut s, 0, 1, &[0]); + s.load(&[0]); + assert!(!s.bit(0, 0, 0, 0)); + s.load(&[u32::MAX]); + assert!(s.bit(0, 0, 0, 0)); + s.load(&[0]); + s.load(&[ENTRIES as u32]); + assert!(s.bit(0, 0, 0, 0)); + assert_eq!(s.refused, 1); + // An entry with no pattern loads, refused, and does not mask. + s.load(&[7]); + assert!(!s.is_loaded()); + assert_eq!(s.refused, 2); + } + + /// A store outside the cache is refused, its rows are consumed (the + /// reply asks for them) and the entry holds nothing; a store keeps the + /// entry's origin and replaces its pattern. + #[test] + fn a_store_outside_the_cache_is_refused_and_a_store_replaces() { + let mut s = Stipple::default(); + assert_eq!(s.begin_store(&[40, 4, 2]), 2, "the rows still follow"); + s.rows(&[1, 2]); + assert_eq!(s.refused, 1); + assert_eq!(s.begin_store(&[3, 33, 1]), 1); + s.rows(&[1]); + assert_eq!(s.refused, 2); + s.load(&[3]); + assert!(!s.is_loaded()); + stored(&mut s, 3, 4, &[0xf000_0000]); + s.rotate(&[3, 2, 0]); + stored(&mut s, 3, 4, &[0x8000_0000]); + s.load(&[3]); + assert!(s.bit(2, 0, 0, 0), "the origin survived the second store"); + assert!(!s.bit(3, 0, 0, 0)); + // A store with no rows is complete at once and leaves the entry empty. + assert_eq!(s.begin_store(&[3, 4, 0]), 0); + assert_eq!(s.pending_rows(), 0); + s.load(&[3]); + assert!(!s.is_loaded()); + } + + /// An absurd row count is refused without asking for the rows. + #[test] + fn a_store_announcing_too_many_rows_is_refused() { + let mut s = Stipple::default(); + assert_eq!(s.begin_store(&[0, 8, u32::MAX]), 0); + assert_eq!((s.pending_rows(), s.refused), (0, 1)); + } + + #[test] + fn rotate_outside_the_cache_is_refused() { + let mut s = Stipple::default(); + s.rotate(&[ENTRIES as u32, 1, 1]); + assert_eq!(s.refused, 1); + } +} diff --git a/src/dev/kona/defs.rs b/src/dev/kona/defs.rs new file mode 100644 index 000000000..bc9f1b541 --- /dev/null +++ b/src/dev/kona/defs.rs @@ -0,0 +1,2829 @@ +//! Every number the board's host interface and drawing back end consume: widget +//! register offsets and field layouts, host command tokens, display-bus +//! registers and the layouts of the multi-field words, each under one name. +//! +//! Names and values are transcribed from the board's published command and +//! register documentation; where it grades a value inferred or unknown, the +//! comment says so. A name is a role in plain words, not a vendor symbol. Nothing else +//! in the module spells these numbers. The register offsets and the first +//! token constants were ported from include/hw/misc/sgi-kona.h and +//! include/hw/misc/sgi-kona-ports.h in jamesbraid/qemu-sgi-origami; software +//! expectations they state are QEMU's model, not hardware facts. +//! +//! This is a table of facts, so it holds names no code uses yet; the module +//! allows dead code for that reason alone. Most of the unused ones are the +//! tokens, display-bus registers and field layouts that the steps still to +//! come (textures, pixel operations, queries, the display's cursor and video +//! paths) read. + +#![allow(dead_code)] + +/// One widget's worth of address space; the rest reads unmapped. +pub const APERTURE: u64 = 16 << 20; + +// Crosstalk widget header. +pub const WIDGET_ID: u32 = 0x04; +pub const WIDGET_CONTROL: u32 = 0x24; +pub const INT_DEST_UPPER: u32 = 0x34; +pub const INT_DEST_LOWER: u32 = 0x3c; +pub const INT_VECTORS: u32 = 0x5c; +pub const PROBE: u32 = 0x64; +pub const LOW_WINDOW_WRITE: u32 = 0x7c; +pub const ONEWIRE: u32 = 0x98; +pub const NIC: u32 = 0x9c; + +// Register window at +0x800000. +pub const CREDIT: u32 = 0x800208; +pub const CREDIT_CONFIG: u32 = 0x800210; +pub const RESET_ARG: u32 = 0x800220; +pub const INTR_END: u32 = 0x800228; +pub const INT_ENABLE: u32 = 0x800230; +pub const INT_CAUSE: u32 = 0x800238; +pub const MAILBOX: u32 = 0x800240; +pub const RETRACE_TAG: u32 = 0x800248; +pub const RESET_STROBE: u32 = 0x800300; +pub const EEPROM_ENABLE: u32 = 0x800308; +pub const LOADER_328: u32 = 0x800328; +pub const ACTIVITY: u32 = 0x8003a8; +pub const LOADER_3B0: u32 = 0x8003b0; +pub const INDIRECT_SELECT: u32 = 0x8003c0; +pub const INDIRECT_DATA: u32 = 0x8003d0; +pub const PORT_ADDR: u32 = 0x8003e8; +pub const PORT_DATA: u32 = 0x8003f0; +pub const PORT_WRITE_NEXT: u32 = 0x8003f8; +/// The kernel FIFO command push port. Approximated as a register-block +/// relative address: the driver's own citation is +0x7c, which the +/// low-window register claims. +pub const DMA_FIFO_PUSH: u32 = 0x80007c; + +// Flow-controlled window and the command page. +pub const DMA_FIFO_KICK: u32 = 0xc00108; +pub const COMMAND_PAGE: u32 = 0xc10000; +pub const COMMAND_PAGE_SIZE: u32 = 0x4000; +pub const COMMAND_DATA_SLOT: u32 = 0xc; +pub const COMMAND_SLOT_SHIFT: u32 = 5; +pub const COMMAND_SLOT_MASK: u32 = 0x1f; +pub const COMMAND_TOKEN_MASK: u32 = 0x1ff; + +// XG map RAM. +pub const MAPRAM_BASE: u32 = 0x100000; +pub const MAPRAM_ENTRIES: usize = (1 << 20) / 4; +pub const PROBE_MAPRAM_COUNT_SHIFT: u32 = 3; +pub const PROBE_MAPRAM_COUNT_MASK: u32 = 0x7 << PROBE_MAPRAM_COUNT_SHIFT; + +// The widget id register. +pub const WIDGET_ID_REVISION_SHIFT: u32 = 28; +pub const WIDGET_ID_PART_SHIFT: u32 = 12; +pub const WIDGET_ID_MANUFACTURER_SHIFT: u32 = 1; +pub const WIDGET_ID_ALWAYS_ONE: u32 = 1; +pub const PART: u32 = 0xc102; +pub const MANUFACTURER: u32 = 0x2aa; + +// The one-wire register, in the layout the Bridge and IOC3 share: a write +// starts a slot. +pub const ONEWIRE_LEVEL: u32 = 1 << 0; +pub const ONEWIRE_DONE: u32 = 1 << 1; +pub const ONEWIRE_SAMPLE_MASK: u32 = 0xff << 2; +pub const ONEWIRE_PULSE_MASK: u32 = 0x3ff << 10; +pub const ONEWIRE_WRITABLE: u32 = ONEWIRE_SAMPLE_MASK | ONEWIRE_PULSE_MASK; +/// The Dallas family code of the board's identity part (a DS2502). +pub const ONEWIRE_FAMILY: u8 = 0x09; + +// Interrupt destination and vectors. +pub const INT_DEST_WIDGET_SHIFT: u32 = 16; +pub const INT_DEST_WIDGET_MASK: u32 = 0xf; +pub const INT_VECTORS_GE_SHIFT: u32 = 9; +pub const INT_VECTORS_GENERAL_MASK: u32 = 0xff; + +/// The fill-level register's field, bits 10:1 on the second host-interface +/// revision: a level in 32-bit words at two-word resolution (inferred). +pub const CREDIT_MASK: u32 = 0x7fe; +/// The interrupt-enable register's bits on the second revision (the first +/// has 0x3ff): one per status bit. +pub const INT_ENABLE_MASK: u32 = 0xfff; + +// Interrupt cause bits. +pub const CAUSE_LOWATER: u32 = 1 << 1; +pub const CAUSE_VIDEO: u32 = 1 << 3; +pub const CAUSE_MAILBOX: u32 = 1 << 4; +pub const CAUSE_GE: u32 = 1 << 5; + +/// Bit 0 of the activity register: the kernel writes 1 once per video interrupt (established by +/// the kernel's use; nothing reads it back). Counted, with no effect. +pub const ACTIVITY_VIDEO: u32 = 1 << 0; +pub const ACTIVITY_BUS: u32 = 1 << 2; + +// The indirect port's internal address space. +pub const FLASH_BASE: u32 = 0x800000; +pub const ICU_CONTROL: u32 = 0x200050; +pub const ARM_LOW_BASE: u32 = 0x0; +pub const ARM_LOW_WORDS: usize = 0x2c / 4; +pub const ARM_HIGH_BASE: u32 = 0x400_0000; +pub const ARM_HIGH_WORDS: usize = (0x414_e630 - 0x400_0000) / 4; +/// Sections of the firmware load the board records. +pub const LOAD_SECTIONS_MAX: usize = 32; + +// Selectors the driver's attach reads its pipe class from through the +// indirect pair. +pub const SELECT_PRODUCT: u32 = 0x8800; +pub const SELECT_REDUCED: u32 = 0xb00; + +// Bracket register bits. +pub const BRACKET_FORCE: u32 = 0x20000; +pub const BRACKET_PRIVILEGED: u32 = 0x10000; + +// Page words: bit 28 marks a host physical page, otherwise the page word is +// a device address through the map RAM. +pub const PAGE_WORD_PHYSICAL: u32 = 1 << 28; +pub const PAGE_WORD_FRAME: u32 = 0x0fff_ffff; +pub const MAPRAM_KIND_MASK: u32 = 0xf000_0000; +pub const MAPRAM_KIND_16K: u32 = 0x2000_0000; +pub const MAPRAM_PAGE_SHIFT: u32 = 14; + +// The textport grammar, the PROM's text console, drawn before any window +// exists. The table does not cover it; these are software expectations from +// QEMU's model (inferred). +/// The textport's sync word. +pub const TEXTPORT_SYNC_WORD: u32 = 1; +/// Commands framed and counted, with no drawing role known. +pub const TEXTPORT_UNMODELLED_1: u32 = 0x1; +pub const TEXTPORT_UNMODELLED_7: u32 = 0x7; +pub const TEXTPORT_UNMODELLED_8: u32 = 0x8; +pub const TEXTPORT_SET_COLOUR: u32 = 0x2; +pub const TEXTPORT_LOAD_COLOUR_MAP: u32 = 0x3; +pub const TEXTPORT_SET_RASTER_POSITION: u32 = 0x4; +pub const TEXTPORT_FILL_BOX: u32 = 0x5; +pub const TEXTPORT_PLOT_POINT: u32 = 0x6; +/// The sync probe, answered by the front end. +pub const TEXTPORT_SYNC_PROBE: u32 = 0x9; +/// Graphics info: the managed area, answered by the front end. +pub const TEXTPORT_GRAPHICS_INFO: u32 = 0xc; +/// The command that opens a bitmap blit. +pub const BITMAP_SELECTOR: u32 = 0xa; +pub const BITMAP_HEADER_WORDS: usize = 6; + + +/// The reasons the board posts on the reason page, and the one a context +/// clean-up carries when its window is destroyed (inferred). +pub const REASON_CONTEXT_CLEAN_UP: u32 = 0x103; +pub const REASON_CONTEXT_DEACTIVATED: u32 = 0x101; +pub const REASON_SWAP: u32 = 0x102; +pub const REASON_SUSPEND: u32 = 0x104; +/// The mailbox word a DMA packet's completion posts, which the packet asks +/// for with its last word. +pub const DMA_DONE_WORD: u32 = 0x100; + +/// Words the GL texture table command carries after its slot word. +pub const GL_TEX_TABLE_WORDS: u32 = 33; +/// Data words behind a cursor load: the image and the colours. +pub const X_CURSOR_DATA_WORDS: u32 = 260; +/// Data words behind a cursor recolour. +pub const X_CURSOR_RECOLOUR_WORDS: u32 = 4; + +/// The words a context's save area holds after a deactivate: word 0 is the +/// model's own marker ("KONA"), word 2 a no-op token the kernel sends back. +pub const SAVE_AREA_MARKER: u32 = 0x4b4f4e41; +pub const SAVE_AREA_TOKEN: u32 = 0x172; + +/// The sync probe's answer. +pub const SYNC_PROBE_REPLY: u32 = 0x1234abcd; + +/// Mailbox reply words the board queues at once. +pub const MAILBOX_MAX_WORDS: usize = 5; +/// Completion reasons that may wait behind an unread one. +pub const REASON_QUEUE: usize = 16; +/// The most words in one reason. +pub const REASON_WORDS: usize = 4; + +/// Display-list memory. +pub const LIST_DEFAULT: usize = 15 << 20; +pub const LIST_GRANULE: u32 = 0x400; +/// The most words a list invocation fetches: the display-list class's size. +pub const LIST_MAX_WORDS: u32 = (LIST_DEFAULT / 4) as u32; +pub const LIST_CALLS_MAX: u32 = 8; +pub const LIST_CALLS_POOL_MAX: u32 = 4; +pub const DMA_ADDR_TAG: u32 = 0x9000; +pub const DMA_PAGE_SIZE: u64 = 4096; +pub const DMA_POOL_PAGE: u32 = 0x4000; +pub const DMA_POOL_MAX: u32 = 256 << 20; +/// The most a swap moves, a model bound. +pub const SWAP_MAX_BYTES: u64 = 256 << 20; + +// Bit fields of the multi-field words. A field is `hi:lo`, inclusive, counted +// from zero at the least significant bit. + +/// A run of bits in a 32-bit word. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BitField { + pub hi: u8, + pub lo: u8, +} + +impl BitField { + pub const fn new(hi: u8, lo: u8) -> Self { + BitField { hi, lo } + } + + /// The field's bits, in place. + pub const fn mask(self) -> u32 { + (((1u64 << (self.hi - self.lo + 1)) - 1) << self.lo) as u32 + } + + /// The field's value in `word`. + pub const fn get(self, word: u32) -> u32 { + (word & self.mask()) >> self.lo + } + + /// `word` with the field set to `value` (truncated to its width). + pub const fn put(self, word: u32, value: u32) -> u32 { + (word & !self.mask()) | ((value << self.lo) & self.mask()) + } +} + +/// The display-ID table entry (XMAP indirect memory, written at `XMAP_ID_TABLE_DATA` +/// with the ID in `XMAP_INDEX`). Established. +pub struct DisplayIdEntry; +impl DisplayIdEntry { + /// The visual's colour-map slot number shifted left 7. + pub const COLOUR_MAP_BASE: BitField = BitField::new(23, 7); + /// The number of index planes minus 1: 7 for an 8-plane visual, 11 for 12. + pub const INDEX_PLANES: BitField = BitField::new(6, 3); + /// The luminance visual flag. What the XMAP does with it is unknown. + pub const LUMINANCE: BitField = BitField::new(2, 2); + /// The grey visual flag. What the XMAP does with it is unknown. + pub const GREY: BitField = BitField::new(1, 1); +} + +/// The per-display-ID mode byte, the third word of `DISPLAY_MODE_BYTE`. +/// Established as the X server builds it; what the XMAP does with bits 4:2 is +/// unknown. +pub struct ModeByte; +impl ModeByte { + /// The back buffer is shown (the kernel rewrites it at each swap). + pub const BACK_SHOWN: BitField = BitField::new(0, 0); + pub const STEREO: BitField = BitField::new(1, 1); + /// Non-colour-index RGB below 24 bits. + pub const RGB: BitField = BitField::new(2, 2); + /// Colour index or luminance. + pub const COLOUR_INDEX: BitField = BitField::new(3, 3); + /// Direct colour for a main window, opaque for an overlay. + pub const DIRECT_OR_OPAQUE: BitField = BitField::new(4, 4); + /// For an overlay, bit 0 is set for a non-RGB overlay (inferred from the X + /// server's visuals; the table lists the bit only as the back buffer shown). + pub const OVERLAY_NON_RGB: BitField = BitField::new(0, 0); + /// For an overlay: 2, or 3 when double buffered and starting in the back + /// buffer, plus 4 for an underlay. + pub const OVERLAY_CODE: BitField = BitField::new(3, 1); +} + +/// The channel set-up word (XMAP indirect memory, written at +/// `XMAP_CHANNEL_SETUP_DATA` with the channel in `XMAP_INDEX`). Established. +pub struct ChannelSetup; +impl ChannelSetup { + /// The gamma page, a map index 0 to 15. + pub const GAMMA_PAGE: BitField = BitField::new(3, 0); + /// Use the external 1024-entry gamma RAM (the model treats it as no gamma). + pub const EXTERNAL_GAMMA: BitField = BitField::new(4, 4); + pub const NO_GAMMA: BitField = BitField::new(5, 5); + pub const ALPHA_TO_BLUE: BitField = BitField::new(6, 6); +} + +/// The gamma load address (written to `XMAP_INDEX` before gamma data). +pub struct GammaAddress; +impl GammaAddress { + /// An address is 19 bits. + pub const BITS: u32 = 19; + pub const ENTRY: BitField = BitField::new(11, 0); + /// 0 red, 1 green, 2 blue. A hardware test program's gamma loads rewrite 3 to green (1); what the board + /// does with 3 is unknown, and the model keeps a fourth bank apart that nothing reads. + pub const COLOUR: BitField = BitField::new(13, 12); + pub const PAGE: BitField = BitField::new(17, 14); + /// Set for one kind of map. What it selects is unknown. + pub const FLAG: BitField = BitField::new(18, 18); +} + +/// The cursor position register. Established. +pub struct CursorPosition; +impl CursorPosition { + /// x less the hot spot and the rectangle origin, plus the bias. + pub const X: BitField = BitField::new(12, 0); + pub const Y: BitField = BitField::new(25, 13); + /// The offset that lets the cursor sit partly off the top-left corner + /// (inferred). + pub const BIAS: i64 = 32; +} + +/// The window-ID test register (raster register 0x1c06). Established. The +/// board encodes the window-placement request's word 9 into it: ordinary IDs +/// 0 to `WINDOW_ID_ORDINARY_LAST` as reference = ID, mask 0x3f; `WINDOW_ID_POPUP` +/// as reference 0x800, mask 0x800; the group IDs `WINDOW_ID_GROUP_FIRST` to +/// `WINDOW_ID_GROUP_LAST` as reference `(ID - 0x40) << 6`, mask 0x7c0; all ones +/// (the kernel's null clip, which makes a pipe own nothing) and anything else as +/// reference 0x3f, mask 0x3f. Which polarity a set mask bit has is inferred: a set bit +/// is compared. The compared value is the painted value shifted right by 2 (inferred): +/// the X server paints main ID n as `n << PAINT_MAIN_SHIFT` under mask 0xfc and an +/// overlay or popup group g as `g << PAINT_GROUP_SHIFT` under mask 0x1f00. Word 10 of the +/// placement, the "obscured" flag, sets the enable bit. +#[allow(non_snake_case)] +pub mod WINDOW_ID_TEST { + use super::BitField; + pub const ENABLE: BitField = BitField::new(0, 0); + pub const REFERENCE_ID: BitField = BitField::new(12, 1); + pub const MASK: BitField = BitField::new(24, 13); +} +/// Where a display ID sits in a painted pixel value (established): a main ID from bit 2, an +/// overlay or popup group (the ID less 0x40) from bit 8. +pub const PAINT_MAIN_SHIFT: u32 = 2; +pub const PAINT_GROUP_SHIFT: u32 = 8; +pub const WINDOW_ID_ORDINARY_LAST: i32 = 0x3e; +pub const WINDOW_ID_POPUP: i32 = 0x3f; +pub const WINDOW_ID_GROUP_FIRST: i32 = 0x40; +pub const WINDOW_ID_GROUP_LAST: i32 = 0x5f; + +// XMAP registers on the display bus (0x100 + n), as the indirect store names +// them. +/// The cursor image base: the address of the 260-word block. +pub const XMAP_CURSOR_IMAGE_BASE: u32 = 0x101; +pub const XMAP_CURSOR_POSITION: u32 = 0x102; +pub const XMAP_CURSOR_MODE: u32 = 0x103; +/// The address for the data ports: a memory address, a channel number or a +/// display ID. +pub const XMAP_INDEX: u32 = 0x118; +pub const XMAP_COLOUR_DATA: u32 = 0x119; +pub const XMAP_GAMMA_DATA: u32 = 0x11b; +/// Display-ID table data (the ID is in `XMAP_INDEX`). +pub const XMAP_ID_TABLE_DATA: u32 = 0x11c; +/// Channel set-up word (the channel is in `XMAP_INDEX`). +pub const XMAP_CHANNEL_SETUP_DATA: u32 = 0x11d; + +/// The display request that loads the DAC cursor (263 words; established: the channel, the six +/// colours as 10-bit values, and 256 image words of two bits a pixel, `pixel = mask << 1 | source`). +/// The model decodes and keeps the load (`display::dac_cursor`) and does not draw it. +pub const VIDEO_DAC_CURSOR_LOAD: u32 = 56; + +// The display generator's video commands the model gives behaviour (the command number is the first +// word after the video token; `tables::VIDEO` frames all 93). +pub const VIDEO_SETUP_FRAME_BUFFER: u32 = 0x08; +pub const VIDEO_CHANNEL_GAMMA_MAP: u32 = 0x0f; +pub const VIDEO_GET_BOARD_CONFIG: u32 = 0x12; +pub const VIDEO_GET_CHANNEL_CONFIG: u32 = 0x13; +pub const VIDEO_RESIZE: u32 = 0x14; +pub const VIDEO_GET_RESIZE: u32 = 0x15; +pub const VIDEO_LOAD_FORMAT: u32 = 0x16; +pub const VIDEO_ENABLE_CHANNEL: u32 = 0x17; +pub const VIDEO_DISABLE_CHANNEL: u32 = 0x18; +pub const VIDEO_CURSOR_PRIORITY: u32 = 0x1e; +pub const VIDEO_CHANNEL_BLANKING: u32 = 0x1f; +pub const VIDEO_GET_CHANNEL_BLANKING: u32 = 0x20; +pub const VIDEO_BIND_TO_X_WINDOW: u32 = 0x34; +pub const VIDEO_GET_VAMPIRE: u32 = 0x3a; +pub const VIDEO_TEMP_BLANK: u32 = 0x3c; +pub const VIDEO_GET_CHANNEL_ENABLES: u32 = 0x42; +pub const VIDEO_PLACEMENT_RES: u32 = 0x43; +pub const VIDEO_GET_FORMAT_INFO: u32 = 0x4a; +pub const VIDEO_VAMPIRE: u32 = 0x4f; +pub const VIDEO_X_SERVER_INIT: u32 = 0x53; + +/// The gamma-map word that means the linear map. +pub const VIDEO_GAMMA_MAP_LINEAR: u32 = 0xffff_fffe; + +/// The channels a DAC cursor load can name: 0 to 7, or 0xde (established). +pub const DAC_CURSOR_CHANNELS: std::ops::RangeInclusive = 0..=7; +/// The one value outside 0 to 7 a load may name. The table gives the value and not what it selects. +pub const DAC_CURSOR_CHANNEL_DE: u32 = 0xde; + +/// The video DAC's cursor control register (indexed register 0x204; the bit meanings are the +/// DAC part's published register description, established; that the part is that DAC is inferred). +pub mod dac_cursor_control { + use super::BitField; + /// The cursor mode: 1 is X11, 2 is XGA. + pub const MODE: BitField = BitField::new(1, 0); + pub const MODE_X11: u32 = 1; + pub const MODE_XGA: u32 = 2; + pub const ENABLE: BitField = BitField::new(2, 2); + pub const INTERLACE: BitField = BitField::new(3, 3); + /// What the kernel writes for the channel that owns the cursor (X11, enabled, not interlaced); + /// it writes 0 for the others (established). + pub const KERNEL_OWNER: u32 = 5; + + /// The control value in words, for the monitor. + pub fn describe(v: u32) -> String { + let mode = match MODE.get(v) { + MODE_X11 => "X11".to_string(), + MODE_XGA => "XGA".to_string(), + n => format!("mode {n}"), + }; + format!("{v:#x} = {mode} cursor, {}, {}", if ENABLE.get(v) != 0 { "enabled" } else { "disabled" }, if INTERLACE.get(v) != 0 { "interlaced" } else { "not interlaced" }) + } +} + +// Cursor modes (the value written to `XMAP_CURSOR_MODE`). +pub const CURSOR_MODE_OFF: u32 = 0; +pub const CURSOR_MODE_CROSSHAIR: u32 = 1; +pub const CURSOR_MODE_GLYPH: u32 = 2; +/// A glyph slot: 64 rows of four words (plane A then plane B), then the four +/// colour words 10:10:10 at `CURSOR_COLOUR_WORDS` (pixel values 3, 2, 1, 0). +pub const CURSOR_SLOT_WORDS: usize = 260; +pub const CURSOR_SLOTS: usize = 39; +pub const CURSOR_COLOUR_WORDS: usize = 256; + +// Display IDs. Established: main-plane windows 0 to 62, the popup at 63, +// overlays 64 to 95. +pub const POPUP_ID: usize = 63; +/// A point up to this size is one pixel; a larger one is a disc. +pub const POINT_PIXEL_MAX: f32 = 3.5; +pub const OVERLAY_ID_FIRST: usize = 64; +pub const OVERLAY_ID_LAST: usize = 95; +/// Display IDs the model's tables hold. +pub const DISPLAY_IDS: usize = OVERLAY_ID_LAST + 1; + +/// OpenGL enumerants the pipe's state tokens carry. Values are OpenGL's. +pub mod gl { + pub const NEVER: u32 = 0x200; + pub const LESS: u32 = 0x201; + pub const EQUAL: u32 = 0x202; + pub const LEQUAL: u32 = 0x203; + pub const GREATER: u32 = 0x204; + pub const NOTEQUAL: u32 = 0x205; + pub const GEQUAL: u32 = 0x206; + pub const ALWAYS: u32 = 0x207; + pub const DEPTH_BUFFER_BIT: u32 = 0x100; + pub const FRONT_LEFT: u32 = 0x400; + pub const FRONT_RIGHT: u32 = 0x401; + pub const BACK_LEFT: u32 = 0x402; + pub const FRONT: u32 = 0x404; + pub const BACK: u32 = 0x405; + pub const RIGHT: u32 = 0x406; + pub const FRONT_AND_BACK: u32 = 0x408; + pub const INVALID_ENUM: u32 = 0x500; + pub const INVALID_OPERATION: u32 = 0x502; + pub const STACK_OVERFLOW: u32 = 0x503; + pub const STACK_UNDERFLOW: u32 = 0x504; + pub const CW: u32 = 0x900; + pub const CULL_FACE: u32 = 0xb44; + pub const LIGHTING: u32 = 0xb50; + pub const LIGHT_MODEL_LOCAL_VIEWER: u32 = 0xb51; + pub const LIGHT_MODEL_TWO_SIDE: u32 = 0xb52; + pub const LIGHT_MODEL_AMBIENT: u32 = 0xb53; + pub const COLOR_MATERIAL: u32 = 0xb57; + pub const DEPTH_TEST: u32 = 0xb71; + pub const NORMALIZE: u32 = 0xba1; + pub const SCISSOR_TEST: u32 = 0xc11; + pub const TEXTURE_2D: u32 = 0xde1; + pub const TEXTURE_3D: u32 = 0x806f; + /// The two-bit RGB internal format, EXT's name for 0x804e. + pub const RGB2_EXT: u32 = 0x804e; + pub const FLAT: u32 = 0x1d00; + pub const COLOR_BUFFER_BIT: u32 = 0x4000; + /// Light n is `LIGHT0 + n`. + pub const LIGHT0: u32 = 0x4000; + pub const LIGHT7: u32 = 0x4007; + /// User clip plane n is `CLIP_PLANE0 + n`, for n = 0 to 5. + pub const CLIP_PLANE0: u32 = 0x3000; + pub const CLIP_PLANE5: u32 = 0x3005; + /// The rest of the enable tokens' clip-plane range, 0x3000 to 0x3fff: an invalid enumerant. + pub const CLIP_PLANE_RANGE_FIRST: u32 = 0x3006; + pub const CLIP_PLANE_RANGE_LAST: u32 = 0x3fff; + /// Texture coordinate generation: the coordinates s, t, r, q are `TEXTURE_COORD_S + i`, + /// their enables `TEXTURE_GEN_S + i`, and the parameter names the mode and the two planes. + pub const TEXTURE_COORD_S: u32 = 0x2000; + pub const TEXTURE_GEN_S: u32 = 0xc60; + pub const TEXTURE_GEN_Q: u32 = 0xc63; + pub const TEXTURE_GEN_MODE: u32 = 0x2500; + pub const OBJECT_PLANE: u32 = 0x2501; + pub const EYE_PLANE: u32 = 0x2502; + /// The point and line distance planes of the texgen extension. + pub const DISTANCE_PLANE_FIRST: u32 = 0x81f4; + pub const DISTANCE_PLANE_LAST: u32 = 0x81f7; + /// The generation modes of the point and line distance planes. + pub const DISTANCE_MODE_FIRST: u32 = 0x81f0; + pub const DISTANCE_MODE_LAST: u32 = 0x81f3; + pub const CLIP_PLANES: usize = 6; + // Capabilities and clear bits of the fragment operations. + pub const STENCIL_BUFFER_BIT: u32 = 0x400; + pub const ACCUM_BUFFER_BIT: u32 = 0x200; + // The multisample enables. + pub const MULTISAMPLE: u32 = 0x809d; + pub const SAMPLE_ALPHA_TO_MASK: u32 = 0x809e; + pub const SAMPLE_ALPHA_TO_ONE: u32 = 0x809f; + pub const SAMPLE_MASK: u32 = 0x80a0; + pub const ALPHA_TEST: u32 = 0xbc0; + pub const LINE_STIPPLE: u32 = 0xb24; + pub const POLYGON_STIPPLE: u32 = 0xb42; + pub const BLEND: u32 = 0xbe2; + pub const STENCIL_TEST: u32 = 0xb90; + pub const COLOR_LOGIC_OP: u32 = 0xbf2; + pub const DITHER: u32 = 0xbd0; + pub const POLYGON_OFFSET_FILL: u32 = 0x8037; + // Fog: the capability, the parameter names the setter carries (the same numbers the + // generic state query lists) and the three modes. + // The polygon modes a face can be drawn in. + pub const POINT: u32 = 0x1b00; + pub const LINE: u32 = 0x1b01; + pub const FILL: u32 = 0x1b02; + // The smoothing enables. + pub const POINT_SMOOTH: u32 = 0xb10; + pub const LINE_SMOOTH: u32 = 0xb20; + pub const POLYGON_SMOOTH: u32 = 0xb41; + pub const FOG: u32 = 0xb60; + /// The seven names the fog setter accepts (the index and the offset extension are kept by the pipe, + /// and not modelled). + pub const FOG_INDEX: u32 = 0xb61; + pub const FOG_OFFSET: u32 = 0x8199; + pub const FOG_DENSITY: u32 = 0xb62; + pub const FOG_START: u32 = 0xb63; + pub const FOG_END: u32 = 0xb64; + pub const FOG_MODE: u32 = 0xb65; + pub const FOG_COLOR: u32 = 0xb66; + pub const LINEAR: u32 = 0x2601; + pub const EXP: u32 = 0x800; + pub const EXP2: u32 = 0x801; + /// The fog function table extension's mode, accepted and not modelled. + pub const FOG_FUNC: u32 = 0x812a; + // Stencil operations. + pub const KEEP: u32 = 0x1e00; + pub const REPLACE: u32 = 0x1e01; + pub const INCR: u32 = 0x1e02; + pub const DECR: u32 = 0x1e03; + pub const INVERT: u32 = 0x150a; + pub const ZERO: u32 = 0; + // Blend factors (`ZERO` and `ONE` are 0 and 1). + pub const ONE: u32 = 1; + pub const SRC_COLOR: u32 = 0x300; + pub const ONE_MINUS_SRC_COLOR: u32 = 0x301; + pub const SRC_ALPHA: u32 = 0x302; + pub const ONE_MINUS_SRC_ALPHA: u32 = 0x303; + pub const DST_ALPHA: u32 = 0x304; + pub const ONE_MINUS_DST_ALPHA: u32 = 0x305; + pub const DST_COLOR: u32 = 0x306; + pub const ONE_MINUS_DST_COLOR: u32 = 0x307; + pub const SRC_ALPHA_SATURATE: u32 = 0x308; + pub const CONSTANT_COLOR: u32 = 0x8001; + pub const ONE_MINUS_CONSTANT_COLOR: u32 = 0x8002; + pub const CONSTANT_ALPHA: u32 = 0x8003; + pub const ONE_MINUS_CONSTANT_ALPHA: u32 = 0x8004; + /// The logic operations are `LOGIC_OP_CLEAR + n` for n = 0 to 15, in the order + /// the X server numbers them (clear, and, and-reverse, copy, ...). + pub const LOGIC_OP_CLEAR: u32 = 0x1500; + // Matrix modes. + pub const MODELVIEW: u32 = 0x1700; + pub const PROJECTION: u32 = 0x1701; + pub const TEXTURE: u32 = 0x1702; + // Light and material parameters. + pub const AMBIENT: u32 = 0x1200; + pub const DIFFUSE: u32 = 0x1201; + pub const SPECULAR: u32 = 0x1202; + pub const POSITION: u32 = 0x1203; + pub const SPOT_DIRECTION: u32 = 0x1204; + pub const SPOT_EXPONENT: u32 = 0x1205; + pub const SPOT_CUTOFF: u32 = 0x1206; + pub const CONSTANT_ATTENUATION: u32 = 0x1207; + pub const LINEAR_ATTENUATION: u32 = 0x1208; + pub const QUADRATIC_ATTENUATION: u32 = 0x1209; + pub const EMISSION: u32 = 0x1600; + pub const SHININESS: u32 = 0x1601; + pub const AMBIENT_AND_DIFFUSE: u32 = 0x1602; +} + +// The X graphics-context load (token WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, thirteen +// words), by word. The token is routed in hardware; the engine program keeps the first four +// words in its own memory and writes words 4 to 12, one each, to nine raster registers, under +// the masks `GC_WORD_MASKS` (read from the instruction operands: the pairing of word to +// register and the masks are established). What the engine does with word 4 in its special +// register 0x23 is unknown. A zero in the background high word makes the engine skip the +// raster-register part of the load. +pub const GC_WORDS: usize = 13; +/// Foreground colour, high half. +pub const GC_FOREGROUND_HIGH: usize = 0; +/// Foreground colour, low half. +pub const GC_FOREGROUND_LOW: usize = 1; +/// Background colour, high half. +pub const GC_BACKGROUND_HIGH: usize = 2; +/// Background colour, low half. +pub const GC_BACKGROUND_LOW: usize = 3; +/// Plane write mask, the red and green 12-bit fields: raster register 0x1c14 (and special register 0x23). +pub const GC_WRITE_MASK_FIRST: usize = 4; +/// Plane write mask, the blue and alpha fields: raster register 0x1c15. +pub const GC_WRITE_MASK_SECOND: usize = 5; +/// The colour format (bits 5:2) and read format (bits 13:11) of the frame-buffer configuration, raster +/// register 0x1c00. The pixel size and the four write enables are not written. +pub const GC_FRAMEBUFFER_CONFIG: usize = 6; +/// The draw buffer (bits 3:0), read-back format (17:14) and read base (23:18) of the operation register, 0x1c02. +pub const GC_OPERATION: usize = 7; +/// The colour base address only, the first of the six 6-bit fields of raster register 0x1c0b. +pub const GC_FRAMEBUFFER_BASES: usize = 8; +/// The logic-operation code (bits 4:1) only of raster register 0x1c0d: no blend field, no enable bit. +pub const GC_BLEND_AND_LOGIC: usize = 9; +/// The bus pixel-format code (5 bits), raster register 0x3ff8. +pub const GC_PIXEL_PACK_CODE: usize = 10; +/// The luminance-alpha colour flag for the texture filter, raster register 0x140f. +pub const GC_TEXTURE_FILTER_COLOUR_FLAG: usize = 11; +/// The luminance-alpha pixel flag for the texture filter, raster register 0x1410. +pub const GC_TEXTURE_FILTER_PIXEL_FLAG: usize = 12; +/// The masks the engine applies to words 4 to 12 (established). +pub const GC_WORD_MASKS: [u32; 9] = [0xff_ffff, 0xff_ffff, 0x383c, 0xff_c00f, 0x3f, 0x1e, 0x1f, 0x1, 0x1]; + +// The X pixel path's seven-word set-up (token WINDOW_SYSTEM_PIXEL_PATH_SET_UP), by +// word. Word 0's low four bits are the transfer kind (0: frame buffer only; +// nonzero also sets up the texture-filter chip; 2 also loads the host-interface +// packer from word 2) and bit 4 the direct-write flag, kept until the finish. Clear, +// the write's data words are counted through the command stream; set, they are not, +// so the rows come by DMA. That the flag exists, that the bitmap loaders send it +// clear and that the count is off when it is set are established; that set means +// DMA is inferred from the recording. Word 1 names the engine's pixel-output module (4: bitmap +// and glyph output with a one-bit-to-pixel conversion; 5: opaque bitmap draw with a +// 16- or 32-bit conversion chosen by word 2); the model also reads word 1 as the +// source pixel packing. Words 3 and 6 are passed to the engine and distributor +// set-up and the X module sends 0: their meaning is unknown, as are the transfer +// kinds other than 0 and 2 and the output modules other than 4 and 5. +pub const PIXEL_SETUP_WORDS: usize = 7; +pub const PIXEL_SETUP_TRANSFER_KIND: usize = 0; +pub const PIXEL_SETUP_OUTPUT_MODULE: usize = 1; +pub const PIXEL_SETUP_CONVERSION_OR_PACKER: usize = 2; +pub const PIXEL_SETUP_ENGINE_VALUE: usize = 3; +pub const PIXEL_SETUP_WIDTH: usize = 4; +pub const PIXEL_SETUP_HEIGHT: usize = 5; +pub const PIXEL_SETUP_DISTRIBUTOR_VALUE: usize = 6; +pub const PIXEL_SETUP_TRANSFER_KIND_MASK: u32 = 0xf; +pub const PIXEL_SETUP_DIRECT_WRITE_FLAG: u32 = 0x10; +pub const PIXEL_OUTPUT_MODULE_BITMAP: u32 = 4; +pub const PIXEL_OUTPUT_MODULE_OPAQUE_BITMAP: u32 = 5; +// The pixel transfer's payloads (established): a read's x and y, measured from the +// bottom; a copy's source x and y and destination x and y; a finish's one word, zero. +pub const PIXEL_READ_WORDS: usize = 2; +pub const PIXEL_COPY_WORDS: usize = 4; + +/// Entries in the front end's context table: 64, and one more for its transient entry. +pub const CONTEXT_SLOTS: usize = 65; + +// The host command tokens, by the role the definitions table gives each. The +// grade is the table's: established unless the comment says otherwise. + +/// Raw data word. +pub const RAW_DATA_WORD: u32 = 0x000; +/// Configure the frame-buffer layout (colour, depth, stencil, accumulation, multisample) (inferred). +pub const CONFIGURE_FRAME_BUFFER_LAYOUT: u32 = 0x001; +/// Flush and rebuild the hardware token map for packed-vertex mode (inferred). +pub const FLUSH_AND_REBUILD_HARDWARE_TOKEN_MAP_FOR_PACKED_VERTEX: u32 = 0x002; +/// Begin primitive: rectangle (shares the quad entry). +pub const BEGIN_PRIMITIVE_RECTANGLE: u32 = 0x003; +/// Configure the read-side frame-buffer layout (inferred). +pub const CONFIGURE_READ_SIDE_FRAME_BUFFER_LAYOUT: u32 = 0x004; +/// Draw a bitmap from a display list (inferred). +pub const DRAW_BITMAP_FROM_DISPLAY_LIST: u32 = 0x005; +/// Draw a bitmap (inferred). +pub const DRAW_BITMAP: u32 = 0x006; +/// Set a light parameter (float form) (inferred). +pub const SET_LIGHT_PARAMETER_FLOAT_FORM: u32 = 0x007; +/// Set a light parameter (integer form) (inferred). +pub const SET_LIGHT_PARAMETER_INTEGER_FORM: u32 = 0x008; +/// Set a lighting-model parameter (float form) (inferred). +pub const SET_LIGHTING_MODEL_PARAMETER_FLOAT_FORM: u32 = 0x009; +/// Set a lighting-model parameter (integer form) (inferred). +pub const SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM: u32 = 0x00a; +/// Load the polygon stipple pattern (inferred). +pub const LOAD_POLYGON_STIPPLE_PATTERN: u32 = 0x00b; +/// Set the scissor box (inferred). +pub const SET_SCISSOR_BOX: u32 = 0x00c; +/// Texture parameter, float. +pub const TEXTURE_PARAMETER_FLOAT: u32 = 0x00d; +/// Texture parameter, integer. +pub const TEXTURE_PARAMETER_INTEGER: u32 = 0x00e; +/// 1D texture image load. +pub const T_1D_TEXTURE_IMAGE_LOAD: u32 = 0x00f; +/// 2D texture image load. +pub const T_2D_TEXTURE_IMAGE_LOAD: u32 = 0x010; +/// Set a texture-environment parameter (float form) (inferred). +pub const SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM: u32 = 0x011; +/// Set a texture-environment parameter (integer form) (inferred). +pub const SET_TEXTURE_ENVIRONMENT_PARAMETER_INTEGER_FORM: u32 = 0x012; +/// Texture-coordinate generation, double form (unknown). +pub const TEXTURE_COORDINATE_GENERATION_DOUBLE_FORM: u32 = 0x013; +/// Texture-coordinate generation, float form (inferred). +pub const TEXTURE_COORDINATE_GENERATION_FLOAT_FORM: u32 = 0x014; +/// Texture-coordinate generation, integer form (unknown). +pub const TEXTURE_COORDINATE_GENERATION_INTEGER_FORM: u32 = 0x015; +/// Bind a texture by old-style binding (unknown). +pub const BIND_TEXTURE_BY_OLD_STYLE_BINDING: u32 = 0x016; +/// Select the feedback record type (inferred). +pub const SELECT_FEEDBACK_RECORD_TYPE: u32 = 0x017; +/// Select-buffer registration. +pub const SELECT_BUFFER_REGISTRATION: u32 = 0x018; +/// Switch between render, select and feedback modes (inferred). +pub const SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES: u32 = 0x019; +/// Clear the selected buffers (inferred). +pub const CLEAR_SELECTED_BUFFERS: u32 = 0x01a; +/// Set the accumulation-buffer clear value (inferred). +pub const SET_ACCUMULATION_BUFFER_CLEAR_VALUE: u32 = 0x01b; +/// Set the colour-index clear value (inferred). +pub const SET_COLOUR_INDEX_CLEAR_VALUE: u32 = 0x01c; +/// Set the colour clear value (inferred). +pub const SET_COLOUR_CLEAR_VALUE: u32 = 0x01d; +/// Set the stencil clear value (inferred). +pub const SET_STENCIL_CLEAR_VALUE: u32 = 0x01e; +/// Set the depth clear value (inferred). +pub const SET_DEPTH_CLEAR_VALUE: u32 = 0x01f; +/// Set the stencil write mask (inferred). +pub const SET_STENCIL_WRITE_MASK: u32 = 0x020; +/// Set the colour write mask (inferred). +pub const SET_COLOUR_WRITE_MASK: u32 = 0x021; +/// Set the depth write mask (inferred). +pub const SET_DEPTH_WRITE_MASK: u32 = 0x022; +/// Set the colour-index write mask (inferred). +pub const SET_COLOUR_INDEX_WRITE_MASK: u32 = 0x023; +/// Enable a capability (inferred). +pub const ENABLE_CAPABILITY: u32 = 0x024; +/// Disable a capability (inferred). +pub const DISABLE_CAPABILITY: u32 = 0x025; +/// Evaluate a 1D grid strip (inferred). +pub const EVALUATE_1D_GRID_STRIP: u32 = 0x026; +/// Evaluate a 2D grid strip (inferred). +pub const EVALUATE_2D_GRID_STRIP: u32 = 0x027; +/// Set up a 1D evaluator grid. +pub const SET_UP_1D_EVALUATOR_GRID: u32 = 0x028; +/// Set up a 2D evaluator grid. +pub const SET_UP_2D_EVALUATOR_GRID: u32 = 0x029; +/// Load 1D evaluator control points (inferred). +pub const LOAD_1D_EVALUATOR_CONTROL_POINTS: u32 = 0x02a; +/// Load 2D evaluator control points (inferred). +pub const LOAD_2D_EVALUATOR_CONTROL_POINTS: u32 = 0x02b; +/// Set up a 1D evaluator map specification (inferred). +pub const SET_UP_1D_EVALUATOR_MAP_SPECIFICATION: u32 = 0x02c; +/// Set up a 2D evaluator map specification (inferred). +pub const SET_UP_2D_EVALUATOR_MAP_SPECIFICATION: u32 = 0x02d; +/// Set the logic operation (inferred). +pub const SET_LOGIC_OPERATION: u32 = 0x02e; +/// Set the pixel zoom factors (inferred). +pub const SET_PIXEL_ZOOM_FACTORS: u32 = 0x02f; +/// Set a pixel-transfer parameter (float form) (inferred). +pub const SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM: u32 = 0x030; +/// Set a pixel-transfer parameter (integer form). +pub const SET_PIXEL_TRANSFER_PARAMETER_INTEGER_FORM: u32 = 0x031; +/// Set a pixel-store parameter (float form). +pub const SET_PIXEL_STORE_PARAMETER_FLOAT_FORM: u32 = 0x032; +/// Set a pixel-store parameter (integer form) (inferred). +pub const SET_PIXEL_STORE_PARAMETER_INTEGER_FORM: u32 = 0x033; +/// Load a pixel map (float values) (unknown). +pub const LOAD_PIXEL_MAP_FLOAT_VALUES: u32 = 0x034; +/// Load a pixel map (unsigned 32-bit values) (unknown). +pub const LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES: u32 = 0x035; +/// Load a pixel map (unsigned 16-bit values) (unknown). +pub const LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES: u32 = 0x036; +/// Select the read buffer (inferred). +pub const SELECT_READ_BUFFER: u32 = 0x037; +/// Copy a pixel rectangle within the frame buffer (inferred). +pub const COPY_PIXEL_RECTANGLE_WITHIN_FRAME_BUFFER: u32 = 0x038; +/// Read a pixel rectangle back to the host (inferred). +pub const READ_PIXEL_RECTANGLE_BACK_TO_HOST: u32 = 0x039; +/// Draw a pixel rectangle from host data (inferred). +pub const DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA: u32 = 0x03a; +/// Query current vertex attributes (inferred). +pub const QUERY_CURRENT_VERTEX_ATTRIBUTES: u32 = 0x03b; +/// Query a clip plane (inferred). +pub const QUERY_CLIP_PLANE: u32 = 0x03c; +/// Query the raster position state (inferred). +pub const QUERY_RASTER_POSITION_STATE: u32 = 0x03d; +/// Query the pending error (inferred). +pub const QUERY_PENDING_ERROR: u32 = 0x03e; +/// Query state (float). +pub const QUERY_STATE_FLOAT: u32 = 0x03f; +/// Query state (integer). +pub const QUERY_STATE_INTEGER: u32 = 0x040; +/// Query a light (float) (inferred). +pub const QUERY_LIGHT_FLOAT: u32 = 0x041; +/// Query a light (integer). +pub const QUERY_LIGHT_INTEGER: u32 = 0x042; +/// Query an evaluator map (double). +pub const QUERY_EVALUATOR_MAP_DOUBLE: u32 = 0x043; +/// Query an evaluator map (float). +pub const QUERY_EVALUATOR_MAP_FLOAT: u32 = 0x044; +/// Query an evaluator map (integer). +pub const QUERY_EVALUATOR_MAP_INTEGER: u32 = 0x045; +/// Query a material (float) (inferred). +pub const QUERY_MATERIAL_FLOAT: u32 = 0x046; +/// Query a material (integer). +pub const QUERY_MATERIAL_INTEGER: u32 = 0x047; +/// Query a pixel map (float) (inferred). +pub const QUERY_PIXEL_MAP_FLOAT: u32 = 0x048; +/// Query a pixel map (unsigned 32-bit). +pub const QUERY_PIXEL_MAP_UNSIGNED_32_BIT: u32 = 0x049; +/// Query a pixel map (unsigned 16-bit). +pub const QUERY_PIXEL_MAP_UNSIGNED_16_BIT: u32 = 0x04a; +/// Query the polygon stipple (inferred). +pub const QUERY_POLYGON_STIPPLE: u32 = 0x04b; +/// Query state (generic) (inferred). +pub const QUERY_STATE_GENERIC: u32 = 0x04c; +/// Query texture environment (float) (inferred). +pub const QUERY_TEXTURE_ENVIRONMENT_FLOAT: u32 = 0x04d; +/// Query texture environment (integer). +pub const QUERY_TEXTURE_ENVIRONMENT_INTEGER: u32 = 0x04e; +/// Query texture generation (double). +pub const QUERY_TEXTURE_GENERATION_DOUBLE: u32 = 0x04f; +/// Query texture generation (float) (inferred). +pub const QUERY_TEXTURE_GENERATION_FLOAT: u32 = 0x050; +/// Query texture generation (integer). +pub const QUERY_TEXTURE_GENERATION_INTEGER: u32 = 0x051; +/// Query a texture image (inferred). +pub const QUERY_TEXTURE_IMAGE: u32 = 0x052; +/// Query a texture parameter (float). +pub const QUERY_TEXTURE_PARAMETER_FLOAT: u32 = 0x053; +/// Query a texture parameter (integer). +pub const QUERY_TEXTURE_PARAMETER_INTEGER: u32 = 0x054; +/// Query a texture-level parameter (float). +pub const QUERY_TEXTURE_LEVEL_PARAMETER_FLOAT: u32 = 0x055; +/// Query a texture-level parameter (integer). +pub const QUERY_TEXTURE_LEVEL_PARAMETER_INTEGER: u32 = 0x056; +/// Begin primitive: points. +pub const BEGIN_PRIMITIVE_POINTS: u32 = 0x057; +/// Begin primitive: independent lines. +pub const BEGIN_PRIMITIVE_INDEPENDENT_LINES: u32 = 0x058; +/// Begin primitive: line loop. +pub const BEGIN_PRIMITIVE_LINE_LOOP: u32 = 0x059; +/// Begin primitive: line strip. +pub const BEGIN_PRIMITIVE_LINE_STRIP: u32 = 0x05a; +/// Begin primitive: independent triangles. +pub const BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES: u32 = 0x05b; +/// Begin primitive: triangle strip. +pub const BEGIN_PRIMITIVE_TRIANGLE_STRIP: u32 = 0x05c; +/// Begin primitive: triangle fan. +pub const BEGIN_PRIMITIVE_TRIANGLE_FAN: u32 = 0x05d; +/// Begin primitive: independent quads. +pub const BEGIN_PRIMITIVE_INDEPENDENT_QUADS: u32 = 0x05e; +/// Begin primitive: quad strip. +pub const BEGIN_PRIMITIVE_QUAD_STRIP: u32 = 0x05f; +/// Begin primitive: polygon. +pub const BEGIN_PRIMITIVE_POLYGON: u32 = 0x060; +/// Set current colour, 3 components, signed byte (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_BYTE: u32 = 0x061; +/// Set current colour, 3 components, double (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE: u32 = 0x062; +/// Set current colour, 3 components, single float (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT: u32 = 0x063; +/// Set current colour, 3 components, signed 32-bit integer (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_32_BIT_INTEGER: u32 = 0x064; +/// Set current colour, 3 components, signed short (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_SHORT: u32 = 0x065; +/// Set current colour, 3 components, unsigned byte (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_BYTE: u32 = 0x066; +/// Set current colour, 3 components, unsigned 32-bit integer (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_32_BIT_INTEGER: u32 = 0x067; +/// Set current colour, 3 components, unsigned short (inferred). +pub const SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_SHORT: u32 = 0x068; +/// Set current colour, 4 components, signed byte (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_BYTE: u32 = 0x069; +/// Set current colour, 4 components, double (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE: u32 = 0x06a; +/// Set current colour, 4 components, single float (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT: u32 = 0x06b; +/// Set current colour, 4 components, signed 32-bit integer (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_32_BIT_INTEGER: u32 = 0x06c; +/// Set current colour, 4 components, signed short (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_SHORT: u32 = 0x06d; +/// Set current colour, 4 components, unsigned byte (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_BYTE: u32 = 0x06e; +/// Set current colour, 4 components, unsigned 32-bit integer (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_32_BIT_INTEGER: u32 = 0x06f; +/// Set current colour, 4 components, unsigned short (inferred). +pub const SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_SHORT: u32 = 0x070; +/// End the open primitive. +pub const END_OPEN_PRIMITIVE: u32 = 0x071; +/// Set current colour index, double (inferred). +pub const SET_CURRENT_COLOUR_INDEX_DOUBLE: u32 = 0x072; +/// Set current colour index, single float (inferred). +pub const SET_CURRENT_COLOUR_INDEX_SINGLE_FLOAT: u32 = 0x073; +/// Set current colour index, signed 32-bit integer (inferred). +pub const SET_CURRENT_COLOUR_INDEX_SIGNED_32_BIT_INTEGER: u32 = 0x074; +/// Set current colour index, signed short (inferred). +pub const SET_CURRENT_COLOUR_INDEX_SIGNED_SHORT: u32 = 0x075; +/// Set current normal, 3 components, signed byte (inferred). +pub const SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_BYTE: u32 = 0x076; +/// Set current normal, 3 components, double (inferred). +pub const SET_CURRENT_NORMAL_3_COMPONENTS_DOUBLE: u32 = 0x077; +/// Set current normal, 3 components, single float (inferred). +pub const SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT: u32 = 0x078; +/// Set current normal, 3 components, signed 32-bit integer (inferred). +pub const SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_32_BIT_INTEGER: u32 = 0x079; +/// Set current normal, 3 components, signed short (inferred). +pub const SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_SHORT: u32 = 0x07a; +/// Set current texture coordinate, 1 components, double (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_DOUBLE: u32 = 0x07b; +/// Set current texture coordinate, 1 components, single float (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT: u32 = 0x07c; +/// Set current texture coordinate, 1 components, signed 32-bit integer (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_32_BIT: u32 = 0x07d; +/// Set current texture coordinate, 1 components, signed short (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_SHORT: u32 = 0x07e; +/// Set current texture coordinate, 2 components, double (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_DOUBLE: u32 = 0x07f; +/// Set current texture coordinate, 2 components, single float (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT: u32 = 0x080; +/// Set current texture coordinate, 2 components, signed 32-bit integer (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_32_BIT: u32 = 0x081; +/// Set current texture coordinate, 2 components, signed short (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT: u32 = 0x082; +/// Set current texture coordinate, 3 components, double (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_DOUBLE: u32 = 0x083; +/// Set current texture coordinate, 3 components, single float (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT: u32 = 0x084; +/// Set current texture coordinate, 3 components, signed 32-bit integer (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_32_BIT: u32 = 0x085; +/// Set current texture coordinate, 3 components, signed short (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_SHORT: u32 = 0x086; +/// Set current texture coordinate, 4 components, double (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_DOUBLE: u32 = 0x087; +/// Set current texture coordinate, 4 components, single float (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT: u32 = 0x088; +/// Set current texture coordinate, 4 components, signed 32-bit integer (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_32_BIT: u32 = 0x089; +/// Set current texture coordinate, 4 components, signed short (inferred). +pub const SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_SHORT: u32 = 0x08a; +/// Submit a vertex, 2 components, double (inferred). +pub const SUBMIT_VERTEX_2_COMPONENTS_DOUBLE: u32 = 0x08b; +/// Submit a vertex, 2 components, single float (inferred). +pub const SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT: u32 = 0x08c; +/// Submit a vertex, 2 components, signed 32-bit integer (inferred). +pub const SUBMIT_VERTEX_2_COMPONENTS_SIGNED_32_BIT_INTEGER: u32 = 0x08d; +/// Submit a vertex, 2 components, signed short (inferred). +pub const SUBMIT_VERTEX_2_COMPONENTS_SIGNED_SHORT: u32 = 0x08e; +/// Submit a vertex, 3 components, double (inferred). +pub const SUBMIT_VERTEX_3_COMPONENTS_DOUBLE: u32 = 0x08f; +/// Submit a vertex, 3 components, single float (inferred). +pub const SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT: u32 = 0x090; +/// Submit a vertex, 3 components, signed 32-bit integer (inferred). +pub const SUBMIT_VERTEX_3_COMPONENTS_SIGNED_32_BIT_INTEGER: u32 = 0x091; +/// Submit a vertex, 3 components, signed short (inferred). +pub const SUBMIT_VERTEX_3_COMPONENTS_SIGNED_SHORT: u32 = 0x092; +/// Submit a vertex, 4 components, double (inferred). +pub const SUBMIT_VERTEX_4_COMPONENTS_DOUBLE: u32 = 0x093; +/// Submit a vertex, 4 components, single float (inferred). +pub const SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT: u32 = 0x094; +/// Submit a vertex, 4 components, signed 32-bit integer (inferred). +pub const SUBMIT_VERTEX_4_COMPONENTS_SIGNED_32_BIT_INTEGER: u32 = 0x095; +/// Submit a vertex, 4 components, signed short (inferred). +pub const SUBMIT_VERTEX_4_COMPONENTS_SIGNED_SHORT: u32 = 0x096; +/// Set a material parameter (float form) (inferred). +pub const SET_MATERIAL_PARAMETER_FLOAT_FORM: u32 = 0x097; +/// Continuation entry of the float material parameter (inferred). +pub const CONTINUATION_ENTRY_OF_FLOAT_MATERIAL_PARAMETER: u32 = 0x098; +/// Set a material parameter (integer form) (inferred). +pub const SET_MATERIAL_PARAMETER_INTEGER_FORM: u32 = 0x099; +/// Continuation entry of the integer material parameter (inferred). +pub const CONTINUATION_ENTRY_OF_INTEGER_MATERIAL_PARAMETER: u32 = 0x09a; +/// Evaluator coordinate, 1D, double (inferred). +pub const EVALUATOR_COORDINATE_1D_DOUBLE: u32 = 0x09b; +/// Continuation entry of the evaluator eval coord1d (inferred). +pub const CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD1D: u32 = 0x09c; +/// Evaluator coordinate, 1D, single float (inferred). +pub const EVALUATOR_COORDINATE_1D_SINGLE_FLOAT: u32 = 0x09d; +/// Continuation entry of the evaluator eval coord1f (inferred). +pub const CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD1F: u32 = 0x09e; +/// Evaluator coordinate, 2D, double (inferred). +pub const EVALUATOR_COORDINATE_2D_DOUBLE: u32 = 0x09f; +/// Continuation entry of the evaluator eval coord2d (inferred). +pub const CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD2D: u32 = 0x0a0; +/// Evaluator coordinate, 2D, single float (inferred). +pub const EVALUATOR_COORDINATE_2D_SINGLE_FLOAT: u32 = 0x0a1; +/// Continuation entry of the evaluator eval coord2f (inferred). +pub const CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD2F: u32 = 0x0a2; +/// Evaluator grid point, 1D (inferred). +pub const EVALUATOR_GRID_POINT_1D: u32 = 0x0a3; +/// Continuation entry of the evaluator eval point1 (inferred). +pub const CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_POINT1: u32 = 0x0a4; +/// Evaluator grid point, 2D (inferred). +pub const EVALUATOR_GRID_POINT_2D: u32 = 0x0a5; +/// Continuation entry of the evaluator eval point2 (inferred). +pub const CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_POINT2: u32 = 0x0a6; +/// Set the current edge flag (inferred). +pub const SET_CURRENT_EDGE_FLAG: u32 = 0x0a7; +/// Set the raster position, 2 components, double (inferred). +pub const SET_RASTER_POSITION_2_COMPONENTS_DOUBLE: u32 = 0x0a8; +/// Set the raster position, 2 components, float (inferred). +pub const SET_RASTER_POSITION_2_COMPONENTS_FLOAT: u32 = 0x0a9; +/// Set the raster position, 2 components, 32-bit integer (inferred). +pub const SET_RASTER_POSITION_2_COMPONENTS_32_BIT_INTEGER: u32 = 0x0aa; +/// Set the raster position, 2 components, short (inferred). +pub const SET_RASTER_POSITION_2_COMPONENTS_SHORT: u32 = 0x0ab; +/// Set the raster position, 3 components, double (inferred). +pub const SET_RASTER_POSITION_3_COMPONENTS_DOUBLE: u32 = 0x0ac; +/// Set the raster position, 3 components, float (inferred). +pub const SET_RASTER_POSITION_3_COMPONENTS_FLOAT: u32 = 0x0ad; +/// Set the raster position, 3 components, 32-bit integer (inferred). +pub const SET_RASTER_POSITION_3_COMPONENTS_32_BIT_INTEGER: u32 = 0x0ae; +/// Set the raster position, 3 components, short (inferred). +pub const SET_RASTER_POSITION_3_COMPONENTS_SHORT: u32 = 0x0af; +/// Set the raster position, 4 components, double (inferred). +pub const SET_RASTER_POSITION_4_COMPONENTS_DOUBLE: u32 = 0x0b0; +/// Set the raster position, 4 components, float (inferred). +pub const SET_RASTER_POSITION_4_COMPONENTS_FLOAT: u32 = 0x0b1; +/// Set the raster position, 4 components, 32-bit integer (inferred). +pub const SET_RASTER_POSITION_4_COMPONENTS_32_BIT_INTEGER: u32 = 0x0b2; +/// Set the raster position, 4 components, short (inferred). +pub const SET_RASTER_POSITION_4_COMPONENTS_SHORT: u32 = 0x0b3; +/// Set a user clip plane (inferred). +pub const SET_USER_CLIP_PLANE: u32 = 0x0b4; +/// Set colour-material tracking (inferred). +pub const SET_COLOUR_MATERIAL_TRACKING: u32 = 0x0b5; +/// Set face culling (inferred). +pub const SET_FACE_CULLING: u32 = 0x0b6; +/// Set a fog parameter (float form). +pub const SET_FOG_PARAMETER_FLOAT_FORM: u32 = 0x0b7; +/// Set a fog parameter (integer form) (inferred). +pub const SET_FOG_PARAMETER_INTEGER_FORM: u32 = 0x0b8; +/// Continuation entry of the integer fog parameter (inferred). +pub const CONTINUATION_ENTRY_OF_INTEGER_FOG_PARAMETER: u32 = 0x0b9; +/// Set front-face winding (inferred). +pub const SET_FRONT_FACE_WINDING: u32 = 0x0ba; +/// Set a quality hint (inferred). +pub const SET_QUALITY_HINT: u32 = 0x0bb; +/// Set the line stipple (inferred). +pub const SET_LINE_STIPPLE: u32 = 0x0bc; +/// Set the line width (inferred). +pub const SET_LINE_WIDTH: u32 = 0x0bd; +/// Set the point size (inferred). +pub const SET_POINT_SIZE: u32 = 0x0be; +/// Set the polygon fill mode (inferred). +pub const SET_POLYGON_FILL_MODE: u32 = 0x0bf; +/// Set flat or smooth shading (inferred). +pub const SET_FLAT_OR_SMOOTH_SHADING: u32 = 0x0c0; +/// Insert a feedback pass-through marker (inferred). +pub const INSERT_FEEDBACK_PASS_THROUGH_MARKER: u32 = 0x0c1; +/// Pop a selection name (inferred). +pub const POP_SELECTION_NAME: u32 = 0x0c2; +/// Push a selection name (inferred). +pub const PUSH_SELECTION_NAME: u32 = 0x0c3; +/// Select the draw buffer (inferred). +pub const SELECT_DRAW_BUFFER: u32 = 0x0c4; +/// Accumulation-buffer operation (inferred). +pub const ACCUMULATION_BUFFER_OPERATION: u32 = 0x0c5; +/// Finish: drain the pipe and signal completion (inferred). +pub const FINISH_DRAIN_PIPE_AND_SIGNAL_COMPLETION: u32 = 0x0c6; +/// Flush the vertex pipeline (inferred). +pub const FLUSH_VERTEX_PIPELINE: u32 = 0x0c7; +/// Pop the attribute stack (inferred). +pub const POP_ATTRIBUTE_STACK: u32 = 0x0c8; +/// Push the attribute stack (inferred). +pub const PUSH_ATTRIBUTE_STACK: u32 = 0x0c9; +/// Set the alpha test (inferred). +pub const SET_ALPHA_TEST: u32 = 0x0ca; +/// Set the blend factors (inferred). +pub const SET_BLEND_FACTORS: u32 = 0x0cb; +/// Set the stencil test (inferred). +pub const SET_STENCIL_TEST: u32 = 0x0cc; +/// Set the stencil operations (inferred). +pub const SET_STENCIL_OPERATIONS: u32 = 0x0cd; +/// Set the depth test (inferred). +pub const SET_DEPTH_TEST: u32 = 0x0ce; +/// Set the depth range (inferred). +pub const SET_DEPTH_RANGE: u32 = 0x0cf; +/// Select the current matrix stack (inferred). +pub const SELECT_CURRENT_MATRIX_STACK: u32 = 0x0d0; +/// Multiply the current matrix by a frustum. +pub const MULTIPLY_CURRENT_MATRIX_BY_FRUSTUM: u32 = 0x0d1; +/// Load the identity matrix. +pub const LOAD_IDENTITY_MATRIX: u32 = 0x0d2; +/// Load a matrix (16 floats). +pub const LOAD_MATRIX_16_FLOATS: u32 = 0x0d3; +/// Load a matrix (16 doubles). +pub const LOAD_MATRIX_16_DOUBLES: u32 = 0x0d4; +/// Multiply by a matrix (16 floats). +pub const MULTIPLY_BY_MATRIX_16_FLOATS: u32 = 0x0d5; +/// Multiply by a matrix (16 doubles). +pub const MULTIPLY_BY_MATRIX_16_DOUBLES: u32 = 0x0d6; +/// Multiply the current matrix by an orthographic projection. +pub const MULTIPLY_CURRENT_MATRIX_BY_ORTHOGRAPHIC_PROJECTION: u32 = 0x0d7; +/// Pop the matrix stack. +pub const POP_MATRIX_STACK: u32 = 0x0d8; +/// Push the matrix stack. +pub const PUSH_MATRIX_STACK: u32 = 0x0d9; +/// Rotate (doubles). +pub const ROTATE_DOUBLES: u32 = 0x0da; +/// Rotate (floats). +pub const ROTATE_FLOATS: u32 = 0x0db; +/// Scale (doubles). +pub const SCALE_DOUBLES: u32 = 0x0dc; +/// Scale (floats). +pub const SCALE_FLOATS: u32 = 0x0dd; +/// Translate (doubles). +pub const TRANSLATE_DOUBLES: u32 = 0x0de; +/// Translate (floats). +pub const TRANSLATE_FLOATS: u32 = 0x0df; +/// Copy engine work memory to control memory (no handler bound) (inferred). +pub const COPY_ENGINE_WORK_MEMORY_TO_CONTROL_MEMORY: u32 = 0x0e0; +/// Pass one command word through (null handler) (inferred). +pub const PASS_ONE_COMMAND_WORD_THROUGH: u32 = 0x0e1; +/// Write one raster-bus word (mode code, low half, high half). +pub const WRITE_ONE_RASTER_BUS_WORD: u32 = 0x0e2; +/// Set the viewport (inferred). +pub const SET_VIEWPORT: u32 = 0x0e3; +/// Record an error code (inferred). +pub const RECORD_ERROR_CODE: u32 = 0x0e4; +/// End-of-simulation report (simulation only) (inferred). +pub const END_OF_SIMULATION_REPORT: u32 = 0x0e5; +/// Set the constant blend colour (inferred). +pub const SET_CONSTANT_BLEND_COLOUR: u32 = 0x0e6; +/// Set the blend equation (unknown). +pub const SET_BLEND_EQUATION: u32 = 0x0e7; +/// Load a colour table (inferred). +pub const LOAD_COLOUR_TABLE: u32 = 0x0e8; +/// Copy a colour table from the frame buffer. +pub const COPY_COLOUR_TABLE_FROM_FRAME_BUFFER: u32 = 0x0e9; +/// Query a colour table (inferred). +pub const QUERY_COLOUR_TABLE: u32 = 0x0ea; +/// Set colour-table parameters (inferred). +pub const SET_COLOUR_TABLE_PARAMETERS: u32 = 0x0eb; +/// Query colour-table parameters (inferred). +pub const QUERY_COLOUR_TABLE_PARAMETERS: u32 = 0x0ec; +/// Load the post-texture scale and bias. +pub const LOAD_POST_TEXTURE_SCALE_AND_BIAS: u32 = 0x0ed; +/// Query the line-mode limit table (inferred). +pub const QUERY_LINE_MODE_LIMIT_TABLE: u32 = 0x0ee; +/// Load a 1D convolution filter (inferred). +pub const LOAD_1D_CONVOLUTION_FILTER: u32 = 0x0ef; +/// Load a 2D convolution filter (inferred). +pub const LOAD_2D_CONVOLUTION_FILTER: u32 = 0x0f0; +/// 3D convolution filter. +pub const T_3D_CONVOLUTION_FILTER: u32 = 0x0f1; +/// Copy a 1D convolution filter from the frame buffer (inferred). +pub const COPY_1D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER: u32 = 0x0f2; +/// Copy a 2D convolution filter from the frame buffer (inferred). +pub const COPY_2D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER: u32 = 0x0f3; +/// Query a convolution filter (inferred). +pub const QUERY_CONVOLUTION_FILTER: u32 = 0x0f4; +/// Load a separable 2D filter (inferred). +pub const LOAD_SEPARABLE_2D_FILTER: u32 = 0x0f5; +/// 3D separable filter. +pub const T_3D_SEPARABLE_FILTER: u32 = 0x0f6; +/// Query a separable filter (inferred). +pub const QUERY_SEPARABLE_FILTER: u32 = 0x0f7; +/// Set pixel-transform parameters (integer) (inferred). +pub const SET_PIXEL_TRANSFORM_PARAMETERS_INTEGER: u32 = 0x0f8; +/// Set pixel-transform parameters (float) (inferred). +pub const SET_PIXEL_TRANSFORM_PARAMETERS_FLOAT: u32 = 0x0f9; +/// Query pixel-transform parameters (integer) (inferred). +pub const QUERY_PIXEL_TRANSFORM_PARAMETERS_INTEGER: u32 = 0x0fa; +/// Query pixel-transform parameters (float). +pub const QUERY_PIXEL_TRANSFORM_PARAMETERS_FLOAT: u32 = 0x0fb; +/// Copy a 1D texture image from the frame buffer. +pub const COPY_1D_TEXTURE_IMAGE_FROM_FRAME_BUFFER: u32 = 0x0fc; +/// Copy a 2D texture image from the frame buffer (inferred). +pub const COPY_2D_TEXTURE_IMAGE_FROM_FRAME_BUFFER: u32 = 0x0fd; +/// Copy a 1D texture sub-image from the frame buffer. +pub const COPY_1D_TEXTURE_SUB_IMAGE_FROM_FRAME_BUFFER: u32 = 0x0fe; +/// Write-gather flush, first slot. +pub const WRITE_GATHER_FLUSH_FIRST_SLOT: u32 = 0x0ff; +/// Raw data word, second form. +pub const RAW_DATA_WORD_SECOND_FORM: u32 = 0x100; +/// Copy a 2D texture sub-image from the frame buffer. +pub const COPY_2D_TEXTURE_SUB_IMAGE_FROM_FRAME_BUFFER: u32 = 0x101; +/// Set up a histogram (inferred). +pub const SET_UP_HISTOGRAM: u32 = 0x102; +/// Reset a histogram (inferred). +pub const RESET_HISTOGRAM: u32 = 0x103; +/// Query a histogram (inferred). +pub const QUERY_HISTOGRAM: u32 = 0x104; +/// Query histogram parameters (inferred). +pub const QUERY_HISTOGRAM_PARAMETERS: u32 = 0x105; +/// Bind a linear texture (inferred). +pub const BIND_LINEAR_TEXTURE: u32 = 0x106; +/// Set up min/max tracking (inferred). +pub const SET_UP_MIN_MAX_TRACKING: u32 = 0x107; +/// Reset min/max tracking (inferred). +pub const RESET_MIN_MAX_TRACKING: u32 = 0x108; +/// Query min/max results (inferred). +pub const QUERY_MIN_MAX_RESULTS: u32 = 0x109; +/// Query min/max parameters (inferred). +pub const QUERY_MIN_MAX_PARAMETERS: u32 = 0x10a; +/// Save light attributes on the engine attribute stack (ARM-injected) (inferred). +pub const SAVE_LIGHT_ATTRIBUTES_ON_ENGINE_ATTRIBUTE_STACK: u32 = 0x10b; +/// Clear the selection name stack (inferred). +pub const CLEAR_SELECTION_NAME_STACK: u32 = 0x10c; +/// Replace the top selection name (inferred). +pub const REPLACE_TOP_SELECTION_NAME: u32 = 0x10d; +/// Set the current raster position (fifteen words) (inferred). +pub const SET_CURRENT_RASTER_POSITION: u32 = 0x10e; +/// Set the fog function table (inferred). +pub const SET_FOG_FUNCTION_TABLE: u32 = 0x10f; +/// Ask whether a copy source overlaps its destination (inferred). +pub const ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION: u32 = 0x110; +/// Suspend pixel-mode state around a transfer (inferred). +pub const SUSPEND_PIXEL_MODE_STATE_AROUND_TRANSFER: u32 = 0x111; +/// Switch the legacy-API compatibility mode (inferred). +pub const SWITCH_LEGACY_API_COMPATIBILITY_MODE: u32 = 0x112; +/// Set the colour-material value (inferred). +pub const SET_COLOUR_MATERIAL_VALUE: u32 = 0x113; +/// Select the video source (inferred). +pub const SELECT_VIDEO_SOURCE: u32 = 0x114; +/// Ask whether a convolution copy overlaps (inferred). +pub const ASK_WHETHER_CONVOLUTION_COPY_OVERLAPS: u32 = 0x115; +/// Resolve whether a pixel format is single-component (inferred). +pub const RESOLVE_WHETHER_PIXEL_FORMAT_IS_SINGLE_COMPONENT: u32 = 0x116; +/// Set the multisample pattern (inferred). +pub const SET_MULTISAMPLE_PATTERN: u32 = 0x117; +/// Set the multisample coverage mask (inferred). +pub const SET_MULTISAMPLE_COVERAGE_MASK: u32 = 0x118; +/// Pop saved light state (inferred). +pub const POP_SAVED_LIGHT_STATE: u32 = 0x119; +/// Set pixel texture generation. +pub const SET_PIXEL_TEXTURE_GENERATION: u32 = 0x11a; +/// Set polygon offset (extension form) (inferred). +pub const SET_POLYGON_OFFSET_EXTENSION_FORM: u32 = 0x11b; +/// Load a 1D texture sub-image. +pub const LOAD_1D_TEXTURE_SUB_IMAGE: u32 = 0x11c; +/// Load a 2D texture sub-image. +pub const LOAD_2D_TEXTURE_SUB_IMAGE: u32 = 0x11d; +/// Load a 3D texture sub-image. +pub const LOAD_3D_TEXTURE_SUB_IMAGE: u32 = 0x11e; +/// Load a 3D texture image. +pub const LOAD_3D_TEXTURE_IMAGE: u32 = 0x11f; +/// 4D texture image. +pub const T_4D_TEXTURE_IMAGE: u32 = 0x120; +/// 4D texture sub-image. +pub const T_4D_TEXTURE_SUB_IMAGE: u32 = 0x121; +/// Allocate 1D texture names. +pub const ALLOCATE_1D_TEXTURE_NAMES: u32 = 0x122; +/// Allocate 2D texture names. +pub const ALLOCATE_2D_TEXTURE_NAMES: u32 = 0x123; +/// Allocate 3D texture names. +pub const ALLOCATE_3D_TEXTURE_NAMES: u32 = 0x124; +/// Query whether a name is a texture. +pub const QUERY_WHETHER_NAME_IS_TEXTURE: u32 = 0x125; +/// Delete texture names. +pub const DELETE_TEXTURE_NAMES: u32 = 0x126; +/// Bind a texture name. +pub const BIND_TEXTURE_NAME: u32 = 0x127; +/// Window-system context initialise (screen size and depth) (inferred). +pub const WINDOW_SYSTEM_CONTEXT_INITIALISE: u32 = 0x128; +/// Window-system graphics-context load (13 words). +pub const WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD: u32 = 0x129; +/// Window-system drawable origin and size load. +pub const WINDOW_SYSTEM_DRAWABLE_ORIGIN_AND_SIZE_LOAD: u32 = 0x12a; +/// Window-system clip load. +pub const WINDOW_SYSTEM_CLIP_LOAD: u32 = 0x12b; +/// Window-system clip list load (ARM-run form) (inferred). +pub const WINDOW_SYSTEM_CLIP_LIST_LOAD: u32 = 0x12c; +/// Window-system scissor load. +pub const WINDOW_SYSTEM_SCISSOR_LOAD: u32 = 0x12d; +/// Load colour-map entries (inferred). +pub const LOAD_COLOUR_MAP_ENTRIES: u32 = 0x12e; +/// Load gamma-table entries (inferred). +pub const LOAD_GAMMA_TABLE_ENTRIES: u32 = 0x12f; +/// Load a cursor image block (inferred). +pub const LOAD_CURSOR_IMAGE_BLOCK: u32 = 0x130; +/// Load the cursor colour words (inferred). +pub const LOAD_CURSOR_COLOUR_WORDS: u32 = 0x131; +/// Load one entry of the display-id table (inferred). +pub const LOAD_ONE_ENTRY_OF_DISPLAY_ID_TABLE: u32 = 0x132; +/// Window created or destroyed notice to the colour-map chip (inferred). +pub const WINDOW_CREATED_OR_DESTROYED_NOTICE_TO_COLOUR_MAP_CHIP: u32 = 0x133; +/// Query engine-side texture attributes (inferred). +pub const QUERY_ENGINE_SIDE_TEXTURE_ATTRIBUTES: u32 = 0x134; +/// Set the pixel-transform mode (inferred). +pub const SET_PIXEL_TRANSFORM_MODE: u32 = 0x135; +/// Window-system pixel path set-up (inferred). +pub const WINDOW_SYSTEM_PIXEL_PATH_SET_UP: u32 = 0x136; +/// Window-system pixel copy (inferred). +pub const WINDOW_SYSTEM_PIXEL_COPY: u32 = 0x137; +/// Window-system pixel read (inferred). +pub const WINDOW_SYSTEM_PIXEL_READ: u32 = 0x138; +/// Window-system pixel write (inferred). +pub const WINDOW_SYSTEM_PIXEL_WRITE: u32 = 0x139; +/// Window-system pixel path finish (inferred). +pub const WINDOW_SYSTEM_PIXEL_PATH_FINISH: u32 = 0x13a; +/// Unused window-system slot 3. +pub const UNUSED_WINDOW_SYSTEM_SLOT_3: u32 = 0x13b; +/// Load a window-system stipple (inferred). +pub const LOAD_WINDOW_SYSTEM_STIPPLE: u32 = 0x13c; +/// Store a window-system stipple (inferred). +pub const STORE_WINDOW_SYSTEM_STIPPLE: u32 = 0x13d; +/// Rotate a window-system stipple (inferred). +pub const ROTATE_WINDOW_SYSTEM_STIPPLE: u32 = 0x13e; +/// Unused window-system slot 4. +pub const UNUSED_WINDOW_SYSTEM_SLOT_4: u32 = 0x13f; +/// Unused window-system slot 5. +pub const UNUSED_WINDOW_SYSTEM_SLOT_5: u32 = 0x140; +/// Window-system 1-word data preload (inferred). +pub const WINDOW_SYSTEM_1_WORD_DATA_PRELOAD: u32 = 0x141; +/// Window-system 2-word data preload (inferred). +pub const WINDOW_SYSTEM_2_WORD_DATA_PRELOAD: u32 = 0x142; +/// Window-system 3-word data preload (inferred). +pub const WINDOW_SYSTEM_3_WORD_DATA_PRELOAD: u32 = 0x143; +/// Window-system 4-word data preload (inferred). +pub const WINDOW_SYSTEM_4_WORD_DATA_PRELOAD: u32 = 0x144; +/// Window-system four-word data preload, byte form (inferred). +pub const WINDOW_SYSTEM_FOUR_WORD_DATA_PRELOAD_BYTE_FORM: u32 = 0x145; +/// Window-system 5-word data preload (inferred). +pub const WINDOW_SYSTEM_5_WORD_DATA_PRELOAD: u32 = 0x146; +/// Window-system 6-word data preload (inferred). +pub const WINDOW_SYSTEM_6_WORD_DATA_PRELOAD: u32 = 0x147; +/// Window-system 7-word data preload (inferred). +pub const WINDOW_SYSTEM_7_WORD_DATA_PRELOAD: u32 = 0x148; +/// Window-system 8-word data preload (inferred). +pub const WINDOW_SYSTEM_8_WORD_DATA_PRELOAD: u32 = 0x149; +/// Window-system 9-word data preload (inferred). +pub const WINDOW_SYSTEM_9_WORD_DATA_PRELOAD: u32 = 0x14a; +/// Window-system 10-word data preload (inferred). +pub const WINDOW_SYSTEM_10_WORD_DATA_PRELOAD: u32 = 0x14b; +/// Draw absolute points. +pub const DRAW_ABSOLUTE_POINTS: u32 = 0x14c; +/// Draw absolute points (burst of 6). +pub const DRAW_ABSOLUTE_POINTS_BURST_OF_6: u32 = 0x14d; +/// Draw absolute points (burst of 24). +pub const DRAW_ABSOLUTE_POINTS_BURST_OF_24: u32 = 0x14e; +/// Draw relative points. +pub const DRAW_RELATIVE_POINTS: u32 = 0x14f; +/// Draw relative points (burst of 6). +pub const DRAW_RELATIVE_POINTS_BURST_OF_6: u32 = 0x150; +/// Draw relative points (burst of 24). +pub const DRAW_RELATIVE_POINTS_BURST_OF_24: u32 = 0x151; +/// Fill rectangles (origin, width, height). +pub const FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT: u32 = 0x152; +/// Fill rectangles (origin, width, height) (burst of 6). +pub const FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT_BURST_OF_6: u32 = 0x153; +/// Fill boxes (two corners). +pub const FILL_BOXES_TWO_CORNERS: u32 = 0x154; +/// Fill boxes (two corners) (burst of 6). +pub const FILL_BOXES_TWO_CORNERS_BURST_OF_6: u32 = 0x155; +/// Fill spans. +pub const FILL_SPANS: u32 = 0x156; +/// Fill spans (burst of 2). +pub const FILL_SPANS_BURST_OF_2: u32 = 0x157; +/// Fill spans (burst of 6). +pub const FILL_SPANS_BURST_OF_6: u32 = 0x158; +/// Draw line segments. +pub const DRAW_LINE_SEGMENTS: u32 = 0x159; +/// Draw line segments (burst of 6) (inferred). +pub const DRAW_LINE_SEGMENTS_BURST_OF_6: u32 = 0x15a; +/// Draw connected polyline. +pub const DRAW_CONNECTED_POLYLINE: u32 = 0x15b; +/// Draw connected polyline (burst of 6). +pub const DRAW_CONNECTED_POLYLINE_BURST_OF_6: u32 = 0x15c; +/// Draw connected polyline, relative points. +pub const DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS: u32 = 0x15d; +/// Draw connected polyline, relative points (burst of 6). +pub const DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS_BURST_OF_6: u32 = 0x15e; +/// Draw points in the background colour. +pub const DRAW_POINTS_IN_BACKGROUND_COLOUR: u32 = 0x15f; +/// Draw points in the background colour (burst of 6). +pub const DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_6: u32 = 0x160; +/// Draw points in the background colour (burst of 24). +pub const DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_24: u32 = 0x161; +/// Unused window-system slot 6. +pub const UNUSED_WINDOW_SYSTEM_SLOT_6: u32 = 0x162; +/// Unused window-system slot 7. +pub const UNUSED_WINDOW_SYSTEM_SLOT_7: u32 = 0x163; +/// Unused window-system slot 8. +pub const UNUSED_WINDOW_SYSTEM_SLOT_8: u32 = 0x164; +/// Unused window-system slot 9. +pub const UNUSED_WINDOW_SYSTEM_SLOT_9: u32 = 0x165; +/// Swap request for a multi-pipe display option (inferred). +pub const SWAP_REQUEST_FOR_MULTI_PIPE_DISPLAY_OPTION: u32 = 0x166; +/// Copy a user list by list DMA (inferred). +pub const COPY_USER_LIST_BY_LIST_DMA: u32 = 0x167; +/// Set polygon offset (inferred). +pub const SET_POLYGON_OFFSET: u32 = 0x168; +/// Diagnostic sub-command dispatch (inferred). +pub const DIAGNOSTIC_SUB_COMMAND_DISPATCH: u32 = 0x169; +/// Reserved diagnostic token 0 (inferred). +pub const RESERVED_DIAGNOSTIC_TOKEN_0: u32 = 0x16a; +/// Reserved diagnostic token 1 (inferred). +pub const RESERVED_DIAGNOSTIC_TOKEN_1: u32 = 0x16b; +/// Copy a list by list DMA (inferred). +pub const COPY_LIST_BY_LIST_DMA: u32 = 0x16c; +/// Execute a list by list DMA (inferred). +pub const EXECUTE_LIST_BY_LIST_DMA: u32 = 0x16d; +/// Execute a list by input DMA (inferred). +pub const EXECUTE_LIST_BY_INPUT_DMA: u32 = 0x16e; +/// Move a list by list DMA (inferred). +pub const MOVE_LIST_BY_LIST_DMA: u32 = 0x16f; +/// Execute a list by list DMA, repeated (inferred). +pub const EXECUTE_LIST_BY_LIST_DMA_REPEATED: u32 = 0x170; +/// Execute a list by input DMA, repeated (inferred). +pub const EXECUTE_LIST_BY_INPUT_DMA_REPEATED: u32 = 0x171; +/// Distributor no-op padding word (inferred). +pub const DISTRIBUTOR_NO_OP_PADDING_WORD: u32 = 0x172; +/// Texture-load set-up (image size and format) (inferred). +pub const TEXTURE_LOAD_SET_UP: u32 = 0x173; +/// Draw texels into texture memory (inferred). +pub const DRAW_TEXELS_INTO_TEXTURE_MEMORY: u32 = 0x174; +/// Texture-load set-up, second form (unknown). +pub const TEXTURE_LOAD_SET_UP_SECOND_FORM: u32 = 0x175; +/// Texture-load by identifier (inferred). +pub const TEXTURE_LOAD_BY_IDENTIFIER: u32 = 0x176; +/// Select the active texture unit (inferred). +pub const SELECT_ACTIVE_TEXTURE_UNIT: u32 = 0x177; +/// Bind a texture by identifier (inferred). +pub const BIND_TEXTURE_BY_IDENTIFIER: u32 = 0x178; +/// Bind a 2D mip-mapped texture (22 words). +pub const BIND_2D_MIP_MAPPED_TEXTURE: u32 = 0x179; +/// Bind a bicubic texture (inferred). +pub const BIND_BICUBIC_TEXTURE: u32 = 0x17a; +/// Bind a 3D mip-mapped texture (inferred). +pub const BIND_3D_MIP_MAPPED_TEXTURE: u32 = 0x17b; +/// Bind a 3D texture (inferred). +pub const BIND_3D_TEXTURE: u32 = 0x17c; +/// Bind a detail texture (inferred). +pub const BIND_DETAIL_TEXTURE: u32 = 0x17d; +/// Load one entry of the ARM texture table (33 words) (inferred). +pub const LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE: u32 = 0x17e; +/// Multi-purpose diagnostic entry (inferred). +pub const MULTI_PURPOSE_DIAGNOSTIC_ENTRY: u32 = 0x17f; +/// Set the vertex-array command table (inferred). +pub const SET_VERTEX_ARRAY_COMMAND_TABLE: u32 = 0x180; +/// Vertex-array set-up (inferred). +pub const VERTEX_ARRAY_SET_UP: u32 = 0x181; +/// Begin a vertex-array loop (inferred). +pub const BEGIN_VERTEX_ARRAY_LOOP: u32 = 0x182; +/// Vertex-array loop entry 0 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_0: u32 = 0x183; +/// Vertex-array loop entry 1 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_1: u32 = 0x184; +/// Vertex-array loop entry 2 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_2: u32 = 0x185; +/// Vertex-array loop entry 3 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_3: u32 = 0x186; +/// Vertex-array loop entry 4 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_4: u32 = 0x187; +/// Vertex-array loop entry 5 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_5: u32 = 0x188; +/// Vertex-array loop entry 6 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_6: u32 = 0x189; +/// Vertex-array loop entry 7 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_7: u32 = 0x18a; +/// Vertex-array loop entry 8 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_8: u32 = 0x18b; +/// Vertex-array loop entry 9 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_9: u32 = 0x18c; +/// Vertex-array loop entry 10 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_10: u32 = 0x18d; +/// Vertex-array loop entry 11 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_11: u32 = 0x18e; +/// Vertex-array loop entry 12 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_12: u32 = 0x18f; +/// Vertex-array loop entry 13 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_13: u32 = 0x190; +/// Vertex-array loop entry 14 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_14: u32 = 0x191; +/// Vertex-array loop entry 15 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_15: u32 = 0x192; +/// Vertex-array loop entry 16 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_16: u32 = 0x193; +/// Vertex-array loop entry 17 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_17: u32 = 0x194; +/// Vertex-array loop entry 18 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_18: u32 = 0x195; +/// Vertex-array loop entry 19 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_19: u32 = 0x196; +/// Vertex-array loop entry 20 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_20: u32 = 0x197; +/// Vertex-array loop entry 21 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_21: u32 = 0x198; +/// Vertex-array loop entry 22 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_22: u32 = 0x199; +/// Vertex-array loop entry 23 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_23: u32 = 0x19a; +/// Vertex-array loop entry 24 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_24: u32 = 0x19b; +/// Vertex-array loop entry 25 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_25: u32 = 0x19c; +/// Vertex-array loop entry 26 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_26: u32 = 0x19d; +/// Vertex-array loop entry 27 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_27: u32 = 0x19e; +/// Vertex-array loop entry 28 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_28: u32 = 0x19f; +/// Vertex-array loop entry 29 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_29: u32 = 0x1a0; +/// Vertex-array loop entry 30 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_30: u32 = 0x1a1; +/// Vertex-array loop entry 31 (inferred). +pub const VERTEX_ARRAY_LOOP_ENTRY_31: u32 = 0x1a2; +/// Vertex-array tear-down (inferred). +pub const VERTEX_ARRAY_TEAR_DOWN: u32 = 0x1a3; +/// Set a sprite parameter (integer) (inferred). +pub const SET_SPRITE_PARAMETER: u32 = 0x1a4; +/// Set sprite parameters (float vector) (inferred). +pub const SET_SPRITE_PARAMETERS: u32 = 0x1a5; +/// Display-generator request carrier (number, payload, checksum) (inferred). +pub const DISPLAY_GENERATOR_REQUEST_CARRIER: u32 = 0x1a6; +/// Set a point parameter (float) (inferred). +pub const SET_POINT_PARAMETER: u32 = 0x1a7; +/// Set point parameters (float vector) (inferred). +pub const SET_POINT_PARAMETERS: u32 = 0x1a8; +/// Swap triangle-mesh vertex (begin/end grammar) (inferred). +pub const SWAP_TRIANGLE_MESH_VERTEX: u32 = 0x1a9; +/// Bind a 2D clip-map texture (inferred). +pub const BIND_2D_CLIP_MAP_TEXTURE: u32 = 0x1aa; +/// Packed vertex: position (3 floats) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS: u32 = 0x1ab; +/// Packed vertex: position (3 floats), colour (4 unsigned bytes in one word) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AC: u32 = 0x1ac; +/// Packed vertex: position (3 floats), normal (3 signed shorts packed) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED: u32 = 0x1ad; +/// Packed vertex: position (3 floats), colour (4 unsigned bytes in one word), normal (3 signed shorts packed) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AE: u32 = 0x1ae; +/// Packed vertex: position (3 floats), texture coordinate (2 floats) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_FLOATS: u32 = 0x1af; +/// Packed vertex: position (3 floats), colour (4 unsigned bytes in one word), texture coordinate (2 floats) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B0: u32 = 0x1b0; +/// Packed vertex: position (3 floats), normal (3 signed shorts packed), texture coordinate (2 floats) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B1: u32 = 0x1b1; +/// Packed vertex: position (3 floats), colour (4 unsigned bytes in one word), normal (3 signed shorts packed), texture coordinate (2 floats) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B2: u32 = 0x1b2; +/// Packed vertex: position (3 floats), texture coordinate (2 signed shorts in one word) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_SIGNED_SHORTS_IN: u32 = 0x1b3; +/// Packed vertex: position (3 floats), colour (4 unsigned bytes in one word), texture coordinate (2 signed shorts in one word) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B4: u32 = 0x1b4; +/// Packed vertex: position (3 floats), normal (3 signed shorts packed), texture coordinate (2 signed shorts in one word) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B5: u32 = 0x1b5; +/// Packed vertex: position (3 floats), colour (4 unsigned bytes in one word), normal (3 signed shorts packed), texture coordinate (2 signed shorts in one word) (inferred). +pub const PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B6: u32 = 0x1b6; +/// Packed-vertex set-up (inferred). +pub const PACKED_VERTEX_SET_UP: u32 = 0x1b7; +/// Set the instrumentation buffer (inferred). +pub const SET_INSTRUMENTATION_BUFFER: u32 = 0x1b8; +/// Start instrumentation (inferred). +pub const START_INSTRUMENTATION: u32 = 0x1b9; +/// Stop instrumentation (inferred). +pub const STOP_INSTRUMENTATION: u32 = 0x1ba; +/// Set a texel-store parameter (inferred). +pub const SET_TEXEL_STORE_PARAMETER: u32 = 0x1bb; +/// Restart select or feedback (inferred). +pub const RESTART_SELECT_OR_FEEDBACK: u32 = 0x1bc; +/// Set the reference plane. +pub const SET_REFERENCE_PLANE: u32 = 0x1bd; +/// Set colour/normal mode value (distributor register 0x293) (inferred). +pub const SET_COLOUR_NORMAL_MODE_VALUE: u32 = 0x1be; +/// Deformation control (inferred). +pub const DEFORMATION_CONTROL: u32 = 0x1bf; +/// Deformation map, float form (inferred). +pub const DEFORMATION_MAP_FLOAT_FORM: u32 = 0x1c0; +/// Deformation map, double form (inferred). +pub const DEFORMATION_MAP_DOUBLE_FORM: u32 = 0x1c1; +/// Continuation entry of the double deformation map (inferred). +pub const CONTINUATION_ENTRY_OF_DOUBLE_DEFORMATION_MAP: u32 = 0x1c2; +/// Load identity deformation (inferred). +pub const LOAD_IDENTITY_DEFORMATION: u32 = 0x1c3; +/// Bind an ARM texture-cache entry (inferred). +pub const BIND_ARM_TEXTURE_CACHE_ENTRY: u32 = 0x1c4; +/// Free an ARM texture-cache entry (inferred). +pub const FREE_ARM_TEXTURE_CACHE_ENTRY: u32 = 0x1c5; +/// Query the deformation state (inferred). +pub const QUERY_DEFORMATION_STATE: u32 = 0x1c6; +/// Set shadow-texture state (inferred). +pub const SET_SHADOW_TEXTURE_STATE: u32 = 0x1c7; +/// Copy a list by input DMA then list DMA (inferred). +pub const COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA: u32 = 0x1c8; +/// Set triangle-mesh mode (inferred). +pub const SET_TRIANGLE_MESH_MODE: u32 = 0x1c9; +/// Load engine transform attributes (inferred). +pub const LOAD_ENGINE_TRANSFORM_ATTRIBUTES: u32 = 0x1ca; +/// Query engine transform attributes (inferred). +pub const QUERY_ENGINE_TRANSFORM_ATTRIBUTES: u32 = 0x1cb; +/// Flush the raster boards (no completion reply) (inferred). +pub const FLUSH_RASTER_BOARDS: u32 = 0x1cc; +/// Read instrumentation counters (inferred). +pub const READ_INSTRUMENTATION_COUNTERS: u32 = 0x1cd; +/// Set lazy-validation mode (inferred). +pub const SET_LAZY_VALIDATION_MODE: u32 = 0x1ce; +/// Packed-vertex clean-up (inferred). +pub const PACKED_VERTEX_CLEAN_UP: u32 = 0x1cf; +/// Subdivision patch (inferred). +pub const SUBDIVISION_PATCH: u32 = 0x1d0; +/// Subdivision patch set-up (inferred). +pub const SUBDIVISION_PATCH_SET_UP: u32 = 0x1d1; +/// Subdivision patch clean-up (inferred). +pub const SUBDIVISION_PATCH_CLEAN_UP: u32 = 0x1d2; +/// Subdivision patch parameter (inferred). +pub const SUBDIVISION_PATCH_PARAMETER: u32 = 0x1d3; +/// Begin primitive: subdivision patch (inferred). +pub const BEGIN_PRIMITIVE_SUBDIVISION_PATCH: u32 = 0x1d4; +/// Bind a texture, current form (inferred). +pub const BIND_TEXTURE_CURRENT_FORM: u32 = 0x1d5; +/// Pop texture-generation state (inferred). +pub const POP_TEXTURE_GENERATION_STATE: u32 = 0x1d6; +/// Change the post-texture scale and bias (inferred). +pub const CHANGE_POST_TEXTURE_SCALE_AND_BIAS: u32 = 0x1d7; +/// Skip 2 words (not data) (inferred). +pub const SKIP_2_WORDS: u32 = 0x1d8; +/// Skip 4 words (not data) (inferred). +pub const SKIP_4_WORDS: u32 = 0x1d9; +/// Skip 5 words (not data) (inferred). +pub const SKIP_5_WORDS: u32 = 0x1da; +/// Swap control: write the swap strobe (inferred). +pub const SWAP_CONTROL_WRITE_SWAP_STROBE: u32 = 0x1db; +/// Set the input-DMA wait-mode flag (inferred). +pub const SET_INPUT_DMA_WAIT_MODE_FLAG: u32 = 0x1dc; +/// Raise a host interrupt with an event code (privileged) (inferred). +pub const RAISE_HOST_INTERRUPT_WITH_EVENT_CODE: u32 = 0x1dd; +/// Pipe-memory resume (privileged) (inferred). +pub const PIPE_MEMORY_RESUME: u32 = 0x1de; +/// Pipe-memory suspend with an event code (privileged) (inferred). +pub const PIPE_MEMORY_SUSPEND_WITH_EVENT_CODE: u32 = 0x1df; +/// Invalidate an ARM texture-cache index (inferred). +pub const INVALIDATE_ARM_TEXTURE_CACHE_INDEX: u32 = 0x1e0; +/// Pipe-memory copy-down (privileged) (inferred). +pub const PIPE_MEMORY_COPY_DOWN: u32 = 0x1e1; +/// Pipe-memory swap-in (privileged) (inferred). +pub const PIPE_MEMORY_SWAP_IN: u32 = 0x1e2; +/// Pipe-memory swap-out (privileged) (inferred). +pub const PIPE_MEMORY_SWAP_OUT: u32 = 0x1e3; +/// Pipe-memory reserve (privileged) (inferred). +pub const PIPE_MEMORY_RESERVE: u32 = 0x1e4; +/// Roll call of every engine (inferred). +pub const ROLL_CALL_OF_EVERY_ENGINE: u32 = 0x1e5; +/// Set the per-display-id mode byte (inferred). +pub const DISPLAY_MODE_BYTE: u32 = 0x1e6; +/// Publish a retrace sequence number (inferred). +pub const PUBLISH_RETRACE_SEQUENCE_NUMBER: u32 = 0x1e7; +/// Payload-path self-test (inferred). +pub const PAYLOAD_PATH_SELF_TEST: u32 = 0x1e8; +/// Query the board configuration block (478 words) (inferred). +pub const QUERY_BOARD_CONFIGURATION_BLOCK: u32 = 0x1e9; +/// Synchronise the pipe (inferred). +pub const SYNCHRONISE_PIPE: u32 = 0x1ea; +/// Start an input DMA (inferred). +pub const START_INPUT_DMA: u32 = 0x1eb; +/// EEPROM operation (flash load, read, write) (inferred). +pub const EEPROM_OPERATION: u32 = 0x1ec; +/// Describe the DMA page pool (privileged) (inferred). +pub const DESCRIBE_DMA_PAGE_POOL: u32 = 0x1ed; +/// Window read-clip set-up (three words) (inferred). +pub const WINDOW_READ_CLIP_SET_UP: u32 = 0x1ee; +/// Load the distributor verification program (inferred). +pub const LOAD_DISTRIBUTOR_VERIFICATION_PROGRAM: u32 = 0x1ef; +/// Dump the distributor verification state (inferred). +pub const DUMP_DISTRIBUTOR_VERIFICATION_STATE: u32 = 0x1f0; +/// Pipe set-up (privileged) (inferred). +pub const PIPE_SET_UP: u32 = 0x1f1; +/// Set up output DMA for a host read (inferred). +pub const SET_UP_OUTPUT_DMA_FOR_HOST_READ: u32 = 0x1f2; +/// Window and clip set-up (eleven words) (inferred). +pub const WINDOW_CLIP_SETUP: u32 = 0x1f3; +/// Display-bank set-up (two words) (inferred). +pub const DISPLAY_BANK_SET_UP: u32 = 0x1f4; +/// Start select/feedback return DMA (six words) (inferred). +pub const START_SELECT_FEEDBACK_RETURN_DMA: u32 = 0x1f5; +/// Finish select/feedback with an event code (inferred). +pub const FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE: u32 = 0x1f6; +/// Context destroy (inferred). +pub const CONTEXT_DESTROY: u32 = 0x1f7; +/// Context clean-up with an event code (inferred). +pub const CONTEXT_CLEAN_UP_WITH_EVENT_CODE: u32 = 0x1f8; +/// Context image load (inferred). +pub const CONTEXT_IMAGE_LOAD: u32 = 0x1f9; +/// Context activate (inferred). +pub const CONTEXT_ACTIVATE: u32 = 0x1fa; +/// Context FIFO synchronise (unknown). +pub const CONTEXT_FIFO_SYNCHRONISE: u32 = 0x1fb; +/// Context deactivate (inferred). +pub const CONTEXT_DEACTIVATE: u32 = 0x1fc; +/// Context initialise (inferred). +pub const CONTEXT_INITIALISE: u32 = 0x1fd; +/// Kernel-private command (ignored) (unknown). +pub const KERNEL_PRIVATE_COMMAND: u32 = 0x1fe; +/// Write-gather flush, last slot. +pub const WRITE_GATHER_FLUSH_LAST_SLOT: u32 = 0x1ff; + +// The name and the family of every host token, for the monitor and the replay census. +// The family follows the table's section: vertex tokens, matrices, arrays, textures, +// queries, and everything else. + +/// The role name of host token `token`, or "" for a number past the token map. +pub fn token_name(token: u32) -> &'static str { + match token { + RAW_DATA_WORD => "RAW_DATA_WORD", + CONFIGURE_FRAME_BUFFER_LAYOUT => "CONFIGURE_FRAME_BUFFER_LAYOUT", + FLUSH_AND_REBUILD_HARDWARE_TOKEN_MAP_FOR_PACKED_VERTEX => "FLUSH_AND_REBUILD_HARDWARE_TOKEN_MAP_FOR_PACKED_VERTEX", + BEGIN_PRIMITIVE_RECTANGLE => "BEGIN_PRIMITIVE_RECTANGLE", + CONFIGURE_READ_SIDE_FRAME_BUFFER_LAYOUT => "CONFIGURE_READ_SIDE_FRAME_BUFFER_LAYOUT", + DRAW_BITMAP_FROM_DISPLAY_LIST => "DRAW_BITMAP_FROM_DISPLAY_LIST", + DRAW_BITMAP => "DRAW_BITMAP", + SET_LIGHT_PARAMETER_FLOAT_FORM => "SET_LIGHT_PARAMETER_FLOAT_FORM", + SET_LIGHT_PARAMETER_INTEGER_FORM => "SET_LIGHT_PARAMETER_INTEGER_FORM", + SET_LIGHTING_MODEL_PARAMETER_FLOAT_FORM => "SET_LIGHTING_MODEL_PARAMETER_FLOAT_FORM", + SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM => "SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM", + LOAD_POLYGON_STIPPLE_PATTERN => "LOAD_POLYGON_STIPPLE_PATTERN", + SET_SCISSOR_BOX => "SET_SCISSOR_BOX", + TEXTURE_PARAMETER_FLOAT => "TEXTURE_PARAMETER_FLOAT", + TEXTURE_PARAMETER_INTEGER => "TEXTURE_PARAMETER_INTEGER", + T_1D_TEXTURE_IMAGE_LOAD => "T_1D_TEXTURE_IMAGE_LOAD", + T_2D_TEXTURE_IMAGE_LOAD => "T_2D_TEXTURE_IMAGE_LOAD", + SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM => "SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM", + SET_TEXTURE_ENVIRONMENT_PARAMETER_INTEGER_FORM => "SET_TEXTURE_ENVIRONMENT_PARAMETER_INTEGER_FORM", + TEXTURE_COORDINATE_GENERATION_DOUBLE_FORM => "TEXTURE_COORDINATE_GENERATION_DOUBLE_FORM", + TEXTURE_COORDINATE_GENERATION_FLOAT_FORM => "TEXTURE_COORDINATE_GENERATION_FLOAT_FORM", + TEXTURE_COORDINATE_GENERATION_INTEGER_FORM => "TEXTURE_COORDINATE_GENERATION_INTEGER_FORM", + BIND_TEXTURE_BY_OLD_STYLE_BINDING => "BIND_TEXTURE_BY_OLD_STYLE_BINDING", + SELECT_FEEDBACK_RECORD_TYPE => "SELECT_FEEDBACK_RECORD_TYPE", + SELECT_BUFFER_REGISTRATION => "SELECT_BUFFER_REGISTRATION", + SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES => "SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES", + CLEAR_SELECTED_BUFFERS => "CLEAR_SELECTED_BUFFERS", + SET_ACCUMULATION_BUFFER_CLEAR_VALUE => "SET_ACCUMULATION_BUFFER_CLEAR_VALUE", + SET_COLOUR_INDEX_CLEAR_VALUE => "SET_COLOUR_INDEX_CLEAR_VALUE", + SET_COLOUR_CLEAR_VALUE => "SET_COLOUR_CLEAR_VALUE", + SET_STENCIL_CLEAR_VALUE => "SET_STENCIL_CLEAR_VALUE", + SET_DEPTH_CLEAR_VALUE => "SET_DEPTH_CLEAR_VALUE", + SET_STENCIL_WRITE_MASK => "SET_STENCIL_WRITE_MASK", + SET_COLOUR_WRITE_MASK => "SET_COLOUR_WRITE_MASK", + SET_DEPTH_WRITE_MASK => "SET_DEPTH_WRITE_MASK", + SET_COLOUR_INDEX_WRITE_MASK => "SET_COLOUR_INDEX_WRITE_MASK", + ENABLE_CAPABILITY => "ENABLE_CAPABILITY", + DISABLE_CAPABILITY => "DISABLE_CAPABILITY", + EVALUATE_1D_GRID_STRIP => "EVALUATE_1D_GRID_STRIP", + EVALUATE_2D_GRID_STRIP => "EVALUATE_2D_GRID_STRIP", + SET_UP_1D_EVALUATOR_GRID => "SET_UP_1D_EVALUATOR_GRID", + SET_UP_2D_EVALUATOR_GRID => "SET_UP_2D_EVALUATOR_GRID", + LOAD_1D_EVALUATOR_CONTROL_POINTS => "LOAD_1D_EVALUATOR_CONTROL_POINTS", + LOAD_2D_EVALUATOR_CONTROL_POINTS => "LOAD_2D_EVALUATOR_CONTROL_POINTS", + SET_UP_1D_EVALUATOR_MAP_SPECIFICATION => "SET_UP_1D_EVALUATOR_MAP_SPECIFICATION", + SET_UP_2D_EVALUATOR_MAP_SPECIFICATION => "SET_UP_2D_EVALUATOR_MAP_SPECIFICATION", + SET_LOGIC_OPERATION => "SET_LOGIC_OPERATION", + SET_PIXEL_ZOOM_FACTORS => "SET_PIXEL_ZOOM_FACTORS", + SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM => "SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM", + SET_PIXEL_TRANSFER_PARAMETER_INTEGER_FORM => "SET_PIXEL_TRANSFER_PARAMETER_INTEGER_FORM", + SET_PIXEL_STORE_PARAMETER_FLOAT_FORM => "SET_PIXEL_STORE_PARAMETER_FLOAT_FORM", + SET_PIXEL_STORE_PARAMETER_INTEGER_FORM => "SET_PIXEL_STORE_PARAMETER_INTEGER_FORM", + LOAD_PIXEL_MAP_FLOAT_VALUES => "LOAD_PIXEL_MAP_FLOAT_VALUES", + LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES => "LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES", + LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES => "LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES", + SELECT_READ_BUFFER => "SELECT_READ_BUFFER", + COPY_PIXEL_RECTANGLE_WITHIN_FRAME_BUFFER => "COPY_PIXEL_RECTANGLE_WITHIN_FRAME_BUFFER", + READ_PIXEL_RECTANGLE_BACK_TO_HOST => "READ_PIXEL_RECTANGLE_BACK_TO_HOST", + DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA => "DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA", + QUERY_CURRENT_VERTEX_ATTRIBUTES => "QUERY_CURRENT_VERTEX_ATTRIBUTES", + QUERY_CLIP_PLANE => "QUERY_CLIP_PLANE", + QUERY_RASTER_POSITION_STATE => "QUERY_RASTER_POSITION_STATE", + QUERY_PENDING_ERROR => "QUERY_PENDING_ERROR", + QUERY_STATE_FLOAT => "QUERY_STATE_FLOAT", + QUERY_STATE_INTEGER => "QUERY_STATE_INTEGER", + QUERY_LIGHT_FLOAT => "QUERY_LIGHT_FLOAT", + QUERY_LIGHT_INTEGER => "QUERY_LIGHT_INTEGER", + QUERY_EVALUATOR_MAP_DOUBLE => "QUERY_EVALUATOR_MAP_DOUBLE", + QUERY_EVALUATOR_MAP_FLOAT => "QUERY_EVALUATOR_MAP_FLOAT", + QUERY_EVALUATOR_MAP_INTEGER => "QUERY_EVALUATOR_MAP_INTEGER", + QUERY_MATERIAL_FLOAT => "QUERY_MATERIAL_FLOAT", + QUERY_MATERIAL_INTEGER => "QUERY_MATERIAL_INTEGER", + QUERY_PIXEL_MAP_FLOAT => "QUERY_PIXEL_MAP_FLOAT", + QUERY_PIXEL_MAP_UNSIGNED_32_BIT => "QUERY_PIXEL_MAP_UNSIGNED_32_BIT", + QUERY_PIXEL_MAP_UNSIGNED_16_BIT => "QUERY_PIXEL_MAP_UNSIGNED_16_BIT", + QUERY_POLYGON_STIPPLE => "QUERY_POLYGON_STIPPLE", + QUERY_STATE_GENERIC => "QUERY_STATE_GENERIC", + QUERY_TEXTURE_ENVIRONMENT_FLOAT => "QUERY_TEXTURE_ENVIRONMENT_FLOAT", + QUERY_TEXTURE_ENVIRONMENT_INTEGER => "QUERY_TEXTURE_ENVIRONMENT_INTEGER", + QUERY_TEXTURE_GENERATION_DOUBLE => "QUERY_TEXTURE_GENERATION_DOUBLE", + QUERY_TEXTURE_GENERATION_FLOAT => "QUERY_TEXTURE_GENERATION_FLOAT", + QUERY_TEXTURE_GENERATION_INTEGER => "QUERY_TEXTURE_GENERATION_INTEGER", + QUERY_TEXTURE_IMAGE => "QUERY_TEXTURE_IMAGE", + QUERY_TEXTURE_PARAMETER_FLOAT => "QUERY_TEXTURE_PARAMETER_FLOAT", + QUERY_TEXTURE_PARAMETER_INTEGER => "QUERY_TEXTURE_PARAMETER_INTEGER", + QUERY_TEXTURE_LEVEL_PARAMETER_FLOAT => "QUERY_TEXTURE_LEVEL_PARAMETER_FLOAT", + QUERY_TEXTURE_LEVEL_PARAMETER_INTEGER => "QUERY_TEXTURE_LEVEL_PARAMETER_INTEGER", + BEGIN_PRIMITIVE_POINTS => "BEGIN_PRIMITIVE_POINTS", + BEGIN_PRIMITIVE_INDEPENDENT_LINES => "BEGIN_PRIMITIVE_INDEPENDENT_LINES", + BEGIN_PRIMITIVE_LINE_LOOP => "BEGIN_PRIMITIVE_LINE_LOOP", + BEGIN_PRIMITIVE_LINE_STRIP => "BEGIN_PRIMITIVE_LINE_STRIP", + BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES => "BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES", + BEGIN_PRIMITIVE_TRIANGLE_STRIP => "BEGIN_PRIMITIVE_TRIANGLE_STRIP", + BEGIN_PRIMITIVE_TRIANGLE_FAN => "BEGIN_PRIMITIVE_TRIANGLE_FAN", + BEGIN_PRIMITIVE_INDEPENDENT_QUADS => "BEGIN_PRIMITIVE_INDEPENDENT_QUADS", + BEGIN_PRIMITIVE_QUAD_STRIP => "BEGIN_PRIMITIVE_QUAD_STRIP", + BEGIN_PRIMITIVE_POLYGON => "BEGIN_PRIMITIVE_POLYGON", + SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_BYTE => "SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_BYTE", + SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE => "SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE", + SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT => "SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT", + SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_32_BIT_INTEGER => "SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_32_BIT_INTEGER", + SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_SHORT => "SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_SHORT", + SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_BYTE => "SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_BYTE", + SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_32_BIT_INTEGER => "SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_32_BIT_INTEGER", + SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_SHORT => "SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_SHORT", + SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_BYTE => "SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_BYTE", + SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE => "SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE", + SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT => "SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT", + SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_32_BIT_INTEGER => "SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_32_BIT_INTEGER", + SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_SHORT => "SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_SHORT", + SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_BYTE => "SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_BYTE", + SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_32_BIT_INTEGER => "SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_32_BIT_INTEGER", + SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_SHORT => "SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_SHORT", + END_OPEN_PRIMITIVE => "END_OPEN_PRIMITIVE", + SET_CURRENT_COLOUR_INDEX_DOUBLE => "SET_CURRENT_COLOUR_INDEX_DOUBLE", + SET_CURRENT_COLOUR_INDEX_SINGLE_FLOAT => "SET_CURRENT_COLOUR_INDEX_SINGLE_FLOAT", + SET_CURRENT_COLOUR_INDEX_SIGNED_32_BIT_INTEGER => "SET_CURRENT_COLOUR_INDEX_SIGNED_32_BIT_INTEGER", + SET_CURRENT_COLOUR_INDEX_SIGNED_SHORT => "SET_CURRENT_COLOUR_INDEX_SIGNED_SHORT", + SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_BYTE => "SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_BYTE", + SET_CURRENT_NORMAL_3_COMPONENTS_DOUBLE => "SET_CURRENT_NORMAL_3_COMPONENTS_DOUBLE", + SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT => "SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT", + SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_32_BIT_INTEGER => "SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_32_BIT_INTEGER", + SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_SHORT => "SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_SHORT", + SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_DOUBLE => "SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_DOUBLE", + SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT => "SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT", + SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_32_BIT => "SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_32_BIT", + SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_SHORT => "SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_SHORT", + SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_DOUBLE => "SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_DOUBLE", + SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT => "SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT", + SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_32_BIT => "SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_32_BIT", + SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT => "SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT", + SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_DOUBLE => "SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_DOUBLE", + SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT => "SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT", + SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_32_BIT => "SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_32_BIT", + SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_SHORT => "SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_SHORT", + SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_DOUBLE => "SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_DOUBLE", + SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT => "SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT", + SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_32_BIT => "SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_32_BIT", + SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_SHORT => "SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_SHORT", + SUBMIT_VERTEX_2_COMPONENTS_DOUBLE => "SUBMIT_VERTEX_2_COMPONENTS_DOUBLE", + SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT => "SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT", + SUBMIT_VERTEX_2_COMPONENTS_SIGNED_32_BIT_INTEGER => "SUBMIT_VERTEX_2_COMPONENTS_SIGNED_32_BIT_INTEGER", + SUBMIT_VERTEX_2_COMPONENTS_SIGNED_SHORT => "SUBMIT_VERTEX_2_COMPONENTS_SIGNED_SHORT", + SUBMIT_VERTEX_3_COMPONENTS_DOUBLE => "SUBMIT_VERTEX_3_COMPONENTS_DOUBLE", + SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT => "SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT", + SUBMIT_VERTEX_3_COMPONENTS_SIGNED_32_BIT_INTEGER => "SUBMIT_VERTEX_3_COMPONENTS_SIGNED_32_BIT_INTEGER", + SUBMIT_VERTEX_3_COMPONENTS_SIGNED_SHORT => "SUBMIT_VERTEX_3_COMPONENTS_SIGNED_SHORT", + SUBMIT_VERTEX_4_COMPONENTS_DOUBLE => "SUBMIT_VERTEX_4_COMPONENTS_DOUBLE", + SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT => "SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT", + SUBMIT_VERTEX_4_COMPONENTS_SIGNED_32_BIT_INTEGER => "SUBMIT_VERTEX_4_COMPONENTS_SIGNED_32_BIT_INTEGER", + SUBMIT_VERTEX_4_COMPONENTS_SIGNED_SHORT => "SUBMIT_VERTEX_4_COMPONENTS_SIGNED_SHORT", + SET_MATERIAL_PARAMETER_FLOAT_FORM => "SET_MATERIAL_PARAMETER_FLOAT_FORM", + CONTINUATION_ENTRY_OF_FLOAT_MATERIAL_PARAMETER => "CONTINUATION_ENTRY_OF_FLOAT_MATERIAL_PARAMETER", + SET_MATERIAL_PARAMETER_INTEGER_FORM => "SET_MATERIAL_PARAMETER_INTEGER_FORM", + CONTINUATION_ENTRY_OF_INTEGER_MATERIAL_PARAMETER => "CONTINUATION_ENTRY_OF_INTEGER_MATERIAL_PARAMETER", + EVALUATOR_COORDINATE_1D_DOUBLE => "EVALUATOR_COORDINATE_1D_DOUBLE", + CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD1D => "CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD1D", + EVALUATOR_COORDINATE_1D_SINGLE_FLOAT => "EVALUATOR_COORDINATE_1D_SINGLE_FLOAT", + CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD1F => "CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD1F", + EVALUATOR_COORDINATE_2D_DOUBLE => "EVALUATOR_COORDINATE_2D_DOUBLE", + CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD2D => "CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD2D", + EVALUATOR_COORDINATE_2D_SINGLE_FLOAT => "EVALUATOR_COORDINATE_2D_SINGLE_FLOAT", + CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD2F => "CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_COORD2F", + EVALUATOR_GRID_POINT_1D => "EVALUATOR_GRID_POINT_1D", + CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_POINT1 => "CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_POINT1", + EVALUATOR_GRID_POINT_2D => "EVALUATOR_GRID_POINT_2D", + CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_POINT2 => "CONTINUATION_ENTRY_OF_EVALUATOR_EVAL_POINT2", + SET_CURRENT_EDGE_FLAG => "SET_CURRENT_EDGE_FLAG", + SET_RASTER_POSITION_2_COMPONENTS_DOUBLE => "SET_RASTER_POSITION_2_COMPONENTS_DOUBLE", + SET_RASTER_POSITION_2_COMPONENTS_FLOAT => "SET_RASTER_POSITION_2_COMPONENTS_FLOAT", + SET_RASTER_POSITION_2_COMPONENTS_32_BIT_INTEGER => "SET_RASTER_POSITION_2_COMPONENTS_32_BIT_INTEGER", + SET_RASTER_POSITION_2_COMPONENTS_SHORT => "SET_RASTER_POSITION_2_COMPONENTS_SHORT", + SET_RASTER_POSITION_3_COMPONENTS_DOUBLE => "SET_RASTER_POSITION_3_COMPONENTS_DOUBLE", + SET_RASTER_POSITION_3_COMPONENTS_FLOAT => "SET_RASTER_POSITION_3_COMPONENTS_FLOAT", + SET_RASTER_POSITION_3_COMPONENTS_32_BIT_INTEGER => "SET_RASTER_POSITION_3_COMPONENTS_32_BIT_INTEGER", + SET_RASTER_POSITION_3_COMPONENTS_SHORT => "SET_RASTER_POSITION_3_COMPONENTS_SHORT", + SET_RASTER_POSITION_4_COMPONENTS_DOUBLE => "SET_RASTER_POSITION_4_COMPONENTS_DOUBLE", + SET_RASTER_POSITION_4_COMPONENTS_FLOAT => "SET_RASTER_POSITION_4_COMPONENTS_FLOAT", + SET_RASTER_POSITION_4_COMPONENTS_32_BIT_INTEGER => "SET_RASTER_POSITION_4_COMPONENTS_32_BIT_INTEGER", + SET_RASTER_POSITION_4_COMPONENTS_SHORT => "SET_RASTER_POSITION_4_COMPONENTS_SHORT", + SET_USER_CLIP_PLANE => "SET_USER_CLIP_PLANE", + SET_COLOUR_MATERIAL_TRACKING => "SET_COLOUR_MATERIAL_TRACKING", + SET_FACE_CULLING => "SET_FACE_CULLING", + SET_FOG_PARAMETER_FLOAT_FORM => "SET_FOG_PARAMETER_FLOAT_FORM", + SET_FOG_PARAMETER_INTEGER_FORM => "SET_FOG_PARAMETER_INTEGER_FORM", + CONTINUATION_ENTRY_OF_INTEGER_FOG_PARAMETER => "CONTINUATION_ENTRY_OF_INTEGER_FOG_PARAMETER", + SET_FRONT_FACE_WINDING => "SET_FRONT_FACE_WINDING", + SET_QUALITY_HINT => "SET_QUALITY_HINT", + SET_LINE_STIPPLE => "SET_LINE_STIPPLE", + SET_LINE_WIDTH => "SET_LINE_WIDTH", + SET_POINT_SIZE => "SET_POINT_SIZE", + SET_POLYGON_FILL_MODE => "SET_POLYGON_FILL_MODE", + SET_FLAT_OR_SMOOTH_SHADING => "SET_FLAT_OR_SMOOTH_SHADING", + INSERT_FEEDBACK_PASS_THROUGH_MARKER => "INSERT_FEEDBACK_PASS_THROUGH_MARKER", + POP_SELECTION_NAME => "POP_SELECTION_NAME", + PUSH_SELECTION_NAME => "PUSH_SELECTION_NAME", + SELECT_DRAW_BUFFER => "SELECT_DRAW_BUFFER", + ACCUMULATION_BUFFER_OPERATION => "ACCUMULATION_BUFFER_OPERATION", + FINISH_DRAIN_PIPE_AND_SIGNAL_COMPLETION => "FINISH_DRAIN_PIPE_AND_SIGNAL_COMPLETION", + FLUSH_VERTEX_PIPELINE => "FLUSH_VERTEX_PIPELINE", + POP_ATTRIBUTE_STACK => "POP_ATTRIBUTE_STACK", + PUSH_ATTRIBUTE_STACK => "PUSH_ATTRIBUTE_STACK", + SET_ALPHA_TEST => "SET_ALPHA_TEST", + SET_BLEND_FACTORS => "SET_BLEND_FACTORS", + SET_STENCIL_TEST => "SET_STENCIL_TEST", + SET_STENCIL_OPERATIONS => "SET_STENCIL_OPERATIONS", + SET_DEPTH_TEST => "SET_DEPTH_TEST", + SET_DEPTH_RANGE => "SET_DEPTH_RANGE", + SELECT_CURRENT_MATRIX_STACK => "SELECT_CURRENT_MATRIX_STACK", + MULTIPLY_CURRENT_MATRIX_BY_FRUSTUM => "MULTIPLY_CURRENT_MATRIX_BY_FRUSTUM", + LOAD_IDENTITY_MATRIX => "LOAD_IDENTITY_MATRIX", + LOAD_MATRIX_16_FLOATS => "LOAD_MATRIX_16_FLOATS", + LOAD_MATRIX_16_DOUBLES => "LOAD_MATRIX_16_DOUBLES", + MULTIPLY_BY_MATRIX_16_FLOATS => "MULTIPLY_BY_MATRIX_16_FLOATS", + MULTIPLY_BY_MATRIX_16_DOUBLES => "MULTIPLY_BY_MATRIX_16_DOUBLES", + MULTIPLY_CURRENT_MATRIX_BY_ORTHOGRAPHIC_PROJECTION => "MULTIPLY_CURRENT_MATRIX_BY_ORTHOGRAPHIC_PROJECTION", + POP_MATRIX_STACK => "POP_MATRIX_STACK", + PUSH_MATRIX_STACK => "PUSH_MATRIX_STACK", + ROTATE_DOUBLES => "ROTATE_DOUBLES", + ROTATE_FLOATS => "ROTATE_FLOATS", + SCALE_DOUBLES => "SCALE_DOUBLES", + SCALE_FLOATS => "SCALE_FLOATS", + TRANSLATE_DOUBLES => "TRANSLATE_DOUBLES", + TRANSLATE_FLOATS => "TRANSLATE_FLOATS", + COPY_ENGINE_WORK_MEMORY_TO_CONTROL_MEMORY => "COPY_ENGINE_WORK_MEMORY_TO_CONTROL_MEMORY", + PASS_ONE_COMMAND_WORD_THROUGH => "PASS_ONE_COMMAND_WORD_THROUGH", + WRITE_ONE_RASTER_BUS_WORD => "WRITE_ONE_RASTER_BUS_WORD", + SET_VIEWPORT => "SET_VIEWPORT", + RECORD_ERROR_CODE => "RECORD_ERROR_CODE", + END_OF_SIMULATION_REPORT => "END_OF_SIMULATION_REPORT", + SET_CONSTANT_BLEND_COLOUR => "SET_CONSTANT_BLEND_COLOUR", + SET_BLEND_EQUATION => "SET_BLEND_EQUATION", + LOAD_COLOUR_TABLE => "LOAD_COLOUR_TABLE", + COPY_COLOUR_TABLE_FROM_FRAME_BUFFER => "COPY_COLOUR_TABLE_FROM_FRAME_BUFFER", + QUERY_COLOUR_TABLE => "QUERY_COLOUR_TABLE", + SET_COLOUR_TABLE_PARAMETERS => "SET_COLOUR_TABLE_PARAMETERS", + QUERY_COLOUR_TABLE_PARAMETERS => "QUERY_COLOUR_TABLE_PARAMETERS", + LOAD_POST_TEXTURE_SCALE_AND_BIAS => "LOAD_POST_TEXTURE_SCALE_AND_BIAS", + QUERY_LINE_MODE_LIMIT_TABLE => "QUERY_LINE_MODE_LIMIT_TABLE", + LOAD_1D_CONVOLUTION_FILTER => "LOAD_1D_CONVOLUTION_FILTER", + LOAD_2D_CONVOLUTION_FILTER => "LOAD_2D_CONVOLUTION_FILTER", + T_3D_CONVOLUTION_FILTER => "T_3D_CONVOLUTION_FILTER", + COPY_1D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER => "COPY_1D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER", + COPY_2D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER => "COPY_2D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER", + QUERY_CONVOLUTION_FILTER => "QUERY_CONVOLUTION_FILTER", + LOAD_SEPARABLE_2D_FILTER => "LOAD_SEPARABLE_2D_FILTER", + T_3D_SEPARABLE_FILTER => "T_3D_SEPARABLE_FILTER", + QUERY_SEPARABLE_FILTER => "QUERY_SEPARABLE_FILTER", + SET_PIXEL_TRANSFORM_PARAMETERS_INTEGER => "SET_PIXEL_TRANSFORM_PARAMETERS_INTEGER", + SET_PIXEL_TRANSFORM_PARAMETERS_FLOAT => "SET_PIXEL_TRANSFORM_PARAMETERS_FLOAT", + QUERY_PIXEL_TRANSFORM_PARAMETERS_INTEGER => "QUERY_PIXEL_TRANSFORM_PARAMETERS_INTEGER", + QUERY_PIXEL_TRANSFORM_PARAMETERS_FLOAT => "QUERY_PIXEL_TRANSFORM_PARAMETERS_FLOAT", + COPY_1D_TEXTURE_IMAGE_FROM_FRAME_BUFFER => "COPY_1D_TEXTURE_IMAGE_FROM_FRAME_BUFFER", + COPY_2D_TEXTURE_IMAGE_FROM_FRAME_BUFFER => "COPY_2D_TEXTURE_IMAGE_FROM_FRAME_BUFFER", + COPY_1D_TEXTURE_SUB_IMAGE_FROM_FRAME_BUFFER => "COPY_1D_TEXTURE_SUB_IMAGE_FROM_FRAME_BUFFER", + WRITE_GATHER_FLUSH_FIRST_SLOT => "WRITE_GATHER_FLUSH_FIRST_SLOT", + RAW_DATA_WORD_SECOND_FORM => "RAW_DATA_WORD_SECOND_FORM", + COPY_2D_TEXTURE_SUB_IMAGE_FROM_FRAME_BUFFER => "COPY_2D_TEXTURE_SUB_IMAGE_FROM_FRAME_BUFFER", + SET_UP_HISTOGRAM => "SET_UP_HISTOGRAM", + RESET_HISTOGRAM => "RESET_HISTOGRAM", + QUERY_HISTOGRAM => "QUERY_HISTOGRAM", + QUERY_HISTOGRAM_PARAMETERS => "QUERY_HISTOGRAM_PARAMETERS", + BIND_LINEAR_TEXTURE => "BIND_LINEAR_TEXTURE", + SET_UP_MIN_MAX_TRACKING => "SET_UP_MIN_MAX_TRACKING", + RESET_MIN_MAX_TRACKING => "RESET_MIN_MAX_TRACKING", + QUERY_MIN_MAX_RESULTS => "QUERY_MIN_MAX_RESULTS", + QUERY_MIN_MAX_PARAMETERS => "QUERY_MIN_MAX_PARAMETERS", + SAVE_LIGHT_ATTRIBUTES_ON_ENGINE_ATTRIBUTE_STACK => "SAVE_LIGHT_ATTRIBUTES_ON_ENGINE_ATTRIBUTE_STACK", + CLEAR_SELECTION_NAME_STACK => "CLEAR_SELECTION_NAME_STACK", + REPLACE_TOP_SELECTION_NAME => "REPLACE_TOP_SELECTION_NAME", + SET_CURRENT_RASTER_POSITION => "SET_CURRENT_RASTER_POSITION", + SET_FOG_FUNCTION_TABLE => "SET_FOG_FUNCTION_TABLE", + ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION => "ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION", + SUSPEND_PIXEL_MODE_STATE_AROUND_TRANSFER => "SUSPEND_PIXEL_MODE_STATE_AROUND_TRANSFER", + SWITCH_LEGACY_API_COMPATIBILITY_MODE => "SWITCH_LEGACY_API_COMPATIBILITY_MODE", + SET_COLOUR_MATERIAL_VALUE => "SET_COLOUR_MATERIAL_VALUE", + SELECT_VIDEO_SOURCE => "SELECT_VIDEO_SOURCE", + ASK_WHETHER_CONVOLUTION_COPY_OVERLAPS => "ASK_WHETHER_CONVOLUTION_COPY_OVERLAPS", + RESOLVE_WHETHER_PIXEL_FORMAT_IS_SINGLE_COMPONENT => "RESOLVE_WHETHER_PIXEL_FORMAT_IS_SINGLE_COMPONENT", + SET_MULTISAMPLE_PATTERN => "SET_MULTISAMPLE_PATTERN", + SET_MULTISAMPLE_COVERAGE_MASK => "SET_MULTISAMPLE_COVERAGE_MASK", + POP_SAVED_LIGHT_STATE => "POP_SAVED_LIGHT_STATE", + SET_PIXEL_TEXTURE_GENERATION => "SET_PIXEL_TEXTURE_GENERATION", + SET_POLYGON_OFFSET_EXTENSION_FORM => "SET_POLYGON_OFFSET_EXTENSION_FORM", + LOAD_1D_TEXTURE_SUB_IMAGE => "LOAD_1D_TEXTURE_SUB_IMAGE", + LOAD_2D_TEXTURE_SUB_IMAGE => "LOAD_2D_TEXTURE_SUB_IMAGE", + LOAD_3D_TEXTURE_SUB_IMAGE => "LOAD_3D_TEXTURE_SUB_IMAGE", + LOAD_3D_TEXTURE_IMAGE => "LOAD_3D_TEXTURE_IMAGE", + T_4D_TEXTURE_IMAGE => "T_4D_TEXTURE_IMAGE", + T_4D_TEXTURE_SUB_IMAGE => "T_4D_TEXTURE_SUB_IMAGE", + ALLOCATE_1D_TEXTURE_NAMES => "ALLOCATE_1D_TEXTURE_NAMES", + ALLOCATE_2D_TEXTURE_NAMES => "ALLOCATE_2D_TEXTURE_NAMES", + ALLOCATE_3D_TEXTURE_NAMES => "ALLOCATE_3D_TEXTURE_NAMES", + QUERY_WHETHER_NAME_IS_TEXTURE => "QUERY_WHETHER_NAME_IS_TEXTURE", + DELETE_TEXTURE_NAMES => "DELETE_TEXTURE_NAMES", + BIND_TEXTURE_NAME => "BIND_TEXTURE_NAME", + WINDOW_SYSTEM_CONTEXT_INITIALISE => "WINDOW_SYSTEM_CONTEXT_INITIALISE", + WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD => "WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD", + WINDOW_SYSTEM_DRAWABLE_ORIGIN_AND_SIZE_LOAD => "WINDOW_SYSTEM_DRAWABLE_ORIGIN_AND_SIZE_LOAD", + WINDOW_SYSTEM_CLIP_LOAD => "WINDOW_SYSTEM_CLIP_LOAD", + WINDOW_SYSTEM_CLIP_LIST_LOAD => "WINDOW_SYSTEM_CLIP_LIST_LOAD", + WINDOW_SYSTEM_SCISSOR_LOAD => "WINDOW_SYSTEM_SCISSOR_LOAD", + LOAD_COLOUR_MAP_ENTRIES => "LOAD_COLOUR_MAP_ENTRIES", + LOAD_GAMMA_TABLE_ENTRIES => "LOAD_GAMMA_TABLE_ENTRIES", + LOAD_CURSOR_IMAGE_BLOCK => "LOAD_CURSOR_IMAGE_BLOCK", + LOAD_CURSOR_COLOUR_WORDS => "LOAD_CURSOR_COLOUR_WORDS", + LOAD_ONE_ENTRY_OF_DISPLAY_ID_TABLE => "LOAD_ONE_ENTRY_OF_DISPLAY_ID_TABLE", + WINDOW_CREATED_OR_DESTROYED_NOTICE_TO_COLOUR_MAP_CHIP => "WINDOW_CREATED_OR_DESTROYED_NOTICE_TO_COLOUR_MAP_CHIP", + QUERY_ENGINE_SIDE_TEXTURE_ATTRIBUTES => "QUERY_ENGINE_SIDE_TEXTURE_ATTRIBUTES", + SET_PIXEL_TRANSFORM_MODE => "SET_PIXEL_TRANSFORM_MODE", + WINDOW_SYSTEM_PIXEL_PATH_SET_UP => "WINDOW_SYSTEM_PIXEL_PATH_SET_UP", + WINDOW_SYSTEM_PIXEL_COPY => "WINDOW_SYSTEM_PIXEL_COPY", + WINDOW_SYSTEM_PIXEL_READ => "WINDOW_SYSTEM_PIXEL_READ", + WINDOW_SYSTEM_PIXEL_WRITE => "WINDOW_SYSTEM_PIXEL_WRITE", + WINDOW_SYSTEM_PIXEL_PATH_FINISH => "WINDOW_SYSTEM_PIXEL_PATH_FINISH", + UNUSED_WINDOW_SYSTEM_SLOT_3 => "UNUSED_WINDOW_SYSTEM_SLOT_3", + LOAD_WINDOW_SYSTEM_STIPPLE => "LOAD_WINDOW_SYSTEM_STIPPLE", + STORE_WINDOW_SYSTEM_STIPPLE => "STORE_WINDOW_SYSTEM_STIPPLE", + ROTATE_WINDOW_SYSTEM_STIPPLE => "ROTATE_WINDOW_SYSTEM_STIPPLE", + UNUSED_WINDOW_SYSTEM_SLOT_4 => "UNUSED_WINDOW_SYSTEM_SLOT_4", + UNUSED_WINDOW_SYSTEM_SLOT_5 => "UNUSED_WINDOW_SYSTEM_SLOT_5", + WINDOW_SYSTEM_1_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_1_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_2_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_2_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_3_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_3_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_4_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_4_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_FOUR_WORD_DATA_PRELOAD_BYTE_FORM => "WINDOW_SYSTEM_FOUR_WORD_DATA_PRELOAD_BYTE_FORM", + WINDOW_SYSTEM_5_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_5_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_6_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_6_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_7_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_7_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_8_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_8_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_9_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_9_WORD_DATA_PRELOAD", + WINDOW_SYSTEM_10_WORD_DATA_PRELOAD => "WINDOW_SYSTEM_10_WORD_DATA_PRELOAD", + DRAW_ABSOLUTE_POINTS => "DRAW_ABSOLUTE_POINTS", + DRAW_ABSOLUTE_POINTS_BURST_OF_6 => "DRAW_ABSOLUTE_POINTS_BURST_OF_6", + DRAW_ABSOLUTE_POINTS_BURST_OF_24 => "DRAW_ABSOLUTE_POINTS_BURST_OF_24", + DRAW_RELATIVE_POINTS => "DRAW_RELATIVE_POINTS", + DRAW_RELATIVE_POINTS_BURST_OF_6 => "DRAW_RELATIVE_POINTS_BURST_OF_6", + DRAW_RELATIVE_POINTS_BURST_OF_24 => "DRAW_RELATIVE_POINTS_BURST_OF_24", + FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT => "FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT", + FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT_BURST_OF_6 => "FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT_BURST_OF_6", + FILL_BOXES_TWO_CORNERS => "FILL_BOXES_TWO_CORNERS", + FILL_BOXES_TWO_CORNERS_BURST_OF_6 => "FILL_BOXES_TWO_CORNERS_BURST_OF_6", + FILL_SPANS => "FILL_SPANS", + FILL_SPANS_BURST_OF_2 => "FILL_SPANS_BURST_OF_2", + FILL_SPANS_BURST_OF_6 => "FILL_SPANS_BURST_OF_6", + DRAW_LINE_SEGMENTS => "DRAW_LINE_SEGMENTS", + DRAW_LINE_SEGMENTS_BURST_OF_6 => "DRAW_LINE_SEGMENTS_BURST_OF_6", + DRAW_CONNECTED_POLYLINE => "DRAW_CONNECTED_POLYLINE", + DRAW_CONNECTED_POLYLINE_BURST_OF_6 => "DRAW_CONNECTED_POLYLINE_BURST_OF_6", + DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS => "DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS", + DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS_BURST_OF_6 => "DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS_BURST_OF_6", + DRAW_POINTS_IN_BACKGROUND_COLOUR => "DRAW_POINTS_IN_BACKGROUND_COLOUR", + DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_6 => "DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_6", + DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_24 => "DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_24", + UNUSED_WINDOW_SYSTEM_SLOT_6 => "UNUSED_WINDOW_SYSTEM_SLOT_6", + UNUSED_WINDOW_SYSTEM_SLOT_7 => "UNUSED_WINDOW_SYSTEM_SLOT_7", + UNUSED_WINDOW_SYSTEM_SLOT_8 => "UNUSED_WINDOW_SYSTEM_SLOT_8", + UNUSED_WINDOW_SYSTEM_SLOT_9 => "UNUSED_WINDOW_SYSTEM_SLOT_9", + SWAP_REQUEST_FOR_MULTI_PIPE_DISPLAY_OPTION => "SWAP_REQUEST_FOR_MULTI_PIPE_DISPLAY_OPTION", + COPY_USER_LIST_BY_LIST_DMA => "COPY_USER_LIST_BY_LIST_DMA", + SET_POLYGON_OFFSET => "SET_POLYGON_OFFSET", + DIAGNOSTIC_SUB_COMMAND_DISPATCH => "DIAGNOSTIC_SUB_COMMAND_DISPATCH", + RESERVED_DIAGNOSTIC_TOKEN_0 => "RESERVED_DIAGNOSTIC_TOKEN_0", + RESERVED_DIAGNOSTIC_TOKEN_1 => "RESERVED_DIAGNOSTIC_TOKEN_1", + COPY_LIST_BY_LIST_DMA => "COPY_LIST_BY_LIST_DMA", + EXECUTE_LIST_BY_LIST_DMA => "EXECUTE_LIST_BY_LIST_DMA", + EXECUTE_LIST_BY_INPUT_DMA => "EXECUTE_LIST_BY_INPUT_DMA", + MOVE_LIST_BY_LIST_DMA => "MOVE_LIST_BY_LIST_DMA", + EXECUTE_LIST_BY_LIST_DMA_REPEATED => "EXECUTE_LIST_BY_LIST_DMA_REPEATED", + EXECUTE_LIST_BY_INPUT_DMA_REPEATED => "EXECUTE_LIST_BY_INPUT_DMA_REPEATED", + DISTRIBUTOR_NO_OP_PADDING_WORD => "DISTRIBUTOR_NO_OP_PADDING_WORD", + TEXTURE_LOAD_SET_UP => "TEXTURE_LOAD_SET_UP", + DRAW_TEXELS_INTO_TEXTURE_MEMORY => "DRAW_TEXELS_INTO_TEXTURE_MEMORY", + TEXTURE_LOAD_SET_UP_SECOND_FORM => "TEXTURE_LOAD_SET_UP_SECOND_FORM", + TEXTURE_LOAD_BY_IDENTIFIER => "TEXTURE_LOAD_BY_IDENTIFIER", + SELECT_ACTIVE_TEXTURE_UNIT => "SELECT_ACTIVE_TEXTURE_UNIT", + BIND_TEXTURE_BY_IDENTIFIER => "BIND_TEXTURE_BY_IDENTIFIER", + BIND_2D_MIP_MAPPED_TEXTURE => "BIND_2D_MIP_MAPPED_TEXTURE", + BIND_BICUBIC_TEXTURE => "BIND_BICUBIC_TEXTURE", + BIND_3D_MIP_MAPPED_TEXTURE => "BIND_3D_MIP_MAPPED_TEXTURE", + BIND_3D_TEXTURE => "BIND_3D_TEXTURE", + BIND_DETAIL_TEXTURE => "BIND_DETAIL_TEXTURE", + LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE => "LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE", + MULTI_PURPOSE_DIAGNOSTIC_ENTRY => "MULTI_PURPOSE_DIAGNOSTIC_ENTRY", + SET_VERTEX_ARRAY_COMMAND_TABLE => "SET_VERTEX_ARRAY_COMMAND_TABLE", + VERTEX_ARRAY_SET_UP => "VERTEX_ARRAY_SET_UP", + BEGIN_VERTEX_ARRAY_LOOP => "BEGIN_VERTEX_ARRAY_LOOP", + VERTEX_ARRAY_LOOP_ENTRY_0 => "VERTEX_ARRAY_LOOP_ENTRY_0", + VERTEX_ARRAY_LOOP_ENTRY_1 => "VERTEX_ARRAY_LOOP_ENTRY_1", + VERTEX_ARRAY_LOOP_ENTRY_2 => "VERTEX_ARRAY_LOOP_ENTRY_2", + VERTEX_ARRAY_LOOP_ENTRY_3 => "VERTEX_ARRAY_LOOP_ENTRY_3", + VERTEX_ARRAY_LOOP_ENTRY_4 => "VERTEX_ARRAY_LOOP_ENTRY_4", + VERTEX_ARRAY_LOOP_ENTRY_5 => "VERTEX_ARRAY_LOOP_ENTRY_5", + VERTEX_ARRAY_LOOP_ENTRY_6 => "VERTEX_ARRAY_LOOP_ENTRY_6", + VERTEX_ARRAY_LOOP_ENTRY_7 => "VERTEX_ARRAY_LOOP_ENTRY_7", + VERTEX_ARRAY_LOOP_ENTRY_8 => "VERTEX_ARRAY_LOOP_ENTRY_8", + VERTEX_ARRAY_LOOP_ENTRY_9 => "VERTEX_ARRAY_LOOP_ENTRY_9", + VERTEX_ARRAY_LOOP_ENTRY_10 => "VERTEX_ARRAY_LOOP_ENTRY_10", + VERTEX_ARRAY_LOOP_ENTRY_11 => "VERTEX_ARRAY_LOOP_ENTRY_11", + VERTEX_ARRAY_LOOP_ENTRY_12 => "VERTEX_ARRAY_LOOP_ENTRY_12", + VERTEX_ARRAY_LOOP_ENTRY_13 => "VERTEX_ARRAY_LOOP_ENTRY_13", + VERTEX_ARRAY_LOOP_ENTRY_14 => "VERTEX_ARRAY_LOOP_ENTRY_14", + VERTEX_ARRAY_LOOP_ENTRY_15 => "VERTEX_ARRAY_LOOP_ENTRY_15", + VERTEX_ARRAY_LOOP_ENTRY_16 => "VERTEX_ARRAY_LOOP_ENTRY_16", + VERTEX_ARRAY_LOOP_ENTRY_17 => "VERTEX_ARRAY_LOOP_ENTRY_17", + VERTEX_ARRAY_LOOP_ENTRY_18 => "VERTEX_ARRAY_LOOP_ENTRY_18", + VERTEX_ARRAY_LOOP_ENTRY_19 => "VERTEX_ARRAY_LOOP_ENTRY_19", + VERTEX_ARRAY_LOOP_ENTRY_20 => "VERTEX_ARRAY_LOOP_ENTRY_20", + VERTEX_ARRAY_LOOP_ENTRY_21 => "VERTEX_ARRAY_LOOP_ENTRY_21", + VERTEX_ARRAY_LOOP_ENTRY_22 => "VERTEX_ARRAY_LOOP_ENTRY_22", + VERTEX_ARRAY_LOOP_ENTRY_23 => "VERTEX_ARRAY_LOOP_ENTRY_23", + VERTEX_ARRAY_LOOP_ENTRY_24 => "VERTEX_ARRAY_LOOP_ENTRY_24", + VERTEX_ARRAY_LOOP_ENTRY_25 => "VERTEX_ARRAY_LOOP_ENTRY_25", + VERTEX_ARRAY_LOOP_ENTRY_26 => "VERTEX_ARRAY_LOOP_ENTRY_26", + VERTEX_ARRAY_LOOP_ENTRY_27 => "VERTEX_ARRAY_LOOP_ENTRY_27", + VERTEX_ARRAY_LOOP_ENTRY_28 => "VERTEX_ARRAY_LOOP_ENTRY_28", + VERTEX_ARRAY_LOOP_ENTRY_29 => "VERTEX_ARRAY_LOOP_ENTRY_29", + VERTEX_ARRAY_LOOP_ENTRY_30 => "VERTEX_ARRAY_LOOP_ENTRY_30", + VERTEX_ARRAY_LOOP_ENTRY_31 => "VERTEX_ARRAY_LOOP_ENTRY_31", + VERTEX_ARRAY_TEAR_DOWN => "VERTEX_ARRAY_TEAR_DOWN", + SET_SPRITE_PARAMETER => "SET_SPRITE_PARAMETER", + SET_SPRITE_PARAMETERS => "SET_SPRITE_PARAMETERS", + DISPLAY_GENERATOR_REQUEST_CARRIER => "DISPLAY_GENERATOR_REQUEST_CARRIER", + SET_POINT_PARAMETER => "SET_POINT_PARAMETER", + SET_POINT_PARAMETERS => "SET_POINT_PARAMETERS", + SWAP_TRIANGLE_MESH_VERTEX => "SWAP_TRIANGLE_MESH_VERTEX", + BIND_2D_CLIP_MAP_TEXTURE => "BIND_2D_CLIP_MAP_TEXTURE", + PACKED_VERTEX_POSITION_3_FLOATS => "PACKED_VERTEX_POSITION_3_FLOATS", + PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AC => "PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AC", + PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED => "PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED", + PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AE => "PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AE", + PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_FLOATS => "PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_FLOATS", + PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B0 => "PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B0", + PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B1 => "PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B1", + PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B2 => "PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B2", + PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_SIGNED_SHORTS_IN => "PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_SIGNED_SHORTS_IN", + PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B4 => "PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B4", + PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B5 => "PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B5", + PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B6 => "PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B6", + PACKED_VERTEX_SET_UP => "PACKED_VERTEX_SET_UP", + SET_INSTRUMENTATION_BUFFER => "SET_INSTRUMENTATION_BUFFER", + START_INSTRUMENTATION => "START_INSTRUMENTATION", + STOP_INSTRUMENTATION => "STOP_INSTRUMENTATION", + SET_TEXEL_STORE_PARAMETER => "SET_TEXEL_STORE_PARAMETER", + RESTART_SELECT_OR_FEEDBACK => "RESTART_SELECT_OR_FEEDBACK", + SET_REFERENCE_PLANE => "SET_REFERENCE_PLANE", + SET_COLOUR_NORMAL_MODE_VALUE => "SET_COLOUR_NORMAL_MODE_VALUE", + DEFORMATION_CONTROL => "DEFORMATION_CONTROL", + DEFORMATION_MAP_FLOAT_FORM => "DEFORMATION_MAP_FLOAT_FORM", + DEFORMATION_MAP_DOUBLE_FORM => "DEFORMATION_MAP_DOUBLE_FORM", + CONTINUATION_ENTRY_OF_DOUBLE_DEFORMATION_MAP => "CONTINUATION_ENTRY_OF_DOUBLE_DEFORMATION_MAP", + LOAD_IDENTITY_DEFORMATION => "LOAD_IDENTITY_DEFORMATION", + BIND_ARM_TEXTURE_CACHE_ENTRY => "BIND_ARM_TEXTURE_CACHE_ENTRY", + FREE_ARM_TEXTURE_CACHE_ENTRY => "FREE_ARM_TEXTURE_CACHE_ENTRY", + QUERY_DEFORMATION_STATE => "QUERY_DEFORMATION_STATE", + SET_SHADOW_TEXTURE_STATE => "SET_SHADOW_TEXTURE_STATE", + COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA => "COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA", + SET_TRIANGLE_MESH_MODE => "SET_TRIANGLE_MESH_MODE", + LOAD_ENGINE_TRANSFORM_ATTRIBUTES => "LOAD_ENGINE_TRANSFORM_ATTRIBUTES", + QUERY_ENGINE_TRANSFORM_ATTRIBUTES => "QUERY_ENGINE_TRANSFORM_ATTRIBUTES", + FLUSH_RASTER_BOARDS => "FLUSH_RASTER_BOARDS", + READ_INSTRUMENTATION_COUNTERS => "READ_INSTRUMENTATION_COUNTERS", + SET_LAZY_VALIDATION_MODE => "SET_LAZY_VALIDATION_MODE", + PACKED_VERTEX_CLEAN_UP => "PACKED_VERTEX_CLEAN_UP", + SUBDIVISION_PATCH => "SUBDIVISION_PATCH", + SUBDIVISION_PATCH_SET_UP => "SUBDIVISION_PATCH_SET_UP", + SUBDIVISION_PATCH_CLEAN_UP => "SUBDIVISION_PATCH_CLEAN_UP", + SUBDIVISION_PATCH_PARAMETER => "SUBDIVISION_PATCH_PARAMETER", + BEGIN_PRIMITIVE_SUBDIVISION_PATCH => "BEGIN_PRIMITIVE_SUBDIVISION_PATCH", + BIND_TEXTURE_CURRENT_FORM => "BIND_TEXTURE_CURRENT_FORM", + POP_TEXTURE_GENERATION_STATE => "POP_TEXTURE_GENERATION_STATE", + CHANGE_POST_TEXTURE_SCALE_AND_BIAS => "CHANGE_POST_TEXTURE_SCALE_AND_BIAS", + SKIP_2_WORDS => "SKIP_2_WORDS", + SKIP_4_WORDS => "SKIP_4_WORDS", + SKIP_5_WORDS => "SKIP_5_WORDS", + SWAP_CONTROL_WRITE_SWAP_STROBE => "SWAP_CONTROL_WRITE_SWAP_STROBE", + SET_INPUT_DMA_WAIT_MODE_FLAG => "SET_INPUT_DMA_WAIT_MODE_FLAG", + RAISE_HOST_INTERRUPT_WITH_EVENT_CODE => "RAISE_HOST_INTERRUPT_WITH_EVENT_CODE", + PIPE_MEMORY_RESUME => "PIPE_MEMORY_RESUME", + PIPE_MEMORY_SUSPEND_WITH_EVENT_CODE => "PIPE_MEMORY_SUSPEND_WITH_EVENT_CODE", + INVALIDATE_ARM_TEXTURE_CACHE_INDEX => "INVALIDATE_ARM_TEXTURE_CACHE_INDEX", + PIPE_MEMORY_COPY_DOWN => "PIPE_MEMORY_COPY_DOWN", + PIPE_MEMORY_SWAP_IN => "PIPE_MEMORY_SWAP_IN", + PIPE_MEMORY_SWAP_OUT => "PIPE_MEMORY_SWAP_OUT", + PIPE_MEMORY_RESERVE => "PIPE_MEMORY_RESERVE", + ROLL_CALL_OF_EVERY_ENGINE => "ROLL_CALL_OF_EVERY_ENGINE", + DISPLAY_MODE_BYTE => "DISPLAY_MODE_BYTE", + PUBLISH_RETRACE_SEQUENCE_NUMBER => "PUBLISH_RETRACE_SEQUENCE_NUMBER", + PAYLOAD_PATH_SELF_TEST => "PAYLOAD_PATH_SELF_TEST", + QUERY_BOARD_CONFIGURATION_BLOCK => "QUERY_BOARD_CONFIGURATION_BLOCK", + SYNCHRONISE_PIPE => "SYNCHRONISE_PIPE", + START_INPUT_DMA => "START_INPUT_DMA", + EEPROM_OPERATION => "EEPROM_OPERATION", + DESCRIBE_DMA_PAGE_POOL => "DESCRIBE_DMA_PAGE_POOL", + WINDOW_READ_CLIP_SET_UP => "WINDOW_READ_CLIP_SET_UP", + LOAD_DISTRIBUTOR_VERIFICATION_PROGRAM => "LOAD_DISTRIBUTOR_VERIFICATION_PROGRAM", + DUMP_DISTRIBUTOR_VERIFICATION_STATE => "DUMP_DISTRIBUTOR_VERIFICATION_STATE", + PIPE_SET_UP => "PIPE_SET_UP", + SET_UP_OUTPUT_DMA_FOR_HOST_READ => "SET_UP_OUTPUT_DMA_FOR_HOST_READ", + WINDOW_CLIP_SETUP => "WINDOW_CLIP_SETUP", + DISPLAY_BANK_SET_UP => "DISPLAY_BANK_SET_UP", + START_SELECT_FEEDBACK_RETURN_DMA => "START_SELECT_FEEDBACK_RETURN_DMA", + FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE => "FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE", + CONTEXT_DESTROY => "CONTEXT_DESTROY", + CONTEXT_CLEAN_UP_WITH_EVENT_CODE => "CONTEXT_CLEAN_UP_WITH_EVENT_CODE", + CONTEXT_IMAGE_LOAD => "CONTEXT_IMAGE_LOAD", + CONTEXT_ACTIVATE => "CONTEXT_ACTIVATE", + CONTEXT_FIFO_SYNCHRONISE => "CONTEXT_FIFO_SYNCHRONISE", + CONTEXT_DEACTIVATE => "CONTEXT_DEACTIVATE", + CONTEXT_INITIALISE => "CONTEXT_INITIALISE", + KERNEL_PRIVATE_COMMAND => "KERNEL_PRIVATE_COMMAND", + WRITE_GATHER_FLUSH_LAST_SLOT => "WRITE_GATHER_FLUSH_LAST_SLOT", + _ => "", + } +} + +/// The family of host token `token`, by the table's section. +pub fn token_family(token: u32) -> &'static str { + match token { + BEGIN_PRIMITIVE_RECTANGLE | BEGIN_PRIMITIVE_POINTS | BEGIN_PRIMITIVE_INDEPENDENT_LINES | BEGIN_PRIMITIVE_LINE_LOOP | BEGIN_PRIMITIVE_LINE_STRIP | BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES | BEGIN_PRIMITIVE_TRIANGLE_STRIP | BEGIN_PRIMITIVE_TRIANGLE_FAN | BEGIN_PRIMITIVE_INDEPENDENT_QUADS | BEGIN_PRIMITIVE_QUAD_STRIP | BEGIN_PRIMITIVE_POLYGON | SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_BYTE | SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE | SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_32_BIT_INTEGER | SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_SHORT | SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_BYTE | SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_32_BIT_INTEGER | SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_SHORT | SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_BYTE | SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE | SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_32_BIT_INTEGER | SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_SHORT | SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_BYTE | SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_32_BIT_INTEGER | SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_SHORT | END_OPEN_PRIMITIVE | SET_CURRENT_COLOUR_INDEX_DOUBLE | SET_CURRENT_COLOUR_INDEX_SINGLE_FLOAT | SET_CURRENT_COLOUR_INDEX_SIGNED_32_BIT_INTEGER | SET_CURRENT_COLOUR_INDEX_SIGNED_SHORT | SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_BYTE | SET_CURRENT_NORMAL_3_COMPONENTS_DOUBLE | SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_32_BIT_INTEGER | SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_DOUBLE | SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_32_BIT | SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_DOUBLE | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_32_BIT | SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_DOUBLE | SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_32_BIT | SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SIGNED_SHORT | SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_DOUBLE | SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT | SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_32_BIT | SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SIGNED_SHORT | SUBMIT_VERTEX_2_COMPONENTS_DOUBLE | SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT | SUBMIT_VERTEX_2_COMPONENTS_SIGNED_32_BIT_INTEGER | SUBMIT_VERTEX_2_COMPONENTS_SIGNED_SHORT | SUBMIT_VERTEX_3_COMPONENTS_DOUBLE | SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT | SUBMIT_VERTEX_3_COMPONENTS_SIGNED_32_BIT_INTEGER | SUBMIT_VERTEX_3_COMPONENTS_SIGNED_SHORT | SUBMIT_VERTEX_4_COMPONENTS_DOUBLE | SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT | SUBMIT_VERTEX_4_COMPONENTS_SIGNED_32_BIT_INTEGER | SUBMIT_VERTEX_4_COMPONENTS_SIGNED_SHORT | SWAP_TRIANGLE_MESH_VERTEX | PACKED_VERTEX_POSITION_3_FLOATS | PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AC | PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED | PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1AE | PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_FLOATS | PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B0 | PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B1 | PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B2 | PACKED_VERTEX_POSITION_3_FLOATS_TEXTURE_COORDINATE_2_SIGNED_SHORTS_IN | PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B4 | PACKED_VERTEX_POSITION_3_FLOATS_NORMAL_3_SIGNED_SHORTS_PACKED_TEXTURE_1B5 | PACKED_VERTEX_POSITION_3_FLOATS_COLOUR_4_UNSIGNED_BYTES_IN_ONE_WORD_1B6 | BEGIN_PRIMITIVE_SUBDIVISION_PATCH => "vertex", + SET_SCISSOR_BOX | SET_USER_CLIP_PLANE | SET_DEPTH_RANGE | SELECT_CURRENT_MATRIX_STACK | MULTIPLY_CURRENT_MATRIX_BY_FRUSTUM | LOAD_IDENTITY_MATRIX | LOAD_MATRIX_16_FLOATS | LOAD_MATRIX_16_DOUBLES | MULTIPLY_BY_MATRIX_16_FLOATS | MULTIPLY_BY_MATRIX_16_DOUBLES | MULTIPLY_CURRENT_MATRIX_BY_ORTHOGRAPHIC_PROJECTION | POP_MATRIX_STACK | PUSH_MATRIX_STACK | ROTATE_DOUBLES | ROTATE_FLOATS | SCALE_DOUBLES | SCALE_FLOATS | TRANSLATE_DOUBLES | TRANSLATE_FLOATS | SET_VIEWPORT => "matrix", + SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM | SET_TEXTURE_ENVIRONMENT_PARAMETER_INTEGER_FORM | TEXTURE_COORDINATE_GENERATION_DOUBLE_FORM | TEXTURE_COORDINATE_GENERATION_FLOAT_FORM | TEXTURE_COORDINATE_GENERATION_INTEGER_FORM | BIND_TEXTURE_BY_OLD_STYLE_BINDING | COPY_2D_TEXTURE_IMAGE_FROM_FRAME_BUFFER | BIND_LINEAR_TEXTURE | TEXTURE_LOAD_SET_UP | DRAW_TEXELS_INTO_TEXTURE_MEMORY | TEXTURE_LOAD_BY_IDENTIFIER | SELECT_ACTIVE_TEXTURE_UNIT | BIND_TEXTURE_BY_IDENTIFIER | BIND_BICUBIC_TEXTURE | BIND_3D_MIP_MAPPED_TEXTURE | BIND_3D_TEXTURE | BIND_DETAIL_TEXTURE | LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE | BIND_2D_CLIP_MAP_TEXTURE | SET_TEXEL_STORE_PARAMETER | BIND_ARM_TEXTURE_CACHE_ENTRY | SET_SHADOW_TEXTURE_STATE | BIND_TEXTURE_CURRENT_FORM | POP_TEXTURE_GENERATION_STATE | CHANGE_POST_TEXTURE_SCALE_AND_BIAS | INVALIDATE_ARM_TEXTURE_CACHE_INDEX => "texture", + EVALUATE_1D_GRID_STRIP | EVALUATE_2D_GRID_STRIP | LOAD_1D_EVALUATOR_CONTROL_POINTS | LOAD_2D_EVALUATOR_CONTROL_POINTS | SET_RASTER_POSITION_2_COMPONENTS_DOUBLE | SET_RASTER_POSITION_2_COMPONENTS_FLOAT | SET_RASTER_POSITION_2_COMPONENTS_32_BIT_INTEGER | SET_RASTER_POSITION_2_COMPONENTS_SHORT | SET_RASTER_POSITION_3_COMPONENTS_DOUBLE | SET_RASTER_POSITION_3_COMPONENTS_FLOAT | SET_RASTER_POSITION_3_COMPONENTS_32_BIT_INTEGER | SET_RASTER_POSITION_3_COMPONENTS_SHORT | SET_RASTER_POSITION_4_COMPONENTS_DOUBLE | SET_RASTER_POSITION_4_COMPONENTS_FLOAT | SET_RASTER_POSITION_4_COMPONENTS_32_BIT_INTEGER | SET_RASTER_POSITION_4_COMPONENTS_SHORT | SET_CURRENT_RASTER_POSITION | SET_VERTEX_ARRAY_COMMAND_TABLE | VERTEX_ARRAY_SET_UP | BEGIN_VERTEX_ARRAY_LOOP | VERTEX_ARRAY_LOOP_ENTRY_0 | VERTEX_ARRAY_LOOP_ENTRY_1 | VERTEX_ARRAY_LOOP_ENTRY_2 | VERTEX_ARRAY_LOOP_ENTRY_3 | VERTEX_ARRAY_LOOP_ENTRY_4 | VERTEX_ARRAY_LOOP_ENTRY_5 | VERTEX_ARRAY_LOOP_ENTRY_6 | VERTEX_ARRAY_LOOP_ENTRY_7 | VERTEX_ARRAY_LOOP_ENTRY_8 | VERTEX_ARRAY_LOOP_ENTRY_9 | VERTEX_ARRAY_LOOP_ENTRY_10 | VERTEX_ARRAY_LOOP_ENTRY_11 | VERTEX_ARRAY_LOOP_ENTRY_12 | VERTEX_ARRAY_LOOP_ENTRY_13 | VERTEX_ARRAY_LOOP_ENTRY_14 | VERTEX_ARRAY_LOOP_ENTRY_15 | VERTEX_ARRAY_LOOP_ENTRY_16 | VERTEX_ARRAY_LOOP_ENTRY_17 | VERTEX_ARRAY_LOOP_ENTRY_18 | VERTEX_ARRAY_LOOP_ENTRY_19 | VERTEX_ARRAY_LOOP_ENTRY_20 | VERTEX_ARRAY_LOOP_ENTRY_21 | VERTEX_ARRAY_LOOP_ENTRY_22 | VERTEX_ARRAY_LOOP_ENTRY_23 | VERTEX_ARRAY_LOOP_ENTRY_24 | VERTEX_ARRAY_LOOP_ENTRY_25 | VERTEX_ARRAY_LOOP_ENTRY_26 | VERTEX_ARRAY_LOOP_ENTRY_27 | VERTEX_ARRAY_LOOP_ENTRY_28 | VERTEX_ARRAY_LOOP_ENTRY_29 | VERTEX_ARRAY_LOOP_ENTRY_30 | VERTEX_ARRAY_LOOP_ENTRY_31 | VERTEX_ARRAY_TEAR_DOWN | PACKED_VERTEX_SET_UP | PACKED_VERTEX_CLEAN_UP | SUBDIVISION_PATCH | SUBDIVISION_PATCH_SET_UP | SUBDIVISION_PATCH_CLEAN_UP | SUBDIVISION_PATCH_PARAMETER => "array", + QUERY_CURRENT_VERTEX_ATTRIBUTES | QUERY_CLIP_PLANE | QUERY_RASTER_POSITION_STATE | QUERY_PENDING_ERROR | QUERY_LIGHT_FLOAT | QUERY_MATERIAL_FLOAT | QUERY_PIXEL_MAP_FLOAT | QUERY_POLYGON_STIPPLE | QUERY_STATE_GENERIC | QUERY_TEXTURE_ENVIRONMENT_FLOAT | QUERY_TEXTURE_GENERATION_FLOAT | QUERY_TEXTURE_IMAGE | QUERY_COLOUR_TABLE | QUERY_COLOUR_TABLE_PARAMETERS | QUERY_LINE_MODE_LIMIT_TABLE | QUERY_CONVOLUTION_FILTER | QUERY_SEPARABLE_FILTER | QUERY_PIXEL_TRANSFORM_PARAMETERS_INTEGER | QUERY_HISTOGRAM | QUERY_HISTOGRAM_PARAMETERS | QUERY_MIN_MAX_RESULTS | QUERY_MIN_MAX_PARAMETERS | QUERY_ENGINE_SIDE_TEXTURE_ATTRIBUTES | QUERY_DEFORMATION_STATE | QUERY_ENGINE_TRANSFORM_ATTRIBUTES | QUERY_BOARD_CONFIGURATION_BLOCK => "query", + _ => "state and other", + } +} + +/// Query reply layouts (section 7.16 of the definitions table). A reply is the completion +/// word at byte 0, written last (established), the count of data words at byte 4, then the data +/// from byte 8; the offsets here are into the data, in 32-bit words. +pub mod reply { + /// The current-value reply (0x03b), 13 words: colour (4 floats), colour index, normal (3 + /// floats), texture coordinates (4 floats), edge flag. Established; the edge flag is a float + /// (the client truncates it to an integer, so set is 1.0), its exact bit pattern inferred. + pub const CURRENT_WORDS: usize = 13; + pub const CURRENT_COLOUR: usize = 0; + pub const CURRENT_INDEX: usize = 4; + pub const CURRENT_NORMAL: usize = 5; + pub const CURRENT_TEXTURE: usize = 8; + pub const CURRENT_EDGE_FLAG: usize = 12; + /// The raster-position reply (0x03d), 15 words with no gaps: colour (4 floats), colour + /// index, texture coordinates (4 floats), position (4 floats), valid flag, distance. + /// Established as to the client's offsets and the tiling of the count; the engine + /// handler's push order is not separately decoded. + pub const RASTER_WORDS: usize = 15; + pub const RASTER_COLOUR: usize = 0; + pub const RASTER_INDEX: usize = 4; + pub const RASTER_TEXTURE: usize = 5; + pub const RASTER_POSITION: usize = 9; + pub const RASTER_VALID: usize = 13; + pub const RASTER_DISTANCE: usize = 14; + /// Feedback vertices: the pipe's formatter scales window x and y by 0.0625 after subtracting + /// the window origin and z, after the depth offset, by 2^-20 (constants 0x3d800000 and + /// 0x35800000). Inferred: the order of the values inside a vertex follows GL's by count only, + /// and the engine's own units are taken to be sixteenths of a pixel and the 20-bit window + /// depth range below. + pub const FEEDBACK_XY_SCALE: f32 = 0.0625; + pub const FEEDBACK_Z_SCALE: f32 = 1.0 / 1_048_576.0; + /// The window-depth range constant (0x497fffff) the pipe loads: the engine's depth for a + /// fraction of 1. Inferred to be the range the z scale above divides out. + pub const WINDOW_DEPTH_RANGE: f32 = 1_048_575.0; + /// A select record's depth words are unsigned 32-bit integers, the pipe's depth bounds scaled + /// and clamped at this float (0x4f7fffff). Inferred; which constant multiplies and which + /// clamps, and whether 0 is the near plane, is read from constants alone. + pub const SELECT_DEPTH_MAX: f32 = 4_294_967_040.0; +} + +/// Texture memory addressing (sections 7.12 and 7.13 of the definitions table). +pub mod texture { + use super::BitField; + /// The texel size code in header word 17: the log2 of the texel's width in 16-bit lanes (0 for + /// 16-bit texels, 1 for 32-bit, 2 for a 64-bit slot of which 48 bits are used). Established + /// by two shipped programs; code 3 is not exercised and is read as the same log2 rule. + pub const SIZE_CODE: BitField = BitField::new(1, 0); + /// Bit 3 of header word 17 selects the bank (and with it which of words 18 and 19 holds the + /// level's address). Established. + pub const BANK: BitField = BitField::new(3, 3); + /// A level address counts 256 bits (sixteen 16-bit lanes) per bank. Established for the + /// vendor allocator, inferred for the hardware; the unit of the global-offset registers is + /// unknown. + pub const ADDRESS_UNIT_LANES: u32 = 16; + /// The detail image's texture target, the public enumerant GL_DETAIL_TEXTURE_2D_SGIS. + pub const DETAIL_TARGET: u32 = 0x8095; + /// The volume texture target, the public enumerant GL_TEXTURE_3D (load header word 0). Established. + pub const VOLUME_TARGET: u32 = 0x806f; + + /// Whether a storage code holds one colour channel (luminance, intensity or alpha, alone or with + /// alpha) and not red, green and blue. The pipe's read-back of colour as luminance sums red, green + /// and blue unless its source is such a texture (established as the stage the pipe picks; the + /// clamped sum is inferred). + pub const fn storage_is_one_channel(code: u32) -> bool { + matches!(code, 0x08..=0x0f | 0x20 | 0x28) + } +} + +/// How the pipe treats a colour index (section 7.6 of the definitions table, from the firmware): as +/// a 12-bit component value, left-justified. A write mask for a `w`-bit index is `(2^w - 1) << (12 - w)` +/// on the main level and `(2^w - 1) << 4` on an overlay or popup level, written to raster register +/// 0x1c14 as `m | m << 12`, and the clear index is scaled by `1 << (12 - w)`. So an 8-bit index is +/// its value shifted left 4 (bits 11:4), a 12-bit one fills bits 11:0, and overlay planes sit in bits +/// `[3 + w : 4]`. Established; which component lane holds the index and its offset in the stored bank +/// word are unknown. The draw-pixels path puts an index of 8 bits or fewer at bits 15:8 of each 16-bit +/// lane on a board mode other than 2 and at bits 15:4 (11:4 for an 8-bit index) on board mode 2 or +/// for an index above 8 bits (the shift model is inferred; what board mode 2 is, is unknown). +pub mod colour_index { + /// The write mask of a `width`-bit index (1 to 12) in component space. + pub const fn write_mask(width: u32, overlay: bool) -> u32 { + let ones = (1u32 << width) - 1; + if overlay { ones << 4 } else { ones << (12 - width) } + } + + /// The value written to the colour write-mask register for a component-space mask. + pub const fn write_mask_register(mask: u32) -> u32 { + mask | mask << 12 + } + + /// The factor a clear index is scaled by. + pub const fn clear_scale(width: u32) -> u32 { + 1 << (12 - width) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn an_index_is_a_left_justified_twelve_bit_component() { + assert_eq!(write_mask(8, false), 0xff0, "CI8 is its value shifted left 4"); + assert_eq!(write_mask(12, false), 0xfff); + assert_eq!(write_mask(2, true), 0x30, "overlay planes sit from bit 4"); + assert_eq!(write_mask(8, true), 0xff0); + assert_eq!(write_mask_register(0xff0), 0xff0_ff0); + assert_eq!([clear_scale(8), clear_scale(12), clear_scale(4)], [16, 1, 256]); + } + } +} + +/// The generic state query's rows (token 0x04c, section 7.16 of the definitions table): the 171 names +/// the pipe's firmware answers, with the words of each reply. A name with no row is answered with an +/// empty reply and an invalid-enumerant error. State rows send one word for each element (a byte or +/// halfword zero-extended); a matrix is 16 words; the depth range (0xb70) is two doubles, 4 words, and +/// the depth clear value (0xb73) one double, 2. The eleven callback rows write their own count: the +/// stack depths and polygon offset values are one word, the reference-plane equation (0x817e) four, +/// and the fog function (0x812a) takes its count from the word before the reply (listed as 1). +/// Established. +pub mod state_query { + pub const ROWS: [(u32, u8); 171] = [ + (0x0b10, 1), + (0x0b11, 1), + (0x0b20, 1), + (0x0b21, 1), + (0x0b24, 1), + (0x0b25, 1), + (0x0b26, 1), + (0x0b40, 2), + (0x0b41, 1), + (0x0b42, 1), + (0x0b44, 1), + (0x0b45, 1), + (0x0b46, 1), + (0x0b50, 1), + (0x0b51, 1), + (0x0b52, 1), + (0x0b53, 4), + (0x0b54, 1), + (0x0b55, 1), + (0x0b56, 1), + (0x0b57, 1), + (0x0b60, 1), + (0x0b61, 1), + (0x0b62, 1), + (0x0b63, 1), + (0x0b64, 1), + (0x0b65, 1), + (0x0b66, 4), + (0x0b70, 4), + (0x0b71, 1), + (0x0b72, 1), + (0x0b73, 2), + (0x0b74, 1), + (0x0b80, 4), + (0x0b90, 1), + (0x0b91, 1), + (0x0b92, 1), + (0x0b93, 1), + (0x0b94, 1), + (0x0b95, 1), + (0x0b96, 1), + (0x0b97, 1), + (0x0b98, 1), + (0x0ba0, 1), + (0x0ba1, 1), + (0x0ba2, 4), + (0x0ba3, 1), + (0x0ba4, 1), + (0x0ba5, 1), + (0x0ba6, 16), + (0x0ba7, 16), + (0x0ba8, 16), + (0x0bb0, 1), + (0x0bc0, 1), + (0x0bc1, 1), + (0x0bc2, 1), + (0x0bd0, 1), + (0x0be0, 1), + (0x0be1, 1), + (0x0be2, 1), + (0x0bf0, 1), + (0x0bf1, 1), + (0x0bf2, 1), + (0x0c01, 1), + (0x0c02, 1), + (0x0c10, 4), + (0x0c11, 1), + (0x0c20, 1), + (0x0c21, 1), + (0x0c22, 4), + (0x0c23, 4), + (0x0c50, 1), + (0x0c51, 1), + (0x0c52, 1), + (0x0c53, 1), + (0x0c54, 1), + (0x0c60, 1), + (0x0c61, 1), + (0x0c62, 1), + (0x0c63, 1), + (0x0cb0, 1), + (0x0cb1, 1), + (0x0cb2, 1), + (0x0cb3, 1), + (0x0cb4, 1), + (0x0cb5, 1), + (0x0cb6, 1), + (0x0cb7, 1), + (0x0cb8, 1), + (0x0cb9, 1), + (0x0d70, 1), + (0x0d80, 1), + (0x0de0, 1), + (0x0de1, 1), + (0x2a00, 1), + (0x2a01, 1), + (0x2a02, 1), + (0x3000, 1), + (0x3001, 1), + (0x3002, 1), + (0x3003, 1), + (0x3004, 1), + (0x3005, 1), + (0x4000, 1), + (0x4001, 1), + (0x4002, 1), + (0x4003, 1), + (0x4004, 1), + (0x4005, 1), + (0x4006, 1), + (0x4007, 1), + (0x8005, 4), + (0x8009, 1), + (0x8010, 1), + (0x8011, 1), + (0x8012, 1), + (0x801c, 1), + (0x801d, 1), + (0x801e, 1), + (0x801f, 1), + (0x8020, 1), + (0x8021, 1), + (0x8022, 1), + (0x8023, 1), + (0x8024, 1), + (0x802e, 1), + (0x8037, 1), + (0x8038, 1), + (0x8039, 1), + (0x806f, 1), + (0x8094, 1), + (0x809d, 1), + (0x809e, 1), + (0x809f, 1), + (0x80a0, 1), + (0x80aa, 1), + (0x80ab, 1), + (0x80ac, 1), + (0x80b1, 16), + (0x80b2, 1), + (0x80b4, 1), + (0x80b5, 1), + (0x80b6, 1), + (0x80b7, 1), + (0x80b8, 1), + (0x80b9, 1), + (0x80ba, 1), + (0x80bb, 1), + (0x80bc, 1), + (0x80d0, 1), + (0x80d1, 1), + (0x80d2, 1), + (0x8126, 1), + (0x8127, 1), + (0x8128, 1), + (0x8129, 3), + (0x812a, 1), + (0x812b, 1), + (0x8148, 1), + (0x8149, 1), + (0x814a, 3), + (0x814b, 3), + (0x817d, 1), + (0x817e, 4), + (0x8181, 1), + (0x8183, 1), + (0x8196, 1), + (0x8198, 1), + (0x8199, 4), + (0x81c4, 1), + (0x81c7, 1), + ]; + + /// The words of the reply to a name, or None for a name the firmware has no row for. + pub fn words(name: u32) -> Option { + ROWS.binary_search_by_key(&name, |r| r.0).ok().map(|i| ROWS[i].1 as usize) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn the_rows_are_sorted_and_carry_the_tables_counts() { + assert!(ROWS.windows(2).all(|w| w[0].0 < w[1].0)); + assert_eq!([words(0xba6), words(0xb70), words(0xb73), words(0xc22), words(0x0b44), words(0x817e)], [Some(16), Some(4), Some(2), Some(4), Some(1), Some(4)]); + assert_eq!(words(0xb14), None, "the one name the client asks and the firmware has no row for"); + assert_eq!(words(0x4000), Some(1), "light 0's enable"); + assert_eq!(ROWS.iter().filter(|r| r.1 == 16).count(), 4, "four matrices"); + } + } +} + +/// The multisample registers of the raster back end (section 7.17 of the definitions table). The +/// model draws multisampled windows from its context state, not from these words, so they are the +/// board's register images: what a context's state makes the firmware write, for the monitor and +/// for tests that tie the model's state to the table. +pub mod multisample { + use super::BitField; + + /// The image-processor multisample control word (raster register 0x1c03). Layout established + /// (the vendor diagnostic names the fields); the effects are inferred. + pub const CONTROL_REGISTER: u32 = 0x1c03; + pub const CONTROL_ENABLE: BitField = BitField::new(0, 0); + /// 0 for 4 samples, 1 for 8. + pub const CONTROL_EIGHT_SAMPLES: BitField = BitField::new(1, 1); + /// 0 AND, 1 replace. + pub const CONTROL_MASK_REPLACE: BitField = BitField::new(2, 2); + pub const CONTROL_MASK: BitField = BitField::new(10, 3); + pub const CONTROL_MAINTAIN_TWO: BitField = BitField::new(11, 11); + pub const CONTROL_OPTIMISE: BitField = BitField::new(13, 12); + /// The optimisation field's values: 0 none, 2 disable, 3 enable. The firmware writes 3 with a + /// multisample buffer, 0 for the 12-bit-colour, 8-sample case, and 2 while blending, a logic + /// operation or a partial colour mask is on (and again for the length of a masked clear); why + /// the hardware needs that is not read (values established, meaning inferred). + pub const OPTIMISE_NONE: u32 = 0; + pub const OPTIMISE_DISABLE: u32 = 2; + pub const OPTIMISE_ENABLE: u32 = 3; + + /// The control word the firmware writes for a window: the mask is always all ones and the + /// operation AND; the maintain-two bit is never written (read as zero). + pub const fn control_word(eight_samples: bool, enabled: bool, optimisation: u32) -> u32 { + let w = CONTROL_ENABLE.put(0, enabled as u32); + let w = CONTROL_EIGHT_SAMPLES.put(w, eight_samples as u32); + let w = CONTROL_MASK.put(w, 0xff); + CONTROL_OPTIMISE.put(w, optimisation) + } + + /// The optimisation value (`OPTIMISE_*`) the firmware's multisample configuration write carries. With no + /// multisample buffer it is 0 (none). With one it is 2 (disable) while `restricted` -- blending, a logic + /// operation or a partial colour mask being on -- and otherwise 3 (enable), except 0 for the 12-bit-colour, + /// 8-sample case. That exception belongs to the configuration write alone: the write that undoes a blend, + /// logic-operation or colour-mask disable puts 3 back whatever the colour size, and this function does not + /// describe that write (the table: values established, the effect inferred). The masked-clear case and the + /// colour-mask flag's exception are not parameters. + pub const fn configuration_optimisation(samples: u32, colour_12_bit: bool, restricted: bool) -> u32 { + match (samples, restricted) { + (0, _) => OPTIMISE_NONE, + (_, true) => OPTIMISE_DISABLE, + (8, _) if colour_12_bit => OPTIMISE_NONE, + _ => OPTIMISE_ENABLE, + } + } + + /// The primitive sampling word (raster register 0x0806). The values are established, and the + /// vendor diagnostic's field names settle which field is which class: the polygon fields are + /// bits 3:2 and 15:14, the line fields bits 1:0, 9:8 and 13:12, and the enable bits 17:16; bits + /// 11:10 are never written. + pub const SAMPLING_REGISTER: u32 = 0x0806; + pub const SAMPLING_LINE_MASK: BitField = BitField::new(1, 0); + pub const SAMPLING_POLYGON_MASK: BitField = BitField::new(3, 2); + pub const SAMPLING_SMOOTH_LINE_MASK: BitField = BitField::new(9, 8); + pub const SAMPLING_LINE_SAMPLE: BitField = BitField::new(13, 12); + pub const SAMPLING_POLYGON_SAMPLE: BitField = BitField::new(15, 14); + pub const SAMPLING_ENABLE: BitField = BitField::new(17, 16); + + /// The word as the firmware leaves it for a context: the enable and the two classes. `buffer` is + /// a multisample buffer being present. The firmware writes the line fields only when smoothing + /// is on, or multisampling is and smoothing is not; otherwise it leaves them, and here they + /// read as zero (a stateless image). + pub const fn sampling_word(enabled: bool, buffer: bool, polygon_smooth: bool, line_smooth: bool) -> u32 { + let mut w = SAMPLING_ENABLE.put(0, enabled as u32); + w = SAMPLING_POLYGON_MASK.put(w, 1); + let ms = enabled && buffer; + w = SAMPLING_POLYGON_SAMPLE.put(w, if ms { 1 } else if polygon_smooth { 2 } else { 0 }); + if line_smooth { + w = SAMPLING_LINE_MASK.put(w, 1); + w = SAMPLING_SMOOTH_LINE_MASK.put(w, 0); + w = SAMPLING_LINE_SAMPLE.put(w, 2); + } else if ms { + w = SAMPLING_LINE_MASK.put(w, 2); + w = SAMPLING_SMOOTH_LINE_MASK.put(w, 1); + w = SAMPLING_LINE_SAMPLE.put(w, 0); + } + w + } + + /// The most leading ones the depth clear word's count field holds. + pub const DEPTH_CLEAR_MAX_ONES: u32 = 14; + + /// The 32-bit fraction the clear word is built from: the depth times 2^32, less one, which is all + /// ones for 1.0 and above and zero for zero and below. Established; the rounding is not read, so a + /// fraction is truncated here (inferred). + pub fn depth_clear_fraction(depth: f64) -> u32 { + if depth >= 1.0 { + u32::MAX + } else { + (depth * 4_294_967_296.0 - 1.0).clamp(0.0, 4_294_967_295.0) as u32 + } + } + + /// The depth clear value (raster register 0x1c09) for a depth other than 32 bits, from the clear + /// depth's 32-bit fraction `f` (`depth_clear_fraction`). The word's top four bits count the leading + /// ones of `f`, at most 14. After those ones the terminating zero is dropped, unless the count + /// reached 14, and the next bits are the mantissa; its split depends on the sample count. With a + /// nonzero sample count the next 12 bits go to 27:16, `f[20:13]` to 7:0 and 0xf000 is set; with + /// none the next 11 bits go to 27:17 and `f[20:13]` to 15:8. Established. + pub const fn depth_clear_word(f: u32, sampled: bool) -> u32 { + let ones = if f.leading_ones() < DEPTH_CLEAR_MAX_ONES { f.leading_ones() } else { DEPTH_CLEAR_MAX_ONES }; + let after = if ones < DEPTH_CLEAR_MAX_ONES { f << (ones + 1) } else { f << DEPTH_CLEAR_MAX_ONES }; + let low = f >> 13 & 0xff; + let w = ones << 28; + if sampled { + w | (after >> 20) << 16 | 0xf000 | low + } else { + w | (after >> 21) << 17 | low << 8 + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn the_control_word_follows_the_firmwares_writes() { + // Four samples, enabled, optimisation 3: bit 0, mask 0xff in 10:3, 3 in 13:12. + assert_eq!(control_word(false, true, OPTIMISE_ENABLE), 1 | 0xff << 3 | 3 << 12); + assert_eq!(control_word(true, true, OPTIMISE_DISABLE), 1 | 2 | 0xff << 3 | 2 << 12); + assert_eq!(control_word(false, false, 0), 0xff << 3); + } + + #[test] + fn the_configuration_optimisation_is_forced_off_under_blend_logic_op_and_colour_mask() { + assert_eq!(configuration_optimisation(4, false, false), OPTIMISE_ENABLE); + assert_eq!(configuration_optimisation(4, false, true), OPTIMISE_DISABLE); + assert_eq!(configuration_optimisation(8, true, false), OPTIMISE_NONE, "the 12-bit-colour, 8-sample case"); + assert_eq!(configuration_optimisation(8, true, true), OPTIMISE_DISABLE, "blending writes 2 whatever the colour size"); + assert_eq!(configuration_optimisation(8, false, true), OPTIMISE_DISABLE); + assert_eq!(configuration_optimisation(0, false, true), OPTIMISE_NONE, "no multisample buffer"); + } + + #[test] + fn the_primitive_sampling_word_follows_the_enable_and_smoothing() { + // Multisampled, no smoothing: polygons 1 and 1, lines 2, 1 and 0, enable 1. + let w = sampling_word(true, true, false, false); + assert_eq!([SAMPLING_ENABLE.get(w), SAMPLING_POLYGON_MASK.get(w), SAMPLING_POLYGON_SAMPLE.get(w)], [1, 1, 1]); + assert_eq!([SAMPLING_LINE_MASK.get(w), SAMPLING_SMOOTH_LINE_MASK.get(w), SAMPLING_LINE_SAMPLE.get(w)], [2, 1, 0]); + // Not multisampled, smooth polygons and lines. + let w = sampling_word(false, true, true, true); + assert_eq!([SAMPLING_ENABLE.get(w), SAMPLING_POLYGON_SAMPLE.get(w)], [0, 2]); + assert_eq!([SAMPLING_LINE_MASK.get(w), SAMPLING_SMOOTH_LINE_MASK.get(w), SAMPLING_LINE_SAMPLE.get(w)], [1, 0, 2]); + // Nothing on: only the polygon mask. + assert_eq!(sampling_word(false, false, false, false), 1 << 2); + // Enabled but no buffer behaves as not multisampled. + assert_eq!(SAMPLING_POLYGON_SAMPLE.get(sampling_word(true, false, false, false)), 0); + } + + #[test] + fn the_depth_clear_fraction_is_the_depth_times_two_to_the_32_less_one() { + assert_eq!(depth_clear_fraction(1.0), u32::MAX); + assert_eq!(depth_clear_fraction(2.0), u32::MAX); + assert_eq!(depth_clear_fraction(0.0), 0); + assert_eq!(depth_clear_fraction(0.5), 0x7fff_ffff); + assert_eq!(depth_clear_fraction(0.75), 0xbfff_ffff); + } + + /// The count is the leading ones of the fraction, capped at 14; the terminating zero goes unless the + /// cap was reached; then the mantissa splits by the sample count. + #[test] + fn the_depth_clear_word_counts_leading_ones_and_splits_the_mantissa() { + // 0xdeadbeef starts 110: two ones, the zero dropped, then 1 1110 1010 1101 ...; f[20:13] is 0x6d. + assert_eq!(depth_clear_word(0xdead_beef, true), 2 << 28 | 0xf56 << 16 | 0xf000 | 0x6d); + assert_eq!(depth_clear_word(0xdead_beef, false), 2 << 28 | 0x7ab << 17 | 0x6d << 8); + // No leading one: the count is zero and only the zero goes. + assert_eq!(depth_clear_word(0x7fff_ffff, true), 0xfff << 16 | 0xf000 | 0xff); + // Depth 1.0 is all ones: the count caps at 14 and no zero is dropped, so the next 12 bits are ones. + assert_eq!(depth_clear_word(depth_clear_fraction(1.0), true), 14 << 28 | 0xfff << 16 | 0xf000 | 0xff); + assert_eq!(depth_clear_word(depth_clear_fraction(1.0), false), 14 << 28 | 0x7ff << 17 | 0xff << 8); + // Exactly 14 ones then a zero keeps the zero in the mantissa, since the cap was reached. + assert_eq!(depth_clear_word(0xfffc_0000, true), 14 << 28 | 0xf000 | 0xe0); + // Fifteen ones also cap at 14, and the next bit (a one) is mantissa. + assert_eq!(depth_clear_word(0xfffe_0000, true), 14 << 28 | 0x800 << 16 | 0xf000 | 0xf0); + // Zero is no ones, and nothing after. + assert_eq!(depth_clear_word(0, true), 0xf000); + assert_eq!(depth_clear_word(0, false), 0); + } + } +} + +/// GL pixel operations: parameter names, format and type codes and the accumulation +/// operations, as the pixel-state tokens and the pixel headers carry them (section 7.15 of the +/// definitions table). The packed type enumerants are GL's public ones. +pub mod pixel { + /// Pixel-store names: the unpack group starts at `UNPACK_BASE`, the pack group at + /// `PACK_BASE`, each six names in the order swap bytes, least-significant-bit first, row + /// length, skip rows, skip pixels, alignment; the volume names follow. + pub const UNPACK_BASE: u32 = 0xcf0; + pub const PACK_BASE: u32 = 0xd00; + pub const STORE_NAMES: u32 = 6; + pub const UNPACK_SKIP_IMAGES: u32 = 0x806d; + pub const UNPACK_IMAGE_HEIGHT: u32 = 0x806e; + pub const UNPACK_SKIP_VOLUMES: u32 = 0x8132; + pub const UNPACK_IMAGE_DEPTH: u32 = 0x8133; + pub const PACK_SKIP_IMAGES: u32 = 0x806b; + pub const PACK_IMAGE_HEIGHT: u32 = 0x806c; + pub const PACK_SKIP_VOLUMES: u32 = 0x8130; + pub const PACK_IMAGE_DEPTH: u32 = 0x8131; + /// Pixel-transfer names (value a float unless noted). + pub const MAP_COLOUR: u32 = 0xd10; + pub const MAP_STENCIL: u32 = 0xd11; + pub const INDEX_SHIFT: u32 = 0xd12; + pub const INDEX_OFFSET: u32 = 0xd13; + pub const RED_SCALE: u32 = 0xd14; + pub const RED_BIAS: u32 = 0xd15; + pub const GREEN_SCALE: u32 = 0xd18; + pub const GREEN_BIAS: u32 = 0xd19; + pub const BLUE_SCALE: u32 = 0xd1a; + pub const BLUE_BIAS: u32 = 0xd1b; + pub const ALPHA_SCALE: u32 = 0xd1c; + pub const ALPHA_BIAS: u32 = 0xd1d; + pub const DEPTH_SCALE: u32 = 0xd1e; + pub const DEPTH_BIAS: u32 = 0xd1f; + /// Post-convolution scales (red to alpha) then biases, and post-colour-matrix scales then biases. + pub const POST_CONVOLUTION_SCALE: u32 = 0x801c; + pub const POST_CONVOLUTION_BIAS: u32 = 0x8020; + pub const POST_COLOUR_MATRIX_SCALE: u32 = 0x80b4; + pub const POST_COLOUR_MATRIX_BIAS: u32 = 0x80b8; + /// Pixel maps, by name. + pub const MAP_INDEX_TO_INDEX: u32 = 0xc70; + pub const MAP_STENCIL_TO_STENCIL: u32 = 0xc71; + pub const MAP_INDEX_TO_RED: u32 = 0xc72; + pub const MAP_RED_TO_RED: u32 = 0xc76; + /// The pixel-mode suspend token's selector values and mode bits. + /// (The pixel-mode word of this token is the colour-matrix classification: a 16-bit map with bit + /// 4 x row + column set when that element is exactly 1.0 and every other is 0, 0x8421 for the + /// identity, 0xffff for any other matrix; the suspend token sets 0x8421. Established, orientation inferred.) + pub const SUSPEND_USE_TEXEL_COPIES: u32 = 2; + pub const SUSPEND_USE_DEFAULT_RECORDS: u32 = 4; + pub const SUSPEND_USE_LIST_RECORD: u32 = 0x20; + pub const SUSPEND_SWITCH_RECORDS: u32 = 1 << 29; + pub const SUSPEND_SAVE: u32 = 1 << 30; + /// Format codes. + pub const FORMAT_RED: u32 = 0; + pub const FORMAT_GREEN: u32 = 1; + pub const FORMAT_BLUE: u32 = 2; + pub const FORMAT_ALPHA: u32 = 3; + pub const FORMAT_RGB: u32 = 4; + pub const FORMAT_RGBA: u32 = 5; + pub const FORMAT_LUMINANCE: u32 = 6; + pub const FORMAT_LUMINANCE_ALPHA: u32 = 7; + /// Codes 8 to 10 are the extension formats 0x8000 (ABGR, four components), 0x800c (CMYK, four) and + /// 0x800d (CMYKA, five). The client's format table is indexed by the public format less 0x1900 for + /// 0x1900 to 0x190a, and the three extension enumerants index its slots 11 to 13, whose entries are + /// these codes. The pipe's component table by format code is 1, 1, 1, 1, 3, 4, 1, 2, 4, 4, 5, 1, 1, + /// 1 and 0 for code 14. Established. + pub const FORMAT_ABGR: u32 = 8; + pub const FORMAT_CMYK: u32 = 9; + pub const FORMAT_CMYKA: u32 = 10; + pub const FORMAT_COLOUR_INDEX: u32 = 11; + pub const FORMAT_STENCIL: u32 = 12; + pub const FORMAT_DEPTH: u32 = 13; + /// Type codes: 0 to 6 are signed byte, unsigned byte, signed short, unsigned short, signed + /// int, unsigned int and float; 7 is the one-bit bitmap; 8 to 12 are the packed types. + pub const TYPE_BYTE: u32 = 0; + pub const TYPE_UNSIGNED_BYTE: u32 = 1; + pub const TYPE_SHORT: u32 = 2; + pub const TYPE_UNSIGNED_SHORT: u32 = 3; + pub const TYPE_INT: u32 = 4; + pub const TYPE_UNSIGNED_INT: u32 = 5; + pub const TYPE_FLOAT: u32 = 6; + pub const TYPE_BITMAP: u32 = 7; + pub const TYPE_3_3_2: u32 = 8; + pub const TYPE_4_4_4_4: u32 = 9; + pub const TYPE_5_5_5_1: u32 = 10; + pub const TYPE_8_8_8_8: u32 = 11; + pub const TYPE_10_10_10_2: u32 = 12; + /// Copy types. + pub const COPY_COLOUR: u32 = 0x1800; + pub const COPY_DEPTH: u32 = 0x1801; + pub const COPY_STENCIL: u32 = 0x1802; + /// Accumulation operations, GL's enumerants. The engine's accumulation operation register + /// (raster register 0x1c0e) holds the value in bits 12:0, the operation (the enumerant minus + /// 0x100: accumulate 0, load 1, return 2, multiply 3, add 4) in bits 15:13, a return-by-four + /// flag in bit 16, a precision flag in bit 17, a speed-up flag in bit 18 and a three-bit + /// low-order extension of the value in bits 21:19 (established from the diagnostic's table). + /// A value of exactly 1.0 is two passes, since it does not fit the value field (inferred). + pub const ACCUM: u32 = 0x100; + pub const ACCUM_LOAD: u32 = 0x101; + pub const ACCUM_RETURN: u32 = 0x102; + pub const ACCUM_MULT: u32 = 0x103; + pub const ACCUM_ADD: u32 = 0x104; + /// A small read's reply goes in the return page when it is not larger than this. + pub const READ_REPLY_LIMIT: usize = 0x3ff0; + /// A small bitmap's pixel count limit (token 0x005). + pub const SMALL_BITMAP_PIXELS: u32 = 0xc40; + /// The overlap query's reply bit. + pub const OVERLAP_BIT: u32 = 1 << 2; +} diff --git a/src/dev/kona/dma.rs b/src/dev/kona/dma.rs new file mode 100644 index 000000000..27205860a --- /dev/null +++ b/src/dev/kona/dma.rs @@ -0,0 +1,380 @@ +//! The pipe's DMA and display lists. +//! +//! A DMA packet is the selector 0x1eb and six words: +//! +//! 1. `io >> 12` +//! 2. `(io & 0xfff) | 0x9000` +//! 3. the descriptor's total byte count +//! 4. its per-line byte count +//! 5. its gap, the bytes between one line's end and the next line +//! 6. `0x100` when the sender wants a completion, else 0 +//! +//! where `io` is the device address of the pinned buffer; the kick register +//! takes literals around the packet as a bracket and is not a word count. +//! Before a packet, host software sends a record, 0x1c8 with +//! a pipe-memory address and a byte count, when a display list is copied into +//! pipe memory. A record says where the next packet's bytes go, so the engine +//! holds it and places that packet's transfer in the list memory. With no +//! record the packet's bytes feed whatever the back end is waiting for +//! (an image, a texture, a bitmap); with neither, the packet is logged +//! without transferring bytes or posting a completion. +//! +//! The completion's mailbox word comes from the packet's word 6; that the +//! pipe posts the packet's own last word is inferred. +//! +//! Display lists live in pipe memory (the list memory) or in the DMA pool +//! the driver registered with 0x1ed. An invocation (0x16d, 0x16e, 0x170, +//! 0x171) fetches (offset, word count) ranges and feeds the words to the +//! command page's decoder, as a compiled list holds the words immediate mode +//! writes there. Pipe-memory offsets count from the start of the context's +//! display-list region, which the kernel's 0x1e4 packets place (inferred). +//! +//! Approximated: a pipe-memory address is a byte offset into the list memory; +//! how the pipe places the display-list region the kernel reserves is +//! unknown, so the list memory is that region, from offset 0. The packet +//! names no destination widget; the machine's DMA handle chooses one. +//! +//! Ported from hw/misc/sgi-kona-dma.c in jamesbraid/qemu-sgi-origami. + +use super::defs::*; +use super::front::Host; +use super::back::KonaBackend; +use super::job::{Job, Reply}; +use super::pipe::{Core, Run}; +use crate::devlog::LogModule; + +pub(super) struct Dma { + /// The display-list memory, big-endian words as the guest wrote them. + pub list: Box<[u8]>, + record_pending: bool, + record_dest: u32, + record_bytes: u32, + pool: Option<(u64, u32)>, + /// Where the current context's display-list region starts in the list + /// memory, in bytes. + pub list_base: u32, + /// A list is running: an invocation from inside it fails. + in_list: bool, + /// The words of the list being invoked, and the bytes a fetch reads, + /// kept between invocations. + words: Vec, + bytes: Vec, +} + +impl Dma { + pub fn new() -> Self { + Dma { list: vec![0u8; LIST_DEFAULT].into_boxed_slice(), record_pending: false, record_dest: 0, record_bytes: 0, pool: None, list_base: 0, in_list: false, words: Vec::new(), bytes: Vec::new() } + } + + /// The display-list memory and the registration the guest made, as the stream + /// module reads and writes them (a list is not running when it is saved). + /// Whether a display list is running. + pub fn running(&self) -> bool { + self.in_list + } + + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { list: _, record_pending: _, record_dest: _, record_bytes: _, pool: _, list_base: _, in_list: _, words: _, bytes: _ } = self; + s.bytes(&mut self.list); + s.bool(&mut self.record_pending); + s.u32(&mut self.record_dest); + s.u32(&mut self.record_bytes); + s.opt(&mut self.pool, |s, p| { + s.u64(&mut p.0); + s.u32(&mut p.1); + }); + s.u32(&mut self.list_base); + if s.loading() { + self.in_list = false; + } + } + + pub fn reset(&mut self) { + // Fresh, zeroed pages: nothing a list left before the reset can run. + self.list = vec![0u8; LIST_DEFAULT].into_boxed_slice(); + self.record_pending = false; + self.record_dest = 0; + self.record_bytes = 0; + self.pool = None; + self.list_base = 0; + self.in_list = false; + } +} + +impl Core { + /// A command handed to the engine: the packet, the list-write record, + /// the pool registration or a display-list packet. + pub(super) fn dma_command(&mut self, h: &Host, be: &mut KonaBackend, selector: u32, a: &[u32]) { + match selector { + START_INPUT_DMA => self.dma_packet(h, be, a), + DESCRIBE_DMA_PAGE_POOL => self.dma_register_pool(a), + EXECUTE_LIST_BY_LIST_DMA => self.dma_invoke(h, be, false, &a[..2]), + EXECUTE_LIST_BY_INPUT_DMA => self.dma_invoke(h, be, true, &a[..2]), + EXECUTE_LIST_BY_LIST_DMA_REPEATED | EXECUTE_LIST_BY_INPUT_DMA_REPEATED => { + // 0x170 n or 0x171 n: n pairs follow. An n outside 1 to the + // packet's limit is logged and opens nothing, so the pairs + // are read as fresh words. + let max = if selector == EXECUTE_LIST_BY_LIST_DMA_REPEATED { LIST_CALLS_MAX } else { LIST_CALLS_POOL_MAX }; + if a[0] == 0 || a[0] > max { + crate::dlog!(LogModule::Kona, "kona: list invocation {selector:#x} names {} ranges, not 1 to {max}", a[0]); + return; + } + self.input.run = Run::Calls { token: selector, need: 2 * a[0] as usize, words: Vec::new() }; + } + COPY_USER_LIST_BY_LIST_DMA => self.dma_pull(h, a), + MOVE_LIST_BY_LIST_DMA => self.dma_move(a), + COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA => { + if self.dma.record_pending { + crate::dlog!(LogModule::Kona, "kona: dma list write {:#x} bytes at {:#x} replaces one no packet took", a[1], a[0]); + } + self.dma.record_pending = true; + self.dma.record_dest = a[0].saturating_add(self.dma.list_base); + self.dma.record_bytes = a[1]; + } + _ => unreachable!("selector {selector:#x} is not a DMA command"), + } + } + + /// The packet, words 1 to 6. Every size is the guest's: the per-line + /// count must be whole words and divide the total, and the buffer the + /// lines and gaps span must fit the model's bound, or the packet is + /// refused with its completion withheld. + fn dma_packet(&mut self, h: &Host, be: &mut KonaBackend, w: &[u32]) { + let record = self.dma.record_pending; + self.dma.record_pending = false; + let done = match w[5] { + DMA_DONE_WORD => true, + 0 => false, + other => { + crate::dlog!(LogModule::Kona, "kona: dma word 6 is {other:#x}, not 0 or {DMA_DONE_WORD:#x}; no completion posted"); + false + } + }; + if w[1] & !0xfff != DMA_ADDR_TAG { + crate::dlog!(LogModule::Kona, "kona: dma word 2 ({:#010x}) carries tag {:#x}, expected {DMA_ADDR_TAG:#x}; refusing", w[1], w[1] & !0xfff); + return; + } + // A high word with bit 28 clear is `io >> 12` of a device address the + // map RAM translates (inferred); with bit 28 set it names an absolute + // host frame, the form the kernel's own pages use. + let physical = w[0] & PAGE_WORD_PHYSICAL != 0; + let io = if physical { ((w[0] & PAGE_WORD_FRAME) as u64) << 12 } else { (w[0] as u64) << 12 } | (w[1] & 0xfff) as u64; + let (total, per_line, gap) = (w[2], w[3], w[4]); + if per_line == 0 || total == 0 || total % per_line != 0 || (record && per_line % 4 != 0) { + crate::dlog!(LogModule::Kona, "kona: dma of {total:#x} bytes in lines of {per_line:#x} is not whole lines of whole words; refusing"); + return; + } + let lines = total / per_line; + let pitch = per_line as u64 + gap as u64; + let span = (io & (DMA_PAGE_SIZE - 1)) + (lines as u64 - 1) * pitch + per_line as u64; + if span > (64 << 20) { + crate::dlog!(LogModule::Kona, "kona: dma of {lines} lines at a pitch of {pitch:#x} spans more than 64 MiB; refusing"); + return; + } + let completed = if record { + self.dma_to_list(h, io, physical, lines, total) + } else { + self.dma_fault = false; + match self.job(h, be, Job::DmaIn { io, physical, lines, per_line, pitch }) { + Reply::Unclaimed => { + crate::dlog!(LogModule::Kona, "kona: dma packet with no list record and nothing waiting for pixels; no completion"); + false + } + _ => !self.dma_fault, + } + }; + if completed && done { + h.post_mailbox(DMA_DONE_WORD); + } + } + + /// The list end of a packet: its bytes, one line of them, at the pending + /// record's pipe-memory address. The kernel's one DMA-write sender sends + /// the record's count as the packet's total and per-line count with no + /// gap, so anything else is refused rather than guessing how a pipe joins + /// lines or reconciles counts. + fn dma_to_list(&mut self, h: &Host, io: u64, physical: bool, lines: u32, total: u32) -> bool { + let (dest, bytes) = (self.dma.record_dest as usize, self.dma.record_bytes); + if lines != 1 || bytes != total { + crate::dlog!(LogModule::Kona, "kona: dma of {lines} lines, {total:#x} bytes, for a list write of {bytes:#x} bytes; refusing"); + return false; + } + let len = self.dma.list.len(); + if dest % 4 != 0 || total % 4 != 0 || dest > len || total as usize > len - dest { + crate::dlog!( + LogModule::Kona, + "kona: {total:#x} bytes at pipe-memory {dest:#x} are not whole words inside the {len:#x}-byte list memory; refusing" + ); + return false; + } + let mut buf = vec![0u8; total as usize]; + let read = if physical { h.dma_read(io, &mut buf) } else { self.device_read(h, io, &mut buf) }; + if !read { + crate::dlog!(LogModule::Kona, "kona: host read of {total:#x} bytes at {io:#x} failed; completion withheld"); + return false; + } + self.dma.list[dest..dest + total as usize].copy_from_slice(&buf); + true + } + + /// 0x1ed page tagged-offset length: the DMA pool the kernel pinned, which + /// list invocations 0x16e and 0x171 and the pull 0x167 read by byte + /// offset. The kernel's pool setup requires base and length to be + /// multiples of the pool page, and its pool update sends a length of 1 + /// for a context with no pool, so a range that is not whole pages leaves + /// no pool registered, silently. A word 2 without the 0x9000 tag, or a + /// pool longer than the model's bound, is logged as well. The pool is one + /// range of the device address space; each page goes through its map RAM + /// entry when the pool is read, which is the model's choice: nothing shows + /// when the pipe reads the entries. + fn dma_register_pool(&mut self, a: &[u32]) { + let base = (a[0] as u64) << 12 | (a[1] & 0xfff) as u64; + self.dma.pool = None; + if a[1] & !0xfff != DMA_ADDR_TAG || a[2] > DMA_POOL_MAX { + crate::dlog!( + LogModule::Kona, + "kona: dma pool {:#x} {:#x} of {:#x} bytes is not tagged, or is longer than {DMA_POOL_MAX:#x} bytes; no pool", + a[0], a[1], a[2] + ); + } else if base.is_multiple_of(DMA_POOL_PAGE as u64) && a[2] != 0 && a[2].is_multiple_of(DMA_POOL_PAGE) { + self.dma.pool = Some((base, a[2])); + } + } + + /// Fetch `nwords` words at byte `offset` of the pool or of the list + /// memory (relative to the region base). + fn fetch_words(&mut self, h: &Host, pool: bool, offset: u32, nwords: u32, out: &mut Vec) -> bool { + let mut buf = std::mem::take(&mut self.dma.bytes); + let ok = self.fetch_into(h, pool, offset, nwords, &mut buf); + if ok { + out.extend(buf.chunks_exact(4).map(|c| u32::from_be_bytes(c.try_into().unwrap()))); + } + self.dma.bytes = buf; + ok + } + + fn fetch_into(&self, h: &Host, pool: bool, offset: u32, nwords: u32, buf: &mut Vec) -> bool { + let bytes = nwords as usize * 4; + buf.clear(); + buf.resize(bytes, 0); + if pool { + let Some((base, len)) = self.dma.pool else { + crate::dlog!(LogModule::Kona, "kona: list invoked from the pool with no pool registered"); + return false; + }; + if offset as u64 + bytes as u64 > len as u64 { + crate::dlog!(LogModule::Kona, "kona: pool range {offset:#x}+{bytes:#x} leaves the {len:#x}-byte pool"); + return false; + } + if !self.device_read(h, base + offset as u64, buf) { + return false; + } + } else { + let at = self.dma.list_base as u64 + offset as u64; + if at % 4 != 0 || at + bytes as u64 > self.dma.list.len() as u64 { + crate::dlog!(LogModule::Kona, "kona: list range {at:#x}+{bytes:#x} leaves the list memory"); + return false; + } + buf.copy_from_slice(&self.dma.list[at as usize..at as usize + bytes]); + } + true + } + + /// Run n (offset, word count) ranges as one list: from the pool, or from + /// pipe memory. The words are the list's as of this invocation. A list + /// that invokes a list finds the engine busy and fails; the outer list + /// goes on. What the driver observes at a list's end is unknown, so + /// nothing is posted. + pub(super) fn dma_invoke(&mut self, h: &Host, be: &mut KonaBackend, pool: bool, pairs: &[u32]) { + if self.dma.in_list { + crate::dlog!(LogModule::Kona, "kona: a list invoked from inside a list fails"); + return; + } + let mut words = std::mem::take(&mut self.dma.words); + words.clear(); + for p in pairs.chunks_exact(2) { + if p[1] > LIST_MAX_WORDS || words.len() as u64 + p[1] as u64 > LIST_MAX_WORDS as u64 { + crate::dlog!(LogModule::Kona, "kona: list of {} words is longer than the display-list class; refusing", p[1]); + self.dma.words = words; + return; + } + if !self.fetch_words(h, pool, p[0], p[1], &mut words) { + self.dma.words = words; + return; + } + } + self.dma.in_list = true; + h.list_running.store(true, std::sync::atomic::Ordering::SeqCst); + for &w in &words { + self.command_word(h, be, w); + } + self.dma.words = words; + h.list_running.store(false, std::sync::atomic::Ordering::SeqCst); + self.dma.in_list = false; + } + + /// 0x167 dest src bytes 0: the client library's copy of a list block from + /// the pool into pipe memory. What its fourth word does is unknown; the + /// one sender sends 0. + fn dma_pull(&mut self, h: &Host, a: &[u32]) { + if self.dma.pool.is_none() { + crate::dlog!(LogModule::Kona, "kona: list pull with no pool registered"); + return; + } + let (dest, bytes) = (self.dma.list_base as u64 + a[0] as u64, a[2] as usize); + if dest % 4 != 0 || bytes % 4 != 0 || dest + bytes as u64 > self.dma.list.len() as u64 { + crate::dlog!(LogModule::Kona, "kona: {bytes:#x} bytes at pipe-memory {dest:#x} are not whole words inside the list memory; refusing"); + return; + } + let mut words = Vec::new(); + if !self.fetch_words(h, true, a[1], (bytes / 4) as u32, &mut words) { + return; + } + for (i, w) in words.iter().enumerate() { + self.dma.list[dest as usize + 4 * i..][..4].copy_from_slice(&w.to_be_bytes()); + } + } + + /// 0x16f dest src length: the client library's pipe-memory block + /// allocator's move, within the list memory. That the length counts bytes + /// is inferred from its caller's other sizes; overlapping ranges move as + /// `memmove` does. + fn dma_move(&mut self, a: &[u32]) { + let base = self.dma.list_base as u64; + let (dest, src, len) = (base + a[0] as u64, base + a[1] as u64, a[2] as u64); + let total = self.dma.list.len() as u64; + if dest + len > total || src + len > total { + crate::dlog!(LogModule::Kona, "kona: list move of {len:#x} bytes from {src:#x} to {dest:#x} leaves the list memory; dropped"); + return; + } + self.dma.list.copy_within(src as usize..(src + len) as usize, dest as usize); + } + + /// 0x1e4 class key offset size: the kernel's placement of one of the + /// current context's pipe-memory regions, offset and size in granules. + /// The library's display-list offsets count from the start of its + /// display-list allocation, class 1 key 0, so two contexts each using + /// offset 0 must not meet in pipe memory. The model therefore adds that + /// region's offset to every list-memory address. Inferred: that the pipe + /// relocates the offsets by the region the kernel placed. A freed region + /// (size 0) leaves no base. + pub(super) fn dma_list_region(&mut self, a: &[u32]) { + if a[0] != 1 || a[1] != 0 { + return; + } + if a[3] == 0 || a[2] > LIST_MAX_WORDS * 4 / LIST_GRANULE { + self.dma.list_base = 0; + return; + } + self.dma.list_base = a[2] * LIST_GRANULE; + } +} + +impl Core { + /// For tests and the monitor: the list memory word at byte `at`. + #[cfg(test)] + pub(super) fn list_word(&self, at: usize) -> u32 { + u32::from_be_bytes(self.dma.list[at..at + 4].try_into().unwrap()) + } +} diff --git a/src/dev/kona/flash.rs b/src/dev/kona/flash.rs new file mode 100644 index 000000000..3760a9532 --- /dev/null +++ b/src/dev/kona/flash.rs @@ -0,0 +1,464 @@ +//! The pipe's flash EEPROM and the operation that programs it. +//! +//! The flash is 512 KiB: the boot image from offset 0, a video combination at +//! `COMBO` behind a marker and a word count, and a twelve-word header at +//! `HEADER` whose word 7 is the state the PROM and the kernel accept only as +//! 2. The graphics install tool reads and programs it with command 0x1ec, an +//! operation number and that operation's words, and reads the pipe's answer +//! from a reply buffer: word 0 goes non-zero last, word 1 is a word count and +//! the data follows. The PROM and the kernel read the same header through the +//! indirect port at internal 0x87ff00. +//! +//! No factory image is known, so a new flash is zero with header +//! state 2 (approximated). The flash keeps its contents across reset, as flash +//! does; given a backing file, every byte the pipe programs is written through +//! so the contents outlast the process as they outlast power-off on a pipe. +//! +//! Ported from hw/misc/sgi-kona.c and hw/misc/sgi-kona-flash-image.c in +//! jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use std::fs::File; +use std::io::{Read, Seek, SeekFrom, Write}; + +pub const FLASH_SIZE: usize = 0x80000; +pub(super) const COMBO: u32 = 0x20000; +pub(super) const COMBO_MARKER: u32 = 0xface_feed; +pub(super) const HEADER: u32 = 0x7ff00; +pub(super) const HEADER_STATE: u32 = HEADER + 7 * 4; +pub(super) const STATE_GOOD: u32 = 2; +/// The identification word op 5 answers: the manufacturer and device codes +/// in bytes 0 and 1, from the example readout in the InfiniteReality upgrade +/// manual (a documentation example, not a measurement). Bytes 2 and 3 are +/// unknown and answered zero. +pub(super) const IDENTITY: u32 = 0x1fc4_0000; +/// The largest count op 1 accepts; a larger one fails before its data. +pub(super) const MAX_WORDS: u32 = 0x7ffb; +/// The longest reply the board posts, in bytes from word 0: the 16 KiB +/// buffer both of the programming tool's paths name. +pub(super) const REPLY_MAX: u32 = 0x4000; + +const OP_WRITE: u32 = 1; +const OP_READ: u32 = 2; +const OP_REPLY_BUFFER: u32 = 3; +const OP_CHECK: u32 = 4; +const OP_IDENTIFY: u32 = 5; +const OP_ERASE_COMBO: u32 = 6; + +const TYPE_IMAGE: u32 = 0; +const TYPE_COMBO: u32 = 1; +const TYPE_HEADER: u32 = 2; + +/// Words after the operation number, before any data. A write's first word is +/// its type, which decides the rest. +const OP_WORDS: [u8; 7] = [0, 1, 2, 3, 1, 1, 0]; + +/// The file a pipe's flash is kept in: opened for reading and writing, and +/// made as a new flash when it does not exist. +pub fn flash_file(path: &std::path::Path) -> Result { + let err = |e: std::io::Error| format!("kona flash {}: {e}", path.display()); + if !path.exists() { + std::fs::write(path, Flash::fresh().image()).map_err(err)?; + } + std::fs::OpenOptions::new().read(true).write(true).open(path).map_err(err) +} + +pub(super) struct Flash { + image: Box<[u8]>, + backing: Option, + /// A write-through failed: reported once, the bytes stay in the flash. + sync_failed: bool, +} + +impl Flash { + /// The flash a new pipe starts with. + pub fn fresh() -> Self { + let mut image = vec![0u8; FLASH_SIZE].into_boxed_slice(); + image[HEADER_STATE as usize..][..4].copy_from_slice(&STATE_GOOD.to_be_bytes()); + Flash { image, backing: None, sync_failed: false } + } + + /// A flash held in `file`, which must be exactly `FLASH_SIZE` bytes. + pub fn backed(mut file: File) -> Result { + let len = file.metadata().map_err(|e| format!("kona: cannot read the size of the flash image: {e}"))?.len(); + if len != FLASH_SIZE as u64 { + return Err(format!("kona: the flash image is {len} bytes, not the required {FLASH_SIZE}")); + } + let mut f = Flash::fresh(); + file.seek(SeekFrom::Start(0)).and_then(|_| file.read_exact(&mut f.image)) + .map_err(|e| format!("kona: cannot read the flash image: {e}"))?; + f.backing = Some(file); + Ok(f) + } + + /// The contents, as the board's own reads see them. + pub fn image(&self) -> &[u8] { + &self.image + } + + /// The big-endian word at flash offset `offset`, or 0 past the end. + pub fn load(&self, offset: u32) -> u32 { + let o = offset as usize; + if o > FLASH_SIZE - 4 { + return 0; + } + u32::from_be_bytes(self.image[o..o + 4].try_into().unwrap()) + } + + /// Program four bytes at `at`; bytes past the end are dropped. True if + /// all four landed. + fn program(&mut self, at: u64, word: u32) -> bool { + let bytes = word.to_be_bytes(); + let mut inside = true; + for (i, b) in bytes.iter().enumerate() { + match self.image.get_mut(at as usize + i) { + Some(slot) => *slot = *b, + None => inside = false, + } + } + if (at as usize) < FLASH_SIZE { + let end = (at as usize + 4).min(FLASH_SIZE); + self.sync(at as usize, end); + } + inside + } + + /// The image, as the stream module reads and writes it. A loaded image is not written to + /// the backing file here: `write_through` does that, once the whole snapshot has parsed. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { image: _, backing: _, sync_failed: _ } = self; + s.bytes(&mut self.image); + } + + /// The whole image to the backing file, as every programmed byte is. + pub fn write_through(&mut self) { + self.sync(0, self.image.len()); + } + + fn sync(&mut self, from: usize, to: usize) { + let Some(file) = self.backing.as_mut() else { return }; + let r = file.seek(SeekFrom::Start(from as u64)).and_then(|_| file.write_all(&self.image[from..to])); + if r.is_err() && !self.sync_failed { + self.sync_failed = true; + crate::dlog!(LogModule::Kona, "kona: write to the flash image failed; later writes may be lost from the image too"); + } + } +} + +/// What a completed step of an operation asks the board to do. +#[derive(Debug, PartialEq, Eq)] +pub(super) enum Action { + /// Post these words on the return page at byte 4: the count, then them. + Reply(Vec), + /// Take the page and offset words as the new reply buffer. + ReplyBuffer { page: u32, offset: u32 }, +} + +/// An operation taking its words from the command stream. +#[derive(Clone, Default, Debug)] +pub(super) struct FlashOp { + op: u32, + need: usize, + have: usize, + args: [u32; 3], + /// Data words still to take (a write). + data: u32, + /// Whether they are stored (an unknown type takes its data and drops it). + store: bool, + addr: u64, + outside: bool, +} + +impl FlashOp { + /// Whether an operation is open: every word is its. + pub fn open(&self) -> bool { + self.op != 0 + } + + /// The operation number arrived. Op 6 acts at once; an unknown number + /// takes no more words. + pub fn begin(&mut self, flash: &mut Flash, op: u32) -> Option { + *self = FlashOp::default(); + if op == OP_ERASE_COMBO { + // Four zero bytes over the combination's marker. Nothing posted. + flash.program(COMBO as u64, 0); + return None; + } + if op == 0 || op as usize >= OP_WORDS.len() { + crate::dlog!(LogModule::Kona, "kona: EEPROM operation {op} is not one of 1 to 6; it takes no more words"); + return None; + } + self.op = op; + self.need = OP_WORDS[op as usize] as usize; + None + } + + /// A word on the command page while the operation is open. + pub fn word(&mut self, flash: &mut Flash, word: u32) -> Option { + if self.have < self.need { + self.args[self.have] = word; + self.have += 1; + if self.op == OP_WRITE && self.have == 1 { + // An offset and a count for the image, a count otherwise. + self.need = if word == TYPE_IMAGE { 3 } else { 2 }; + } + if self.have == self.need { + return self.ready(flash); + } + return None; + } + if self.store { + self.store_word(flash, word); + } + self.data -= 1; + if self.data == 0 { + let status = self.store as u32; + *self = FlashOp::default(); + return Some(Action::Reply(vec![status])); + } + None + } + + fn store_word(&mut self, flash: &mut Flash, word: u32) { + if !flash.program(self.addr, word) && !self.outside { + crate::dlog!( + LogModule::Kona, + "kona: EEPROM write reaches flash offset {:#x}, past its 512 KiB; the bytes past it are dropped", + self.addr + ); + self.outside = true; + } + self.addr += 4; + } + + /// The fixed words are in. + fn ready(&mut self, flash: &mut Flash) -> Option { + if self.op == OP_WRITE { + return self.write_open(flash); + } + let done = std::mem::take(self); + match done.op { + OP_READ => read(flash, done.args[0], done.args[1]), + OP_REPLY_BUFFER => Some(Action::ReplyBuffer { page: done.args[0], offset: done.args[1] }), + // Approximated: the ARM sends a value round the transaction bus + // and answers 1 when it comes back intact. The model has no such + // bus; a working pipe's check passes. + OP_CHECK => Some(Action::Reply(vec![1])), + OP_IDENTIFY => Some(Action::Reply(vec![IDENTITY])), + _ => unreachable!("operation {} has no fixed words to finish", done.op), + } + } + + /// A write's fixed words are in. A count above `MAX_WORDS` fails before + /// any data word, so the next word opens a command. Otherwise + /// `max(count, 1)` data words follow (the ARM always reads a last word), + /// stored from the type's place: the image at its offset, the combination + /// behind its marker and count, the header at its own place. An unknown + /// type takes its data and stores none. + fn write_open(&mut self, flash: &mut Flash) -> Option { + let ty = self.args[0]; + let count = self.args[self.need - 1]; + if count > MAX_WORDS { + crate::dlog!(LogModule::Kona, "kona: EEPROM write of {count} words, more than {MAX_WORDS}; refused"); + *self = FlashOp::default(); + return Some(Action::Reply(vec![0])); + } + self.data = count.max(1); + self.store = true; + match ty { + TYPE_IMAGE => self.addr = self.args[1] as u64, + TYPE_COMBO => { + self.addr = COMBO as u64; + self.store_word(flash, COMBO_MARKER); + self.store_word(flash, count); + } + TYPE_HEADER => self.addr = HEADER as u64, + _ => { + crate::dlog!(LogModule::Kona, "kona: EEPROM write of unknown type {ty}; its words are stored nowhere"); + self.store = false; + } + } + None + } +} + +/// Op 2: `bytes` bytes of flash from `offset`, rounded down to a word as the +/// ARM's word index does. A length that is not whole words would keep the ARM's +/// copy loop from ending, and a reply longer than the buffer would run past +/// it: both are logged and answered with nothing. Approximated: words past +/// the 512 KiB are answered zero. +fn read(flash: &Flash, offset: u32, bytes: u32) -> Option { + if bytes % 4 != 0 { + crate::dlog!(LogModule::Kona, "kona: EEPROM read of {bytes:#x} bytes, not whole words; nothing posted"); + return None; + } + if 8 + bytes as u64 > REPLY_MAX as u64 { + crate::dlog!(LogModule::Kona, "kona: EEPROM read of {bytes:#x} bytes is longer than the reply buffer; nothing posted"); + return None; + } + let offset = offset & !3; + if offset as u64 + bytes as u64 > FLASH_SIZE as u64 { + crate::dlog!(LogModule::Kona, "kona: EEPROM read at flash offset {offset:#x} runs past its 512 KiB; read as zero there"); + } + Some(Action::Reply((0..bytes / 4).map(|i| flash.load(offset.wrapping_add(4 * i))).collect())) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn run(op: &mut FlashOp, f: &mut Flash, words: &[u32]) -> Vec { + let mut out = Vec::new(); + let mut it = words.iter(); + let first = *it.next().unwrap(); + out.extend(op.begin(f, first)); + for w in it { + out.extend(op.word(f, *w)); + } + out + } + + #[test] + fn a_new_flash_is_zero_with_the_header_state_the_prom_accepts() { + let f = Flash::fresh(); + assert_eq!(f.load(HEADER_STATE), STATE_GOOD); + assert_eq!(f.load(0), 0); + assert_eq!(f.load(HEADER), 0); + } + + #[test] + fn an_image_write_lands_at_its_offset_and_answers_one() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + let a = run(&mut op, &mut f, &[OP_WRITE, TYPE_IMAGE, 0x100, 2, 0xaabbccdd, 0x11223344]); + assert_eq!(a, vec![Action::Reply(vec![1])]); + assert_eq!(f.load(0x100), 0xaabbccdd); + assert_eq!(f.load(0x104), 0x11223344); + assert!(!op.open()); + } + + #[test] + fn a_combination_write_is_framed_by_its_marker_and_count() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + run(&mut op, &mut f, &[OP_WRITE, TYPE_COMBO, 1, 0x55]); + assert_eq!(f.load(COMBO), COMBO_MARKER); + assert_eq!(f.load(COMBO + 4), 1); + assert_eq!(f.load(COMBO + 8), 0x55); + } + + #[test] + fn erasing_the_combination_zeroes_its_marker_and_posts_nothing() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + run(&mut op, &mut f, &[OP_WRITE, TYPE_COMBO, 1, 0x55]); + assert!(run(&mut op, &mut f, &[OP_ERASE_COMBO]).is_empty()); + assert_eq!(f.load(COMBO), 0); + assert_eq!(f.load(COMBO + 8), 0x55); + assert!(!op.open()); + } + + #[test] + fn an_oversized_count_is_refused_before_any_data() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + let a = run(&mut op, &mut f, &[OP_WRITE, TYPE_HEADER, MAX_WORDS + 1]); + assert_eq!(a, vec![Action::Reply(vec![0])]); + assert!(!op.open()); + } + + #[test] + fn an_unknown_type_takes_its_data_and_stores_none() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + let a = run(&mut op, &mut f, &[OP_WRITE, 9, 2, 0xdead, 0xbeef]); + assert_eq!(a, vec![Action::Reply(vec![0])]); + assert_eq!(f.load(0xdead), 0); + assert!(!op.open()); + } + + #[test] + fn an_unknown_operation_takes_nothing() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + assert!(run(&mut op, &mut f, &[0]).is_empty()); + assert!(!op.open()); + assert!(run(&mut op, &mut f, &[7]).is_empty()); + assert!(!op.open()); + } + + #[test] + fn a_read_past_the_end_answers_zero_and_a_write_past_it_is_dropped() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + let a = run(&mut op, &mut f, &[OP_READ, FLASH_SIZE as u32 - 4, 8]); + assert_eq!(a, vec![Action::Reply(vec![0, 0])]); + let a = run(&mut op, &mut f, &[OP_WRITE, TYPE_IMAGE, FLASH_SIZE as u32 - 4, 2, 1, 2]); + assert_eq!(a, vec![Action::Reply(vec![1])]); + assert_eq!(f.load(FLASH_SIZE as u32 - 4), 1); + } + + #[test] + fn a_read_longer_than_the_reply_buffer_or_not_whole_words_posts_nothing() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + assert!(run(&mut op, &mut f, &[OP_READ, 0, 3]).is_empty()); + assert!(run(&mut op, &mut f, &[OP_READ, 0, REPLY_MAX]).is_empty()); + } + + #[test] + fn check_and_identify_answer_without_taking_effect() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + assert_eq!(run(&mut op, &mut f, &[OP_CHECK, 0]), vec![Action::Reply(vec![1])]); + assert_eq!(run(&mut op, &mut f, &[OP_IDENTIFY, 0]), vec![Action::Reply(vec![IDENTITY])]); + } + + #[test] + fn the_backed_flash_writes_through_and_reloads() { + let dir = crate::dev::kona::tests::ScratchDir::new("flash"); + let path = dir.join("flash.bin"); + std::fs::write(&path, vec![0u8; FLASH_SIZE]).unwrap(); + let open = || std::fs::OpenOptions::new().read(true).write(true).open(&path).unwrap(); + let mut f = Flash::backed(open()).unwrap(); + let mut op = FlashOp::default(); + run(&mut op, &mut f, &[OP_WRITE, TYPE_IMAGE, 0x40, 1, 0x1234_5678]); + drop(f); + let again = Flash::backed(open()).unwrap(); + assert_eq!(again.load(0x40), 0x1234_5678); + // A file of the wrong size is refused. + std::fs::write(&path, vec![0u8; 10]).unwrap(); + assert!(Flash::backed(open()).is_err()); + } + + /// An image group reads back from where it was + /// written, including from an offset inside the group. + #[test] + fn a_written_image_reads_back_from_any_word() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + let words: Vec = (0..64u32).map(|i| 0xa500_0000 | (i * 0x10203)).collect(); + let mut req = vec![OP_WRITE, TYPE_IMAGE, 0x4000, 64]; + req.extend(&words); + assert_eq!(run(&mut op, &mut f, &req), vec![Action::Reply(vec![1])]); + assert_eq!(run(&mut op, &mut f, &[OP_READ, 0x4000, 256]), vec![Action::Reply(words.clone())]); + assert_eq!(run(&mut op, &mut f, &[OP_READ, 0x4008, 8]), vec![Action::Reply(vec![words[2], words[3]])]); + } + + /// A header written with state 3 is the + /// state the PROM's check reads next time, at the address the indirect port + /// reads it from. + #[test] + fn a_written_header_is_what_the_next_read_returns() { + let mut f = Flash::fresh(); + let mut op = FlashOp::default(); + let mut header: Vec = (0..12u32).map(|i| 0x1111_1111 * (i + 1)).collect(); + header[7] = 3; + let mut req = vec![OP_WRITE, TYPE_HEADER, 12]; + req.extend(&header); + assert_eq!(run(&mut op, &mut f, &req), vec![Action::Reply(vec![1])]); + assert_eq!(run(&mut op, &mut f, &[OP_READ, HEADER, 48]), vec![Action::Reply(header)]); + assert_eq!(f.load(HEADER_STATE), 3); + } +} diff --git a/src/dev/kona/frame.rs b/src/dev/kona/frame.rs new file mode 100644 index 000000000..2baf31d13 --- /dev/null +++ b/src/dev/kona/frame.rs @@ -0,0 +1,244 @@ +//! Command-word framing: a port takes one 32-bit word at a time, opens a +//! command on a selector word and collects the argument words that table +//! says follow it. +//! +//! The board's ports share this shape: the textport after its sync word, +//! the command page's kernel and graphics-process commands, and the GL +//! client's commands. Each owner supplies its own selector table. A word +//! between commands that no table entry matches is reported to the owner +//! and discarded, so the next word starts a command afresh and later +//! commands stay aligned. +//! +//! Ported from hw/misc/sgi-command-port.c in jamesbraid/qemu-sgi-origami. + +use std::sync::OnceLock; + +/// The most argument words any selector's entry may declare. +pub(super) const MAX_ARGS: usize = 32; + +/// One command: its opening word and the argument words after it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct Selector { + pub selector: u32, + pub argc: u8, +} + +impl Selector { + pub const fn new(selector: u32, argc: u8) -> Self { + Selector { selector, argc } + } +} + +/// A selector table with its lookup. Every word a port takes between +/// commands is looked up, and a table runs to a few hundred entries, so the +/// lookup is a flat array indexed by selector rather than a search. +pub(super) struct SelectorTable { + /// `slot[s]` is one more than the index of the entry for selector `s`, + /// zero for none. A repeated selector keeps its first entry. + slot: Box<[u16]>, + entries: &'static [Selector], +} + +impl SelectorTable { + fn new(entries: &'static [Selector]) -> Self { + let limit = entries.iter().map(|e| e.selector as usize + 1).max().unwrap_or(0); + assert!(entries.len() < u16::MAX as usize); + let mut slot = vec![0u16; limit].into_boxed_slice(); + for (i, e) in entries.iter().enumerate().rev() { + assert!(e.argc as usize <= MAX_ARGS); + slot[e.selector as usize] = i as u16 + 1; + } + SelectorTable { slot, entries } + } + + #[inline] + pub fn get(&self, selector: u32) -> Option { + match self.slot.get(selector as usize) { + Some(&s) if s != 0 => Some(self.entries[s as usize - 1]), + _ => None, + } + } +} + +/// The tables the board's ports frame on, built once for the process. +pub(super) struct FrameTables { + pub dma_fifo: SelectorTable, + pub textport: SelectorTable, + pub kernel: SelectorTable, + pub gl: SelectorTable, +} + +pub(super) fn tables() -> &'static FrameTables { + static TABLES: OnceLock = OnceLock::new(); + TABLES.get_or_init(|| { + use super::tables as t; + FrameTables { + dma_fifo: SelectorTable::new(t::DMA_FIFO), + textport: SelectorTable::new(t::TEXTPORT), + kernel: SelectorTable::new(t::KERNEL), + gl: SelectorTable::new(t::GL), + } + }) +} + +/// What a word did to a port. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Step { + /// The word opened a command or was one of its arguments; more follow. + Partial, + /// The word completed a command: `Port::packet`. + Packet, + /// Between commands, a word no entry matches: discarded. + Unknown, +} + +/// A completed command. +#[derive(Clone, Copy)] +pub(super) struct Packet { + pub selector: u32, + pub argc: usize, + pub args: [u32; MAX_ARGS], +} + +impl Packet { + pub fn args(&self) -> &[u32] { + &self.args[..self.argc] + } +} + +/// One framer: the command it has open, if any, and the words so far. +#[derive(Clone)] +pub(super) struct Port { + table: &'static SelectorTable, + framing: bool, + selector: u32, + expected: usize, + have: usize, + args: [u32; MAX_ARGS], +} + +impl Port { + pub fn new(table: &'static SelectorTable) -> Self { + Port { table, framing: false, selector: 0, expected: 0, have: 0, args: [0; MAX_ARGS] } + } + + pub fn reset(&mut self) { + self.framing = false; + self.selector = 0; + self.expected = 0; + self.have = 0; + } + + /// Whether a command is open. + pub fn framing(&self) -> bool { + self.framing + } + + /// The opening word of the open command. + pub fn selector(&self) -> u32 { + self.selector + } + + /// Argument words the open command still needs. + pub fn missing(&self) -> usize { + self.expected - self.have + } + + /// Whether `word` would open a command here. + pub fn knows(&self, word: u32) -> bool { + self.table.get(word).is_some() + } + + /// The words of the open command held so far, its opening word + /// included: what the credit register counts. + pub fn held(&self) -> u32 { + if self.framing { + 1 + self.have as u32 + } else { + 0 + } + } + + pub fn push(&mut self, word: u32) -> Step { + if !self.framing { + let Some(found) = self.table.get(word) else { + return Step::Unknown; + }; + self.framing = true; + self.selector = word; + self.expected = found.argc as usize; + self.have = 0; + } else { + self.args[self.have] = word; + self.have += 1; + } + if self.have == self.expected { + self.framing = false; + Step::Packet + } else { + Step::Partial + } + } + + /// The command the last `push` completed. + pub fn packet(&self) -> Packet { + Packet { selector: self.selector, argc: self.expected, args: self.args } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn textport() -> Port { + Port::new(&tables().textport) + } + + #[test] + fn a_command_frames_on_its_selector_and_count() { + let mut p = textport(); + assert_eq!(p.push(0x3), Step::Partial); + assert_eq!(p.held(), 1); + for w in [7, 1, 2] { + assert_eq!(p.push(w), Step::Partial); + } + assert_eq!(p.push(3), Step::Packet); + let pkt = p.packet(); + assert_eq!((pkt.selector, pkt.args()), (3, &[7, 1, 2, 3][..])); + assert!(!p.framing()); + } + + #[test] + fn an_unknown_word_is_discarded_and_the_next_still_opens() { + let mut p = textport(); + assert_eq!(p.push(0xdead_beef), Step::Unknown); + assert_eq!(p.push(0x9), Step::Partial); + assert_eq!(p.push(0), Step::Packet); + } + + #[test] + fn an_argument_may_equal_a_selector() { + let mut p = textport(); + assert_eq!(p.push(0x2), Step::Partial); + assert_eq!(p.push(0x9), Step::Packet); + assert_eq!(p.packet().args(), &[0x9]); + } + + #[test] + fn a_zero_argument_selector_completes_on_its_own_word() { + let mut p = Port::new(&tables().dma_fifo); + assert_eq!(p.push(0x1f5), Step::Packet); + assert_eq!(p.packet().argc, 0); + } + + #[test] + fn the_first_entry_of_a_repeated_selector_wins() { + let t = tables(); + for table in [&t.dma_fifo, &t.textport, &t.kernel, &t.gl] { + for e in table.entries { + let first = table.entries.iter().find(|x| x.selector == e.selector).unwrap(); + assert_eq!(table.get(e.selector), Some(*first)); + } + } + } +} diff --git a/src/dev/kona/front.rs b/src/dev/kona/front.rs new file mode 100644 index 000000000..553935b41 --- /dev/null +++ b/src/dev/kona/front.rs @@ -0,0 +1,485 @@ +//! The host interface registers a processor reaches without waiting for the +//! command stream: the widget header, the interrupt registers, the mailbox +//! and the plain-storage registers the driver and the firmware write. +//! +//! The mutex around `Front` only serialises the register file; it is held +//! for a few instructions and never across a call out of the board. A +//! method that raises an interrupt returns the packet and the caller sends +//! it after dropping the lock. +//! +//! Ported from hw/misc/sgi-kona.c in jamesbraid/qemu-sgi-origami. + +use crate::irq::InterruptController; +use crate::physical; +use crate::traits::{BusDevice, BUS_OK}; +use super::defs::*; +use super::ident::{self, Identity, KonaConfig}; +use crate::devlog::LogModule; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering::Relaxed}; +use std::sync::Arc; + +pub(super) struct Front { + pub widget_control: u32, + /// What it resets to: the host widget in bits 25:22 (the model's choice). + widget_control_reset: u32, + pub identity: Identity, + pub onewire_reg: u32, + pub probe_value: u32, + pub low_window_write: u32, + pub int_dest_upper: u32, + pub int_dest_lower: u32, + pub int_vectors: u32, + pub reset_arg: u32, + pub int_enable: u32, + pub credit_config: u32, + pub int_cause: u32, + /// Reply words queued for the driver, `mailbox[pos..len]` unread. Bit + /// `i` of `completions` marks word `i` (counted from `pos`) as an + /// unsolicited completion rather than a reply: clearing the mailbox + /// cause bit consumes it. + mailbox: [u32; MAILBOX_MAX_WORDS], + mailbox_len: usize, + mailbox_pos: usize, + completions: u32, + pub retrace_tag: u32, + pub reset_strobe: u32, + pub eeprom_enable: u32, + pub loader_3b0: u32, + pub loader_328: u32, + /// A video cause has been delivered and not yet acknowledged. + video_delivered: bool, + /// The missing interrupt destination has been logged for the video + /// cause: it rises every frame, so it says so once. + video_unrouted_logged: bool, + /// Which run of the board a back end's call belongs to. A reset moves it + /// on, so a call a back end thread made from before the reset (a retrace + /// it completes, a frame it announces) is dropped instead of raising a + /// cause the reset cleared. Not itself reset. + epoch: u64, +} + +impl Front { + pub fn new(cfg: &KonaConfig) -> Self { + Front { + widget_control: cfg.widget_control_reset(), + widget_control_reset: cfg.widget_control_reset(), + identity: Identity::new(cfg), + onewire_reg: 0, + probe_value: 0, + low_window_write: 0, + int_dest_upper: 0, + int_dest_lower: 0, + int_vectors: 0, + reset_arg: 0, + int_enable: 0, + credit_config: 0, + int_cause: 0, + mailbox: [0; MAILBOX_MAX_WORDS], + mailbox_len: 0, + mailbox_pos: 0, + completions: 0, + retrace_tag: 0, + reset_strobe: 0, + eeprom_enable: 0, + loader_3b0: 0, + loader_328: 0, + video_delivered: false, + video_unrouted_logged: false, + epoch: 0, + } + } + + /// The registers, as the stream module reads and writes them. The configuration + /// (the widget's reset value, the probe) and the epoch are the board's, not the + /// guest's, and stay. The identity chain's state goes as the text its own save makes. + pub fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { widget_control: _, widget_control_reset: _, identity: _, onewire_reg: _, probe_value: _, low_window_write: _, int_dest_upper: _, int_dest_lower: _, int_vectors: _, reset_arg: _, int_enable: _, credit_config: _, int_cause: _, mailbox: _, mailbox_len: _, mailbox_pos: _, completions: _, retrace_tag: _, reset_strobe: _, eeprom_enable: _, loader_3b0: _, loader_328: _, video_delivered: _, video_unrouted_logged: _, epoch: _ } = self; + s.u32(&mut self.widget_control); + let mut bus = self.identity.bus.save_state().to_string(); + s.string(&mut bus); + if s.loading() { + match bus.parse::().map_err(|e| e.to_string()).and_then(|v| self.identity.bus.load_state(&v)) { + Ok(()) => {} + Err(e) => s.fail(&format!("the one-wire chain: {e}")), + } + } + for v in [ + &mut self.onewire_reg, + &mut self.low_window_write, + &mut self.int_dest_upper, + &mut self.int_dest_lower, + &mut self.int_vectors, + &mut self.reset_arg, + &mut self.int_enable, + &mut self.credit_config, + &mut self.int_cause, + &mut self.completions, + &mut self.retrace_tag, + &mut self.reset_strobe, + &mut self.eeprom_enable, + &mut self.loader_3b0, + &mut self.loader_328, + ] { + s.u32(v); + } + s.u32s(&mut self.mailbox); + s.usize(&mut self.mailbox_len); + s.usize(&mut self.mailbox_pos); + s.bool(&mut self.video_delivered); + s.bool(&mut self.video_unrouted_logged); + } + + /// Power-on state. The identity chain and the probe value are the + /// board's, not the driver's, and stay. + pub fn reset(&mut self) { + self.widget_control = self.widget_control_reset; + self.onewire_reg = 0; + self.low_window_write = 0; + self.int_dest_upper = 0; + self.int_dest_lower = 0; + self.int_vectors = 0; + self.reset_arg = 0; + self.int_enable = 0; + self.credit_config = 0; + self.int_cause = 0; + self.mailbox = [0; MAILBOX_MAX_WORDS]; + self.mailbox_len = 0; + self.mailbox_pos = 0; + self.completions = 0; + self.retrace_tag = 0; + self.reset_strobe = 0; + self.eeprom_enable = 0; + self.loader_3b0 = 0; + self.loader_328 = 0; + self.video_delivered = false; + self.video_unrouted_logged = false; + } + + /// The current epoch: a ticket made now is good until the next reset. + pub fn epoch(&self) -> u64 { + self.epoch + } + + /// Start a new epoch, so calls carrying an older one are dropped. + pub fn advance_epoch(&mut self) { + self.epoch += 1; + } + + /// The map RAM's entry count: `N << 15` with `N` from the probe + /// register's bits 5:3, zero meaning 8. + pub fn mapram_count(&self) -> usize { + let n = (self.probe_value & PROBE_MAPRAM_COUNT_MASK) >> PROBE_MAPRAM_COUNT_SHIFT; + (if n == 0 { 8 } else { n as usize }) << 15 + } + + /// Whether mailbox words are unread. + fn mailbox_unread(&self) -> usize { + self.mailbox_len - self.mailbox_pos + } + + /// Replace the queue with a reply and raise the mailbox cause. + pub fn mailbox_reply(&mut self, words: &[u32]) { + assert!(words.len() <= MAILBOX_MAX_WORDS); + self.mailbox[..words.len()].copy_from_slice(words); + self.mailbox_len = words.len(); + self.mailbox_pos = 0; + self.completions = 0; + if !words.is_empty() { + self.int_cause |= CAUSE_MAILBOX; + } + } + + /// One unsolicited mailbox word behind any reply words still unread. A + /// queue with no room left loses it, logged. + pub fn mailbox_post(&mut self, word: u32) { + let unread = self.mailbox_unread(); + if unread >= MAILBOX_MAX_WORDS { + crate::dlog!(LogModule::Kona, "kona: mailbox full; completion word {word:#x} lost"); + return; + } + self.mailbox.copy_within(self.mailbox_pos..self.mailbox_len, 0); + self.completions >>= self.mailbox_pos; + self.completions |= 1 << unread; + self.mailbox[unread] = word; + self.mailbox_len = unread + 1; + self.mailbox_pos = 0; + self.int_cause |= CAUSE_MAILBOX; + } + + /// The interrupt packet for the general or the geometry-engine vector, + /// to the destination the driver programmed. Assert-only. None, logged, + /// when no destination was programmed. + pub fn raise_vector(&self, general: bool) -> Option { + let (vector, what) = if general { + ((self.int_vectors & INT_VECTORS_GENERAL_MASK) as u8, "general") + } else { + ((self.int_vectors >> INT_VECTORS_GE_SHIFT) as u8, "geometry-engine") + }; + if self.int_dest_upper == 0 && self.int_dest_lower == 0 { + crate::dlog!(LogModule::Kona, "kona: {what} interrupt with no destination programmed; no packet sent"); + return None; + } + Some(InterruptPacket { + widget: ((self.int_dest_upper >> INT_DEST_WIDGET_SHIFT) & INT_DEST_WIDGET_MASK) as u8, + address: ((self.int_dest_upper & 0xffff) as u64) << 32 | self.int_dest_lower as u64, + vector, + }) + } + + /// Deliver the video cause once, if the driver enabled it. + pub fn video_deliver(&mut self) -> Option { + if self.int_cause & self.int_enable & CAUSE_VIDEO != 0 && !self.video_delivered { + // Checked here, not left to `raise_vector`, which logs every call: + // the video cause rises each frame and logs once. + if self.int_dest_upper == 0 && self.int_dest_lower == 0 { + if !self.video_unrouted_logged { + self.video_unrouted_logged = true; + crate::dlog!(LogModule::Kona, "kona: video interrupt with no destination programmed; no packet sent"); + } + return None; + } + let p = self.raise_vector(true); + self.video_delivered = p.is_some(); + return p; + } + None + } + + /// A retrace the back end counted: the tag register reads the request's + /// tag, and the video cause rises. + pub fn retrace(&mut self, tag: u32) -> Option { + self.retrace_tag = tag; + self.int_cause |= CAUSE_VIDEO; + self.video_deliver() + } + + /// A retrace the back end counted in `epoch`: as `retrace`, unless a + /// reset has begun a later epoch. + pub fn retrace_in(&mut self, epoch: u64, tag: u32) -> Option { + if epoch != self.epoch { + return None; + } + self.retrace(tag) + } + + /// A frame the display announced in `epoch`: the video cause rises, with + /// no tag, unless a reset has begun a later epoch. + pub fn video_frame(&mut self, epoch: u64) -> Option { + if epoch != self.epoch { + return None; + } + self.int_cause |= CAUSE_VIDEO; + self.video_deliver() + } + + /// The pipe has drained: the low-watermark cause, if the driver has + /// enabled it. The driver enables it only while a thread waits for the + /// pipe to drain below its threshold. Model choice: raised when the work + /// and the words behind it are all done; when a real pipe raises it, + /// against which level, is unknown. + pub fn lowater(&mut self) -> Option { + if self.int_enable & CAUSE_LOWATER != 0 { + self.int_cause |= CAUSE_LOWATER; + return self.raise_vector(true); + } + None + } + + /// A read of a register this file owns. `None` for any other offset. + pub fn read(&mut self, off: u32, credit: u32) -> Option { + Some(match off { + WIDGET_ID => ident::widget_id(0), + WIDGET_CONTROL => self.widget_control, + PROBE => self.probe_value, + ONEWIRE | NIC => self.onewire_reg, + LOW_WINDOW_WRITE => self.low_window_write, + INT_DEST_UPPER => self.int_dest_upper, + INT_DEST_LOWER => self.int_dest_lower, + INT_VECTORS => self.int_vectors, + CREDIT => credit, + CREDIT_CONFIG => self.credit_config, + RESET_ARG => self.reset_arg, + // Software never reads it (inferred); the bits it holds are those + // of the status register. + INT_ENABLE => self.int_enable, + INT_CAUSE => self.int_cause, + RETRACE_TAG => self.retrace_tag, + MAILBOX => { + if self.mailbox_pos < self.mailbox_len { + self.mailbox[self.mailbox_pos] + } else { + 0 + } + } + RESET_STROBE => self.reset_strobe, + EEPROM_ENABLE => self.eeprom_enable, + LOADER_3B0 => self.loader_3b0, + LOADER_328 => self.loader_328, + _ => return None, + }) + } + + /// A write of a register this file owns: false for any other offset. + /// `packet` receives a packet the write raised. + pub fn write(&mut self, off: u32, value: u32, packet: &mut Option) -> bool { + match off { + // Unknown control fields are stored without interpreted effects. + WIDGET_CONTROL => self.widget_control = value, + ONEWIRE | NIC => self.onewire_reg = self.identity.write(value), + LOW_WINDOW_WRITE => self.low_window_write = value, + INT_DEST_UPPER => self.int_dest_upper = value, + INT_DEST_LOWER => self.int_dest_lower = value, + INT_VECTORS => self.int_vectors = value, + RESET_ARG => self.reset_arg = value, + CREDIT_CONFIG => self.credit_config = value, + INT_ENABLE => { + self.int_enable = value & INT_ENABLE_MASK; + *packet = self.video_deliver(); + } + INT_CAUSE => { + // Write-1-to-clear, the geometry-engine bit included. + self.int_cause &= !value; + if value & CAUSE_VIDEO != 0 { + self.video_delivered = false; + } + // A completion word is acknowledged by this write alone: the + // driver's DMA-write wait reads the mailbox once and writes + // nothing back before its handler clears the cause bit, + // where the reply protocol consumes each word with a mailbox + // write first. So clearing the bit consumes an unread + // completion at the head; left queued, completions would + // fill the queue. + if value & CAUSE_MAILBOX != 0 { + if self.mailbox_pos < self.mailbox_len && self.completions & (1 << self.mailbox_pos) != 0 { + self.mailbox_pos += 1; + } + // Re-arm synchronously if a next word is queued: the + // model has no device-side timing to simulate the real + // interrupt's re-assertion latency. + if self.mailbox_pos < self.mailbox_len { + self.int_cause |= CAUSE_MAILBOX; + } + } + } + MAILBOX => { + // Any value (the driver always writes 0) consumes the + // current reply word; the cause register is not touched. + if self.mailbox_pos < self.mailbox_len { + self.mailbox_pos += 1; + } + } + RESET_STROBE => self.reset_strobe = value, + EEPROM_ENABLE => self.eeprom_enable = value, + LOADER_3B0 => self.loader_3b0 = value, + LOADER_328 => self.loader_328 = value, + _ => return false, + } + true + } + + /// The host-interface reset bit of the reset strobe: what the kernel + /// rewrites after its 1, read, 0, read pulse. (The map RAM, which the + /// pulse also clears, is the board's thread's.) + pub fn hip_reset(&mut self) { + self.int_enable = 0; + self.reset_arg = 0; + self.credit_config = 0; + self.low_window_write = 0; + } +} + +/// A crosstalk interrupt packet as the driver programmed it: a write that sets +/// one bit in the destination's pending register. The processor clears the bit +/// itself; the pipe never sends a clear. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct InterruptPacket { + /// The destination widget number (bits 19:16 of the upper destination + /// register). + pub widget: u8, + /// The 48-bit address within that widget. + pub address: u64, + /// The eight-bit vector. + pub vector: u8, +} + +/// The register file with its connections: what the processor's thread and +/// the board's thread both hold. +pub(super) struct Host { + pub front: Mutex, + /// Where interrupt packets go: each is one `set_line(vector, true)`, which + /// the controller takes as setting pending bit `vector`, never as an edge. + pub irq: Arc, + /// Host physical memory, as the board's DMA reaches it. + pub mem: Arc, + /// Packets with no receiver. A packet is delivered when it names the widget + /// the pipe's DMA goes to (the widget control register's field, which host + /// software fills with its interrupt target); any other is counted and + /// dropped. A multi-node machine needs a destination-to-controller map in the + /// machine, not in the board. + pub unrouted: AtomicU64, + /// Words the framers hold of commands still arriving, published by the + /// board's thread at the end of each run: the credit register's count. + pub held_words: AtomicU32, + /// Completion reasons wait behind an unread one: the next access to the + /// cause register looks again. + pub reasons_waiting: AtomicBool, + /// A display list is running: the credit register reads full. + pub list_running: AtomicBool, + /// The watchdog activity latch: set when a command word or a firmware + /// load word arrives, cleared by the driver writing the bit back. A + /// latch read and written on every word, so not behind the register + /// file's lock. + pub activity: AtomicBool, + /// Writes of the activity register's bit 0 (the kernel's once-per-video-interrupt + /// acknowledgement of the status bit; nothing reads it back). + pub video_acks: AtomicU64, +} + +impl Host { + /// Run `f` on the register file, then send the packet it returns, with + /// the lock dropped. + pub fn with_front(&self, f: impl FnOnce(&mut Front) -> (R, Option)) -> R { + let (r, packet) = f(&mut self.front.lock()); + if let Some(p) = packet { + self.deliver(p); + } + r + } + + /// Send `p` to the interrupt controller, with no board lock held. A packet + /// for a widget other than the host's is counted, and logged once. + pub fn deliver(&self, p: InterruptPacket) { + let host_widget = ((self.front.lock().widget_control >> 22) & 0xf) as u8; + if p.widget == host_widget { + self.irq.set_line(p.vector as u32, true); + } else if self.unrouted.fetch_add(1, Relaxed) == 0 { + crate::dlog!(LogModule::Kona, "kona: interrupt packet for widget {} (host widget {host_widget}), vector {:#x}: no receiver; not delivered", p.widget, p.vector); + } + } + + /// Read `buf.len()` bytes of host memory at `addr`; false if the bus fails. + pub fn dma_read(&self, addr: u64, buf: &mut [u8]) -> bool { + physical::read_bytes(&*self.mem, addr, buf) == BUS_OK + } + + /// Write `data` to host memory at `addr`; false if the bus fails. + pub fn dma_write(&self, addr: u64, data: &[u8]) -> bool { + physical::write_bytes(&*self.mem, addr, data) == BUS_OK + } + + /// The general vector's packet for a mailbox post. + pub fn post_mailbox(&self, word: u32) { + self.with_front(|f| { + f.mailbox_post(word); + ((), f.raise_vector(true)) + }); + } + + /// Queue a reply and raise the mailbox cause. No vector: the driver + /// polls for the replies the PROM and the textport protocol use. + pub fn mailbox_reply(&self, words: &[u32]) { + self.front.lock().mailbox_reply(words); + } +} diff --git a/src/dev/kona/handle.rs b/src/dev/kona/handle.rs new file mode 100644 index 000000000..d9d36b17f --- /dev/null +++ b/src/dev/kona/handle.rs @@ -0,0 +1,51 @@ +//! What a back end's own threads may do to the front end. +//! +//! `BackPort` reaches the front end only inside `KonaBackend::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 when a frame starts. The handle is how it does that. It holds the +//! register file, which owns nothing of the back end, so there is no cycle. +//! +//! A handle call takes the epoch its ticket was made in (`epoch`). A reset +//! begins a new epoch, so a thread that was in the middle of completing a +//! retrace when the reset happened cannot raise a cause the reset cleared. + +use super::front::Host; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::Arc; + +/// The front end as a back end's display thread may use it. Cheap to clone. +#[derive(Clone)] +pub struct DisplayHandle { + host: Arc, +} + +impl DisplayHandle { + pub(super) fn new(host: &Arc) -> Self { + DisplayHandle { host: host.clone() } + } + + /// The epoch a ticket made now belongs to. + pub fn epoch(&self) -> u64 { + self.host.front.lock().epoch() + } + + /// A retrace request made in `epoch` was honoured: the tag register reads + /// `tag` and the video interrupt rises. + pub fn retrace_done(&self, epoch: u64, tag: u32) { + self.host.with_front(|f| ((), f.retrace_in(epoch, tag))); + } + + /// A frame started in `epoch`: the video interrupt rises, with no tag. + pub fn video_frame(&self, epoch: u64) { + self.host.with_front(|f| ((), f.video_frame(epoch))); + } + + /// The back end is working: the driver's watchdog sees activity. For a + /// drain loop or the raster thread while it works. Not for the display + /// thread, which runs whether or not the pipe does: if it set the latch + /// the watchdog could never fire. + pub fn activity(&self) { + self.host.activity.store(true, Relaxed); + } +} diff --git a/src/dev/kona/ident.rs b/src/dev/kona/ident.rs new file mode 100644 index 000000000..3dc43fe0b --- /dev/null +++ b/src/dev/kona/ident.rs @@ -0,0 +1,669 @@ +//! What the pipe says it is: the widget id register, the one-wire identity +//! record, the two words the driver's attach classes the pipe by, and the +//! configuration block the host interface processor answers a configuration +//! request with. +//! +//! The configuration is a product of the table below, the InfiniteReality2 +//! of the Onyx2 deskside by default: a GE14-4 geometry board, one or two RM7 +//! raster managers each with a TM7 texture board of 64 MB, and a DG5 display +//! generator of eight channels. The InfiniteReality2E and InfiniteReality3 +//! change the boards, the board set and the texture size; the display +//! generator is the deskside's DG5-8 for all (a limit: see [`Display`]). The deskside has no second +//! host-interface record, so the one-wire chain holds the geometry board's +//! alone. Part numbers, names and revisions are those of the ported model of +//! that pipe, or the capture a product's row names; ROM serials and barcodes +//! are synthetic and marked so (the serial's top 32 bits are the marker +//! 0x51454d55 and the barcode starts "EMU"; both follow the ported +//! synthetic-serial scheme). +//! +//! Ported from hw/misc/sgi-kona.c in jamesbraid/qemu-sgi-origami. + +use super::defs::*; +use crate::dev::w1::sgi::BoardRecord; +use crate::dev::w1::{slot, Bus, Ds2502}; + +/// One board of the configuration: the name, part number and revision its +/// one-wire record carries. A field the ported model leaves blank is empty. +#[derive(Clone, Copy)] +pub(super) struct BoardSpec { + name: &'static str, + part: &'static str, + revision: &'static str, +} + +/// Geometry engines, four on every product the model presents. +const ENGINES: u32 = 4; +const OCCUPANCY: u32 = 0xf; +/// Display generator channels: the DG5-8's. +pub(super) const CHANNELS: u32 = 8; + +/// What a display generator board reports about itself. The identity word the +/// configuration block ends its raster group with is built from these: the high +/// byte and the 0x1002 low half-word are constant for a DG5 board (inferred +/// from the captures), bits 18 to 16 are the channel count less one, 21 to 19 +/// the display option, bit 22 the colour-map size and bit 23 external gamma. +/// +/// Limit: the display model has a compile-time channel count (`CHANNELS`), so +/// every product presents the DG5-8. The word of a DG5-2 with a GVO option, as +/// the Onyx2 rack capture reads it, is checked in a test and the board is not +/// presented: that would need the channel arrays in `back::display` sized by +/// the board. +pub(super) struct Display { + pub channels: u32, + /// 3 TVO, 4 DPLEX, 5 DVP, 6 GVO, 7 none. + pub option: u32, + pub colour_map: bool, + pub external_gamma: bool, + /// Bits 31 to 24 of the identity word. + high: u32, + pub board: BoardSpec, +} + +impl Display { + pub(super) fn word(&self) -> u32 { + (self.high << 24) + | (self.external_gamma as u32) << 23 + | (self.colour_map as u32) << 22 + | self.option << 19 + | (self.channels - 1) << 16 + | 0x1002 + } +} + +/// The DG5-8 of the deskside capture: eight channels, external gamma, no +/// display option and so no option record. +pub(super) const DG5_8: Display = Display { + channels: 8, + option: 7, + colour_map: false, + external_gamma: true, + high: 0x51, + board: BoardSpec { name: "", part: "030-1087-001", revision: "" }, +}; +/// "4MB external" FIFO RAM, as the ported model reports. +const FIFO_RAM_4MB_EXTERNAL: u32 = 1; + +/// The chassis a pipe fits in. Iris builds the Onyx2 deskside alone; the +/// others are recorded so a row says where its product shipped. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Host { + Onyx2Deskside, + Onyx2Rack, + Onyx300, + Onyx3000, +} + +impl Host { + pub fn name(self) -> &'static str { + match self { + Host::Onyx2Deskside => "Onyx2 deskside", + Host::Onyx2Rack => "Onyx2 rack", + Host::Onyx300 => "Onyx 300", + Host::Onyx3000 => "Onyx 3000", + } + } +} + +/// The products of the pipe the model presents. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Product { + Ir2, + Ir2e, + Ir3, +} + +/// One row of the product table: everything the model reports that +/// differs between products, as data. A field the model does not +/// present differently stays a shared constant above. +pub struct ProductSpec { + pub product: Product, + /// The name `[graphics] board` and a recording's board record use. + pub name: &'static str, + /// The name the guest's `hinv` prints for the board. + pub label: &'static str, + /// The board-set word at the start of the configuration block. + board_set: u32, + /// The display generator the product presents. + display: &'static Display, + /// What the pipe answers when the driver's attach reads the product + /// selector (`SELECT_PRODUCT`): the low bits class the pipe. + class_word: u32, + geometry: BoardSpec, + raster: BoardSpec, + texture: BoardSpec, + /// Texture memory on each raster manager's texture board, and the size + /// code the block carries for it. + texture_mb: u32, + texture_code: u32, + /// The geometry board's identity word and the raster manager's. + board_word: u32, + raster_word: u32, + /// Where the product shipped. + pub hosts: &'static [Host], +} + +impl ProductSpec { + /// Texture memory on each raster manager's texture board, in MB. + pub fn texture_mb(&self) -> u32 { + self.texture_mb + } + + pub fn fits(&self, host: Host) -> bool { + self.hosts.contains(&host) + } + + /// Whether the product may go in `host`, or why not. A chassis the table + /// leaves out is one no manual or capture places the product in. + pub fn check_host(&self, host: Host) -> Result<(), String> { + if self.fits(host) { + return Ok(()); + } + let shipped: Vec<_> = self.hosts.iter().map(|h| h.name()).collect(); + Err(format!( + "{} is not modelled in the {}: no manual or capture places it there (it fits the {})", + self.label, + host.name(), + shipped.join(", ") + )) + } +} + +/// The product table, in the order of [`Product`]. +pub const PRODUCTS: [ProductSpec; 3] = [ + // The Onyx2 deskside's InfiniteReality2 (GE14-4 with RM7 and TM7): the + // deskside capture's values. + ProductSpec { + product: Product::Ir2, + name: "ir2", + label: "InfiniteReality2", + board_set: 1, + display: &DG5_8, + class_word: 0, + geometry: BoardSpec { name: "GE14-4", part: "030-1129-002", revision: "F" }, + raster: BoardSpec { name: "RM7", part: "030-1054-001", revision: "G" }, + texture: BoardSpec { name: "TM7", part: "030-1053-001", revision: "F" }, + texture_mb: 64, + texture_code: 2, + board_word: 0x8016_0a04, + raster_word: IR2_RASTER_WORD, + hosts: &[Host::Onyx2Deskside], + }, + // InfiniteReality2E: the GE16-4 geometry board with RM9 raster managers + // and the TM7-64 texture board. The attach classes a pipe whose geometry + // board's record holds "GE16-" as set 4 (established). The raster slot + // word is the InfiniteReality2's, type 6 with texture code 2: type 0xb + // would make the pipe present as an InfiniteReality3. The GE16-4 and its + // identity word are the rack capture's, and the geometry word is inferred; + // the RM9 and TM7 part numbers are established for the 2E, their + // revisions and the chip words in the block are unknown (the chip words + // are the InfiniteReality2's). + ProductSpec { + product: Product::Ir2e, + name: "ir2e", + label: "InfiniteReality2E", + board_set: 4, + display: &DG5_8, + class_word: 0, + geometry: GE16_4, + raster: BoardSpec { revision: "", ..RM9 }, + texture: BoardSpec { name: "TM7", part: "030-1053-001", revision: "" }, + texture_mb: 64, + texture_code: 2, + board_word: GE16_BOARD_WORD, + raster_word: IR2_RASTER_WORD, + hosts: &[Host::Onyx2Deskside, Host::Onyx2Rack, Host::Onyx300], + }, + // InfiniteReality3: the GE16-4 and RM9 records with a 256 MB TM8 texture + // board per raster manager, as the rack capture presents them (each + // raster manager holds its own copy, inferred). The TM8's name is + // inferred from the board report's pairing and its revision is unknown. + // The attach classes the pipe by the product word reading 1. + ProductSpec { + product: Product::Ir3, + name: "ir3", + label: "InfiniteReality3", + board_set: 5, + display: &DG5_8, + class_word: 1, + geometry: GE16_4, + raster: RM9, + texture: BoardSpec { name: "TM8", part: "030-1588-001", revision: "" }, + texture_mb: 256, + texture_code: 4, + board_word: GE16_BOARD_WORD, + raster_word: IR3_RASTER_WORD, + hosts: &[Host::Onyx2Rack, Host::Onyx300, Host::Onyx3000], + }, +]; + +// The boards the GE16 products share, as the Onyx2 rack capture reads them. +const GE16_4: BoardSpec = BoardSpec { name: "GE16-4", part: "030-1398-001", revision: "E" }; +const RM9: BoardSpec = BoardSpec { name: "RM9", part: "030-1402-001", revision: "E" }; +const GE16_BOARD_WORD: u32 = 0x80f6_1806; +/// A raster slot word: type 6 (a TM7, the 2 and 2E) with texture code 2, and +/// type 0xb (a TM8) with code 4 (the 3). +const IR2_RASTER_WORD: u32 = 0x0302_0c06; +const IR3_RASTER_WORD: u32 = 0x0304_1606; + +impl Product { + pub fn spec(self) -> &'static ProductSpec { + &PRODUCTS[self as usize] + } + + /// The product `name` selects, or an error naming those there are. + pub fn from_name(name: &str) -> Result { + PRODUCTS.iter().find(|p| p.name == name).map(|p| p.product).ok_or_else(|| { + let names: Vec<_> = PRODUCTS.iter().map(|p| p.name).collect(); + format!("kona: product {name:?}: the model has {}", names.join(", ")) + }) + } +} + +/// The configuration a pipe presents. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct KonaConfig { + /// The product the pipe is, a row of [`PRODUCTS`]. + pub product: Product, + /// Raster managers: the deskside holds one or two. + pub rm_count: u32, + /// Which pipe slot the machine puts this on, 0 to 3: each slot's + /// records take sixteen synthetic serial instances. + pub identity_index: u8, + /// The widget the pipe's DMA goes to out of reset: the host node's + /// crossbar widget, 0 to 15. The register's real power-on value is + /// unknown. The model starts it here so DMA works before software has + /// programmed it; the ported model does the same with its board's hub widget. + pub host_widget: u8, + /// The value the probe register reads. Its bits 5:3 size the map RAM: `N << 15` + /// entries, zero meaning 8. + pub probe: u32, + /// The managed area in pixels, until a video format combination sets another. + pub managed: (u32, u32), + /// The tile workers and the tile size in pixels, `[graphics] threads` and + /// `tile`. Zero workers runs every job inline. + pub threads: usize, + pub tile: u32, +} + +impl KonaConfig { + /// A `product` pipe with `rm_count` raster managers in pipe slot 0. + pub fn new(product: Product, rm_count: u32) -> Result { + if rm_count != 1 && rm_count != 2 { + return Err(format!( + "kona: rm-count {rm_count}: an Onyx2 deskside pipe holds one or two raster managers" + )); + } + Ok(KonaConfig { product, rm_count, ..KonaConfig::default() }) + } + + /// An InfiniteReality2 with `rm_count` RM7 boards in pipe slot 0. + pub fn ir2(rm_count: u32) -> Result { + Self::new(Product::Ir2, rm_count) + } + + /// The product's row. + pub fn spec(&self) -> &'static ProductSpec { + self.product.spec() + } + + pub fn in_slot(mut self, index: u8) -> Result { + if index >= 4 { + return Err(format!("kona: identity index {index}: each pipe slot's records use sixteen serial instances, 0 to 3")); + } + self.identity_index = index; + Ok(self) + } + + /// The widget the pipe's DMA goes to out of reset (bits 25:22 of the + /// widget control register). + pub fn with_host_widget(mut self, widget: u8) -> Result { + if widget > 15 { + return Err(format!("kona: host widget {widget}: a widget number is four bits")); + } + self.host_widget = widget; + Ok(self) + } + + /// The widget control register's value out of reset. + pub(super) fn widget_control_reset(&self) -> u32 { + (self.host_widget as u32) << 22 + } + + /// The machine's `[graphics]` worker count and tile size. + pub fn with_pool(mut self, threads: usize, tile: u32) -> Self { + self.threads = threads; + self.tile = tile; + self + } + + /// The most words a pixel's record may take at the managed size. + pub fn record_words(&self) -> u32 { + super::back::store::layout::record_words(self.rm_count, self.managed.0, self.managed.1) + } + + /// Texture memory on each raster manager's texture board, in bytes. + pub fn texture_bytes_per_rm(&self) -> u64 { + (self.spec().texture_mb as u64) << 20 + } + + fn base(&self) -> u32 { + 16 * self.identity_index as u32 + } +} + +impl Default for KonaConfig { + fn default() -> Self { + KonaConfig { product: Product::Ir2, rm_count: 2, identity_index: 0, host_widget: 0, probe: 0, managed: (1280, 1024), threads: 2, tile: 32 } + } +} + +/// The widget id register: presence bit, manufacturer, part, revision. +pub(super) fn widget_id(revision: u32) -> u32 { + (revision << WIDGET_ID_REVISION_SHIFT) + | (PART << WIDGET_ID_PART_SHIFT) + | (MANUFACTURER << WIDGET_ID_MANUFACTURER_SHIFT) + | WIDGET_ID_ALWAYS_ONE +} + +/// The synthetic one-wire ROM serial of component `letter`, instance `n`: +/// the marker 0x51454d55, the letter, the instance. +pub(super) fn synthetic_serial(letter: u8, n: u32) -> u64 { + 0x5145_4d55u64 << 16 | (letter as u64) << 8 | n as u64 +} + +/// The barcode a board record carries: the marker, the letter and the +/// instance in decimal, as short as the instance allows (six characters +/// below 100). +fn barcode(letter: char, n: u32) -> String { + let width = if n < 100 { 6 } else if n < 1000 { 7 } else { 8 }; + format!("EMU{letter}{n:0w$}", w = width - 4) +} + +/// The identity bus: the geometry board's record on a DS2502. +pub(super) struct Identity { + pub bus: Bus, +} + +impl Identity { + pub fn new(cfg: &KonaConfig) -> Self { + let n = cfg.base(); + let geometry = &cfg.spec().geometry; + let record = BoardRecord { + barcode: &barcode('G', n), + part: geometry.part, + revision: geometry.revision, + name: geometry.name, + }; + let mut bus = Bus::new(); + bus.add(Box::new(Ds2502::new(ONEWIRE_FAMILY, synthetic_serial(b'G', n), &record.image()))); + Identity { bus } + } + + /// The one-wire register after a write of `value`: the slot the write + /// started, completed in the same access with DONE set and LEVEL the + /// sampled wire. The pulse and sample fields the guest wrote persist. + pub fn write(&mut self, value: u32) -> u32 { + let (pulse, sample) = slot::fields(value as u64); + let level = slot::slot(&mut self.bus, pulse, sample); + (value & ONEWIRE_WRITABLE) | ONEWIRE_DONE | if level { ONEWIRE_LEVEL } else { 0 } + } +} + +/// The two words the driver's attach reads through the indirect pair to +/// class the pipe: after selecting `SELECT_PRODUCT` the low bits are the +/// product's row (0 for an InfiniteReality2), after `SELECT_REDUCED` bit 10 +/// would make it a Reality (0). +pub(super) fn class_word(cfg: &KonaConfig, selector: u32) -> Option { + match selector { + SELECT_PRODUCT => Some(cfg.spec().class_word), + SELECT_REDUCED => Some(0), + _ => None, + } +} + +fn put(block: &mut [u8], at: usize, word: u32) { + block[at..at + 4].copy_from_slice(&word.to_be_bytes()); +} + +fn put_words(block: &mut [u8], at: usize, count: usize, word: u32) { + for i in 0..count { + put(block, at + 4 * i, word); + } +} + +/// `text` right-justified in a field of `width` spaces. +fn put_text(field: &mut [u8], text: &str) { + let width = field.len(); + let len = text.len().min(width); + field.fill(b' '); + field[width - len..].copy_from_slice(&text.as_bytes()[..len]); +} + +/// One 0x4c-byte board record in the layout the driver's one-wire reader +/// builds and the board report prints: flags (0, both pages present), the six-byte +/// one-wire number, family 0x0b, a good CRC, page format 1, then barcode, +/// part, revision, the three flag bytes and the name. +fn put_record(rec: &mut [u8], b: &BoardSpec, instance: u32) { + let serial = synthetic_serial(b'G', instance); + for i in 0..6 { + rec[0x04 + i] = (serial >> (40 - 8 * i)) as u8; + } + rec[0x0a] = 0x0b; + rec[0x0b] = 1; + rec[0x0c] = 1; + put_text(&mut rec[0x0d..0x17], &barcode('G', instance)); + put_text(&mut rec[0x17..0x2a], b.part); + rec[0x2c..0x32].fill(b' '); + put_text(&mut rec[0x32..0x36], b.revision); + rec[0x36..0x3c].fill(0xff); + put_text(&mut rec[0x3c..0x4a], b.name); +} + +// Offsets of the configuration block's fields, from its start. +const CFG_BOARD_SET: usize = 0x00; +const CFG_ENGINES: usize = 0x04; +const CFG_RMS: usize = 0x08; +const CFG_FIFO_RAM: usize = 0x0c; +const CFG_CHANNELS: usize = 0x14; +const CFG_VIDEO_OPTION: usize = 0x18; +const CFG_GAMMA: usize = 0x20; +const CFG_RM_PRESENT: usize = 0x24; +const CFG_RM_TEXTURE: usize = 0x28; +const CFG_DG_CHANNEL_COUNT: usize = 0x44; +const CFG_ACTIVE_ENGINES: usize = 0x54; +const CFG_ACTIVE_RMS: usize = 0x58; +const CFG_OCCUPANCY: usize = 0x5c; +const CFG_WIDTH: usize = 0x84; +const CFG_HEIGHT: usize = 0x88; +const CFG_PIXEL_DEPTH: usize = 0x8c; +const CFG_POOLS: usize = 0x90; +const CFG_BOARD_WORD: usize = 0x100; +const CFG_GE_WORDS: usize = 0x104; +const CFG_RM_CHIPS: usize = 0x114; +const CFG_TRAILING_WORD: usize = 0x344; +const CFG_DG_WORDS: usize = 0x348; +const CFG_DG_CHANNELS: usize = 0x35c; +const CFG_NIC_RM: usize = 0x3e8; +const CFG_NIC_TM: usize = 0x564; +const CFG_NIC_DG: usize = 0x6e0; + +/// The block's size in bytes, and where the answer puts it on the return +/// page. +pub(super) const CONFIG_SIZE: usize = 0x778; +pub(super) const CONFIG_OFFSET: u32 = 8; + +/// The four pipe-memory pools the driver's allocator checks requests +/// against: the co-processor, the shared display lists, the geometry +/// engines and texture memory. No source gives the raw words, so these are +/// the model's own choice inside the kernel's limits: the granule is +/// non-zero and divides every alignment requested, alignment over granule +/// stays below 256, and the geometry class takes keys to 10 and is not +/// shared. +struct Pool { + max_key: u32, + max_align: u32, + copies: u32, + shared: u32, + copy_down: u32, + granule: u32, + capacity: u32, +} + +fn pools(cfg: &KonaConfig) -> [Pool; 4] { + [ + Pool { max_key: 0, max_align: 0x4000, copies: 1, shared: 0, copy_down: 1, granule: 0x400, capacity: 0x100 }, + Pool { max_key: 0, max_align: 0x4000, copies: 1, shared: 1, copy_down: 1, granule: 0x400, capacity: 0x3c00 }, + Pool { max_key: 10, max_align: 0x4000, copies: ENGINES, shared: 0, copy_down: 1, granule: 0x400, capacity: 0x400 }, + Pool { max_key: 0, max_align: 0x4000, copies: 1, shared: 0, copy_down: 1, granule: 0x400, capacity: cfg.spec().texture_mb * (1 << 20) / 0x400 }, + ] +} + +/// The configuration block the pipe answers a configuration request with: +/// every field at the offset the driver's board report reads it from. +/// `width` and `height` are the managed area's, 1280x1024 until a video +/// format combination sets up another. +pub(super) fn config_block(cfg: &KonaConfig, width: u32, height: u32) -> Vec { + let mut b = vec![0u8; CONFIG_SIZE]; + let base = cfg.base(); + let spec = cfg.spec(); + + put(&mut b, CFG_BOARD_SET, spec.board_set); + put(&mut b, CFG_ENGINES, ENGINES); + put(&mut b, CFG_RMS, cfg.rm_count); + put(&mut b, CFG_FIFO_RAM, FIFO_RAM_4MB_EXTERNAL); + put(&mut b, CFG_CHANNELS, CHANNELS); + put(&mut b, CFG_VIDEO_OPTION, 0); + put(&mut b, CFG_GAMMA, spec.display.external_gamma as u32); + // The display generator's own channel count, which the combination + // checker compares a combination's enabled channels against. + put(&mut b, CFG_DG_CHANNEL_COUNT, CHANNELS); + put(&mut b, CFG_ACTIVE_ENGINES, ENGINES); + put(&mut b, CFG_ACTIVE_RMS, cfg.rm_count); + put(&mut b, CFG_OCCUPANCY, OCCUPANCY); + put(&mut b, CFG_WIDTH, width); + put(&mut b, CFG_HEIGHT, height); + // Small pixel depth. + put(&mut b, CFG_PIXEL_DEPTH, 0); + + for (i, p) in pools(cfg).iter().enumerate() { + let at = CFG_POOLS + 0x1c * i; + for (k, w) in [p.max_key, p.max_align, p.copies, p.shared, p.copy_down, p.granule, p.capacity] + .into_iter() + .enumerate() + { + put(&mut b, at + 4 * k, w); + } + } + + put(&mut b, CFG_BOARD_WORD, spec.board_word); + for (i, w) in [0, 0x1483_2057, 0x2473_1057, 0x1423_1057].into_iter().enumerate() { + put(&mut b, CFG_GE_WORDS + 4 * i, w); + } + for i in 0..cfg.rm_count as usize { + let chips = CFG_RM_CHIPS + 0x8c * i; + put(&mut b, CFG_RM_PRESENT + 8 * i, 1); + put(&mut b, CFG_RM_TEXTURE + 8 * i, spec.texture_code); + put(&mut b, chips, spec.raster_word); + put(&mut b, chips + 0x04, 0x1503_2057); + put(&mut b, chips + 0x08, 0x1543_1057); + put_words(&mut b, chips + 0x0c, 4, 0x2463_1057); + put_words(&mut b, chips + 0x1c, 8, 0x24d3_1057); + put_words(&mut b, chips + 0x3c, 20, 0x5493_8057); + put_record(&mut b[CFG_NIC_RM + 0x4c * i..][..0x4c], &spec.raster, base + 2 + i as u32); + put_record(&mut b[CFG_NIC_TM + 0x4c * i..][..0x4c], &spec.texture, base + 6 + i as u32); + } + put(&mut b, CFG_TRAILING_WORD, spec.display.word()); + put(&mut b, CFG_DG_WORDS, 0x0553_2057); + put_words(&mut b, CFG_DG_WORDS + 4, 4, 0x1583_8057); + put_words(&mut b, CFG_DG_CHANNELS, CHANNELS as usize, 0x1563_1057); + put_record(&mut b[CFG_NIC_DG..][..0x4c], &spec.display.board, base + 10); + b +} + +#[cfg(test)] +pub(in crate::dev::kona) mod tests { + use super::*; + + #[test] + fn the_widget_id_composes_revision_part_manufacturer_and_presence() { + assert_eq!(widget_id(0), 0x0c10_2000 | (0x2aa << 1) | 1); + assert_eq!(widget_id(3) >> 28, 3); + } + + #[test] + fn barcodes_lengthen_with_the_instance() { + assert_eq!(barcode('G', 0), "EMUG00"); + assert_eq!(barcode('G', 63), "EMUG63"); + assert_eq!(barcode('G', 123), "EMUG123"); + } + + #[test] + fn a_deskside_holds_one_or_two_raster_managers() { + assert!(KonaConfig::ir2(1).is_ok()); + assert!(KonaConfig::ir2(2).is_ok()); + assert!(KonaConfig::ir2(4).is_err()); + assert!(KonaConfig::ir2(0).is_err()); + } + + #[test] + fn the_host_widget_sets_the_widget_control_reset_value() { + assert_eq!(KonaConfig::default().widget_control_reset(), 0); + let cfg = KonaConfig::default().with_host_widget(9).unwrap(); + assert_eq!(cfg.widget_control_reset(), 9 << 22); + assert!(KonaConfig::default().with_host_widget(16).is_err()); + } + + pub(in crate::dev::kona) fn synthetic_serial(instance: u32) -> u64 { + 0x5145_4d55u64 << 16 | (b'G' as u64) << 8 | instance as u64 + } + + /// A block as `config-ir2-*rm.hex` holds it: sixteen bytes a row, in hex. + pub(in crate::dev::kona) fn parse_hex(text: &str) -> Vec { + text.split_whitespace().map(|b| u8::from_str_radix(b, 16).unwrap()).collect() + } + + /// The block the pipe builds is the deskside's byte for byte, for two + /// raster managers and for one. The files hold the fixed bytes. + #[test] + fn the_configuration_block_matches_the_deskside_layout() { + for (rms, want) in [(2, include_str!("tests/data/config-ir2-2rm.hex")), (1, include_str!("tests/data/config-ir2-1rm.hex"))] { + let cfg = KonaConfig::ir2(rms).unwrap(); + let block = config_block(&cfg, 1280, 1024); + let want = parse_hex(want); + assert_eq!(block.len(), want.len()); + if let Some(i) = (0..block.len()).step_by(4).find(|&i| block[i..i + 4] != want[i..i + 4]) { + panic!("{rms} RMs: first difference at {i:#x}: {:x?} against {:x?}", &block[i..i + 4], &want[i..i + 4]); + } + } + } + + /// `Product::spec` indexes the table by the enum, so a row out of order + /// would present another product's boards. + #[test] + fn each_product_indexes_its_own_row() { + for (i, row) in PRODUCTS.iter().enumerate() { + assert_eq!(row.product as usize, i, "{}", row.name); + assert_eq!(row.product.spec().name, row.name); + assert_eq!(Product::from_name(row.name), Ok(row.product)); + } + let e = Product::from_name("ir9").unwrap_err(); + assert!(e.contains("ir2"), "{e}"); + } + + /// The display word is built from the board: the DG5-8's and the DG5-2 with + /// a GVO option's match the words the captures read. + #[test] + fn the_display_word_follows_the_display_board() { + assert_eq!(DG5_8.word(), 0x51bf_1002); + let dg5_2_gvo = Display { channels: 2, option: 6, colour_map: false, external_gamma: false, high: 0xf9, board: BoardSpec { name: "", part: "030-1242-001", revision: "D" } }; + assert_eq!(dg5_2_gvo.word(), 0xf931_1002); + assert_eq!(Display { option: 7, ..dg5_2_gvo }.word(), 0xf939_1002); + } + + #[test] + fn the_managed_size_is_reported() { + let block = config_block(&KonaConfig::default(), 640, 480); + assert_eq!(u32::from_be_bytes(block[0x84..0x88].try_into().unwrap()), 640); + assert_eq!(u32::from_be_bytes(block[0x88..0x8c].try_into().unwrap()), 480); + } +} diff --git a/src/dev/kona/job.rs b/src/dev/kona/job.rs new file mode 100644 index 000000000..bf8ad5c7a --- /dev/null +++ b/src/dev/kona/job.rs @@ -0,0 +1,173 @@ +//! The jobs the host interface processor hands the drawing back end. +//! +//! The front end frames the command stream, answers the commands the host +//! interface processor answers itself (pages, replies, the configuration +//! request, the flash, DMA and display lists) and hands every command that +//! changes drawing or display state to the back end as a typed `Job`. +//! +//! A back end runs on the host interface processor's thread, in stream order. +//! It reaches the front end only through the `BackPort` it is handed: the +//! return and reason pages, the mailbox, the retrace completion and the +//! pipe's address space for its own DMA. + +use super::pipe::Core; + +/// Which grammar a command belongs to; selectors repeat between them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Grammar { + /// The PROM's textport, after its sync word. + Textport, + /// The command page's kernel, X server and graphics-process commands. + Kernel, + /// The GL client's commands. + Gl, +} + +/// What happened to a rendering context the host interface processor tracks. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ContextEvent { + /// The context in table entry `slot` became current. `fresh`: it starts + /// afresh (a new save area), not a restore of one the pipe has seen. + Activated { slot: usize, fresh: bool }, + /// The current context was switched out and its save area written. + Deactivated { slot: usize }, + /// The driver destroyed the current context's window. + WindowReleased { slot: usize }, +} + +/// One thing for the back end to do. +#[derive(Debug)] +pub enum Job<'a> { + /// A command the front end framed. `handled`: the front end already did + /// its part (a page registered, a reply posted) and this is the back end's + /// look at it, for state that depends on seeing every token. `packed` is + /// the top half of a GL command's opening word, which the byte and short + /// forms use for data. + Command { grammar: Grammar, selector: u32, args: &'a [u32], packed: u32, handled: bool }, + /// The words behind a command that carried them: a colour map, a gamma + /// table, a cursor, a texture table, or a run the back end asked for. + Data { grammar: Grammar, selector: u32, args: &'a [u32], words: &'a [u32] }, + /// A textport bitmap blit: six header words, then the bits. + Bitmap { header: &'a [u32], payload: &'a [u32] }, + /// A video command whose checksum verified: the command number and its + /// data words. + Video { command: u32, data: &'a [u32] }, + /// A retrace request: complete it with `BackPort::retrace_done`. + Retrace { tag: u32 }, + /// A context change. + Context(ContextEvent), + /// A store to the indirect data register under a selector the front end + /// does not answer itself (the display generator's cursor registers). + Indirect { selector: u32, value: u32 }, + /// A DMA packet no display list is waiting for: `lines` rows of + /// `per_line` bytes at `io`, `pitch` apart. `physical`: `io` is a host + /// physical address (the packet's high word had bit 28 set); otherwise it + /// is a device address the map RAM translates. The back end reads the rows + /// through `BackPort::dma_read`. + DmaIn { io: u64, physical: bool, lines: u32, per_line: u32, pitch: u64 }, + /// A word that opened no command and was dropped: no framer's table has + /// it. A token whose length the tables lack shows up as its own word and + /// the words behind it, one job each. The zero that pads an odd-length + /// command out to a doubleword store arrives this way too, and is safe to + /// ignore. + Dropped { word: u32 }, + /// The loader released the firmware (a store of 1 to the unstall + /// register): the video format combination stored in the flash, if its + /// marker and count are good. + FirmwareStart { combination: &'a [u32] }, +} + +/// What a job leaves the command stream waiting for. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Reply { + /// Nothing more. + Done, + /// This many data words follow the command; they come back as one + /// `Job::Data`. + Data(u32), + /// A `DmaIn` nothing was waiting for. + Unclaimed, +} + +/// The front end as a back end may use it. +pub struct BackPort<'a> { + pub(super) core: &'a mut Core, + pub(super) host: &'a super::front::Host, +} + +impl BackPort<'_> { + /// Answer on the return page: `data` at byte `offset`, then the ready + /// word 1 at byte 0, the only thing the waiting process polls. False, + /// logged, with no return page registered. + pub fn post_return(&mut self, offset: u32, data: &[u8]) -> bool { + self.core.post_return(self.host, 0, offset, data) + } + + /// As `post_reason`, with the several words a completion may carry. + pub fn post_reason_words(&mut self, token: u32, words: &[u32]) { + self.core.post_reason(self.host, token, words); + } + + /// Whether a reason page is registered. + pub fn reason_page_valid(&self) -> bool { + self.core.reason_page.is_some() + } + + /// A retrace request was honoured: the tag register reads `tag` and the + /// video interrupt rises. + pub fn retrace_done(&mut self, tag: u32) { + self.host.with_front(|f| ((), f.retrace(tag))); + } + + /// The managed area's size, which the configuration request reports. + pub fn set_managed_size(&mut self, width: u32, height: u32) { + self.core.managed = (width, height); + } + + /// Whether a return page is registered. + pub fn return_page_valid(&self) -> bool { + self.core.return_page.is_some() + } + + /// The table entry of the current context. + pub fn context(&self) -> Option { + self.core.current + } + + /// Read pipe-address-space bytes (the device addresses the driver's + /// packets carry, translated through the map RAM). False if a page is + /// unmapped or the host read fails. + pub fn device_read(&mut self, device: u64, buf: &mut [u8]) -> bool { + self.core.device_read(self.host, device, buf) + } + + /// Whether every page of `len` bytes at pipe address `device` has a map + /// RAM entry, so a write there will not stop partway for want of one. + pub fn device_mapped(&self, device: u64, len: usize) -> bool { + let page = 1u64 << super::defs::MAPRAM_PAGE_SHIFT; + let (first, last) = (device & !(page - 1), (device + len.max(1) as u64 - 1) & !(page - 1)); + (first..=last).step_by(page as usize).all(|at| self.core.device_address(self.host, at).is_some()) + } + + /// Write pipe-address-space bytes. + pub fn device_write(&mut self, device: u64, data: &[u8]) -> bool { + self.core.device_write(self.host, device, data) + } + + /// Read `buf.len()` bytes of a DMA packet's buffer: at host physical + /// address `io` when `physical`, else at device address `io` through the + /// map RAM. False if a page is unmapped or the host read fails. + pub fn dma_read(&mut self, io: u64, physical: bool, buf: &mut [u8]) -> bool { + if physical { + self.host.dma_read(io, buf) + } else { + self.core.device_read(self.host, io, buf) + } + } + + /// A transfer the back end made failed on the bus: the DMA packet it + /// served is completed as failed, withholding its completion word. + pub fn dma_fault(&mut self) { + self.core.dma_fault = true; + } +} diff --git a/src/dev/kona/mod.rs b/src/dev/kona/mod.rs new file mode 100644 index 000000000..917cfcab6 --- /dev/null +++ b/src/dev/kona/mod.rs @@ -0,0 +1,846 @@ +//! The InfiniteReality (KONA) graphics pipe's front end: the host interface. +//! +//! The pipe is a crosstalk-style widget. The processor stores commands to a +//! command page; a Host Interface Processor on the board takes them, answers +//! the ones that are about the board itself and passes the ones that draw to +//! the Geometry Distributor, Geometry Engines, Raster Managers and Display +//! Generator behind it. This module is the first part: the widget header and +//! identity registers, the one-wire identity record, the board's flash +//! EEPROM and the operation the graphics install tool programs it with, +//! reset, the command FIFO, command framing, DMA and display-list storage. +//! Drawing is behind the typed job interface in `job`. +//! +//! The machine gives the board an `InterruptController` (each interrupt packet +//! is one `set_line(vector, true)`, a set of the pending bit and never an edge) +//! and a `BusDevice` for host memory; it reaches the board through the +//! `BusDevice` impl, with offsets inside the 16 MiB aperture. +//! +//! - `front`: the registers a processor reaches without waiting for the +//! command stream, behind one short lock. +//! - `regs`: which accesses join the stream, which are served at once and +//! which wait; the registers the Host Interface Processor owns. +//! - `pipe`: the Host Interface Processor's state and the command stream it +//! runs (framers, pages, contexts, reasons, swaps). +//! - `frame`, `tables`: command-word framing and the selector tables. +//! - `dma`: DMA packets, display-list memory and list invocation. +//! - `flash`: the flash EEPROM and its programming operation. +//! - `ident`: widget id, one-wire record, configuration block. +//! - `job`: the back end's interface. +//! - `back`: the back end that draws and displays. +//! - `handle`: what a back end's display thread may call on the front end. +//! +//! # Threads +//! +//! One thread, `kona-hip`, stands for the Host Interface Processor. The +//! processor stores into a command FIFO and goes on, as it does on the +//! board, and a display list the processor invokes can run for as long as +//! its words take to decode. A store to the command page (and the few other +//! stream registers) goes into the shared graphics ring (`GFifo`) with no +//! lock beyond the ring's; a full ring answers the bus busy, never a spin on +//! the processor's thread. The thread takes a run of stores at a time and +//! runs them in order. A register the stream changes or the stream reads +//! (`regs::classify`) is answered busy until the ring is empty and the thread +//! idle; a register the driver polls while the pipe works is served from the +//! register file at once. +//! +//! Without the thread running (before `start`) a stream store waits in the ring; +//! only the test-only `test_write` runs it on the caller's thread. +//! +//! # Locks +//! +//! `Host::front` guards the register file and is held for a few instructions, +//! never across a call out of the board. `Shared::pipe` is a mutex over the Host +//! Interface Processor's state and the back end: `kona-hip` holds it for each +//! batch of up to 32 stores, across calls to the machine's memory and interrupt +//! controller (nothing the machine calls takes it back), and a processor only +//! `try_lock`s it, for a read that waits for the stream, answering busy when it +//! is out. Stores, including the ones that wait for the stream and the register +//! writes that change the pipe's state, go into the ring and are applied by the +//! thread in order. The mutex is registered with the lock monitor. +//! +//! Ported from hw/misc/sgi-kona.c, sgi-kona-ports.c, sgi-kona-dma.c and +//! sgi-command-port.c in jamesbraid/qemu-sgi-origami. + +// The crate allows dead code; this board's code does not hide any. +#![warn(dead_code, unused_imports)] + +mod back; +pub mod stream; +mod defs; +mod dma; +mod flash; +mod frame; +mod front; +mod handle; +mod ident; +mod job; +mod pipe; +mod regs; +mod tables; +#[cfg(test)] +mod tests; + +// What a machine needs: the board, its configuration and the flash's size. +pub use back::snapshot::KonaChunk; +pub use flash::{flash_file, FLASH_SIZE}; +pub use ident::{KonaConfig, Product as KonaProduct}; +use back::KonaBackend; + +#[cfg(test)] +use job::{ContextEvent, Job, Reply}; + +use defs::APERTURE; +use handle::DisplayHandle; +use pipe::LoadSection; + +use crate::gfifo::GFifo; +use crate::irq::InterruptController; +use crate::traits::{BusDevice, BusRead32, BusRead64, Device, Resettable, BUS_BUSY, BUS_ERR, BUS_OK}; +use front::{Front, Host}; +use parking_lot::Mutex; +use pipe::Core; +use regs::{classify, Access}; +use std::fs::File; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering::{Relaxed, SeqCst}}; +use std::sync::Arc; +use std::thread::JoinHandle; + +/// The host ring under the command stream: 64K stores, as the other +/// graphics boards' rings are. +type Ring = GFifo<{ 1 << 16 }>; + +/// The most stores the thread looks at before running them. +const RUN_MAX: usize = 32; + +/// The Host Interface Processor's state with the back end it hands jobs to. +struct Pipe { + core: Core, + backend: KonaBackend, +} + +/// The longest a monitor command that reads state waits for the command stream. +const MONITOR_WAIT: std::time::Duration = std::time::Duration::from_millis(500); + +struct Shared { + /// Its own `Arc` so a back end's display thread can hold it weakly: the + /// register file does not own the back end, the pipe does. + host: Arc, + /// The Host Interface Processor's state with the back end. `kona-hip` holds + /// it for each batch of stores, across calls to the machine's memory and + /// interrupt controller; that cannot deadlock, because processor accesses + /// only `try_lock` it, the machine never calls into the board, and nothing + /// takes it while holding another lock. A plain mutex because no processor + /// access needs the pipe for more than a pure read. + pipe: Mutex, + ring: Box, + /// The back end's pixel reads, which need not wait for the state. + pixels: back::display::Pixels, + /// The window the back end draws the screen into. + display: Arc, + /// How long a monitor command that reads state waits for it. + monitor_wait: AtomicU64, + /// The thread died (its back end panicked): bus accesses fail. + dead: AtomicBool, + /// The thread runs, so stream stores are taken from the ring. + running: AtomicBool, + exit: AtomicBool, + /// The thread is parked in `wait_for_work`: a producer wakes it only + /// then, so a store reads a line nobody writes instead of locking. + asleep: AtomicBool, + sleeper: Mutex>, + /// A cause-register access found completion reasons waiting behind an + /// unread one: the thread posts them, not the processor. + reason_poke: AtomicBool, +} + +impl Shared { + fn wake(&self) { + // Store the entry, then look at `asleep`; the thread stores `asleep`, + // then looks at the ring. Both sides need the fence for either to + // see the other (a store-then-load handshake). + std::sync::atomic::fence(SeqCst); + if self.asleep.load(SeqCst) { + if let Some(t) = self.sleeper.lock().as_ref() { + t.unpark(); + } + } + } + + /// The thread: sleep until a store waits or `stop` asks. False when the + /// thread is to finish. The timeout is only a backstop for a wake the + /// fences should never lose. + fn wait_for_work(&self) -> bool { + *self.sleeper.lock() = Some(std::thread::current()); + while self.ring.is_empty() && !self.exit.load(SeqCst) && !self.reason_poke.load(SeqCst) { + self.asleep.store(true, Relaxed); + std::sync::atomic::fence(SeqCst); + if self.ring.is_empty() && !self.exit.load(SeqCst) && !self.reason_poke.load(SeqCst) { + std::thread::park_timeout(std::time::Duration::from_millis(50)); + } + self.asleep.store(false, Relaxed); + } + !self.exit.load(SeqCst) + } + + /// Run the stores waiting, a run at a time, in order, until the ring is + /// empty. + fn run_stores(&self) -> bool { + let mut run = [(0u32, 0u64); RUN_MAX]; + let mut worked = false; + loop { + let n = self.ring.peek_run_at(0, &mut run); + if n == 0 { + return worked; + } + worked = true; + { + let mut guard = self.pipe.lock(); + let pipe = &mut *guard; + for &(off, val) in &run[..n] { + // A waiting write (a widget register, the credit or tag) is applied here, in stream order, with the stores. + match classify(off, true) { + Access::Waits => pipe.core.write_wait(&self.host, &mut pipe.backend, off, val as u32), + _ => pipe.core.stream_store(&self.host, &mut pipe.backend, off, val as u32), + } + } + pipe.core.reason_drain(&self.host); + self.publish(&pipe.core); + // A tile worker that panicked leaves the pixels unknowable: the board is dead, + // as for a panic of this thread. + if pipe.backend.is_poisoned() && !self.dead.swap(true, SeqCst) { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: a tile worker panicked; the board is dead"); + } + // Taken out of the ring only now that they have run: a + // register that waits for the ring to be empty then finds the + // state the stores left. + self.ring.consume_n_at(0, n); + self.ring.flush_head(); + } + } + } + + /// What the polled registers read of the stream. + fn publish(&self, core: &Core) { + self.host.held_words.store(core.held_words(), SeqCst); + self.host.reasons_waiting.store(core.reasons_waiting(), SeqCst); + } + + /// The ring is empty and the thread has finished its work: the FIFO-low + /// cause, if the driver enabled it, and the machine's signal. + fn drained(&self) { + self.host.with_front(|f| ((), f.lowater())); + } + + fn thread_main(self: &Arc) { + let _dead = DeadOnPanic(self); + while self.wait_for_work() { + if self.run_stores() { + self.drained(); + } + // Reasons a cause-register access found waiting are posted here. + if self.reason_poke.load(SeqCst) { + // The poke is cleared with the pipe held, so a test that waits for the + // poke and then for the pipe cannot see both clear mid-drain. + let mut guard = self.pipe.lock(); + self.reason_poke.store(false, SeqCst); + guard.core.reason_drain(&self.host); + self.publish(&guard.core); + } + } + } + + /// Run whatever is left in the ring on the calling thread (a stop with stores waiting). + fn drain_inline(&self) { + if self.run_stores() { + self.drained(); + } + } +} + +/// Marks the board dead if the thread unwinds: its back end panicked. The +/// bus then fails visibly instead of answering busy forever to a thread that +/// is gone. +struct DeadOnPanic<'a>(&'a Shared); + +impl Drop for DeadOnPanic<'_> { + fn drop(&mut self) { + if std::thread::panicking() { + self.0.dead.store(true, SeqCst); + self.0.running.store(false, SeqCst); + crate::dlog!(crate::devlog::LogModule::Kona, "kona: the Host Interface Processor thread panicked; the board is dead"); + } + } +} + +/// The InfiniteReality pipe's host interface. +pub struct Kona { + shared: Arc, + thread: Mutex>>, +} + +impl Kona { + /// A pipe of configuration `cfg`: its interrupt packets reach `irq`, its DMA + /// reaches `mem`, and its display shows the machine's `status`. With `flash`, the + /// flash is held in that file, which must be exactly `FLASH_SIZE` bytes: + /// every byte the pipe programs is written through, so the contents outlast + /// the process as they outlast power-off on a pipe. + pub fn new(cfg: KonaConfig, irq: Arc, mem: Arc, status: Arc, flash: Option) -> Result, String> { + let flash = match flash { + Some(f) => flash::Flash::backed(f)?, + None => flash::Flash::fresh(), + }; + let backend = KonaBackend::new(cfg); + backend.set_status(status); + let pixels = backend.pixels(); + let display: Arc = backend.display(); + let mut front = Front::new(&cfg); + front.probe_value = cfg.probe; + let host = Arc::new(Host { + front: Mutex::new(front), + irq, + mem, + unrouted: Default::default(), + held_words: Default::default(), + reasons_waiting: Default::default(), + list_running: Default::default(), + activity: Default::default(), + video_acks: Default::default(), + }); + // SAFETY: a zeroed GFifo is an empty one (`GFifo::new`); built in + // place because the ring is too big for the stack. + let ring: Box = unsafe { Box::new_zeroed().assume_init() }; + let shared = Arc::new(Shared { + host, + pipe: Mutex::new(Pipe { core: Core::new(cfg, flash), backend }), + ring, + pixels, + display, + monitor_wait: AtomicU64::new(MONITOR_WAIT.as_nanos() as u64), + dead: AtomicBool::new(false), + running: AtomicBool::new(false), + exit: AtomicBool::new(false), + asleep: AtomicBool::new(false), + sleeper: Mutex::new(None), + reason_poke: AtomicBool::new(false), + }); + crate::locks::register_mutex_fn("kona pipe", &shared, |s| s.pipe.is_locked()); + Ok(Arc::new(Kona { shared, thread: Mutex::new(None) })) + } + + /// A read of `size` bytes at aperture offset `off`: the bus status and + /// the data. The registers are word-sized: an 8-byte read is two word + /// reads, high word at the lower address. + fn bus_read(&self, off: u64, size: u32) -> (u32, u64) { + if self.shared.dead.load(Relaxed) { + return (BUS_ERR, 0); + } + if off >= APERTURE { + return (BUS_ERR, 0); + } + let off = off as u32; + match size { + 4 => match self.read_word(off) { + Some(v) => (BUS_OK, v as u64), + None => (BUS_BUSY, 0), + }, + 8 => { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: 8-byte load at offset {off:#x} split into two 4-byte accesses"); + match (self.read_word(off), self.read_word(off.wrapping_add(4))) { + (Some(hi), Some(lo)) => (BUS_OK, (hi as u64) << 32 | lo as u64), + _ => (BUS_BUSY, 0), + } + } + _ => (BUS_ERR, 0), + } + } + + /// A store of `size` bytes at aperture offset `off`. BUS_BUSY: the caller + /// retries the same store. + /// + /// An 8-byte store to the command port is two words, high first, each as + /// a 4-byte store at the same offset (the X driver module's bulk data + /// helpers store aligned pairs of data words that way, and the GL client's + /// vector senders store a selector and its first argument as one + /// doubleword at any stream position). Any other 8-byte store is split + /// into two word stores at `off` and `off + 4`, logged: no evidence says + /// what a real pipe does with one. + fn bus_write(&self, off: u64, size: u32, val: u64) -> u32 { + if self.shared.dead.load(Relaxed) { + return BUS_ERR; + } + if off >= APERTURE { + return BUS_ERR; + } + let off = off as u32; + match size { + 4 => self.write_word(off, val as u32), + 8 => { + let (hi, lo) = ((val >> 32) as u32, val as u32); + if off == defs::COMMAND_PAGE { + self.write_words(&[(off, hi), (off, lo)]) + } else { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: 8-byte store at offset {off:#x} split into two 4-byte accesses"); + self.write_words(&[(off, hi), (off.wrapping_add(4), lo)]) + } + } + _ => BUS_ERR, + } + } + + fn read_word(&self, off: u32) -> Option { + match classify(off, false) { + Access::Now => Some(self.read_now(off)), + _ => { + // A read that waits for the stream answers busy while stores wait + // in the ring or the thread is mid-batch; otherwise it takes the + // pipe, for a pure read that cannot reach memory or an interrupt. + // The ring is checked first, which is sound because `run_stores` + // takes entries out of the ring only after the batch has run and + // still under the pipe lock: an empty ring means every earlier + // store has finished. + let s = &*self.shared; + if !s.ring.is_empty() { + s.wake(); + return None; + } + let mut pipe = s.pipe.try_lock()?; + Some(pipe.core.read_wait(&s.host.front, off)) + } + } + } + + fn read_now(&self, off: u32) -> u32 { + use defs::*; + let s = &*self.shared; + let pending = !s.ring.is_empty(); + match off { + INT_CAUSE => { + // Reasons waiting behind an unread one are looked for when + // the cause register is accessed. + if s.host.reasons_waiting.load(SeqCst) { + s.reason_poke.store(true, SeqCst); + s.wake(); + } + } + ACTIVITY => { + // The latch reads set while work waits, the pipe being busy, + // and the work sets it again as it consumes words. + return if s.host.activity.load(Relaxed) || pending { ACTIVITY_BUS } else { 0 }; + } + _ => {} + } + // The register is the graphics FIFO's fill level: the words the pipe + // holds, those of commands still arriving and the stores waiting in + // the ring, rounded up to two (the field's bit 0 never sets). It + // reads full while a display list runs: one store can stand for a + // long list, so a count of words would understate the wait. Software + // expectation: users wait for it to fall (to zero, below 0x5dd, or + // to 0x300) before storing more. Inferred: the unit is 32-bit words. + // Unknown: the FIFO's depth (levels of 0x70c have been read from real + // pipes, so it holds at least that). + let credit = if s.host.list_running.load(SeqCst) { + CREDIT_MASK + } else { + let words = s.host.held_words.load(SeqCst) + s.ring.len() as u32; + ((words + 1) & !1).min(CREDIT_MASK) + }; + s.host.front.lock().read(off, credit).unwrap_or_else(|| { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: unimplemented read of offset {off:#x}"); + 0 + }) + } + + /// One word of a store. A store that joins the stream, or waits for it, goes + /// into the ring and nothing more. + #[inline] + fn write_word(&self, off: u32, val: u32) -> u32 { + self.write_words(&[(off, val)]) + } + + /// The words of one store, together: they all go in or none does, so a busy + /// answer can be retried. Words that join the stream or wait for it go into + /// the ring for `kona-hip` to apply in order (stores wait there until the + /// thread starts); words served at once are applied here, after the ring + /// took the others. + fn write_words(&self, words: &[(u32, u32)]) -> u32 { + let s = &*self.shared; + // A map RAM store with nothing ahead of it in the stream is applied + // here, as a waiting read already is: the driver tests the map RAM + // word by word, a store and a load each, a million times at start-up, + // and a hand-off to the thread for every store costs it a thread wake. + if let &[(o, v)] = words { + if (defs::MAPRAM_BASE..defs::MAPRAM_BASE + 4 * defs::MAPRAM_ENTRIES as u32).contains(&o) && s.ring.is_empty() { + if let Some(mut pipe) = s.pipe.try_lock() { + if s.ring.is_empty() { + let p = &mut *pipe; + p.core.stream_store(&s.host, &mut p.backend, o, v); + return BUS_OK; + } + } + } + } + let mut queued = [(0u32, 0u64); 2]; + let mut n = 0; + for &(o, v) in words { + if classify(o, true) != Access::Now { + queued[n] = (o, v as u64); + n += 1; + } + } + let ok = match &queued[..n] { + [] => true, + [(o, v)] => s.ring.try_push(*o, *v), + [(o0, v0), (o1, v1)] => s.ring.try_push2(*o0, *v0, *o1, *v1), + _ => unreachable!(), + }; + if n > 0 { + s.wake(); + } + if !ok { + return BUS_BUSY; + } + for &(o, v) in words { + if classify(o, true) == Access::Now { + self.write_now(o, v); + } + } + BUS_OK + } + + fn write_now(&self, off: u32, value: u32) { + let s = &*self.shared; + let host = &s.host; + if off == defs::ACTIVITY { + if value & defs::ACTIVITY_BUS != 0 { + host.activity.store(false, Relaxed); + } + if value & defs::ACTIVITY_VIDEO != 0 { + host.video_acks.fetch_add(1, Relaxed); + } + if value & !(defs::ACTIVITY_BUS | defs::ACTIVITY_VIDEO) != 0 { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: unimplemented write of offset {off:#x} (value {value:#x})"); + } + return; + } + let mut packet = None; + let known = host.front.lock().write(off, value, &mut packet); + if let Some(p) = packet { + host.deliver(p); + } + if !known && off != defs::INTR_END { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: unimplemented write of offset {off:#x} (value {value:#x})"); + } + if off == defs::INT_CAUSE && host.reasons_waiting.load(SeqCst) { + s.reason_poke.store(true, SeqCst); + s.wake(); + } + } + + /// The window the pipe shows the guest, for the machine's `get_display`. + pub fn display(&self) -> Arc { + self.shared.display.clone() + } + + /// The widget the pipe's DMA requests go to: bits 25:22 of the widget + /// control register, which host software fills with its interrupt + /// target. A machine's DMA handle may choose the destination with it. + pub fn dma_widget(&self) -> u32 { + (self.shared.host.front.lock().widget_control >> 22) & 0xf + } + + /// Why the pipe stopped taking commands, if it did. + /// How many times the kernel has written the video-interrupt bit of the activity register. + pub fn video_acks(&self) -> u64 { + self.shared.host.video_acks.load(Relaxed) + } + + pub fn failure(&self) -> Option { + if self.shared.dead.load(Relaxed) { + return Some("kona: the back end panicked on the Host Interface Processor thread; the board is dead".into()); + } + let pipe = self.shared.pipe.lock(); + if pipe.backend.is_poisoned() { + return Some("kona: a tile worker panicked; the board is dead".into()); + } + pipe.core.failed.clone() + } + + /// The flash's contents. + pub fn flash_image(&self) -> Vec { + self.shared.pipe.lock().core.flash.image().to_vec() + } + + /// The sections of the last completed firmware load. + pub fn load_record(&self) -> Vec { + self.shared.pipe.lock().core.load_record().to_vec() + } + + /// A store with no board thread: the bus write, then the stores it queued + /// run on this thread. + #[cfg(test)] + pub(super) fn test_write(&self, off: u64, size: u32, val: u64) -> u32 { + let r = self.bus_write(off, size, val); + self.run_stores(); + r + } + + /// Run the stores in the ring on this thread, with no board thread: a test + /// driver that wants each store to have run before the next goes in (a + /// replay's records are numbered as they arrive). + #[cfg(test)] + pub(super) fn run_stores(&self) { + self.shared.drain_inline(); + } + + /// Wait until every store taken has been run: the ring empty and the pipe + /// between batches. (With no thread running, stores stay in the ring.) + #[cfg(test)] + pub(super) fn settle(&self) { + let s = &*self.shared; + while s.running.load(SeqCst) && !s.dead.load(SeqCst) && !(s.ring.is_empty() && !s.reason_poke.load(SeqCst) && s.pipe.try_lock().is_some()) { + s.wake(); + std::thread::yield_now(); + } + } + + /// Run `f` on the back end, with the thread's work finished first. + pub fn with_backend(&self, f: impl FnOnce(&mut KonaBackend) -> R) -> R { + let s = &*self.shared; + // The stores in the ring run first; the thread then holds the pipe only + // for a batch. + while s.running.load(SeqCst) && !s.dead.load(SeqCst) && !s.ring.is_empty() { + s.wake(); + std::thread::yield_now(); + } + let mut guard = s.pipe.lock(); + f(&mut guard.backend) + } + + /// Bring the back end to rest for a snapshot. The machine's processors are + /// stopped: the stores in the ring run to completion, then the back end + /// finishes its queued work and parks its threads. `resume` undoes it. + pub fn quiesce(&self) { + let s = &*self.shared; + while s.running.load(SeqCst) && !s.dead.load(SeqCst) && !s.ring.is_empty() { + s.wake(); + std::thread::yield_now(); + } + s.pipe.lock().backend.quiesce(); + } + + /// The board's state as chunks, for a snapshot: the pixel store, the cursor memory and the + /// texture arena, and the small state (the register file, the pipe's registers, pages, + /// contexts and memories, the display tables, the textport and every rendering context + /// with its level map) as one more list named `state`. The board is quiesced, and the + /// stream is between commands: a snapshot taken in the middle of one is refused, so is + /// one with a primitive or a pixel transfer open. Counters are not saved. + pub fn save_chunks(&self, store: &crate::chunk_store::ChunkStore) -> std::io::Result> { + let io = |e: String| std::io::Error::new(std::io::ErrorKind::InvalidInput, e); + let mut pipe = self.shared.pipe.lock(); + let p = &mut *pipe; + let state = self.state_bytes(p).map_err(io)?; + let mut chunks = p.backend.save_chunks(store)?; + let n = state.len(); + chunks.push(KonaChunk { name: format!("state-{n}"), hashes: back::snapshot::put_bytes(store, &state)? }); + Ok(chunks) + } + + /// Restore what `save_chunks` wrote, into a board of the same configuration, all or nothing. + /// Every chunk is read, and the small state is parsed once over throwaway objects, before + /// the board is touched; only then does the real pass run (which cannot fail on the bytes, + /// since a visit is a function of them alone), the bulk state go in, and the flash image, + /// the one effect outside the board, be written through to its file last. + pub fn load_chunks(&self, store: &crate::chunk_store::ChunkStore, list: &[KonaChunk]) -> Result<(), String> { + let (state, rest): (Vec<_>, Vec<_>) = list.iter().cloned().partition(|c| c.name.starts_with("state-")); + let [c] = &state[..] else { return Err("a snapshot holds one state list".into()) }; + let n: usize = c.name["state-".len()..].parse().map_err(|_| "the state list's size is not a number")?; + let blob = back::snapshot::get_bytes(store, &c.hashes, n)?; + let mut pipe = self.shared.pipe.lock(); + let p = &mut *pipe; + let prepared = p.backend.prepare_chunks(store, &rest)?; + self.load_state(p, &blob)?; + p.backend.apply_chunks(prepared)?; + p.core.flash.write_through(); + self.shared.host.activity.store(false, Relaxed); + self.shared.publish(&p.core); + Ok(()) + } + + /// The small state as the bytes `load_state` reads: the register file, the pipe's registers, + /// pages, queues, contexts and memories, and the back end's small state. Refused between the + /// words of a command and with a primitive or a pixel transfer open. + fn state_bytes(&self, p: &mut Pipe) -> Result, String> { + p.core.at_boundary()?; + let mut w = stream::Writer::default(); + self.shared.host.front.lock().visit(&mut w); + p.core.visit(&mut w); + p.backend.visit_small(&mut w)?; + Ok(w.out) + } + + /// Read the small state from `blob`: parsed once over throwaway objects, which changes + /// nothing and refuses a blob that is cut short or has bytes left, then read into the board. + fn load_state(&self, p: &mut Pipe, blob: &[u8]) -> Result<(), String> { + { + let cfg = p.core.cfg.clone(); + let mut r = stream::Reader::new(blob); + Front::new(&cfg).visit(&mut r); + Core::new(cfg, flash::Flash::fresh()).visit(&mut r); + KonaBackend::check_small(&mut r); + r.finish()?; + } + let mut r = stream::Reader::new(blob); + self.shared.host.front.lock().visit(&mut r); + p.core.visit(&mut r); + p.backend.visit_small(&mut r)?; + r.finish() + } + + /// Undo `quiesce`. + pub fn resume(&self) { + self.shared.pipe.lock().backend.resume(); + } + + fn reset(&self) { + let was_running = self.is_running(); + Device::stop(self); + let s = &*self.shared; + s.ring.reset(); + { + let mut pipe = s.pipe.lock(); + pipe.core.reset(); + // A new epoch before the back end cancels its tickets, so a call + // a thread of its made in the old one is dropped. + s.host.front.lock().advance_epoch(); + pipe.backend.reset(); + s.publish(&pipe.core); + } + s.host.front.lock().reset(); + s.host.activity.store(false, Relaxed); + if was_running { + Device::start(self); + } + } +} + +/// The processor's access to the widget's aperture. Only the register-sized +/// widths are served; the others answer `BUS_ERR`. +impl BusDevice for Kona { + fn read32(&self, addr: u64) -> BusRead32 { + match self.bus_read(addr, 4) { + (BUS_OK, v) => BusRead32::ok(v as u32), + (BUS_BUSY, _) => BusRead32::busy(), + _ => BusRead32::err(), + } + } + + fn read64(&self, addr: u64) -> BusRead64 { + match self.bus_read(addr, 8) { + (BUS_OK, v) => BusRead64::ok(v), + (BUS_BUSY, _) => BusRead64::busy(), + _ => BusRead64::err(), + } + } + + fn write32(&self, addr: u64, val: u32) -> u32 { + self.bus_write(addr, 4, val as u64) + } + + fn write64(&self, addr: u64, val: u64) -> u32 { + self.bus_write(addr, 8, val) + } +} + +impl Device for Kona { + fn step(&self, _cycles: u64) {} + + fn start(&self) { + let mut t = self.thread.lock(); + if t.is_some() { + return; + } + let s = self.shared.clone(); + s.pipe.lock().backend.attach(DisplayHandle::new(&s.host)); + s.exit.store(false, SeqCst); + s.running.store(true, SeqCst); + *t = Some( + std::thread::Builder::new() + .name("kona-hip".into()) + .spawn(move || s.thread_main()) + .expect("kona thread"), + ); + } + + fn stop(&self) { + let Some(t) = self.thread.lock().take() else { return }; + let s = &*self.shared; + s.exit.store(true, SeqCst); + s.wake(); + if let Some(th) = s.sleeper.lock().as_ref() { + th.unpark(); + } + let _ = t.join(); + s.running.store(false, SeqCst); + // What the thread had not taken runs here, unless it died: its back + // end would only panic again. + if !s.dead.load(SeqCst) { + s.drain_inline(); + } + // The back end's own threads join with the slot taken and no + // register-file lock held: one of them may be inside a handle call, + // which takes only that lock and sends its packet after dropping it, + // and never asks for the slot. + // Always, even for a dead board: `start` attaches again, and the + // first run's threads must be gone by then. A back end that panics + // here is logged, not rethrown, since this runs from `Drop`. + let stopped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| s.pipe.lock().backend.stop())); + if stopped.is_err() { + crate::dlog!(crate::devlog::LogModule::Kona, "kona: the back end panicked while stopping"); + } + } + + fn is_running(&self) -> bool { + self.shared.running.load(SeqCst) + } + + fn get_clock(&self) -> u64 { + 0 + } + + fn register_commands(&self) -> Vec<(String, String)> { + vec![("kona".into(), "InfiniteReality pipe: status|counters|hash|pix X Y|shot FILE.png (kona help)".into())] + } + + fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { + if cmd != "kona" { + return Err(format!("unknown kona command: {cmd}")); + } + // Pixel reads never wait. The commands that read the back end's state + // wait for the command stream at most this long, then say so. + match self.shared.pixels.read(args, &mut *w) { + Ok(true) => return Ok(()), + Ok(false) => {} + Err(e) => return Err(e.to_string()), + } + let Some(mut guard) = self.shared.pipe.try_lock_for(std::time::Duration::from_nanos(self.shared.monitor_wait.load(Relaxed))) else { + return Err("kona: board busy".into()); + }; + match guard.backend.monitor(args, &mut *w) { + Ok(true) => Ok(()), + Ok(false) => Err("kona: status|counters|hash|pix X Y|shot FILE.png".into()), + Err(e) => Err(e.to_string()), + } + } +} + +impl Drop for Kona { + fn drop(&mut self) { + Device::stop(self); + } +} + +impl Resettable for Kona { + fn power_on(&self) { + self.reset(); + } +} diff --git a/src/dev/kona/pipe.rs b/src/dev/kona/pipe.rs new file mode 100644 index 000000000..312d764ac --- /dev/null +++ b/src/dev/kona/pipe.rs @@ -0,0 +1,1605 @@ +//! The host interface processor's state and the command stream it runs. +//! +//! `Core` is everything the board's thread owns: the framers that turn the +//! command page's words into commands, the pages the pipe answers on, the +//! contexts, the flash, the map RAM, the display-list memory and the DMA +//! state. A processor's store to a command port is queued and reaches `Core` +//! in order; a register read that depends on the stream waits until the +//! stream has run (the bus answers busy meanwhile), then reaches it too. +//! +//! The command page takes plain words. A word is an argument of the command +//! still open, else it opens one: the textport's sync word starts a textport +//! command, a kernel or graphics-process token starts a command of the kernel +//! table, and a GL selector starts a GL command. How the grammars share the +//! page is inferred from the senders (see `Core::textport_push`); the pipe's +//! own rule is unknown. +//! +//! Ported from hw/misc/sgi-kona.c in jamesbraid/qemu-sgi-origami. + +use super::defs::*; +use super::dma::Dma; +use super::flash::{Action as FlashAction, Flash, FlashOp}; +use super::frame::{tables, Packet, Port, Step, FrameTables}; +use super::front::Host; +use super::ident::{self, KonaConfig}; +use super::back::KonaBackend; +use super::job::{BackPort, ContextEvent, Grammar, Job, Reply}; +use crate::devlog::LogModule; +use std::collections::VecDeque; + +/// A run of words behind a command, or a command taking its words by a rule +/// of its own. +#[derive(Clone)] +pub(super) enum Run { + None, + /// Words behind a command: a colour map, a gamma table, a cursor, a + /// texture table, or what the back end asked for. They come back as one + /// `Job::Data`. + Words { grammar: Grammar, selector: u32, args: Vec, words: Vec, remaining: u32 }, + /// A video command: its number, then data words and a checksum equal to + /// the number plus the data words. + Video { command: u32, data: Vec, sum: u32 }, + /// A flash EEPROM operation. + Flash(FlashOp), + /// A list invocation taking its (offset, word count) pairs. + Calls { token: u32, need: usize, words: Vec }, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BitmapState { + Idle, + Header, + Payload, +} + +/// A textport bitmap blit in progress: six header words, then +/// ceil(width * height / 32) payload words. +#[derive(Clone)] +struct Bitmap { + state: BitmapState, + header: Vec, + payload: Vec, + expected: usize, +} + +impl Bitmap { + fn idle() -> Self { + Bitmap { state: BitmapState::Idle, header: Vec::new(), payload: Vec::new(), expected: 0 } + } +} + +/// Where the command stream is: everything a context switch sets aside and +/// brings back with the context. +#[derive(Clone)] +pub(super) struct Input { + textport: Port, + gl: Port, + /// A user command the kernel framer had open when a forced store set it + /// aside. + kernel_suspended: Port, + /// The textport expects its sync word. + expect_sync: bool, + /// A sync word was seen after the GL grammar began: the next word says + /// whether it was the textport's or the start of a buffer configuration. + gl_sync_held: bool, + /// 1 after a held `1 1`, 2 after its third word. + level1: u8, + level1_arg: u32, + /// The top half of the opening word of the GL command open. + gl_token_high: u32, + bitmap: Bitmap, + pub run: Run, + /// The argument and word buffers of the last data run, kept so the next + /// run allocates nothing. + spare_args: Vec, + spare_words: Vec, +} + +impl Input { + /// The stream's position between commands, as the stream module reads and writes it. + fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { textport: _, gl: _, kernel_suspended: _, expect_sync: _, gl_sync_held: _, level1: _, level1_arg: _, gl_token_high: _, bitmap: _, run: _, spare_args: _, spare_words: _ } = self; + s.bool(&mut self.expect_sync); + s.bool(&mut self.gl_sync_held); + s.u8(&mut self.level1); + s.u32(&mut self.level1_arg); + s.u32(&mut self.gl_token_high); + if s.loading() { + self.textport.reset(); + self.gl.reset(); + self.kernel_suspended.reset(); + self.bitmap = Bitmap::idle(); + self.run = Run::None; + } + } + + fn new(t: &'static FrameTables) -> Self { + Input { + textport: Port::new(&t.textport), + gl: Port::new(&t.gl), + kernel_suspended: Port::new(&t.kernel), + expect_sync: true, + gl_sync_held: false, + level1: 0, + level1_arg: 0, + gl_token_high: 0, + bitmap: Bitmap::idle(), + run: Run::None, + spare_args: Vec::new(), + spare_words: Vec::new(), + } + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct Context { + save_page: u32, + save_offset: u32, + return_page: Option, +} + +/// The most contexts kept, and the entry transient ones (a restore of a save +/// area nobody set up) use. A host limit, not the pipe's capacity. +const CONTEXT_LIMIT: usize = CONTEXT_SLOTS - 1; +const CONTEXT_TRANSIENT: usize = CONTEXT_LIMIT; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum SwapStage { + Idle, + Mode, + Data, + Reason, +} + +struct SwapIn { + stage: SwapStage, + header: [u32; 6], + mode: u32, + data_left: u64, +} + +pub(super) struct Core { + pub cfg: KonaConfig, + tables: &'static FrameTables, + pub input: Input, + saved: Vec>>, + /// The kernel/graphics-process framer and its dma-fifo twin. + pub kernel: Port, + pub dma_fifo: Port, + /// The privileged bit of the bracket register is set: the words are a + /// kernel command's. + kernel_input: bool, + /// The kernel command now open was opened under that bit. + kernel_privileged: bool, + /// A forced store suspended the current context's user command. + suspended_owner: Option, + /// Once a context has been activated the page speaks the GL grammar. + gl_grammar: bool, + swapin: SwapIn, + + // Pages the pipe answers on. + pub return_page: Option, + pub(super) reason_page: Option, + reason_unread: bool, + reason_posted: u32, + reason_queue: VecDeque<(u32, [u32; REASON_WORDS], usize)>, + + // Contexts. + contexts: Vec, + context_count: usize, + pub current: Option, + context_page_pending: bool, + /// Why the pipe stopped taking commands, if it did. + pub failed: Option, + + pub managed: (u32, u32), + + pub map_ram: Box<[u32]>, + pub flash: Flash, + pub dma: Dma, + pub dma_fault: bool, + + // The indirect port into the pipe-internal address space. + pub(super) port_addr: u32, + pub(super) icu_control: u32, + pub(super) arm_low: [u32; ARM_LOW_WORDS], + pub(super) arm_high: Vec, + pub(super) load_open: bool, + pub(super) load_count: usize, + pub(super) load_sections: Vec, + pub(super) load_record: Vec, + pub(super) indirect_select: u32, +} + +/// One section of the firmware load: where its words started and how many +/// there were. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LoadSection { + pub addr: u32, + pub words: u32, +} + +impl Core { + pub fn new(cfg: KonaConfig, flash: Flash) -> Self { + let t = tables(); + Core { + cfg, + tables: t, + input: Input::new(t), + saved: (0..=CONTEXT_LIMIT).map(|_| None).collect(), + kernel: Port::new(&t.kernel), + dma_fifo: Port::new(&t.dma_fifo), + kernel_input: false, + kernel_privileged: false, + suspended_owner: None, + gl_grammar: false, + swapin: SwapIn { stage: SwapStage::Idle, header: [0; 6], mode: 0, data_left: 0 }, + return_page: None, + reason_page: None, + reason_unread: false, + reason_posted: 0, + reason_queue: VecDeque::new(), + contexts: vec![Context::default(); CONTEXT_LIMIT + 1], + context_count: 0, + current: None, + context_page_pending: false, + failed: None, + managed: (1280, 1024), + map_ram: vec![0u32; MAPRAM_ENTRIES].into_boxed_slice(), + flash, + dma: Dma::new(), + dma_fault: false, + port_addr: 0, + icu_control: 0, + arm_low: [0; ARM_LOW_WORDS], + arm_high: vec![0; ARM_HIGH_WORDS], + load_open: false, + load_count: 0, + load_sections: Vec::new(), + load_record: Vec::new(), + indirect_select: 0, + } + } + + /// Whether the stream is between commands, which is the only place a snapshot is + /// taken: no framer has a command open, no run waits for its words and no swap is + /// part way in (the context table's saved streams too). + pub(super) fn at_boundary(&self) -> Result<(), String> { + let idle = |i: &Input| !i.textport.framing() && !i.gl.framing() && !i.kernel_suspended.framing() && matches!(i.run, Run::None) && i.bitmap.state == BitmapState::Idle; + let still = idle(&self.input) && self.saved.iter().flatten().all(|i| idle(i)) && !self.kernel.framing() && !self.dma_fifo.framing() && self.swapin.stage == SwapStage::Idle && !self.dma.running(); + if still { Ok(()) } else { Err("the command stream is in the middle of a command".into()) } + } + + /// The pipe's registers, pages, contexts and memories, as the stream module reads + /// and writes them. Save only `at_boundary`; the configuration and the framing tables + /// are the build's. A loaded stream starts with every framer idle. + pub(super) fn visit(&mut self, s: &mut S) { + // Every field is named, so a new one fails to compile until it is saved here or left out on purpose. + let Self { cfg: _, tables: _, input: _, saved: _, kernel: _, dma_fifo: _, kernel_input: _, kernel_privileged: _, suspended_owner: _, gl_grammar: _, swapin: _, return_page: _, reason_page: _, reason_unread: _, reason_posted: _, reason_queue: _, contexts: _, context_count: _, current: _, context_page_pending: _, failed: _, managed: _, map_ram: _, flash: _, dma: _, dma_fault: _, port_addr: _, icu_control: _, arm_low: _, arm_high: _, load_open: _, load_count: _, load_sections: _, load_record: _, indirect_select: _ } = self; + self.input.visit(s); + s.len(self.saved.len()); + let t = self.tables; + for slot in self.saved.iter_mut() { + let mut some = slot.is_some(); + s.bool(&mut some); + if s.loading() { + *slot = some.then(|| Box::new(Input::new(t))); + } + if let Some(i) = slot { + i.visit(s); + } + } + s.bool(&mut self.kernel_input); + s.bool(&mut self.kernel_privileged); + s.opt(&mut self.suspended_owner, |s, v| s.usize(v)); + s.bool(&mut self.gl_grammar); + s.opt(&mut self.return_page, |s, v| s.u64(v)); + s.opt(&mut self.reason_page, |s, v| s.u64(v)); + s.bool(&mut self.reason_unread); + s.u32(&mut self.reason_posted); + let n = s.len(self.reason_queue.len()); + if s.loading() { + self.reason_queue.clear(); + self.reason_queue.resize(n.min(1 << 16), (0, [0; REASON_WORDS], 0)); + } + for (a, w, b) in self.reason_queue.iter_mut() { + s.u32(a); + s.u32s(w); + s.usize(b); + } + s.len(self.contexts.len()); + for c in self.contexts.iter_mut() { + s.u32(&mut c.save_page); + s.u32(&mut c.save_offset); + s.opt(&mut c.return_page, |s, v| s.u64(v)); + } + s.usize(&mut self.context_count); + s.opt(&mut self.current, |s, v| s.usize(v)); + s.bool(&mut self.context_page_pending); + let mut failed = self.failed.clone().unwrap_or_default(); + let mut has = self.failed.is_some(); + s.bool(&mut has); + s.string(&mut failed); + if s.loading() { + self.failed = has.then_some(failed); + } + s.u32(&mut self.managed.0); + s.u32(&mut self.managed.1); + s.u32s(&mut self.map_ram); + self.flash.visit(s); + self.dma.visit(s); + s.bool(&mut self.dma_fault); + s.u32(&mut self.port_addr); + s.u32(&mut self.icu_control); + s.u32s(&mut self.arm_low); + s.vec_u32(&mut self.arm_high); + s.bool(&mut self.load_open); + s.usize(&mut self.load_count); + for list in [&mut self.load_sections, &mut self.load_record] { + let n = s.len(list.len()); + if s.loading() { + list.clear(); + list.resize(n, LoadSection { addr: 0, words: 0 }); + } + for l in list.iter_mut() { + s.u32(&mut l.addr); + s.u32(&mut l.words); + } + } + s.u32(&mut self.indirect_select); + if s.loading() { + for p in [&mut self.kernel, &mut self.dma_fifo] { + p.reset(); + } + self.swapin.stage = SwapStage::Idle; + self.swapin.data_left = 0; + } + } + + /// Power-on state. The flash and the configuration stay. + pub fn reset(&mut self) { + let t = self.tables; + self.input = Input::new(t); + self.saved.iter_mut().for_each(|s| *s = None); + self.kernel.reset(); + self.dma_fifo.reset(); + self.kernel_input = false; + self.kernel_privileged = false; + self.suspended_owner = None; + self.gl_grammar = false; + self.swapin.stage = SwapStage::Idle; + self.return_page = None; + self.reason_page = None; + self.reason_unread = false; + self.reason_posted = 0; + self.reason_queue.clear(); + self.contexts.iter_mut().for_each(|c| *c = Context::default()); + self.context_count = 0; + self.current = None; + self.context_page_pending = false; + self.failed = None; + self.managed = (1280, 1024); + self.map_ram.fill(0); + self.dma.reset(); + self.dma_fault = false; + self.port_addr = 0; + self.icu_control = 0; + self.arm_low = [0; ARM_LOW_WORDS]; + self.arm_high.fill(0); + self.load_open = false; + self.load_count = 0; + self.load_sections.clear(); + self.load_record.clear(); + self.indirect_select = 0; + } + + /// Words the framers hold of commands still arriving. + pub fn held_words(&self) -> u32 { + self.input.textport.held() + self.kernel.held() + self.input.gl.held() + self.dma_fifo.held() + } + + pub fn reasons_waiting(&self) -> bool { + !self.reason_queue.is_empty() + } + + pub(super) fn job(&mut self, h: &Host, be: &mut KonaBackend, job: Job<'_>) -> Reply { + be.job(&mut BackPort { core: self, host: h }, job) + } + + // ----- the command page ------------------------------------------------ + + /// A plain word at the command page's offset 0 or its data slot, as a + /// fetched display list's words go through it. + pub fn command_word(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + if self.failed.is_none() { + self.textport_push(h, be, word); + } + } + + /// A store to the command page at `offset`. + /// + /// Offset 0 and the data slot (0xc) both carry a plain word. A store at + /// 0xc + 0x20 * t opens token t with the stored value as its first + /// argument, the way the alternative X driver module sends its commands. + /// Approximated: the decoding is inferred from the stores the senders + /// make. What distinguishes offset 0 from 0xc, and what the low address + /// bits of a slot store do, are unknown, so slot stores are accepted at + /// 0xc only, and any other offset is logged and dropped. + pub fn command_page_write(&mut self, h: &Host, be: &mut KonaBackend, offset: u32, value: u32) { + if self.failed.is_some() { + return; + } + let token = (offset >> COMMAND_SLOT_SHIFT) & COMMAND_TOKEN_MASK; + if offset == 0 || offset == COMMAND_DATA_SLOT { + h.activity.store(true, std::sync::atomic::Ordering::Relaxed); + self.command_input(h, be, value); + return; + } + if token == 0 || offset & COMMAND_SLOT_MASK != COMMAND_DATA_SLOT { + crate::dlog!(LogModule::Kona, "kona: unexpected store of {value:#x} at command page offset {offset:#x}; dropped"); + return; + } + if !self.kernel.knows(token) { + crate::dlog!( + LogModule::Kona, + "kona: unknown token {token:#x} stored at command page offset {offset:#x}; value {value:#x} dropped" + ); + return; + } + if !self.kernel_input { + self.user_input(); + } + self.drop_open_commands(token, offset); + h.activity.store(true, std::sync::atomic::Ordering::Relaxed); + self.kernel_word(h, be, token); + self.push_kernel(h, be, value); + } + + /// A slot store ends whatever was still collecting words: the partial + /// command is dropped, logged, and the new one opens. + fn drop_open_commands(&mut self, token: u32, offset: u32) { + let at = |what: String| { + crate::dlog!(LogModule::Kona, "kona: token {token:#x} stored at command page offset {offset:#x} while {what}; partial command dropped"); + }; + if self.kernel.framing() { + at(format!("token {:#x} still needs {} words", self.kernel.selector(), self.kernel.missing())); + self.kernel.reset(); + } + if self.input.gl.framing() { + at(format!("token {:#x} still needs {} words", self.input.gl.selector(), self.input.gl.missing())); + self.input.gl.reset(); + } + if self.input.gl_sync_held { + at("a held 1 waits (the 1 is dropped)".into()); + self.input.gl_sync_held = false; + self.input.expect_sync = true; + } + if self.input.level1 != 0 { + at("a held 1 1 waits (the words are dropped)".into()); + self.input.level1 = 0; + self.input.expect_sync = true; + } + match std::mem::replace(&mut self.input.run, Run::None) { + Run::None => {} + Run::Words { selector, remaining, .. } => { + at(format!("token {selector:#x} still needs {remaining} words")); + } + Run::Video { command, data, .. } => { + at(format!("video command {command:#x} awaits its checksum after {} data words", data.len())); + } + Run::Flash(op) => { + let _ = op; + at("an EEPROM operation is still taking words".into()); + } + Run::Calls { .. } => at("a list invocation still takes its ranges".into()), + } + } + + /// A word on the command page. While the kernel's privileged bit is set + /// the word belongs to a kernel command and goes to the kernel framer + /// alone, leaving any suspended user command untouched. A privileged word + /// that opens no kernel command is logged and ignored while a user + /// command is suspended; with none suspended it takes the ordinary path. + /// Otherwise a suspended command resumes if its context is current again; + /// if another context sends first the suspended command is saved with its + /// owner, and a context whose saved command waits gets it back before its + /// first word. + fn command_input(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + if self.swapin.stage != SwapStage::Idle && self.swap_in_word(h, word) { + return; + } + if self.kernel_input { + if self.kernel.framing() || self.kernel.knows(word) { + self.kernel_word(h, be, word); + return; + } + if self.suspended_owner.is_some() { + crate::dlog!(LogModule::Kona, "kona: privileged word {word:#x} opens no kernel command; ignored"); + return; + } + self.textport_push(h, be, word); + return; + } + self.user_input(); + if self.gl_grammar && kernel_only_token(word) && !self.user_command_open() && !self.kernel.framing() { + crate::dlog!(LogModule::Kona, "kona: kernel-only token {word:#x} without the privileged bit"); + } + self.textport_push(h, be, word); + } + + /// The current context's user input, before its word or slot store is + /// taken. + fn user_input(&mut self) { + if let Some(owner) = self.suspended_owner.take() { + if Some(owner) == self.current || !self.user_command_open() { + self.kernel_resume(); + } else { + self.input_save(owner); + } + } + if let Some(cur) = self.current { + if self.saved[cur].is_some() && !self.user_command_open() { + self.input_restore(cur); + } + } + } + + /// A store to the bracket register. Force ends argument collection: a + /// kernel command still open is abandoned, logged, and an open user + /// command is suspended with the current context. A command the kernel + /// framer opened without the privileged bit is a user command (the X + /// server's, or a GL client's list call): it is set aside so the kernel's + /// packet frames on an idle framer. Privileged, with or without force, + /// makes the following words a kernel command's; 0 returns the input to + /// user commands. + pub fn bracket_write(&mut self, value: u32) { + if value & !(BRACKET_FORCE | BRACKET_PRIVILEGED) != 0 { + crate::dlog!(LogModule::Kona, "kona: bracket store {value:#x} sets bits the kernel never sets"); + } + if value & BRACKET_FORCE != 0 { + if self.kernel.framing() && !self.kernel_privileged && !self.input.kernel_suspended.framing() { + self.input.kernel_suspended = self.kernel.clone(); + self.kernel.reset(); + } else if self.kernel.framing() { + crate::dlog!(LogModule::Kona, "kona: forced store ends kernel command {:#x} short of its words", self.kernel.selector()); + self.kernel.reset(); + } + if self.suspended_owner.is_none() && self.user_command_open() { + self.suspended_owner = self.current; + } + } + self.kernel_input = value & BRACKET_PRIVILEGED != 0; + } + + /// Whether a user command is part-way through its words. + fn user_command_open(&self) -> bool { + let i = &self.input; + (self.kernel.framing() && !self.kernel_privileged) + || i.kernel_suspended.framing() + || i.textport.framing() + || i.gl.framing() + || i.gl_sync_held + || i.level1 != 0 + || !matches!(i.run, Run::None) + || i.bitmap.state != BitmapState::Idle + } + + /// Put a user command the kernel framer had open back on it. + fn kernel_resume(&mut self) { + if !self.input.kernel_suspended.framing() { + return; + } + if self.kernel.framing() { + crate::dlog!( + LogModule::Kona, + "kona: kernel command {:#x} still open when a user command resumes; it ends short of its words", + self.kernel.selector() + ); + self.kernel.reset(); + } + self.kernel = std::mem::replace(&mut self.input.kernel_suspended, Port::new(&self.tables.kernel)); + self.kernel_privileged = false; + } + + /// Copy the open user command out to `owner`'s saved slot and leave the + /// input idle for the context now sending words: the pipe is inferred to + /// keep a context's open command with the context. + fn input_save(&mut self, owner: usize) { + let fresh = Input::new(self.tables); + self.saved[owner] = Some(Box::new(std::mem::replace(&mut self.input, fresh))); + } + + fn input_restore(&mut self, owner: usize) { + if let Some(v) = self.saved[owner].take() { + self.input = *v; + } + } + + // ----- feeding the framers --------------------------------------------- + + /// A word for the kernel framer. A word that opens a command records + /// whether the privileged bit was set, which decides what a forced store + /// does to the command if it is still open. + fn kernel_word(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + if !self.kernel.framing() { + self.kernel_privileged = self.kernel_input; + } + self.push_kernel(h, be, word); + } + + fn push_kernel(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + if self.kernel.push(word) == Step::Packet { + let p = self.kernel.packet(); + self.kernel_packet(h, be, p); + } + } + + fn push_textport(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + match self.input.textport.push(word) { + Step::Packet => { + let p = self.input.textport.packet(); + self.textport_packet(h, be, p); + } + Step::Unknown => { + crate::dlog!(LogModule::Kona, "kona: unsupported textport selector {word:#x}"); + self.job(h, be, Job::Dropped { word }); + self.input.expect_sync = true; + } + Step::Partial => {} + } + } + + fn push_gl(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + if self.input.gl.push(word) == Step::Packet { + let p = self.input.gl.packet(); + let packed = self.input.gl_token_high; + let reply = self.job(h, be, Job::Command { grammar: Grammar::Gl, selector: p.selector, args: p.args(), packed, handled: false }); + self.open_run(Grammar::Gl, &p, reply); + // Known runs of the GL grammar. + if p.selector == LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE && reply == Reply::Done { + self.start_words(Grammar::Gl, &p, GL_TEX_TABLE_WORDS); + } + } + } + + pub fn push_dma_fifo(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + match self.dma_fifo.push(word) { + Step::Packet => { + let p = self.dma_fifo.packet(); + match p.selector { + START_INPUT_DMA | COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA | DESCRIBE_DMA_PAGE_POOL => self.dma_command(h, be, p.selector, p.args()), + _ => { + // The kernel FIFO table's other selectors belong to + // other mechanisms (the display-mode set, select feed): + // framed so the port stays in step, not modelled. + crate::dlog!(LogModule::Kona, "kona: dma fifo selector {:#05x} (argc {}) framed, not modelled", p.selector, p.argc); + } + } + } + Step::Unknown => { + crate::dlog!(LogModule::Kona, "kona: dma fifo word {word:#010x} is not a selector the port frames; dropped"); + self.job(h, be, Job::Dropped { word }); + } + Step::Partial => {} + } + } + + /// The reply of a job that may want data words behind its command. + fn open_run(&mut self, grammar: Grammar, p: &Packet, reply: Reply) { + if let Reply::Data(n) = reply { + self.start_words(grammar, p, n); + } + } + + fn start_words(&mut self, grammar: Grammar, p: &Packet, n: u32) { + if n == 0 { + return; + } + let mut args = std::mem::take(&mut self.input.spare_args); + args.clear(); + args.extend_from_slice(p.args()); + let mut words = std::mem::take(&mut self.input.spare_words); + words.clear(); + words.reserve((n as usize).min(1 << 16)); + self.input.run = Run::Words { grammar, selector: p.selector, args, words, remaining: n }; + } + + /// A word while a run takes its words. + fn run_word(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + match &mut self.input.run { + Run::None => unreachable!("run_word with no run open"), + Run::Words { words, remaining, .. } => { + words.push(word); + *remaining -= 1; + if *remaining == 0 { + let Run::Words { grammar, selector, args, words, .. } = std::mem::replace(&mut self.input.run, Run::None) else { + unreachable!() + }; + let r = self.job(h, be, Job::Data { grammar, selector, args: &args, words: &words }); + debug_assert!(r != Reply::Unclaimed); + self.input.spare_args = args; + self.input.spare_words = words; + } + } + Run::Video { command, data, sum } => { + let known = super::tables::VIDEO.get(*command as usize).copied().flatten(); + let done = match known { + Some(n) => data.len() == n as usize, + None => word == command.wrapping_add(*sum), + }; + if done { + let (command, data, expect) = (*command, std::mem::take(data), command.wrapping_add(*sum)); + self.input.run = Run::None; + self.video_complete(h, be, command, &data, word, expect); + return; + } + if known.is_none() && data.len() + 1 == VIDEO_SEARCH_MAX_WORDS { + crate::dlog!( + LogModule::Kona, + "kona: video command {command:#x} has no checksum within {VIDEO_SEARCH_MAX_WORDS} data words; closed, nothing posted" + ); + self.input.run = Run::None; + return; + } + data.push(word); + *sum = sum.wrapping_add(word); + } + Run::Flash(op) => { + let action = op.word(&mut self.flash, word); + if !op.open() { + self.input.run = Run::None; + } + self.flash_action(h, action); + } + Run::Calls { token, need, words } => { + words.push(word); + if words.len() == *need { + let (token, words) = (*token, std::mem::take(words)); + self.input.run = Run::None; + self.dma_invoke(h, be, token == EXECUTE_LIST_BY_INPUT_DMA_REPEATED, &words); + } + } + } + } + + fn video_complete(&mut self, h: &Host, be: &mut KonaBackend, command: u32, data: &[u32], checksum: u32, expect: u32) { + if checksum != expect { + crate::dlog!(LogModule::Kona, "kona: video command {command:#x} checksum {checksum:#x} is not {expect:#x}; dropped"); + return; + } + if command as usize >= super::tables::VIDEO.len() { + crate::dlog!(LogModule::Kona, "kona: unmodelled video command {command:#x}; consumed, nothing posted"); + return; + } + self.job(h, be, Job::Video { command, data }); + } + + // ----- the textport and its neighbours --------------------------------- + + /// Whether a word opens a GL command, and its selector. + /// + /// Once the page speaks the GL grammar, the selector is the word's low 16 + /// bits: the byte and short attribute forms put data in the top half, and + /// no form read uses bits 0 to 15 for data (that no selector is wider + /// than 16 bits is inferred). Before the first + /// context activation only the whole words of `gl_token_early` open one. + fn gl_token_word(&self, word: u32) -> Option { + let low = word & 0xffff; + if !self.gl_grammar && (word != low || !gl_token_early(low)) { + return None; + } + self.tables.gl.get(low).map(|_| low) + } + + fn gl_open(&mut self, h: &Host, be: &mut KonaBackend, word: u32, selector: u32) { + self.input.gl_token_high = word & 0xffff_0000; + self.push_gl(h, be, selector); + } + + /// The textport's own bulk-transfer framing, layered in front of the + /// generic framer: strip the wire protocol's leading sync word, then + /// either forward a word to the framers or, mid bitmap blit (command 0xa, + /// excluded from the selector table for exactly this reason), accumulate + /// it into the header or payload. + /// + /// The GL client's commands 0x1 to 0xa and 0xc have the numbers of + /// textport commands, and a textport command always follows the sync + /// word, so a GL command meets a textport number only in the word after + /// a sync word. How the pipe tells the grammars apart is unknown. + /// Approximated: + /// before the first context activation the page speaks only the textport + /// grammar, and a bare word opens a GL command only when it is a whole + /// selector of `gl_token_early`. After it, every word between packets + /// whose low 16 bits are a GL selector opens that command, and a 1 is + /// held until the next word: a textport command makes it the sync word, + /// anything else makes it the buffer configuration with that word first. + /// + /// A level-1 (overlay) buffer configuration is told apart from the + /// textport's command 1 by looking two words further on: while a context + /// is current, `1 1 a b` is the configuration `1 a b ...` only when b is + /// 0, 2, 4 or 8, the index sizes the overlay visuals carry. Approximated; + /// the rule misreads a buffer configuration whose level is a textport + /// command number and takes a stray word whose low half is a GL selector + /// as that command. + fn textport_push(&mut self, h: &Host, be: &mut KonaBackend, word: u32) { + if !matches!(self.input.run, Run::None) { + self.run_word(h, be, word); + return; + } + // Kernel commands only start where a textport command could, between + // packets, so the framings never interleave. + if self.kernel.framing() { + self.push_kernel(h, be, word); + return; + } + if self.input.gl.framing() { + self.push_gl(h, be, word); + return; + } + if self.input.expect_sync { + if self.gl_grammar && word == TEXTPORT_SYNC_WORD { + self.input.gl_sync_held = true; + self.input.expect_sync = false; + return; + } + if word != TEXTPORT_SYNC_WORD && self.kernel.knows(word) { + self.kernel_word(h, be, word); + return; + } + if let Some(selector) = self.gl_token_word(word) { + self.gl_open(h, be, word, selector); + return; + } + if word != TEXTPORT_SYNC_WORD { + // Among these are the words of a command whose token has no + // length here: they arrive one by one as fresh words. + crate::dlog!(LogModule::Kona, "kona: textport expected a sync word, got {word:#x}"); + self.job(h, be, Job::Dropped { word }); + return; + } + self.input.expect_sync = false; + return; + } + if self.input.level1 == 1 { + self.input.level1_arg = word; + self.input.level1 = 2; + return; + } + if self.input.level1 == 2 { + self.input.level1 = 0; + let arg = self.input.level1_arg; + if !matches!(word, 0 | 2 | 4 | 8) { + // Textport command 1 and its argument, then the word again. + self.push_textport(h, be, CONFIGURE_FRAME_BUFFER_LAYOUT); + self.push_textport(h, be, arg); + self.textport_push(h, be, word); + return; + } + self.input.expect_sync = true; + self.input.gl_token_high = 0; + for w in [CONFIGURE_FRAME_BUFFER_LAYOUT, 1, arg, word] { + self.push_gl(h, be, w); + } + return; + } + if self.input.gl_sync_held { + self.input.gl_sync_held = false; + if word == 1 && self.current.is_some() { + self.input.level1 = 1; + return; + } + if !self.textport_selector_known(word) { + self.input.expect_sync = true; + self.input.gl_token_high = 0; + self.push_gl(h, be, CONFIGURE_FRAME_BUFFER_LAYOUT); + self.push_gl(h, be, word); + return; + } + } + match self.input.bitmap.state { + // The bitmap selector only starts a blit between packets: a word + // equal to it is also a valid argument of other selectors. + BitmapState::Idle => { + if !self.input.textport.framing() && word == BITMAP_SELECTOR { + self.input.bitmap = Bitmap { state: BitmapState::Header, ..Bitmap::idle() }; + return; + } + self.push_textport(h, be, word); + } + BitmapState::Header => { + self.input.bitmap.header.push(word); + if self.input.bitmap.header.len() < BITMAP_HEADER_WORDS { + return; + } + // h0 and h1 are the width and height; h2 to h5 the x and y + // origin and advance. + let (w, hgt) = (self.input.bitmap.header[0] as u64, self.input.bitmap.header[1] as u64); + let expected = (w * hgt).div_ceil(32); + if expected == 0 { + self.input.bitmap = Bitmap::idle(); + self.input.expect_sync = true; + return; + } + if expected > BITMAP_MAX_PAYLOAD_WORDS { + crate::dlog!(LogModule::Kona, "kona: bitmap blit {w}x{hgt} exceeds this model's payload bound; dropping"); + self.input.bitmap = Bitmap::idle(); + self.input.expect_sync = true; + return; + } + self.input.bitmap.expected = expected as usize; + self.input.bitmap.state = BitmapState::Payload; + } + BitmapState::Payload => { + self.input.bitmap.payload.push(word); + if self.input.bitmap.payload.len() < self.input.bitmap.expected { + return; + } + let b = std::mem::replace(&mut self.input.bitmap, Bitmap::idle()); + self.job(h, be, Job::Bitmap { header: &b.header, payload: &b.payload }); + self.input.expect_sync = true; + } + } + } + + /// A word that can follow the textport's sync word as its command. + fn textport_selector_known(&self, word: u32) -> bool { + word == BITMAP_SELECTOR || self.tables.textport.get(word).is_some() + } + + fn textport_packet(&mut self, h: &Host, be: &mut KonaBackend, p: Packet) { + if self.failed.is_some() { + return; + } + match p.selector { + // Sync probe. + TEXTPORT_SYNC_PROBE => h.mailbox_reply(&[SYNC_PROBE_REPLY]), + // Graphics info: width and height are the model's managed area; + // the rest of the reply is approximated zero. + TEXTPORT_GRAPHICS_INFO => h.mailbox_reply(&[0, 0, 0, self.managed.0, self.managed.1]), + _ => { + self.job(h, be, Job::Command { grammar: Grammar::Textport, selector: p.selector, args: p.args(), packed: 0, handled: false }); + } + } + self.input.expect_sync = true; + } + + // ----- the kernel's commands ------------------------------------------- + + fn kernel_packet(&mut self, h: &Host, be: &mut KonaBackend, p: Packet) { + if self.failed.is_some() { + return; + } + let a = p.args(); + let mut handled = true; + match p.selector { + PIPE_SET_UP => self.register_reason_page(h, a[0], a[1]), + CONTEXT_CLEAN_UP_WITH_EVENT_CODE => { + // The destroy sends 0x103 only while its context is still + // the one the board holds; no context identifier accompanies + // it. The back end releases that window. + if a[0] == REASON_CONTEXT_CLEAN_UP { + if let Some(slot) = self.current { + self.job(h, be, Job::Context(ContextEvent::WindowReleased { slot })); + } + } + self.post_reason(h, p.selector, &[a[0]]); + } + RAISE_HOST_INTERRUPT_WITH_EVENT_CODE => self.post_reason(h, p.selector, &[a[0]]), + CONTEXT_DEACTIVATE => self.context_deactivate(h, be, a[0]), + SET_UP_OUTPUT_DMA_FOR_HOST_READ => self.register_return_page(h, a[0], a[1]), + QUERY_BOARD_CONFIGURATION_BLOCK => { + let block = ident::config_block(&self.cfg, self.managed.0, self.managed.1); + self.post_return(h, p.selector, ident::CONFIG_OFFSET, &block); + } + EEPROM_OPERATION => { + let mut op = FlashOp::default(); + let action = op.begin(&mut self.flash, a[0]); + if op.open() { + self.input.run = Run::Flash(op); + } + self.flash_action(h, action); + } + WINDOW_SYSTEM_CONTEXT_INITIALISE => { + // Echo the request: the model can never report a mode other + // than the one asked for, where a real pipe presumably + // reports the mode it set up. + let mut words = [0u8; 12]; + for (i, w) in a[..3].iter().enumerate() { + words[4 * i..4 * i + 4].copy_from_slice(&w.to_be_bytes()); + } + self.post_return(h, p.selector, 8, &words); + } + CONTEXT_INITIALISE => self.context_activate(h, be, p.selector, a[0], a[1]), + CONTEXT_IMAGE_LOAD => { + self.context_activate(h, be, p.selector, a[1], a[2]); + if a[0] != SAVE_AREA_MARKER { + crate::dlog!( + LogModule::Kona, + "kona: context restore carries save-area word {:#x}, not the {SAVE_AREA_MARKER:#x} the model leaves", + a[0] + ); + } + } + START_INPUT_DMA | COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA | DESCRIBE_DMA_PAGE_POOL | EXECUTE_LIST_BY_LIST_DMA | EXECUTE_LIST_BY_INPUT_DMA | EXECUTE_LIST_BY_LIST_DMA_REPEATED + | EXECUTE_LIST_BY_INPUT_DMA_REPEATED | COPY_USER_LIST_BY_LIST_DMA | MOVE_LIST_BY_LIST_DMA => self.dma_command(h, be, p.selector, a), + PUBLISH_RETRACE_SEQUENCE_NUMBER => { + self.job(h, be, Job::Retrace { tag: a[0] }); + } + DISPLAY_GENERATOR_REQUEST_CARRIER => { + self.input.run = Run::Video { command: a[0], data: Vec::new(), sum: 0 }; + } + LOAD_COLOUR_MAP_ENTRIES | LOAD_GAMMA_TABLE_ENTRIES => self.start_words(Grammar::Kernel, &p, a[1]), + LOAD_CURSOR_IMAGE_BLOCK => self.start_words(Grammar::Kernel, &p, X_CURSOR_DATA_WORDS), + LOAD_CURSOR_COLOUR_WORDS => self.start_words(Grammar::Kernel, &p, X_CURSOR_RECOLOUR_WORDS), + PIPE_MEMORY_SWAP_OUT => self.mem_swap_out(h, a), + PIPE_MEMORY_SWAP_IN => { + self.swapin.header.copy_from_slice(&a[..6]); + self.swapin.stage = SwapStage::Mode; + } + PIPE_MEMORY_SUSPEND_WITH_EVENT_CODE => { + if a[0] != REASON_SUSPEND { + crate::dlog!(LogModule::Kona, "kona: suspend with reason {:#x}; answered as 0x104", a[0]); + } + // A suspend replies n = 0; what a real pipe saves is unknown. + self.post_reason(h, p.selector, &[REASON_SUSPEND, 0]); + } + PIPE_MEMORY_RESERVE => self.dma_list_region(a), + _ => handled = false, + } + let r = self.job(h, be, Job::Command { grammar: Grammar::Kernel, selector: p.selector, args: a, packed: 0, handled }); + if !handled { + self.open_run(Grammar::Kernel, &p, r); + } + } + + // ----- pages and replies ----------------------------------------------- + + /// The page a 0x1f1 or 0x1f2 command names in its first two words `page` + /// and `offset`. With bit 28 of `page` set the page is host physical; + /// otherwise `page << 12` plus `offset`'s low twelve bits is a device + /// address, translated through the map RAM entry for its 16 KiB page. + /// Both readings are inferred from the driver's uses. A device address + /// the map RAM does not map yields nothing. + pub(super) fn command_page(&self, h: &Host, what: &str, page: u32, offset: u32) -> Option { + if page & PAGE_WORD_PHYSICAL != 0 { + return Some(((page & PAGE_WORD_FRAME) as u64) << 12 | (offset & 0xfff) as u64); + } + let device = (page as u64) << 12 | (offset & 0xfff) as u64; + match self.device_address(h, device) { + Some(a) => Some(a), + None => { + crate::dlog!( + LogModule::Kona, + "kona: {what} page at device address {device:#x} is not mapped; not registered" + ); + None + } + } + } + + fn register_reason_page(&mut self, h: &Host, page: u32, offset: u32) { + if let Some(a) = self.command_page(h, "reason", page, offset) { + self.reason_page = Some(a); + self.reason_unread = false; + } + } + + /// 0x1f2: the return-data page, where the pipe answers a process's + /// requests and the process polls word 0. The kernel's context switch + /// sends it straight after a context's first-activation 0x1fd, so that + /// page becomes the context's own, restored with it by 0x1f9; a 0x1f2 + /// with no first activation waiting only replaces the page the pipe + /// answers on. A page that does not translate leaves any earlier + /// registration in place, except for a context waiting for its page, + /// which is left with none rather than another context's. + fn register_return_page(&mut self, h: &Host, page: u32, offset: u32) { + let waiting = if self.context_page_pending { self.current } else { None }; + self.context_page_pending = false; + match self.command_page(h, "return", page, offset) { + Some(a) => { + self.return_page = Some(a); + if let Some(c) = waiting { + self.contexts[c].return_page = Some(a); + } + } + None => { + if waiting.is_some() { + self.return_page = None; + } + } + } + } + + /// Post a return-page answer: `data` at `offset`, then a non-zero word 0, + /// the only thing the waiting process polls. The pipe writes word 0 last + /// (established): software may use the page as soon as word 0 goes + /// non-zero, so the data must already be there. Approximated: the model answers at + /// once rather than after queued work. With no return page it logs and + /// writes nothing. + pub(super) fn post_return(&mut self, h: &Host, token: u32, offset: u32, data: &[u8]) -> bool { + let Some(page) = self.return_page else { + crate::dlog!(LogModule::Kona, "kona: token {token:#x} with no return page registered; nothing posted"); + return false; + }; + if !data.is_empty() && !h.dma_write(page + offset as u64, data) { + crate::dlog!(LogModule::Kona, "kona: {}-byte write to {:#x} failed", data.len(), page + offset as u64); + return false; + } + h.dma_write(page, &1u32.to_be_bytes()) + } + + fn flash_action(&mut self, h: &Host, action: Option) { + match action { + None => {} + Some(FlashAction::Reply(words)) => { + // The word count, then the words, from word 1 of the reply + // buffer, and word 0 last, as the ARM's reply routines write + // it. Approximated: at once, where the ARM first programs the + // part. + let mut reply = Vec::with_capacity(4 + 4 * words.len()); + reply.extend_from_slice(&(words.len() as u32).to_be_bytes()); + for w in &words { + reply.extend_from_slice(&w.to_be_bytes()); + } + self.post_return(h, EEPROM_OPERATION, 4, &reply); + } + Some(FlashAction::ReplyBuffer { page, offset }) => { + // The pipe keeps the reply buffer in the same place 0x1f2 + // sets, so later answers to either land there. Unlike 0x1f2 + // it keeps the offset word's low 12 bits. + if let Some(a) = self.command_page(h, "EEPROM reply", page, offset) { + self.return_page = Some(a); + } + } + } + } + + // ----- completion reasons ---------------------------------------------- + + /// Post a completion reason on the reason page: written big-endian from + /// word 0, then the geometry-engine cause bit set and its interrupt + /// raised, which is what the driver's geometry-engine interrupt handler + /// reads and dispatches on. The handler reads word 0 once an interrupt + /// and writes it back ORed with a marker, so a second reason written + /// before that read would replace the first. The model therefore posts + /// one reason at a time: a reason that finds the last one's word 0 still + /// on the page waits in a queue, and goes out once the page shows the + /// driver's rewrite, looked for whenever the cause register is accessed + /// and when the board's thread finishes a run. + /// + /// Approximated: hardware is inferred to post only once its queued work + /// is done, and the model has no queued work, so a reason with nothing + /// ahead of it posts at once. With no page registered it logs and posts + /// nothing. + pub(super) fn post_reason(&mut self, h: &Host, token: u32, words: &[u32]) { + assert!(!words.is_empty() && words.len() <= REASON_WORDS); + if self.reason_page.is_none() { + crate::dlog!(LogModule::Kona, "kona: token {token:#x} reason {:#x} with no reason page registered; nothing posted", words[0]); + return; + } + if self.reason_queue.is_empty() && !self.reason_pending(h) { + self.reason_write(h, words); + return; + } + if self.reason_queue.len() == REASON_QUEUE { + crate::dlog!(LogModule::Kona, "kona: {REASON_QUEUE} completion reasons wait unread; reason {:#x} lost", words[0]); + return; + } + let mut w = [0; REASON_WORDS]; + w[..words.len()].copy_from_slice(words); + self.reason_queue.push_back((token, w, words.len())); + self.reason_drain(h); + } + + fn reason_write(&mut self, h: &Host, words: &[u32]) { + let Some(page) = self.reason_page else { return }; + let bytes: Vec = words.iter().flat_map(|w| w.to_be_bytes()).collect(); + if !h.dma_write(page, &bytes) { + crate::dlog!(LogModule::Kona, "kona: {}-byte write to {page:#x} failed", bytes.len()); + return; + } + self.reason_unread = true; + self.reason_posted = words[0]; + h.with_front(|f| { + f.int_cause |= CAUSE_GE; + ((), f.raise_vector(false)) + }); + } + + /// Whether the last reason's word 0 is still on the page unread. A page + /// that cannot be read back cannot hold the next reason back. + fn reason_pending(&mut self, h: &Host) -> bool { + if !self.reason_unread { + return false; + } + let mut word = [0u8; 4]; + let still = match self.reason_page { + Some(page) => h.dma_read(page, &mut word) && u32::from_be_bytes(word) == self.reason_posted, + None => false, + }; + self.reason_unread = still; + still + } + + /// Post queued reasons while the page is free. + pub fn reason_drain(&mut self, h: &Host) { + while !self.reason_queue.is_empty() && !self.reason_pending(h) { + let (_, w, n) = self.reason_queue.pop_front().unwrap(); + self.reason_write(h, &w[..n]); + } + } + + // ----- contexts -------------------------------------------------------- + + /// 0x1fd page offset len, or 0x1f9 word0 page offset sentinel: make the + /// context whose save area the two words name current, and answer on its + /// return page from here on. A 0x1fd starts the context afresh with no + /// return page until the 0x1f2 that follows it. A 0x1f9 restores the page + /// that 0x1f2 gave it; one naming a save area no 0x1fd named is logged and + /// leaves the pipe with no return page, so an answer goes nowhere rather + /// than to another context. The key is the two words as sent, which the + /// kernel derives from one fixed allocation per context. + fn context_activate(&mut self, h: &Host, be: &mut KonaBackend, token: u32, page: u32, offset: u32) { + let first = token == CONTEXT_INITIALISE; + let mut fresh = first; + let offset = offset & 0xfff; + // The restore's 0x1e4 packets place the new context's regions. + self.dma.list_base = 0; + let mut slot = (0..self.context_count) + .find(|&i| self.contexts[i].save_page == page && self.contexts[i].save_offset == offset); + if slot.is_none() && !first { + crate::dlog!( + LogModule::Kona, + "kona: context restore names save area {:#x}, which no {CONTEXT_INITIALISE:#x} set up; no return page", + (page as u64) << 12 | offset as u64 + ); + slot = Some(CONTEXT_TRANSIENT); + fresh = true; + } + let slot = match slot { + Some(s) => s, + None => { + if self.context_count == CONTEXT_LIMIT { + self.stop(format!( + "kona: command {token:#x} key {page:#x}:{offset:#x}: logical context limit reached ({CONTEXT_LIMIT} retained)" + )); + return; + } + self.context_count + } + }; + if fresh { + self.saved[slot] = None; + self.contexts[slot] = Context { save_page: page, save_offset: offset, return_page: None }; + if first && slot == self.context_count { + self.context_count += 1; + } + } + self.current = Some(slot); + self.gl_grammar = true; + self.context_page_pending = first; + self.return_page = self.contexts[slot].return_page; + self.job(h, be, Job::Context(ContextEvent::Activated { slot, fresh })); + } + + /// 0x1fc index: switch the current context out. The kernel's context + /// switch then sleeps until the pipe posts reason 0x101, and the driver's + /// geometry-engine interrupt handler wakes it and cancels its 90 s + /// deactivate timeout. When the context is restored later, the context + /// switch reads words 0 and 2 of its save area back, so they are written + /// before the post. The index names the context's entry in the kernel's + /// table, not the save area, so the save area is the one the last 0x1fd or + /// 0x1f9 named. + /// + /// Approximated: hardware writes a real context record into the save + /// area; the model writes only the two words software reads back, and + /// posts at once, since it keeps no queued work. + fn context_deactivate(&mut self, h: &Host, be: &mut KonaBackend, index: u32) { + match self.current { + None => crate::dlog!(LogModule::Kona, "kona: context deactivate {index} with no context current; no save area written"), + Some(slot) => { + let c = self.contexts[slot]; + if let Some(addr) = self.command_page(h, "save area", c.save_page, c.save_offset) { + h.dma_write(addr, &SAVE_AREA_MARKER.to_be_bytes()); + h.dma_write(addr + 8, &SAVE_AREA_TOKEN.to_be_bytes()); + } + self.job(h, be, Job::Context(ContextEvent::Deactivated { slot })); + } + } + self.current = None; + self.context_page_pending = false; + self.post_reason(h, CONTEXT_DEACTIVATE, &[REASON_CONTEXT_DEACTIVATED]); + } + + /// The pipe stops taking commands: a limit the model keeps was reached. + fn stop(&mut self, why: String) { + crate::dlog!(LogModule::Kona, "{why}"); + self.failed = Some(why); + } + + // ----- kernel pool swaps ----------------------------------------------- + + /// The kernel's pipe-memory swaps. When a pool lacks room it evicts other + /// contexts' regions with 0x1e3: class, offset and size in granules, the + /// granule in bytes, the copy count, the host buffer's device address as + /// page >> 12 and (address & 0xfff) | 0x9000, and the completion reason + /// 0x102. Swap-in, 0x1e2, carries the same six words and then either the + /// tagged offset, for the pipe to read the buffer back by DMA, or + /// 0xffffffff and the buffer's words inline, then 0x102; it completes + /// through the mailbox with 0x102. + /// + /// The kernel keeps the swapped-out buffer untouched and only hands it + /// back on swap-in, so the bytes are the pipe's own. Approximated: the + /// model keeps every texture and list in its own storage, whatever the + /// pool placement, so a swap moves none of it. A swap-out writes zeros of + /// the full length into the buffer, a swap-in reads the buffer and + /// discards it, and both complete at once. + fn swap_bytes(a: &[u32]) -> u64 { + a[2] as u64 * a[3] as u64 * a[4] as u64 + } + + fn mem_swap_out(&mut self, h: &Host, a: &[u32]) { + let bytes = Self::swap_bytes(a); + let device = (a[5] as u64) << 12 | (a[6] & 0xfff) as u64; + let ok = a[6] & !0xfff == DMA_ADDR_TAG && a[7] == REASON_SWAP && bytes <= SWAP_MAX_BYTES && self.device_zero(h, device, bytes); + if !ok { + crate::dlog!( + LogModule::Kona, + "kona: swap-out of class {} offset {:#x} size {:#x} to {:#x} {:#x} reason {:#x} not completed", + a[0], a[1], a[2], a[5], a[6], a[7] + ); + return; + } + self.post_reason(h, PIPE_MEMORY_SWAP_OUT, &[REASON_SWAP]); + } + + fn swap_in_done(&mut self, h: &Host) { + let bytes = Self::swap_bytes(&self.swapin.header); + let mut ok = bytes <= SWAP_MAX_BYTES; + if ok && self.swapin.mode != u32::MAX { + let device = (self.swapin.header[5] as u64) << 12 | (self.swapin.mode & 0xfff) as u64; + let mut chunk = vec![0u8; 4096]; + let mut done = 0; + while ok && done < bytes { + let n = (bytes - done).min(4096) as usize; + ok = self.device_read(h, device + done, &mut chunk[..n]); + done += n as u64; + } + } + if !ok { + crate::dlog!( + LogModule::Kona, + "kona: swap-in of class {} offset {:#x} size {:#x} not completed", + self.swapin.header[0], self.swapin.header[1], self.swapin.header[2] + ); + return; + } + h.post_mailbox(REASON_SWAP); + } + + /// A word after a swap-in's six. False for a word that does not belong to + /// it, which ends the swap-in, logged, and is taken afresh. + fn swap_in_word(&mut self, h: &Host, word: u32) -> bool { + match self.swapin.stage { + SwapStage::Mode => { + if word == u32::MAX { + let bytes = Self::swap_bytes(&self.swapin.header); + self.swapin.mode = word; + if bytes % 4 == 0 && bytes <= SWAP_MAX_BYTES { + self.swapin.data_left = bytes / 4; + self.swapin.stage = if self.swapin.data_left != 0 { SwapStage::Data } else { SwapStage::Reason }; + return true; + } + } else if word & !0xfff == DMA_ADDR_TAG { + self.swapin.mode = word; + self.swapin.stage = SwapStage::Reason; + return true; + } + } + SwapStage::Data => { + self.swapin.data_left -= 1; + if self.swapin.data_left == 0 { + self.swapin.stage = SwapStage::Reason; + } + return true; + } + SwapStage::Reason => { + self.swapin.stage = SwapStage::Idle; + if word == REASON_SWAP { + self.swap_in_done(h); + return true; + } + } + SwapStage::Idle => return false, + } + crate::dlog!( + LogModule::Kona, + "kona: swap-in of class {} ended by word {word:#x}; not completed", + self.swapin.header[0] + ); + self.swapin.stage = SwapStage::Idle; + false + } + + // ----- the map RAM ----------------------------------------------------- + + /// The host physical address a device address maps to through the XG map + /// RAM, for the one entry encoding the driver's page mapper writes: bits + /// 31:28 = 0x2 (a 16 KiB page), bits 27:0 the 4 KiB frame number of that + /// page. + pub(super) fn device_address(&self, h: &Host, device: u64) -> Option { + let index = (device >> MAPRAM_PAGE_SHIFT) as usize; + let entry = if index < h.front.lock().mapram_count() { self.map_ram[index] } else { 0 }; + if entry & MAPRAM_KIND_MASK != MAPRAM_KIND_16K { + return None; + } + Some(((entry & PAGE_WORD_FRAME) as u64) << 12 | (device & ((1 << MAPRAM_PAGE_SHIFT) - 1))) + } + + /// Read pipe-address-space bytes, page by page. + pub(super) fn device_read(&self, h: &Host, device: u64, buf: &mut [u8]) -> bool { + let mut done = 0; + while done < buf.len() { + let at = device + done as u64; + let n = (buf.len() - done).min(((1u64 << MAPRAM_PAGE_SHIFT) - (at & ((1 << MAPRAM_PAGE_SHIFT) - 1))) as usize); + let Some(host) = self.device_address(h, at) else { + crate::dlog!(LogModule::Kona, "kona: read of device address {at:#x} has no map RAM entry"); + return false; + }; + if !h.dma_read(host, &mut buf[done..done + n]) { + return false; + } + done += n; + } + true + } + + /// Write pipe-address-space bytes, page by page. + pub(super) fn device_write(&self, h: &Host, device: u64, data: &[u8]) -> bool { + let mut done = 0; + while done < data.len() { + let at = device + done as u64; + let n = (data.len() - done).min(((1u64 << MAPRAM_PAGE_SHIFT) - (at & ((1 << MAPRAM_PAGE_SHIFT) - 1))) as usize); + let Some(host) = self.device_address(h, at) else { + crate::dlog!(LogModule::Kona, "kona: write to device address {at:#x} has no map RAM entry"); + return false; + }; + if !h.dma_write(host, &data[done..done + n]) { + return false; + } + done += n; + } + true + } + + /// Zero `bytes` of the device address space, checking every page maps + /// before writing any. + fn device_zero(&self, h: &Host, device: u64, bytes: u64) -> bool { + let page = 1u64 << MAPRAM_PAGE_SHIFT; + let mut at = device; + while at < device + bytes { + if self.device_address(h, at).is_none() { + return false; + } + at = (at | (page - 1)) + 1; + } + let zeros = vec![0u8; 1 << 16]; + let mut done = 0; + while done < bytes { + let n = (bytes - done).min(zeros.len() as u64) as usize; + if !self.device_write(h, device + done, &zeros[..n]) { + return false; + } + done += n as u64; + } + true + } +} + +/// The video command search gives up after this many data words, a model +/// bound. +const VIDEO_SEARCH_MAX_WORDS: usize = 1024; + +/// The most payload words a textport bitmap blit may carry: the managed +/// area's bits, a model bound against a guest-controlled size. +const BITMAP_MAX_PAYLOAD_WORDS: u64 = 1280 * 1024 / 32; + +/// The tokens only the kernel sends: the context switch and teardown. +fn kernel_only_token(word: u32) -> bool { + matches!(word, CONTEXT_DEACTIVATE | CONTEXT_IMAGE_LOAD | CONTEXT_INITIALISE | CONTEXT_CLEAN_UP_WITH_EVENT_CODE) +} + +/// The GL commands a whole word opens before the first context activation: +/// the make-current and drawing commands the model framed before it framed +/// the rest of the GL grammar. No client sends them then; the page carries +/// the PROM's textport and the kernel, and taking any other GL selector +/// there would change how their stray words frame. +fn gl_token_early(selector: u32) -> bool { + matches!( + selector, + SELECT_ACTIVE_TEXTURE_UNIT | CHANGE_POST_TEXTURE_SCALE_AND_BIAS | LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE | QUERY_LINE_MODE_LIMIT_TABLE | SET_LAZY_VALIDATION_MODE | SET_VIEWPORT | SET_SCISSOR_BOX | RECORD_ERROR_CODE | DISABLE_CAPABILITY | SET_COLOUR_CLEAR_VALUE | CLEAR_SELECTED_BUFFERS | SET_FLAT_OR_SMOOTH_SHADING | SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT + | BEGIN_PRIMITIVE_POINTS..=BEGIN_PRIMITIVE_POLYGON | SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT | END_OPEN_PRIMITIVE | FLUSH_VERTEX_PIPELINE | FINISH_DRAIN_PIPE_AND_SIGNAL_COMPLETION | QUERY_PENDING_ERROR + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::kona::back::KonaBackend; + use crate::dev::kona::tests::TestHost; + use std::sync::Arc; + + fn rig() -> (Arc, Host, Core) { + let t = TestHost::new(); + let (h, core) = crate::dev::kona::tests::bare(&t, KonaConfig::default()); + (t, h, core) + } + + const PHYS: u32 = PAGE_WORD_PHYSICAL; + + #[test] + fn an_answer_stores_its_data_and_then_the_ready_word() { + let (t, h, mut core) = rig(); + assert!(!core.post_return(&h, 0x1ec, 8, &[1, 2, 3, 4]), "no return page: nothing is posted"); + assert_eq!(t.word(0x1000), 0); + core.return_page = Some(0x1000); + assert!(core.post_return(&h, 0x1ec, 8, &[1, 2, 3, 4])); + assert_eq!(t.bytes(0x1008, 4), [1, 2, 3, 4]); + assert_eq!(t.word(0x1000), 1); + } + + #[test] + fn a_device_address_maps_through_its_16k_page() { + let (t, h, mut core) = rig(); + core.map_ram[3] = MAPRAM_KIND_16K | (0x40_0000 >> 12); + core.map_ram[4] = MAPRAM_KIND_16K | (0x50_0000 >> 12); + let dev = |page: u64, off: u64| (page << MAPRAM_PAGE_SHIFT) + off; + assert_eq!(core.device_address(&h, dev(3, 0x123)), Some(0x40_0123)); + assert_eq!(core.device_address(&h, dev(5, 0)), None, "an unmapped page"); + // A transfer that crosses into the next page goes to that page's frame. + assert!(core.device_write(&h, dev(3, 0x3ffe), &[1, 2, 3, 4])); + assert_eq!(t.bytes(0x40_3ffe, 2), [1, 2]); + assert_eq!(t.bytes(0x50_0000, 2), [3, 4]); + let mut back = [0u8; 4]; + assert!(core.device_read(&h, dev(3, 0x3ffe), &mut back)); + assert_eq!(back, [1, 2, 3, 4]); + assert!(!core.device_write(&h, dev(4, 0x3fff), &[9, 9]), "the next page is unmapped"); + } + + #[test] + fn a_reason_waits_until_the_last_one_has_been_read() { + let (t, h, mut core) = rig(); + core.register_reason_page(&h, PHYS | (0x2000 >> 12), 0); + core.post_reason(&h, 0x1f8, &[0x103]); + core.post_reason(&h, 0x1dd, &[0x777]); + assert_eq!(t.word(0x2000), 0x103); + assert!(core.reasons_waiting()); + core.reason_drain(&h); + assert_eq!(t.word(0x2000), 0x103, "still unread"); + t.put(0x2000, &(0x103u32 | 0xbad0_0000).to_be_bytes()); + core.reason_drain(&h); + assert_eq!(t.word(0x2000), 0x777); + assert!(!core.reasons_waiting()); + assert_eq!(h.front.lock().int_cause & CAUSE_GE, CAUSE_GE); + } + + #[test] + fn a_restored_context_answers_on_its_own_return_page() { + let (t, h, mut core) = rig(); + let mut be = KonaBackend::new(KonaConfig::default()); + let save = |n: u32| PHYS | (0x10_0000 + 0x1_0000 * n) >> 12; + core.context_activate(&h, &mut be, CONTEXT_INITIALISE, save(0), 0); + core.register_return_page(&h, PHYS | (0x3000 >> 12), 0); + core.context_activate(&h, &mut be, CONTEXT_INITIALISE, save(1), 0); + core.register_return_page(&h, PHYS | (0x4000 >> 12), 0); + assert_eq!(core.return_page, Some(0x4000)); + core.context_activate(&h, &mut be, CONTEXT_IMAGE_LOAD, save(0), 0); + assert_eq!(core.return_page, Some(0x3000)); + assert_eq!(core.current, Some(0)); + // A save area nobody set up has no page to answer on. + core.context_activate(&h, &mut be, CONTEXT_IMAGE_LOAD, save(7), 0); + assert_eq!(core.return_page, None); + let _ = t; + } +} diff --git a/src/dev/kona/regs.rs b/src/dev/kona/regs.rs new file mode 100644 index 000000000..c6547a64b --- /dev/null +++ b/src/dev/kona/regs.rs @@ -0,0 +1,321 @@ +//! The registers the board's thread owns and the stores that join the +//! command stream. +//! +//! Which accesses wait for the stream is `classify`: a store that belongs to +//! the command stream queues behind the stores before it; a register the +//! driver reads and acknowledges while the pipe works is served at once; every +//! other access reads what the stream writes or changes what it reads, so it +//! waits for the stream (the bus answers busy meanwhile). Inferred from the +//! driver's use, not measured: which registers the pipe serves while busy. +//! +//! The indirect port reaches a pipe-internal 32-bit address space: the +//! address port selects a word, the data port reads or writes it, and the +//! advancing write port writes it and adds 4 to the address. The firmware and +//! driver read the flash EEPROM's header through it; a user-space loader +//! writes the pipe's firmware through it. Approximated: the data port does not +//! advance the address. Every reader selects a new address before each access, +//! so no evidence says whether the hardware advances. The advancing write's +//! advance is what the loader depends on (its sections abut at address plus +//! four times their word count), inferred for the hardware. +//! +//! Ported from hw/misc/sgi-kona.c in jamesbraid/qemu-sgi-origami. + +use super::defs::*; +use super::flash::FLASH_SIZE; +use super::front::{Front, Host}; +use parking_lot::Mutex; +use super::ident; +use super::back::KonaBackend; +use super::job::{Job}; +use super::pipe::{Core, LoadSection}; +use crate::devlog::LogModule; + +/// How an access meets the command stream. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) enum Access { + /// Served at once from the register file, whatever the stream is doing. + Now, + /// A store that joins the stream, in order. + Queues, + /// Waits for the stream to have run. + Waits, +} + +pub(super) fn classify(off: u32, write: bool) -> Access { + let stream = if write { Access::Queues } else { Access::Waits }; + if (COMMAND_PAGE..COMMAND_PAGE + COMMAND_PAGE_SIZE).contains(&off) { + return stream; + } + match off { + DMA_FIFO_PUSH | DMA_FIFO_KICK | PORT_WRITE_NEXT => stream, + INT_CAUSE | INT_ENABLE | MAILBOX => Access::Now, + CREDIT => { + if write { + Access::Waits + } else { + Access::Now + } + } + RETRACE_TAG | WIDGET_ID => { + if write { + Access::Waits + } else { + Access::Now + } + } + // Software expectation (from the ported model): host software reads and + // writes the low-window word and acknowledges video interrupts while + // the pipe works, and polls the activity latch to decide whether the + // pipe has hung. The command stream reaches none of them. + LOW_WINDOW_WRITE | ACTIVITY => Access::Now, + INTR_END => { + if write { + Access::Now + } else { + Access::Waits + } + } + _ if (MAPRAM_BASE..MAPRAM_BASE + 4 * MAPRAM_ENTRIES as u32).contains(&off) => stream, + _ => Access::Waits, + } +} + +impl Core { + /// One word of a store that joined the stream. + pub fn stream_store(&mut self, h: &Host, be: &mut KonaBackend, off: u32, value: u32) { + match off { + o if (COMMAND_PAGE..COMMAND_PAGE + COMMAND_PAGE_SIZE).contains(&o) => { + self.command_page_write(h, be, o - COMMAND_PAGE, value) + } + DMA_FIFO_PUSH => { + h.activity.store(true, std::sync::atomic::Ordering::Relaxed); + self.push_dma_fifo(h, be, value); + } + DMA_FIFO_KICK => self.bracket_write(value), + PORT_WRITE_NEXT => { + h.activity.store(true, std::sync::atomic::Ordering::Relaxed); + self.port_write_next(value); + } + o if (MAPRAM_BASE..MAPRAM_BASE + 4 * MAPRAM_ENTRIES as u32).contains(&o) => { + let index = ((o - MAPRAM_BASE) / 4) as usize; + if index < h.front.lock().mapram_count() { + self.map_ram[index] = value; + } else { + crate::dlog!(LogModule::Kona, "kona: map RAM write at {o:#x} beyond the {} entries the probe register selects", h.front.lock().mapram_count()); + } + } + _ => unreachable!("offset {off:#x} is not a stream store"), + } + } + + /// A read that waits for the stream. + pub fn read_wait(&mut self, front: &Mutex, off: u32) -> u32 { + match off { + PORT_ADDR => return self.port_addr, + // Does not advance; see the module header. + PORT_DATA => return self.internal_read(self.port_addr), + INDIRECT_SELECT => return self.indirect_select, + INDIRECT_DATA => { + if let Some(w) = ident::class_word(&self.cfg, self.indirect_select) { + return w; + } + } + o if (MAPRAM_BASE..MAPRAM_BASE + 4 * MAPRAM_ENTRIES as u32).contains(&o) => { + let index = ((o - MAPRAM_BASE) / 4) as usize; + let count = front.lock().mapram_count(); + if index < count { + return self.map_ram[index]; + } + crate::dlog!(LogModule::Kona, "kona: map RAM read at {o:#x} beyond the {count} entries the probe register selects"); + return 0; + } + _ => {} + } + if let Some(v) = front.lock().read(off, 0) { + return v; + } + crate::dlog!(LogModule::Kona, "kona: unimplemented read of offset {off:#x}"); + 0 + } + + /// A write that waits for the stream. + pub fn write_wait(&mut self, h: &Host, be: &mut KonaBackend, off: u32, value: u32) { + match off { + PORT_ADDR => { + self.port_addr = value; + self.load_open = false; + return; + } + // The firmware and driver write 0 here to prime an EEPROM read; + // outside the backed ranges that write is dropped. + PORT_DATA => return self.internal_write(self.port_addr, value), + INDIRECT_SELECT => { + self.indirect_select = value; + return; + } + INDIRECT_DATA => { + self.job(h, be, Job::Indirect { selector: self.indirect_select, value }); + return; + } + RESET_STROBE => { + // A write with bit 0 set resets what host software rewrites + // after its 1, read, 0, read pulse (inferred from that + // rewrite): the enable, + // the global enable, the credit configuration, the group word + // and every map RAM entry, which becomes invalid. It leaves + // the DMA registers, the low-water state, the command framers + // and the interrupt routing alone: the kernel never rewrites + // them after a pulse and what the pulse does to them is + // unknown. Bit 1 (ARM start) starts nothing: the model has no + // ARM. + h.front.lock().reset_strobe = value; + if value & 1 != 0 { + h.front.lock().hip_reset(); + self.map_ram.fill(0); + } + return; + } + LOADER_328 => { + h.front.lock().loader_328 = value; + if value == 1 { + self.firmware_start(h, be); + } + return; + } + _ => {} + } + let mut packet = None; + let known = h.front.lock().write(off, value, &mut packet); + if let Some(p) = packet { + h.deliver(p); + } + if !known { + crate::dlog!(LogModule::Kona, "kona: unimplemented write of offset {off:#x} (value {value:#x})"); + } + } + + /// The loader released the firmware: the stored video format combination + /// goes to the back end, as the released ARM would run it. The stored + /// words are video commands framed as on the command page after 0x1a6. + fn firmware_start(&mut self, h: &Host, be: &mut KonaBackend) { + let count = self.flash.load(super::flash::COMBO + 4) as usize; + let words: Vec = if self.flash.load(super::flash::COMBO) == super::flash::COMBO_MARKER && count < 0x8000 { + (0..count).map(|i| self.flash.load(super::flash::COMBO + 8 + 4 * i as u32)).collect() + } else { + Vec::new() + }; + self.job(h, be, Job::FirmwareStart { combination: &words }); + } + + // ----- the indirect port ------------------------------------------------ + + pub(super) fn internal_read(&self, addr: u32) -> u32 { + if addr.wrapping_sub(FLASH_BASE) < FLASH_SIZE as u32 && addr & 3 == 0 { + return self.flash.load(addr - FLASH_BASE); + } + if addr == ICU_CONTROL { + return self.icu_control; + } + self.internal_word(addr).copied().unwrap_or(0) + } + + pub(super) fn internal_write(&mut self, addr: u32, value: u32) { + if addr == ICU_CONTROL { + let old = self.icu_control; + self.icu_control = value; + // The load record is closed where the loader releases the ARM's + // memory: the 1-to-0 edge. + if old == 1 && value == 0 { + self.load_record = std::mem::take(&mut self.load_sections); + self.load_open = false; + self.load_count = 0; + } + return; + } + match self.internal_word_mut(addr) { + Some(w) => *w = value, + None => crate::dlog!(LogModule::Kona, "kona: internal write of {value:#x} to {addr:#x} dropped"), + } + } + + /// One word of the pipe-internal space the indirect port reaches, or + /// None outside the two ranges the loader writes: the ARM's reset code at + /// address 0 and its image, data and the geometry-engine microcode from + /// 0x4000000. Backed with plain storage the model never executes or + /// interprets. + fn internal_word(&self, addr: u32) -> Option<&u32> { + if addr & 3 != 0 { + return None; + } + if ((addr - ARM_LOW_BASE) as usize) < ARM_LOW_WORDS * 4 { + return self.arm_low.get(((addr - ARM_LOW_BASE) / 4) as usize); + } + if addr >= ARM_HIGH_BASE && ((addr - ARM_HIGH_BASE) as usize) < ARM_HIGH_WORDS * 4 { + return self.arm_high.get(((addr - ARM_HIGH_BASE) / 4) as usize); + } + None + } + + fn internal_word_mut(&mut self, addr: u32) -> Option<&mut u32> { + if addr & 3 != 0 { + return None; + } + if ((addr - ARM_LOW_BASE) as usize) < ARM_LOW_WORDS * 4 { + return self.arm_low.get_mut(((addr - ARM_LOW_BASE) / 4) as usize); + } + if addr >= ARM_HIGH_BASE && ((addr - ARM_HIGH_BASE) as usize) < ARM_HIGH_WORDS * 4 { + return self.arm_high.get_mut(((addr - ARM_HIGH_BASE) / 4) as usize); + } + None + } + + /// The advancing write: a write after a select opens a new section of the + /// load record, and each write extends the open one. The address wraps at + /// 32 bits like the port register it lives in. + fn port_write_next(&mut self, value: u32) { + if !self.load_open { + if self.load_sections.len() < LOAD_SECTIONS_MAX { + self.load_sections.push(LoadSection { addr: self.port_addr, words: 0 }); + } + self.load_count += 1; + self.load_open = true; + } + // A section past the recorded ones keeps counting but is not kept. + if let Some(s) = self.load_sections.get_mut(self.load_count - 1) { + s.words += 1; + } + self.internal_write(self.port_addr, value); + self.port_addr = self.port_addr.wrapping_add(4); + } + + /// The sections of the last completed firmware load. + pub fn load_record(&self) -> &[LoadSection] { + &self.load_record + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_stream_registers_queue_and_the_polled_ones_do_not_wait() { + assert_eq!(classify(COMMAND_PAGE, true), Access::Queues); + assert_eq!(classify(COMMAND_PAGE + 0xc, true), Access::Queues); + assert_eq!(classify(COMMAND_PAGE, false), Access::Waits); + assert_eq!(classify(DMA_FIFO_PUSH, true), Access::Queues); + assert_eq!(classify(MAPRAM_BASE + 8, true), Access::Queues); + assert_eq!(classify(MAPRAM_BASE + 8, false), Access::Waits); + for off in [INT_CAUSE, INT_ENABLE, MAILBOX, LOW_WINDOW_WRITE, ACTIVITY] { + assert_eq!(classify(off, false), Access::Now); + assert_eq!(classify(off, true), Access::Now); + } + assert_eq!(classify(CREDIT, false), Access::Now); + assert_eq!(classify(CREDIT, true), Access::Waits); + assert_eq!(classify(WIDGET_ID, false), Access::Now); + assert_eq!(classify(INTR_END, true), Access::Now); + assert_eq!(classify(INTR_END, false), Access::Waits); + assert_eq!(classify(PORT_DATA, false), Access::Waits); + assert_eq!(classify(ONEWIRE, true), Access::Waits); + } +} diff --git a/src/dev/kona/stream.rs b/src/dev/kona/stream.rs new file mode 100644 index 000000000..300aad381 --- /dev/null +++ b/src/dev/kona/stream.rs @@ -0,0 +1,268 @@ +//! One description of the board's small state, read both ways. +//! +//! Each piece of state lists its fields once, in a `visit` function over a `Stream`: +//! a `Writer` appends them to a byte vector, a `Reader` takes them back in the same +//! order. Because the same function does both, a field cannot be saved and not +//! loaded. Values are little-endian. A reader that runs out of bytes, or meets a +//! length it will not accept, fails once and reads zeros after, and `finish` says +//! whether it read exactly what was written. +//! +//! What is saved is what the guest can have set: registers, tables, the +//! contexts' state. Counters, scratch buffers and caches are not, so a restored +//! board counts from zero and saving it again gives the same bytes. + +/// The most items a saved length may claim, so a corrupt snapshot cannot ask for +/// gigabytes. +const LIMIT: usize = 1 << 26; + +pub trait Stream { + fn loading(&self) -> bool; + fn u32(&mut self, v: &mut u32); + fn u64(&mut self, v: &mut u64); + fn bytes(&mut self, v: &mut [u8]); + fn fail(&mut self, why: &str); + /// Bytes left to read, when reading; None when writing. + fn remaining(&self) -> Option { + None + } + /// False once a read has failed. + fn ok(&self) -> bool { + true + } + + fn u8(&mut self, v: &mut u8) { + let mut x = *v as u32; + self.u32(&mut x); + *v = x as u8; + } + fn u16(&mut self, v: &mut u16) { + let mut x = *v as u32; + self.u32(&mut x); + *v = x as u16; + } + fn i32(&mut self, v: &mut i32) { + let mut x = *v as u32; + self.u32(&mut x); + *v = x as i32; + } + fn i64(&mut self, v: &mut i64) { + let mut x = *v as u64; + self.u64(&mut x); + *v = x as i64; + } + fn f32(&mut self, v: &mut f32) { + let mut x = v.to_bits(); + self.u32(&mut x); + *v = f32::from_bits(x); + } + fn bool(&mut self, v: &mut bool) { + let mut x = *v as u32; + self.u32(&mut x); + *v = x != 0; + } + fn usize(&mut self, v: &mut usize) { + let mut x = *v as u64; + self.u64(&mut x); + *v = x as usize; + } + fn u32s(&mut self, v: &mut [u32]) { + v.iter_mut().for_each(|x| self.u32(x)); + } + fn u16s(&mut self, v: &mut [u16]) { + v.iter_mut().for_each(|x| self.u16(x)); + } + fn i32s(&mut self, v: &mut [i32]) { + v.iter_mut().for_each(|x| self.i32(x)); + } + fn f32s(&mut self, v: &mut [f32]) { + v.iter_mut().for_each(|x| self.f32(x)); + } + + /// A length: written as given, read as saved (and refused past `LIMIT`). + fn len(&mut self, n: usize) -> usize { + let mut x = n as u64; + self.u64(&mut x); + if self.loading() && x as usize > LIMIT { + self.fail("a saved length is past the limit"); + return 0; + } + // Every item takes a byte at least: a length past what is left is corrupt, and is not allocated. + if self.remaining().is_some_and(|r| x as usize > r) { + self.fail("a saved length is past the bytes left"); + return 0; + } + x as usize + } + + fn vec_u32(&mut self, v: &mut Vec) { + let mut n = self.len(v.len()); + if self.remaining().is_some_and(|r| n > r / 4) { + self.fail("a saved word list is longer than the bytes left"); + n = 0; + } + if self.loading() { + v.clear(); + v.resize(n, 0); + } + self.u32s(v); + } + fn vec_u8(&mut self, v: &mut Vec) { + let n = self.len(v.len()); + if self.loading() { + v.clear(); + v.resize(n, 0); + } + self.bytes(v); + } + fn string(&mut self, v: &mut String) { + let mut b = v.clone().into_bytes(); + self.vec_u8(&mut b); + if self.loading() { + *v = String::from_utf8(b).unwrap_or_else(|_| { + self.fail("a saved string is not text"); + String::new() + }); + } + } + /// An optional value: a flag, then the value if there is one. + fn opt(&mut self, v: &mut Option, f: fn(&mut Self, &mut T)) + where + Self: Sized, + { + let mut some = v.is_some(); + self.bool(&mut some); + if self.loading() { + *v = some.then(T::default); + } + if let Some(t) = v { + f(self, t); + } + } +} + +/// Appends what is visited. +#[derive(Default)] +pub struct Writer { + pub out: Vec, +} + +impl Stream for Writer { + fn loading(&self) -> bool { + false + } + fn u32(&mut self, v: &mut u32) { + self.out.extend_from_slice(&v.to_le_bytes()); + } + fn u64(&mut self, v: &mut u64) { + self.out.extend_from_slice(&v.to_le_bytes()); + } + fn bytes(&mut self, v: &mut [u8]) { + self.out.extend_from_slice(v); + } + fn fail(&mut self, _why: &str) {} +} + +/// Takes back what a `Writer` wrote. +pub struct Reader<'a> { + data: &'a [u8], + at: usize, + error: Option, +} + +impl<'a> Reader<'a> { + pub fn new(data: &'a [u8]) -> Self { + Reader { data, at: 0, error: None } + } + + fn take(&mut self, n: usize) -> Option<&'a [u8]> { + let s = self.data.get(self.at..self.at.checked_add(n)?); + match s { + Some(s) => { + self.at += n; + Some(s) + } + None => { + self.fail("the saved state ends early"); + None + } + } + } + + /// Ok if nothing failed and every byte was read. + pub fn finish(self) -> Result<(), String> { + match self.error { + Some(e) => Err(e), + None if self.at != self.data.len() => Err(format!("{} bytes of saved state unread", self.data.len() - self.at)), + None => Ok(()), + } + } +} + +impl Stream for Reader<'_> { + fn loading(&self) -> bool { + true + } + fn u32(&mut self, v: &mut u32) { + *v = self.take(4).map_or(0, |b| u32::from_le_bytes(b.try_into().expect("four bytes"))); + } + fn u64(&mut self, v: &mut u64) { + *v = self.take(8).map_or(0, |b| u64::from_le_bytes(b.try_into().expect("eight bytes"))); + } + fn bytes(&mut self, v: &mut [u8]) { + match self.take(v.len()) { + Some(b) => v.copy_from_slice(b), + None => v.fill(0), + } + } + fn fail(&mut self, why: &str) { + self.error.get_or_insert_with(|| why.to_string()); + } + fn remaining(&self) -> Option { + Some(self.data.len() - self.at) + } + fn ok(&self) -> bool { + self.error.is_none() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn visit(s: &mut S, a: &mut u32, b: &mut Vec, c: &mut Option, d: &mut String, e: &mut f32) { + s.u32(a); + s.vec_u32(b); + s.opt(c, |s, v| s.u64(v)); + s.string(d); + s.f32(e); + } + + #[test] + fn what_is_written_is_read_back_and_nothing_more() { + let (mut a, mut b, mut c, mut d, mut e) = (7u32, vec![1, 2, 3], Some(9u64), "héllo".to_string(), 1.5f32); + let mut w = Writer::default(); + visit(&mut w, &mut a, &mut b, &mut c, &mut d, &mut e); + let (mut a2, mut b2, mut c2, mut d2, mut e2) = (0, vec![], None, String::new(), 0.0); + let mut r = Reader::new(&w.out); + visit(&mut r, &mut a2, &mut b2, &mut c2, &mut d2, &mut e2); + assert!(r.finish().is_ok()); + assert_eq!((a2, b2.clone(), c2, d2.clone(), e2), (7, vec![1, 2, 3], Some(9), "héllo".to_string(), 1.5)); + // Cut short: a failure, zeros after it. + let mut r = Reader::new(&w.out[..6]); + visit(&mut r, &mut a2, &mut b2, &mut c2, &mut d2, &mut e2); + assert!(r.finish().unwrap_err().contains("ends early")); + // Extra bytes are refused. + let mut long = w.out.clone(); + long.push(0); + let mut r = Reader::new(&long); + visit(&mut r, &mut a2, &mut b2, &mut c2, &mut d2, &mut e2); + assert!(r.finish().unwrap_err().contains("unread")); + // A length past the limit is refused, not allocated. + let mut huge = Writer::default(); + huge.u64(&mut (u64::MAX / 2)); + let mut v = Vec::new(); + let mut r = Reader::new(&huge.out); + r.vec_u32(&mut v); + assert!(v.is_empty() && r.finish().is_err()); + } +} diff --git a/src/dev/kona/tables.rs b/src/dev/kona/tables.rs new file mode 100644 index 000000000..758ec5767 --- /dev/null +++ b/src/dev/kona/tables.rs @@ -0,0 +1,622 @@ +//! The selector tables: for each command word a port can open, how many +//! argument words follow. Counts are those of the senders read. +//! +//! Ported from hw/misc/sgi-kona-ports.c in jamesbraid/qemu-sgi-origami. + +use super::defs::*; +use super::frame::Selector; + +/// The kernel FIFO push port: the DMA packet, the list-write record and the pool +/// registration, then the display-mode and select-feed selectors. +pub(super) const DMA_FIFO: &[Selector] = &[ + Selector::new(START_INPUT_DMA, 6), + Selector::new(COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA, 2), + Selector::new(DESCRIBE_DMA_PAGE_POOL, 3), + Selector::new(DISPLAY_MODE_BYTE, 3), + Selector::new(START_SELECT_FEEDBACK_RETURN_DMA, 0), + Selector::new(FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE, 0), +]; + +/// The textport's commands, which follow a sync word. +pub(super) const TEXTPORT: &[Selector] = &[ + Selector::new(TEXTPORT_UNMODELLED_1, 1), + Selector::new(TEXTPORT_SET_COLOUR, 1), + Selector::new(TEXTPORT_LOAD_COLOUR_MAP, 4), + Selector::new(TEXTPORT_SET_RASTER_POSITION, 2), + Selector::new(TEXTPORT_FILL_BOX, 4), + Selector::new(TEXTPORT_PLOT_POINT, 2), + Selector::new(TEXTPORT_UNMODELLED_7, 2), + Selector::new(TEXTPORT_UNMODELLED_8, 2), + Selector::new(TEXTPORT_SYNC_PROBE, 1), + Selector::new(TEXTPORT_GRAPHICS_INFO, 1), +]; + +/// The command page's tokens: kernel, graphics-process and X server commands. +pub(super) const KERNEL: &[Selector] = &[ + Selector::new(PIPE_SET_UP, 3), + Selector::new(SET_UP_OUTPUT_DMA_FOR_HOST_READ, 3), + Selector::new(CONTEXT_INITIALISE, 3), + Selector::new(CONTEXT_CLEAN_UP_WITH_EVENT_CODE, 1), + Selector::new(RAISE_HOST_INTERRUPT_WITH_EVENT_CODE, 1), + Selector::new(QUERY_BOARD_CONFIGURATION_BLOCK, 1), + Selector::new(EEPROM_OPERATION, 1), + Selector::new(DISPLAY_MODE_BYTE, 3), + Selector::new(CONTEXT_DEACTIVATE, 1), + Selector::new(CONTEXT_IMAGE_LOAD, 4), + Selector::new(WINDOW_CLIP_SETUP, 11), + Selector::new(WINDOW_READ_CLIP_SET_UP, 3), + Selector::new(DISPLAY_BANK_SET_UP, 2), + Selector::new(DESCRIBE_DMA_PAGE_POOL, 3), + Selector::new(START_INPUT_DMA, 6), + Selector::new(COPY_LIST_BY_INPUT_DMA_THEN_LIST_DMA, 2), + Selector::new(START_SELECT_FEEDBACK_RETURN_DMA, 6), + Selector::new(FINISH_SELECT_FEEDBACK_WITH_EVENT_CODE, 2), + Selector::new(EXECUTE_LIST_BY_LIST_DMA, 2), + Selector::new(EXECUTE_LIST_BY_INPUT_DMA, 2), + Selector::new(EXECUTE_LIST_BY_LIST_DMA_REPEATED, 1), + Selector::new(EXECUTE_LIST_BY_INPUT_DMA_REPEATED, 1), + Selector::new(COPY_USER_LIST_BY_LIST_DMA, 4), + Selector::new(MOVE_LIST_BY_LIST_DMA, 3), + Selector::new(PUBLISH_RETRACE_SEQUENCE_NUMBER, 1), + Selector::new(PIPE_MEMORY_RESERVE, 4), + Selector::new(PIPE_MEMORY_COPY_DOWN, 4), + Selector::new(PIPE_MEMORY_SWAP_OUT, 8), + Selector::new(PIPE_MEMORY_SWAP_IN, 6), + Selector::new(PIPE_MEMORY_SUSPEND_WITH_EVENT_CODE, 1), + Selector::new(PIPE_MEMORY_RESUME, 0), + Selector::new(DISTRIBUTOR_NO_OP_PADDING_WORD, 1), + Selector::new(WINDOW_SYSTEM_CONTEXT_INITIALISE, 3), + Selector::new(WINDOW_SYSTEM_GRAPHICS_CONTEXT_LOAD, 13), + Selector::new(WINDOW_SYSTEM_DRAWABLE_ORIGIN_AND_SIZE_LOAD, 4), + Selector::new(WINDOW_SYSTEM_FOUR_WORD_DATA_PRELOAD_BYTE_FORM, 4), + Selector::new(WINDOW_SYSTEM_CLIP_LOAD, 5), + Selector::new(WINDOW_SYSTEM_SCISSOR_LOAD, 4), + Selector::new(LOAD_COLOUR_MAP_ENTRIES, 2), + Selector::new(LOAD_GAMMA_TABLE_ENTRIES, 2), + Selector::new(LOAD_CURSOR_IMAGE_BLOCK, 1), + Selector::new(LOAD_CURSOR_COLOUR_WORDS, 1), + Selector::new(LOAD_ONE_ENTRY_OF_DISPLAY_ID_TABLE, 2), + Selector::new(WINDOW_CREATED_OR_DESTROYED_NOTICE_TO_COLOUR_MAP_CHIP, 1), + Selector::new(WINDOW_SYSTEM_PIXEL_PATH_SET_UP, 7), + Selector::new(WINDOW_SYSTEM_PIXEL_READ, 2), + Selector::new(WINDOW_SYSTEM_PIXEL_WRITE, 2), + Selector::new(WINDOW_SYSTEM_PIXEL_PATH_FINISH, 1), + Selector::new(WINDOW_SYSTEM_PIXEL_COPY, 4), + Selector::new(LOAD_WINDOW_SYSTEM_STIPPLE, 1), + Selector::new(STORE_WINDOW_SYSTEM_STIPPLE, 3), + Selector::new(ROTATE_WINDOW_SYSTEM_STIPPLE, 3), + Selector::new(DRAW_ABSOLUTE_POINTS, 1), + Selector::new(DRAW_ABSOLUTE_POINTS_BURST_OF_6, 6), + Selector::new(WINDOW_SYSTEM_1_WORD_DATA_PRELOAD, 1), + Selector::new(WINDOW_SYSTEM_3_WORD_DATA_PRELOAD, 3), + Selector::new(WINDOW_SYSTEM_5_WORD_DATA_PRELOAD, 5), + Selector::new(DRAW_ABSOLUTE_POINTS_BURST_OF_24, 24), + Selector::new(DRAW_POINTS_IN_BACKGROUND_COLOUR, 1), + Selector::new(DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_6, 6), + Selector::new(DRAW_POINTS_IN_BACKGROUND_COLOUR_BURST_OF_24, 24), + Selector::new(DRAW_RELATIVE_POINTS, 1), + Selector::new(DRAW_RELATIVE_POINTS_BURST_OF_6, 6), + Selector::new(DRAW_RELATIVE_POINTS_BURST_OF_24, 24), + Selector::new(DRAW_LINE_SEGMENTS, 2), + Selector::new(DRAW_LINE_SEGMENTS_BURST_OF_6, 12), + // The polyline tokens' word counts are the X server's sender's (established): + // 1, 7, 2 and 7. A longer polyline arrives as n - 1 words of data preload and + // the one-word token for the last point (the pipe's consumption is inferred). + Selector::new(DRAW_CONNECTED_POLYLINE, 1), + Selector::new(DRAW_CONNECTED_POLYLINE_BURST_OF_6, 7), + Selector::new(DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS, 2), + Selector::new(DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS_BURST_OF_6, 7), + Selector::new(WINDOW_SYSTEM_2_WORD_DATA_PRELOAD, 2), + Selector::new(WINDOW_SYSTEM_4_WORD_DATA_PRELOAD, 4), + Selector::new(WINDOW_SYSTEM_6_WORD_DATA_PRELOAD, 6), + Selector::new(WINDOW_SYSTEM_8_WORD_DATA_PRELOAD, 8), + Selector::new(WINDOW_SYSTEM_10_WORD_DATA_PRELOAD, 10), + Selector::new(FILL_BOXES_TWO_CORNERS, 2), + Selector::new(FILL_BOXES_TWO_CORNERS_BURST_OF_6, 12), + Selector::new(FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT, 2), + Selector::new(FILL_RECTANGLES_ORIGIN_WIDTH_HEIGHT_BURST_OF_6, 12), + Selector::new(FILL_SPANS, 3), + Selector::new(FILL_SPANS_BURST_OF_2, 5), + Selector::new(FILL_SPANS_BURST_OF_6, 13), + Selector::new(WRITE_GATHER_FLUSH_LAST_SLOT, 1), + Selector::new(DISPLAY_GENERATOR_REQUEST_CARRIER, 1), +]; + +/// The GL client's commands, keyed by the low 16 bits of the opening word. +pub(super) const GL: &[Selector] = &[ + Selector::new(CONFIGURE_FRAME_BUFFER_LAYOUT, 10), + Selector::new(FLUSH_AND_REBUILD_HARDWARE_TOKEN_MAP_FOR_PACKED_VERTEX, 1), + Selector::new(CONFIGURE_READ_SIDE_FRAME_BUFFER_LAYOUT, 10), + Selector::new(SELECT_VIDEO_SOURCE, 1), + Selector::new(SELECT_ACTIVE_TEXTURE_UNIT, 1), + Selector::new(CHANGE_POST_TEXTURE_SCALE_AND_BIAS, 4), + Selector::new(LOAD_ONE_ENTRY_OF_ARM_TEXTURE_TABLE, 1), + Selector::new(RESTART_SELECT_OR_FEEDBACK, 1), + Selector::new(QUERY_LINE_MODE_LIMIT_TABLE, 1), + Selector::new(SET_LAZY_VALIDATION_MODE, 1), + Selector::new(SWITCH_BETWEEN_RENDER_SELECT_AND_FEEDBACK_MODES, 1), + Selector::new(CLEAR_SELECTION_NAME_STACK, 1), + Selector::new(REPLACE_TOP_SELECTION_NAME, 1), + Selector::new(POP_SELECTION_NAME, 1), + Selector::new(PUSH_SELECTION_NAME, 1), + Selector::new(SELECT_FEEDBACK_RECORD_TYPE, 1), + Selector::new(INSERT_FEEDBACK_PASS_THROUGH_MARKER, 1), + Selector::new(SET_VIEWPORT, 4), + Selector::new(SET_SCISSOR_BOX, 4), + Selector::new(RECORD_ERROR_CODE, 1), + Selector::new(SET_COLOUR_CLEAR_VALUE, 4), + Selector::new(CLEAR_SELECTED_BUFFERS, 1), + Selector::new(SET_ACCUMULATION_BUFFER_CLEAR_VALUE, 4), + Selector::new(ACCUMULATION_BUFFER_OPERATION, 2), + Selector::new(SET_COLOUR_INDEX_CLEAR_VALUE, 1), + Selector::new(SET_STENCIL_CLEAR_VALUE, 1), + Selector::new(SET_DEPTH_CLEAR_VALUE, 2), + Selector::new(BEGIN_PRIMITIVE_POINTS, 1), // gl_begin_first + 0 + Selector::new(BEGIN_PRIMITIVE_INDEPENDENT_LINES, 1), // gl_begin_first + 1 + Selector::new(BEGIN_PRIMITIVE_LINE_LOOP, 1), // gl_begin_first + 2 + Selector::new(BEGIN_PRIMITIVE_LINE_STRIP, 1), // gl_begin_first + 3 + Selector::new(BEGIN_PRIMITIVE_INDEPENDENT_TRIANGLES, 1), // gl_begin_first + 4 + Selector::new(BEGIN_PRIMITIVE_TRIANGLE_STRIP, 1), // gl_begin_first + 5 + Selector::new(BEGIN_PRIMITIVE_TRIANGLE_FAN, 1), // gl_begin_first + 6 + Selector::new(BEGIN_PRIMITIVE_INDEPENDENT_QUADS, 1), // gl_begin_first + 7 + Selector::new(BEGIN_PRIMITIVE_QUAD_STRIP, 1), // gl_begin_first + 8 + Selector::new(BEGIN_PRIMITIVE_POLYGON, 1), + Selector::new(END_OPEN_PRIMITIVE, 1), + Selector::new(BEGIN_PRIMITIVE_RECTANGLE, 1), + Selector::new(FLUSH_VERTEX_PIPELINE, 1), + Selector::new(FINISH_DRAIN_PIPE_AND_SIGNAL_COMPLETION, 1), + Selector::new(FLUSH_RASTER_BOARDS, 1), + Selector::new(QUERY_PENDING_ERROR, 1), + Selector::new(QUERY_CURRENT_VERTEX_ATTRIBUTES, 1), + Selector::new(QUERY_RASTER_POSITION_STATE, 1), + Selector::new(LOAD_ENGINE_TRANSFORM_ATTRIBUTES, 1), + Selector::new(POP_TEXTURE_GENERATION_STATE, 1), + Selector::new(SET_DEPTH_RANGE, 4), + Selector::new(SELECT_CURRENT_MATRIX_STACK, 1), + Selector::new(MULTIPLY_CURRENT_MATRIX_BY_FRUSTUM, 12), + Selector::new(LOAD_IDENTITY_MATRIX, 1), + Selector::new(LOAD_MATRIX_16_FLOATS, 16), + Selector::new(LOAD_MATRIX_16_DOUBLES, 32), + Selector::new(MULTIPLY_BY_MATRIX_16_FLOATS, 16), + Selector::new(MULTIPLY_BY_MATRIX_16_DOUBLES, 32), + Selector::new(MULTIPLY_CURRENT_MATRIX_BY_ORTHOGRAPHIC_PROJECTION, 12), + Selector::new(POP_MATRIX_STACK, 1), + Selector::new(PUSH_MATRIX_STACK, 1), + Selector::new(ROTATE_DOUBLES, 8), + Selector::new(ROTATE_FLOATS, 4), + Selector::new(SCALE_DOUBLES, 6), + Selector::new(SCALE_FLOATS, 3), + Selector::new(TRANSLATE_DOUBLES, 6), + Selector::new(TRANSLATE_FLOATS, 3), + Selector::new(SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_BYTE, 1), + Selector::new(SET_CURRENT_COLOUR_3_COMPONENTS_DOUBLE, 6), + Selector::new(SET_CURRENT_COLOUR_3_COMPONENTS_SINGLE_FLOAT, 3), + Selector::new(SET_CURRENT_COLOUR_3_COMPONENTS_SIGNED_32_BIT_INTEGER, 3), + Selector::new(SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_BYTE, 1), + Selector::new(SET_CURRENT_COLOUR_3_COMPONENTS_UNSIGNED_32_BIT_INTEGER, 3), + Selector::new(SET_CURRENT_COLOUR_4_COMPONENTS_DOUBLE, 8), + Selector::new(SET_CURRENT_COLOUR_4_COMPONENTS_SINGLE_FLOAT, 4), + Selector::new(SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_32_BIT_INTEGER, 4), + Selector::new(SET_CURRENT_COLOUR_4_COMPONENTS_SIGNED_SHORT, 2), + Selector::new(SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_BYTE, 1), + Selector::new(SET_CURRENT_COLOUR_4_COMPONENTS_UNSIGNED_32_BIT_INTEGER, 4), + Selector::new(SET_CURRENT_COLOUR_INDEX_DOUBLE, 2), + Selector::new(SET_CURRENT_COLOUR_INDEX_SINGLE_FLOAT, 1), + Selector::new(SET_CURRENT_COLOUR_INDEX_SIGNED_32_BIT_INTEGER, 1), + Selector::new(SET_CURRENT_COLOUR_INDEX_SIGNED_SHORT, 1), + Selector::new(SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_BYTE, 1), + Selector::new(SET_CURRENT_NORMAL_3_COMPONENTS_DOUBLE, 6), + Selector::new(SET_CURRENT_NORMAL_3_COMPONENTS_SINGLE_FLOAT, 3), + Selector::new(SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_32_BIT_INTEGER, 3), + Selector::new(SET_CURRENT_NORMAL_3_COMPONENTS_SIGNED_SHORT, 1), + Selector::new(SET_CURRENT_TEXTURE_COORDINATE_1_COMPONENTS_SINGLE_FLOAT, 1), + Selector::new(SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SINGLE_FLOAT, 2), + Selector::new(SET_CURRENT_TEXTURE_COORDINATE_2_COMPONENTS_SIGNED_SHORT, 1), + Selector::new(SET_CURRENT_TEXTURE_COORDINATE_3_COMPONENTS_SINGLE_FLOAT, 3), + Selector::new(SET_CURRENT_TEXTURE_COORDINATE_4_COMPONENTS_SINGLE_FLOAT, 4), + Selector::new(SUBMIT_VERTEX_2_COMPONENTS_DOUBLE, 4), + Selector::new(SUBMIT_VERTEX_2_COMPONENTS_SINGLE_FLOAT, 2), + Selector::new(SUBMIT_VERTEX_2_COMPONENTS_SIGNED_32_BIT_INTEGER, 2), + Selector::new(SUBMIT_VERTEX_2_COMPONENTS_SIGNED_SHORT, 1), + Selector::new(SUBMIT_VERTEX_3_COMPONENTS_DOUBLE, 6), + Selector::new(SUBMIT_VERTEX_3_COMPONENTS_SINGLE_FLOAT, 3), + Selector::new(SUBMIT_VERTEX_3_COMPONENTS_SIGNED_32_BIT_INTEGER, 3), + Selector::new(SUBMIT_VERTEX_3_COMPONENTS_SIGNED_SHORT, 1), + Selector::new(SUBMIT_VERTEX_4_COMPONENTS_DOUBLE, 8), + Selector::new(SUBMIT_VERTEX_4_COMPONENTS_SINGLE_FLOAT, 4), + Selector::new(SUBMIT_VERTEX_4_COMPONENTS_SIGNED_32_BIT_INTEGER, 4), + Selector::new(SUBMIT_VERTEX_4_COMPONENTS_SIGNED_SHORT, 2), + Selector::new(SET_CURRENT_EDGE_FLAG, 1), + Selector::new(SET_RASTER_POSITION_2_COMPONENTS_FLOAT, 2), + Selector::new(SET_RASTER_POSITION_3_COMPONENTS_FLOAT, 3), + Selector::new(SET_RASTER_POSITION_4_COMPONENTS_FLOAT, 4), + Selector::new(SET_CURRENT_RASTER_POSITION, 15), + Selector::new(SET_UP_1D_EVALUATOR_GRID, 4), + Selector::new(SET_UP_2D_EVALUATOR_GRID, 8), + Selector::new(SKIP_2_WORDS, 1), + Selector::new(SKIP_4_WORDS, 3), + Selector::new(SKIP_5_WORDS, 4), + Selector::new(SET_POINT_PARAMETER, 2), + Selector::new(SET_POINT_PARAMETERS, 4), + Selector::new(SET_SPRITE_PARAMETER, 2), + Selector::new(SET_SPRITE_PARAMETERS, 4), + Selector::new(SET_MULTISAMPLE_COVERAGE_MASK, 2), + Selector::new(SET_MULTISAMPLE_PATTERN, 1), + Selector::new(LOAD_1D_CONVOLUTION_FILTER, 7), + Selector::new(LOAD_2D_CONVOLUTION_FILTER, 8), + Selector::new(COPY_1D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER, 4), + Selector::new(COPY_2D_CONVOLUTION_FILTER_FROM_FRAME_BUFFER, 5), + Selector::new(QUERY_CONVOLUTION_FILTER, 3), + Selector::new(LOAD_SEPARABLE_2D_FILTER, 9), + Selector::new(QUERY_SEPARABLE_FILTER, 3), + Selector::new(SET_PIXEL_TRANSFORM_PARAMETERS_INTEGER, 3), + Selector::new(SET_PIXEL_TRANSFORM_PARAMETERS_FLOAT, 6), + Selector::new(RESOLVE_WHETHER_PIXEL_FORMAT_IS_SINGLE_COMPONENT, 4), + Selector::new(LOAD_COLOUR_TABLE, 9), + Selector::new(QUERY_COLOUR_TABLE, 4), + Selector::new(SET_COLOUR_TABLE_PARAMETERS, 6), + Selector::new(QUERY_COLOUR_TABLE_PARAMETERS, 2), + Selector::new(SET_UP_HISTOGRAM, 5), + Selector::new(RESET_HISTOGRAM, 1), + Selector::new(QUERY_HISTOGRAM, 1), + Selector::new(QUERY_HISTOGRAM_PARAMETERS, 2), + Selector::new(SET_UP_MIN_MAX_TRACKING, 2), + Selector::new(RESET_MIN_MAX_TRACKING, 1), + Selector::new(QUERY_MIN_MAX_RESULTS, 1), + Selector::new(QUERY_MIN_MAX_PARAMETERS, 1), + Selector::new(SET_LIGHT_PARAMETER_FLOAT_FORM, 6), + Selector::new(SET_LIGHT_PARAMETER_INTEGER_FORM, 6), + Selector::new(SET_LIGHTING_MODEL_PARAMETER_FLOAT_FORM, 5), + Selector::new(SET_LIGHTING_MODEL_PARAMETER_INTEGER_FORM, 5), + Selector::new(SET_MATERIAL_PARAMETER_FLOAT_FORM, 6), + Selector::new(SET_COLOUR_MATERIAL_TRACKING, 2), + Selector::new(ENABLE_CAPABILITY, 1), + Selector::new(DISABLE_CAPABILITY, 1), + Selector::new(SET_FLAT_OR_SMOOTH_SHADING, 1), + Selector::new(SET_DEPTH_TEST, 1), + Selector::new(SET_DEPTH_WRITE_MASK, 1), + Selector::new(SET_BLEND_FACTORS, 2), + Selector::new(SET_CONSTANT_BLEND_COLOUR, 4), + Selector::new(SET_BLEND_EQUATION, 1), + Selector::new(SET_ALPHA_TEST, 2), + Selector::new(SET_STENCIL_TEST, 3), + Selector::new(SET_STENCIL_OPERATIONS, 3), + Selector::new(SET_STENCIL_WRITE_MASK, 1), + Selector::new(SET_COLOUR_WRITE_MASK, 4), + Selector::new(SET_COLOUR_INDEX_WRITE_MASK, 1), + Selector::new(SET_LOGIC_OPERATION, 1), + Selector::new(SET_FACE_CULLING, 1), + Selector::new(SET_FRONT_FACE_WINDING, 1), + Selector::new(SET_POLYGON_FILL_MODE, 2), + Selector::new(SET_POLYGON_OFFSET, 2), + Selector::new(SET_POLYGON_OFFSET_EXTENSION_FORM, 2), + Selector::new(LOAD_POLYGON_STIPPLE_PATTERN, 3), + Selector::new(SET_LINE_WIDTH, 2), + Selector::new(SET_POINT_SIZE, 2), + Selector::new(SET_LINE_STIPPLE, 2), + Selector::new(SET_FOG_PARAMETER_FLOAT_FORM, 5), + Selector::new(SET_FOG_PARAMETER_INTEGER_FORM, 5), + Selector::new(SET_USER_CLIP_PLANE, 9), + Selector::new(SET_QUALITY_HINT, 2), + Selector::new(SELECT_DRAW_BUFFER, 1), + Selector::new(SELECT_READ_BUFFER, 1), + Selector::new(SET_PIXEL_ZOOM_FACTORS, 2), + Selector::new(SET_PIXEL_TRANSFER_PARAMETER_FLOAT_FORM, 2), + Selector::new(SET_PIXEL_STORE_PARAMETER_INTEGER_FORM, 2), + Selector::new(LOAD_PIXEL_MAP_FLOAT_VALUES, 4), + Selector::new(LOAD_PIXEL_MAP_UNSIGNED_32_BIT_VALUES, 4), + Selector::new(LOAD_PIXEL_MAP_UNSIGNED_16_BIT_VALUES, 4), + Selector::new(SET_TEXTURE_ENVIRONMENT_PARAMETER_FLOAT_FORM, 5), + Selector::new(SET_TEXTURE_ENVIRONMENT_PARAMETER_INTEGER_FORM, 5), + Selector::new(DRAW_BITMAP_FROM_DISPLAY_LIST, 8), + Selector::new(DRAW_BITMAP, 9), + Selector::new(DRAW_PIXEL_RECTANGLE_FROM_HOST_DATA, 8), + Selector::new(READ_PIXEL_RECTANGLE_BACK_TO_HOST, 7), + Selector::new(COPY_PIXEL_RECTANGLE_WITHIN_FRAME_BUFFER, 7), + Selector::new(ASK_WHETHER_COPY_SOURCE_OVERLAPS_ITS_DESTINATION, 4), + Selector::new(QUERY_TEXTURE_IMAGE, 18), + Selector::new(SUSPEND_PIXEL_MODE_STATE_AROUND_TRANSFER, 2), + Selector::new(ASK_WHETHER_CONVOLUTION_COPY_OVERLAPS, 4), + Selector::new(QUERY_STATE_GENERIC, 1), + Selector::new(PUSH_ATTRIBUTE_STACK, 1), + Selector::new(POP_ATTRIBUTE_STACK, 1), + Selector::new(POP_SAVED_LIGHT_STATE, 1), + Selector::new(BIND_ARM_TEXTURE_CACHE_ENTRY, 1), + Selector::new(FREE_ARM_TEXTURE_CACHE_ENTRY, 1), + Selector::new(DRAW_TEXELS_INTO_TEXTURE_MEMORY, 20), + Selector::new(SET_TEXEL_STORE_PARAMETER, 2), + Selector::new(TEXTURE_LOAD_BY_IDENTIFIER, 1), + Selector::new(BIND_TEXTURE_BY_IDENTIFIER, 1), + Selector::new(BIND_2D_MIP_MAPPED_TEXTURE, 22), + Selector::new(BIND_3D_MIP_MAPPED_TEXTURE, 14), + Selector::new(BIND_3D_TEXTURE, 4), + Selector::new(BIND_DETAIL_TEXTURE, 6), + Selector::new(BIND_BICUBIC_TEXTURE, 5), + Selector::new(BIND_2D_CLIP_MAP_TEXTURE, 8), + Selector::new(SET_SHADOW_TEXTURE_STATE, 3), + Selector::new(TEXTURE_LOAD_SET_UP, 11), + Selector::new(BIND_TEXTURE_CURRENT_FORM, 5), + // Five words, as the table's count and text agree; the recorded GL demos frame correctly with five and misframe with seven. + Selector::new(BIND_LINEAR_TEXTURE, 5), + Selector::new(LOAD_POST_TEXTURE_SCALE_AND_BIAS, 4), + Selector::new(INVALIDATE_ARM_TEXTURE_CACHE_INDEX, 2), + Selector::new(SWITCH_LEGACY_API_COMPATIBILITY_MODE, 1), + Selector::new(SET_COLOUR_MATERIAL_VALUE, 1), + Selector::new(SWAP_TRIANGLE_MESH_VERTEX, 1), + Selector::new(SET_COLOUR_NORMAL_MODE_VALUE, 1), + Selector::new(SET_TRIANGLE_MESH_MODE, 1), + Selector::new(TEXTURE_COORDINATE_GENERATION_FLOAT_FORM, 9), + Selector::new(SET_RASTER_POSITION_2_COMPONENTS_DOUBLE, 4), + Selector::new(SET_RASTER_POSITION_2_COMPONENTS_32_BIT_INTEGER, 2), + Selector::new(SET_RASTER_POSITION_2_COMPONENTS_SHORT, 2), + Selector::new(SET_RASTER_POSITION_3_COMPONENTS_DOUBLE, 6), + Selector::new(SET_RASTER_POSITION_3_COMPONENTS_32_BIT_INTEGER, 3), + Selector::new(SET_RASTER_POSITION_3_COMPONENTS_SHORT, 3), + Selector::new(SET_RASTER_POSITION_4_COMPONENTS_DOUBLE, 8), + Selector::new(SET_RASTER_POSITION_4_COMPONENTS_32_BIT_INTEGER, 4), + Selector::new(SET_RASTER_POSITION_4_COMPONENTS_SHORT, 4), + Selector::new(SET_VERTEX_ARRAY_COMMAND_TABLE, 1), + Selector::new(VERTEX_ARRAY_SET_UP, 1), + Selector::new(BEGIN_VERTEX_ARRAY_LOOP, 3), + Selector::new(VERTEX_ARRAY_TEAR_DOWN, 1), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_1, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_2, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_3, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_4, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_5, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_6, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_7, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_8, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_9, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_10, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_11, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_12, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_13, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_14, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_15, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_16, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_17, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_18, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_19, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_20, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_21, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_22, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_23, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_24, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_25, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_26, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_27, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_28, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_29, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_30, 0), + Selector::new(VERTEX_ARRAY_LOOP_ENTRY_31, 0), +]; + +/// The video command table: the data words each command carries between its +/// number and its checksum (established for all 93 requests). +pub(super) const VIDEO: &[Option] = &[ + Some(1), // 0x00 Test + Some(2), // 0x01 VCWrite + Some(1), // 0x02 VCRead + Some(10), // 0x03 DiagXmapColor + Some(1), // 0x04 DisableDiagXmapColor + Some(4), // 0x05 VCBusTrace + Some(4107), // 0x06 DiagCursorFromImage + Some(1), // 0x07 DebugMask + Some(3), // 0x08 SetupFrameBuffer + Some(1), // 0x09 InputSync + Some(1), // 0x0a GetInputSync + Some(2), // 0x0b HPhase + Some(1), // 0x0c GetHPhase + Some(1), // 0x0d VPhase + Some(1), // 0x0e GetVPhase + Some(2), // 0x0f ChannelGammaMap + Some(1), // 0x10 GetChannelGammaMap + Some(1), // 0x11 Fetal + Some(1), // 0x12 GetBoardConfig + Some(1), // 0x13 GetChannelConfig + Some(8), // 0x14 Resize + Some(1), // 0x15 GetResize + Some(383), // 0x16 LoadFormat + Some(3), // 0x17 EnableChannel + Some(1), // 0x18 DisableChannel + Some(16), // 0x19 LoadGenlockParams + Some(1), // 0x1a EnableGenlock + Some(2), // 0x1b CursorPosition + Some(1), // 0x1c CursorChannel + Some(1), // 0x1d CursorType + Some(2), // 0x1e CursorPriority + Some(2), // 0x1f ChannelBlanking + Some(1), // 0x20 GetChannelBlanking + Some(2), // 0x21 OutputGain + Some(1), // 0x22 GetOutputGain + Some(4), // 0x23 SyncSelects + Some(1), // 0x24 GetSyncSelects + Some(2), // 0x25 OutputPedestal + Some(1), // 0x26 GetOutputPedestal + Some(1), // 0x27 InitTextport + Some(1), // 0x28 LoadTextportGlyph + Some(4), // 0x29 LoadTextportColorIndex + Some(1), // 0x2a EnableVocReadback + Some(1), // 0x2b MasterClockSelect + Some(1), // 0x2c GetMasterClockSelect + Some(1), // 0x2d AutoInit + Some(1), // 0x2e LoadEncoder + Some(1), // 0x2f EncoderOutputGain + Some(1), // 0x30 GetEncoderOutputGain + Some(1), // 0x31 SCHPhase + Some(1), // 0x32 GetSCHPhase + Some(1), // 0x33 EncoderTestPattern + Some(2), // 0x34 BindChannelToXWid + Some(1328), // 0x35 LoadPabXilinx + Some(4), // 0x36 LoadPabSizeRegs + Some(2), // 0x37 LoadPabCtrlRegs + Some(263), // 0x38 LoadDacCursor + Some(7), // 0x39 VampireAndFieldDominance + Some(1), // 0x3a GetVampire + Some(1), // 0x3b FastlistChunksize + Some(1), // 0x3c TempBlankAllChannels + Some(1), // 0x3d GetDGInterruptState + Some(1), // 0x3e XMapClockSelect + Some(1), // 0x3f GetXMapClockSelect + Some(3), // 0x40 ChannelResetRatios + Some(1), // 0x41 ResetRMBoards + Some(1), // 0x42 GetChannelEnables + Some(9), // 0x43 PlacementRes + Some(1), // 0x44 GenlockSwitching + Some(1), // 0x45 GetGenlockSwitching + Some(2), // 0x46 ChannelFieldDominance + Some(1), // 0x47 GetChannelFieldDominance + Some(1), // 0x48 BoardResetRatio + Some(1), // 0x49 GetBoardResetRatio + Some(1), // 0x4a GetChannelFormatInfo + Some(5), // 0x4b ChannelGammaMapState + Some(1), // 0x4c GetChannelGammaMapState + Some(1), // 0x4d EnableEncoder + Some(2), // 0x4e ResizeSyncType + Some(7), // 0x4f Vampire + Some(1), // 0x50 QueryMonitorName + Some(34), // 0x51 MonitorCommand + Some(6), // 0x52 LoadXilinx + Some(1), // 0x53 XServerVideoInit + Some(1), // 0x54 InternalGenlock + Some(1), // 0x55 GetInternalGenlock + Some(3), // 0x56 BufferParams + Some(1), // 0x57 GetBufferParams + Some(3), // 0x58 DataFormat + Some(1), // 0x59 GetDataFormat + Some(1), // 0x5a DVPMult2Freq + Some(42), // 0x5b VBOBCommand + Some(1), // 0x5c GetVBOBReply +]; + +/// The reply each video command waits for, by command number: the reply id and the bytes of data +/// after it, or `None` for a command nobody waits on. Established for all 93 requests (32 of them +/// reply; the monitor command, which carries a payload, does not). +pub(super) const VIDEO_REPLY: &[Option<(u8, u8)>] = &[ + Some((0, 4)), // 0x00 Test + None, // 0x01 VCWrite + Some((1, 4)), // 0x02 VCRead + None, // 0x03 DiagXmapColor + None, // 0x04 DisableDiagXmapColor + None, // 0x05 VCBusTrace + None, // 0x06 DiagCursorFromImage + None, // 0x07 DebugMask + None, // 0x08 SetupFrameBuffer + None, // 0x09 InputSync + Some((2, 4)), // 0x0a GetInputSync + None, // 0x0b HPhase + Some((3, 4)), // 0x0c GetHPhase + None, // 0x0d VPhase + Some((4, 4)), // 0x0e GetVPhase + None, // 0x0f ChannelGammaMap + Some((5, 4)), // 0x10 GetChannelGammaMap + None, // 0x11 Fetal + Some((6, 20)), // 0x12 GetBoardConfig + Some((7, 76)), // 0x13 GetChannelConfig + None, // 0x14 Resize + Some((8, 72)), // 0x15 GetResize + None, // 0x16 LoadFormat + None, // 0x17 EnableChannel + None, // 0x18 DisableChannel + None, // 0x19 LoadGenlockParams + None, // 0x1a EnableGenlock + None, // 0x1b CursorPosition + None, // 0x1c CursorChannel + None, // 0x1d CursorType + None, // 0x1e CursorPriority + None, // 0x1f ChannelBlanking + Some((9, 4)), // 0x20 GetChannelBlanking + None, // 0x21 OutputGain + Some((10, 4)), // 0x22 GetOutputGain + None, // 0x23 SyncSelects + Some((11, 24)), // 0x24 GetSyncSelects + None, // 0x25 OutputPedestal + Some((12, 4)), // 0x26 GetOutputPedestal + None, // 0x27 InitTextport + None, // 0x28 LoadTextportGlyph + None, // 0x29 LoadTextportColorIndex + None, // 0x2a EnableVocReadback + None, // 0x2b MasterClockSelect + Some((13, 4)), // 0x2c GetMasterClockSelect + None, // 0x2d AutoInit + None, // 0x2e LoadEncoder + None, // 0x2f EncoderOutputGain + Some((14, 4)), // 0x30 GetEncoderOutputGain + None, // 0x31 SCHPhase + Some((15, 4)), // 0x32 GetSCHPhase + None, // 0x33 EncoderTestPattern + None, // 0x34 BindChannelToXWid + None, // 0x35 LoadPabXilinx + None, // 0x36 LoadPabSizeRegs + None, // 0x37 LoadPabCtrlRegs + None, // 0x38 LoadDacCursor + None, // 0x39 VampireAndFieldDominance + Some((16, 24)), // 0x3a GetVampire + None, // 0x3b FastlistChunksize + None, // 0x3c TempBlankAllChannels + Some((17, 12)), // 0x3d GetDGInterruptState + None, // 0x3e XMapClockSelect + Some((18, 4)), // 0x3f GetXMapClockSelect + None, // 0x40 ChannelResetRatios + None, // 0x41 ResetRMBoards + Some((19, 8)), // 0x42 GetChannelEnables + None, // 0x43 PlacementRes + None, // 0x44 GenlockSwitching + Some((20, 4)), // 0x45 GetGenlockSwitching + None, // 0x46 ChannelFieldDominance + Some((21, 4)), // 0x47 GetChannelFieldDominance + None, // 0x48 BoardResetRatio + Some((22, 4)), // 0x49 GetBoardResetRatio + Some((23, 20)), // 0x4a GetChannelFormatInfo + None, // 0x4b ChannelGammaMapState + Some((24, 16)), // 0x4c GetChannelGammaMapState + None, // 0x4d EnableEncoder + None, // 0x4e ResizeSyncType + None, // 0x4f Vampire + Some((25, 68)), // 0x50 QueryMonitorName + None, // 0x51 MonitorCommand + Some((27, 4)), // 0x52 LoadXilinx + None, // 0x53 XServerVideoInit + None, // 0x54 InternalGenlock + Some((28, 4)), // 0x55 GetInternalGenlock + None, // 0x56 BufferParams + Some((29, 8)), // 0x57 GetBufferParams + None, // 0x58 DataFormat + Some((30, 8)), // 0x59 GetDataFormat + None, // 0x5a DVPMult2Freq + None, // 0x5b VBOBCommand + Some((31, 164)), // 0x5c GetVBOBReply +]; + +#[cfg(test)] +mod tests { + use super::*; + + /// Every selector of the kernel and GL tables is a host token the definitions + /// name, once, and the table's own entries do not repeat a token. + #[test] + fn the_selector_tables_and_the_definitions_agree() { + for (what, table) in [("kernel", KERNEL), ("GL", GL), ("dma fifo", DMA_FIFO)] { + let mut seen = std::collections::BTreeSet::new(); + for s in table { + assert!(!token_name(s.selector).is_empty(), "{what} table: {:#x} is not a token", s.selector); + assert!(seen.insert(s.selector), "{what} table: {:#x} ({}) twice", s.selector, token_name(s.selector)); + } + } + // The textport grammar is its own: its numbers are not the tokens'. + assert!(TEXTPORT.iter().all(|s| s.selector < 0x10)); + } + + /// The four polyline tokens carry 1, 7, 2 and 7 words (the X server's + /// sender counts). A one-word token that framed as two would pull the next + /// command's token word in as a point and desync everything after it. + #[test] + fn the_polyline_tokens_frame_one_seven_two_and_seven_words() { + let argc = |sel| KERNEL.iter().find(|s| s.selector == sel).map(|s| s.argc); + assert_eq!(argc(DRAW_CONNECTED_POLYLINE), Some(1)); + assert_eq!(argc(DRAW_CONNECTED_POLYLINE_BURST_OF_6), Some(7)); + assert_eq!(argc(DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS), Some(2)); + assert_eq!(argc(DRAW_CONNECTED_POLYLINE_RELATIVE_POINTS_BURST_OF_6), Some(7)); + } +} diff --git a/src/dev/kona/tests/bringup.rs b/src/dev/kona/tests/bringup.rs new file mode 100644 index 000000000..3fb9862c9 --- /dev/null +++ b/src/dev/kona/tests/bringup.rs @@ -0,0 +1,1135 @@ +//! Firmware-load and post-load bring-up, command framing, the pages the pipe +//! answers on, completion reasons, contexts and the bracket register. Ported +//! from tests/qtest/sn0-origin2000-kona-bringup-test.c; the bracket, context +//! and swap cases are new. + +use super::*; + +const PORT_ADDR: u64 = 0x8003e8; +const PORT_DATA: u64 = 0x8003f0; +const PORT_WRITE_NEXT: u64 = 0x8003f8; +const LOADER_3B0: u64 = 0x8003b0; +const LOADER_328: u64 = 0x800328; +const ACTIVITY: u64 = 0x8003a8; +const RESET_STROBE: u64 = 0x800300; +const RESET_ARG: u64 = 0x800220; +const CREDIT_CONFIG: u64 = 0x800210; +const INT_ENABLE: u64 = 0x800230; +const INT_CAUSE: u64 = 0x800238; +const MAILBOX: u64 = 0x800240; +const RETRACE_TAG: u64 = 0x800248; +const EEPROM_ENABLE: u64 = 0x800308; +const INT_DEST_UPPER: u64 = 0x34; +const INT_DEST_LOWER: u64 = 0x3c; +const INT_VECTORS: u64 = 0x5c; +const LOW_WINDOW: u64 = 0x7c; +const MAPRAM: u64 = 0x100000; + +const ICU_CONTROL: u32 = 0x200050; +const ARM_HIGH_BASE: u32 = 0x400_0000; +const ARM_HIGH_END: u32 = 0x414_e630; +const EEPROM_CHECKED_ADDR: u32 = 0x87ff1c; +const CAUSE_MAILBOX: u32 = 0x10; +const CAUSE_GE: u32 = 0x20; + +/// The interrupt programming the ported tests use: vectors +/// 0xf (xg), 0x11 (geometry engine) and 0x10 (general), and node 0's +/// interrupt-modify register at widget 9 as the destination. +const KERNEL_INT_VECTORS: u32 = 0x3e2210; +const KERNEL_INT_DEST_UPPER: u32 = 0x98000; +const KERNEL_INT_DEST_LOWER: u32 = 0x180_0090; +const GE_VECTOR: u8 = 0x11; +const GENERAL_VECTOR: u8 = 0x10; + +const REASON_PAGE: u64 = 0x30_0000; +const OTHER_PAGE: u64 = 0x31_0000; +const RETURN_PAGE: u64 = 0x32_0000; +const CONFIG_OFFSET: u64 = 8; +const CONFIG_SIZE: usize = 0x778; + +impl Fixture { + fn port_select(&self, addr: u32) { + self.w(PORT_ADDR, addr); + } + + fn port_load(&self, addr: u32, words: &[u32]) { + self.port_select(addr); + for w in words { + self.w(PORT_WRITE_NEXT, *w); + } + } + + fn port_peek(&self, addr: u32) -> u32 { + self.port_select(addr); + self.r(PORT_DATA) + } + + fn port_poke(&self, addr: u32, v: u32) { + self.port_select(addr); + self.w(PORT_DATA, v); + } + + /// The interrupt programming the ported tests use. + fn program_interrupts(&self) { + self.w(INT_DEST_UPPER, KERNEL_INT_DEST_UPPER); + self.w(INT_DEST_LOWER, KERNEL_INT_DEST_LOWER); + self.w(INT_VECTORS, KERNEL_INT_VECTORS); + } + + /// The PROM's sync probe: its mailbox reply shows the command page is + /// between commands. + fn assert_probe_answers(&self) { + self.cmd(&[1, 9, 0]); + assert_eq!(self.r(INT_CAUSE) & CAUSE_MAILBOX, CAUSE_MAILBOX); + assert_eq!(self.r(MAILBOX), 0x1234abcd); + self.w(MAILBOX, 0); + self.w(INT_CAUSE, CAUSE_MAILBOX); + } + + fn assert_no_reply(&self) { + assert_eq!(self.r(INT_CAUSE) & CAUSE_MAILBOX, 0, "no mailbox reply is queued"); + assert_eq!(self.r(MAILBOX), 0); + } + + fn kick(&self, v: u32) { + self.w(KICK, v); + } + + fn reply_to(&self, addr: u64) { + self.cmd(&[0x1ec, 3, 0x1000_0000 | (addr >> 12) as u32, (addr & 0xfff) as u32, 0x4000]); + } +} + +/// Three advancing writes from 0x4000000, then a select of +/// 0x4000004, read the second word. Discriminates an advancing port from a +/// fixed one and a word stride from a byte stride. +#[test] +fn port_stride() { + let f = Fixture::new(); + f.port_load(ARM_HIGH_BASE, &[0x1111_1111, 0x2222_2222, 0x3333_3333]); + assert_eq!(f.port_peek(ARM_HIGH_BASE + 4), 0x2222_2222); + assert_eq!(f.port_peek(ARM_HIGH_BASE), 0x1111_1111); + assert_eq!(f.port_peek(ARM_HIGH_BASE + 8), 0x3333_3333); +} + +/// Two reads of +0x8003f0 without a new +/// select return the same word. This checks the model's chosen behaviour, not +/// a hardware fact: whether the data port advances is unknown. +#[test] +fn port_read_does_not_advance() { + let f = Fixture::new(); + f.port_load(ARM_HIGH_BASE, &[0xaaaa_0001, 0xaaaa_0002]); + f.port_select(ARM_HIGH_BASE); + assert_eq!(f.r(PORT_DATA), 0xaaaa_0001); + assert_eq!(f.r(PORT_DATA), 0xaaaa_0001); +} + +/// The established EEPROM word keeps +/// answering through the same port once the loader's ranges are backed, and a +/// load does not disturb it. +#[test] +fn eeprom_word_still_answers() { + let f = Fixture::new(); + assert_eq!(f.port_peek(EEPROM_CHECKED_ADDR), 2); + f.port_load(ARM_HIGH_BASE, &[1, 2, 3]); + f.port_load(0, &[4, 5]); + assert_eq!(f.port_peek(EEPROM_CHECKED_ADDR), 2); +} + +/// Reads outside the backed ranges +/// return zero, and a word written just past either range is dropped rather +/// than aliased into it. +#[test] +fn port_unbacked_addresses_read_zero() { + let f = Fixture::new(); + f.port_load(0, &[0xc0de_0001; 11]); + f.port_poke(ARM_HIGH_BASE, 0xc0de_0002); + // The low range holds eleven words, 0x0 to 0x2b. + assert_eq!(f.port_peek(0x28), 0xc0de_0001); + assert_eq!(f.port_peek(0x2c), 0); + f.port_poke(0x2c, 0xdead_0001); + assert_eq!(f.port_peek(0x2c), 0); + assert_eq!(f.port_peek(0), 0xc0de_0001, "not aliased into the start"); + // The high range ends at 0x414e62f. + f.port_poke(ARM_HIGH_END - 4, 0xc0de_0003); + assert_eq!(f.port_peek(ARM_HIGH_END - 4), 0xc0de_0003); + f.port_poke(ARM_HIGH_END, 0xdead_0002); + assert_eq!(f.port_peek(ARM_HIGH_END), 0); + assert_eq!(f.port_peek(ARM_HIGH_BASE), 0xc0de_0002, "not aliased into the start"); + assert_eq!(f.port_peek(0x1234_5678), 0); + assert_eq!(f.port_peek(ARM_HIGH_BASE + 2), 0, "unaligned"); +} + +/// The loader's shape in small: +/// 0x200050 <- 1, two sections, 0x200050 <- 0 records each section's address +/// and word count, once, at the 1-to-0 edge. +#[test] +fn load_record_closes_at_arm_release() { + let f = Fixture::new(); + f.port_poke(ICU_CONTROL, 1); + f.port_load(ARM_HIGH_BASE, &[1, 2, 3]); + f.port_load(0, &[4, 5]); + assert!(f.kona.load_record().is_empty(), "the record closes at the edge"); + f.port_poke(ICU_CONTROL, 0); + assert_eq!( + f.kona.load_record(), + [LoadSection { addr: ARM_HIGH_BASE, words: 3 }, LoadSection { addr: 0, words: 2 }] + ); + assert_eq!(f.port_peek(ICU_CONTROL), 0); + f.port_poke(ICU_CONTROL, 1); + assert_eq!(f.port_peek(ICU_CONTROL), 1); + assert_eq!(f.port_peek(ARM_HIGH_BASE + 8), 3, "the loaded words read back through the port"); +} + +/// A larger load keeps counting but records only the first 32 sections. +#[test] +fn a_load_of_many_sections_records_the_first_thirty_two() { + let f = Fixture::new(); + f.port_poke(ICU_CONTROL, 1); + for i in 0..40u32 { + f.port_load(ARM_HIGH_BASE + 16 * i, &[i, i]); + } + f.port_poke(ICU_CONTROL, 0); + let rec = f.kona.load_record(); + assert_eq!(rec.len(), 32); + assert_eq!(rec[31], LoadSection { addr: ARM_HIGH_BASE + 16 * 31, words: 2 }); +} + +/// The two registers the loader writes +/// after the load are stored, not reported as unimplemented. A store of 1 to +/// the unstall register releases the firmware: the back end is told, with the +/// video combination stored in the flash if its marker and count are good. +#[test] +fn loader_registers_are_stored() { + let f = Fixture::hooked(); + f.w(LOADER_3B0, 5); + assert_eq!(f.r(LOADER_3B0), 5); + f.w(LOADER_328, 1); + assert_eq!(f.r(LOADER_328), 1); + assert_eq!(f.take_jobs(), ["firmware []"]); +} + +/// The ARM's start runs the combination the install tool stored. +#[test] +fn firmware_start_hands_over_the_stored_combination() { + let f = Fixture::hooked(); + f.reply_to(RETURN_PAGE); + f.cmd(&[0x1ec, 1, 1, 2, 0x16, 0x17]); + f.take_jobs(); + f.w(LOADER_328, 1); + assert_eq!(f.take_jobs(), ["firmware [16, 17]"]); + // A combination whose marker the erase zeroed is not run. + f.cmd(&[0x1ec, 6]); + f.take_jobs(); + f.w(LOADER_328, 1); + assert_eq!(f.take_jobs(), ["firmware []"]); +} + +/// Reset clears load state. +#[test] +fn reset_clears_load_state() { + let f = Fixture::new(); + f.port_poke(ICU_CONTROL, 1); + f.port_load(ARM_HIGH_BASE, &[7, 8]); + f.port_poke(ICU_CONTROL, 0); + f.w(LOADER_3B0, 5); + f.w(LOADER_328, 1); + f.kona.power_on(); + assert_eq!(f.r(PORT_ADDR), 0); + assert_eq!(f.port_peek(ARM_HIGH_BASE), 0); + assert!(f.kona.load_record().is_empty()); + assert_eq!(f.r(LOADER_3B0), 0); + assert_eq!(f.r(LOADER_328), 0); +} + +/// A graphics process's 0x1f2 0 0x9000 0x4000 +/// and 0x1e9 0, and a context activation's 0x1fd whose arguments look like the +/// textport's sync probe, are each consumed whole. A model that dropped the +/// tokens as unexpected words would read 0x1fd's arguments 1 9 0 as a sync +/// probe and queue a mailbox reply; this one queues nothing, and the textport +/// stays in step afterwards. An unknown token takes the unexpected-word path. +#[test] +fn kernel_command_framing() { + let f = Fixture::new(); + f.cmd(&[0x1f2, 0, 0x9000, 0x4000]); + f.cmd(&[0x1e9, 0]); + f.cmd(&[0x1fd, 1, 9, 0]); + f.assert_no_reply(); + f.assert_probe_answers(); + // An unknown token is dropped as a stray word, and its arguments arrive as + // fresh words: the probe in them answers. + f.cmd(&[0x7ff, 1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); +} + +/// The kernel's pipe-memory allocator's first +/// allocation for the minimal GLX program sends 0x1e4 2 9 0 1 inside the +/// +0xc00108 bracket, then 0x172 0 with none; a free sends 0x1e4 with offset +/// and size 0. Pool pressure adds copy-down, 0x1e1 and four words. Each is +/// consumed whole: the write-gather flush 0x1ff sent straight after the first +/// 0x172 0 frames as its own packet, and 0x1e1's words here are shaped like the +/// sync probe, which a model that dropped the token would answer on the +/// mailbox. +#[test] +fn kernel_pool_packets() { + let f = Fixture::hooked(); + f.kick(0x30000); + f.cmd(&[0x1e4, 2, 9, 0, 1]); + f.kick(0); + f.cmd(&[0x172, 0, 0x1ff, 0xdead_beef]); + f.cmd(&[0x1e4, 2, 9, 0, 0]); + f.cmd(&[0x1e1, 1, 9, 0, 0]); + f.assert_no_reply(); + assert_eq!( + f.jobs(), + [ + "Kernel 0x1e4 [2, 9, 0, 1] packed=0x0 handled=true", + "Kernel 0x172 [0] packed=0x0 handled=false", + "Kernel 0x1ff [deadbeef] packed=0x0 handled=false", + "Kernel 0x1e4 [2, 9, 0, 0] packed=0x0 handled=true", + "Kernel 0x1e1 [1, 9, 0, 0] packed=0x0 handled=false", + ] + ); + f.assert_probe_answers(); +} + +/// 0x1f1 registers the reason page: physical +/// when bit 28 of the page word is set (the driver's pipe-start form), +/// otherwise through the map RAM entry for the device address's 16 KiB page. An +/// unmapped device address registers nothing and says so. +#[test] +fn reason_page_registration() { + let f = Fixture::new(); + f.cmd(&[0x1f1, 0x1000_0000 | (REASON_PAGE >> 12) as u32, 0, 0x4000]); + f.cmd(&[0x1f8, 0x103]); + assert_eq!(f.host.word(REASON_PAGE), 0x103); + + // Mapped: device address 0x34000 (page 0xd) through entry 0xd. + let g = Fixture::new(); + g.map(0xd, OTHER_PAGE); + g.cmd(&[0x1f1, 0x34, 0x9000, 0x4000]); + g.cmd(&[0x1f8, 0x103]); + assert_eq!(g.host.word(OTHER_PAGE), 0x103); + + // Unmapped: nothing registered, nothing posted. + let u = Fixture::new(); + u.program_interrupts(); + u.cmd(&[0x1f1, 0x34, 0x9000, 0x4000]); + u.cmd(&[0x1f8, 0x103]); + assert_eq!(u.host.word(OTHER_PAGE), 0); + assert_eq!(u.r(INT_CAUSE) & CAUSE_GE, 0); + assert!(u.host.sent().is_empty()); +} + +/// With the pipe-start page registered, the driver's +/// rendering-node teardown's 0x1f8 0x103 writes 0x103 big-endian to word 0 of +/// the page, sets cause bit 0x20 and sends exactly one interrupt, on the +/// geometry-engine vector 0x11; the handler's write of 0x20 clears the bit and +/// sends nothing more. +#[test] +fn reason_posting() { + let f = Fixture::new(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + f.cmd(&[0x1f8, 0x103]); + assert_eq!(f.host.word(REASON_PAGE), 0x103); + assert_eq!(f.r(INT_CAUSE) & CAUSE_GE, CAUSE_GE); + assert_eq!(f.host.sent(), [Sent { vector: GE_VECTOR, level: true }]); + f.w(INT_CAUSE, CAUSE_GE); + assert_eq!(f.r(INT_CAUSE) & CAUSE_GE, 0); + assert_eq!(f.host.sent().len(), 1); +} + +/// 0x1f8 0x103 before any 0x1f1 writes no +/// guest memory, sets no cause bit and raises nothing. +#[test] +fn no_reason_page_no_post() { + let f = Fixture::hooked(); + f.program_interrupts(); + f.cmd(&[0x1f8, 0x103]); + assert_eq!(f.r(INT_CAUSE), 0); + assert!(f.host.sent().is_empty()); + assert_eq!(f.host.word(REASON_PAGE), 0); +} + +/// A reason with no destination programmed sets the cause bit and sends no +/// packet. +#[test] +fn a_reason_with_no_destination_sends_no_packet() { + let f = Fixture::new(); + f.reason_page(REASON_PAGE); + f.cmd(&[0x1f8, 0x103]); + assert_eq!(f.host.word(REASON_PAGE), 0x103); + assert_eq!(f.r(INT_CAUSE) & CAUSE_GE, CAUSE_GE); + assert!(f.host.sent().is_empty()); +} + +/// The X server process's 0x1f2 0 0x9000 +/// 0x4000 and 0x1e9 0 between the driver's pipe start and the teardown leave +/// the stream in step: 0x1e9 answers on 0x1f2's page and 0x1f8 0x103 still +/// posts to the page 0x1f1 registered. +#[test] +fn process_commands_then_post() { + let f = Fixture::new(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + f.return_page(RETURN_PAGE); + f.cmd(&[0x1e9, 0]); + assert_eq!(f.host.word(RETURN_PAGE), 1); + f.cmd(&[0x1f8, 0x103]); + assert_eq!(f.host.word(REASON_PAGE), 0x103); +} + +/// The deactivate's completion: reason 0x101 +/// in word 0 of the reason page, the geometry-engine cause bit and one +/// interrupt on its vector, which the driver's geometry-engine interrupt takes +/// as the wakeup for the sleeping swap. The save area carries the two words the +/// software reads back. The handler's write of 0x20 clears the bit. +#[test] +fn context_deactivate_posts() { + let f = Fixture::hooked(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + // The kernel's first activation: a save area, then the return page. + f.cmd(&[0x1fd, 0x1000_0000 | (OTHER_PAGE >> 12) as u32, 0x40, 0x10000]); + f.return_page(RETURN_PAGE); + f.take_jobs(); + f.cmd(&[0x1fc, 3]); + assert_eq!(f.host.word(REASON_PAGE), 0x101); + assert_eq!(f.host.word(OTHER_PAGE + 0x40), 0x4b4f_4e41); + assert_eq!(f.host.word(OTHER_PAGE + 0x40 + 8), 0x172); + assert_eq!(f.r(INT_CAUSE) & CAUSE_GE, CAUSE_GE); + assert_eq!(f.host.sent().len(), 1); + assert_eq!(f.host.sent()[0].vector, GE_VECTOR); + f.w(INT_CAUSE, CAUSE_GE); + assert_eq!(f.r(INT_CAUSE) & CAUSE_GE, 0); + assert_eq!(f.jobs(), ["context Deactivated { slot: 0 }", "Kernel 0x1fc [3] packed=0x0 handled=true"]); +} + +/// The word after 0x1fc is its +/// argument: an index of 1 must not open a textport packet, so a 0x1f8 0x103 +/// sent next still posts 0x103 and the textport's own sync probe still answers +/// afterwards. +#[test] +fn context_deactivate_argument_is_framed() { + let f = Fixture::new(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + f.cmd(&[0x1fc, 1]); + assert_eq!(f.host.word(REASON_PAGE), 0x101); + // The driver's handler reads the reason and writes it back with a marker. + f.host.put(REASON_PAGE, &(0x101u32 | 0xbad0_0000).to_be_bytes()); + f.w(INT_CAUSE, CAUSE_GE); + f.cmd(&[0x1f8, 0x103]); + assert_eq!(f.host.word(REASON_PAGE), 0x103); + f.assert_probe_answers(); +} + +/// A second reason waits behind one the driver has not yet read: it posts when +/// the page shows the driver's rewrite, looked for when the cause register is +/// accessed. +#[test] +fn a_second_reason_waits_for_the_first_to_be_read() { + let f = Fixture::new(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + f.cmd(&[0x1f8, 0x103]); + f.cmd(&[0x1dd, 0x777]); + assert_eq!(f.host.word(REASON_PAGE), 0x103, "the second waits"); + assert_eq!(f.host.sent().len(), 1); + // The driver's handler reads word 0 and writes it back with a marker. + f.host.put(REASON_PAGE, &(0x103u32 | 0xbad0_0000).to_be_bytes()); + f.w(INT_CAUSE, CAUSE_GE); + assert_eq!(f.host.word(REASON_PAGE), 0x777, "the cause access finds the rewrite and posts it"); + assert_eq!(f.host.sent().len(), 2); + assert_eq!(f.r(INT_CAUSE) & CAUSE_GE, CAUSE_GE); +} + +/// The driver's pipe start kills +/// the pipe if +0x800238 shows bit 0x100, so the cause must come out of reset +/// without it; and reset forgets the reason page and the interrupt registers, +/// so a post afterwards writes nothing. +#[test] +fn reset_clears_cause_and_reason_page() { + let f = Fixture::hooked(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + f.cmd(&[0x1f8, 0x103]); + f.host.put(REASON_PAGE, &[0; 4]); + f.kona.power_on(); + assert_eq!(f.r(INT_CAUSE), 0); + f.cmd(&[0x1f8, 0x103]); + assert_eq!(f.host.word(REASON_PAGE), 0); + assert_eq!(f.host.sent().len(), 1, "nothing new went out"); +} + +/// The driver's periodic timeout check's +/// read-then-write-4 round sees bit 0x4 only when a command-port, DMA FIFO or +/// load word arrived since the previous round. The fatal-error path's write of +/// 8 neither sets nor clears it. +#[test] +fn activity_latch() { + let f = Fixture::new(); + assert_eq!(f.r(ACTIVITY), 0); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(ACTIVITY), 4); + f.w(ACTIVITY, 8); + assert_eq!(f.r(ACTIVITY), 4, "a write of 8 does not clear it"); + f.w(ACTIVITY, 4); + assert_eq!(f.r(ACTIVITY), 0); + f.w(0x80007c, 0xdead); + assert_eq!(f.r(ACTIVITY), 4); + f.w(ACTIVITY, 4); + f.port_select(0); + f.w(PORT_WRITE_NEXT, 1); + assert_eq!(f.r(ACTIVITY), 4); +} + +/// The kernel's once-per-video-interrupt write of bit 0 is counted and changes nothing: it does not +/// touch the busy latch, and it is not an unimplemented write. +#[test] +fn the_video_acknowledgement_is_counted() { + let f = Fixture::new(); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(ACTIVITY), 4); + f.w(ACTIVITY, 1); + f.w(ACTIVITY, 1); + assert_eq!(f.r(ACTIVITY), 4, "the latch is not cleared by bit 0"); + assert_eq!(f.kona.video_acks(), 2); +} + +/// The reset register (bit 0 +/// host-interface reset, bit 1 ARM start) stores the firmware's 3, 2 and the +/// driver's 1, 0, and the pipe still takes commands after. +#[test] +fn named_registers_are_observable() { + let f = Fixture::new(); + for v in [3, 2, 1, 0] { + f.w(RESET_STROBE, v); + assert_eq!(f.r(RESET_STROBE), v); + } + f.assert_probe_answers(); +} + +/// The kernel's host-interface reset pulse, 1, read, 0, +/// read at +0x800300, resets what the kernel rewrites after it: the global +/// enable at +0x800220, the flow configuration at +0x800210, the group word at +/// +0x7c and every map RAM entry. Registers the kernel does not rewrite keep +/// their values: the interrupt vectors and destination, the loader's words and +/// the EEPROM enable. +#[test] +fn reset_pulse() { + let f = Fixture::new(); + f.program_interrupts(); + f.w(RESET_ARG, 0x8000); + f.w(CREDIT_CONFIG, 0x700); + f.w(LOW_WINDOW, 1); + f.w(INT_ENABLE, 0x100); + f.w(EEPROM_ENABLE, 1); + f.w(LOADER_3B0, 5); + f.w(MAPRAM + 8, 0x2000_1234); + f.w(RESET_STROBE, 1); + assert_eq!(f.r(RESET_STROBE), 1); + f.w(RESET_STROBE, 0); + assert_eq!(f.r(RESET_STROBE), 0); + for reg in [RESET_ARG, CREDIT_CONFIG, LOW_WINDOW, MAPRAM + 8] { + assert_eq!(f.r(reg), 0, "register {reg:#x} was reset"); + } + assert_eq!(f.r(INT_VECTORS), KERNEL_INT_VECTORS); + assert_eq!(f.r(INT_DEST_UPPER), KERNEL_INT_DEST_UPPER); + assert_eq!(f.r(INT_DEST_LOWER), KERNEL_INT_DEST_LOWER); + assert_eq!(f.r(EEPROM_ENABLE), 1); + assert_eq!(f.r(LOADER_3B0), 5); +} + +/// The kernel driver's widget-side reset, in its +/// order: widget control written and read back, the interrupt destination, the +/// vectors, the error register +0x6c read, cleared through +0x2c and written +/// back, bit 17 of +0x24 set by read-modify-write, +0x74 rewritten, then 0xfff +/// to the cause register and 0 to +0x800390 and +0x800398. The model raises no +/// widget errors, so +0x6c reads 0 both times. The registers the model gives +/// no meaning accept the accesses, and the pipe still answers afterwards. +#[test] +fn widget_reset_accesses() { + let f = Fixture::new(); + f.w(0x24, 0x3000); + assert_eq!(f.r(0x24), 0x3000); + f.program_interrupts(); + assert_eq!(f.r(0x6c), 0); + f.w(0x2c, 0); + f.w(0x6c, 0); + assert_eq!(f.r(0x6c), 0); + let c = f.r(0x24); + f.w(0x24, c | 1 << 17); + assert_eq!(f.r(0x24), 0x3000 | 1 << 17); + let w = f.r(0x74); + f.w(0x74, (w & 0x0fff_0000) | 0x3f40); + f.w(INT_CAUSE, 0xfff); + f.w(0x800390, 0); + f.w(0x800398, 0); + assert_eq!(f.r(0x800390), 0); + f.assert_probe_answers(); +} + +fn config_map(f: &Fixture, host_page: u64) { + f.w(MAPRAM, 0x2000_0000 | (host_page >> 12) as u32); +} + +/// The 0x1e9 answer placement: map RAM entry 0 +/// maps device address 0 to a host page, then a graphics process's 0x1f2 0 0x9000 0x4000 +/// and 0x1e9 0. The host page holds the configuration block from byte 8 and a +/// non-zero word 0; word 1 and the bytes past the block are untouched. +#[test] +fn config_answer_placement() { + let f = Fixture::new(); + let page = 0x40_0000u64; + f.host.put(page, &[0xaa; 8]); + f.host.put(page + 8 + CONFIG_SIZE as u64, &[0x77; 4]); + config_map(&f, page); + f.cmd(&[0x1f2, 0, 0x9000, 0x4000, 0x1e9, 0]); + assert_eq!(f.host.word(page), 1); + assert_eq!(f.host.word(page + 4), 0xaaaa_aaaa, "word 1 is untouched"); + assert_eq!(f.host.word(page + CONFIG_OFFSET), 1, "the board set: IR2"); + assert_eq!(f.host.word(page + 8 + CONFIG_SIZE as u64), 0x7777_7777, "bytes past the block are untouched"); +} + +/// The pool descriptors in the 0x1e9 answer, +/// against the limits the kernel's allocator puts on them: each granule is +/// non-zero and divides 0x400; each largest alignment is a multiple of the +/// granule and at most 255 granules; class 2, which the library's first +/// make-current allocates with key 9, takes that key and is not shared; class 1, +/// the display lists, is shared; and class 3's granule times capacity is the +/// 64 MiB of texture memory the library reads back. +#[test] +fn config_pool_descriptors() { + let f = Fixture::new(); + f.return_page(RETURN_PAGE); + f.cmd(&[0x1e9, 0]); + let block = |class: u64, word: u64| f.host.word(RETURN_PAGE + CONFIG_OFFSET + 0x90 + 0x1c * class + 4 * word); + for class in 0..4 { + let (max_key, max_align, shared, granule) = (block(class, 0), block(class, 1), block(class, 3), block(class, 5)); + assert!(granule != 0 && 0x400 % granule == 0, "class {class}: granule"); + assert!(max_align % granule == 0 && max_align / granule < 256, "class {class}: alignment"); + if class == 2 { + assert!(max_key >= 9 && shared == 0, "class 2 takes key 9 and is not shared"); + } + if class == 1 { + assert_eq!(shared, 1); + } + } + assert_eq!(block(3, 5) as u64 * block(3, 6) as u64, 64 << 20); + assert_eq!(block(2, 2), 4, "one geometry engine copy each"); +} + +/// The driver's context-swap physical form of +/// 0x1f2: bit 28 of the page word set, the page frame below it and the byte +/// offset in the second word. A second 0x1f2 replaces the first: the model keeps +/// one return page. +#[test] +fn return_page_physical() { + let f = Fixture::new(); + f.cmd(&[0x1f2, 0x1000_0000 | (RETURN_PAGE >> 12) as u32, 0x20, 0x4000]); + f.cmd(&[0x128, 1, 2, 3]); + assert_eq!(f.host.word(RETURN_PAGE + 0x20), 1); + assert_eq!(f.host.word(RETURN_PAGE + 0x20 + 8), 1); + f.cmd(&[0x1f2, 0x1000_0000 | (OTHER_PAGE >> 12) as u32, 0, 0x4000]); + f.cmd(&[0x128, 4, 5, 6]); + assert_eq!(f.host.word(OTHER_PAGE), 1); + assert_eq!(f.host.word(OTHER_PAGE + 8), 4); + assert_eq!(f.host.word(RETURN_PAGE + 0x20 + 8), 1, "the first page keeps its answer"); +} + +/// 0x1e9 before any 0x1f2 writes no guest +/// memory and raises nothing; so does 0x1f2 naming an unmapped device page, +/// which registers nothing. +#[test] +fn no_return_page_no_post() { + let f = Fixture::hooked(); + f.cmd(&[0x1e9, 0]); + assert!(f.host.bytes(RETURN_PAGE, 16).iter().all(|b| *b == 0)); + assert_eq!(f.r(INT_CAUSE), 0); + f.cmd(&[0x1f2, 0x34, 0x9000, 0x4000]); + f.cmd(&[0x1e9, 0]); + assert!(f.host.bytes(RETURN_PAGE, 16).iter().all(|b| *b == 0)); + f.assert_probe_answers(); +} + +/// Reset clears return page. +#[test] +fn reset_clears_return_page() { + let f = Fixture::new(); + f.return_page(RETURN_PAGE); + f.kona.power_on(); + f.cmd(&[0x1e9, 0]); + assert_eq!(f.host.word(RETURN_PAGE), 0); +} + +/// X initialise, sent as plain words: after a physical +/// 0x1f2, 0x128 W H D writes W, H and D to words 2 to 4 of that page, then 1 to +/// word 0, and leaves words 1 and 5 alone. With no return page it writes +/// nothing. +#[test] +fn x_initialise_echo() { + let f = Fixture::new(); + f.cmd(&[0x128, 1280, 1024, 5]); + assert_eq!(f.host.word(RETURN_PAGE), 0); + f.host.put(RETURN_PAGE + 4, &[0xee; 4]); + f.host.put(RETURN_PAGE + 20, &[0xee; 4]); + f.return_page(RETURN_PAGE); + f.cmd(&[0x128, 1280, 1024, 5]); + assert_eq!(f.host.word(RETURN_PAGE), 1); + assert_eq!(f.host.word(RETURN_PAGE + 4), 0xeeee_eeee); + assert_eq!( + [f.host.word(RETURN_PAGE + 8), f.host.word(RETURN_PAGE + 12), f.host.word(RETURN_PAGE + 16)], + [1280, 1024, 5] + ); + assert_eq!(f.host.word(RETURN_PAGE + 20), 0xeeee_eeee); +} + +/// Display packets keep command framing and +/// leave replies untouched: the DID map, display mode and flush frames reach +/// the back end and the page stays in step. +#[test] +fn display_tokens_are_consumed() { + let f = Fixture::hooked(); + f.cmd(&[0x132, 5, 0x1234, 0x1e6, 1, 9, 0, 0x133, 1, 0x1ff, 0xdead_beef]); + f.assert_no_reply(); + assert_eq!( + f.jobs(), + [ + "Kernel 0x132 [5, 1234] packed=0x0 handled=false", + "Kernel 0x1e6 [1, 9, 0] packed=0x0 handled=false", + "Kernel 0x133 [1] packed=0x0 handled=false", + "Kernel 0x1ff [deadbeef] packed=0x0 handled=false", + ] + ); + f.assert_probe_answers(); +} + +/// W at +0xc1250c, then H and D at +0xc1000c, frame as +/// 0x128 W H D, observed through the X initialise echo. The write-gather +/// flush at +0xc13fec frames as 0x1ff 0xdeadbeef. +#[test] +fn address_decode() { + let f = Fixture::hooked(); + f.return_page(RETURN_PAGE); + f.w(0xc1250c, 1280); + f.w(0xc1000c, 1024); + f.w(0xc1000c, 5); + assert_eq!(f.host.word(RETURN_PAGE), 1); + assert_eq!(f.host.word(RETURN_PAGE + 8), 1280); + assert_eq!(f.host.word(RETURN_PAGE + 12), 1024); + f.take_jobs(); + f.w(0xc13fec, 0xdead_beef); + assert_eq!(f.take_jobs(), ["Kernel 0x1ff [deadbeef] packed=0x0 handled=false"]); +} + +/// The data slot carries plain words: a graphics process's 0x1f2 +/// and 0x1e9 sent as words at +0xc1000c frame exactly as they do at +0xc10000. +#[test] +fn data_slot_tokens() { + let f = Fixture::new(); + for w in [0x1f2, 0x1000_0000 | (RETURN_PAGE >> 12) as u32, 0, 0x4000, 0x1e9, 0] { + f.w(0xc1000c, w); + } + assert_eq!(f.host.word(RETURN_PAGE), 1); + assert_eq!(f.host.word(RETURN_PAGE + 8), 1, "the configuration block follows"); + f.assert_probe_answers(); +} + +/// A token the kernel table has no word count for. +fn unknown_token() -> u32 { + (0x100..0x200).find(|t| super::super::frame::tables().kernel.get(*t).is_none()).unwrap() +} + +/// Stores the model cannot place are dropped without +/// disturbing later framing: a slot naming a token with no word count, a store +/// elsewhere in a slot or in the first slot, and a slot store while a command +/// is still open, whose partial command is dropped rather than completed by +/// the next words. +#[test] +fn slot_stores_are_dropped_without_disturbing_framing() { + let f = Fixture::hooked(); + f.return_page(RETURN_PAGE); + f.take_jobs(); + f.w(0xc1000c + 0x20 * unknown_token() as u64, 0); + f.w(0xc10000 + 0x20 * 0x128 + 0x10, 0x1234); + f.w(0xc10000 + 0x10, 0x1234); + assert!(f.jobs().is_empty()); + // A slot store while a command is open drops the partial command. + f.cmd(&[0x1f1, 0x1000_0000]); + f.w(0xc1000c + 0x20 * 0x128, 640); + f.cmd(&[480, 7]); + assert_eq!(f.host.word(RETURN_PAGE + 8), 640); + assert_eq!(f.host.word(RETURN_PAGE + 12), 480); + f.assert_probe_answers(); +} + +/// The variable colour-map length: 0x12e with +/// start 0 and count 3, then three entries, frames as one command and the next +/// word opens a new one. The entries are shaped like 0x1e9 and its argument, so +/// a model that took fewer words would answer a 0x1e9. Sent both as plain words +/// and with the start index in the token's slot. +#[test] +fn color_map_has_a_variable_length() { + let f = Fixture::hooked(); + f.return_page(RETURN_PAGE); + f.take_jobs(); + f.cmd(&[0x12e, 0, 3, 0x1e9, 0, 0x1f2]); + assert_eq!(f.host.word(RETURN_PAGE), 0, "no configuration was answered"); + assert_eq!( + f.take_jobs(), + ["Kernel 0x12e [0, 3] packed=0x0 handled=true", "data Kernel 0x12e [0, 3] [1e9, 0, 1f2]"] + ); + f.assert_probe_answers(); + // The start index in the token's slot. + f.w(0xc1000c + 0x20 * 0x12e, 0); + f.cmd(&[3, 0x1e9, 0, 0x1f2]); + assert_eq!(f.host.word(RETURN_PAGE), 0); + assert_eq!(f.take_jobs().len(), 2); + // A count of zero takes no entries. + f.cmd(&[0x12e, 7, 0]); + f.assert_probe_answers(); + assert_eq!(f.take_jobs(), ["Kernel 0x12e [7, 0] packed=0x0 handled=true"]); +} + +/// The gamma and cursor loads take their words the same way. +#[test] +fn gamma_and_cursor_loads_take_their_words() { + let f = Fixture::hooked(); + f.take_jobs(); + f.cmd(&[0x12f, 0x10, 2, 0x1e9, 0]); + let mut cursor = vec![0x130, 0]; + cursor.extend((0..260).map(|i| 0x1000 + i)); + f.cmd(&cursor); + f.cmd(&[0x131, 0, 1, 2, 3, 4]); + let jobs = f.take_jobs(); + assert_eq!(jobs.len(), 6); + assert!(jobs[1].starts_with("data Kernel 0x12f [10, 2] [1e9, 0]"), "{}", jobs[1]); + assert!(jobs[3].starts_with("data Kernel 0x130 [0] [1000, 1001,"), "{}", jobs[3]); + assert!(jobs[5].starts_with("data Kernel 0x131 [0] [1, 2, 3, 4]"), "{}", jobs[5]); + f.assert_probe_answers(); +} + +/// Words the model cannot frame are dropped and +/// the page stays in step: an unknown token's argument words, arriving as fresh +/// words, and an unknown token's slot store. +#[test] +fn unframable_words_are_dropped() { + let f = Fixture::new(); + f.cmd(&[0x7fe, 0x7fd, 0x7fc]); + f.w(0xc1000c + 0x20 * unknown_token() as u64, 5); + f.assert_no_reply(); + f.assert_probe_answers(); +} + +/// A video command: its number, the data words its table gives and a +/// checksum of the number and the data. A number past the table has no length +/// and frames by searching for the checksum. A bad checksum drops the command. +#[test] +fn video_commands_frame_by_table_or_by_checksum() { + let f = Fixture::hooked(); + f.take_jobs(); + // 0x08 SetupFrameBuffer: three data words. + f.cmd(&[0x1a6, 0x08, 1280, 1024, 3, 0x08 + 1280 + 1024 + 3]); + // 0x00 Test: one data word. + f.cmd(&[0x1a6, 0x00, 11, 11]); + // 0x5d is past the table: the checksum is searched for, and the command is consumed with nothing posted. + f.cmd(&[0x1a6, 0x5d, 11, 22, 33, 0x5d + 66]); + // A wrong checksum drops the command. + f.cmd(&[0x1a6, 0x08, 1, 2, 3, 99]); + assert_eq!( + f.take_jobs(), + [ + "Kernel 0x1a6 [8] packed=0x0 handled=true", + "video 0x8 [500, 400, 3]", + "Kernel 0x1a6 [0] packed=0x0 handled=true", + "video 0x0 [b]", + "Kernel 0x1a6 [5d] packed=0x0 handled=true", + "Kernel 0x1a6 [8] packed=0x0 handled=true", + ] + ); + f.assert_probe_answers(); +} + +/// , for the command stream: a partial +/// command and an open bitmap blit do not survive a reset. +#[test] +fn reset_clears_the_command_stream() { + let f = Fixture::new(); + f.cmd(&[0x1f1, 0x1000_0000]); + f.cmd(&[1, 0xa, 4, 2]); + f.kona.power_on(); + f.assert_probe_answers(); +} + +/// New: the bracket register. The privileged bit makes the following words a +/// kernel command's. +#[test] +fn the_privileged_bracket_frames_a_kernel_packet() { + let f = Fixture::new(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + f.kick(0x30000); + f.cmd(&[0x1f8, 0x103]); + f.kick(0); + assert_eq!(f.host.word(REASON_PAGE), 0x103); + f.assert_probe_answers(); +} + +/// New: a forced store suspends the user command the kernel framer had open, +/// and the command resumes where it stopped once the kernel's packet is done. +/// A GC load is thirteen words; the privileged sender interrupts it. +#[test] +fn a_forced_bracket_suspends_a_user_command_and_resumes_it() { + let f = Fixture::hooked(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + // A command suspended with no context current has no owner to resume it, + // so the user is a context. + f.cmd(&[0x1fd, 0x1000_0000 | (0x50_0000 >> 12), 0, 0x10000]); + f.take_jobs(); + // The user opens 0x129 and sends five of its thirteen words. + f.cmd(&[0x129, 1, 2, 3, 4, 5]); + // The kernel breaks in with its own packet. + f.kick(0x30000); + f.cmd(&[0x1f8, 0x103]); + f.kick(0); + assert_eq!(f.host.word(REASON_PAGE), 0x103); + // The kernel's packet and the window release it names; not the user's. + assert_eq!(f.take_jobs(), ["context WindowReleased { slot: 0 }", "Kernel 0x1f8 [103] packed=0x0 handled=true"]); + // The user's remaining words complete it. + f.cmd(&[6, 7, 8, 9, 10, 11, 12, 13]); + let jobs = f.take_jobs(); + assert_eq!(jobs.len(), 1, "{jobs:?}"); + assert!(jobs[0].starts_with("Kernel 0x129 [1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d]"), "{jobs:?}"); +} + +/// New: a forced store with a kernel command still open ends it short, and the +/// next packet frames afresh. +#[test] +fn a_forced_bracket_abandons_an_open_kernel_command() { + let f = Fixture::new(); + f.kick(0x30000); + f.cmd(&[0x1f1, 0x1000_0000]); + f.kick(0x30000); + f.cmd(&[0x1dd, 5]); + f.kick(0); + f.assert_no_reply(); + f.assert_probe_answers(); +} + +/// New: contexts. A first activation starts a context with no return page until +/// the 0x1f2 that follows it; a restore brings back the page its context was +/// given; a restore naming a save area nobody set up leaves the pipe with no +/// return page. +#[test] +fn contexts_keep_their_return_pages() { + let f = Fixture::hooked(); + let (save_a, save_b) = (0x50_0000u64, 0x51_0000u64); + let phys = |a: u64| 0x1000_0000 | (a >> 12) as u32; + let (page_a, page_b) = (0x52_0000u64, 0x53_0000u64); + f.cmd(&[0x1fd, phys(save_a), 0, 0x10000]); + f.return_page(page_a); + f.cmd(&[0x1fd, phys(save_b), 0, 0x10000]); + f.return_page(page_b); + f.cmd(&[0x128, 1, 2, 3]); + assert_eq!(f.host.word(page_b + 8), 1, "context B answers on its page"); + f.cmd(&[0x1fc, 1]); + // Restore A: its page comes back. + f.cmd(&[0x1f9, 0x4b4f_4e41, phys(save_a), 0, 0xeded_eded]); + f.cmd(&[0x128, 7, 8, 9]); + assert_eq!(f.host.word(page_a + 8), 7); + assert_eq!(f.host.word(page_b + 8), 1); + // A save area nobody set up. + f.cmd(&[0x1f9, 0x4b4f_4e41, phys(0x54_0000), 0, 0xeded_eded]); + f.cmd(&[0x128, 4, 5, 6]); + assert_eq!(f.host.word(page_a + 8), 7, "an answer goes nowhere rather than to another context"); + let jobs = f.jobs(); + assert!(jobs.iter().any(|j| j == "context Activated { slot: 1, fresh: true }")); + assert!(jobs.iter().any(|j| j == "context Activated { slot: 0, fresh: false }")); +} + +/// New: the pipe stops taking commands when the model's context limit is +/// reached, and says why. +#[test] +fn the_context_limit_stops_the_pipe() { + let f = Fixture::new(); + for i in 0..65u32 { + f.cmd(&[0x1fd, 0x1000_0000 | ((0x60_0000 >> 12) + i), 0, 0x10000]); + } + assert!(f.kona.failure().unwrap().contains("context limit")); + f.cmd(&[1, 9, 0]); + f.assert_no_reply(); + f.kona.power_on(); + assert!(f.kona.failure().is_none()); + f.assert_probe_answers(); +} + +/// New: the swap-out and swap-in the kernel sends under pool pressure complete +/// at once: a swap-out zeroes the buffer and posts 0x102 on the reason page, a +/// swap-in posts it on the mailbox. +#[test] +fn swaps_complete_at_once() { + let f = Fixture::new(); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + let buf = 0x70_0000u64; + f.map(0x1c0, buf); + f.host.put(buf, &[0xaa; 64]); + let dev = 0x1c0u64 << 14; + // class, offset, size 2, granule 0x10, copies 1: 0x20 bytes at the device + // address, then its reason. + let page_words = [(dev >> 12) as u32, 0x9000 | (dev & 0xfff) as u32]; + f.cmd(&[0x1e3, 0, 0, 2, 0x10, 1, page_words[0], page_words[1], 0x102]); + assert_eq!(f.host.word(REASON_PAGE), 0x102); + assert_eq!(f.host.bytes(buf, 0x20), vec![0; 0x20]); + assert_eq!(f.host.bytes(buf + 0x20, 4), vec![0xaa; 4], "only the buffer is zeroed"); + // Swap-in with the tagged offset, then the reason. + f.cmd(&[0x1e2, 0, 0, 2, 0x10, 1, page_words[0], page_words[1], 0x102]); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, CAUSE_MAILBOX); + assert_eq!(f.r(MAILBOX), 0x102); +} + +/// New: a swap-in ended by the wrong word is not completed and the word is +/// taken afresh. +#[test] +fn a_swap_in_ended_by_the_wrong_word_is_dropped() { + let f = Fixture::new(); + f.cmd(&[0x1e2, 0, 0, 1, 1, 1, 0x34, 0x9000, 0x1234]); + f.assert_no_reply(); + f.assert_probe_answers(); +} + +/// New: a suspend answers 0x104 with no saved words on the reason page. +#[test] +fn a_suspend_answers_with_no_words() { + let f = Fixture::new(); + f.reason_page(REASON_PAGE); + f.cmd(&[0x1df, 0x104]); + assert_eq!(f.host.word(REASON_PAGE), 0x104); + assert_eq!(f.host.word(REASON_PAGE + 4), 0); +} + +/// New: a retrace request completes through the back end: the tag register +/// reads the tag, the video cause rises and, once the driver enabled it, one +/// interrupt goes out on the general vector, not another until the cause is +/// acknowledged. +#[test] +fn a_retrace_completes_the_tag_and_raises_video_once() { + let f = Fixture::hooked(); + f.program_interrupts(); + f.w(INT_ENABLE, 0xf7c); + f.cmd(&[0x1e7, 0x77]); + assert_eq!(f.r(RETRACE_TAG), 0x77); + assert_eq!(f.r(INT_CAUSE) & 8, 8); + assert_eq!(f.host.sent().len(), 1); + assert_eq!(f.host.sent()[0].vector, GENERAL_VECTOR); + f.cmd(&[0x1e7, 0x78]); + assert_eq!(f.r(RETRACE_TAG), 0x78); + assert_eq!(f.host.sent().len(), 1, "not again before the acknowledge"); + f.w(INT_CAUSE, 8); + f.cmd(&[0x1e7, 0x79]); + assert_eq!(f.host.sent().len(), 2); +} + +/// New: the GL grammar. Once a context is active, a word whose low 16 bits are +/// a GL selector opens that command with the top half as its packed data; a +/// sync word followed by a textport command stays the textport's; a 1 followed +/// by a word that is no textport command begins a buffer configuration. +#[test] +fn the_gl_grammar_follows_the_first_activation() { + let f = Fixture::hooked(); + f.cmd(&[0x1fd, 0x1000_0000 | (0x50_0000 >> 12), 0, 0x10000]); + f.take_jobs(); + // glColor3ub: the opening word carries the red and green bytes. + f.cmd(&[0xff80_0066, 0x4000_0000]); + // The textport's own command still answers. + f.assert_probe_answers(); + // A buffer configuration (selector 1, ten words) begins after a held 1. + f.cmd(&[1, 0x1ff, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + let jobs = f.take_jobs(); + assert_eq!(jobs.len(), 2, "{jobs:?}"); + assert_eq!(jobs[0], "Gl 0x66 [40000000] packed=0xff800000 handled=false"); + assert!(jobs[1].starts_with("Gl 0x1 [1ff, 2, 3,"), "{}", jobs[1]); +} + +/// The pixel and bitmap tokens take 8 (small bitmap), 9 (large bitmap), 8 (pixel draw) and 2 (pixel-mode suspend) +/// words behind the opener: the table's own text and the recorded bitmaps say so (its word-count column said 7, 7, +/// 7 and 1). A command of that many words is one job and the next command after it is not misread. +#[test] +fn the_pixel_and_bitmap_tokens_take_their_framed_words() { + let f = Fixture::hooked(); + f.cmd(&[0x1fd, 0x1000_0000 | (0x50_0000 >> 12), 0, 0x10000]); + f.take_jobs(); + for (selector, words) in [(0x005u32, 8usize), (0x006, 9), (0x03a, 8), (0x111, 2)] { + let mut cmd = vec![selector]; + cmd.extend((1..=words as u32).map(|w| 0x100 + w)); + cmd.extend([0x57, 0]); + f.cmd(&cmd); + let jobs = f.take_jobs(); + let args = jobs[0].split('[').nth(1).and_then(|a| a.split(']').next()).unwrap_or("").split(", ").count(); + assert!(jobs[0].starts_with(&format!("Gl {selector:#x} [")) && args == words, "{selector:#x} framed {args} words, not {words}: {jobs:?}"); + assert_eq!(jobs.len(), 2, "{selector:#x}: the next command follows it: {jobs:?}"); + assert!(jobs[1].starts_with("Gl 0x57 [0]"), "{jobs:?}"); + } +} + +/// New: before the first activation only the whole GL selectors the early +/// grammar knows open a GL command; the PROM's textport words are not misread. +#[test] +fn before_a_context_the_page_speaks_the_textport() { + let f = Fixture::hooked(); + f.cmd(&[1, 0x3, 5, 1, 2, 3]); + f.cmd(&[0x57, 0]); + assert_eq!( + f.take_jobs(), + ["Textport 0x3 [5, 1, 2, 3] packed=0x0 handled=false", "Gl 0x57 [0] packed=0x0 handled=false"] + ); +} + +/// New: a GL texture table is a slot word and 33 transport words; a back end +/// can also ask for data words behind a command it is handed. +#[test] +fn data_words_follow_a_command_that_has_them() { + let rec = Recorder { data: vec![(0x138, 2)], ..Default::default() }; + let f = Fixture::with(KonaConfig::default(), rec); + f.cmd(&[0x138, 10, 20, 0x1e9, 0]); + assert_eq!( + f.take_jobs(), + ["Kernel 0x138 [a, 14] packed=0x0 handled=false", "data Kernel 0x138 [a, 14] [1e9, 0]"] + ); + f.cmd(&[0x1fd, 0x1000_0000 | (0x50_0000 >> 12), 0, 0x10000]); + f.take_jobs(); + let mut table = vec![0x17e, 3]; + table.extend(1000..1033); + f.cmd(&table); + f.assert_probe_answers(); + let jobs = f.take_jobs(); + assert_eq!(jobs.len(), 2); + assert!(jobs[1].starts_with("data Gl 0x17e [3] [3e8, 3e9,"), "{}", jobs[1]); +} + +/// A word that opens no command reaches the back end as a dropped word, in +/// stream order with the commands around it, so a token whose length the +/// tables lack can be seen. +#[test] +fn a_word_with_no_command_is_a_dropped_job() { + let f = Fixture::hooked(); + f.cmd(&[1, 2, 7, 0x500, 0x500]); + assert_eq!(f.jobs().last().unwrap(), "dropped 0x500"); + assert_eq!(f.jobs().iter().filter(|j| j.starts_with("dropped")).count(), 2); +} + +/// New: a back end can post a completion of four words, which the reason queue +/// now carries, and can ask whether a reason page is registered first. +#[test] +fn a_back_end_posts_a_reason_of_four_words() { + let rec = Recorder { reasons: vec![(0x138, 0x1f6, vec![0x100, 0x77, 0x5, 0x6])], ..Default::default() }; + let f = Fixture::with(KonaConfig::default(), rec); + f.program_interrupts(); + f.reason_page(REASON_PAGE); + f.cmd(&[0x138, 3, 4]); + assert_eq!([0, 1, 2, 3].map(|i| f.host.word(REASON_PAGE + 4 * i)), [0x100, 0x77, 0x5, 0x6]); + assert_eq!(f.r(INT_CAUSE) & CAUSE_GE, CAUSE_GE); +} diff --git a/src/dev/kona/tests/config.rs b/src/dev/kona/tests/config.rs new file mode 100644 index 000000000..4618800f5 --- /dev/null +++ b/src/dev/kona/tests/config.rs @@ -0,0 +1,95 @@ +//! What the pipe presents: the 0x1e9 configuration answer, built from +//! the ported values for the deskside pipe, and the words host software reads its +//! class from. Ported from tests/qtest/sn0-origin2000-kona-config-test.c for +//! the InfiniteReality2 of the Onyx2 deskside (GE14-4, RM7, TM7, eight-channel +//! DG5). + +use super::ident::tests::{parse_hex, synthetic_serial}; +use super::*; + +const INDIRECT_SELECT: u64 = 0x8003c0; +const INDIRECT_DATA: u64 = 0x8003d0; +const RETURN_PAGE: u64 = 0x32_0000; +const CONFIG_OFFSET: u64 = 8; +const CONFIG_SIZE: usize = 0x778; + +/// A graphics process's 0x1f2, then 0x1e9 0; returns the posted block. +pub(super) fn read_config(f: &Fixture) -> Vec { + f.cmd(&[0x1f2, 0x1000_0000 | (RETURN_PAGE >> 12) as u32, (RETURN_PAGE & 0xfff) as u32, 0x4000]); + f.cmd(&[0x1e9, 0]); + assert_eq!(f.host.word(RETURN_PAGE), 1); + f.host.bytes(RETURN_PAGE + CONFIG_OFFSET, CONFIG_SIZE) +} + +fn assert_config(f: &Fixture, want: &str) { + let block = read_config(f); + let want = parse_hex(want); + if let Some(i) = (0..CONFIG_SIZE).step_by(4).find(|&i| block[i..i + 4] != want[i..i + 4]) { + panic!("first difference at block offset {i:#x}: {:x?} against {:x?}", &block[i..i + 4], &want[i..i + 4]); + } + assert_eq!(block, want); +} + +/// The deskside pipe, byte for byte against the fixed block: +/// two RM7 boards, each with its TM7, and the eight-channel DG. +#[test] +fn ir2_ge14_4_two_raster_managers() { + assert_config(&Fixture::new(), include_str!("data/config-ir2-2rm.hex")); +} + +/// A deskside pipe with a single raster manager: one RM's chip words and its +/// TM record, and the raster-manager counts follow. +#[test] +fn ir2_ge14_4_one_raster_manager() { + let f = Fixture::with(KonaConfig::ir2(1).unwrap(), Recorder::default()); + assert_config(&f, include_str!("data/config-ir2-1rm.hex")); +} + +/// The pipe slot gives each board's records their own serial instances, sixteen +/// to a slot, so two pipes never share a serial. +#[test] +fn a_pipe_slot_moves_the_serial_instances() { + let f = Fixture::with(KonaConfig::ir2(2).unwrap().in_slot(1).unwrap(), Recorder::default()); + let block = read_config(&f); + // The first RM record's one-wire number is the slot's instance 2. + let serial = synthetic_serial(16 + 2); + for i in 0..6 { + assert_eq!(block[0x3e8 + 4 + i], (serial >> (40 - 8 * i)) as u8); + } + assert!(KonaConfig::ir2(2).unwrap().in_slot(4).is_err()); +} + +/// The two words the driver's attach classes the pipe by, +/// read through the indirect port: after selecting 0x8800 the low two bits are +/// the product's (0 for an InfiniteReality2), after 0xb00 bit 10 would make it +/// a Reality (clear). +#[test] +fn class_words() { + let f = Fixture::new(); + f.w(INDIRECT_SELECT, 0x8800); + assert_eq!(f.r(INDIRECT_DATA) & 3, 0); + f.w(INDIRECT_SELECT, 0xb00); + assert_eq!(f.r(INDIRECT_DATA) & 0x400, 0); + assert_eq!(f.r(INDIRECT_SELECT), 0xb00); +} + +/// configuration no source grounds is refused with its reason. A +/// deskside pipe holds one or two raster managers. +#[test] +fn configurations_without_a_source_are_refused() { + for n in [0, 3, 4] { + let e = KonaConfig::ir2(n).unwrap_err(); + assert!(e.contains("one or two raster managers"), "{e}"); + } +} + +/// A store to the indirect data register under a selector the front end does +/// not answer is the display generator's: the back end is handed it. +#[test] +fn other_indirect_selectors_reach_the_back_end() { + let f = Fixture::hooked(); + f.w(INDIRECT_SELECT, 0x101); + f.w(INDIRECT_DATA, 0x1234); + assert_eq!(f.take_jobs(), ["indirect 0x101 0x1234"]); + assert_eq!(f.r(INDIRECT_DATA), 0, "its reads are not modelled"); +} diff --git a/src/dev/kona/tests/data/config-ir2-1rm.hex b/src/dev/kona/tests/data/config-ir2-1rm.hex new file mode 100644 index 000000000..051569a73 --- /dev/null +++ b/src/dev/kona/tests/data/config-ir2-1rm.hex @@ -0,0 +1,120 @@ +00 00 00 01 00 00 00 04 00 00 00 01 00 00 00 01 +00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 00 +00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 04 00 00 00 01 00 00 00 0f +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 05 00 00 00 04 00 00 00 00 00 +00 00 00 00 00 00 40 00 00 00 00 01 00 00 00 00 +00 00 00 01 00 00 04 00 00 00 01 00 00 00 00 00 +00 00 40 00 00 00 00 01 00 00 00 01 00 00 00 01 +00 00 04 00 00 00 3c 00 00 00 00 0a 00 00 40 00 +00 00 00 04 00 00 00 00 00 00 00 01 00 00 04 00 +00 00 04 00 00 00 00 00 00 00 40 00 00 00 00 01 +00 00 00 00 00 00 00 01 00 00 04 00 00 01 00 00 +80 16 0a 04 00 00 00 00 14 83 20 57 24 73 10 57 +14 23 10 57 03 02 0c 06 15 03 20 57 15 43 10 57 +24 63 10 57 24 63 10 57 24 63 10 57 24 63 10 57 +24 d3 10 57 24 d3 10 57 24 d3 10 57 24 d3 10 57 +24 d3 10 57 24 d3 10 57 24 d3 10 57 24 d3 10 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 51 bf 10 02 05 53 20 57 15 83 80 57 +15 83 80 57 15 83 80 57 15 83 80 57 15 63 10 57 +15 63 10 57 15 63 10 57 15 63 10 57 15 63 10 57 +15 63 10 57 15 63 10 57 15 63 10 57 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 51 45 4d 55 +47 02 0b 01 01 20 20 20 20 45 4d 55 47 30 32 20 +20 20 20 20 20 20 30 33 30 2d 31 30 35 34 2d 30 +30 31 00 00 20 20 20 20 20 20 20 20 20 47 ff ff +ff ff ff ff 20 20 20 20 20 20 20 20 20 20 20 52 +4d 37 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 51 45 4d 55 47 06 0b 01 +01 20 20 20 20 45 4d 55 47 30 36 20 20 20 20 20 +20 20 30 33 30 2d 31 30 35 33 2d 30 30 31 00 00 +20 20 20 20 20 20 20 20 20 46 ff ff ff ff ff ff +20 20 20 20 20 20 20 20 20 20 20 54 4d 37 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 51 45 4d 55 47 0a 0b 01 01 20 20 20 +20 45 4d 55 47 31 30 20 20 20 20 20 20 20 30 33 +30 2d 31 30 38 37 2d 30 30 31 00 00 20 20 20 20 +20 20 20 20 20 20 ff ff ff ff ff ff 20 20 20 20 +20 20 20 20 20 20 20 20 20 20 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 diff --git a/src/dev/kona/tests/data/config-ir2-2rm.hex b/src/dev/kona/tests/data/config-ir2-2rm.hex new file mode 100644 index 000000000..96ffbbb9a --- /dev/null +++ b/src/dev/kona/tests/data/config-ir2-2rm.hex @@ -0,0 +1,120 @@ +00 00 00 01 00 00 00 04 00 00 00 02 00 00 00 01 +00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 00 +00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 +00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 04 00 00 00 02 00 00 00 0f +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 05 00 00 00 04 00 00 00 00 00 +00 00 00 00 00 00 40 00 00 00 00 01 00 00 00 00 +00 00 00 01 00 00 04 00 00 00 01 00 00 00 00 00 +00 00 40 00 00 00 00 01 00 00 00 01 00 00 00 01 +00 00 04 00 00 00 3c 00 00 00 00 0a 00 00 40 00 +00 00 00 04 00 00 00 00 00 00 00 01 00 00 04 00 +00 00 04 00 00 00 00 00 00 00 40 00 00 00 00 01 +00 00 00 00 00 00 00 01 00 00 04 00 00 01 00 00 +80 16 0a 04 00 00 00 00 14 83 20 57 24 73 10 57 +14 23 10 57 03 02 0c 06 15 03 20 57 15 43 10 57 +24 63 10 57 24 63 10 57 24 63 10 57 24 63 10 57 +24 d3 10 57 24 d3 10 57 24 d3 10 57 24 d3 10 57 +24 d3 10 57 24 d3 10 57 24 d3 10 57 24 d3 10 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +03 02 0c 06 15 03 20 57 15 43 10 57 24 63 10 57 +24 63 10 57 24 63 10 57 24 63 10 57 24 d3 10 57 +24 d3 10 57 24 d3 10 57 24 d3 10 57 24 d3 10 57 +24 d3 10 57 24 d3 10 57 24 d3 10 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 54 93 80 57 +54 93 80 57 54 93 80 57 54 93 80 57 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 51 bf 10 02 05 53 20 57 15 83 80 57 +15 83 80 57 15 83 80 57 15 83 80 57 15 63 10 57 +15 63 10 57 15 63 10 57 15 63 10 57 15 63 10 57 +15 63 10 57 15 63 10 57 15 63 10 57 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 51 45 4d 55 +47 02 0b 01 01 20 20 20 20 45 4d 55 47 30 32 20 +20 20 20 20 20 20 30 33 30 2d 31 30 35 34 2d 30 +30 31 00 00 20 20 20 20 20 20 20 20 20 47 ff ff +ff ff ff ff 20 20 20 20 20 20 20 20 20 20 20 52 +4d 37 00 00 00 00 00 00 51 45 4d 55 47 03 0b 01 +01 20 20 20 20 45 4d 55 47 30 33 20 20 20 20 20 +20 20 30 33 30 2d 31 30 35 34 2d 30 30 31 00 00 +20 20 20 20 20 20 20 20 20 47 ff ff ff ff ff ff +20 20 20 20 20 20 20 20 20 20 20 52 4d 37 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 51 45 4d 55 47 06 0b 01 +01 20 20 20 20 45 4d 55 47 30 36 20 20 20 20 20 +20 20 30 33 30 2d 31 30 35 33 2d 30 30 31 00 00 +20 20 20 20 20 20 20 20 20 46 ff ff ff ff ff ff +20 20 20 20 20 20 20 20 20 20 20 54 4d 37 00 00 +00 00 00 00 51 45 4d 55 47 07 0b 01 01 20 20 20 +20 45 4d 55 47 30 37 20 20 20 20 20 20 20 30 33 +30 2d 31 30 35 33 2d 30 30 31 00 00 20 20 20 20 +20 20 20 20 20 46 ff ff ff ff ff ff 20 20 20 20 +20 20 20 20 20 20 20 54 4d 37 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 51 45 4d 55 47 0a 0b 01 01 20 20 20 +20 45 4d 55 47 31 30 20 20 20 20 20 20 20 30 33 +30 2d 31 30 38 37 2d 30 30 31 00 00 20 20 20 20 +20 20 20 20 20 20 ff ff ff ff ff ff 20 20 20 20 +20 20 20 20 20 20 20 20 20 20 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 diff --git a/src/dev/kona/tests/dma.rs b/src/dev/kona/tests/dma.rs new file mode 100644 index 000000000..478ac4837 --- /dev/null +++ b/src/dev/kona/tests/dma.rs @@ -0,0 +1,595 @@ +//! DMA packets, display-list memory and list invocation. Ported from +//! tests/qtest/sn0-origin2000-kona-dma-test.c: the cases about the list path +//! and the packet's framing. Those that place pixels in a plane or read them +//! back are the back end's. +//! +//! A display list holds the words immediate mode writes to the command page, +//! so a list of textport commands shows what it fed the decoder by the jobs +//! the back end is handed. + +use super::*; + +const DMA_FIFO_PUSH: u64 = 0x80007c; +const DMA_FIFO_KICK: u64 = 0xc00108; +const INT_CAUSE: u64 = 0x800238; +const MAILBOX: u64 = 0x800240; +const INT_DEST_UPPER: u64 = 0x34; +const INT_DEST_LOWER: u64 = 0x3c; +const INT_VECTORS: u64 = 0x5c; +const CAUSE_MAILBOX: u32 = 0x10; + +const LIST_ROW: u32 = 4096; +const LIST_BYTES: u32 = 15 << 20; +const SRC: u64 = 0x40_0000; +const POOL_DEVICE: u64 = 0x34000; +const POOL_HOST: u64 = 0x80_0000; +const POOL_LEN: u32 = 0x8000; + +/// A colour map load, a colour select and a point: the chain every glyph +/// stroke uses, thirteen words. +const LIST: [u32; 13] = [1, 3, 5, 0x10, 0x20, 0x30, 1, 2, 5, 1, 6, 7, 8]; +const LIST_JOBS: [&str; 3] = [ + "Textport 0x3 [5, 10, 20, 30] packed=0x0 handled=false", + "Textport 0x2 [5] packed=0x0 handled=false", + "Textport 0x6 [7, 8] packed=0x0 handled=false", +]; + +impl Fixture { + fn dma(&self, words: &[u32]) { + for w in words { + self.w(DMA_FIFO_PUSH, *w); + } + } + + fn list_write(&self, dest: u32, bytes: u32) { + self.dma(&[0x1c8, dest, bytes]); + } + + fn packet(&self, io: u64, total: u32, per_line: u32, gap: u32, done: u32) { + self.dma(&[0x1eb, 0x1000_0000 | (io >> 12) as u32, 0x9000 | (io & 0xfff) as u32, total, per_line, gap, done]); + } + + fn list_word(&self, at: usize) -> u32 { + self.kona.shared.pipe.lock().core.list_word(at) + } + + /// Map `len` bytes of the device address space at `device` onto `host`. + fn map_range(&self, device: u64, host: u64, len: u64) { + for off in (0..len).step_by(0x4000) { + self.map((device + off) >> 14, host + off); + } + } + + /// The pool the kernel pinned, and its registration. + fn pool(&self) { + self.map_range(POOL_DEVICE, POOL_HOST, POOL_LEN as u64); + self.cmd(&[0x1ed, (POOL_DEVICE >> 12) as u32, 0x9000 | (POOL_DEVICE & 0xfff) as u32, POOL_LEN]); + } + + /// The decoder's jobs: the textport commands a list fed it. + fn textport_jobs(&self) -> Vec { + self.jobs().into_iter().filter(|j| j.starts_with("Textport")).collect() + } + + /// Put `words` into list memory at byte `at`, as a list write would. + fn put_list(&self, at: u32, words: &[u32]) { + self.host.put_words(SRC, words); + self.list_write(at, 4 * words.len() as u32); + self.packet(SRC, 4 * words.len() as u32, 4 * words.len() as u32, 0, 0); + } +} + +/// The driver's display-list copy: a +/// 0x1c8 record naming pipe-memory address 0xffc and 4,104 bytes, then a packet +/// of that many bytes in one line. The bytes land in the list memory from that +/// address, and the packet completes once with the mailbox word its word 6 asks +/// for. +#[test] +fn list_write_lands_in_list_memory() { + let f = Fixture::hooked(); + let words: Vec = (0..1026).map(|i| 0x16d0_0000 + i).collect(); + f.host.put_words(SRC, &words); + f.list_write(0xffc, 4104); + f.packet(SRC, 4104, 4104, 0, 0x100); + assert_eq!(f.list_word(0xff8), 0); + assert_eq!(f.list_word(0xffc), words[0]); + assert_eq!(f.list_word(LIST_ROW as usize), words[1]); + assert_eq!(f.list_word(2 * LIST_ROW as usize - 4), words[1024]); + assert_eq!(f.list_word(2 * LIST_ROW as usize), words[1025]); + assert_eq!(f.list_word(2 * LIST_ROW as usize + 4), 0); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, CAUSE_MAILBOX); + assert_eq!(f.r(MAILBOX), 0x100); + // The record is spent: the next packet is not a list write. + f.take_jobs(); + f.packet(SRC, 16, 16, 0, 0); + assert_eq!(f.take_jobs(), ["dma-in 0x400000 1 0x10 0x10 physical"]); + assert_eq!(f.list_word(0xffc), words[0]); +} + +/// Lists sit past the first MiB of the +/// display-list class: a list write there lands, as anywhere inside the class's +/// 15 MiB. +#[test] +fn list_write_past_the_first_mib() { + let f = Fixture::new(); + f.host.put_words(SRC, &[0xa, 0xb, 0xc, 0xd]); + f.list_write(0x243000, 16); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.list_word(0x243000), 0xa); + assert_eq!(f.list_word(0x24300c), 0xd); + assert_eq!(f.r(MAILBOX), 0x100); + // The last word of the class. + f.list_write(LIST_BYTES - 4, 4); + f.packet(SRC, 4, 4, 0, 0); + assert_eq!(f.list_word(LIST_BYTES as usize - 4), 0xa); +} + +/// A list write that would run past the +/// list memory's end, or whose record does not match the packet's byte count, +/// is refused, completes nothing, and writes nothing. +#[test] +fn list_write_outside_the_memory_is_refused() { + let f = Fixture::new(); + f.host.put_words(SRC, &[1, 2, 3, 4]); + f.list_write(LIST_BYTES - 8, 16); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.list_word(LIST_BYTES as usize - 8), 0); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + // A record that does not match the packet's byte count. + f.list_write(0x100, 8); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.list_word(0x100), 0); + // Two lines for a list write. + f.list_write(0x100, 16); + f.packet(SRC, 16, 8, 0, 0x100); + assert_eq!(f.list_word(0x100), 0); + // An address that is not whole words. + f.list_write(0x102, 16); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.list_word(0x100), 0); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); +} + +/// Words that are not a selector the port +/// frames are dropped one at a time, and the kick's values frame nothing, so a +/// packet after them completes normally. +#[test] +fn stray_words_resynchronize() { + let f = Fixture::new(); + f.host.put_words(SRC, &[7, 8, 9, 10]); + f.dma(&[0xdead_beef, 0x1234, 0]); + f.w(DMA_FIFO_KICK, 0x30000); + f.w(DMA_FIFO_KICK, 0x10000); + f.list_write(0x40, 16); + f.w(DMA_FIFO_KICK, 0); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.list_word(0x40), 7); + assert_eq!(f.r(MAILBOX), 0x100); +} + +/// A packet pushed in two +/// parts moves nothing and completes nothing until its last word arrives. +#[test] +fn a_partial_packet_holds_state_across_pushes() { + let f = Fixture::new(); + f.host.put_words(SRC, &[7, 8, 9, 10]); + f.list_write(0x40, 16); + f.dma(&[0x1eb, 0x1000_0000 | (SRC >> 12) as u32, 0x9000, 16]); + assert_eq!(f.list_word(0x40), 0); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + assert_eq!(f.r(0x800208), 4, "four words held"); + f.dma(&[16, 0, 0x100]); + assert_eq!(f.list_word(0x40), 7); + assert_eq!(f.r(MAILBOX), 0x100); + assert_eq!(f.r(0x800208), 0); +} + +/// Software is expected to send the +/// kernel's packets on the +0xc10000 command page, so the record and the +/// packet reach the engine there as plain words too, token first, with the +/// kick literals around the packet. +#[test] +fn the_command_page_reaches_the_engine() { + let f = Fixture::new(); + f.host.put_words(SRC, &[7, 8, 9, 10]); + f.w(KICK, 0x30000); + f.cmd(&[0x1c8, 0x40, 16]); + f.w(KICK, 0x10000); + f.cmd(&[0x1eb, 0x1000_0000 | (SRC >> 12) as u32, 0x9000, 16, 16, 0, 0x100]); + f.w(KICK, 0); + assert_eq!(f.list_word(0x40), 7); + assert_eq!(f.list_word(0x4c), 10); + assert_eq!(f.r(MAILBOX), 0x100); +} + +/// Word 6 decides the completion: +/// 0x100 posts the mailbox word, 0 posts nothing. Both packets still transfer. +#[test] +fn completion_follows_word_6() { + let f = Fixture::new(); + f.host.put_words(SRC, &[1, 2, 3, 4]); + f.list_write(0x80, 16); + f.packet(SRC, 16, 16, 0, 0); + assert_eq!(f.list_word(0x80), 1); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + f.list_write(0x90, 16); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.list_word(0x90), 1); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, CAUSE_MAILBOX); + // Any other value asks for no completion. + f.w(MAILBOX, 0); + f.w(INT_CAUSE, CAUSE_MAILBOX); + f.list_write(0xa0, 16); + f.packet(SRC, 16, 16, 0, 0x55); + assert_eq!(f.list_word(0xa0), 1); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); +} + +/// A packet whose word 6 is 0x100 completes +/// as the driver's interrupt arming expects: the mailbox holds 0x100 under +/// cause bit 0x10, one interrupt goes out on the general vector, and the +/// read-clear-acknowledge sequence clears both. A packet whose word 6 is 0 +/// posts nothing, and neither does a display list's completion. +#[test] +fn a_completion_posts_the_mailbox_and_one_interrupt() { + let f = Fixture::new(); + f.w(INT_DEST_UPPER, 0x98000); + f.w(INT_DEST_LOWER, 0x180_0090); + f.w(INT_VECTORS, 0x3e2210); + f.host.put_words(SRC, &[1, 2, 3, 4]); + f.list_write(0x80, 16); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.host.sent().len(), 1); + assert_eq!(f.host.sent()[0].vector, 0x10); + assert_eq!(f.r(MAILBOX), 0x100); + f.w(MAILBOX, 0); + f.w(INT_CAUSE, CAUSE_MAILBOX); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + assert_eq!(f.r(MAILBOX), 0); + // A list's completion posts nothing. + f.put_list(0, &LIST); + f.cmd(&[0x16d, 0, 13]); + assert_eq!(f.host.sent().len(), 1); +} + +/// Word 2 must carry the fixed 0x9000 tag: a +/// packet whose word 2 carries another is refused, moves nothing and completes +/// nothing, withholding the word the sender waits for. The port stays in step +/// for the next packet. +#[test] +fn a_tag_mismatch_is_refused() { + let f = Fixture::new(); + f.host.put_words(SRC, &[1, 2, 3, 4]); + f.list_write(0x80, 16); + f.dma(&[0x1eb, 0x1000_0000 | (SRC >> 12) as u32, 0x8000, 16, 16, 0, 0x100]); + assert_eq!(f.list_word(0x80), 0); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + // The record was spent by the refused packet; send both again. + f.list_write(0x80, 16); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.list_word(0x80), 1); + assert_eq!(f.r(MAILBOX), 0x100); +} + +/// A descriptor that is not whole lines of +/// whole words is refused: a zero per-line count, a per-line count that is not +/// whole words, and a total that is not a whole number of lines. +#[test] +fn shape_constraints_are_refused() { + let f = Fixture::hooked(); + f.host.put_words(SRC, &[1, 2, 3, 4]); + for (total, per_line) in [(16, 0), (16, 6), (16, 12), (0, 4)] { + f.list_write(0x80, total); + f.packet(SRC, total, per_line, 0, 0x100); + assert_eq!(f.list_word(0x80), 0, "total {total:#x} in lines of {per_line:#x}"); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + } + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.r(MAILBOX), 0x100, "the port is in step"); +} + +/// A host DMA that cannot complete is reported the +/// one way the model can without inventing a cause bit: its completion is +/// withheld, so the driver's timeout fires. No cause bit and no mailbox word +/// appear, and the pipe still completes a good packet. +#[test] +fn a_failed_host_dma_withholds_its_completion() { + let f = Fixture::hooked(); + f.list_write(0x80, 16); + f.packet(0x7fff_f000, 16, 16, 0, 0x100); + assert_eq!(f.list_word(0x80), 0); + assert_eq!(f.r(INT_CAUSE), 0); + assert_eq!(f.r(MAILBOX), 0); + f.host.put_words(SRC, &[1, 2, 3, 4]); + f.list_write(0x80, 16); + f.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(f.r(MAILBOX), 0x100); +} + +/// A packet with no list record is the back end's: lines and gaps reach it as +/// the descriptor names them, and it completes as the packet asks unless +/// nothing was waiting for it. +#[test] +fn a_packet_without_a_record_goes_to_the_back_end() { + let f = Fixture::hooked(); + f.packet(SRC + 4, 16, 8, 8, 0x100); + assert_eq!(f.take_jobs(), ["dma-in 0x400004 2 0x8 0x10 physical"]); + assert_eq!(f.r(MAILBOX), 0x100); + + let rec = Recorder { unclaimed: true, ..Default::default() }; + let g = Fixture::with(KonaConfig::default(), rec); + g.packet(SRC, 16, 16, 0, 0x100); + assert_eq!(g.r(INT_CAUSE) & CAUSE_MAILBOX, 0, "nothing was waiting: no completion"); +} + +/// 0x1ed registers a pool; 0x171 invokes +/// one list from it as two ranges, the first six words at pool offset 0x100 and +/// the other seven at 0x800. Every word reaches the command page's decoder in +/// order, across the break between ranges. +#[test] +fn a_list_from_the_pool_reaches_the_decoder() { + let f = Fixture::hooked(); + f.pool(); + f.host.put_words(POOL_HOST + 0x100, &LIST[..6]); + f.host.put_words(POOL_HOST + 0x800, &LIST[6..]); + f.cmd(&[0x171, 2, 0x100, 6, 0x800, 7]); + assert_eq!(f.textport_jobs(), LIST_JOBS); + // One range, 0x16e. + f.take_jobs(); + f.host.put_words(POOL_HOST + 0x2000, &LIST); + f.cmd(&[0x16e, 0x2000, 13]); + assert_eq!(f.textport_jobs(), LIST_JOBS); +} + +/// The pool's pages go through the map RAM +/// when the pool is read: a page whose entry maps nothing fails the whole read. +#[test] +fn the_pool_is_read_through_the_map_ram() { + let f = Fixture::hooked(); + f.pool(); + f.host.put_words(POOL_HOST + 0x100, &LIST); + f.w(0x100000 + 4 * (POOL_DEVICE >> 14), 0); + f.cmd(&[0x16e, 0x100, 13]); + assert!(f.textport_jobs().is_empty()); + // Mapped again, it reads. + f.map(POOL_DEVICE >> 14, POOL_HOST); + f.cmd(&[0x16e, 0x100, 13]); + assert_eq!(f.textport_jobs(), LIST_JOBS); + // The pool is not a single page: a range into the second one. + f.take_jobs(); + f.host.put_words(POOL_HOST + 0x4000 + 0x10, &LIST); + f.cmd(&[0x16e, 0x4010, 13]); + assert_eq!(f.textport_jobs(), LIST_JOBS); +} + +/// The pipe-memory path: 0x167 +/// pulls the list from the pool into pipe memory at 0x2000, and 0x170 invokes +/// it from there as two ranges. 0x16f then moves the last four words, the point, +/// to 0x3000, and 0x16d invokes them alone, redrawing the point. +#[test] +fn a_list_from_pipe_memory_reaches_the_decoder() { + let f = Fixture::hooked(); + f.pool(); + f.host.put_words(POOL_HOST + 0x100, &LIST); + f.cmd(&[0x167, 0x2000, 0x100, 52, 0]); + assert_eq!(f.list_word(0x2000), 1); + assert_eq!(f.list_word(0x2000 + 48), 8); + f.cmd(&[0x170, 2, 0x2000, 6, 0x2018, 7]); + assert_eq!(f.textport_jobs(), LIST_JOBS); + f.take_jobs(); + f.cmd(&[0x16f, 0x3000, 0x2000 + 36, 16]); + assert_eq!(f.list_word(0x3000), 1); + assert_eq!(f.list_word(0x300c), 8); + f.cmd(&[0x16d, 0x3000, 4]); + assert_eq!(f.textport_jobs(), [LIST_JOBS[2]]); + // An overlapping move goes as memmove does. + f.cmd(&[0x16f, 0x3004, 0x3000, 16]); + assert_eq!(f.list_word(0x3004), 1); + assert_eq!(f.list_word(0x3010), 8); + // A move that leaves the memory is dropped. + f.cmd(&[0x16f, LIST_BYTES - 8, 0x3000, 16]); + assert_eq!(f.list_word(LIST_BYTES as usize - 8), 0); +} + +/// The library's display-list +/// offsets count from the start of the context's display-list region, which the +/// kernel's 0x1e4 packets place at the model's 0x400-byte granule: two contexts +/// each using offset 0 do not meet in pipe memory. A freed region leaves no +/// base. +#[test] +fn list_offsets_follow_the_context_region() { + let f = Fixture::hooked(); + f.pool(); + f.host.put_words(POOL_HOST, &LIST); + // Class 1 key 0 at granule 4: a base of 0x1000. + f.cmd(&[0x1e4, 1, 0, 4, 8]); + f.cmd(&[0x167, 0, 0, 52, 0]); + assert_eq!(f.list_word(0x1000), 1); + assert_eq!(f.list_word(0), 0); + f.cmd(&[0x16d, 0, 13]); + assert_eq!(f.textport_jobs(), LIST_JOBS); + // Another class or key places nothing. + f.cmd(&[0x1e4, 2, 0, 9, 8]); + f.cmd(&[0x1e4, 1, 3, 9, 8]); + f.take_jobs(); + f.cmd(&[0x16d, 0, 13]); + assert_eq!(f.textport_jobs(), LIST_JOBS); + // Freed: the base is gone. + f.cmd(&[0x1e4, 1, 0, 4, 0]); + f.take_jobs(); + f.cmd(&[0x16d, 0, 13]); + assert!(f.textport_jobs().is_empty()); + // A context activation clears the base before its regions are placed. + f.cmd(&[0x1e4, 1, 0, 4, 8]); + f.cmd(&[0x1fd, 0x1000_0000 | (0x50_0000 >> 12), 0, 0x10000]); + f.take_jobs(); + f.cmd(&[0x16d, 0, 13]); + assert!(f.textport_jobs().is_empty()); +} + +/// 0x170 runs up to eight ranges of a list in +/// pipe memory as one list. Nine is past the packet's limit and opens nothing: +/// the sync probe after it frames as a probe and is answered. +#[test] +fn a_list_call_takes_up_to_eight_ranges() { + let f = Fixture::hooked(); + f.put_list(0x2000, &LIST); + let counts = [2u32, 2, 2, 2, 2, 1, 1, 1]; + let mut cmd = vec![0x170, 8]; + let mut at = 0x2000; + for c in counts { + cmd.extend([at, c]); + at += 4 * c; + } + f.cmd(&cmd); + assert_eq!(f.textport_jobs(), LIST_JOBS); + f.take_jobs(); + f.cmd(&[0x170, 9]); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); + f.cmd(&[0x171, 5]); + f.w(MAILBOX, 0); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); + f.cmd(&[0x170, 0]); + assert!(f.textport_jobs().is_empty()); +} + +/// A resident list may be as long as the +/// display-list class, 15 MiB of words, and runs whole. One word longer than the +/// class is refused before anything is read. +#[test] +fn a_list_may_be_as_long_as_the_class() { + let f = Fixture::new(); + f.put_list(0, &[1, 9, 0]); + let words = LIST_BYTES / 4; + f.cmd(&[0x16d, 0, words]); + assert_eq!(f.r(MAILBOX), 0x1234abcd, "the whole class ran"); + f.w(MAILBOX, 0); + f.w(INT_CAUSE, CAUSE_MAILBOX); + f.cmd(&[0x16d, 0, words + 1]); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0, "one word more is refused whole"); +} + +/// A list that invokes a list: the inner +/// invocation finds the engine running and fails, and the outer list goes on +/// after it. Without a pool, a pool invocation is refused. +#[test] +fn a_nested_list_fails_and_the_outer_list_goes_on() { + let f = Fixture::hooked(); + // The inner list is a point; the outer list sets a colour, calls the inner + // one and draws another point. + f.put_list(0x100, &[1, 6, 1, 1]); + f.put_list(0x0, &[1, 2, 5, 0x16d, 0x100, 4, 1, 6, 7, 8]); + f.take_jobs(); + f.cmd(&[0x16d, 0, 10]); + let jobs = f.jobs(); + assert_eq!( + f.textport_jobs(), + ["Textport 0x2 [5] packed=0x0 handled=false", "Textport 0x6 [7, 8] packed=0x0 handled=false"], + "{jobs:?}" + ); + assert!(jobs.iter().any(|j| j.starts_with("Kernel 0x16d [100, 4]")), "the call itself is seen"); + // No pool registered. + f.take_jobs(); + f.cmd(&[0x16e, 0, 3]); + assert!(f.textport_jobs().is_empty()); +} + +/// A list range that leaves the list memory, or a pool range that leaves the +/// pool, is refused with nothing fed. +#[test] +fn a_range_outside_its_memory_is_refused() { + let f = Fixture::hooked(); + f.pool(); + f.host.put_words(POOL_HOST + POOL_LEN as u64 - 8, &[1, 6]); + f.cmd(&[0x16e, POOL_LEN - 8, 4]); + assert!(f.textport_jobs().is_empty()); + f.cmd(&[0x16d, LIST_BYTES - 8, 4]); + f.cmd(&[0x16d, 2, 1]); + assert!(f.textport_jobs().is_empty()); +} + +/// A pool that is not whole 16 KiB pages leaves no pool registered, silently; +/// one without the tag, or past the model's bound, is refused too. +#[test] +fn a_pool_must_be_whole_pages_and_tagged() { + let f = Fixture::hooked(); + f.map_range(POOL_DEVICE, POOL_HOST, POOL_LEN as u64); + f.host.put_words(POOL_HOST, &LIST); + let page = (POOL_DEVICE >> 12) as u32; + for words in [ + [0x1ed, page, 0x9000, 1], + [0x1ed, page, 0x9000, 0x1000], + [0x1ed, page, 0x0000, POOL_LEN], + [0x1ed, page + 1, 0x9000, POOL_LEN], + [0x1ed, page, 0x9000, 0x1000_4000], + ] { + f.cmd(&words); + f.cmd(&[0x16e, 0, 13]); + assert!(f.textport_jobs().is_empty(), "{words:x?}"); + } + f.cmd(&[0x1ed, page, 0x9000, POOL_LEN]); + f.cmd(&[0x16e, 0, 13]); + assert_eq!(f.textport_jobs(), LIST_JOBS); +} + +/// A reset forgets the pool, the region base and a pending record. +#[test] +fn reset_forgets_the_pool_the_region_and_the_record() { + let f = Fixture::hooked(); + f.pool(); + f.cmd(&[0x1e4, 1, 0, 4, 8]); + f.cmd(&[0x1c8, 0x40, 16]); + f.kona.power_on(); + f.host.put_words(POOL_HOST, &LIST); + f.cmd(&[0x16e, 0, 13]); + assert!(f.textport_jobs().is_empty()); + // The record is gone: a packet is the back end's. + f.host.put_words(SRC, &[1, 2, 3, 4]); + f.packet(SRC, 16, 16, 0, 0); + assert_eq!(f.list_word(0x40), 0); + assert!(f.jobs().last().unwrap().starts_with("dma-in")); +} + +/// A DMA completion goes out on the general vector. +#[test] +fn a_dma_completion_goes_out_on_the_general_vector() { + let f = Fixture::new(); + f.w(INT_DEST_UPPER, 0x98000); + f.w(INT_DEST_LOWER, 0x180_0090); + f.w(INT_VECTORS, 0x3e2210); + f.host.put_words(0x40_0000, &[1, 2, 3, 4]); + f.cmd(&[0x1c8, 0x100, 16]); + f.cmd(&[0x1eb, 0x1000_0400, 0x9000, 16, 16, 0, 0x100]); + assert_eq!(f.host.sent().len(), 1); + assert_eq!(f.host.sent()[0].vector, 0x10); +} + + +/// A packet whose high word has bit 28 clear carries `io >> 12` of a device +/// address: the map RAM entry `word >> 2` translates it, and the low two bits +/// of the word are bits 13:12 of the offset into its 16 KiB page. A packet +/// into an unmapped page is refused and completes nothing. +#[test] +fn a_mapped_packet_goes_through_the_map_ram() { + let f = Fixture::hooked(); + // Device address 0x1b234: entry 6, offset 0x3234 in the page. + f.map(6, 0x60_0000); + f.host.put_words(0x60_3234, &[0x11, 0x22, 0x33, 0x44]); + f.list_write(0x80, 16); + f.dma(&[0x1eb, 0x1b, 0x9234, 16, 16, 0, 0x100]); + assert_eq!(f.list_word(0x80), 0x11); + assert_eq!(f.list_word(0x8c), 0x44); + assert_eq!(f.r(MAILBOX), 0x100); + // Entry 7 is not mapped. + f.w(MAILBOX, 0); + f.w(INT_CAUSE, CAUSE_MAILBOX); + f.list_write(0x90, 16); + f.dma(&[0x1eb, 0x1c, 0x9000, 16, 16, 0, 0x100]); + assert_eq!(f.list_word(0x90), 0); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + // With no record the back end gets the device address, not a physical one. + f.dma(&[0x1eb, 0x1b, 0x9234, 16, 16, 0, 0]); + assert_eq!(f.take_jobs(), ["dma-in 0x1b234 1 0x10 0x10"]); +} diff --git a/src/dev/kona/tests/eeprom.rs b/src/dev/kona/tests/eeprom.rs new file mode 100644 index 000000000..564148d23 --- /dev/null +++ b/src/dev/kona/tests/eeprom.rs @@ -0,0 +1,439 @@ +//! The flash EEPROM operation, command 0x1ec. Ported from +//! tests/qtest/sn0-origin2000-kona-eeprom-test.c. +//! +//! The pipe's flash holds its boot image, a video combination and a 48-byte +//! header. The graphics install tool reads and programs it with command 0x1ec, +//! an operation number and that operation's words, and reads the pipe's answer +//! from a reply buffer: word 0 goes non-zero last, word 1 is a word count and +//! the data follows. The PROM and the kernel read the same header through the +//! indirect port at internal 0x87ff00. + +use super::*; + +const PORT_ADDR: u64 = 0x8003e8; +const PORT_DATA: u64 = 0x8003f0; +const INT_CAUSE: u64 = 0x800238; +const MAILBOX: u64 = 0x800240; + +const EEPROM_OPERATION: u32 = 0x1ec; +const OP_WRITE: u32 = 1; +const OP_READ: u32 = 2; +const OP_REPLY_BUFFER: u32 = 3; +const OP_CHECK: u32 = 4; +const OP_IDENTIFY: u32 = 5; +const OP_ERASE_COMBO: u32 = 6; +const TYPE_IMAGE: u32 = 0; +const TYPE_COMBO: u32 = 1; +const TYPE_HEADER: u32 = 2; + +const FLASH_COMBO: u32 = 0x20000; +const FLASH_HEADER: u32 = 0x7ff00; +const HEADER_WORDS: usize = 12; +const HEADER_STATE: usize = 7; +/// The indirect port reaches the flash's read side here. +const INTERNAL_FLASH: u32 = 0x800000; +const FLASH_IDENTITY: u32 = 0x1fc4_0000; +const REPLY_PAGE: u64 = 0x32_0000; +const OTHER_PAGE: u64 = 0x33_0000; + +impl Fixture { + pub(super) fn peek(&self, addr: u32) -> u32 { + self.w(PORT_ADDR, addr); + self.r(PORT_DATA) + } + + pub(super) fn clear(&self, addr: u64, len: usize) { + self.host.put(addr, &vec![0; len]); + } + + /// The unmanaged path's reply buffer: 0x1ec 3, the page word with the + /// host-physical bit, the offset word and the buffer length. + pub(super) fn reply_buffer(&self, addr: u64) { + self.cmd(&[EEPROM_OPERATION, OP_REPLY_BUFFER, 0x1000_0000 | (addr >> 12) as u32, (addr & 0xfff) as u32, 0x4000]); + } + + /// The reply at `addr`: word 0 must be set; returns the count in word 1. + pub(super) fn reply_count(&self, addr: u64) -> u32 { + assert_eq!(self.host.word(addr), 1, "word 0 goes non-zero last"); + self.host.word(addr + 4) + } + + pub(super) fn reply_data(&self, addr: u64, i: u64) -> u32 { + self.host.word(addr + 8 + 4 * i) + } + + /// Op 1: a type, the offset for type 0 only, a count, then the words. + pub(super) fn flash_write(&self, ty: u32, offset: u32, words: &[u32]) { + self.cmd(&[EEPROM_OPERATION, OP_WRITE, ty]); + if ty == TYPE_IMAGE { + self.cmd(&[offset]); + } + self.cmd(&[words.len() as u32]); + self.cmd(words); + } + + pub(super) fn flash_read(&self, offset: u32, bytes: u32) { + self.cmd(&[EEPROM_OPERATION, OP_READ, offset, bytes]); + } + + /// The PROM's sync probe, 1 9 0: its mailbox reply shows the command page + /// is between commands, so no word of an earlier operation is still owed. + pub(super) fn assert_in_step(&self) { + self.cmd(&[1, 9, 0]); + // Wait for the stream: the polled registers below do not. + self.r(PORT_DATA); + assert_eq!(self.r(INT_CAUSE) & 0x10, 0x10); + assert_eq!(self.r(MAILBOX), 0x1234abcd); + self.w(MAILBOX, 0); + self.w(INT_CAUSE, 0x10); + } + + pub(super) fn assert_check_answers(&self, at: u64) { + self.clear(at, 0x10); + self.cmd(&[EEPROM_OPERATION, OP_CHECK, 0]); + assert_eq!(self.reply_count(at), 1); + assert_eq!(self.reply_data(at, 0), 1); + } +} + +/// The check, 0x1ec 4 0, answers count 1 and a +/// non-zero status in the buffer 0x1ec 3 named, at its byte offset: the tool +/// polls word 0 and then needs word 2 non-zero, and without it prints +/// "graphics not responding". Fails on a model that does not frame 0x1ec, does +/// not post, or drops the offset's low 12 bits as 0x1f2 does on the pipe. +#[test] +fn check_answers_at_offset() { + let f = Fixture::new(); + let at = REPLY_PAGE + 0x100; + f.clear(REPLY_PAGE, 0x200); + f.reply_buffer(at); + f.cmd(&[EEPROM_OPERATION, OP_CHECK, 0]); + assert_eq!(f.reply_count(at), 1); + assert_eq!(f.reply_data(at, 0), 1); + assert_eq!(f.host.word(REPLY_PAGE), 0); + f.assert_in_step(); +} + +/// 0x1ec 5 0: count 1, the identity word. +#[test] +fn identify() { + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.cmd(&[EEPROM_OPERATION, OP_IDENTIFY, 0]); + assert_eq!(f.reply_count(REPLY_PAGE), 1); + assert_eq!(f.reply_data(REPLY_PAGE, 0), FLASH_IDENTITY); + f.assert_in_step(); +} + +/// The default flash is zero, but the header's state +/// reads 2, through op 2 and through the indirect port the PROM and kernel +/// use. A model that kept a fixed answer at one port address only would fail +/// the op 2 half. +#[test] +fn default_header() { + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.flash_read(FLASH_HEADER, HEADER_WORDS as u32 * 4); + assert_eq!(f.reply_count(REPLY_PAGE), HEADER_WORDS as u32); + for i in 0..HEADER_WORDS { + let want = if i == HEADER_STATE { 2 } else { 0 }; + assert_eq!(f.reply_data(REPLY_PAGE, i as u64), want); + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_HEADER + 4 * i as u32), want); + } +} + +/// The tool's header write, 0x1ec 1 2 12 and +/// twelve words: status 1, and the words read back through op 2 and through +/// the indirect port at 0x87ff00, so a header written with state 3 is what the +/// PROM's check sees on the next boot. +#[test] +fn header_write_reaches_port() { + let f = Fixture::new(); + let mut header: Vec = (0..HEADER_WORDS as u32).map(|i| 0x1111_1111 * (i + 1)).collect(); + header[HEADER_STATE] = 3; + + f.reply_buffer(REPLY_PAGE); + f.flash_write(TYPE_HEADER, 0, &header); + assert_eq!(f.reply_count(REPLY_PAGE), 1); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 1); + + f.clear(REPLY_PAGE, 0x100); + f.flash_read(FLASH_HEADER, 4 * HEADER_WORDS as u32); + assert_eq!(f.reply_count(REPLY_PAGE), HEADER_WORDS as u32); + for (i, want) in header.iter().enumerate() { + assert_eq!(f.reply_data(REPLY_PAGE, i as u64), *want); + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_HEADER + 4 * i as u32), *want); + } + f.assert_in_step(); +} + +/// An image group, 0x1ec 1 0 a n and n words, +/// reads back from a with op 2, including at an offset op 2 reads unaligned to +/// the group. +#[test] +fn image_write_reads_back() { + let words: Vec = (0..64u32).map(|i| 0xa500_0000 | (i * 0x10203)).collect(); + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.flash_write(TYPE_IMAGE, 0x4000, &words); + assert_eq!(f.reply_count(REPLY_PAGE), 1); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 1); + + f.clear(REPLY_PAGE, 0x400); + f.flash_read(0x4000, words.len() as u32 * 4); + assert_eq!(f.reply_count(REPLY_PAGE), words.len() as u32); + for (i, w) in words.iter().enumerate() { + assert_eq!(f.reply_data(REPLY_PAGE, i as u64), *w); + } + + f.clear(REPLY_PAGE, 0x400); + f.flash_read(0x4000 + 8, 8); + assert_eq!(f.reply_count(REPLY_PAGE), 2); + assert_eq!(f.reply_data(REPLY_PAGE, 0), words[2]); + assert_eq!(f.reply_data(REPLY_PAGE, 1), words[3]); + f.assert_in_step(); +} + +/// A count above 0x7ffb fails before any data +/// word is read: status 0, and the next word starts a new command. A model +/// that took the count's words would swallow the sync probe that follows. +#[test] +fn oversized_count_refused() { + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.cmd(&[EEPROM_OPERATION, OP_WRITE, TYPE_IMAGE, 0, 0x7ffc]); + assert_eq!(f.reply_count(REPLY_PAGE), 1); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 0); + f.assert_in_step(); +} + +/// A combination, 0x1ec 1 1 n and n words, +/// lands at 0x20000 after the marker 0xfacefeed and n; 0x1ec 6, which takes no +/// word and posts nothing, zeroes the marker. +#[test] +fn combination_and_its_erase() { + let words = [0xc0ffee01, 0xc0ffee02, 0xc0ffee03]; + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.flash_write(TYPE_COMBO, 0, &words); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 1); + + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_COMBO), 0xfacefeed); + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_COMBO + 4), 3); + for (i, w) in words.iter().enumerate() { + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_COMBO + 8 + 4 * i as u32), *w); + } + + f.clear(REPLY_PAGE, 0x100); + f.cmd(&[EEPROM_OPERATION, OP_ERASE_COMBO]); + assert_eq!(f.host.word(REPLY_PAGE), 0); + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_COMBO), 0); + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_COMBO + 4), 3); + f.assert_in_step(); +} + +/// An operation number outside 1 to 6 takes +/// no more words: the ARM returns having read nothing more. The check that +/// follows answers, which fails on a model that swallowed its 0x1ec as the +/// unknown operation's word. +#[test] +fn unknown_op_takes_nothing() { + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.cmd(&[EEPROM_OPERATION, 7]); + f.assert_check_answers(REPLY_PAGE); + f.cmd(&[EEPROM_OPERATION, 0]); + f.assert_check_answers(REPLY_PAGE); + f.assert_in_step(); +} + +/// A write of an unknown type takes its +/// count's data words, stores none of them and answers 0. +#[test] +fn unknown_type_consumes_data() { + let words = [0x0bad_da7a, 0x0bad_da7b]; + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.cmd(&[EEPROM_OPERATION, OP_WRITE, 3, 2]); + f.cmd(&words); + assert_eq!(f.reply_count(REPLY_PAGE), 1); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 0); + for off in (0..0x10).step_by(4) { + let w = f.peek(INTERNAL_FLASH + off); + assert!(w != words[0] && w != words[1]); + } + f.assert_check_answers(REPLY_PAGE); + f.assert_in_step(); +} + +/// Reads the model refuses: a length that +/// is not whole words, which would never end the ARM's copy loop, and a reply +/// longer than the 16 KiB buffer. Each takes its two words, posts nothing, and +/// leaves the page in step. +#[test] +fn refused_reads_post_nothing() { + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + for len in [6, 0x4000 - 4, 0x4000] { + f.clear(REPLY_PAGE, 0x10); + f.flash_read(0, len); + assert_eq!(f.host.word(REPLY_PAGE), 0, "length {len:#x}"); + f.assert_check_answers(REPLY_PAGE); + } + // The longest read that fits: count plus 0xff8 data words. + f.clear(REPLY_PAGE, 0x10); + f.flash_read(0, 0x4000 - 8); + assert_eq!(f.reply_count(REPLY_PAGE), (0x4000 - 8) / 4); + f.assert_in_step(); +} + +/// A read running past the 512 KiB answers +/// the words inside it and, as the model's choice, zero past it; the count is +/// the whole request. +#[test] +fn read_past_end_answers_zero() { + let words = [0xe0d0_f000, 0xe0d0_f004]; + let f = Fixture::new(); + f.reply_buffer(OTHER_PAGE); + f.flash_write(TYPE_IMAGE, 0x7fff8, &words); + f.clear(REPLY_PAGE, 0x100); + f.reply_buffer(REPLY_PAGE); + f.flash_read(0x7fff8, 16); + assert_eq!(f.reply_count(REPLY_PAGE), 4); + assert_eq!(f.reply_data(REPLY_PAGE, 0), words[0]); + assert_eq!(f.reply_data(REPLY_PAGE, 1), words[1]); + assert_eq!(f.reply_data(REPLY_PAGE, 2), 0); + assert_eq!(f.reply_data(REPLY_PAGE, 3), 0); + // An offset op 2 rounds down to a word. + f.clear(REPLY_PAGE, 0x100); + f.flash_read(0x7fff8 + 3, 4); + assert_eq!(f.reply_data(REPLY_PAGE, 0), words[0]); + f.assert_in_step(); +} + +/// A write running past the 512 KiB stores the +/// bytes inside it, drops the rest, answers 1, and does not wrap round to +/// offset 0 even from an offset near 2^32. +#[test] +fn write_past_end_dropped() { + let words = [0x5a5a_0001, 0x5a5a_0002]; + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.flash_write(TYPE_IMAGE, 0x7fffc, &words); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 1); + assert_eq!(f.peek(INTERNAL_FLASH + 0x7fffc), words[0]); + + f.clear(REPLY_PAGE, 0x100); + f.flash_write(TYPE_IMAGE, 0xffff_fffc, &words); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 1); + assert_ne!(f.peek(INTERNAL_FLASH), words[1]); + f.assert_in_step(); +} + +/// The flash is non-volatile: a written word +/// survives a system reset, where the reply buffer does not. +#[test] +fn flash_survives_reset() { + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.flash_write(TYPE_IMAGE, 0x100, &[0x5eed_1e55]); + f.kona.power_on(); + assert_eq!(f.peek(INTERNAL_FLASH + 0x100), 0x5eed_1e55); + // The reply buffer did not survive: the check answers nowhere. + f.clear(REPLY_PAGE, 0x10); + f.cmd(&[EEPROM_OPERATION, OP_CHECK, 0]); + assert_eq!(f.host.word(REPLY_PAGE), 0); +} + +/// The EEPROM operation is interrupted mid-write by a slot store: the partial +/// operation is dropped and the new command opens. +#[test] +fn a_slot_store_drops_a_partial_operation() { + let f = Fixture::new(); + f.reply_buffer(REPLY_PAGE); + f.cmd(&[EEPROM_OPERATION, OP_WRITE, TYPE_IMAGE, 0x100, 4, 0x1111]); + // Token 0x172 stored in its slot, with its argument. + f.w(0xc1000c + 0x20 * 0x172, 0); + f.assert_in_step(); + assert_eq!(f.peek(INTERNAL_FLASH + 0x100), 0x1111, "the word taken before the drop stays"); + assert_eq!(f.peek(INTERNAL_FLASH + 0x104), 0); +} + +/// The backed flash survives the board: what the pipe programmed is in the +/// file, and a new board over the file reads it. +#[test] +fn a_flash_file_outlasts_the_board() { + let dir = ScratchDir::new("eeprom-flash"); + let path = dir.join("flash.bin"); + std::fs::write(&path, vec![0u8; crate::dev::kona::FLASH_SIZE]).unwrap(); + let open = || std::fs::OpenOptions::new().read(true).write(true).open(&path).unwrap(); + let build = |host: &Arc| Kona::new(test_cfg(), host.clone(), host.clone(), crate::dev::kona::tests::test_status(), Some(open())).unwrap(); + { + let host = TestHost::new(); + let f = Fixture::of(host.clone(), build(&host)); + f.reply_buffer(REPLY_PAGE); + f.flash_write(TYPE_IMAGE, 0x2000, &[0xfeed_f00d, 0xcafe_0001]); + assert_eq!(f.reply_data(REPLY_PAGE, 0), 1); + } + let host = TestHost::new(); + let f = Fixture::of(host.clone(), build(&host)); + assert_eq!(f.peek(INTERNAL_FLASH + 0x2000), 0xfeed_f00d); + assert_eq!(f.peek(INTERNAL_FLASH + 0x2004), 0xcafe_0001); +} + +/// A snapshot that does not parse is refused with the board and its flash file as they were: the state +/// blob cut short (every byte of the first 64, then at a growing stride, then the last bytes) or given a +/// trailing byte leaves the board's state unchanged and the flash file unwritten. The cuts go to the state +/// loader as bytes in memory; one refusal and the whole snapshot go through the chunk store, the path a +/// machine takes, and the file takes the image. +#[test] +fn a_snapshot_that_does_not_parse_leaves_the_board_and_its_flash_file_alone() { + use crate::dev::kona::back::snapshot::put_bytes; + let dir = ScratchDir::new("eeprom-snapshot"); + let file = |name: &str| { + let path = dir.join(name); + std::fs::write(&path, vec![0u8; crate::dev::kona::FLASH_SIZE]).unwrap(); + path + }; + let (pa, pb) = (file("a.bin"), file("b.bin")); + let make = |path: &std::path::Path| { + let host = TestHost::new(); + let f = std::fs::OpenOptions::new().read(true).write(true).open(path).unwrap(); + (host.clone(), Kona::new(test_cfg(), host.clone(), host, crate::dev::kona::tests::test_status(), Some(f)).unwrap()) + }; + let cs = crate::chunk_store::ChunkStore::new(dir.join("saves")); + // A board with a programmed flash image, snapshotted. + let (host, kona) = make(&pa); + let a = Fixture::of(host, kona); + a.reply_buffer(REPLY_PAGE); + a.flash_write(TYPE_IMAGE, 0x2000, &[0xfeed_f00d, 0xcafe_0001]); + a.settle(); + let saved = a.kona.save_chunks(&cs).unwrap(); + let blob = a.kona.state_bytes(&mut a.kona.shared.pipe.lock()).unwrap(); + // Another board, its flash file untouched. + // Not started, so nothing moves under the comparison. + let (_host, b) = make(&pb); + let before = b.save_chunks(&cs).unwrap(); + let state_before = b.state_bytes(&mut b.shared.pipe.lock()).unwrap(); + let flash_before = std::fs::read(&pb).unwrap(); + // Every byte of the first 64, a doubling stride after, and the last bytes: the cuts fall inside every field the blob has. + let cuts: Vec = (0..64).chain(std::iter::successors(Some(97usize), |c| Some(c * 3 / 2 + 1)).take_while(|&c| c < blob.len())).chain(blob.len() - 8..blob.len()).collect(); + for cut in cuts { + assert!(b.load_state(&mut b.shared.pipe.lock(), &blob[..cut]).is_err(), "a blob cut at {cut} loaded"); + } + let mut long = blob.clone(); + long.push(0); + assert!(b.load_state(&mut b.shared.pipe.lock(), &long).is_err(), "a blob with a trailing byte loaded"); + // The same refusal through the chunk store, with the bulk chunks the snapshot has. + let cut = &blob[..blob.len() / 2]; + let mut list: Vec<_> = saved.iter().filter(|c| !c.name.starts_with("state-")).cloned().collect(); + list.push(crate::dev::kona::KonaChunk { name: format!("state-{}", cut.len()), hashes: put_bytes(&cs, cut).unwrap() }); + assert!(b.load_chunks(&cs, &list).is_err()); + assert_eq!(b.state_bytes(&mut b.shared.pipe.lock()).unwrap(), state_before, "a refused load changed the board"); + assert_eq!(b.save_chunks(&cs).unwrap(), before, "a refused load changed the board"); + assert_eq!(std::fs::read(&pb).unwrap(), flash_before, "a refused load wrote the flash file"); + // The whole snapshot loads, and the file takes the image. + b.load_chunks(&cs, &saved).unwrap(); + assert_eq!(&std::fs::read(&pb).unwrap()[0x2000..0x2008], &[0xfe, 0xed, 0xf0, 0x0d, 0xca, 0xfe, 0x00, 0x01]); + assert_eq!(b.save_chunks(&cs).unwrap(), saved); +} diff --git a/src/dev/kona/tests/fifo.rs b/src/dev/kona/tests/fifo.rs new file mode 100644 index 000000000..3c3219f12 --- /dev/null +++ b/src/dev/kona/tests/fifo.rs @@ -0,0 +1,427 @@ +//! The command FIFO between the processor and the Host Interface Processor's +//! thread: stores queue in the shared ring, a full ring answers the bus busy, +//! registers that depend on the stream wait for it, and the registers a driver +//! polls while the pipe works do not. +//! +//! The cases that fill the ring hold the pipe's lock, so the thread cannot take +//! a run out of it; nothing here waits on a clock. + +use super::*; +use crate::traits::Device; + +const PORT_DATA: u64 = 0x8003f0; +const CREDIT: u64 = 0x800208; +const ACTIVITY: u64 = 0x8003a8; +const INT_ENABLE: u64 = 0x800230; +const INT_CAUSE: u64 = 0x800238; +const MAILBOX: u64 = 0x800240; +const MAPRAM: u64 = 0x100000; +const EEPROM_OPERATION: u32 = 0x1ec; +const OP_WRITE: u32 = 1; +const OP_READ: u32 = 2; +const OP_REPLY_BUFFER: u32 = 3; +const OP_CHECK: u32 = 4; +const OP_IDENTIFY: u32 = 5; +const TYPE_IMAGE: u32 = 0; +const TYPE_HEADER: u32 = 2; +const FLASH_HEADER: u32 = 0x7ff00; +const HEADER_STATE: usize = 7; +const INTERNAL_FLASH: u32 = 0x800000; +const REPLY_PAGE: u64 = 0x32_0000; + +/// Textport colour selects: each is one job, in the order stored. +fn colours(n: u32) -> Vec { + (0..n).flat_map(|i| [1, 2, i]).collect() +} + +fn indices(jobs: &[String]) -> Vec { + jobs.iter() + .map(|j| u32::from_str_radix(j.split('[').nth(1).unwrap().split(']').next().unwrap(), 16).unwrap()) + .collect() +} + +/// Stores go into the ring and the thread runs them in the order the +/// processor stored them; a read the stream feeds waits until it has. +#[test] +fn stores_run_in_order_on_the_thread() { + let (f, board) = threaded(); + let n = 20_000; + for w in colours(n) { + f.w(COMMAND_PORT, w); + } + // A register the stream feeds: busy until the ring is empty and the thread + // idle. + f.r(PORT_DATA); + let jobs = f.jobs(); + assert_eq!(jobs.len(), n as usize); + assert_eq!(indices(&jobs), (0..n).collect::>()); + drop(board); +} + +/// A full ring answers a store busy: it never spins on the processor's thread. +/// The processor retries it, and once the thread has made room every store is +/// run, none twice and none lost. +#[test] +fn a_full_ring_answers_busy_never_spins() { + let (f, _board) = threaded(); + let held = f.kona.shared.pipe.lock(); + let mut pushed = 0u32; + let mut busy = false; + for i in 0..70_000u32 { + // Stray words: each is dropped and none opens a command. + match f.kona.bus_write(COMMAND_PORT, 4, 0x1000 + i as u64) { + BUS_OK => pushed += 1, + BUS_BUSY => { + busy = true; + break; + } + other => panic!("status {other}"), + } + } + assert!(busy, "{pushed} stores went in and the ring never filled"); + assert!(pushed > 60_000); + // The same store is refused again while the thread has not made room. + assert_eq!(f.kona.bus_write(COMMAND_PORT, 4, 0x1000), BUS_BUSY); + // A register the stream feeds is busy too, and a polled one is not. + assert_eq!(f.kona.bus_read(PORT_DATA, 4).0, BUS_BUSY); + assert_eq!(f.kona.bus_read(CREDIT, 4), (BUS_OK, 0x7fe), "a full FIFO reads full"); + drop(held); + f.w(COMMAND_PORT, 1); + f.w(COMMAND_PORT, 9); + f.w(COMMAND_PORT, 0); + assert_eq!(f.r(PORT_DATA), 0); + assert_eq!(f.r(MAILBOX), 0x1234abcd, "the probe after the flood was framed"); + assert_eq!(f.r(CREDIT), 0); +} + +/// The registers a driver reads and acknowledges while the pipe works are +/// served at once, with stores behind the thread; every other read waits. +#[test] +fn polled_registers_do_not_wait_for_the_stream() { + let (f, _board) = threaded(); + let held = f.kona.shared.pipe.lock(); + for w in [1, 9, 0] { + retry(|| f.kona.bus_write(COMMAND_PORT, 4, w as u64) == BUS_OK); + } + // Served at once: the cause, the mailbox, the enable, the activity latch + // (the pipe is busy), the credit register (words waiting) and the widget id. + for reg in [INT_CAUSE, MAILBOX, INT_ENABLE, ACTIVITY, CREDIT, 0x04] { + assert_eq!(f.kona.bus_read(reg, 4).0, BUS_OK, "register {reg:#x}"); + } + assert_eq!(f.kona.bus_read(ACTIVITY, 4).1, 4); + assert_eq!(f.kona.bus_read(CREDIT, 4).1, 4, "three words wait, rounded up to two"); + // Everything else reads what the stream writes or changes what it reads. + for reg in [PORT_DATA, MAPRAM, 0x24, 0x98, 0x800220] { + assert_eq!(f.kona.bus_read(reg, 4).0, BUS_BUSY, "register {reg:#x}"); + } + // A write that waits for the stream joins it, in order, behind the words before it. + assert_eq!(f.kona.bus_write(0x24, 4, 1), BUS_OK); + assert_eq!(f.kona.bus_write(MAPRAM, 4, 1), BUS_OK, "map RAM stores join the stream"); + drop(held); + f.r(PORT_DATA); + assert_eq!(f.r(MAILBOX), 0x1234abcd); + assert_eq!(f.r(MAPRAM), 1); +} + +/// A map RAM store with nothing ahead of it is applied at once, with the +/// thread not running: the driver's word-by-word test of the map RAM then +/// costs no hand-off. One behind a queued store still waits its turn. +#[test] +fn a_map_ram_store_with_an_empty_stream_is_served_at_once() { + let f = Fixture::idle(KonaConfig::ir2(1).unwrap(), Recorder::default().hook()); + assert!(!f.kona.is_running()); + assert_eq!(f.kona.bus_write(MAPRAM, 4, 0x1000_0001), BUS_OK); + assert_eq!(f.kona.bus_read(MAPRAM, 4), (BUS_OK, 0x1000_0001)); + assert_eq!(f.kona.bus_write(MAPRAM + 4, 4, 0x1000_0002), BUS_OK); + assert_eq!(f.kona.bus_read(MAPRAM + 4, 4), (BUS_OK, 0x1000_0002)); + // A command word is in the ring, so the next map RAM store queues behind + // it, and a read waits. + assert_eq!(f.kona.bus_write(COMMAND_PORT, 4, 1), BUS_OK); + assert_eq!(f.kona.bus_write(MAPRAM + 8, 4, 7), BUS_OK); + assert_eq!(f.kona.bus_read(MAPRAM + 8, 4).0, BUS_BUSY); +} + +/// The low-watermark cause rises once the thread has run its work, when the +/// driver enabled it. +#[test] +fn the_low_watermark_cause_follows_the_work() { + let (f, _board) = threaded(); + f.w(0x34, 0x98000); + f.w(0x3c, 0x180_0090); + f.w(0x5c, 0x3e2210); + f.w(INT_ENABLE, 2); + for w in colours(100) { + f.w(COMMAND_PORT, w); + } + retry(|| f.r(INT_CAUSE) & 2 == 2); + f.r(PORT_DATA); + let sent = f.host.sent(); + assert!(sent.iter().any(|p| p.vector == 0x10), "{sent:?}"); +} + +/// Without the thread the same stores run in the store, so a test sees the +/// effect at once. +#[test] +fn without_the_thread_a_store_runs_in_the_store() { + let f = Fixture::hooked(); + f.cmd(&colours(3)); + assert_eq!(f.jobs().len(), 3); +} + +/// An 8-byte store to the command port is two words, high first, each as a +/// 4-byte store at the same offset; any other 8-byte store is two word stores +/// at the offset and the next. An 8-byte load is two word loads. +#[test] +fn a_doubleword_is_two_words() { + for threaded in [false, true] { + let (f, _board) = if threaded { + let (f, b) = self::threaded(); + (f, Some(b)) + } else { + (Fixture::hooked(), None) + }; + f.w64(COMMAND_PORT, 1 << 32 | 9); + f.w(COMMAND_PORT, 0); + f.r(PORT_DATA); + assert_eq!(f.r(MAILBOX), 0x1234abcd); + f.w64(MAPRAM, 0x11 << 32 | 0x22); + assert_eq!(f.r(MAPRAM), 0x11); + assert_eq!(f.r(MAPRAM + 4), 0x22); + let mut v = 0; + retry(|| { + let (s, d) = f.kona.bus_read(MAPRAM, 8); + v = d; + s == BUS_OK + }); + assert_eq!(v, 0x11 << 32 | 0x22); + // A doubleword that carries a command's last word and the next one's + // selector, as the GL client's stores do. + f.r(PORT_DATA); + f.take_jobs(); + f.w64(COMMAND_PORT, 1 << 32 | 2); + f.w64(COMMAND_PORT, 7 << 32 | 1); + f.w64(COMMAND_PORT, 6 << 32 | 3); + f.w64(COMMAND_PORT, 4 << 32); + f.r(PORT_DATA); + // The last doubleword's low word is padding after the command: a word + // that opens nothing, which the back end is told of. + assert_eq!( + f.take_jobs(), + ["Textport 0x2 [7] packed=0x0 handled=false", "Textport 0x6 [3, 4] packed=0x0 handled=false", "dropped 0x0"] + ); + } +} + +/// Stopping the thread runs the stores it had not taken, so a stopped board's +/// state is whole. +#[test] +fn stop_runs_what_is_left() { + let (f, board) = threaded(); + let n = 3000; + for w in colours(n) { + f.w(COMMAND_PORT, w); + } + board.stop(); + assert!(!board.is_running()); + assert_eq!(f.jobs().len(), n as usize); + // Stores after the stop wait in the ring until the thread starts again. + f.cmd(&[1, 2, 99]); + assert_eq!(f.jobs().len(), n as usize); + board.start(); + f.settle(); + assert_eq!(f.jobs().len(), n as usize + 1); +} + +/// A reset while the thread runs leaves it running, with the stream cleared. +#[test] +fn a_reset_restarts_the_thread() { + let (f, board) = threaded(); + f.cmd(&[0x1f1, 0x1000_0000]); + f.kona.power_on(); + assert!(board.is_running()); + f.cmd(&[1, 9, 0]); + f.r(PORT_DATA); + assert_eq!(f.r(MAILBOX), 0x1234abcd); + assert_eq!(f.r(INT_CAUSE) & 0x10, 0x10); +} + +/// A back end whose job holds the thread, as a drain on busy tiles does, until +/// the test lets it go. +fn hold(inside: Arc, release: Arc) -> TestHook { + use std::sync::atomic::Ordering::SeqCst; + TestHook { + job: Box::new(move |_, _| { + inside.store(true, SeqCst); + while !release.load(SeqCst) { + std::thread::yield_now(); + } + Some(Reply::Done) + }), + events: Arc::default(), + on_attach: None, + } +} + +/// A reset that arrives while the thread is inside a long job waits for it, as +/// it would for a drain, and then clears the stream and restarts the thread; +/// nothing deadlocks and nothing from before the reset survives it. +#[test] +fn a_reset_while_a_job_holds_the_thread_waits_and_then_proceeds() { + use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; + let (inside, release) = (Arc::new(AtomicBool::new(false)), Arc::new(AtomicBool::new(false))); + let host = TestHost::new(); + let kona = Kona::new(test_cfg(), host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + kona.shared.pipe.lock().backend.hook = Some(hold(inside.clone(), release.clone())); + let f = Fixture::of(host, kona); + retry(|| f.kona.bus_write(COMMAND_PORT, 4, 0x1000) == BUS_OK); + retry(|| inside.load(SeqCst)); + let (done, k) = (Arc::new(AtomicBool::new(false)), f.kona.clone()); + let flag = done.clone(); + let resetter = std::thread::spawn(move || { + crate::traits::Resettable::power_on(&*k); + flag.store(true, SeqCst); + }); + std::thread::sleep(std::time::Duration::from_millis(100)); + assert!(!done.load(SeqCst), "the reset waits for the job"); + release.store(true, SeqCst); + resetter.join().unwrap(); + assert!(f.kona.is_running(), "the thread runs again"); + f.settle(); +} + +/// An 8-byte store is all or nothing: refused busy it has done none of its +/// words, so the processor's retry does not repeat the first. +#[test] +fn a_busy_doubleword_has_no_effect() { + let (f, _board) = threaded(); + let held = f.kona.shared.pipe.lock(); + // Fill the ring, so no store has room. + while f.kona.bus_write(COMMAND_PORT, 4, 0x1000) == BUS_OK {} + assert_eq!(f.kona.bus_write(0x24, 8, 7 << 32 | 9), BUS_BUSY); + assert_eq!(f.kona.bus_write(MAPRAM + 0x100, 8, 0xffff_ffff_0000_0001), BUS_BUSY); + drop(held); + f.settle(); + assert_eq!(f.r(0x24), 9 << 22, "the busy store left widget control alone"); + f.w64(0x24, 7 << 32 | 9); + assert_eq!(f.r(0x24), 7); + f.w64(MAPRAM + 0x100, 0xffff_ffff_0000_0001); + assert_eq!(f.r(MAPRAM + 0x100), 0xffff_ffff); +} + +/// What the graphics install tool needs of the board, for the install tool's +/// the install tool: the credit register reads drained before every +/// command word; the check, header read and identify answer with a complete +/// reply (word 0 stored last); a write group is acknowledged with a non-zero +/// status; the flash gives back byte for byte what was written, in 0x2000-byte +/// reads, and the header words survive; the identify word is the same every +/// time. Run on the thread, as the tool runs against a machine: every word +/// waits for the credit register first and every reply is polled for. +#[test] +fn the_install_tool_can_program_and_verify_the_flash() { + let (f, _board) = threaded(); + // Before every command word: the masked level below 0x5dd. + let word = |w: u32| { + retry(|| f.r(CREDIT) & 0xffe < 0x5dd); + f.w(COMMAND_PORT, w); + }; + let words = |ws: &[u32]| ws.iter().for_each(|w| word(*w)); + // Word 0 cleared by the tool, then polled until the board stores it last. + let request = |ws: &[u32]| -> u64 { + f.host.put(REPLY_PAGE, &[0; 4]); + words(ws); + retry(|| f.host.word(REPLY_PAGE) != 0); + REPLY_PAGE + }; + let data = |at: u64, n: u64| -> Vec { (0..n).map(|i| f.reply_data(at, i)).collect() }; + + // Unmanaged path: the reply buffer from the two address words the kernel's + // mapping returned and the length. + words(&[EEPROM_OPERATION, OP_REPLY_BUFFER, 0x1000_0000 | (REPLY_PAGE >> 12) as u32, (REPLY_PAGE & 0xfff) as u32, 0x4000]); + + // The check: word 1 not negative, word 2 non-zero. + let at = request(&[EEPROM_OPERATION, OP_CHECK, 0]); + assert!((f.host.word(at + 4) as i32) >= 0); + assert_ne!(f.reply_data(at, 0), 0); + + // The header and the identification word, which must be the same twice. + let at = request(&[EEPROM_OPERATION, OP_READ, FLASH_HEADER, 0x30]); + assert_eq!(f.host.word(at + 4), 12); + let old_header = data(at, 12); + let at = request(&[EEPROM_OPERATION, OP_IDENTIFY, 0]); + let identity = f.reply_data(at, 0); + let at = request(&[EEPROM_OPERATION, OP_IDENTIFY, 0]); + assert_eq!(f.reply_data(at, 0), identity); + assert_eq!((identity >> 24, (identity >> 16) & 0xff), (0x1f, 0xc4), "the documented manufacturer and device codes"); + + // Code and data in groups of 0x1000 words, a rising 0x4000 bytes each, + // every group acknowledged before the next. + let image: Vec> = + (0..3u32).map(|g| (0..0x1000u32).map(|i| (g << 28) ^ i.wrapping_mul(0x9e37_79b1)).collect()).collect(); + for (g, group) in image.iter().enumerate() { + words(&[EEPROM_OPERATION, OP_WRITE, TYPE_IMAGE, g as u32 * 0x4000, 0x1000]); + f.host.put(REPLY_PAGE, &[0; 4]); + words(group); + retry(|| f.host.word(REPLY_PAGE) != 0); + assert_ne!(f.reply_data(REPLY_PAGE, 0), 0, "group {g} is acknowledged with a non-zero status"); + } + + // The header, state 2, with the codes it read. + let mut header: Vec = (0..12).map(|i| 0x0101_0101 * (i + 1)).collect(); + header[HEADER_STATE] = 2; + header[5] = identity >> 24; + header[6] = (identity >> 16) & 0xff; + words(&[EEPROM_OPERATION, OP_WRITE, TYPE_HEADER, 12]); + f.host.put(REPLY_PAGE, &[0; 4]); + words(&header); + retry(|| f.host.word(REPLY_PAGE) != 0); + assert_ne!(f.reply_data(REPLY_PAGE, 0), 0); + assert_ne!(header, old_header); + + // Read back in 0x2000-byte reads and compare. + for (g, group) in image.iter().enumerate() { + for half in 0..2u32 { + let at = request(&[EEPROM_OPERATION, OP_READ, g as u32 * 0x4000 + half * 0x2000, 0x2000]); + assert_eq!(f.host.word(at + 4), 0x800); + assert_eq!(data(at, 0x800), group[half as usize * 0x800..][..0x800], "group {g} half {half}"); + } + } + let at = request(&[EEPROM_OPERATION, OP_READ, FLASH_HEADER, 0x30]); + let back = data(at, 12); + assert_eq!(back, header); + // Through the indirect port too, as the PROM's check sees it. + assert_eq!(f.peek(INTERNAL_FLASH + FLASH_HEADER + 4 * HEADER_STATE as u32), 2); + f.assert_in_step(); +} + +/// A back end that panics. + +/// If the back end panics on the thread, the board is dead and says so: the bus +/// answers an error instead of busy forever, and `failure` names why. +#[test] +fn a_panicking_back_end_kills_the_board_visibly() { + let host = TestHost::new(); + let kona = Kona::new(test_cfg(), host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + kona.shared.pipe.lock().backend.hook = Some(TestHook { job: Box::new(|_, _| panic!("the back end panics on a job")), events: Arc::default(), on_attach: None }); + let board = crate::test_board::TestBoard::new(kona.clone()); + board.start(); + for w in [1, 2, 5] { + kona.bus_write(COMMAND_PORT, 4, w); + } + retry(|| kona.bus_read(PORT_DATA, 4).0 == BUS_ERR); + assert_eq!(kona.bus_write(COMMAND_PORT, 4, 1), BUS_ERR); + assert!(kona.failure().unwrap().contains("panicked")); + assert!(!board.is_running()); + // Stopping a dead board does not run the stores again. + board.stop(); +} + +/// A monitor waiting for the processor's state sleeps until it is put back. +#[test] +fn a_monitor_waits_for_the_state_and_gets_it() { + let (f, _board) = threaded(); + let held = f.kona.shared.pipe.lock(); + let k = f.kona.clone(); + let waiter = std::thread::spawn(move || k.failure()); + drop(held); + assert_eq!(waiter.join().unwrap(), None); +} diff --git a/src/dev/kona/tests/handle.rs b/src/dev/kona/tests/handle.rs new file mode 100644 index 000000000..50616706a --- /dev/null +++ b/src/dev/kona/tests/handle.rs @@ -0,0 +1,296 @@ +//! The display handle a back end's threads call, and the lifecycle hooks the +//! board gives a back end: attach, stop, quiesce and resume. + +use super::*; +use crate::traits::Device; + +const ACTIVITY: u64 = 0x8003a8; +const INT_ENABLE: u64 = 0x800230; +const INT_CAUSE: u64 = 0x800238; +const RETRACE_TAG: u64 = 0x800248; +const INT_DEST_UPPER: u64 = 0x34; +const INT_DEST_LOWER: u64 = 0x3c; +const INT_VECTORS: u64 = 0x5c; +const CAUSE_VIDEO: u32 = 8; +const GENERAL_VECTOR: u8 = 0x10; + +/// What a test says of the back end it stands in for: it notes the lifecycle calls and keeps the handle it is given. +#[derive(Default)] +struct Probe { + events: Arc>>, + handle: Arc>>, + jobs: Arc>, + /// Panic on the first job, to kill the board. + panics: bool, +} + +impl Probe { + /// The hook that notes the lifecycle, keeps the handle and counts jobs (or panics on one). + fn hook(self) -> TestHook { + let Probe { events, handle, jobs, panics } = self; + TestHook { + job: Box::new(move |_, _| { + assert!(!panics, "the back end panics on a job"); + *jobs.lock() += 1; + Some(Reply::Done) + }), + events, + on_attach: Some(Box::new(move |h| *handle.lock() = Some(h))), + } + } +} + +struct Rig { + f: Fixture, + events: Arc>>, + handle: Arc>>, + jobs: Arc>, +} + +impl Rig { + fn new() -> Rig { + Rig::with(Probe::default()) + } + + fn with(probe: Probe) -> Rig { + let (events, handle, jobs) = (probe.events.clone(), probe.handle.clone(), probe.jobs.clone()); + let f = Fixture::idle(test_cfg(), probe.hook()); + Rig { f, events, handle, jobs } + } + + fn handle(&self) -> DisplayHandle { + self.handle.lock().clone().expect("attach gave a handle") + } + + fn events(&self) -> Vec<&'static str> { + self.events.lock().clone() + } + + /// The driver's interrupt programming, and the video cause enabled. + fn program_video(&self) { + self.f.w(INT_DEST_UPPER, 0x98000); + self.f.w(INT_DEST_LOWER, 0x180_0090); + self.f.w(INT_VECTORS, 0x3e_2210); + self.f.w(INT_ENABLE, 0xf7c); + } +} + +/// Starting the board attaches the back end before the thread runs; stopping +/// it stops the back end once, after the stores the thread had not taken ran. +#[test] +fn start_attaches_and_stop_stops_after_the_stream_ran() { + let r = Rig::new(); + assert!(r.events().is_empty(), "nothing before the board starts"); + let board = TestBoard::new(r.f.kona.clone()); + board.start(); + r.f.settle(); + assert_eq!(r.events(), ["attach"]); + for w in [1, 2, 7] { + r.f.w(0xc10000, w); + } + board.stop(); + assert_eq!(r.events(), ["attach", "stop"]); + assert_eq!(*r.jobs.lock(), 1, "the command ran before the back end stopped"); + // A stop with no thread running is not a stop of the back end. + r.f.kona.stop(); + assert_eq!(r.events(), ["attach", "stop"]); +} + +/// A reset while running stops the back end, begins a new epoch and attaches +/// again: the display thread of the old run is gone before the new one starts. +#[test] +fn a_reset_cycles_the_back_end() { + let r = Rig::new(); + let board = TestBoard::new(r.f.kona.clone()); + board.start(); + r.f.settle(); + r.f.kona.power_on(); + assert_eq!(r.events(), ["attach", "stop", "reset", "attach"]); + board.stop(); +} + +/// A ticket made before a reset cannot complete after it: the tag register +/// and the video cause stay as the reset left them. A ticket of the new epoch +/// completes. The reset joins the back end first, so this is the epoch check +/// alone, not a race with a display thread mid-call. +#[test] +fn a_ticket_from_before_a_reset_is_dropped() { + let r = Rig::new(); + r.program_video(); + let board = TestBoard::new(r.f.kona.clone()); + board.start(); + r.f.settle(); + let h = r.handle(); + let old = h.epoch(); + r.f.kona.power_on(); + r.program_video(); + h.retrace_done(old, 0x55); + h.video_frame(old); + assert_eq!(r.f.r(RETRACE_TAG), 0); + assert_eq!(r.f.r(INT_CAUSE) & CAUSE_VIDEO, 0); + assert!(r.f.host.sent().is_empty()); + let now = r.handle().epoch(); + assert_ne!(now, old); + h.retrace_done(now, 0x56); + assert_eq!(r.f.r(RETRACE_TAG), 0x56); + assert_eq!(r.f.r(INT_CAUSE) & CAUSE_VIDEO, CAUSE_VIDEO); + assert_eq!(r.f.host.sent().len(), 1); + board.stop(); +} + +/// A frame raises the video cause with no tag and sends one interrupt until +/// the driver acknowledges it. +#[test] +fn a_frame_raises_video_once_without_a_tag() { + let r = Rig::new(); + r.program_video(); + r.f.kona.start(); + r.f.settle(); + let h = r.handle(); + let e = h.epoch(); + h.video_frame(e); + h.video_frame(e); + assert_eq!(r.f.r(INT_CAUSE) & CAUSE_VIDEO, CAUSE_VIDEO); + assert_eq!(r.f.r(RETRACE_TAG), 0); + assert_eq!(r.f.host.sent().len(), 1); + assert_eq!(r.f.host.sent()[0].vector, GENERAL_VECTOR); + r.f.w(INT_CAUSE, CAUSE_VIDEO as u32); + h.video_frame(e); + assert_eq!(r.f.host.sent().len(), 2); + r.f.kona.stop(); +} + +/// With no destination programmed a frame sets the cause and sends nothing. +#[test] +fn a_frame_with_no_destination_sends_no_packet() { + let r = Rig::new(); + r.f.w(INT_ENABLE, 0xf7c); + r.f.kona.start(); + r.f.settle(); + let h = r.handle(); + for _ in 0..3 { + h.video_frame(h.epoch()); + } + assert_eq!(r.f.r(INT_CAUSE) & CAUSE_VIDEO, CAUSE_VIDEO); + assert!(r.f.host.sent().is_empty()); + r.f.kona.stop(); +} + +/// The back end's activity reaches the driver's watchdog latch. +#[test] +fn activity_reaches_the_watchdog_latch() { + let r = Rig::new(); + r.f.kona.start(); + assert_eq!(r.f.r(ACTIVITY), 0); + r.handle().activity(); + assert_eq!(r.f.r(ACTIVITY), 4); + r.f.w(ACTIVITY, 4); + assert_eq!(r.f.r(ACTIVITY), 0); + r.f.kona.stop(); +} + +/// The handle holds the register file weakly: with the board gone every call +/// is a no-op, and the handle does not keep the register file alive. +#[test] +fn a_handle_outlives_the_board_harmlessly() { + let r = Rig::new(); + r.f.kona.start(); + r.f.settle(); + let h = r.handle(); + let weak = Arc::downgrade(&r.f.kona); + drop(r); + assert!(weak.upgrade().is_none(), "the board is freed with the handle outstanding"); + assert_eq!(h.epoch(), 0); + h.retrace_done(0, 1); + h.video_frame(0); + h.activity(); +} + +/// Quiesce lets the stores in the ring run, then quiesces the back end; +/// resume undoes it. +#[test] +fn quiesce_runs_the_ring_then_the_back_end() { + let r = Rig::new(); + let board = TestBoard::new(r.f.kona.clone()); + board.start(); + r.f.settle(); + for _ in 0..100 { + for w in [1, 2, 7] { + r.f.w(0xc10000, w); + } + } + r.f.kona.quiesce(); + assert_eq!(*r.jobs.lock(), 100); + assert_eq!(r.events(), ["attach", "quiesce"]); + r.f.kona.resume(); + assert_eq!(r.events(), ["attach", "quiesce", "resume"]); + board.stop(); +} + +/// A board whose back end panicked is dead, but stopping it still reaches the +/// back end: the next start attaches again and the first run's threads must +/// be gone by then. +#[test] +fn a_dead_board_still_stops_the_back_end() { + let r = Rig::with(Probe { panics: true, ..Default::default() }); + let board = TestBoard::new(r.f.kona.clone()); + board.start(); + r.f.settle(); + for w in [1, 2, 7] { + r.f.kona.bus_write(0xc10000, 4, w as u64); + } + retry(|| r.f.kona.bus_read(INT_CAUSE, 4).0 == crate::traits::BUS_ERR); + assert!(r.f.kona.failure().unwrap().contains("panicked")); + board.stop(); + assert_eq!(r.events(), ["attach", "stop"]); +} + +/// Packets from several threads at once all reach the controller, each as one +/// set of its vector's pending bit: the order across threads does not change +/// the pending bits, and a second packet for a set bit is another call. +#[test] +fn concurrent_packets_all_reach_the_controller() { + use super::super::front::InterruptPacket; + let f = Fixture::new(); + let host = f.kona.shared.host.clone(); + let packet = |vector: u8| InterruptPacket { widget: 9, address: 0x180_0090, vector }; + std::thread::scope(|s| { + for t in 0..4u8 { + let host = &host; + s.spawn(move || { + for _ in 0..100 { + // Two threads share vector 0x10; the others have their own. + host.deliver(packet(if t < 2 { 0x10 } else { 0x20 + t })); + } + }); + } + }); + let sent = f.host.sent(); + assert_eq!(sent.len(), 400); + assert!(sent.iter().all(|p| p.level), "the board never sends a clear"); + assert_eq!(sent.iter().filter(|p| p.vector == 0x10).count(), 200); + assert_eq!(sent.iter().filter(|p| p.vector == 0x22).count(), 100); + assert_eq!(sent.iter().filter(|p| p.vector == 0x23).count(), 100); + assert!(f.host.pending(0x10) && f.host.pending(0x22) && f.host.pending(0x23) && !f.host.pending(0x11)); + // The processor clearing a bit and a later packet setting it again are two interrupts. + f.host.clear(0x10); + assert!(!f.host.pending(0x10)); + host.deliver(packet(0x10)); + assert!(f.host.pending(0x10)); + assert_eq!(f.host.sent().len(), 401); +} + +/// A packet for a widget other than the host's is counted and not delivered. +#[test] +fn unrouted_packets_are_counted_and_not_delivered() { + use super::super::front::InterruptPacket; + use std::sync::atomic::Ordering; + let f = Fixture::new(); + let host = f.kona.shared.host.clone(); + host.deliver(InterruptPacket { widget: 3, address: 0x80, vector: 0x11 }); + host.deliver(InterruptPacket { widget: 4, address: 0x80, vector: 0x11 }); + assert!(f.host.sent().is_empty()); + assert_eq!(host.unrouted.load(Ordering::Relaxed), 2); + host.deliver(InterruptPacket { widget: 9, address: 0x80, vector: 0x11 }); + assert_eq!(f.host.sent().len(), 1); +} diff --git a/src/dev/kona/tests/host.rs b/src/dev/kona/tests/host.rs new file mode 100644 index 000000000..8bbb1e8f4 --- /dev/null +++ b/src/dev/kona/tests/host.rs @@ -0,0 +1,314 @@ +//! The fake machine of a test: a HEART-like pending-bit recorder as the +//! interrupt controller, and guest RAM on the bus. A replay's host is the same +//! machine with its memory served from a recording (`TestHost::replaying`). + +use super::*; +use crate::irq::InterruptController; +use crate::traits::{BusDevice, BusRead32, BusRead8}; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; +use std::sync::Arc; + +/// One `set_line(vector, level)` call, in order. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::dev::kona) struct Sent { + pub vector: u8, + pub level: bool, +} + +pub(in crate::dev::kona) struct TestHost { + log: Mutex>, + /// Pending bits, one per vector: a `true` call sets a bit, as HEART's + /// `set_vector` does; nothing here clears one but `clear`. + pending: Mutex<[bool; 256]>, + pub ram: Mutex>, + /// Addresses at and above this fail to read and write. + pub limit: u64, + /// Guest memory served from a recording instead of RAM: reads come from its + /// records, writes are kept, and neither fails. + recorded: Option, +} + +/// A recording's view of guest memory, for a replay: the reads the board is +/// expected to make, and the writes it made with the record index the replay was at. +pub(in crate::dev::kona) struct Recorded { + pub reads: Mutex, + pub writes: Mutex)>>, + at: Arc, +} + +pub(in crate::dev::kona) struct ReadRec { + pub src: u8, + pub addr: u64, + pub bytes: Vec, + /// Bytes at the front already served. + pub served: usize, +} + +#[derive(Default)] +pub(in crate::dev::kona) struct Reads { + pub recs: Vec, + /// The first record not yet wholly served. + pub cursor: usize, + /// Reads answered from a record. + pub matched: u64, + /// Reads answered from a record later than the first unserved one. + pub skipped_over: u64, + /// Bytes of reads no record covered, answered with zeros, with the first + /// few addresses. + pub unmatched: u64, + pub unmatched_at: Vec<(u64, usize)>, + /// The pull being served: its record, the bytes served and the address the + /// next read must have to continue it. + pub pull: Option<(usize, usize, u64)>, +} + + +impl Recorded { + /// Continue where `other` is in the recording: which reads are served, the cursor and the pull in + /// progress, so a board started part way through the stream is answered as the first one is. + pub fn continue_from(&self, other: &Recorded) { + let (mut mine, theirs) = (self.reads.lock(), other.reads.lock()); + for (m, t) in mine.recs.iter_mut().zip(&theirs.recs) { + m.served = t.served; + } + mine.cursor = theirs.cursor; + mine.pull = theirs.pull; + } + + /// Read `buf.len()` bytes at `addr` from the records. + pub fn read_into(&self, addr: u64, buf: &mut [u8]) { + let mut r = self.reads.lock(); + let mut done = 0; + while done < buf.len() { + let want = addr + done as u64; + // The first record from the cursor with unserved bytes at `want`. + let found = (r.cursor..r.recs.len()).find(|&i| { + let rec = &r.recs[i]; + // A pull's record (source 4) is keyed by its offset in the pool, not by an address. + rec.src != 4 && want >= rec.addr + rec.served as u64 && want < rec.addr + rec.bytes.len() as u64 + }); + let Some(i) = found else { + // A list pull reads the pool by offset, which the board turns into host + // addresses through its map RAM; the recording keeps the offset. Pulls + // happen in the order they were recorded, so serve the next one, from + // its first byte, as the reads that follow it continue. + let current = r.pull.filter(|&(i, used, next)| next == want && used < r.recs[i].bytes.len()); + let started = current.or_else(|| (0..r.recs.len()).find(|&i| r.recs[i].src == 4 && r.recs[i].served == 0).map(|i| (i, 0, want))); + if let Some((i, used, _)) = started { + let n = (r.recs[i].bytes.len() - used).min(buf.len() - done); + buf[done..done + n].copy_from_slice(&r.recs[i].bytes[used..used + n]); + r.recs[i].served = used + n; + r.pull = Some((i, used + n, want + n as u64)); + done += n; + continue; + } + r.unmatched += (buf.len() - done) as u64; + if r.unmatched_at.len() < 8 { + r.unmatched_at.push((want, buf.len() - done)); + } + buf[done..].fill(0); + return; + }; + if i > r.cursor { + r.skipped_over += 1; + } + let rec = &mut r.recs[i]; + let from = (want - rec.addr) as usize; + let n = (rec.bytes.len() - from).min(buf.len() - done); + buf[done..done + n].copy_from_slice(&rec.bytes[from..from + n]); + rec.served = from + n; + done += n; + while r.cursor < r.recs.len() && r.recs[r.cursor].served == r.recs[r.cursor].bytes.len() { + r.cursor += 1; + } + } + r.matched += 1; + } +} + + + +/// The configuration of a test board: its interrupt packets go to widget 9, where +/// the kernel's programming in the tests sends them. +pub(in crate::dev::kona) fn test_cfg() -> KonaConfig { + KonaConfig { host_widget: 9, ..KonaConfig::default() } +} + +/// A directory under the host's temp directory for a test to write files in, removed when this is +/// dropped, whether the test passed or panicked: a `remove_dir_all` at the end of a test is skipped +/// by the first failed assert, and a directory with a flash image in it is half a megabyte. +pub(in crate::dev::kona) struct ScratchDir(std::path::PathBuf); + +impl ScratchDir { + /// A new, empty directory named for `tag`, this process and a counter, so tests that run at + /// once do not share one. + pub fn new(tag: &str) -> ScratchDir { + static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let path = std::env::temp_dir().join(format!("iris-kona-{tag}-{}-{n}", std::process::id())); + let _ = std::fs::remove_dir_all(&path); + std::fs::create_dir_all(&path).unwrap(); + ScratchDir(path) + } + + pub fn path(&self) -> &std::path::Path { + &self.0 + } + + pub fn join(&self, name: &str) -> std::path::PathBuf { + self.0.join(name) + } +} + +impl Drop for ScratchDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// A test that panics takes its directory with it, which is the point of the guard. +#[test] +fn a_scratch_dir_is_removed_when_its_test_panics() { + let seen = Mutex::new(None); + let ended = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let dir = ScratchDir::new("panics"); + std::fs::write(dir.join("file"), b"left behind").unwrap(); + *seen.lock() = Some(dir.path().to_path_buf()); + panic!("a failed assert, made on purpose"); + })); + assert!(ended.is_err()); + let path = seen.lock().take().expect("the directory was made"); + assert!(!path.exists(), "{} outlived the panic", path.display()); +} + +pub(in crate::dev::kona) const RAM: usize = 16 << 20; + +impl TestHost { + pub fn new() -> Arc { + Arc::new(TestHost { log: Mutex::new(Vec::new()), pending: Mutex::new([false; 256]), ram: Mutex::new(vec![0; RAM]), limit: RAM as u64, recorded: None }) + } + + /// A host that serves the board's reads from `reads` (source, address, bytes), + /// in order, and keeps its writes with the index `at` held when it made them. + pub fn replaying(reads: Vec<(u8, u64, Vec)>, at: Arc) -> Arc { + let recs = reads.into_iter().map(|(src, addr, bytes)| ReadRec { src, addr, bytes, served: 0 }).collect(); + let recorded = Recorded { reads: Mutex::new(Reads { recs, ..Default::default() }), writes: Mutex::new(Vec::new()), at }; + Arc::new(TestHost { log: Mutex::new(Vec::new()), pending: Mutex::new([false; 256]), ram: Mutex::new(Vec::new()), limit: 0, recorded: Some(recorded) }) + } + + /// The recording's memory, for a host made by `replaying`. + pub fn recorded(&self) -> &Recorded { + self.recorded.as_ref().expect("a replaying host") + } + + pub fn word(&self, addr: u64) -> u32 { + let r = self.ram.lock(); + u32::from_be_bytes(r[addr as usize..addr as usize + 4].try_into().unwrap()) + } + + pub fn put(&self, addr: u64, bytes: &[u8]) { + self.ram.lock()[addr as usize..addr as usize + bytes.len()].copy_from_slice(bytes); + } + + pub fn put_words(&self, addr: u64, words: &[u32]) { + for (i, w) in words.iter().enumerate() { + self.put(addr + 4 * i as u64, &w.to_be_bytes()); + } + } + + pub fn bytes(&self, addr: u64, len: usize) -> Vec { + self.ram.lock()[addr as usize..addr as usize + len].to_vec() + } + + /// Every interrupt call, in order. + pub fn sent(&self) -> Vec { + self.log.lock().clone() + } + + /// Whether pending bit `vector` is set. + pub fn pending(&self, vector: u8) -> bool { + self.pending.lock()[vector as usize] + } + + /// The processor clearing a pending bit. + pub fn clear(&self, vector: u8) { + self.pending.lock()[vector as usize] = false; + } +} + +/// A register file and a stream state with no thread, over a fake machine: for a +/// test that calls the front end or a back end's port directly. +pub(in crate::dev::kona) fn bare(t: &Arc, cfg: KonaConfig) -> (super::super::front::Host, super::super::pipe::Core) { + let host = super::super::front::Host { + front: Mutex::new(super::super::front::Front::new(&cfg)), + irq: t.clone(), + mem: t.clone(), + unrouted: Default::default(), + held_words: Default::default(), + reasons_waiting: Default::default(), + list_running: Default::default(), + activity: Default::default(), + video_acks: Default::default(), + }; + (host, super::super::pipe::Core::new(cfg, super::super::flash::Flash::fresh())) +} + +impl InterruptController for TestHost { + fn set_line(&self, line: u32, level: bool) { + self.log.lock().push(Sent { vector: line as u8, level }); + if level { + self.pending.lock()[line as usize & 0xff] = true; + } + } +} + +impl BusDevice for TestHost { + fn read8(&self, addr: u64) -> BusRead8 { + if let Some(r) = &self.recorded { + let mut b = [0u8]; + r.read_into(addr, &mut b); + return BusRead8::ok(b[0]); + } + if addr >= self.limit { + return BusRead8::err(); + } + BusRead8::ok(self.ram.lock()[addr as usize]) + } + + fn write8(&self, addr: u64, val: u8) -> u32 { + if let Some(r) = &self.recorded { + r.writes.lock().push((r.at.load(Relaxed), addr, vec![val])); + return BUS_OK; + } + if addr >= self.limit { + return BUS_ERR; + } + self.ram.lock()[addr as usize] = val; + BUS_OK + } + + fn read32(&self, addr: u64) -> BusRead32 { + if let Some(r) = &self.recorded { + let mut b = [0u8; 4]; + r.read_into(addr, &mut b); + return BusRead32::ok(u32::from_be_bytes(b)); + } + if addr + 4 > self.limit { + return BusRead32::err(); + } + BusRead32::ok(u32::from_be_bytes(self.ram.lock()[addr as usize..addr as usize + 4].try_into().unwrap())) + } + + fn write32(&self, addr: u64, val: u32) -> u32 { + if let Some(r) = &self.recorded { + r.writes.lock().push((r.at.load(Relaxed), addr, val.to_be_bytes().to_vec())); + return BUS_OK; + } + if addr + 4 > self.limit { + return BUS_ERR; + } + self.ram.lock()[addr as usize..addr as usize + 4].copy_from_slice(&val.to_be_bytes()); + BUS_OK + } +} diff --git a/src/dev/kona/tests/mod.rs b/src/dev/kona/tests/mod.rs new file mode 100644 index 000000000..bf93e583c --- /dev/null +++ b/src/dev/kona/tests/mod.rs @@ -0,0 +1,222 @@ +//! Register-contract tests of the pipe's front end. +//! +//! The cases are ported from the QEMU model's qtests (bring-up, config, +//! EEPROM and DMA); a doc comment names the case a test comes from. They +//! reach the board the way a machine does, through `bus_read` and +//! `bus_write`, with the interrupt controller and memory played by `TestHost`. A +//! `Fixture` starts the board's thread, retries a store the bus answers busy and +//! waits for the thread to have run what it took (`settle`); the cases that +//! check start and stop use `TestBoard`. + +use super::*; +use crate::test_board::TestBoard; +use back::TestHook; + +/// What a test says of the back end it stands in: it records every job and answers as told. +#[derive(Default)] +pub(super) struct Recorder { + pub jobs: Arc>>, + /// Data words a command is to be followed by, by selector. + pub data: Vec<(u32, u32)>, + pub unclaimed: bool, + /// Reasons to post from the job that answers a command, by selector: + /// the token and its words. + pub reasons: Vec<(u32, u32, Vec)>, +} + +impl Recorder { + /// The test hook that records every job and answers as the test says, in place + /// of the real back end. + fn hook(self) -> TestHook { + let Recorder { jobs, data, unclaimed, reasons, .. } = self; + TestHook { + job: Box::new(move |port, job| { + let line = match job { + Job::Command { grammar, selector, args, packed, handled } => { + format!("{grammar:?} {selector:#x} {args:x?} packed={packed:#x} handled={handled}") + } + Job::Data { grammar, selector, args, words } => format!("data {grammar:?} {selector:#x} {args:x?} {words:x?}"), + Job::Bitmap { header, payload } => format!("bitmap {header:x?} {payload:x?}"), + Job::Video { command, data } => format!("video {command:#x} {data:x?}"), + Job::Retrace { tag } => format!("retrace {tag:#x}"), + Job::Context(e) => format!("context {e:?}"), + Job::Indirect { selector, value } => format!("indirect {selector:#x} {value:#x}"), + Job::DmaIn { io, physical, lines, per_line, pitch } => { + format!("dma-in {io:#x} {lines} {per_line:#x} {pitch:#x}{}", if *physical { " physical" } else { "" }) + } + Job::Dropped { word } => format!("dropped {word:#x}"), + Job::FirmwareStart { combination } => format!("firmware {combination:x?}"), + }; + jobs.lock().push(line); + match job { + Job::Retrace { tag } => port.retrace_done(*tag), + Job::Command { selector, handled: false, .. } => { + if let Some((_, token, words)) = reasons.iter().find(|(s, ..)| s == selector) { + port.post_reason_words(*token, words); + } + if let Some(&(_, n)) = data.iter().find(|(s, _)| s == selector) { + return Some(Reply::Data(n)); + } + } + Job::DmaIn { .. } if unclaimed => return Some(Reply::Unclaimed), + _ => {} + } + Some(Reply::Done) + }), + events: Arc::default(), + on_attach: None, + } + } +} + +/// The machine's status-bar inputs, idle. +pub(super) fn test_status() -> Arc { + crate::disp::StatusSource::new(Arc::default(), Arc::default()) +} + +pub(super) struct Fixture { + pub host: Arc, + pub kona: Arc, + pub jobs: Arc>>, +} + +impl Fixture { + /// An IR2 with a recording back end double, its thread started: for a test of + /// the job contract, or one that needs a back end to hold, panic or answer + /// in a way the real one does not. + pub fn hooked() -> Fixture { + Fixture::with(test_cfg(), Recorder::default()) + } + + /// An IR2 with the real back end, its thread started: for a test that asserts + /// on what the board did (guest memory, registers, interrupts, the store). + pub fn new() -> Fixture { + let host = TestHost::new(); + let kona = Kona::new(test_cfg(), host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + Fixture::of(host, kona) + } + + pub fn with(cfg: KonaConfig, rec: Recorder) -> Fixture { + let jobs = rec.jobs.clone(); + let mut f = Fixture::hooked_with(cfg, rec.hook()); + f.jobs = jobs; + f + } + + /// A board of `cfg` whose back end is `hook`, its thread started. Stores wait + /// in the ring until the thread starts; it stops with the board. + pub fn hooked_with(cfg: KonaConfig, hook: TestHook) -> Fixture { + let f = Fixture::idle(cfg, hook); + crate::traits::Device::start(&*f.kona); + f + } + + /// As `hooked_with`, not started: a test starts it, and the stores it made + /// wait in the ring until then. + pub fn idle(cfg: KonaConfig, hook: TestHook) -> Fixture { + let host = TestHost::new(); + let kona = Kona::new(cfg, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + kona.shared.pipe.lock().backend.hook = Some(hook); + Fixture { host, kona, jobs: Default::default() } + } + + /// A store of one word, retried while the bus answers busy: the + /// processor retries a refused store, so a test does. + pub fn w(&self, off: u64, val: u32) { + retry(|| self.kona.bus_write(off, 4, val as u64) == BUS_OK); + self.settle(); + } + + pub fn w64(&self, off: u64, val: u64) { + retry(|| self.kona.bus_write(off, 8, val) == BUS_OK); + self.settle(); + } + + /// A fixture over a board a test built: its thread started. + pub fn of(host: Arc, kona: Arc) -> Fixture { + crate::traits::Device::start(&*kona); + Fixture { host, kona, jobs: Default::default() } + } + + /// Wait for the thread to have run every store taken: the ring empty and the + /// pipe between batches. What a test reads in guest memory or from the back + /// end after a store is then what the store did. + pub fn settle(&self) { + self.kona.settle(); + } + + /// A load of one word, retried while busy. + pub fn r(&self, off: u64) -> u32 { + let mut v = 0; + retry(|| { + let (s, d) = self.kona.bus_read(off, 4); + v = d as u32; + s == BUS_OK + }); + v + } + + /// Words to the command port. + pub fn cmd(&self, words: &[u32]) { + for &w in words { + self.w(COMMAND_PORT, w); + } + } + + pub fn jobs(&self) -> Vec { + self.jobs.lock().clone() + } + + pub fn take_jobs(&self) -> Vec { + std::mem::take(&mut *self.jobs.lock()) + } + + /// Map RAM entry `i` to the 16 KiB host frame at `host_addr`. + pub fn map(&self, i: u64, host_addr: u64) { + self.w(0x100000 + 4 * i, 0x2000_0000 | (host_addr >> 12) as u32); + } + + /// The kernel's reason-page registration with a physical page. + pub fn reason_page(&self, addr: u64) { + self.cmd(&[0x1f1, 0x1000_0000 | (addr >> 12) as u32, (addr & 0xfff) as u32, 0x4000]); + } + + pub fn return_page(&self, addr: u64) { + self.cmd(&[0x1f2, 0x1000_0000 | (addr >> 12) as u32, (addr & 0xfff) as u32, 0x4000]); + } +} + +pub(super) const COMMAND_PORT: u64 = 0xc10000; +pub(super) const KICK: u64 = 0xc00108; + +/// Retry `f` until it succeeds. A bounded loop, not a wall-clock wait: a +/// store is refused only while the thread works its way through the ring. +pub(super) fn retry(mut f: impl FnMut() -> bool) { + for _ in 0..50_000_000u32 { + if f() { + return; + } + std::thread::yield_now(); + } + panic!("the bus kept answering busy"); +} + +/// A fixture with the thread running, freed with the test. +pub(super) fn threaded() -> (Fixture, TestBoard) { + let f = Fixture::hooked(); + let board = TestBoard::new(f.kona.clone()); + board.start(); + (f, board) +} + +mod regs; +mod host; +pub(super) use host::{bare, test_cfg, ScratchDir, Sent, TestHost}; + +mod bringup; +mod handle; +mod config; +mod products; +mod eeprom; +mod dma; +mod fifo; diff --git a/src/dev/kona/tests/products.rs b/src/dev/kona/tests/products.rs new file mode 100644 index 000000000..b7e2c066e --- /dev/null +++ b/src/dev/kona/tests/products.rs @@ -0,0 +1,154 @@ +//! What differs between the products the pipe can be: the board set and texture +//! size its configuration block reports, the boards its records name, the word the +//! driver's attach classes it by, and the chassis it fits. The InfiniteReality2's +//! block is checked byte for byte in `config`; these check the fields that move. + +use super::config::read_config; +use super::regs::{geometry_rom, Wire}; +use super::*; +use crate::dev::kona::ident::{Host, Product, PRODUCTS}; + +const INDIRECT_SELECT: u64 = 0x8003c0; +const INDIRECT_DATA: u64 = 0x8003d0; +const ONEWIRE: u64 = 0x98; + +fn pipe(product: Product, rms: u32) -> Fixture { + Fixture::with(KonaConfig::new(product, rms).unwrap(), Recorder::default()) +} + +fn word(block: &[u8], at: usize) -> u32 { + u32::from_be_bytes(block[at..at + 4].try_into().unwrap()) +} + +/// A right-justified text field of an identity record, trimmed. +fn field(block: &[u8], from: usize, to: usize) -> String { + String::from_utf8_lossy(&block[from..to]).trim().to_string() +} + +/// The first raster manager's identity record starts at 0x3e8, the first texture +/// board's at 0x564: part at +0x17, name at +0x3c. +fn rm_part_and_name(block: &[u8]) -> (String, String) { + (field(block, 0x3e8 + 0x17, 0x3e8 + 0x2a), field(block, 0x3e8 + 0x3c, 0x3e8 + 0x4a)) +} + +fn tm_part_and_name(block: &[u8]) -> (String, String) { + (field(block, 0x564 + 0x17, 0x564 + 0x2a), field(block, 0x564 + 0x3c, 0x564 + 0x4a)) +} + +#[test] +fn the_board_set_and_texture_size_follow_the_product() { + // (product, board set, texture size code, texture MiB) + for (product, set, code, mb) in [(Product::Ir2, 1, 2, 64), (Product::Ir2e, 4, 2, 64), (Product::Ir3, 5, 4, 256)] { + let block = read_config(&pipe(product, 2)); + let name = product.spec().name; + assert_eq!(word(&block, 0x00), set, "{name} board set"); + assert_eq!(word(&block, 0x04), 4, "{name} engines"); + assert_eq!(word(&block, 0x08), 2, "{name} raster managers"); + for rm in 0..2 { + assert_eq!((word(&block, 0x24 + 8 * rm), word(&block, 0x28 + 8 * rm)), (1, code), "{name} RM{rm}"); + } + // The texture pool counts 1 KiB granules. + assert_eq!(word(&block, 0xe4 + 0x18) as u64, mb << 10, "{name} texture pool"); + assert_eq!(KonaConfig::new(product, 1).unwrap().texture_bytes_per_rm(), mb << 20); + } +} + +#[test] +fn a_single_raster_manager_leaves_the_second_slot_empty() { + let block = read_config(&pipe(Product::Ir3, 1)); + assert_eq!((word(&block, 0x24), word(&block, 0x28)), (1, 4)); + assert_eq!((word(&block, 0x2c), word(&block, 0x30)), (0, 0)); + assert_eq!(word(&block, 0x58), 1); +} + +#[test] +fn the_records_name_each_products_boards() { + let ir2 = read_config(&pipe(Product::Ir2, 1)); + assert_eq!(rm_part_and_name(&ir2), ("030-1054-001".into(), "RM7".into())); + assert_eq!(tm_part_and_name(&ir2), ("030-1053-001".into(), "TM7".into())); + assert_eq!(word(&ir2, 0x114), 0x0302_0c06, "InfiniteReality2 raster board word"); + for product in [Product::Ir2e, Product::Ir3] { + let block = read_config(&pipe(product, 1)); + assert_eq!(rm_part_and_name(&block), ("030-1402-001".into(), "RM9".into()), "{product:?}"); + assert_eq!(word(&block, 0x100), 0x80f6_1806, "{product:?} geometry board word"); + // Every product presents the DG5-8 (see `ident::Display`). + assert_eq!(word(&block, 0x344), 0x51bf_1002, "{product:?} display word"); + } + // The 2E's raster slot is type 6 with texture code 2, as the 2's: type 0xb + // makes the pipe present as an InfiniteReality3 with 256 MB. + let ir2e = read_config(&pipe(Product::Ir2e, 1)); + assert_eq!(word(&ir2e, 0x114), 0x0302_0c06); + assert_eq!((word(&ir2e, 0x114) >> 9) & 0x7f, 6, "slot board type"); + assert_eq!(tm_part_and_name(&ir2e), ("030-1053-001".into(), "TM7".into())); + // The 2E's RM9 and TM7 revisions are unknown, so the records leave them blank. + assert_eq!(field(&ir2e, 0x3e8 + 0x32, 0x3e8 + 0x36), ""); + assert_eq!(field(&ir2e, 0x564 + 0x32, 0x564 + 0x36), ""); + // The 3's raster slot is type 0xb with code 4, and its 256 MB texture board + // is the TM8, 030-1588-001, revision unknown. + let ir3 = read_config(&pipe(Product::Ir3, 1)); + assert_eq!(word(&ir3, 0x114), 0x0304_1606); + assert_eq!((word(&ir3, 0x114) >> 9) & 0x7f, 0xb, "slot board type"); + assert_eq!(tm_part_and_name(&ir3), ("030-1588-001".into(), "TM8".into())); + assert_eq!(field(&ir3, 0x564 + 0x32, 0x564 + 0x36), ""); + // Board set 3 has no producer: no product emits it. + for row in &PRODUCTS { + assert_ne!(word(&read_config(&pipe(row.product, 1)), 0), 3, "{}", row.name); + } +} + +/// The attach names a pipe InfiniteReality2E when the geometry board's record +/// holds "GE16-", so the one-wire record is what makes the guest's `hinv` +/// differ between the 2 and the 2E. +#[test] +fn the_geometry_boards_one_wire_record_is_the_products() { + for (product, part, revision, name) in + [(Product::Ir2, "030-1129-002", "F", "GE14-4"), (Product::Ir2e, "030-1398-001", "E", "GE16-4"), (Product::Ir3, "030-1398-001", "E", "GE16-4")] + { + let f = pipe(product, 1); + let w = Wire::new(&f, ONEWIRE); + assert!(w.match_rom(&geometry_rom())); + let (_, mem) = w.read_memory(0, 64); + let text = |a: usize, n: usize| String::from_utf8(mem[a..a + n].to_vec()).unwrap(); + assert_eq!(format!("{}{}", text(21, 9), text(35, 3)), part, "{product:?}"); + assert_eq!(text(38, revision.len()), revision, "{product:?}"); + assert_eq!(text(48, name.len()), name, "{product:?}"); + } +} + +/// After selecting 0x8800 the attach reads the product in the low two bits: +/// 1 classes the pipe InfiniteReality3. After 0xb00 bit 10 stays clear for all. +#[test] +fn the_class_words_follow_the_product() { + for (product, low) in [(Product::Ir2, 0), (Product::Ir2e, 0), (Product::Ir3, 1)] { + let f = pipe(product, 1); + f.w(INDIRECT_SELECT, 0x8800); + assert_eq!(f.r(INDIRECT_DATA) & 3, low, "{product:?}"); + f.w(INDIRECT_SELECT, 0xb00); + assert_eq!(f.r(INDIRECT_DATA) & 0x400, 0, "{product:?}"); + } +} + +#[test] +fn a_chassis_the_product_did_not_ship_in_is_refused_with_the_reason() { + let spec = |p: Product| p.spec(); + assert!(spec(Product::Ir2).check_host(Host::Onyx2Deskside).is_ok()); + assert!(spec(Product::Ir2).check_host(Host::Onyx2Rack).is_err()); + assert!(spec(Product::Ir2e).check_host(Host::Onyx2Deskside).is_ok()); + assert!(spec(Product::Ir2e).check_host(Host::Onyx2Rack).is_ok()); + assert!(spec(Product::Ir3).check_host(Host::Onyx2Rack).is_ok()); + assert!(spec(Product::Ir3).check_host(Host::Onyx3000).is_ok()); + let e = spec(Product::Ir3).check_host(Host::Onyx2Deskside).unwrap_err(); + assert!(e.contains("InfiniteReality3") && e.contains("Onyx2 deskside") && e.contains("Onyx2 rack"), "{e}"); + for row in &PRODUCTS { + assert!(!row.hosts.is_empty(), "{}", row.name); + } +} + +#[test] +fn the_deskside_raster_manager_counts_hold_for_every_product() { + for row in &PRODUCTS { + assert!(KonaConfig::new(row.product, 2).is_ok()); + let e = KonaConfig::new(row.product, 4).unwrap_err(); + assert!(e.contains("one or two raster managers"), "{}: {e}", row.name); + } +} diff --git a/src/dev/kona/tests/regs.rs b/src/dev/kona/tests/regs.rs new file mode 100644 index 000000000..d4dca0c04 --- /dev/null +++ b/src/dev/kona/tests/regs.rs @@ -0,0 +1,591 @@ +//! Identity, plain-storage and map RAM registers, and the textport's mailbox +//! protocol. Ported from tests/qtest/sn0-origin2000-kona-test.c. + +use super::*; +use crate::dev::w1::crc::crc8; +use crate::dev::w1::sgi::BoardRecord; + +const WIDGET_ID: u64 = 0x04; +const WIDGET_CONTROL: u64 = 0x24; +const PROBE: u64 = 0x64; +const ONEWIRE: u64 = 0x98; +const NIC: u64 = 0x9c; +const LOW_WINDOW: u64 = 0x7c; +const CREDIT: u64 = 0x800208; +const CREDIT_CONFIG: u64 = 0x800210; +const RESET_ARG: u64 = 0x800220; +const INT_ENABLE: u64 = 0x800230; +const INT_CAUSE: u64 = 0x800238; +const MAILBOX: u64 = 0x800240; +const RESET_STROBE: u64 = 0x800300; +const EEPROM_ENABLE: u64 = 0x800308; +const PORT_ADDR: u64 = 0x8003e8; +const PORT_DATA: u64 = 0x8003f0; +const DMA_FIFO_PUSH: u64 = 0x80007c; +const MAPRAM: u64 = 0x100000; +const MAPRAM_ENTRIES: u64 = 0x40000; +const CAUSE_MAILBOX: u32 = 0x10; + +/// The 1-wire protocol the way Linux's sgi_w1.c drives the register: +/// `pulse << 10 | sample << 2`, one slot per register write, the sampled wire +/// in bit 0 of the register once DONE (bit 1) is set. +pub(super) struct Wire<'a> { + f: &'a Fixture, + reg: u64, +} + +impl<'a> Wire<'a> { + pub fn new(f: &'a Fixture, reg: u64) -> Self { + Wire { f, reg } + } + + fn slot(&self, pulse: u32, sample: u32) -> bool { + self.f.w(self.reg, pulse << 10 | sample << 2); + let r = self.f.r(self.reg); + assert_eq!(r & 2, 2, "the slot completes in the write that started it"); + r & 1 != 0 + } + + /// A reset: true if a part answers presence (the wire reads low). + pub fn reset(&self) -> bool { + !self.slot(520, 65) + } + + fn bit(&self, b: bool) -> bool { + if b { self.slot(6, 13) } else { self.slot(80, 30) } + } + + pub fn byte(&self, b: u8) -> u8 { + (0..8).fold(0, |acc, i| acc | (self.bit(b >> i & 1 != 0) as u8) << i) + } + + /// Match ROM on `rom`: true if a part answered the reset. + pub fn match_rom(&self, rom: &[u8; 8]) -> bool { + let present = self.reset(); + self.byte(0x55); + for b in rom { + self.byte(*b); + } + present + } + + /// Read Memory from `addr`, `len` bytes after the address CRC. + pub fn read_memory(&self, addr: u8, len: usize) -> (u8, Vec) { + self.byte(0xf0); + self.byte(addr); + self.byte(0); + let crc = self.byte(0xff); + (crc, (0..len).map(|_| self.byte(0xff)).collect()) + } + + /// The standard search: every ROM on the wire. + pub fn search(&self) -> Vec<[u8; 8]> { + let mut roms = Vec::new(); + let mut last_disc: i32 = -1; + let mut rom = [0u8; 8]; + loop { + assert!(self.reset()); + self.byte(0xf0); + let mut disc: i32 = -1; + for i in 0..64 { + let b = self.bit(true); + let nb = self.bit(true); + let choose = match (b, nb) { + (true, true) => panic!("no part answered bit {i}"), + (false, false) => { + if (i as i32) == last_disc { + true + } else if (i as i32) > last_disc { + disc = i as i32; + false + } else { + rom[i / 8] >> (i % 8) & 1 != 0 + } + } + (b, _) => b, + }; + if choose { rom[i / 8] |= 1 << (i % 8) } else { rom[i / 8] &= !(1 << (i % 8)) } + self.bit(choose); + } + roms.push(rom); + if disc < 0 { + return roms; + } + last_disc = disc; + } + } +} + +/// The ROM of the geometry board's part: family 0x09, the synthetic serial +/// marker 'G' 0, then the CRC-8 of the first seven bytes. +pub(super) fn geometry_rom() -> [u8; 8] { + let mut rom = [0x09, 0x00, 0x47, 0x55, 0x4d, 0x45, 0x51, 0]; + rom[7] = crc8(&rom[..7]); + rom +} + +fn second_rom() -> [u8; 8] { + let mut rom = [0x09, 0x01, 0x47, 0x55, 0x4d, 0x45, 0x51, 0]; + rom[7] = crc8(&rom[..7]); + rom +} + +/// The widget id register reads the presence +/// bit, the manufacturer, the part and the revision; the aperture ends at +/// 16 MiB; the registers are word-sized. +#[test] +fn identity_and_aperture() { + let f = Fixture::new(); + let id = f.r(WIDGET_ID); + assert_eq!(id & 1, 1); + assert_eq!((id >> 1) & 0x7ff, 0x2aa); + assert_eq!((id >> 12) & 0xffff, 0xc102); + assert_eq!(id >> 28, 0); + assert_eq!(f.kona.bus_read(APERTURE, 4).0, BUS_ERR); + assert_eq!(f.kona.bus_write(APERTURE, 4, 0), BUS_ERR); + assert_eq!(f.kona.bus_read(WIDGET_ID, 2).0, BUS_ERR); + assert_eq!(f.kona.bus_read(WIDGET_ID, 1).0, BUS_ERR); + assert_eq!(f.kona.bus_write(WIDGET_CONTROL, 1, 0), BUS_ERR); + // The last word of the aperture is inside it. + assert_eq!(f.kona.bus_read(APERTURE - 4, 4).0, BUS_OK); +} + +/// Widget control is plain storage. +#[test] +fn widget_control_is_plain_storage() { + let f = Fixture::new(); + assert_eq!(f.r(WIDGET_CONTROL), 9 << 22, "the test board's host widget"); + f.w(WIDGET_CONTROL, 0x0003_f00f); + assert_eq!(f.r(WIDGET_CONTROL), 0x0003_f00f); +} + +/// Both the board-type check and +/// the driver's presence check require the field to read 0, and its bits +/// 5:3 give the map RAM its largest size. +#[test] +fn probe_reads_documented_reset_state() { + let f = Fixture::new(); + assert_eq!(f.r(PROBE), 0); + f.w(PROBE, 0x3f); + assert_eq!(f.r(PROBE), 0, "the probe register is read-only"); +} + +/// An offset no register +/// owns reads zero and a write to it does not persist. +#[test] +fn unknown_offset_reads_zero_and_accepts_writes() { + let f = Fixture::new(); + f.w(0x6c, 0x1234); + assert_eq!(f.r(0x6c), 0); + f.w(0x800390, 0); + assert_eq!(f.r(0x800390), 0); +} + +/// The register reads zero at rest, +/// the value both the PROM's (exactly zero) and the driver's (not above zero) +/// waits proceed on. +#[test] +fn credit_register_reads_zero_at_rest() { + let f = Fixture::new(); + assert_eq!(f.r(CREDIT), 0); +} + +/// The register is a fill level: the words +/// the command-port framers hold of a packet still arriving, rounded up to two +/// (bit 0 never sets). A kernel 0x1f1 with its token and one of three words +/// holds 2, a textport command with its selector and two of four words holds 3 +/// and reads 4, a DMA packet with its selector and three of six words on the +/// push register holds 4. Each drops to zero once its last word arrives. A +/// model that read a fixed zero fails the first mid-packet read. A single +/// held word reads 2: the level rounds up to two and saturates at 0x7fe +/// (`a_full_ring_answers_busy_never_spins` checks a full ring reads 0x7fe). +#[test] +fn credit_follows_consumption() { + let f = Fixture::new(); + f.cmd(&[0x1f1]); + assert_eq!(f.r(CREDIT), 2, "one held word rounds up to two"); + f.cmd(&[0x1000_0000]); + assert_eq!(f.r(CREDIT), 2); + f.cmd(&[0, 0x4000]); + assert_eq!(f.r(CREDIT), 0); + + f.cmd(&[1, 3, 7, 1]); + assert_eq!(f.r(CREDIT), 4); + f.cmd(&[2, 3]); + assert_eq!(f.r(CREDIT), 0); + + for w in [0x1eb, 1, 0x9000, 4] { + f.w(DMA_FIFO_PUSH, w); + } + assert_eq!(f.r(CREDIT), 4); + assert_eq!(f.r(CREDIT) & 1, 0, "bit 0 is never set"); +} + +/// The driver's one write of 0x700 is kept and +/// read back and changes nothing the credit register says. +#[test] +fn credit_config_is_stored_and_changes_nothing() { + let f = Fixture::new(); + assert_eq!(f.r(CREDIT_CONFIG), 0); + f.w(CREDIT_CONFIG, 0x700); + assert_eq!(f.r(CREDIT_CONFIG), 0x700); + assert_eq!(f.r(CREDIT), 0); + f.cmd(&[0x1f1, 0x1000_0000]); + assert_eq!(f.r(CREDIT), 2); +} + +/// Low window write is plain storage. +#[test] +fn low_window_write_is_plain_storage() { + let f = Fixture::new(); + f.w(LOW_WINDOW, 0x23); + assert_eq!(f.r(LOW_WINDOW), 0x23); +} + +/// Reset and eeprom control registers are plain storage. +#[test] +fn reset_and_eeprom_control_registers_are_plain_storage() { + let f = Fixture::new(); + for (reg, v) in [(RESET_ARG, 0x8000), (EEPROM_ENABLE, 1), (RESET_STROBE, 2)] { + f.w(reg, v); + assert_eq!(f.r(reg), v); + } + // Software never reads the enable register: it holds the bits written, + // within the status register's twelve. + f.w(INT_ENABLE, 0xf7c); + assert_eq!(f.r(INT_ENABLE), 0xf7c); + f.w(INT_ENABLE, 0xffff_ffff); + assert_eq!(f.r(INT_ENABLE), 0xfff); +} + +/// The PROM and +/// the driver read the flash header's state word through the indirect port; +/// every other address in the backed range reads zero. +#[test] +fn eeprom_data_answers_checked_address_and_zero_elsewhere() { + let f = Fixture::new(); + f.w(PORT_ADDR, 0x87ff1c); + assert_eq!(f.r(PORT_DATA), 2); + f.w(PORT_ADDR, 0x87ff00); + assert_eq!(f.r(PORT_DATA), 0); + f.w(PORT_ADDR, 0x87ff20); + assert_eq!(f.r(PORT_DATA), 0); +} + +/// "1,9,0" on the wire: the +/// mailbox's single reply word drains in one step with nothing pending behind +/// it, then the cause bit stays clear. +#[test] +fn textport_sync_probe_mailbox_protocol() { + let f = Fixture::new(); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, CAUSE_MAILBOX); + assert_eq!(f.r(MAILBOX), 0x1234abcd); + f.w(MAILBOX, 0); + f.w(INT_CAUSE, CAUSE_MAILBOX); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); + assert_eq!(f.r(MAILBOX), 0); + // The textport is in step for the next command. + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); +} + +/// "1,0xc,0" replies five +/// words, width and height (established) at indices 3 and 4. The cause bit +/// re-arms synchronously behind each of the first four drains and clears only +/// after the fifth. +#[test] +fn textport_graphics_info_mailbox_protocol() { + let f = Fixture::new(); + f.cmd(&[1, 0xc, 0]); + let mut got = Vec::new(); + for i in 0..5 { + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, CAUSE_MAILBOX, "word {i} is ready"); + got.push(f.r(MAILBOX)); + f.w(MAILBOX, 0); + f.w(INT_CAUSE, CAUSE_MAILBOX); + } + assert_eq!(got, [0, 0, 0, 1280, 1024]); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); +} + +/// A selector outside the +/// textport's table is dropped and the port expects a sync word again, so a +/// probe completing right after shows it stayed in step. +#[test] +fn textport_unknown_selector_resynchronizes() { + let f = Fixture::new(); + f.cmd(&[1, 0x77]); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); +} + +/// A command argument +/// equal to the bitmap selector (0xa) mid-packet is not a fresh blit start: +/// command 3's own r/g/b arguments routinely carry it. +#[test] +fn bitmap_value_mid_packet_does_not_start_a_blit() { + let f = Fixture::hooked(); + f.cmd(&[1, 3, 0xa, 0xa, 0xa, 0xa]); + assert_eq!(f.jobs(), ["Textport 0x3 [a, a, a, a] packed=0x0 handled=false"]); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); +} + +/// , up to the seam: six header +/// words and a packed 1-bpp payload of ceil(width * height / 32) words reach +/// the back end whole, and the port is in step afterwards. +#[test] +fn textport_bitmap_blit_reaches_the_back_end() { + let f = Fixture::hooked(); + // 4x2 bits: one payload word. + f.cmd(&[1, 0xa, 4, 2, 0, 0, 5, 0, 0xa500_0000]); + assert_eq!(f.jobs(), ["bitmap [4, 2, 0, 0, 5, 0] [a5000000]"]); + // 33x1 bits: two payload words. + f.take_jobs(); + f.cmd(&[1, 0xa, 33, 1, 0, 0, 0, 0, 0xffff_ffff, 0x8000_0000]); + assert_eq!(f.jobs(), ["bitmap [21, 1, 0, 0, 0, 0] [ffffffff, 80000000]"]); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); +} + +/// A blit of no bits takes no payload. +#[test] +fn an_empty_bitmap_blit_takes_no_payload() { + let f = Fixture::hooked(); + f.cmd(&[1, 0xa, 0, 7, 0, 0, 0, 0]); + f.cmd(&[1, 9, 0]); + assert_eq!(f.r(MAILBOX), 0x1234abcd); + assert!(f.jobs().is_empty()); +} + +/// Command 8 is accepted and +/// handed to the back end; the size graphics info reports stays fixed. +#[test] +fn textport_screen_size_is_a_job_only() { + let f = Fixture::hooked(); + f.cmd(&[1, 8, 640, 480]); + assert_eq!(f.jobs(), ["Textport 0x8 [280, 1e0] packed=0x0 handled=false"]); + f.cmd(&[1, 0xc, 0]); + for _ in 0..3 { + f.w(MAILBOX, 0); + f.w(INT_CAUSE, CAUSE_MAILBOX); + } + assert_eq!(f.r(MAILBOX), 1280); +} + +/// Selectors 1, 4 +/// and 7 are framed and handed on; the front end answers none of them. +#[test] +fn reserved_textport_selectors_are_framed_and_handed_on() { + let f = Fixture::hooked(); + f.cmd(&[1, 1, 0, 1, 4, 100, 200, 1, 7, 1, 2]); + assert_eq!(f.jobs().len(), 3); + assert_eq!(f.r(INT_CAUSE) & CAUSE_MAILBOX, 0); +} + +/// The driver's map RAM probe: write +/// 0x10000000 | i, read back, expect an exact match, over a sample plus the +/// first and last entry; entry 1 is re-checked at the end so a stride bug +/// cannot hide behind the entry written last. +#[test] +fn mapram_stride_sample() { + let f = Fixture::new(); + let sample = [0, 1, 2, 3, 7, 255, 256, 4095, 4096, 65535, 65536, MAPRAM_ENTRIES - 1]; + for i in sample { + f.w(MAPRAM + 4 * i, 0x1000_0000 | i as u32); + assert_eq!(f.r(MAPRAM + 4 * i), 0x1000_0000 | i as u32, "entry {i}"); + } + assert_eq!(f.r(MAPRAM + 4), 0x1000_0001); +} + +/// An entry's physical width is unknown; +/// the model stores all of it. +#[test] +fn mapram_stores_full_32_bits() { + let f = Fixture::new(); + f.w(MAPRAM + 8, 0xffff_ffff); + assert_eq!(f.r(MAPRAM + 8), 0xffff_ffff); + f.w(MAPRAM + 8, 0x2000_0000 | (0x1234 << 2)); + assert_eq!(f.r(MAPRAM + 8), 0x2000_48d0); +} + +/// The first offset past +/// the window reads zero and keeps no write; the table's last entry takes one. +#[test] +fn mapram_bounds_fall_through_to_unimplemented() { + let f = Fixture::new(); + f.w(MAPRAM + 4 * MAPRAM_ENTRIES, 0x55); + assert_eq!(f.r(MAPRAM + 4 * MAPRAM_ENTRIES), 0); + f.w(MAPRAM + 4 * (MAPRAM_ENTRIES - 1), 0x77); + assert_eq!(f.r(MAPRAM + 4 * (MAPRAM_ENTRIES - 1)), 0x77); + assert_eq!(f.r(MAPRAM - 4), 0); +} + +/// Mapram reset clears table. +#[test] +fn mapram_reset_clears_the_table() { + let f = Fixture::new(); + f.w(MAPRAM, 0x1234); + f.w(MAPRAM + 4 * 100, 0x5678); + f.kona.power_on(); + assert_eq!(f.r(MAPRAM), 0); + assert_eq!(f.r(MAPRAM + 4 * 100), 0); +} + +/// A nonzero size code in the +/// probe register's bits 5:3 shrinks the active table: N = 1 selects 1 << 15 +/// entries. The last entry below the count stores; the first at the count +/// reads zero and keeps no write. +#[test] +fn mapram_size_code_limits_entry_count() { + let host = TestHost::new(); + let kona = Kona::new(KonaConfig { probe: 1 << 3, ..test_cfg() }, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + let f = Fixture::of(host, kona); + assert_eq!(f.r(PROBE), 1 << 3); + f.w(MAPRAM + 4 * ((1 << 15) - 1), 0xabcd); + assert_eq!(f.r(MAPRAM + 4 * ((1 << 15) - 1)), 0xabcd); + f.w(MAPRAM + 4 * (1 << 15), 0xabcd); + assert_eq!(f.r(MAPRAM + 4 * (1 << 15)), 0); +} + +/// Mapram ordinary mapping write read back. +#[test] +fn mapram_ordinary_mapping_round_trips() { + let f = Fixture::new(); + let paddr: u64 = 0x0123_4000; + let entry = 0x2000_0000 | ((paddr >> 14) << 2) as u32; + f.w(MAPRAM + 4 * 9, entry); + assert_eq!(f.r(MAPRAM + 4 * 9), entry); +} + +/// The driver's attach-time probe over the +/// whole table: write 0x10000000 | i to every entry, then read every entry +/// back and expect an exact match. +#[test] +fn mapram_full_window_sweep() { + let f = Fixture::new(); + for i in 0..MAPRAM_ENTRIES { + f.w(MAPRAM + 4 * i, 0x1000_0000 | i as u32); + } + for i in 0..MAPRAM_ENTRIES { + assert_eq!(f.r(MAPRAM + 4 * i), 0x1000_0000 | i as u32, "entry {i}"); + } +} + +fn geometry_image() -> [u8; 64] { + BoardRecord { barcode: "EMUG00", part: "030-1129-002", revision: "F", name: "GE14-4" }.image() +} + +/// , with the deskside's record. Match ROM on +/// the geometry board's part, then Read Memory returns its barcode, part +/// number, revision and name. +#[test] +fn onewire_geometry_identity() { + let f = Fixture::new(); + let w = Wire::new(&f, ONEWIRE); + assert!(w.match_rom(&geometry_rom()), "the part answers presence"); + let (crc, mem) = w.read_memory(0, 64); + assert_eq!(crc, crc8(&[0xf0, 0, 0])); + assert_eq!(&mem[..], &geometry_image()[..]); + let text = |a: usize, n: usize| String::from_utf8(mem[a..a + n].to_vec()).unwrap(); + assert_eq!(text(1, 6), "EMUG00"); + assert_eq!(text(21, 9), "030-1129-"); + assert_eq!(text(35, 3), "002"); + assert_eq!(text(38, 1), "F"); + assert_eq!(text(48, 6), "GE14-4"); +} + +/// A deskside pipe has no second +/// host-interface record: Match ROM against its identity selects no part, and +/// a search finds the geometry board's record alone. +#[test] +fn onewire_has_no_second_record() { + let f = Fixture::new(); + let w = Wire::new(&f, ONEWIRE); + w.match_rom(&second_rom()); + let (_, mem) = w.read_memory(0, 8); + assert_eq!(mem, [0xff; 8], "no part answers a Read Memory after a Match ROM nothing matched"); + assert_eq!(w.search(), vec![geometry_rom()]); +} + +/// +0x9c is the kernel +/// driver's offset onto the identical chain: a slot issued through one offset +/// reads back through the other with no further pulse, and the same records +/// answer through either. +#[test] +fn nic_register_mirrors_the_onewire_register() { + let f = Fixture::new(); + f.w(ONEWIRE, 520 << 10 | 65 << 2); + assert_eq!(f.r(NIC), f.r(ONEWIRE)); + assert_eq!(f.r(NIC) & 3, 2, "done, wire low: presence"); + f.w(NIC, 6 << 10 | 13 << 2); + assert_eq!(f.r(ONEWIRE), f.r(NIC)); + let w = Wire::new(&f, NIC); + assert!(w.match_rom(&geometry_rom())); + assert_eq!(w.read_memory(0, 64).1, geometry_image()); +} + +/// Mutated widget control and a one-wire +/// transaction left mid-slot return to zero on reset. +#[test] +fn reset_clears_plain_storage() { + let f = Fixture::hooked(); + f.w(WIDGET_CONTROL, 0xffff); + f.w(ONEWIRE, 520 << 10 | 65 << 2); + f.w(LOW_WINDOW, 0x23); + f.w(RESET_ARG, 0x8000); + f.w(0x34, 0x98000); + f.w(0x3c, 0x1800090); + f.w(0x5c, 0x3e2210); + f.kona.power_on(); + for reg in [WIDGET_CONTROL, ONEWIRE, LOW_WINDOW, RESET_ARG, 0x34, 0x3c, 0x5c, INT_CAUSE, MAILBOX] { + // Widget control resets to the host widget (9 on a test board). + assert_eq!(f.r(reg), if reg == WIDGET_CONTROL { 9 << 22 } else { 0 }, "register {reg:#x}"); + } + // The board's identity survives. + assert!(Wire::new(&f, ONEWIRE).match_rom(&geometry_rom())); +} + +/// The kernel driver's +/// own initialisation: strobes the reset register (1, read, 0, read), then +/// writes 0x100 to the enable and 0x8000 to the reset argument. The strobe +/// reads back what was written. +#[test] +fn the_drivers_initialisation_sequence_is_accepted() { + let f = Fixture::new(); + f.w(RESET_STROBE, 1); + assert_eq!(f.r(RESET_STROBE), 1); + f.w(RESET_STROBE, 0); + assert_eq!(f.r(RESET_STROBE), 0); + f.w(INT_ENABLE, 0x100); + f.w(RESET_ARG, 0x8000); + assert_eq!(f.r(RESET_ARG), 0x8000); +} + +/// The widget a pipe's DMA goes to is a field of the widget control register +/// (bits 25:22), which host software fills with its interrupt target. +#[test] +fn the_dma_widget_is_a_field_of_widget_control() { + let f = Fixture::new(); + assert_eq!(f.kona.dma_widget(), 9); + f.w(WIDGET_CONTROL, 3 << 22 | 0xb); + assert_eq!(f.kona.dma_widget(), 3); +} + +/// The widget control register resets to the host widget in bits 25:22, so a +/// pipe's DMA has a destination before software programs it (the model's +/// choice; the power-on value is unknown). +#[test] +fn widget_control_resets_to_the_host_widget() { + let host = TestHost::new(); + let cfg = KonaConfig::default().with_host_widget(9).unwrap(); + let kona = Kona::new(cfg, host.clone(), host.clone(), crate::dev::kona::tests::test_status(), None).unwrap(); + let f = Fixture::of(host, kona); + assert_eq!(f.r(WIDGET_CONTROL), 9 << 22); + assert_eq!(f.kona.dma_widget(), 9); + f.w(WIDGET_CONTROL, 0x3 << 22); + assert_eq!(f.kona.dma_widget(), 3); + f.kona.power_on(); + assert_eq!(f.r(WIDGET_CONTROL), 9 << 22); +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index db68fe591..527324261 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -37,6 +37,7 @@ pub mod ns16550; pub mod rad1; pub mod mgras; pub mod odyssey; +pub mod kona; pub mod ng1; pub mod serial_host; pub mod serial_log; diff --git a/src/devlog.rs b/src/devlog.rs index f81cb60eb..7e8bbac9d 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -76,6 +76,9 @@ pub enum LogModule { /// configuration NVRAM and its I2C controller: protocol surprises and /// refused commands. Module = 37, + /// The InfiniteReality (KONA) graphics pipe's host interface: guest + /// accesses and command words the model does not support. + Kona = 38, } // Mask bits shared by L1i / L1d / L2c @@ -90,7 +93,7 @@ pub const NVME_LOG_REGS: u32 = 0x4; // every register read and write below the pub const NVME_LOG_IO: u32 = 0x8; // I/O doorbells, commands and completions impl LogModule { - pub const COUNT: usize = 38; + pub const COUNT: usize = 39; pub fn from_str(s: &str) -> Option { match s { @@ -132,6 +135,7 @@ impl LogModule { "l1" => Some(Self::L1), "usb" => Some(Self::Usb), "module" => Some(Self::Module), + "kona" => Some(Self::Kona), _ => None, } } @@ -176,6 +180,7 @@ impl LogModule { Self::L1 => "l1", Self::Usb => "usb", Self::Module => "module", + Self::Kona => "kona", } } @@ -188,7 +193,7 @@ impl LogModule { Self::L1i, Self::L1d, Self::L2c, Self::Ultra, Self::Scsi1, Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, Self::Bedrock, Self::Hub, Self::Pci, Self::Bridge, Self::Heart, Self::Nvme, - Self::L1, Self::Usb, Self::Module, + Self::L1, Self::Usb, Self::Module, Self::Kona, ] } @@ -356,6 +361,7 @@ impl DevLog { ModuleLog::new(), // L1 ModuleLog::new(), // Usb ModuleLog::new(), // Module + ModuleLog::new(), // Kona ], } } diff --git a/src/ip27_deskside.rs b/src/ip27_deskside.rs index ed982f1d3..ccd452636 100644 --- a/src/ip27_deskside.rs +++ b/src/ip27_deskside.rs @@ -38,10 +38,13 @@ //! - Origin 2000: node 2 has no I/O of its own, since a deskside's second //! Crossbow carries the other six XIO slots, none of them fitted here. The //! Bridge reaches only the Hub that owns its Crossbow, node 1's. -//! - Onyx2: the graphics slot holds a test widget that answers its -//! identification and nothing else, not an InfiniteReality pipe. Either -//! Hub's processor reaches the BaseIO-G, and the Bridge takes DMA and -//! interrupts to the Hub whose widget the guest programs into it, so the +//! - Onyx2: the graphics slot holds an InfiniteReality2 or 2E pipe +//! (`[graphics] board = "ir2"`, `"ir2:2rm"`, `"ir2e"` or `"ir2e:2rm"`), or +//! with `none` a test widget that answers its identification and nothing +//! else. The pipe's DMA and interrupt packets +//! reach the Hub at the widget the guest programs into it, as the Bridge's +//! do. Either Hub's processor reaches the BaseIO-G, and the Bridge takes DMA +//! and interrupts to the Hub whose widget the guest programs into it, so the //! board the stock PROM makes the global master receives them. //! - Cached accesses to node numbers 1 and up route through the Hub, and a //! number no Hub holds fails. A node that holds a nonzero number sends node @@ -53,9 +56,11 @@ use crate::config::{MachineConfig, MachineProfile}; use crate::dev::bridge::Bridge; use crate::dev::elsc::ModuleOutput; use crate::dev::elsc_nvram::ElscNvram; +use crate::devlog::LogModule; use crate::dev::hub::{Hub, HubConfig, Space}; use crate::dev::hub::SlotSignals; use crate::dev::ioc3::{ByteBus, Ioc3, Ioc3Board}; +use crate::dev::kona::{Kona, KonaConfig}; use crate::dev::rad1::Rad1; use crate::dev::node_flash::FlashValues; use crate::dev::o2000_module::{ModuleConfig, Origin2000Module}; @@ -64,9 +69,12 @@ use crate::dev::w1::sgi::{board_record_part, BoardRecord}; use crate::dev::xbow::{widget_id, Xbow}; use crate::pci::DevFn; use crate::ip27_machine::{cpu_module, fit_io_prom, pci_devices, product, PciPopulation, section, table, BoardIo, BoardSpec, Ip27Board, Ip27Machine, ModuleController, Node, NodeRecord, Unmodelled}; -use crate::traits::{BusDevice, Resettable}; +use crate::irq::InterruptController; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Resettable, BUS_ERR}; +use crate::xtalk::Upstream; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::sync::{Arc, OnceLock, Weak}; use std::time::Duration; pub type DesksideMachine = Ip27Machine; @@ -170,6 +178,50 @@ pub static ONYX2: DesksideLayout = DesksideLayout { /// The test widget's identification: not a real part. const TEST_WIDGET_ID: u32 = widget_id(0, 0x7e57, 0x36); +/// Where the pipe's interrupt packets and DMA go: the host at the widget the +/// guest has programmed into the pipe, behind that widget's Crossbow link. +/// Nothing is delivered while the widget names no host with a live link. +struct PipeHosts { + pipe: OnceLock>, + up: Upstream, +} + +impl PipeHosts { + fn host(&self) -> Option<&crate::xtalk::HostPort> { + let widget = self.pipe.get()?.upgrade()?.dma_widget() as u8; + let host = self.up.get(widget).filter(|h| h.link_up()); + if host.is_none() { + crate::dlog!(LogModule::Kona, "Onyx2: the pipe names widget {widget}, which is no Hub with a live link"); + } + host + } +} + +impl InterruptController for PipeHosts { + fn set_line(&self, line: u32, level: bool) { + if let Some(h) = self.host() { + h.irq.set_line(line, level); + } + } +} + +macro_rules! pipe_dma { + ($r:ident, $w:ident, $t:ty, $out:ident) => { + fn $r(&self, a: u64) -> $out { + self.host().map_or($out::err(), |h| h.mem.$r(a)) + } + fn $w(&self, a: u64, v: $t) -> u32 { + self.host().map_or(BUS_ERR, |h| h.mem.$w(a, v)) + } + }; +} +impl BusDevice for PipeHosts { + pipe_dma!(read8, write8, u8, BusRead8); + pipe_dma!(read16, write16, u16, BusRead16); + pipe_dma!(read32, write32, u32, BusRead32); + pipe_dma!(read64, write64, u64, BusRead64); +} + /// The module management as the machine drives it: the adapters to reset /// with the module, and the outputs to drain. pub(crate) struct ModuleManagement { @@ -311,7 +363,14 @@ impl Deskside { /// Every node board is a host of the one Crossbow, which carries the /// BaseIO and the graphics slot. Returns the Bridge and the node that owns /// the Crossbow. - fn wire_shared<'a>(nodes: &[&'a Node], cfg: &MachineConfig, layout: &DesksideLayout) -> Result<(Arc, &'a Node), String> { + /// An InfiniteReality2 pipe in the graphics slot too, when the configuration + /// names one: `status` is its status bar's source. + fn wire_shared<'a>( + nodes: &[&'a Node], + cfg: &MachineConfig, + layout: &DesksideLayout, + status: &Arc, + ) -> Result<(Arc, &'a Node, Option>), String> { let mut widgets = 1u16 << layout.baseio_widget; widgets |= layout.graphics_widget.map_or(0, |w| 1 << w); for i in 0..nodes.len() { @@ -329,17 +388,35 @@ impl Deskside { xbow.connect_host(owner_port.host, owner.hub.interrupt_target()); xbow.register_locks(); - let hosts = nodes.iter().enumerate().map(|(i, n)| n.hub.host_port(layout.hub_widgets[i])).collect(); - let bridge = Self::baseio_bridge(cfg, layout, xbow.upstream(hosts))?; + let hosts = || nodes.iter().enumerate().map(|(i, n)| n.hub.host_port(layout.hub_widgets[i])).collect::>(); + let bridge = Self::baseio_bridge(cfg, layout, xbow.upstream(hosts()))?; xbow.serve_bridge(layout.baseio_widget, &bridge); + let pipe = match (cfg.graphics_board().kona(), layout.graphics_widget) { + (Some((product, rm_count)), Some(_)) => { + let pipe_hosts = Arc::new(PipeHosts { pipe: OnceLock::new(), up: xbow.upstream(hosts()) }); + let kona_cfg = KonaConfig::new(product, rm_count)? + .with_host_widget(owner_port.host.widget)? + .with_pool(cfg.graphics.threads, cfg.graphics.tile); + let flash = cfg.graphics.flash.as_deref().map(|p| crate::dev::kona::flash_file(Path::new(p))).transpose()?; + let pipe = Kona::new(kona_cfg, pipe_hosts.clone(), pipe_hosts.clone(), status.clone(), flash)?; + let _ = pipe_hosts.pipe.set(Arc::downgrade(&pipe)); + Some(pipe) + } + _ => None, + }; + let peers: Vec<(u8, Arc)> = nodes.iter().enumerate().map(|(i, n)| (layout.hub_widgets[i], n.hub.inbound())).collect(); for node in nodes { let hub = &node.hub; hub.connect_widget(0, xbow.clone()); if let Some(w) = layout.graphics_widget { - hub.connect_widget(w, xbow.port(w, Arc::new(Unmodelled::identified("graphics slot (a test widget)", TEST_WIDGET_ID)))); + let slot: Arc = match &pipe { + Some(p) => p.clone(), + None => Arc::new(Unmodelled::identified("graphics slot (a test widget)", TEST_WIDGET_ID)), + }; + hub.connect_widget(w, xbow.port(w, slot)); } hub.connect_widget(layout.baseio_widget, xbow.port(layout.baseio_widget, bridge.clone())); for (i, n) in nodes.iter().enumerate() { @@ -348,7 +425,7 @@ impl Deskside { } hub.connect_packets(xbow.packets(peers.clone())); } - Ok((bridge, owner)) + Ok((bridge, owner, pipe)) } } @@ -391,9 +468,15 @@ impl Ip27Board for Deskside { fn wire(nodes: &[&Node], cfg: &MachineConfig) -> Result { let layout = Self::layout(cfg.machine.profile)?; let controller = Self::controller(nodes, cfg)?; - let (bridge, host) = match layout.crossbows { - Crossbows::PerNode => Self::wire_per_node(nodes, cfg, layout)?, - Crossbows::Shared => Self::wire_shared(nodes, cfg, layout)?, + // The status bar's activity bits, shared with the Bridge's devices. + let heartbeat = Arc::new(AtomicU64::new(0)); + let status = crate::disp::StatusSource::new(heartbeat.clone(), Arc::new(AtomicU64::new(0))); + let (bridge, host, pipe) = match layout.crossbows { + Crossbows::PerNode => { + let (bridge, host) = Self::wire_per_node(nodes, cfg, layout)?; + (bridge, host, None) + } + Crossbows::Shared => Self::wire_shared(nodes, cfg, layout, &status)?, }; if let [a, b] = nodes { Hub::cable(&a.hub, &b.hub); @@ -402,11 +485,27 @@ impl Ip27Board for Deskside { } } let pci_bridge = bridge.clone(); - let population = PciPopulation { bridge, ioc3: layout.ioc3, controller }; + let population = PciPopulation { bridge, ioc3: layout.ioc3, controller, heartbeat }; let mut io = pci_devices(host, cfg, population)?; if let Some(media) = &layout.mediaio { Self::fit_mediaio(host, cfg, &pci_bridge, media, &mut io); } + if let Some(p) = pipe { + io.display = Some(p.display()); + io.status = Some(status); + io.devices.push(p.clone()); + io.monitor.push(p.clone()); + let reset = std::mem::replace(&mut io.reset, Box::new(|| {})); + let board = p.clone(); + io.reset = Box::new(move || { + reset(); + board.power_on(); + }); + #[cfg(test)] + { + io.pipe = Some(p); + } + } Ok(io) } } diff --git a/src/ip27_deskside/tests.rs b/src/ip27_deskside/tests.rs index 8b057b22d..45220089b 100644 --- a/src/ip27_deskside/tests.rs +++ b/src/ip27_deskside/tests.rs @@ -321,6 +321,176 @@ fn the_bridge_sends_dma_and_interrupts_to_the_hub_the_guest_names() { }); } +const WIDGET_PIPE: u8 = 8; + +fn onyx2_with(dir: &Path, nodes: u8, board: crate::config::GraphicsBoard) -> MachineConfig { + let mut cfg = config(ONYX2_PROFILE, dir, nodes); + cfg.graphics.board = Some(board); + cfg +} + +/// A store to the pipe's aperture through a Hub's I/O space, retried while +/// the pipe's ring is full. +fn pipe_store(hub: &Arc, off: u64, v: u32) { + let io = hub.claim(Space::Io); + for _ in 0..1_000_000 { + match io.write32((u64::from(WIDGET_PIPE) << 24) + off, v) { + crate::traits::BUS_BUSY => std::thread::yield_now(), + status => return assert_eq!(status, BUS_OK, "store of {v:#x} at {off:#x}"), + } + } + panic!("the pipe stayed busy"); +} + +fn pipe_commands(hub: &Arc, words: &[u32]) { + for &w in words { + pipe_store(hub, 0xc1_0000, w); + } +} + +#[test] +fn the_onyx2_layout_puts_the_pipe_where_the_test_widget_stood() { + assert_eq!(ONYX2.graphics_widget, Some(WIDGET_PIPE)); +} + +#[test] +fn a_two_node_onyx2_builds_with_a_pipe_that_answers_its_widget_identity() { + use crate::config::GraphicsBoard; + big_stack(|| { + for board in [GraphicsBoard::Ir2, GraphicsBoard::Ir2x2] { + let dir = Scratch::new("pipe"); + let m = DesksideMachine::new(&onyx2_with(&dir.0, 2, board)).unwrap(); + assert!(m.pipe().is_some()); + assert!(crate::workstation::Workstation::get_display(&m).is_some()); + for node in 0..2 { + let io = m.node_at(node).hub.claim(Space::Io); + let id = io.read32((u64::from(WIDGET_PIPE) << 24) + 4); + assert_eq!(id.status, BUS_OK, "{board:?} from node {node}"); + // Revision in the top nibble; part, manufacturer and the + // presence bit under it. + assert_eq!(id.data & 0x0fff_ffff, 0x0c10_2555, "{board:?} from node {node}"); + assert_ne!(id.data, TEST_WIDGET_ID); + } + } + }); +} + +#[test] +fn without_a_board_the_graphics_slot_holds_the_test_widget() { + big_stack(|| { + let (_dir, m) = built(ONYX2_PROFILE, "nopipe", 1); + assert!(m.pipe().is_none()); + assert!(crate::workstation::Workstation::get_display(&m).is_none()); + }); +} + +#[test] +fn the_pipe_names_its_boards_by_the_graphics_key() { + use crate::config::GraphicsBoard; + for (name, board) in [("ir2", GraphicsBoard::Ir2), ("ir2:1rm", GraphicsBoard::Ir2), ("ir2:2rm", GraphicsBoard::Ir2x2)] { + assert_eq!(name.parse::(), Ok(board), "{name}"); + } + for board in [GraphicsBoard::Ir2, GraphicsBoard::Ir2x2] { + assert!(board.supports(MachineProfile::Onyx2Ip27)); + for other in [MachineProfile::OctaneIp30, MachineProfile::Origin2000Ip27, MachineProfile::Origin200Ip29, MachineProfile::IndyIp24, MachineProfile::Indigo2Ip22] { + assert!(!board.supports(other), "{board:?} on {other:?}"); + } + assert_eq!(board.as_str().parse::(), Ok(board)); + } + assert_eq!(GraphicsBoard::Ir2.rm_count(), Some(1)); + assert_eq!(GraphicsBoard::Ir2x2.rm_count(), Some(2)); +} + +/// The 2E fits the deskside under the names `ir2e` and `ir2e:2rm`. +#[test] +fn the_deskside_takes_the_2e() { + use crate::config::{GraphicsBoard, MachineConfig}; + for (name, board, rms) in [("ir2e", GraphicsBoard::Ir2e, 1), ("ir2e:2rm", GraphicsBoard::Ir2ex2, 2)] { + assert_eq!(name.parse::(), Ok(board), "{name}"); + assert_eq!(board.rm_count(), Some(rms)); + assert!(board.supports(MachineProfile::Onyx2Ip27), "{name}"); + assert_eq!(board.as_str(), name); + } + let mut cfg = MachineConfig::default(); + cfg.machine.profile = MachineProfile::Onyx2Ip27; + cfg.graphics.board = Some(GraphicsBoard::Ir2e); + assert!(cfg.validate().is_ok()); +} + +/// The pipe's DMA and interrupt packets reach the Hub at the widget the +/// guest programs into it. Each packet sets its vector's pending bit; the +/// pipe never clears one, so a second packet after the handler cleared the +/// bit raises it again. +#[test] +fn the_pipe_sends_dma_and_interrupts_to_the_hub_the_guest_names() { + use crate::dev::hub::reg::pi; + use crate::traits::Device; + const WIDGET_CONTROL: u64 = 0x24; + const INT_DEST_UPPER: u64 = 0x34; + const INT_DEST_LOWER: u64 = 0x3c; + const INT_VECTORS: u64 = 0x5c; + const INT_CAUSE: u64 = 0x80_0238; + const GE_VECTOR: u32 = 0x11; + const REASON_PAGE: u64 = 0x30_0000; + big_stack(|| { + let dir = Scratch::new("route"); + let m = DesksideMachine::new(&onyx2_with(&dir.0, 2, crate::config::GraphicsBoard::Ir2)).unwrap(); + let pipe = m.pipe().unwrap().clone(); + pipe.start(); + let memory = |i: usize| m.node_at(i).memory.physical().read32(REASON_PAGE).data; + let pending = |i: usize| m.node_at(i).hub.read64(pi::INT_PEND0).data; + let post = |hub: &Arc| { + pipe_commands(hub, &[0x1f8, 0x103]); + pipe.quiesce(); + pipe.resume(); + }; + + // Node slot 2 is widget 9 and node slot 1 is widget 10. + for (widget, node, other) in [(9u32, 1usize, 0usize), (10, 0, 1)] { + let from = &m.node_at(other).hub; + pipe_store(from, WIDGET_CONTROL, widget << 22); + pipe_store(from, INT_DEST_UPPER, widget << 16 | 0x8000); + pipe_store(from, INT_DEST_LOWER, 0x180_0090); + pipe_store(from, INT_VECTORS, 0x3e_2210); + pipe_commands(from, &[0x1f1, 0x1000_0000 | (REASON_PAGE >> 12) as u32, (REASON_PAGE & 0xfff) as u32, 0x4000]); + + post(from); + assert_eq!(memory(node), 0x103, "widget {widget}: the named node did not get the reason"); + assert_eq!(memory(other), 0, "widget {widget}: the other node saw it"); + assert_eq!(pending(node) & 1 << GE_VECTOR, 1 << GE_VECTOR, "widget {widget}"); + assert_eq!(pending(other) & 1 << GE_VECTOR, 0, "widget {widget}: the other node heard it"); + + // The handler acknowledges the cause and clears the pending bit; + // the next reason raises it again. + pipe_store(from, INT_CAUSE, 0x20); + assert_eq!(m.node_at(node).hub.write64(pi::INT_PEND_MOD, u64::from(GE_VECTOR)), BUS_OK); + assert_eq!(pending(node) & 1 << GE_VECTOR, 0); + // The pipe holds the next reason until the driver clears the page word. + assert_eq!(m.node_at(node).memory.physical().write32(REASON_PAGE, 0), BUS_OK); + post(from); + assert_eq!(pending(node) & 1 << GE_VECTOR, 1 << GE_VECTOR, "widget {widget}: the second packet"); + assert_eq!(m.node_at(node).hub.write64(pi::INT_PEND_MOD, u64::from(GE_VECTOR)), BUS_OK); + pipe_store(from, INT_CAUSE, 0x20); + assert_eq!(m.node_at(node).memory.physical().write32(REASON_PAGE, 0), BUS_OK); + } + pipe.stop(); + }); +} + +#[test] +fn a_dropped_machine_frees_its_pipe() { + big_stack(|| { + let dir = Scratch::new("droppipe"); + let m = DesksideMachine::new(&onyx2_with(&dir.0, 2, crate::config::GraphicsBoard::Ir2)).unwrap(); + let pipe = Arc::downgrade(m.pipe().unwrap()); + let hub = Arc::downgrade(&m.node_at(0).hub); + crate::traits::Device::start(&**m.pipe().unwrap()); + drop(m); + assert_eq!(pipe.strong_count(), 0, "the pipe"); + assert_eq!(hub.strong_count(), 0, "the Hub"); + }); +} + /// With an I/O PROM image named, the Bridge's flash window at widget offset /// 0xc0_0000 shows it, and without one nothing answers there. #[test] diff --git a/src/ip27_machine.rs b/src/ip27_machine.rs index 9323ddbde..bf1b304c1 100644 --- a/src/ip27_machine.rs +++ b/src/ip27_machine.rs @@ -240,6 +240,13 @@ pub struct BoardIo { pub controller: Arc, /// Put the board's devices back to power-on. pub reset: Box, + /// The window of the graphics board, when one is fitted. + pub display: Option>, + /// The status bar's source, to learn the processors' clocks once they exist. + pub status: Option>, + /// The InfiniteReality pipe, for a test to drive. + #[cfg(test)] + pub pipe: Option>, /// The Bridge, for a test to drive. #[cfg(test)] pub bridge: Arc, @@ -493,6 +500,14 @@ impl Ip27Machine { units.push(Unit { node, cpu }); } let watch = ControllerWatch::new(io.controller.clone(), tx, &units[0].node.timers); + if let Some(status) = &io.status { + let cpu = &units[0].cpu; + status.set_cpu_cycles(cpu.cycles_ptr()); + status.set_count_hz(cpu.count_hz_atomic()); + for u in &units[1..] { + status.add_cpu_stats(u.cpu.cycles_ptr(), u.cpu.fasttick_count()); + } + } let monitor = Monitor::new(); monitor.register_device(init_devlog()); @@ -540,6 +555,12 @@ impl Ip27Machine { &self.units[n].cpu } + /// The InfiniteReality pipe, for tests. + #[cfg(test)] + pub(crate) fn pipe(&self) -> Option<&Arc> { + self.io.pipe.as_ref() + } + /// The board's Bridge, for tests. #[cfg(test)] pub(crate) fn bridge(&self) -> &Arc { @@ -714,7 +735,13 @@ impl Workstation for Ip27Machine { self.lifecycle = state; } fn get_display(&self) -> Option> { - None + self.io.display.clone() + } + fn save_snapshot(&mut self, _name: &str) -> Result<(), String> { + Err(format!("snapshots of the {} are not supported yet: the Hubs and node memory have no saved state", self.name)) + } + fn load_snapshot(&mut self, _name: &str) -> Result<(), String> { + Err(format!("snapshots of the {} are not supported yet: the Hubs and node memory have no saved state", self.name)) } fn register_monitor_device(&self, device: Arc) { self.monitor.register_device(device); @@ -892,7 +919,7 @@ impl Ip27Board for Origin200 { for widget in (0..16).filter(|&w| w != WIDGET_HUB) { node.hub.connect_widget(widget, bridge.clone()); } - pci_devices(node, cfg, PciPopulation { bridge, ioc3: &crate::dev::ioc3::IP29, controller }) + pci_devices(node, cfg, PciPopulation { bridge, ioc3: &crate::dev::ioc3::IP29, controller, heartbeat: Arc::new(AtomicU64::new(0)) }) } } @@ -915,14 +942,15 @@ pub(crate) struct PciPopulation { /// SuperIO interrupt takes. pub ioc3: &'static Ioc3Board, pub controller: Arc, + /// The status bar's activity bits, which the Bridge's devices set. + pub heartbeat: Arc, } /// The PCI devices both IP27 I/O boards carry behind their Bridge: two /// ISP1040s and an IOC3 with its Ethernet, serial ports and Timekeeper. pub(crate) fn pci_devices(node: &Node, cfg: &MachineConfig, population: PciPopulation) -> Result { - let PciPopulation { bridge, ioc3: ioc3_board, controller } = population; + let PciPopulation { bridge, ioc3: ioc3_board, controller, heartbeat } = population; let (scsi_name, name) = (table(cfg), product(cfg)); - let heartbeat = Arc::new(AtomicU64::new(0)); let (bus_a, bus_b) = (Isp1040::new_bus(), Isp1040::new_bus()); let attached = crate::scsi::attach_from_config(cfg, Some(scsi_name), &|c| match c { 0 => Ok(&bus_a), @@ -994,6 +1022,10 @@ pub(crate) fn pci_devices(node: &Node, cfg: &MachineConfig, population: PciPopul nat, controller, reset, + display: None, + status: None, + #[cfg(test)] + pipe: None, #[cfg(test)] bridge: test_bridge, }) diff --git a/src/snapshot.rs b/src/snapshot.rs index 186c4877c..eef41abfc 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -55,7 +55,12 @@ use toml::Value; /// levels in their own layout. A v5 reader would load none of it and leave /// every BAR unassigned under a running kernel, and a v6 reader does the /// same with a v5 snapshot, so both machines refuse the other schema. -pub const SCHEMA_VERSION: u32 = 6; +/// +/// v6 → v7: `ChunksManifest` gains the KONA board's chunk lists (the pixel store's +/// arrays and the cursor memory). postcard is not self-describing, so a v6 reader +/// would take the new field's bytes for something else and a v7 reader would fail on +/// a v6 `chunks.bin`; the bump makes both refuse. Nothing else changed shape. +pub const SCHEMA_VERSION: u32 = 7; const MANIFEST_FILE: &str = "snapshot.toml"; @@ -475,6 +480,8 @@ pub struct ChunksManifest { pub bank_chunks: [Vec; 4], /// REX3 framebuffer chunks: (rgb, aux). `None` when running headless. pub framebuffer_chunks: Option<(Vec, Vec)>, + /// The KONA pipe's pixel store and cursor memory; empty when the machine has none. + pub kona_chunks: Vec, } impl ChunksManifest { @@ -486,6 +493,8 @@ impl ChunksManifest { .iter() .flat_map(|(rgb, aux)| rgb.iter().chain(aux.iter())), ) + // A zero chunk's place holds the zero hash, which the store never holds. + .chain(self.kona_chunks.iter().flat_map(|c| c.hashes.iter()).filter(|h| **h != [0u8; 32])) } }