diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ad7c4a6..3d52f3d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -304,6 +304,13 @@ hashes are given where a change is easiest to understand by reading the commit. flags, window-ID modes and VC3 tables, the draw bank, the interrupt lines and, on the Octane, the HEART packets and acknowledgements (docs/perfetto.md). +- **The Octane takes the VPro boards**: `[graphics] board = "v6"`, + `"v8"`, `"v10"` or `"v12"` puts a port of QEMU's VPro (Odyssey) model + on the graphics port; IRIX needs an install with the VPro driver, and + the V10 and V12 bring the Octane2's Crossbow 1.4 with them. The board's front end and raster unit each + run on a thread of their own and the display on a third, so long GL + streams no longer hold up the processor. + (`src/dev/odyssey`) - **The Octane takes the textured IMPACT boards.** `[graphics] board = "highimpact"` is the SI with texture option and `"maximpact"` the MXI (two GEs, two REs), with the board version bytes the IP30 kernel decodes; diff --git a/FEATURES.md b/FEATURES.md index c8ed22e8f..96298368a 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -178,7 +178,7 @@ the first `[section]`, because TOML tables continue until the next header. | Key | Default | Values and behavior | |---|---|---| -| `graphics.board` | `newport`; `solidimpact` on the Octane | newport, xz, extreme, solidimpact, highimpact, maximpact, none. Extreme is Indigo2 only. The Octane takes only `solidimpact` (SI), `highimpact` (SI with texture option), `maximpact` (MXI) or `none` (no board: the machine runs on its serial console, with no window). | +| `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). | | `graphics.heads` | `1` | 1 or 2; dual-head requires Newport. | | `graphics.resolution` | `guest` | guest, 1024x768, 1280x960, 1280x1024; presets require Newport. | @@ -408,7 +408,7 @@ iris-gui does not parse this standalone option set; use its named-machine UI. | `--list-net-interfaces` | Print the host network interfaces libpcap can bridge onto, then exit. Requires a build with --features pcap. | | `--no-scsi-deferred-int` | Disable deferred SCSI status interrupts (default: enabled for OpenBSD/NetBSD compatibility). | | `--cpu` | Emulated CPU: `r4400` (default), `r5000`, or `r10000`. Overrides `[machine] cpu`. | -| `--graphics` | Graphics board: `newport` (default), `xz`, `extreme`, `solidimpact`, `highimpact`, `maximpact` (or `impact:solid`, `impact:high`, `impact:max`). | +| `--graphics` | Graphics board: `newport` (default), `xz`, `extreme`, `solidimpact`, `highimpact`, `maximpact` (or `impact:solid`, `impact:high`, `impact:max`), `v6`, `v8`, `v10`, `v12` (`vpro:v12` and so on, Octane only). | | `--gdb-port` | Enable GDB stub on the given TCP port (e.g. --gdb-port 1234). Connect with: target remote localhost: | | `--monitor-port` | Monitor console port on 127.0.0.1 (default 8888). Give each iris running at once its own. | | `--vnc [ADDR]` | Serve the screen, keyboard and mouse over VNC on ADDR (`host:port` or a port; default 127.0.0.1:5900). Sets `vnc.listen`. | @@ -514,6 +514,7 @@ and offline-tool controls are included so they are not mistaken for GUI settings | `IRIS_O2_AUDIO_CAPTURE` | O2 host audio capture output path (what the host plays). | [src/dev/mace_audio.rs](src/dev/mace_audio.rs) | | `IRIS_O2_AUDIO_DUMP` | File to record the O2's audio output at the guest's rate, before resampling. | [src/dev/mace_audio.rs](src/dev/mace_audio.rs) | | `IRIS_O2_AUDIO_TRACE` | Any presence traces every O2 audio register access and codec operation on stderr. | [src/dev/mace_audio.rs](src/dev/mace_audio.rs) | +| `IRIS_ODSY_REC` | Record VPro bus stores, loads and DMA reads from power-on to this path (gzip; replayed by the tests in `src/dev/odyssey/golden/`). | [src/dev/odyssey/record.rs](src/dev/odyssey/record.rs) | | `IRIS_REX3_REC` | Record REX3 bus operations from board start to this path (`gputrace` builds). | [src/dev/ng1/rex3_rec.rs](src/dev/ng1/rex3_rec.rs) | | `IRIS_REX3_TRACE` | Trace REX3 GFIFO and CPU accesses from board start to this path (`gputrace` builds). | [src/dev/ng1/rex3_trace.rs](src/dev/ng1/rex3_trace.rs) | | `IRIS_NO_EXIT_ON_POWEROFF` | Any presence prevents process exit on guest power-off/CI quit; set by GUI. | [src/ci.rs](src/ci.rs) | @@ -535,6 +536,8 @@ and offline-tool controls are included so they are not mistaken for GUI settings | `MGRAS_REPLAY_OUT` | Optional IMPACT replay image output path. | [src/dev/mgras/record.rs](src/dev/mgras/record.rs) | | `MGRAS_REPLAY_TRACE` | IMPACT replay tracing selector (`gputrace` builds). | [src/dev/mgras/record.rs](src/dev/mgras/record.rs) | | `MGRAS_SIZE_PROBE` | Input for the opt-in IMPACT size/probe test. | [src/dev/mgras/mgras_tests.rs](src/dev/mgras/mgras_tests.rs) | +| `VPRO_GOLDEN` | `tolerance` lets a VPro golden frame whose hash changed pass inside its stored limits. | [src/dev/odyssey/golden/mod.rs](src/dev/odyssey/golden/mod.rs) | +| `VPRO_REPLAY` | Replay input path for one VPro recording; `VPRO_REPLAY_DIR` picks the directory that is replayed otherwise. | [src/dev/odyssey/golden/replay.rs](src/dev/odyssey/golden/replay.rs) | `RUST_LOG` configures the host Rust logger; `IRIS_DEBUG_LOG` configures device logging. Standard host variables (`HOME`, `XDG_CACHE_HOME`, `LOCALAPPDATA`, diff --git a/HACKING.md b/HACKING.md index 7f0215f78..9359e06d5 100644 --- a/HACKING.md +++ b/HACKING.md @@ -299,6 +299,7 @@ Every device that has real work to do runs on its own thread: | `ISP1040-0`, `ISP1040-1` | Octane SCSI request rings and disk transfers | | `IOC3-RX-A`, `IOC3-RX-B`, `IOC3-NVRAM` | Octane serial input per port; the DS1685 NVRAM file writer (IOC3's other deadlines are `hptimer` one-shots) | | `ip30-eth-rx`, `ip30-eth-net` | Octane ethernet receive pump; the network backend | +| `odyssey-work`, `odyssey-raster`, `odyssey-display` | VPro: the front end decodes the queued command-FIFO stores (the processor's stores only queue), the raster unit draws what the front end sets up, and the display copies the planes out, 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, and the display reads the planes while they are drawn | 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/HELP.md b/HELP.md index 812efcf6a..6739341dc 100644 --- a/HELP.md +++ b/HELP.md @@ -621,7 +621,7 @@ cpu = "r4400" # "r5000" or "r10000" (--cpu); use R10000 on IP28 # cpus = 2 # Octane only: a second processor (docs/Octane/multiprocessor.md) [graphics] -board = "newport" # "newport", "xz", "extreme", "solidimpact" ("impact:solid"), "highimpact" ("impact:high"), "maximpact" ("impact:max"); the Octane: IMPACT (default solidimpact) or "none" +board = "newport" # "newport", "xz", "extreme", "solidimpact" ("impact:solid"), "highimpact" ("impact:high"), "maximpact" ("impact:max"); the Octane: IMPACT (default solidimpact), "v6", "v8", "v10", "v12" ("vpro:v12" and so on; need IRIX 6.5.10 or later installed with the VPro driver, and PROM 4.0, or 4.5 for the V10 and V12) or "none" heads = 1 # 2 = dual-head Newport (second REX3 in GIO slot 1) resolution = "guest" # or "1024x768", "1280x960", "1280x1024" @@ -757,7 +757,7 @@ Configuration --nvram NVRAM file (default: nvram.bin) --nveeprom Indigo2 NVRAM EEPROM file (default: nveeprom.bin) --ip22 Emulate an Indigo2 (IP22) instead of an Indy (IP24) - --graphics newport, xz, extreme, solidimpact, highimpact, maximpact + --graphics newport, xz, extreme, solidimpact, highimpact, maximpact, v6, v8, v10, v12 --cpu r4400 (default), r5000, or r10000 --bank0..--bank3 RAM bank sizes (0/8/16/32/64/128; IP28: also 256/512) --clock-fixed-mhz CP0 Count frequency (IP22/IP24: 33; IP28: 97.5) diff --git a/clanker/INDEX.md b/clanker/INDEX.md index e196e9e9e..feb7c3799 100644 --- a/clanker/INDEX.md +++ b/clanker/INDEX.md @@ -245,11 +245,14 @@ IMPACT (MGRAS/GR4): HQ3/GE HLE, RSS raster, pixel memory, the raster JIT. Hardwa ## odyssey/ -Odyssey (VPro V6/V8/V10/V12): the Octane2/Fuel/Tezro graphics board. Not emulated; references and groundwork only. +Odyssey (VPro V6/V8/V10/V12): the Octane2/Fuel/Tezro graphics board. Modelled on the Octane (`[graphics] board = "v12"` and the other three). | Note | Kind | Title | |---|---|---| +| [bringup.md](odyssey/bringup.md) | howto | VPro on the Octane: board catalogue, Crossbow code 5, PROM/IRIX minimums, disk images | +| [gotchas.md](odyssey/gotchas.md) | rule | VPro gotchas: IMPACT installs lack the driver, textport FIFO cost, the f64 depth path | | [public-references.md](odyssey/public-references.md) | open | Odyssey (VPro) public references: two console-level drivers, no GL stream | +| [threads-and-locks.md](odyssey/threads-and-locks.md) | design | VPro threads, who owns the board and raster state, the processors' lock, the lock-free display, the command ring and its ordering rules | ## hostgl/ diff --git a/clanker/odyssey/bringup.md b/clanker/odyssey/bringup.md new file mode 100644 index 000000000..b31980d55 --- /dev/null +++ b/clanker/odyssey/bringup.md @@ -0,0 +1,76 @@ +# VPro (Odyssey) on the Octane: boards, versions and bring-up + +Status: howto (iris `src/dev/odyssey/`, 2026-10-30). This note is what +you need to start a guest and read a trace. + +**What it is.** The Octane's second graphics option after IMPACT: a +crosstalk widget (part 0xc013, manufacturer 0x023) on widget 11, where the +IMPACT board otherwise sits. Everything goes through a command FIFO; there +is no linear frame buffer. `public-references.md` lists the public code for +the board. + +**Selecting it.** `[graphics] board = "v6" | "v8" | "v10" | "v12"` (also +`vpro:v6` and so on). `impact` stays the Octane's default and the value is +refused on the other machines. `perfetto.md` in `docs/` lists the VPro +tracks. + +## Board catalogue + +| Board | Widget revision | gfxinfo BUZZ | Memory | Banks | Identity record | +|---|---|---|---|---|---| +| V6 | 2 | A.1 | 32 MB | 2 | `ODY32` (030-1403) | +| V8 | 2 | A.1 | 128 MB | 4 | `ODY128` (030-1404) | +| V10 | 10 | B.1 | 32 MB | 2 | `ODY32VERSIONB` (030-1655) | +| V12 | 10 | B.1 | 128 MB | 4 | `ODY128VERSIONB` (030-1611) | + +PB&J version is 1 on all four. Commands, frame buffers and textures share +one SDRAM pool on every board. Inferred, not captured: the V8's A.1 and +banks, the V10's revision 10, and the 32 MB boards' two banks. The Fuel and Tezro boards are other assemblies; the Octane +names are answered on those machines too, a known limitation. + +Guest-side consequences worth knowing before you debug a "wrong" screen: + +- The X server refuses 1280x1024_60 at 16-byte depth on a BUZZ A.1 board. + V6 and V8 therefore start in `1280x720_72`, V10 and V12 in + `1280x1024_60`. That is the guest's decision, not a model bug. +- `GL_MAX_TEXTURE_SIZE` is 512 below 128 MB and 2048 otherwise. +- On a small board the kernel's board-memory manager pages objects to host + memory when the pool fills. + +## Crossbow and PROM minimums + +SGI's VPro upgrade kit (007-4210-003) needs Crossbow 1.4, IRIX 6.5.10, and +PROM 4.5 for V10/V12 or PROM 4.0 for V6/V8. The Octane presents Crossbow +revision code 4. An Octane2's Crossbow reads code 5 at the PROM and shows as +1.4 in its inventory, so a machine with a V10 or V12 presents code 5. + +Code 5 has registers the earlier parts lack: a reset fence for the core and +one per link at 0x78 to 0xb8 (reset 0xff, bits 7:0 kept), a resource lock at +0xc0 that takes a value only while free, and its clear at 0xc8. The +Octane2's PROM programs the fences; answering them with an access error, as +code 4 does, leaves it off its path. The fences gate nothing in the model, +because no warm reset travels through the modelled switch. Revision code 4 +and earlier keep the range reserved. + +iris warns at start-up when the PROM image's banner is older than the board +needs. `IP30prom.bin` (from IRIX 6.5.30) holds two PROMs: Rev 4.9 is the +recovery PROM and Rev 4.17 the main one; the PROM console reports 4.17, as +`flash -V` does in IRIX. + +## Disk images + +The VPro needs a kernel driver and X module (`odyssey.so`, `libGLcore` for +`IP30ODSY64`) that an IMPACT install does not carry; an IMPACT install boots, +then X finds no screen. In the scratch inputs, `octane2-vpro-*.raw` and +`vpro-octane2-installed-*.raw` are VPro installs (IRIX 6.5.30 on an Octane2 +V12, which boots to the desktop with `glxinfo` reporting `VPRO/B/128`); +`si-octane-*.raw` is an IMPACT install. Names are the only marker; confirm +with `hinv` or `gfxinfo` after boot. + +## Checking it came up + +- `gfxinfo` prints `BUZZ version B.1`, `PB&J version 1`, memory size. +- `glxinfo` renderer is `VPRO//`. +- `hinv` names V6/V8/V10/V12 from generation and size. +- `GL_ODY_*` environment variables override libGLcore's fast-path defaults, + which differ between BUZZ A and B. diff --git a/clanker/odyssey/gotchas.md b/clanker/odyssey/gotchas.md new file mode 100644 index 000000000..826d3672f --- /dev/null +++ b/clanker/odyssey/gotchas.md @@ -0,0 +1,50 @@ +# VPro gotchas + +Status: rule (2026-10-30). + +- **An IMPACT install has no VPro driver.** An Octane disk installed with an + IMPACT board boots, but X finds no screen on the VPro. Use a disk + installed on a VPro machine (see `bringup.md`). Do not patch a disk by + hand; install with the install tooling. + +- **Textport output costs per FIFO word.** A scrolled line in the PROM or an + IRIX textport is hundreds of thousands of command-FIFO words, so slow + scrolling is store cost, not draw cost. Take the host interface's lock per + store and the textport crawls; the ring in `fifo.rs` exists for this. Do not + add per-store work (logging, perfetto events, extra atomics beyond the + level and wake flag) to the push path. `VPro cfifo level` is sampled for + that reason. + +- **The depth path is double precision, on purpose.** The clip, window and + depth maths accumulate in `f64`. In `f32` a vertex just beyond the far plane + under a perspective model-view rounds onto it, and the keep-or-clip + decision flips. Lighting uses `dev::gl`'s single-precision core because the + board's microcode does; transform, clip planes and the window map do not. + Do not move them onto `dev::gl` without a far-plane test, and expect pixel + changes if you do. + +- **Revision code 4 vs 5.** A V10 or V12 needs the Octane2's Crossbow + (revision code 5) with its reset fences and resource lock. With code 4 the + PROM's fence writes hit a reserved range and the machine never reaches the + graphics init. + +- **PROM too old.** V10/V12 need PROM 4.5, V6/V8 4.0; iris warns at start-up. + Heed the warning before reporting a hang. + +- **Board profile or X format surprises.** V6/V8 start at 1280x720_72 because + the guest refuses 1280x1024_60 at 16-byte depth on BUZZ A.1. That is not a + scanout bug. + +- **Retrace is counted by the display thread.** It latches swaps at its own + frame pace, up to a frame (about 16 ms) after the guest arms one. Swap + timing measured to better than that is noise. + +- **Model choices to remember.** The FIFO's depth is unknown in any unit. + The model clamps the level at 32,768 words, whatever the stored `0x6bX` + values say, and uses a 16-word low-water threshold; `0x6b2` as the + status-bit-15 threshold is a model choice too. The level is in words, a + doubleword store counting two. The tests pin the model, not the + hardware. + +- **No Mesa renderer.** The host-Mesa path from the QEMU model was left out; + everything is drawn by the scalar rasteriser. diff --git a/clanker/odyssey/public-references.md b/clanker/odyssey/public-references.md index 7b3c2be9e..eb4253e80 100644 --- a/clanker/odyssey/public-references.md +++ b/clanker/odyssey/public-references.md @@ -1,6 +1,6 @@ # Odyssey (VPro) public references: two console-level drivers, no GL stream -Status: survey, 2026-10-25. Nothing in IRIS emulates Odyssey yet. This note +Status: survey, 2026-10-25 (before the model landed; see bringup.md). This note records what public code exists for the board so the Octane/Tezro work does not re-search it. Verify the repositories before relying on details; the T2 pages and patch trees move. diff --git a/clanker/odyssey/threads-and-locks.md b/clanker/odyssey/threads-and-locks.md new file mode 100644 index 000000000..0e2fc8c08 --- /dev/null +++ b/clanker/odyssey/threads-and-locks.md @@ -0,0 +1,274 @@ +# VPro threads, the lock-free state and the two queues + +Status: reference (iris `src/dev/odyssey/{interface,fifo,worker,raster_unit,mod}.rs`, +2026-10-30). + +**Why it is shaped this way.** The board decodes and draws behind the +processor: the driver stores more commands and polls registers while a +primitive is drawn. The model lets the processor go on. Where the board's +own units share memory it shares it the same way: the display and the +rendering engine read and write the planes, the lookup RAM and the mode +registers at the same time, with no lock or borrow between them. Software +orders them with buffer swaps, fences, query sync words, flag bits and +waiting reads, and the model keeps exactly those. A synchronisation fault +in a guest, or in the model, shows as a flash of wrong planes or colours +for a frame, as it does on the hardware; a persistent fault means a +behaviour is not modelled. Do not add a lock or a borrow to hide one. + +## Chips and threads + +Each thread stands for a chip, or a part of one, of the board: + +| Thread | Hardware | Grade and source | +| --- | --- | --- | +| CPU thread | the processor's widget accesses | | +| `odyssey-work` | BUZZ front end: command parse, register writes, transform, lighting, primitive setup | BUZZ as the one transform and raster chip: established (`007-4271-001` p. 5, `108-0314-003` p. 1) | +| `odyssey-raster` | BUZZ raster unit: raster, texture, shading, framebuffer writes | one raster unit: inferred (US 6,259,460) | +| `odyssey-display` | PB&J back end: retrace, colour map, cursor, scanout | PB&J as the display back end with no drawing role: established (`108-0314-003` p. 1) | + +The command FIFO lives in board SDRAM. Its depth, in any unit, is unknown; +the model gives it 32,768 words, +a model choice independent of the `0x6b1` the kernel stores. +The split between the two BUZZ threads follows US 6,417,858, which puts an +output FIFO between the transform engine and the rasteriser (inferred to +apply to BUZZ). Nothing models more than one raster unit. + +## What crosses each queue + +- **Command FIFO** (`fifo.rs`, CPU to `odyssey-work`): the processor's + stores to the two command-FIFO ports and the context-switch stores, each + as (offset, size, value). The level the guest sees + is in words, against the model's 32,768. +- **Raster queue** (`raster_unit.rs`, `odyssey-work` to `odyssey-raster`): + jobs, each owning what its raster work reads, captured when the front + end set it up. A GL primitive is a `GlDrawJob`: its raster rules, draw + state, vertices, texture chain and the texture tables it uses. The + raster unit hands a drawn one back through a small return ring and the + front end fills it again, keeping its vertex buffer and texture chain, + so a draw allocates nothing (allocating per draw on one thread and + freeing on the other had both contending for the allocator). The rest + are closures: a GL clear's or pattern rectangle's raster ops; a fence's + address and token; a query's snapshot and sync word. This encoding is + the model's own: the hardware's is unknown. Capturing state at its place + in the stream is how raster state reaches the raster unit in order. +- **The planes** stay where `Board` keeps them. The raster unit owns them + while it has jobs and reaches them through raw spans, as the IMPACT + model's engines reach their pixel memory (`clanker/mgras/DESIGN.md`). The + front end's own plane work (integer and image drawing, pixel DMA, the + object store, reset) waits for the raster queue to empty first + (`Board::raster_wait`). That wait is the command stream's order, not a + lock: the board's pipeline runs a plane access behind the drawing before + it. A DMA into an object that already has its rows does not wait: its + texels go to the raster unit as a job, behind the draws that may still + read the old ones (`Board::object_stage`). A texture upload otherwise + drained the whole raster queue, up to 180 ms of drawing in Smoke & + Mirrors. +- **The display** takes nothing from any of them. It reads the planes + through raw pointers (`PlaneView`, reader.rs), as the MGRAS, REX3 and GR2 + displays read theirs. See "The display" below. + +Both rings are the shared `GFifo`, 64K entries. The IMPACT model's raster +FIFO has a head per raster engine with scanline interleave; the VPro's has +one head, as the board has one raster unit. + +The front end is bounded by estimated work, not jobs. Each job carries a +cost in pixels (a draw's area from its vertices' window positions, a +clear's or block fill's pixels at an eighth, plus a small fixed overhead), +and the raster queue keeps their running total. A front end that finds +more than `RASTER_WORK_BUDGET` (2^17 pixels, a few milliseconds of shaded +fill) queued waits on a condition variable until the raster unit has +worked it down to half, and a stop wakes it. Smoke & Mirrors queues up to +about 11,700 small triangles, so a bound of 256, 1,024 or 4,096 jobs made +the front end, and through the full command FIFO the processor, wait for +the raster unit, and cut mirrors from about 4 to 3.0, 3.2 and 3.6 swaps a +second; perfly did not change. + +## Locks and who owns what + +There is no board lock and no raster lock. As on the other iris boards +(GR2's `regs`, `re3` and `hq_engine`, MGRAS's `eng` and `rss`), the state of +a unit is an `UnsafeCell` owned by the unit's thread, and a processor thread +touches it only after the unit's FIFO has drained, with the contract written +at the `unsafe impl Sync` (`BoardCell` in `mmio.rs`, the raster queue's +`ctx`). Unlike GR2, which has one writer, VPro supports two processors +(`[machine] cpus = 2`), so the processors take a small lock against each +other, as MGRAS's producers take its `submit` lock (below). + +- **`Board`** (everything but the host registers, `BoardCell`) belongs to + the front end's thread while it runs stores. A processor's access reaches + it by one of three ways: a store goes into the command FIFO + (`CommandFifo::push`); an access that must see the drawing is answered + busy by the bus until the FIFO and the raster queue are empty + (`BoardLocks::bus_read`, `bus_write`, rule 2 below), and then runs on the + processor's thread with the front end idle; and a few accesses that do + not wait (the level word, the cursor and back-end read windows, an + unmodelled offset) read whole words the front end writes only as whole + words or atomics, and show the old or the new value (`BoardCell::get` is + `unsafe`, with its contract; it is visible to the board's module only). + With the front end stopped (tests, a stopped machine) the caller runs + what is queued (`Board::work_drain`); with the front end running it runs + nothing. +- **The processors' lock** (`BoardLocks::submit`) serialises one processor's + access against the other's, from its busy check to the end of the access + and across a command push, so two processors cannot both pass the check + and borrow the board, or wake the front end under each other. The front + end's thread and the raster thread never take it, so it adds no wait for + drawing. A processor access to a host-interface register does not take it. +- **`HostInterface`** is behind the front lock, a register file's own + lock held for a few instructions: identity, control, the interrupt bank, + the routing, the swap bank and the FIFO status, plus the interrupt state. + A front access (those registers) takes only it + and never waits for a draw. It is a leaf; the raster thread takes it for + a fence's interrupt, and the display takes it to latch swaps. +- **The raster unit's state** (`RasterCtx`: the stipple phase and the rows + drawn) belongs to the raster thread while it runs. The front end runs a + job itself only with no raster thread (tests, a stopped machine); the + raster thread can still publish its rows after `pending` falls to 0, and + nothing else touches the state then. +- **The display** takes no board or raster state and waits for no drawing. +- What remains are the queues' own locks: the command ring's, the ring + consumer's accounting against reset (`CommandFifo::pop`), the raster + queue's wake and idle condition variables, and the thread handles. + +A guest that stores to a register while it waits on a drain, or two +processors that program the board without their own ordering, can race each +other's register values (the processors' lock keeps the borrows apart, not +the guest's order). The hardware has no interlock there either; a fault shows as a +wrong frame or a lost register store, not as a model that hides it. + +## The display + +The display thread (`Odyssey::frame`) polls the retrace, stages, resolves +and presents with nothing held against the board. A frame reads: + +| What | How it is shared | Ordering it relies on | +| --- | --- | --- | +| Colour, id and overlay planes | raw pointers (`PlaneView::copy_row`), the one function that reads memory a writer may be storing to | the writer marks a row after its last store; the frame takes the marks before it copies | +| Dirty and unnamed rows | atomic bitsets (`RowMarks`) | store, then mark; the frame takes the marks first | +| Rows the raster unit drew | atomic words (`PublishedRows`) | published before a fence, query or sync word the guest can see, when the queue drains and at the end of a slice | +| Lookup RAM, display modes, overlay modes, cursor | atomic words (`SharedRegs`) and a generation | the store marks every line after it, and the frame copies the registers after it takes the marks; the generation is read before the copy | +| Scanout mode, retrace restart | atomic words (`ModeCell`) and two counters | counters move after the fields and are read before them | +| Swap latch, displayed-buffer bits, retrace interrupt | the host interface, under the front lock | see below | + +Sync points kept, each because the hardware has it: + +- **The buffer swap.** The swap arm is latched at the vertical retrace, and + only the display thread latches. The displayed-buffer bits are read once + per frame, after the latch, and used for the flip, the lines staged and + the resolve. A guest that draws into the buffer that is not on the screen + and arms the swap after its fence never shows a mix of two frames. The + host-interface lock only serialises the register file the swap bank lives + in; the display holds it for a few instructions. +- **Fences, queries and sync words.** The raster unit publishes the rows it + drew before it runs a completion job, so a guest that waits for one before + it arms a swap has its rows known to the display when the swap latches. +- **Waiting reads.** A register read that waits drains the command FIFO and + the raster queue (rule 2 below). + +What is not kept, because the hardware has none: a frame can copy a row the +raster unit is drawing, a lookup entry the front end is storing, or the old +and new halves of a mode change. The writer marks after it stores, so the +next frame copies the row or entry again. A guest that arms a swap before +its drawing is done sees the half-drawn frame for as long as the queue +takes, as it would on the board. + +## The front end's run + +The worker parks (`park_timeout`, 50 ms, and an unpark from a store that +joins an empty queue), waits `GAP` (100 us) so a burst of stores lands +first, then runs the queue until it is empty. It needs no time slices: +nothing waits for it to let go of the board (the `slice` flag and the +`odyssey-work` `slice` trace event mark a run). Devices start before the +processors, so no processor store is in flight when `work_start` sets +`running`. A processor access that must see the stores is told busy and retries, +taking its interrupts meanwhile, so the guest's 10 ms clock tick is never +held behind a long draw. A run always runs at least one store. + +## The command ring + +The PROM's and IRIX's textports send hundreds of thousands of FIFO words per +scrolled line, so a store must cost little. Stores go into iris's shared +`GFifo` (`src/gfifo.rs`, 64K entries; `fifo.rs` wraps it). The processor +pushes without the front lock and wakes the worker only when it sleeps. With +two processors (`[machine] cpus = 2`) a second producer pushes the same way: +the ring's own lock orders them, with no lock of ours above it. A push that +loses that lock to the other processor, or finds the last place taken between +the room check and the push, is refused, and its store retries the push behind the thread's run +(`BoardLocks::write_command`), not around it. The ring is deeper than the board's FIFO, so a second producer's +overshoot of the level by a store is absorbed. The consumer (the worker, or a draining access with the worker stopped) looks +at up to 32 stores, runs them, and takes the ones it ran out together +(`CommandFifo::take_run`, `retire`): the level and the store count +move once a run, not once a store, because each store costs the +worker little and the processor reads those words. A store that has run but +not retired still counts in the level, so the level never reads below what the +board has left to take. A push counts its store (level, store count) +before the ring shows it and takes the counts back off if the ring +refuses it: the worker can take a store and retire it the instant it lands, +before the push returns, and a retire must find every store it takes already +counted or the level goes below zero. +While the worker runs every command store queues, so decoding and drawing +never run on the processor's thread. + +The guest sees the model's FIFO, not the ring: 32,768 words, a model choice +whatever the kernel's `0x6b1` set-up stores. The level is the words waiting, a doubleword store counting +two (also a model choice). STATUS0 bit 16 is empty, bit 17 below the model's +16-word low-water threshold, bit 15 at or above the stored `0x6b2` (flags bit +25 rises on reaching it). Which register is a high-water mark is unknown: +applying `0x6b2` there is a model choice. A processor store is refused with the bus busy +(`BoardLocks::bus_write`) only when the 64K-entry host ring is full, as +IMPACT's and GR2's are: the processor retries the instruction, taking +interrupts meanwhile, and the board's thread, which then starts its next +run without the pause, makes room. Past 32,768 words the guest reads a +full FIFO (high-water bit up once a `0x6b2` is set) while its stores still +succeed. +A caller other than the bus, with the board's thread stopped, still runs +the queue and then its store. The flow-control target word `+0x1074` is stored, not routed, so +a store to it goes in at once even while the FIFO holds work. + +## The FIFO ordering rules + +These are guest-visible, and hold across both queues; any change to either +must keep them. + +1. One in-order stream: stores run in arrival order, and raster jobs in the + order the front end set them up. +2. A register read that waits (`access_waits`) drains the command FIFO and + then the raster queue, so it sees the drawing of every store before it. + The memory word `0x106c` never waits (`level_word`): its fields are + straps and FIFO levels, none a drawing result. The kernel reads it at + every flow-control attach, which each GL make-current runs; waiting + there stalled the processor behind the drawing of each frame. A waiting + store is held back for the command FIFO only (`BoardLocks::bus_write`); + the raster unit never shared state with it, and a store that touches the + planes waits for the raster queue itself (`Board::raster_wait`). The + registers that do not wait answer from the front at once. On the bus + (`BoardLocks::bus_read`, `bus_write`) such an access answers busy while + that work is still queued and the board's threads run: the processor + retries it, taking interrupts, and the board's threads run the work, + as IMPACT's waiting reads do. Callers other than the bus, with the board's thread stopped, drain it + themselves. +3. STATUS0's empty, low-water and high-water bits follow the words in the + command FIFO, not the raster queue: they are the board's FIFO. +4. The low-water flag latches as a store retires below the model's threshold (16 words), + whatever the enable mask says; it reaches the driver only while enabled. +5. Completion words are written when the raster unit is done with the work + before them: fences and a current-state query's publication (snapshot, + counter, sync word) run as raster jobs; the context-switch done word + waits for the raster queue to empty. +6. Stopping the machine finishes the command FIFO and then the raster queue; + a reset abandons the waiting stores and waits for the raster queue before + it clears the planes. +7. The display waits for nothing: it copies what is in the planes and the + registers when the frame runs. Rows and entries it copies half-written + are copied again at the next frame. +8. The back end's display-FIFO level (`0x106c` bits 6:0) stays 0: back-end + writes apply when the front end runs them. + +## Tracing it + +`docs/perfetto.md`: `VPro cfifo level` is sampled at run edges and STATUS0 +reads, not per store. The `slice` slice on `odyssey-work` against `drain` on +the CPU thread shows who is running the commands, and `raster slice` on +`odyssey-raster` who is drawing. A long `raster wait` on `odyssey-work` +points at front-end plane work behind queued drawing; a long `slice` with +the CPU answered busy on a read, at the front end still running stores. diff --git a/docs/octane-ip30.md b/docs/octane-ip30.md index 6c0675e7d..9c04ef568 100644 --- a/docs/octane-ip30.md +++ b/docs/octane-ip30.md @@ -46,7 +46,7 @@ cpu = "r10000" flash = "flash-ip30.bin" # keep the PROM's flash records between runs [graphics] -# board = "maximpact" # "solidimpact" (default), "highimpact", "maximpact" or "none" +# board = "maximpact" # "solidimpact" (default), "highimpact", "maximpact", "v6", "v8", "v10", "v12" or "none" [[port_forward]] proto = "tcp" @@ -130,12 +130,31 @@ the board, as on the other machines: | `solidimpact` (default) | SI | 1 / 1 | none | | `highimpact` | SI with texture option | 1 / 1 | 4 TRAMs | | `maximpact` | MXI | 2 / 2 | 4 TRAMs per RE | +| `v6` (also `vpro:v6`) | V6 | (VPro, BUZZ A) | in its 32 MB board memory | +| `v8` (also `vpro:v8`) | V8 | (VPro, BUZZ A) | in its 128 MB board memory | +| `v10` (also `vpro:v10`) | V10 | (VPro, BUZZ B) | in its 32 MB board memory | +| `v12` (also `vpro:v12`) | V12 | (VPro, BUZZ B) | in its 128 MB board memory | The two textured boards are new and less tested than the SI. Its screen is the iris window and what iris-ci screenshots show; the keyboard and mouse are IOC3's PS/2 ports. `board = "none"` leaves the port empty, and the machine then runs on its serial console with no window. +`v6`, `v8`, `v10` and `v12` are the four Octane VPro boards on the same +port: a port of the VPro (Odyssey) model in jamesbraid/qemu-sgi-origami +(`src/dev/odyssey`), with the X server and OpenGL drawn by the model's +scalar rasteriser on the board's own thread. Each answers its board's +widget revision, memory size and banks and the identity record on its +NIC (`ODY32`, `ODY128`, `ODY32VERSIONB`, `ODY128VERSIONB`). The PROM draws +its startup screen on any of them, but IRIX needs an install with the +VPro kernel driver and X server module: a disk installed with an IMPACT +board finds no screen on a VPro. SGI's upgrade kit (007-4210-003) asks +for PROM 4.0 and IRIX 6.5.10 for the V6 and V8, and PROM 4.5, IRIX 6.5.10 +and Crossbow 1.4 for the V10 and V12. The V10 and V12 shipped only in the +Octane2, so choosing one makes the machine an Octane2: it presents the +Octane2's Crossbow (revision code 5, 1.4 in hinv) and Bridge (revision 4). +iris warns at start-up when the PROM image is older than the board needs. + ## Networking The IOC3 Ethernet (an ICS1890 PHY at MII address 1) is on iris's NAT diff --git a/docs/perfetto.md b/docs/perfetto.md index cbb4b0b04..f9466e6dd 100644 --- a/docs/perfetto.md +++ b/docs/perfetto.md @@ -85,6 +85,11 @@ host thread's name. | Graphics | `IMPACT swap wait` | SCHEDULE_SWAP held until the raster engines have drawn the frame before it (arg: raster entries queued), so a flip never shows a half-drawn frame. | | Graphics | `IMPACT status`, `IMPACT flags`, `IMPACT vc3` | Every STATUS read with its value (bit 2 is vertical blank); flags raised (arg: raster entries queued << 32 \| bits) and cleared by the host; VC3 register and SRAM writes (the window-ID tables) and current-line reads. | | Graphics | `IMPACT irq` | The board's three interrupt lines (arg: line << 1 \| level: 0 FIFO, 1 general, 2 retrace). On the Octane, the interrupt packets sent to HEART (arg: vector << 1 \| set) and the kernel's acknowledgements (arg: offset << 32 \| bits). | +| Graphics | `VPro cfifo level` | The command-FIFO level STATUS0 is built from (words queued, not yet run), sampled at slice start and end and at STATUS0 reads. | +| Graphics | `VPro irq` | `cfifo low water` (the level fell below the mark as a store retired), `dma done`, and an `interrupt packet` per packet sent to HEART (arg: routed-source shift << 9 \| vector << 1 \| set). | +| Graphics | `VPro display`, `VPro capture`, `VPro present` | `swap arm` (the driver's flip request, arg: mask), `retrace` (flips latched, arg: displayed-buffer bits); the refresh thread resolving the frame and handing it to the window. Inside a capture, `frame planes` on `odyssey-display` is the part that copies the dirty lines aside, with no lock and no wait for the raster unit. | +| `odyssey-work` | thread | `slice` per run of queued stores on the front-end thread (arg: stores waiting), `dma descriptor` per page-table transfer (arg: table address). A processor access that drains the queue shows `drain` on its own thread. | +| `odyssey-raster` | thread | `raster slice` per run of raster jobs on the raster thread (arg: jobs pending). A front end that waits for the raster unit before it touches the planes shows `raster wait` on its own thread (arg: jobs pending). | | jitv2 | `jitv2 compile queue`, `jitv2-compile-*` | `compile queue full` from the first dropped request to the next that fits; a `compile` slice per page compile (on the CPU thread in inline-compile mode). | ## Adding instrumentation diff --git a/scripts/vpro-goldens.sh b/scripts/vpro-goldens.sh new file mode 100755 index 000000000..edfc749e8 --- /dev/null +++ b/scripts/vpro-goldens.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# scripts/vpro-goldens.sh: the VPro (Odyssey) regression nets. +# +# Run from the repo root: scripts/vpro-goldens.sh COMMAND +# +# check draw-level frames and vertex numbers, bit-exact (cargo test) +# tolerance the same, passing a frame that moved inside its stored limits +# bless re-take the frames, hashes and vertex numbers +# replay replay the recorded guest sessions to their checkpoints +# (fails when there are no recordings) +# replay-bless re-take the recordings' checkpoints +# +# VPRO_RUN is a prefix for the build container when cargo is not on the host +# (for example VPRO_RUN="/path/to/icargo"); it is put before `env`, so the +# variables below reach the tests inside it. Paths given to the tests must be +# visible there. +# +# The README beside the data, src/dev/odyssey/testdata/README.md, says when +# to re-take each net and how to judge a change by its tolerance. + +set -euo pipefail +cd "$(dirname "$0")/.." + +run=${VPRO_RUN:-} +cargo_test() { # ENV... -- FILTER [TEST-HARNESS-ARGS...] + local envs=() + while [ "$1" != "--" ]; do envs+=("$1"); shift; done + shift + local filter=$1 + shift + # shellcheck disable=SC2086 + $run env "${envs[@]}" cargo test --release --lib "$filter" -- "$@" +} + +cmd=${1:-check} +case $cmd in +check) + cargo_test -- odyssey::golden + ;; +tolerance) + cargo_test VPRO_GOLDEN=tolerance -- odyssey::golden + ;; +bless) + cargo_test VPRO_GOLDEN_BLESS=1 -- odyssey::golden::bless --ignored --nocapture + ;; +replay) + cargo_test VPRO_REPLAY_REQUIRE=1 -- odyssey::golden::replay::recorded_sessions --nocapture + ;; +replay-bless) + cargo_test VPRO_REPLAY_BLESS=1 -- odyssey::golden::replay::replay_bless --ignored --nocapture + ;; +*) + sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' + exit 2 + ;; +esac diff --git a/src/config.rs b/src/config.rs index 64e294986..56859b94c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -618,6 +618,22 @@ pub enum GraphicsBoard { /// Maximum IMPACT (2 GE11, 2 RE4). #[clap(name = "maximpact", alias = "impact:max", alias = "max_impact")] MaxImpact, + /// VPro V6: BUZZ A, 32 MB. Octane only (`src/dev/odyssey`). + #[clap(name = "v6", alias = "vpro:v6", alias = "vpro_v6")] + V6, + /// VPro V8: BUZZ A, 128 MB. Octane only. + #[clap(name = "v8", alias = "vpro:v8", alias = "vpro_v8")] + V8, + /// VPro V10: BUZZ B, 32 MB. Octane only. It shipped only in the Octane2 + /// and needs Crossbow 1.4, so the machine takes the Octane2's Crossbow and + /// Bridge revisions. + #[clap(name = "v10", alias = "vpro:v10", alias = "vpro_v10")] + V10, + /// VPro V12: BUZZ B, 128 MB. Octane only. It shipped only in the Octane2 + /// and needs Crossbow 1.4, so the machine takes the Octane2's Crossbow and + /// Bridge revisions. + #[clap(name = "v12", alias = "vpro:v12", alias = "vpro_v12")] + V12, /// No graphics board: the machine runs on its serial console. Octane /// only. #[clap(name = "none")] @@ -642,6 +658,10 @@ impl GraphicsBoard { Self::SolidImpact => "IMPACT Solid", Self::HighImpact => "IMPACT High", Self::MaxImpact => "IMPACT Maximum", + Self::V6 => "VPro V6", + Self::V8 => "VPro V8", + Self::V10 => "VPro V10", + Self::V12 => "VPro V12", Self::None => "None", } } @@ -654,6 +674,10 @@ impl GraphicsBoard { Self::SolidImpact => "solidimpact", Self::HighImpact => "highimpact", Self::MaxImpact => "maximpact", + Self::V6 => "v6", + Self::V8 => "v8", + Self::V10 => "v10", + Self::V12 => "v12", Self::None => "none", } } @@ -662,14 +686,19 @@ impl GraphicsBoard { matches!(self, Self::SolidImpact | Self::HighImpact | Self::MaxImpact) } + /// The VPro boards (`src/dev/odyssey`). + pub fn is_vpro(self) -> bool { + matches!(self, Self::V6 | Self::V8 | Self::V10 | Self::V12) + } + /// 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. pub fn supports(self, profile: MachineProfile) -> bool { match profile { - MachineProfile::OctaneIp30 => self.is_impact() || self == Self::None, - MachineProfile::Indigo2Ip22 => self != Self::None, - _ => !matches!(self, Self::Extreme | Self::None), + MachineProfile::OctaneIp30 => self.is_impact() || self.is_vpro() || self == Self::None, + MachineProfile::Indigo2Ip22 => !self.is_vpro() && self != Self::None, + _ => !self.is_vpro() && !matches!(self, Self::Extreme | Self::None), } } @@ -694,9 +723,13 @@ impl std::str::FromStr for GraphicsBoard { "solidimpact" | "impact:solid" | "solid_impact" | "solid-impact" | "solid" => Ok(Self::SolidImpact), "highimpact" | "impact:high" | "high_impact" | "high-impact" | "high" => Ok(Self::HighImpact), "maximpact" | "impact:max" | "max_impact" | "max-impact" | "maximumimpact" | "maximum_impact" | "maximum-impact" | "max" => Ok(Self::MaxImpact), + "v6" | "vpro:v6" | "vpro_v6" | "vpro-v6" => Ok(Self::V6), + "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), "none" => Ok(Self::None), _ => Err(format!( - "invalid graphics board \"{s}\" (valid: newport, xz, extreme, solidimpact, highimpact, maximpact, impact:solid, impact:high, impact:max, 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, none)" )), } } @@ -1996,7 +2029,7 @@ 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, none)") + format!("graphics.board \"{name}\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, v6, v8, v10, v12, none)") } else { format!("graphics.board \"{name}\" is only valid on the Octane") }); @@ -2726,7 +2759,9 @@ mod export_tests { assert_eq!(cfg.graphics_board(), GraphicsBoard::SolidImpact); cfg.validate().expect("default Octane"); for (t, b) in [("none", GraphicsBoard::None), ("impact:solid", GraphicsBoard::SolidImpact), - ("maximpact", GraphicsBoard::MaxImpact), ("highimpact", GraphicsBoard::HighImpact)] { + ("maximpact", GraphicsBoard::MaxImpact), ("highimpact", GraphicsBoard::HighImpact), + ("v6", GraphicsBoard::V6), ("vpro:v8", GraphicsBoard::V8), ("v10", GraphicsBoard::V10), + ("v12", GraphicsBoard::V12), ("vpro:v12", GraphicsBoard::V12)] { let cfg = octane(&format!("[graphics]\nboard = \"{t}\"")); assert_eq!(cfg.graphics_board(), b, "{t}"); cfg.validate().expect(t); @@ -2740,6 +2775,8 @@ mod export_tests { let cfg: MachineConfig = toml::from_str("").expect("parse"); assert_eq!(cfg.graphics_board(), GraphicsBoard::Newport); assert!(!GraphicsBoard::None.supports(MachineProfile::IndyIp24)); + assert!(!GraphicsBoard::V12.supports(MachineProfile::IndyIp24)); + assert!(!GraphicsBoard::V12.supports(MachineProfile::Indigo2Ip22)); } #[test] diff --git a/src/dev/bridge.rs b/src/dev/bridge.rs index a80a1fdb1..57d60a0b3 100644 --- a/src/dev/bridge.rs +++ b/src/dev/bridge.rs @@ -47,7 +47,7 @@ //! //! Ported from hw/pci-host/sgi-bridge.c in jamesbraid/qemu-sgi-origami. -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, Weak}; use parking_lot::{Mutex, RwLock}; @@ -158,6 +158,7 @@ const RESERVED_DATA_HALF: u32 = 0x19c; const ID_REVISION_SHIFT: u32 = 28; const ID_PART_SHIFT: u32 = 12; const ID_MANUFACTURER_SHIFT: u32 = 1; +/// The silicon revision a Bridge reports until the board names its own. const REVISION: u32 = 1; const PART: u32 = 0xc002; const MANUFACTURER: u32 = 0x036; @@ -347,13 +348,13 @@ impl State { /// The chip's own core: the register file, the table and the latched /// errors. The interrupt pins' levels belong to whatever drives them. - fn reset_core(&mut self, flash_strap: bool) { + fn reset_core(&mut self, flash_strap: bool, revision: u32) { for (i, slot) in self.reg.iter_mut().enumerate() { *slot = reg_info(i as u32 * 8).map_or(0, |r| r.reset); } self.set_r( reg::IDENTIFICATION, - (REVISION << ID_REVISION_SHIFT) | (PART << ID_PART_SHIFT) | (MANUFACTURER << ID_MANUFACTURER_SHIFT) | 1, + ((revision & 0xf) << ID_REVISION_SHIFT) | (PART << ID_PART_SHIFT) | (MANUFACTURER << ID_MANUFACTURER_SHIFT) | 1, ); if flash_strap { let s = self.r(reg::STATUS); @@ -392,6 +393,8 @@ pub struct Bridge { slots: RwLock<[Option>; DEVICES]>, flash: RwLock<[Option; 2]>, flash_strap: AtomicBool, + /// The silicon revision in the identification word, bits 31:28. + revision: AtomicU32, /// The one-wire net behind the NIC register; none fitted reads as an /// empty bus. microlan: Mutex>, @@ -499,7 +502,7 @@ impl Bridge { dma_pci_address: 0, dma_device: 0, }; - st.reset_core(false); + st.reset_core(false, REVISION); Arc::new(Self { st: Mutex::new(st), irq_order: Mutex::new(()), @@ -508,6 +511,7 @@ impl Bridge { slots: RwLock::new(Default::default()), flash: RwLock::new(Default::default()), flash_strap: AtomicBool::new(false), + revision: AtomicU32::new(REVISION), microlan: Mutex::new(None), link_alive: RwLock::new(Box::new(|w| w == WIDGET_HEART)), bar_sink: RwLock::new(None), @@ -635,6 +639,16 @@ impl Bridge { st.set_r(reg::STATUS, if select1 { s | STATUS_FLASH_SELECT } else { s & !STATUS_FLASH_SELECT }); } + /// The silicon revision the identification word reports in bits 31:28 + /// (the board's chip: 3 on the 175 MHz Octane, 4 on a measured Octane2). + /// It survives a reset, as the chip's mask does. + pub fn set_revision(&self, revision: u32) { + self.revision.store(revision, Ordering::Release); + let mut st = self.st.lock(); + let id = st.r(reg::IDENTIFICATION); + st.set_r(reg::IDENTIFICATION, (id & !(0xf << ID_REVISION_SHIFT)) | ((revision & 0xf) << ID_REVISION_SHIFT)); + } + /// Tell the Bridge which widgets the Crossbow has a live link to. A DMA /// request to any other widget is dropped by the Crossbow, which the /// Bridge's request timer reports. The default is HEART alone. @@ -680,8 +694,9 @@ impl Bridge { /// re-evaluated against the cleared enables. pub fn reset(&self) { let strap = self.flash_strap.load(Ordering::Acquire); + let revision = self.revision.load(Ordering::Acquire); let mut st = self.st.lock(); - st.reset_core(strap); + st.reset_core(strap, revision); st.pin_level = st.pci_pin_level | st.gpio_pin_level; drop(st); self.publish_bars(); diff --git a/src/dev/bridge_tests.rs b/src/dev/bridge_tests.rs index 1135b3571..cf16dd1a6 100644 --- a/src/dev/bridge_tests.rs +++ b/src/dev/bridge_tests.rs @@ -272,6 +272,26 @@ fn power_up_registers() { assert_eq!(r.get(reg::LINK_CONFIG), LINK_CONFIG_RESET); } +/// The identification word's top nibble is the chip's silicon revision. The +/// 175 MHz Octane reads 3 (an OpenBSD boot prints "Bridge revision 3"); a +/// measured Octane2's PROM monitor read the word as 0x4c00206d, revision 4. +/// A reset keeps the board's revision. +/// +/// QEMU: octane/bridge-identity. +#[test] +fn identification_carries_the_boards_revision() { + let r = Rig::new(); + r.bridge.set_revision(3); + assert_eq!(r.get(reg::IDENTIFICATION), 0x3c00_206d); + r.reset(); + assert_eq!(r.get(reg::IDENTIFICATION), 0x3c00_206d); + + r.bridge.set_revision(4); + assert_eq!(r.get(reg::IDENTIFICATION), 0x4c00_206d); + r.reset(); + assert_eq!(r.get(reg::IDENTIFICATION), 0x4c00_206d); +} + #[test] fn control_strobes_read_zero_and_read_only_registers_ignore_writes() { let r = Rig::new(); diff --git a/src/dev/gl/light.rs b/src/dev/gl/light.rs index a2dd977e7..10c7923a7 100644 --- a/src/dev/gl/light.rs +++ b/src/dev/gl/light.rs @@ -27,7 +27,7 @@ pub struct Material { pub spec: [f32; SPEC_N], } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] #[repr(C)] pub struct Light { pub ambient: [f32; 3], @@ -51,7 +51,7 @@ pub struct Light { } /// Lighting and fog state. Plain data, valid when zeroed (see `init`). -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] #[repr(C)] pub struct Lighting { pub on: u32, @@ -81,6 +81,23 @@ pub struct Lighting { pub fog_color: [f32; 3], } +// All zero, as Lighting's are: `spec` is longer than Default's arrays. +impl Default for Material { + fn default() -> Self { + Material { + emission: [0.0; 3], + ambient: [0.0; 3], + diffuse: [0.0; 4], + specular: [0.0; 3], + shininess: 0.0, + spec_table: 0, + spec_scale: 0.0, + spec_start: 0.0, + spec: [0.0; SPEC_N], + } + } +} + fn dot(a: [f32; 3], b: [f32; 3]) -> f32 { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] } diff --git a/src/dev/gl/vertex.rs b/src/dev/gl/vertex.rs index 1b65bf8de..d31e92f84 100644 --- a/src/dev/gl/vertex.rs +++ b/src/dev/gl/vertex.rs @@ -80,6 +80,157 @@ pub fn clip_cross(a: &Wv, b: &Wv, da v } +/// A vertex the clipper can cut: its clip- and eye-space positions, and the +/// point a fraction of the way along an edge. `Wv` is one; a board whose +/// vertices carry other attributes implements it for its own type. +pub trait ClipVertex: Copy { + fn h(&self) -> &[f32; 4]; + fn e(&self) -> &[f32; 4]; + /// The point on a-b where a plane with signed distances da and db + /// crosses it. + fn cross(a: &Self, b: &Self, da: f32, db: f32) -> Self; + /// Whether the vertex carries polygon-mode edge flags (the edge from it + /// to the next vertex is a boundary edge). A clipped polygon then keeps + /// the flags of the edges it keeps, flags the edges clipping adds, and + /// adds no vertex where an edge leaves or enters exactly at a vertex. + const EDGE_FLAGS: bool = false; + fn edge(&self) -> bool { + false + } + fn set_edge(&mut self, _edge: bool) {} +} + +impl ClipVertex for Wv { + #[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 { + clip_cross(a, b, da, db) + } +} + +/// How a geometry engine divides, snaps and clips, fixed per board at +/// compile time: `Frustum` for GR2 and IMPACT, `NearW` for VPro. +pub trait Geometry { + /// The view-volume planes (bits 0..5 of `Clip::mask`) that clip + /// geometrically. + const VOLUME: u32; + /// Signed distance of clip position h to view-volume plane i (0..5), + /// >= 0 inside. + fn volume(h: &[f32; 4], i: usize) -> f32; + /// Normalized device coordinates of clip position h; None where it has + /// no window position. + fn divide(h: &[f32; 4]) -> Option<[f32; 3]>; + /// Window coordinate v on the grid of 1/`subpixel` of a pixel. + fn snap(v: f32, subpixel: f32) -> f32; +} + +/// OpenGL's pipeline: clip to the whole view volume -w <= x, y, z <= w, +/// divide by w, round to the nearest grid point. +pub struct Frustum; + +impl Geometry for Frustum { + const VOLUME: u32 = 0x3f; + #[inline(always)] + fn volume(h: &[f32; 4], i: usize) -> f32 { + match i { + 0 => h[3] + h[0], + 1 => h[3] - h[0], + 2 => h[3] + h[1], + 3 => 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 <= 1e-6 { + return None; + } + Some([cx / cw, cy / cw, cz / cw]) + } + #[inline(always)] + fn snap(v: f32, subpixel: f32) -> f32 { + (v * subpixel).round() / subpixel + } +} + +/// VPro's geometry engine as its graded formats describe it. +/// +/// - No near or far plane: the board does not clip the frustum +/// geometrically (inferred), and the raster drops a fragment by its depth +/// instead. +/// - The w plane. The library's preclip is off by default and the library +/// stores positions untested, so what the board does with w <= 0 is +/// unknown. iris clips on w >= `NearW::W_MIN` in place of plane 4, so +/// nothing at or behind the eye reaches the integer edge functions, and +/// the board logs the first time that plane clips anything +/// (`NearW::W_PLANE`). +/// - Planes 0..3 still clip x and y, to a guard band that is a limit of +/// iris's integer edge arithmetic, not of the board. The caller sets +/// the band: `WINDOW_REACH` (8192 pixels about the window) on the +/// combined-matrix path, `GL_WINDOW_GUARD` (16384 pixels from the +/// drawable's origin) on the window-matrix path, each its path's own +/// choice. 8192 matches the library's own class-3 guard (|x| or |y| > +/// 8192 |w|) and is twice the largest drawable (under 4096). The hard +/// limit is the +-65536 check in the board's `project()`. +/// - The divide is a single-precision reciprocal rounded to nearest, then a +/// multiply: the order a vendor-simulated frame shows, where a truncated +/// reciprocal fails it. +/// - Window positions truncate toward minus infinity onto the grid: the +/// best-supported inference (the vendor frames bound the grid to 1/16 +/// pixel or finer). +pub struct NearW; + +impl NearW { + /// The near-w plane's place: w >= W_MIN. The library's own clipper counts + /// a vertex inside at distance >= 0 with no epsilon, and plane 4 could + /// say w >= 0 the same way. iris keeps the smallest normal float so a + /// vertex it keeps has w > 0 and divides. Single-precision + /// interpolation can still land a crossing on w = 0, which `divide` + /// refuses (the primitive is dropped and logged), and the x and y + /// planes already need w >= max(|x|, |y|) >= 0, so the plane only + /// matters for primitives edge-on through the eye. A modelling choice, + /// not a hardware fact. + pub const W_MIN: f32 = f32::MIN_POSITIVE; + /// Bit of the w plane in an outcode or a clip mask. + pub const W_PLANE: u32 = 1 << 4; +} + +impl Geometry for NearW { + const VOLUME: u32 = 0x1f; + #[inline(always)] + fn volume(h: &[f32; 4], i: usize) -> f32 { + match i { + 0 => h[3] + h[0], + 1 => h[3] - h[0], + 2 => h[3] + h[1], + 3 => h[3] - h[1], + _ => h[3] - NearW::W_MIN, + } + } + #[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).floor() / subpixel + } +} + /// The clip planes in use. Plain data, valid zeroed. #[derive(Clone, Copy, Default)] #[repr(C)] @@ -92,30 +243,43 @@ pub struct Clip { impl Clip { /// Signed distance of `v` to clip plane `i` (>= 0 inside). pub fn dist(&self, v: &Wv, i: usize) -> f32 { - let h = &v.h; - match i { - 0 => h[3] + h[0], - 1 => h[3] - h[0], - 2 => h[3] + h[1], - 3 => h[3] - h[1], - 4 => h[3] + h[2], - 5 => h[3] - h[2], - _ => { - let p = &self.user[i - 6]; - p[0] * v.e[0] + p[1] * v.e[1] + p[2] * v.e[2] + p[3] * v.e[3] - } + self.dist_by::(v, i) + } + + /// `dist` under geometry `G`. + #[inline(always)] + pub fn dist_by(&self, v: &V, i: usize) -> f32 { + if i < 6 { + return G::volume(v.h(), i); } + let (p, e) = (&self.user[i - 6], v.e()); + p[0] * e[0] + p[1] * e[1] + p[2] * e[2] + p[3] * e[3] } /// Planes in use: the view volume plus the enabled user planes. pub fn mask(&self) -> u32 { - 0x3f | ((self.user_on & 0x3f) << 6) + self.mask_by::() + } + + /// `mask` under geometry `G`. + #[inline(always)] + pub fn mask_by(&self) -> u32 { + G::VOLUME | ((self.user_on & 0x3f) << 6) } /// Bit i set when `v` is outside plane i. pub fn outcode(&self, v: &Wv) -> u32 { - let mask = self.mask(); - (0..CLIP_PLANES).filter(|&i| mask & (1 << i) != 0 && self.dist(v, i) < 0.0) + self.outcode_by::(v) + } + + /// `outcode` under geometry `G`. A NaN distance counts as inside here + /// (`< 0.0` is false), while the clip loops count it as outside + /// (`>= 0.0` is false); a NaN vertex then reaches `project()`, which + /// drops it. + #[inline(always)] + pub fn outcode_by(&self, v: &V) -> u32 { + let mask = self.mask_by::(); + (0..CLIP_PLANES).filter(|&i| mask & (1 << i) != 0 && self.dist_by::(v, i) < 0.0) .fold(0, |o, i| o | (1 << i)) } @@ -127,8 +291,19 @@ impl Clip { if any == 0 { return n; } - let mut n = n; let mut tmp = [Wv::default(); MAX_POLY]; + self.clip_polygon_by::(v, &mut tmp, n, any).0 + } + + /// `clip_polygon` under geometry `G` for the planes in `any` (the + /// vertices' outcodes together), with `tmp` as large a workspace as + /// `v`. Also says whether the polygon outgrew `v`, whose excess + /// vertices are then dropped. + #[inline(always)] + pub fn clip_polygon_by(&self, v: &mut [V], tmp: &mut [V], n: usize, any: u32) -> (usize, bool) { + let cap = v.len().min(tmp.len()); + let mut n = n; + let mut over = false; for plane in 0..CLIP_PLANES { if any & (1 << plane) == 0 || n == 0 { continue; @@ -136,35 +311,62 @@ impl Clip { let mut m = 0; for i in 0..n { let (a, b) = (&v[i], &v[(i + 1) % n]); - let (da, db) = (self.dist(a, plane), self.dist(b, plane)); - if da >= 0.0 && m < MAX_POLY { - tmp[m] = *a; - m += 1; - } - if (da >= 0.0) != (db >= 0.0) && m < MAX_POLY { - tmp[m] = clip_cross(a, b, da, db); - m += 1; + let (da, db) = (self.dist_by::(a, plane), self.dist_by::(b, plane)); + if da >= 0.0 { + if m < cap { + tmp[m] = *a; + m += 1; + } else { + over = true; + } + if !(db >= 0.0) { + if V::EDGE_FLAGS && da == 0.0 { + // Leaving at a: the edge clipping adds starts there. + // m >= 1: da >= 0 just pushed a (m == 0 < cap, cap >= 1) or set over. + tmp[m - 1].set_edge(true); + } else if m < cap { + tmp[m] = V::cross(a, b, da, db); + tmp[m].set_edge(true); + m += 1; + } else { + over = true; + } + } + } else if db >= 0.0 && !(V::EDGE_FLAGS && db == 0.0) { + if m < cap { + tmp[m] = V::cross(a, b, da, db); + tmp[m].set_edge(a.edge()); + m += 1; + } else { + over = true; + } } } v[..m].copy_from_slice(&tmp[..m]); n = m; } - n + (n, over) } /// Clip the segment a-b; None when it is entirely outside. - pub fn clip_line(&self, mut a: Wv, mut b: Wv) -> Option<(Wv, Wv)> { + pub fn clip_line(&self, a: Wv, b: Wv) -> Option<(Wv, Wv)> { let any = a.oc | b.oc; + self.clip_line_by::(a, b, any) + } + + /// `clip_line` under geometry `G` for the planes in `any`. + #[inline(always)] + pub fn clip_line_by(&self, mut a: V, mut b: V, any: u32) -> Option<(V, V)> { for plane in 0..CLIP_PLANES { if any & (1 << plane) == 0 { continue; } - let (da, db) = (self.dist(&a, plane), self.dist(&b, plane)); + let (da, db) = (self.dist_by::(&a, plane), self.dist_by::(&b, plane)); match (da >= 0.0, db >= 0.0) { (true, true) => {} (false, false) => return None, - (true, false) => b = clip_cross(&a, &b, da, db), - (false, true) => a = clip_cross(&a, &b, da, db), + (true, false) => b = V::cross(&a, &b, da, db), + (false, true) => a = V::cross(&a, &b, da, db), } } Some((a, b)) @@ -197,14 +399,18 @@ impl Viewport { /// offset by `origin` and snapped to 1/`subpixel` of a pixel (the /// raster engine's fixed-point precision; removes float noise such as /// 319.99998 on edges). - pub fn project(&self, mut out: Wv, origin: [f32; 2], subpixel: f32) -> Wv { - let [cx, cy, cz, cw] = out.h; + pub fn project(&self, out: Wv, origin: [f32; 2], subpixel: f32) -> Wv { + self.project_by::(out, origin, subpixel) + } + + /// `project` under geometry `G`. + #[inline(always)] + pub fn project_by(&self, mut out: Wv, origin: [f32; 2], subpixel: f32) -> Wv { out.ok = 0; - if cw <= 1e-6 { + let Some([nx, ny, nz]) = G::divide(&out.h) else { return out; - } - let (nx, ny, nz) = (cx / cw, cy / cw, cz / cw); - let snap = |v: f32| (v * subpixel).round() / subpixel; + }; + let snap = |v: f32| G::snap(v, subpixel); out.x = snap(origin[0] + self.x0 + (nx + 1.0) * 0.5 * (self.x1 - self.x0 + 1.0)); out.y = snap(origin[1] + self.y0 + (ny + 1.0) * 0.5 * (self.y1 - self.y0 + 1.0)); out.z = self.zcenter + nz * self.zscale; @@ -246,6 +452,124 @@ mod tests { assert!(v[..n].iter().all(|w| w.h[0] <= 1.0 + 1e-6)); } + type Ov = super::Wv<0, 0>; + + fn nearw(x: f32, y: f32, z: f32, w: f32) -> Ov { + let mut v = Ov { h: [x, y, z, w], ..Default::default() }; + v.oc = Clip::default().outcode_by::(&v); + v + } + + #[test] + fn nearw_multiplies_by_the_reciprocal_where_frustum_divides() { + // 7/49 and 5/49 differ in the last place between x * (1/w) and x / w. + for (x, w) in [(7.0f32, 49.0f32), (5.0, 49.0)] { + let h = [x, 0.0, 0.0, w]; + let n = NearW::divide(&h).unwrap()[0]; + let f = Frustum::divide(&h).unwrap()[0]; + assert_eq!(n, x * (1.0 / w)); + assert_eq!(f, x / w); + assert_ne!(n, f, "{x}/{w}"); + } + } + + #[test] + fn nearw_floors_to_the_grid_where_frustum_rounds() { + let v = 3.9999998f32; + assert_eq!(NearW::snap(v, 256.0), 3.9960938); + assert_eq!(Frustum::snap(v, 256.0), 4.0); + // Toward minus infinity, not toward zero. + assert_eq!(NearW::snap(-0.001, 256.0), -1.0 / 256.0); + assert_eq!(NearW::snap(-3.0, 256.0), -3.0); + } + + #[test] + fn nearw_has_no_z_plane() { + assert_eq!(NearW::VOLUME, 0x1f); + let c = Clip::default(); + for z in [-5.0, 5.0] { + assert_eq!(c.outcode_by::(&nearw(0.0, 0.0, z, 1.0)), 0); + assert_ne!(c.outcode::<4, 1>(&at_z(z)), 0); + } + let mut v = [nearw(0.0, 0.0, 9.0, 1.0), nearw(0.5, 0.0, -9.0, 1.0), nearw(0.0, 0.5, 9.0, 1.0)]; + let mut tmp = v; + let (n, over) = c.clip_polygon_by::(&mut v, &mut tmp, 3, 0); + assert_eq!((n, over), (3, false)); + assert_eq!(v[1].h[2], -9.0); + } + + fn at_z(z: f32) -> Wv { + let mut v = Wv { h: [0.0, 0.0, z, 1.0], ..Default::default() }; + v.oc = Clip::default().outcode(&v); + v + } + + #[test] + fn nearw_clips_at_the_w_plane() { + let c = Clip::default(); + let behind = nearw(0.0, 0.0, 0.0, -1.0); + let front = nearw(0.0, 0.0, 0.0, 1.0); + assert_eq!(behind.oc & NearW::W_PLANE, NearW::W_PLANE); + assert_eq!(front.oc, 0); + // The x and y planes already cut the edge at w = 0, and the w plane + // leaves it there or one rounding step above: nothing kept is behind + // the eye. + let (a, b) = c.clip_line_by::(front, behind, behind.oc | front.oc).unwrap(); + assert_eq!(a.h[3], 1.0); + assert!(b.h[3] >= 0.0 && b.h[3] < 1e-30, "{}", b.h[3]); + // Wholly behind the eye: nothing. + let behind2 = nearw(0.1, 0.0, 0.0, -2.0); + assert!(c.clip_line_by::(behind, behind2, behind.oc | behind2.oc).is_none()); + // Nothing divides at or behind the eye. + assert!(NearW::divide(&[0.0, 0.0, 0.0, 0.0]).is_none()); + assert!(NearW::divide(&behind.h).is_none()); + } + + #[derive(Clone, Copy)] + struct Ef { + h: [f32; 4], + edge: bool, + } + + impl ClipVertex for Ef { + fn h(&self) -> &[f32; 4] { + &self.h + } + fn e(&self) -> &[f32; 4] { + &self.h + } + fn cross(a: &Self, b: &Self, da: f32, db: f32) -> Self { + let t = da / (da - db); + Ef { h: std::array::from_fn(|k| a.h[k] + (b.h[k] - a.h[k]) * t), edge: false } + } + const EDGE_FLAGS: bool = true; + fn edge(&self) -> bool { + self.edge + } + fn set_edge(&mut self, edge: bool) { + self.edge = edge; + } + } + + #[test] + fn clipping_keeps_edge_flags_and_flags_the_edges_it_adds() { + let c = Clip::default(); + let ef = |x: f32, edge: bool| Ef { h: [x, 0.0, 0.0, 1.0], edge }; + // Room for the vertex clipping adds: a triangle clipped by one plane + // can have four. + let mut v = [ef(0.0, true), ef(2.0, false), ef(0.0, false), ef(0.0, false)]; + v[2].h[1] = 0.5; + let any = v[..3].iter().fold(0, |o, w| o | c.outcode_by::(w)); + let mut tmp = v; + let (n, over) = c.clip_polygon_by::(&mut v, &mut tmp, 3, any); + assert!(!over); + assert_eq!(n, 4); + // Kept vertex 0 keeps its flag; the leaving edge's new vertex is a + // boundary edge; the entering edge's new vertex inherits the entry + // edge's flag (false); the last kept vertex keeps its own. + assert_eq!(v.map(|w| w.edge)[..n], [true, true, false, false]); + } + #[test] fn line_outside_one_plane_is_dropped() { assert!(Clip::default().clip_line(at(2.0, 0.0), at(3.0, 0.5)).is_none()); diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 6b4626074..9ef1bb57a 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -27,6 +27,7 @@ pub mod mace_ust; pub mod ns16550; pub mod rad1; pub mod mgras; +pub mod odyssey; pub mod ng1; pub mod serial_host; pub mod serial_log; diff --git a/src/dev/odyssey/bits.rs b/src/dev/odyssey/bits.rs new file mode 100644 index 000000000..ab49a2d03 --- /dev/null +++ b/src/dev/odyssey/bits.rs @@ -0,0 +1,36 @@ +//! Bit-field helpers for register and command words, and byte-order reads. + +/// Bits `start .. start + len` of `v`, sign-extended. +pub(super) fn sextract32(v: u32, start: u32, len: u32) -> i32 { + debug_assert!(len > 0 && start + len <= 32); + ((v << (32 - len - start)) as i32) >> (32 - len) +} + +/// `v` with bits `start .. start + len` replaced by `field`. +pub(super) fn deposit32(v: u32, start: u32, len: u32, field: u32) -> u32 { + debug_assert!(len > 0 && start + len <= 32); + let mask = (u32::MAX >> (32 - len)) << start; + (v & !mask) | ((field << start) & mask) +} + +/// Big- and little-endian words from the front of `b`, which must hold them. +#[inline] +pub(super) fn be16(b: &[u8]) -> u16 { + u16::from_be_bytes([b[0], b[1]]) +} +#[inline] +pub(super) fn be32(b: &[u8]) -> u32 { + u32::from_be_bytes([b[0], b[1], b[2], b[3]]) +} +#[inline] +pub(super) fn be64(b: &[u8]) -> u64 { + u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) +} +#[inline] +pub(super) fn le16(b: &[u8]) -> u16 { + u16::from_le_bytes([b[0], b[1]]) +} +#[inline] +pub(super) fn le32(b: &[u8]) -> u32 { + u32::from_le_bytes([b[0], b[1], b[2], b[3]]) +} diff --git a/src/dev/odyssey/board.rs b/src/dev/odyssey/board.rs new file mode 100644 index 000000000..d0b72fe63 --- /dev/null +++ b/src/dev/odyssey/board.rs @@ -0,0 +1,1463 @@ +//! The Odyssey (VPro) board's constants and state. The interrupt packet path +//! is the `irq` field of the host interface (`HostInterface`, interface.rs). +//! +//! Ported from include/hw/misc/sgi-odyssey.h in jamesbraid/qemu-sgi-origami. + +use super::consts::{ + clip, config, flctl, gl_cmd, intr, nic, tex, write, ODSY_CONTROL_WIDGET_MASK, ODSY_DEPTH24_MAX, + ODSY_DEPTH_FUNC_RESET, ODSY_DRAW_MASK_RESET, ODSY_F32_ONE, ODSY_F32_POINT_EIGHT, + ODSY_F32_POINT_TWO, ODSY_XT_REQ_TIMEOUT_RESET, +}; +use super::fifo::CommandFifo; +use super::geometry::LightScratch; +use super::gl_context::{op, vtx, GL_CLIP_PLANES, GL_MODELVIEW, GL_SCISSOR_TEST, GlContext, GlDrawable}; +use super::interface::HostInterface; +use super::memory::{BoardMemory, ClipRect, Packing, Plane, PlaneFormat}; +use super::pixel_dma::GuestMemory; +use super::raster_unit::RasterQueue; +use super::reader::{DisplayShared, PlaneView}; +use super::scanout::Scanout; +#[cfg(test)] +use super::reader::ScanoutReader; +/// Distinct unimplemented offsets reported before the device falls silent +/// about them. Diagnostic bookkeeping, not guest state. +pub(super) const ODSY_UNIMP_REPORTS: u32 = 64; + +/// The vertices an integer GL primitive holds while it streams: a QUADS +/// rectangle's four, or a strip's first and previous vertex. +pub(super) const ODSY_GL_HELD_VERTICES: u32 = 4; + +/// The operands a register-select word's gather keeps: the arity field's +/// largest count, a 16-word burst. +pub(super) const ODSY_CMD_OPERANDS_MAX: u32 = 16; + +/// The window-clip register's four rectangles, two words each. +pub(super) const ODSY_CLIP_RECTS: u32 = 4; + +/// Geometry engine program state: constant-memory entries an 11-bit index +/// reaches. +pub(super) const ODSY_GL_CONSTANTS: u32 = 2048; +/// Off-screen board-memory objects the model keeps (texture levels). A +/// model budget, not a board limit: TyrQuake holds some 1,500 levels at once +/// and samples its oldest every frame, which a table of 1024 forgot. +pub(super) const ODSY_OBJECTS: u32 = 16384; +/// The slices of one volume texture a draw samples (Board::texture_setup()). +pub(super) const ODSY_VOLUME_SLICES: u32 = 1024; +pub(super) const ODSY_OBJ_POOL_BYTES: usize = 128 * 1024 * 1024; +pub(super) const ODSY_OBJ_POOL_WORDS: usize = ODSY_OBJ_POOL_BYTES / 4; +pub(super) const ODSY_OBJ_VALID_BYTES: usize = ODSY_OBJ_POOL_WORDS / 8; +/// The texture level table's entries (0x0e80 .. 0x0ebf). +pub(super) const ODSY_TEX_LEVELS: u32 = 64; +pub(super) const ODSY_TEX_COLOUR_TABLE_ENTRIES: u32 = 256; +pub(super) const ODSY_TEX_DETAIL_FUNCTION_ENTRIES: u32 = 256; +pub(super) const ODSY_GL_ENTRIES: u32 = 4; +pub(super) const ODSY_GL_LIGHTS: u32 = 8; +pub(super) const ODSY_GL_LIGHT_COLOURS: u32 = 25; +pub(super) const ODSY_GL_PROGRAM_REPORTS: u32 = 16; +/// Program memory: 2048 instructions of six halfwords, of which the window +/// at 0x738 is the last 0xc7 (prog::WINDOW_BASE). +pub(super) const ODSY_GL_PROGRAM_ENTRIES: u32 = 2048; +pub(super) const ODSY_GL_WINDOW_ENTRIES: u32 = 0xc7; + +/// The one modeled mode: the desktop target size. +pub(super) const ODSY_WIDTH: u32 = 1280; +pub(super) const ODSY_HEIGHT: u32 = 1024; + +#[derive(Clone, Copy, Default)] +pub(super) struct OpenVertex { + pub word: u32, + pub operands: [u32; 4], + pub attribute: [[u32; 5]; 19], + pub slot: u32, +} + +/// Diagnostic bookkeeping, not guest state: what has been reported once. +pub(super) struct Reports { + pub memory_control_reported: bool, + pub setup_7f0_reported: bool, + pub stats_reported: bool, + pub overlay_mode_reported: bool, + pub cursor_reported: u32, + pub draw_mask_stereo_reported: bool, + pub draw_mask_high_reported: bool, + pub drawable_word_reported: bool, + pub obj_full_reported: bool, + pub obj_forget_reported: bool, + pub board_oob_reported: bool, + pub tex_rect_reported: bool, + pub gl_aux_reported: bool, + pub aux_mask_reported: bool, + /// A rectangle copy with exactly one base at the id-plane base was + /// refused and noted once. + pub copy_mixed_base_reported: bool, + pub aux_image_reported: bool, + pub gl_rect_depth_logged: bool, + pub gl_rect_stencil_logged: bool, + pub gl_rect_pattern_logged: bool, + pub gl_index_mask_logged: bool, + pub gl_program_logged: [u32; (ODSY_GL_PROGRAM_REPORTS * 3) as usize], + pub gl_program_logged_count: u32, + pub gl_program_mem_reported: bool, + pub gl_two_sided_logged: bool, + pub gl_texgen_logged: bool, + pub gl_clip_logged: bool, + pub gl_texture_stage_logged: bool, + pub gl_block_logged: bool, + pub ctx_open_prim_logged: bool, + pub ctx_shadow_fail_reported: bool, + pub gl_dither_reported: bool, + pub colour_mode_reported: bool, + pub gl_index_lit_reported: bool, + pub image_code_reported: bool, + pub reported_addr: [std::sync::atomic::AtomicU32; ODSY_UNIMP_REPORTS as usize], + pub reported_addr_count: std::sync::atomic::AtomicU32, + pub reported_cmd: [u32; ODSY_UNIMP_REPORTS as usize], + pub reported_cmd_count: u32, + pub line_stipple_reported: bool, + pub dma_bpp_reported: bool, + pub dma_readback_reported: bool, + pub dma_code_reported: bool, + /// One bit per dma::REPORT_* reason. + pub dma_read_reported: u32, +} + +/// The board-memory objects (ODSY_OBJ_POOL_BYTES): off-screen images, +/// textures and saved state. +pub(super) struct Objects { + // The board-memory objects (ODSY_OBJ_POOL_BYTES). + pub mem: BoardMemory, + pub count: u32, + pub used: u32, + pub base: Vec, + /// obj_base inverted, base to index + 1 (Board::object_find()). + pub index: std::collections::HashMap, + pub width: Vec, + pub offset: Vec, + pub rows: Vec, + /// When each object was last selected or grown (obj_clock's count). + pub stamp: Vec, + pub clock: u64, + /// ODSY_OBJ_VALID_BYTES bytes. + pub valid: Vec, +} + +/// The texture state words as last pushed. +pub(super) struct TexState { + // The texture state words as last pushed. + pub mode: u32, + pub sampler: [u32; 7], + pub colour_table: [[u32; ODSY_TEX_COLOUR_TABLE_ENTRIES as usize]; 2], + pub colour_table_valid: [u32; 2], + pub detail_function: [u32; ODSY_TEX_DETAIL_FUNCTION_ENTRIES as usize], + pub detail_function_valid: u32, + pub env: [u32; 5], + pub levels: [u32; (ODSY_TEX_LEVELS * 3) as usize], + pub slice_offset: [u32; (ODSY_VOLUME_SLICES + 2) as usize], + pub slice_width: [u32; (ODSY_VOLUME_SLICES + 2) as usize], + pub level0: u32, + pub lod: [u32; 4], + /// The 0x203b post-filter scale and bias words, R, G, B, A. + pub scale_bias: [u32; 8], + /// The 0x0c3e texture offset words. + pub offset: [u32; 3], + pub coord: [u32; 2], + pub coord_r: u32, + pub coord_q: u32, + pub linear_plane: [[u32; 4]; 2], + pub linear_plane_r: [u32; 4], + pub linear_plane_q: [u32; 4], + pub linear_valid: u32, +} + +/// The current-query and raster-position records the kernel reads back. +pub(super) struct Query { + // The command-FIFO interpreter (0x110000). + pub current: [[u32; 5]; 5], + pub captured: [[u32; 5]; 5], + /// The index, colour and texture-coordinate records the raster position + /// latched (indexed like `query_current`; the others are unused). + pub latched: [[u32; 5]; 5], + pub current_valid: u32, + pub latched_valid: u32, + pub captured_valid: u32, + pub stage: u32, + /// The open query's kind (word 0x4c0 + kind). + pub kind: u32, + pub count: u32, + pub sync_index: u32, + pub epoch: u64, + pub captured_epoch: u64, + pub base: u64, + pub sync_base: u64, +} + +/// The BEGIN-0xa image raster path. +pub(super) struct ImageRaster { + // The BEGIN-0xa image raster path. + pub active: bool, + pub x1: u32, + pub y1: u32, + pub x2: u32, + pub y2: u32, + pub cur_x: u32, + pub cur_y: u32, + pub fmt: u32, + pub group_len: u32, + pub group_painted: u32, + pub code: u32, + /// The header's whole write code, format and type (write::CODE_MASK). + pub write_code: u32, + /// The bytes of the pixel a write-table image is part way through. + pub pixel: [u8; 16], + pub bytes: u32, + pub nbytes: u32, +} + +/// The command-FIFO interpreter (0x110000): the word being parsed and its +/// operands. +pub(super) struct CommandParse { + pub state: u32, + pub tag: u32, + pub operand: [u32; ODSY_CMD_OPERANDS_MAX as usize], + pub operand_count: u32, + pub operand_index: u32, + pub from_kernel: bool, +} + +/// Where the current context's window is, and the window as the kernel last +/// pushed it on its own port. +pub(super) struct Window { + // Where the current context's window is. + pub origin: [i32; 2], + pub read_origin: [i32; 2], + pub clip_words: [u32; (2 * ODSY_CLIP_RECTS) as usize], + /// The second operand of the last 0x840 (enable, mask and value). + pub id_compare: u32, + pub clip_kept: u32, + pub clip_rect: [ClipRect; ODSY_CLIP_RECTS as usize], + // The window as the kernel last pushed it on its own port. + pub committed_origin: [i32; 2], + pub committed_clip: [u32; (2 * ODSY_CLIP_RECTS) as usize], + pub committed_id_compare: u32, + pub prior_origin: [i32; 2], + pub origin_pending: bool, + pub after_tail: bool, +} + +/// The widget's configuration and the host-side registers that live on the +/// board: the properties, the modelled registers and the back end's indexed +/// file. +pub(super) struct Widget { + // Properties. + pub revision: u32, + /// megabytes + pub memory_size: u32, + pub memory_banks: u32, + pub cas_latency: u32, + /// bits 2:0 of 0x23380c + pub backend_version: u32, + pub product: String, + pub nic_serial: u64, + pub nic_record: String, + // Composed in Board::configure from the properties above. + /// The product's record, if nic-record unset. + pub product_record: String, + pub memory_config: u32, + // Modeled registers. + pub flctl_config: u32, + /// The memory controller's configuration word (+0x1004), as written. + pub memory_control: u32, + /// The FIFO set-up registers 0x6b0-0x6b3 and 0x7b5, in that order, as + /// last written. Values only: their roles are unknown. + pub fifo_setup: [u32; 5], + /// Register 0x7f0 as last written. Its unit and meaning are unknown; the + /// model records the value and runs no timer. Whether a nonzero value has + /// been reported. + pub setup_7f0: u32, + // The DMA-sync command's host-write base (48-bit guest physical address). + pub host_write_addr_upper: u32, + pub host_write_addr_lower: u32, + // The statistics buffer address (0x401c, 0x4024) and control (0x402c). + pub stats_addr_upper: u32, + pub stats_addr_lower: u32, + pub stats_control: u32, + // The kernel driver's context switch save address (0x4004/0x400c). + pub ctx_shadow_addr_upper: u32, + pub ctx_shadow_addr_lower: u32, + pub buzz: [u32; 6], + pub nic: u32, + pub i2c_main_cmd: u32, + pub i2c_opt_cmd: u32, + /// Back-end entries 0x2000-0x2fff as last written (0x1000 entries). The + /// colour lookup RAM, the display modes, the overlay modes and the + /// cursor, which the display reads, are in `scanout.shared.regs`. + pub backend_ctl: Vec, + pub backend_index: u32, + pub backend_count: u32, + // Command-FIFO self-test. + pub selftest_progress: u32, + pub cfifo_selftest_ready: bool, +} + +/// The geometry engine's program state: lighting, materials, the transforms +/// it keeps and the program it runs. +pub(super) struct Geometry { + // Lighting: the geometry engine's program state. + pub constants: [u32; ODSY_GL_CONSTANTS as usize], + pub constant_index: u32, + pub entry: [u32; ODSY_GL_ENTRIES as usize], + pub light_colour: [u32; (ODSY_GL_LIGHT_COLOURS * 4) as usize], + pub light_vec: [u32; (ODSY_GL_LIGHTS * 16) as usize], + pub light_colour_sel: u32, + pub light_vec_sel: u32, + pub material: [u32; 16], + pub shininess_sqrt: u32, + /// Raw back material retained for idle context save, not lighting. + pub back_material: [u32; 16], + pub back_shininess_sqrt: u32, + /// The class 0x35 colour-material word's eight bits. + pub colour_material: u32, + pub normal: [u32; 3], + pub normal_matrix: [u32; 16], + pub colour: [u32; 4], + /// The model-view (slot 0x24), row-major float bits as sent. + pub modelview: [u32; 16], + /// The user clip planes in eye coordinates, four floats' bits a plane. + pub clip_eye: [u32; (GL_CLIP_PLANES * 4) as usize], + pub lit: bool, + pub local_viewer: bool, + /// The geometry program renormalises transformed normals. + pub normalize: bool, + pub colour_overridden: bool, + /// Program memory as uploaded: ODSY_GL_PROGRAM_ENTRIES * 6 + /// halfwords. + pub program_mem: Vec, + pub program_addr: u32, + /// The classes 0x20 to 0x35 as written; 0x27 also sets gl_program_addr. + pub regs_20_35: [u32; 22], + pub program_half: u32, + /// Derived from the retained program list. + pub linear_texgen: u32, +} + +/// The board's planes: colour buffers, depth, stencil, drawable id and +/// overlay. The raster unit owns them while it has jobs (raster_unit.rs). +pub(super) struct Planes { + /// Colour buffer 0's RGBA8 plane over the shared board memory. + pub colour: BoardMemory, + /// The id BoardMemory::add_plane returned. + pub colour_plane: u32, + /// The second colour buffer (buffer 1 in the displayed-buffer sense). + pub buffer1: BoardMemory, + pub buffer1_plane: u32, + /// The per-pixel drawable-id plane, one byte per framebuffer pixel. + pub did: Vec, + /// The overlay pixel byte beside each id byte; same lifetime as did. + pub overlay: Vec, + pub depth: BoardMemory, + pub depth_plane: u32, + pub stencil: BoardMemory, + pub stencil_plane: u32, +} + +/// The 2D stream's GL immediate-mode subset, the X server's and greeter's +/// drawing: points, lines, polygons and the one-bit text pattern. +pub(super) struct Stream2d { + // The 2D stream's GL immediate-mode subset. + pub primitive: u32, + pub color: u32, + pub vertex_x: [u32; ODSY_GL_HELD_VERTICES as usize], + pub vertex_y: [u32; ODSY_GL_HELD_VERTICES as usize], + pub vertex_count: u32, + pub line_stipple: [u32; 3], + /// One byte per framebuffer pixel; crossing marks of an open POLYGON. + pub poly_marks: Vec, + pub poly_row_lo: u32, + pub poly_row_hi: u32, + pub poly_col_lo: u32, + // The one-bit pattern the greeter draws its text with. + pub pattern_buffer: [u32; 32], + pub pattern_shape: u32, + pub pattern_mode: u32, + pub pattern_load_offset: u32, +} + +/// The GL primitive in flight between glBegin and glEnd, and what a context +/// switch needs to restart it. +pub(super) struct OpenPrimitive { + pub in_prim: bool, + pub begin_open: bool, + pub integer_prim: bool, + pub open_r: u32, + pub open_c: u32, + pub open_m3: u32, + pub open_m4: u32, + pub open_held: u32, + pub open_vertex: [OpenVertex; 3], + pub restart_type: u32, + pub restart_select: u32, + pub restart_absorb: u32, + pub restart_total: u32, + pub restart_c: u32, + pub restart_m3: u32, + pub restart_m4: u32, + pub restart_armed: bool, + pub restart_vertex: [[f32; vtx::FLOATS]; 3], +} + +/// The front end's state, which the board's thread owns while it runs +/// stores (`BoardCell`), in parts by concern, each part with a single +/// owner (MGRAS instead splits its front, engine and raster state by +/// thread). A processor reaches it only through `BoardLocks::board()`, with +/// the FIFO drained or for the whole-word reads `BoardCell` names. The other threads' state is not here: the raster +/// unit's is `RasterCtx`, the display's `DisplayShared`, and the registers a +/// processor touches without waiting are the host interface's (`front`). +pub(super) struct Board { + /// The crosstalk widget number. + pub slot: u32, + /// The host interface, under its own lock (interface.rs): the registers a + /// processor may touch while the board works, the interrupt routing + /// and the command-FIFO queue. + pub front: std::sync::Arc>, + /// The command FIFO, shared with the host interface. + pub fifo: std::sync::Arc, + /// The queue to BUZZ's raster unit (raster_unit.rs), which owns the + /// planes below while it has jobs. + pub raster: std::sync::Arc, + /// Guest memory as the board's DMA reaches it (HEART's RAM on the + /// Octane). + pub guest: GuestMemory, + pub nic_bus: Option, + + /// The widget's configuration and the host-side registers that live + /// on the board: the properties, the modelled registers and the back + /// end's indexed file. + pub widget: Widget, + + /// Diagnostic bookkeeping, not guest state: what has been reported once. + pub diag: Reports, + + /// The board's planes: colour buffers, depth, stencil, drawable id + /// and overlay. The raster unit owns them while it has jobs + /// (raster_unit.rs). + pub planes: Planes, + + // Buffer selection. + pub draw_mask: u32, + pub draw_word: u32, + pub read_word: u32, + + /// The board-memory objects (ODSY_OBJ_POOL_BYTES): off-screen images, + /// textures and saved state. + pub objects: Objects, + pub dest_base: u32, + + /// The texture state words as last pushed. + pub tex: TexState, + pub dma_seq: u64, + /// Monotonic for this device instance. + pub trace_sequence: std::sync::atomic::AtomicU64, + + /// Tests: the next slice on the board's thread first sleeps this long. + #[cfg(test)] + pub test_slice_sleep_ms: u32, + + /// The current-query and raster-position records the kernel reads back. + pub query: Query, + + /// The command-FIFO interpreter (0x110000): the word being parsed and + /// its operands. + pub cmd: CommandParse, + + /// Where the current context's window is, and the window as the + /// kernel last pushed it on its own port. + pub win: Window, + + /// The 2D stream's GL immediate-mode subset, the X server's and + /// greeter's drawing: points, lines, polygons and the one-bit text + /// pattern. + pub stream: Stream2d, + + // The true-colour OpenGL path. + /// The GL context. During Board::gl_command() it runs outside the board + /// and this holds a stand-in (`gl_spare`), which the draws that command + /// emits must not reach. + pub gl_ctx: Box, + /// The stand-in for `gl_ctx` while a command runs; None during one. + pub gl_spare: Option>, + /// The drawable Board::gl_bind() last built for the shared context. + pub gl_bind_drawable: GlDrawable, + /// The lighting state Board::gl_light() keeps between vertices; None + /// until the first lit vertex. + pub gl_light_scratch: Option>, + /// The GL primitive in flight between glBegin and glEnd, and what a + /// context switch needs to restart it. + pub prim: OpenPrimitive, + pub gl_matrix_slot: u32, + pub gl_matrix: [u32; 16], + pub gl_texture_matrix: [u32; 16], + pub gl_rect: [u32; 10], + + // Depth. + pub gl_depth_func: u32, + pub gl_write_mask: u32, + pub gl_combined: [u32; 16], + pub gl_block16: [u32; 4], + + /// The scissor and window clip box, 0x081d, as last written. + pub gl_clip_box: [u32; 2], + pub gl_fog: [u32; 6], + + /// The geometry engine's program state: lighting, materials, the + /// transforms it keeps and the program it runs. + pub geom: Geometry, + + // The current fill's colour source. + pub gl_color_index: u32, + pub gl_color_is_index: bool, + + /// The colour-mode words as last sent, class 0x22 then 0x23. + pub colour_mode: [u32; 2], + + pub dest_buffer_word: u32, + pub plane_write_mask: u32, + + /// The colour buffer's base: the first operand of the last colour 0x1017 + /// block (0x2000 for the screen). + pub dma_colour_base: u32, + /// The first colour buffer the X server selected: the screen's. A pixel + /// buffer's select can look the same, and moves dma_colour_base. + pub screen_base: u32, + + // The screen-to-screen copy command's fixed literal template's three runtime slots. + pub copy_src: u32, + pub copy_size: u32, + pub copy_dst: u32, + + /// 0x0c21 operands 1 and 2, the constant colour. + pub const_colour: [u32; 2], + /// 0x0c21 operand 0, the factors and equation, as last written. + pub blend_word: u32, + + /// The BEGIN-0xa image raster path. + pub image: ImageRaster, + + /// The scanout's mode and the marks the display takes. The presented + /// frame and the retrace are the display's own (`ScanoutReader`). + pub scanout: Scanout, + /// Tests: the reader that stands in for the display thread + /// (Board::scanout_update()). + #[cfg(test)] + pub reader: ScanoutReader, +} + +impl Default for Board { + fn default() -> Self { + let front: std::sync::Arc> = Default::default(); + let fifo = front.lock().fifo.clone(); + let raster: std::sync::Arc = Default::default(); + let shared = std::sync::Arc::new(DisplayShared::new(Default::default(), front.clone(), raster.clone())); + let scanout = Scanout::new(shared); + Board { + slot: 0, + front, + fifo, + raster, + guest: None, + nic_bus: None, + widget: Widget { + revision: 0, + memory_size: 0, + memory_banks: 0, + cas_latency: 0, + backend_version: 0, + product: String::new(), + nic_serial: 0, + nic_record: String::new(), + product_record: String::new(), + memory_config: 0, + flctl_config: 0, + memory_control: 0, + fifo_setup: [0; 5], + setup_7f0: 0, + host_write_addr_upper: 0, + host_write_addr_lower: 0, + stats_addr_upper: 0, + stats_addr_lower: 0, + stats_control: 0, + ctx_shadow_addr_upper: 0, + ctx_shadow_addr_lower: 0, + buzz: [0; 6], + nic: 0, + i2c_main_cmd: 0, + i2c_opt_cmd: 0, + backend_ctl: vec![0; 0x1000], + backend_index: 0, + backend_count: 0, + selftest_progress: 0, + cfifo_selftest_ready: false, + }, + diag: Reports { + memory_control_reported: false, + setup_7f0_reported: false, + stats_reported: false, + overlay_mode_reported: false, + cursor_reported: 0, + draw_mask_stereo_reported: false, + draw_mask_high_reported: false, + drawable_word_reported: false, + obj_full_reported: false, + obj_forget_reported: false, + board_oob_reported: false, + tex_rect_reported: false, + gl_aux_reported: false, + aux_mask_reported: false, + copy_mixed_base_reported: false, + aux_image_reported: false, + gl_rect_depth_logged: false, + gl_rect_stencil_logged: false, + gl_rect_pattern_logged: false, + gl_index_mask_logged: false, + gl_program_logged: [0; (ODSY_GL_PROGRAM_REPORTS * 3) as usize], + gl_program_logged_count: 0, + gl_program_mem_reported: false, + gl_two_sided_logged: false, + gl_texgen_logged: false, + gl_clip_logged: false, + gl_texture_stage_logged: false, + gl_block_logged: false, + ctx_open_prim_logged: false, + ctx_shadow_fail_reported: false, + gl_dither_reported: false, + colour_mode_reported: false, + gl_index_lit_reported: false, + image_code_reported: false, + reported_addr: std::array::from_fn(|_| std::sync::atomic::AtomicU32::new(0)), + reported_addr_count: std::sync::atomic::AtomicU32::new(0), + reported_cmd: [0; ODSY_UNIMP_REPORTS as usize], + reported_cmd_count: 0, + line_stipple_reported: false, + dma_bpp_reported: false, + dma_readback_reported: false, + dma_code_reported: false, + dma_read_reported: 0, + }, + planes: Planes { + colour: Default::default(), + colour_plane: 0, + buffer1: Default::default(), + buffer1_plane: 0, + did: Vec::new(), + overlay: Vec::new(), + depth: Default::default(), + depth_plane: 0, + stencil: Default::default(), + stencil_plane: 0, + }, + draw_mask: 0, + draw_word: 0, + read_word: 0, + objects: Objects { + mem: Default::default(), + count: 0, + used: 0, + base: vec![0; ODSY_OBJECTS as usize], + index: Default::default(), + width: vec![0; ODSY_OBJECTS as usize], + offset: vec![0; ODSY_OBJECTS as usize], + rows: vec![0; ODSY_OBJECTS as usize], + stamp: vec![0; ODSY_OBJECTS as usize], + clock: 0, + valid: vec![0; ODSY_OBJ_VALID_BYTES], + }, + dest_base: 0, + tex: TexState { + mode: 0, + sampler: [0; 7], + colour_table: [[0; ODSY_TEX_COLOUR_TABLE_ENTRIES as usize]; 2], + colour_table_valid: [0; 2], + detail_function: [0; ODSY_TEX_DETAIL_FUNCTION_ENTRIES as usize], + detail_function_valid: 0, + env: [0; 5], + levels: [0; (ODSY_TEX_LEVELS * 3) as usize], + slice_offset: [0; (ODSY_VOLUME_SLICES + 2) as usize], + slice_width: [0; (ODSY_VOLUME_SLICES + 2) as usize], + level0: 0, + lod: [0; 4], + scale_bias: [0; 8], + offset: [0; 3], + coord: [0; 2], + coord_r: 0, + coord_q: 0, + linear_plane: [[0; 4]; 2], + linear_plane_r: [0; 4], + linear_plane_q: [0; 4], + linear_valid: 0, + }, + dma_seq: 0, + trace_sequence: std::sync::atomic::AtomicU64::new(0), + #[cfg(test)] + test_slice_sleep_ms: 0, + query: Query { + current: [[0; 5]; 5], + captured: [[0; 5]; 5], + latched: [[0; 5]; 5], + current_valid: 0, + latched_valid: 0, + captured_valid: 0, + stage: 0, + kind: 0, + count: 0, + sync_index: 0, + epoch: 0, + captured_epoch: 0, + base: 0, + sync_base: 0, + }, + cmd: CommandParse { + state: 0, + tag: 0, + operand: [0; ODSY_CMD_OPERANDS_MAX as usize], + operand_count: 0, + operand_index: 0, + from_kernel: false, + }, + win: Window { + origin: [0; 2], + read_origin: [0; 2], + clip_words: [0; (2 * ODSY_CLIP_RECTS) as usize], + id_compare: 0, + clip_kept: 0, + clip_rect: std::array::from_fn(|_| Default::default()), + committed_origin: [0; 2], + committed_clip: [0; (2 * ODSY_CLIP_RECTS) as usize], + committed_id_compare: 0, + prior_origin: [0; 2], + origin_pending: false, + after_tail: false, + }, + stream: Stream2d { + primitive: 0, + color: 0, + vertex_x: [0; ODSY_GL_HELD_VERTICES as usize], + vertex_y: [0; ODSY_GL_HELD_VERTICES as usize], + vertex_count: 0, + line_stipple: [0; 3], + poly_marks: Vec::new(), + poly_row_lo: 0, + poly_row_hi: 0, + poly_col_lo: 0, + pattern_buffer: [0; 32], + pattern_shape: 0, + pattern_mode: 0, + pattern_load_offset: 0, + }, + gl_ctx: Box::default(), + gl_spare: Some(Box::default()), + gl_bind_drawable: Default::default(), + gl_light_scratch: None, + prim: OpenPrimitive { + in_prim: false, + begin_open: false, + integer_prim: false, + open_r: 0, + open_c: 0, + open_m3: 0, + open_m4: 0, + open_held: 0, + open_vertex: std::array::from_fn(|_| Default::default()), + restart_type: 0, + restart_select: 0, + restart_absorb: 0, + restart_total: 0, + restart_c: 0, + restart_m3: 0, + restart_m4: 0, + restart_armed: false, + restart_vertex: [[0.0; vtx::FLOATS]; 3], + }, + gl_matrix_slot: 0, + gl_matrix: [0; 16], + gl_texture_matrix: [0; 16], + gl_rect: [0; 10], + gl_depth_func: 0, + gl_write_mask: 0, + gl_combined: [0; 16], + gl_block16: [0; 4], + gl_clip_box: [0; 2], + gl_fog: [0; 6], + geom: Geometry { + constants: [0; ODSY_GL_CONSTANTS as usize], + constant_index: 0, + entry: [0; ODSY_GL_ENTRIES as usize], + light_colour: [0; (ODSY_GL_LIGHT_COLOURS * 4) as usize], + light_vec: [0; (ODSY_GL_LIGHTS * 16) as usize], + light_colour_sel: 0, + light_vec_sel: 0, + material: [0; 16], + shininess_sqrt: 0, + back_material: [0; 16], + back_shininess_sqrt: 0, + colour_material: 0, + normal: [0; 3], + normal_matrix: [0; 16], + colour: [0; 4], + modelview: [0; 16], + clip_eye: [0; (GL_CLIP_PLANES * 4) as usize], + lit: false, + local_viewer: false, + normalize: false, + colour_overridden: false, + program_mem: vec![0; (ODSY_GL_PROGRAM_ENTRIES * 6) as usize], + program_addr: 0, + regs_20_35: [0; 22], + program_half: 0, + linear_texgen: 0, + }, + gl_color_index: 0, + gl_color_is_index: false, + colour_mode: [0; 2], + dest_buffer_word: 0, + plane_write_mask: 0, + dma_colour_base: 0, + screen_base: 0, + copy_src: 0, + copy_size: 0, + copy_dst: 0, + const_colour: [0; 2], + blend_word: 0, + image: ImageRaster { + active: false, + x1: 0, + y1: 0, + x2: 0, + y2: 0, + cur_x: 0, + cur_y: 0, + fmt: 0, + group_len: 0, + group_painted: 0, + code: 0, + write_code: 0, + pixel: [0; 16], + bytes: 0, + nbytes: 0, + }, + scanout, + #[cfg(test)] + reader: Default::default(), + } + } +} + +impl Board { + /// The board-identity record. Its field spelling is the one captured off + /// real firmware, and the prologue (a format byte, four spaces and a + /// semicolon, so a scanning reader meets ";Name:" and a positional reader + /// finds the value at byte 11) follows the Bridge model's convention. + /// + /// One field by default, because the stock PROM was observed reading + /// exactly one 32-byte page and stopping; a longer record is truncated + /// where the master stops clocking. Without a nic-record, a selected + /// product supplies its board's name, and otherwise the default is the + /// public owner record of the board the Odyssey contract describes. + pub fn build_nic_rom(&mut self, rom: &mut [u8; nic::ROM_SIZE]) { + let fields: &str = if !self.widget.nic_record.is_empty() { + &self.widget.nic_record + } else if !self.widget.product_record.is_empty() { + &self.widget.product_record + } else { + nic::RECORD_DEFAULT + }; + let record = format!("\u{1} ;{};", fields); + let record = record.as_bytes(); + let length = record.len().min(nic::ROM_SIZE); + + rom.fill(0xff); + rom[..length].copy_from_slice(&record[..length]); + if length < nic::PAGE as usize { + rom[length..nic::PAGE as usize].fill(b' '); + } + } + + pub fn attach_nic(&mut self) -> Result<(), String> { + let mut rom = [0u8; nic::ROM_SIZE]; + + if self.widget.nic_serial & !nic::SERIAL_MASK != 0 { + return Err(format!("the sgi-odyssey one-wire serial {:#x} does not fit in 48 bits", self.widget.nic_serial)); + } + self.build_nic_rom(&mut rom); + let mut bus = crate::dev::w1::Bus::new(); + bus.add(Box::new(crate::dev::w1::Ds2505::new(nic::FAMILY as u8, self.widget.nic_serial, &rom))); + self.nic_bus = Some(bus); + Ok(()) + } + + pub fn compose_memory_config(&mut self) -> Result<(), String> { + let code: u32 = match self.widget.memory_size { + 32 => 0, + 64 => 1, + 128 => 2, + _ => { + return Err(format!("sgi-odyssey memory-size must be 32, 64 or 128 megabytes, not {}", self.widget.memory_size)); + } + }; + if self.widget.memory_banks != 2 && self.widget.memory_banks != 4 { + return Err(format!("sgi-odyssey memory-banks must be 2 or 4, not {}", self.widget.memory_banks)); + } + if self.widget.cas_latency != 2 && self.widget.cas_latency != 3 { + return Err(format!("sgi-odyssey cas-latency must be 2 or 3, not {}", self.widget.cas_latency)); + } + // The outstanding-operation count and the data-FIFO level, which the + // driver waits to reach zero, stay zero: neither is modeled. + self.widget.memory_config = code << config::SIZE_SHIFT + | (if self.widget.memory_banks == 4 { config::FOUR_BANKS } else { 0 }) + | (if self.widget.cas_latency == 2 { config::LATENCY_2 } else { 0 }) + | config::COPY_FIELD; + Ok(()) + } + + pub fn reset_registers(&mut self) { + // The driver reads its own widget number out of the low nibble before + // anything has written the register. The rest of the reset value is not + // established, so it is zero. + let identity = self.identity(); + let mut f = self.front.lock(); + f.identity = identity; + f.widget_control = self.slot & ODSY_CONTROL_WIDGET_MASK; + f.xt_req_timeout = ODSY_XT_REQ_TIMEOUT_RESET; + f.int_dest_upper = 0; + f.int_dest_lower = 0; + f.error_command = 0; + // The command FIFO is empty out of reset, so its low-water condition + // holds and the low-water interrupt is pending. The kernel driver's + // board reset arms a watchdog and then tears the board down unless the + // driver sees this interrupt once, which is what lets it create the + // direct rendering node the X server opens. Latch it at reset, because + // the guest cannot reach the later FIFO-drain edge (a command + // submission) until that node exists. Masked here until the driver + // enables the source; enabling it then asserts. + f.intr_flags = intr::FIFO_LOW_WATER; + f.intr_enable = 0; + // The level falls with the enables. The routed destination is reset + // below, so no clearing packet has anywhere to go. + f.interrupt_asserted = false; + f.flag_held = false; + f.sw_token = 0; + f.flctl_target = 0; + f.flctl_target_reported = false; + f.route_dest_upper = 0; + f.route_dest_lower = 0; + f.route_vectors = 0; + f.swap_arm = 0; + f.swap_pending = 0; + f.swap_control = 0; + f.swap_control_companion = 0; + f.swap_set = 0; + f.swap_clear = 0; + f.displayed_buffer = 0; + f.displayed_written = false; + drop(f); + self.widget.flctl_config = flctl::CONFIG_RESET; + self.widget.memory_control = 0; + self.widget.fifo_setup = [0; 5]; + self.widget.setup_7f0 = 0; + self.widget.host_write_addr_upper = 0; + self.widget.host_write_addr_lower = 0; + self.widget.stats_addr_upper = 0; + self.widget.stats_addr_lower = 0; + self.widget.stats_control = 0; + self.widget.ctx_shadow_addr_upper = 0; + self.widget.ctx_shadow_addr_lower = 0; + self.widget.buzz = [0; 6]; + self.draw_mask = ODSY_DRAW_MASK_RESET; + self.draw_word = 0; + self.read_word = 0; + self.diag.draw_mask_stereo_reported = false; + self.diag.draw_mask_high_reported = false; + self.diag.drawable_word_reported = false; + self.widget.nic = 0; + self.widget.i2c_main_cmd = 0; + self.widget.i2c_opt_cmd = 0; + self.widget.backend_ctl.fill(0); + self.regs().reset(); + self.diag.overlay_mode_reported = false; + self.diag.cursor_reported = 0; + self.diag.gl_aux_reported = false; + self.diag.aux_mask_reported = false; + self.diag.copy_mixed_base_reported = false; + self.diag.aux_image_reported = false; + self.widget.backend_index = 0; + self.widget.backend_count = 0; + self.diag.reported_addr_count.store(0, std::sync::atomic::Ordering::Relaxed); + self.diag.reported_cmd_count = 0; + self.diag.line_stipple_reported = false; + self.diag.dma_bpp_reported = false; + self.diag.dma_readback_reported = false; + self.diag.dma_code_reported = false; + self.diag.dma_read_reported = 0; + self.widget.selftest_progress = 0; + self.widget.cfifo_selftest_ready = false; + + self.query_cancel(); + self.query_records_reset(); + self.win.origin = [0; 2]; + self.win.read_origin = [0; 2]; + for i in 0..ODSY_CLIP_RECTS as usize { + self.win.clip_words[2 * i] = if i != 0 { clip::RESET_EMPTY } else { 0 }; + self.win.clip_words[2 * i + 1] = if i != 0 { 0 } else { clip::RESET_FULL }; + } + self.clip_update(); + self.win.id_compare = 0; + self.win.committed_origin = self.win.origin; + self.win.committed_clip = self.win.clip_words; + self.win.committed_id_compare = 0; + self.win.prior_origin = [0; 2]; + self.win.origin_pending = false; + self.win.after_tail = false; + self.cmd.from_kernel = false; + self.stream.primitive = 0; + self.stream.color = 0; + // GL's initial index is 1 (OpenGL 1.2.1 section 2.7), the value the + // index record's default saves and a replay restores, so a context that + // never sets an index draws GL vertices in index 1 before and after its + // first switch. The board's reset index is unknown; this is a model + // default. Limitation: the integer rectangle fill takes the index only + // after an index word, so with none since reset it draws the packed + // colour 0, and index 1 after a replay. Only a register test reaches + // that: the X server sends an index or a colour before every fill. + self.gl_color_index = 1; + self.gl_color_is_index = false; + // Out of reset the mode is unknown; RGBA until a context says otherwise. + self.colour_mode = [0; 2]; + self.diag.colour_mode_reported = false; + self.diag.gl_index_lit_reported = false; + self.dest_buffer_word = 0; + self.plane_write_mask = 0; + self.dma_colour_base = 0; + self.screen_base = 0; + self.dest_base = 0; + self.objects.count = 0; + self.objects.used = 0; + self.objects.base.fill(0); + self.objects.index.clear(); + self.objects.width.fill(0); + self.objects.offset.fill(0); + self.objects.rows.fill(0); + self.objects.stamp.fill(0); + self.objects.clock = 0; + self.objects.valid.fill(0); + self.diag.obj_full_reported = false; + self.diag.obj_forget_reported = false; + self.diag.board_oob_reported = false; + self.tex.mode = 0; + self.tex.sampler = [0; 7]; + for t in self.tex.colour_table.iter_mut() { + t.fill(0); + } + self.tex.colour_table_valid = [0; 2]; + self.tex.detail_function.fill(0); + self.tex.detail_function_valid = 0; + self.tex.env = [0; 5]; + self.tex.levels.fill(0); + self.tex.level0 = 0; + self.tex.lod[0] = 0; + self.tex.lod[1] = 0; + self.tex.lod[2] = 0x3100; + self.tex.lod[3] = 0x0f00; + for c in 0..4usize { + self.tex.scale_bias[c * 2] = tex::SCALE_BIAS_ONE; + self.tex.scale_bias[c * 2 + 1] = 0; + } + self.tex.offset = [0; 3]; + self.tex.coord = [0; 2]; + self.tex.coord_r = 0; + self.tex.coord_q = ODSY_F32_ONE; + self.diag.tex_rect_reported = false; + self.stream.vertex_x.fill(0); + self.stream.vertex_y.fill(0); + self.stream.vertex_count = 0; + self.stream.line_stipple[0] = 0xffff; + self.stream.line_stipple[1] = 0xffff; + self.stream.line_stipple[2] = 0; + // No polygon is open; Board::reset clears the marks themselves. + self.stream.poly_row_lo = ODSY_HEIGHT; + self.stream.poly_row_hi = 0; + self.stream.poly_col_lo = ODSY_WIDTH; + // The shared GL context to OpenGL's initial state (identity matrices, + // white current colour, depth test off), unbound until the first GL + // command binds the drawable. A guest reset can land mid-primitive, with + // an open polygon's vertex buffer still allocated; GlContext::reset() frees + // it before reinitializing. It is safe here even before Board::allocate + // allocates the colour plane. Its viewport starts empty: nothing is + // drawn until the client's combined matrix arrives. + self.gl().reset(gl_cmd::CONTEXT_HW_ID); + { + // Opt into the shared context's per-face polygon modes (0x0415) and + // line stipple: unset, it silently keeps every face at legacy FILL + // no matter what 0x0415 decodes to. GlContext::reset() above has + // already cleared in_begin, the one precondition, even when reset + // lands mid-primitive. + assert!(self.gl().enable_polygon_modes()); + assert!(self.gl().enable_line_stipple()); + } + self.prim.in_prim = false; + self.prim.begin_open = false; + self.prim.integer_prim = false; + self.prim.open_r = 0; + self.prim.open_c = 0; + self.prim.open_m3 = 0; + self.prim.open_m4 = 0; + self.prim.open_held = 0; + self.prim.restart_type = 0; + self.prim.restart_select = 0; + self.prim.restart_absorb = 0; + self.prim.restart_total = 0; + self.prim.restart_c = 0; + self.prim.restart_m3 = 0; + self.prim.restart_m4 = 0; + self.prim.restart_armed = false; + // The clip box and its scissor test: GlContext::init() leaves the shared + // context's scissor box and test both at zero, which would clip away + // every draw if the test were simply switched on. There is no reset + // pattern-mode value that already matches a usable scissor default, so + // the full window is set here explicitly before the test is enabled, + // once and for good. + self.gl_clip_box[0] = clip::BOX_FIELD_MASK & (ODSY_WIDTH - 1); + self.gl_clip_box[1] = clip::BOX_FIELD_MASK & (ODSY_HEIGHT - 1); + self.gl_fog = [0; 6]; + self.gl_apply_clip_box(); + { + let scissor_test = GL_SCISSOR_TEST; + + self.gl_feed(op::ENABLE as u16, &[scissor_test]); + } + self.gl_matrix_slot = GL_MODELVIEW; + self.geom.modelview = [0; 16]; + for i in 0..4usize { + self.geom.modelview[i * 5] = ODSY_F32_ONE; + } + self.geom.clip_eye.fill(0); + self.gl_matrix = [0; 16]; + self.gl_texture_matrix = [0; 16]; + for i in 0..4usize { + self.gl_texture_matrix[i * 5] = ODSY_F32_ONE; + } + self.gl_rect = [0; 10]; + self.gl_depth_func = ODSY_DEPTH_FUNC_RESET; + self.gl_write_mask = write::MASK_RESET; + self.gl_combined = [0; 16]; + // The identity projection's (-0, -0, -0, 1): the depth row unflipped. + self.gl_block16[0] = 0x80000000; + self.gl_block16[1] = 0x80000000; + self.gl_block16[2] = 0x80000000; + self.gl_block16[3] = 0x3f800000; + self.diag.gl_rect_depth_logged = false; + self.diag.gl_rect_stencil_logged = false; + self.diag.gl_rect_pattern_logged = false; + self.diag.gl_index_mask_logged = false; + // Lighting out of reset. The hardware's values are unknown; the model + // starts at GL's initial material, light-model ambient, normal and + // colour, zero light colours and vectors (the library loads every light + // before a lit draw), an identity normal matrix and no program. + self.geom.constants.fill(0); + // Program memory's reset content is unknown; the kernel loads it. + self.geom.program_mem.fill(0); + self.geom.program_addr = 0; + self.geom.regs_20_35 = [0; 22]; + self.geom.program_half = 0; + self.diag.gl_program_mem_reported = false; + self.geom.constant_index = 0; + self.geom.entry = [0; ODSY_GL_ENTRIES as usize]; + // GL's own initial state, colour material off, as the library sends. + self.geom.colour_material = 0; + self.geom.light_colour.fill(0); + for i in 0..3usize { + self.geom.light_colour[gl_cmd::LIGHT_MODEL_AMBIENT as usize * 4 + i] = ODSY_F32_POINT_TWO; + self.geom.material[i] = ODSY_F32_POINT_TWO; // ambient + self.geom.material[4 + i] = ODSY_F32_POINT_EIGHT; // diffuse + self.geom.material[8 + i] = 0; // specular + self.geom.material[12 + i] = 0; // emission + } + for i in 0..4usize { + self.geom.material[i * 4 + 3] = ODSY_F32_ONE; + self.geom.colour[i] = ODSY_F32_ONE; + } + self.geom.light_colour[gl_cmd::LIGHT_MODEL_AMBIENT as usize * 4 + 3] = ODSY_F32_ONE; + self.geom.light_vec.fill(0); + self.geom.light_colour_sel = 0; + self.geom.light_vec_sel = 0; + self.geom.shininess_sqrt = 0; + self.back_material_reset(); + self.geom.normal[0] = 0; + self.geom.normal[1] = 0; + self.geom.normal[2] = ODSY_F32_ONE; + self.geom.normal_matrix = [0; 16]; + for i in 0..4usize { + self.geom.normal_matrix[i * 5] = ODSY_F32_ONE; + } + self.geom.lit = false; + self.geom.local_viewer = false; + self.geom.normalize = false; + self.geom.colour_overridden = false; + self.diag.gl_program_logged_count = 0; + self.diag.gl_two_sided_logged = false; + self.diag.gl_texgen_logged = false; + self.geom.linear_texgen = 0; + self.tex.linear_plane = [[0; 4]; 2]; + self.tex.linear_plane_r = [0; 4]; + self.tex.linear_plane_q = [0; 4]; + self.tex.linear_valid = 0; + self.diag.gl_clip_logged = false; + self.diag.gl_texture_stage_logged = false; + self.diag.gl_block_logged = false; + self.diag.ctx_open_prim_logged = false; + self.diag.ctx_shadow_fail_reported = false; + self.diag.gl_dither_reported = false; + self.scanout.shared.group25.store(false, std::sync::atomic::Ordering::Relaxed); + self.copy_src = 0; + self.copy_size = 0; + self.copy_dst = 0; + self.stream.pattern_buffer = [0; 32]; + self.stream.pattern_shape = 0; + self.const_colour = [0; 2]; + self.blend_word = 0; + self.stream.pattern_mode = 0; + self.stream.pattern_load_offset = 0; + self.image.active = false; + self.image.x1 = 0; + self.image.y1 = 0; + self.image.x2 = 0; + self.image.y2 = 0; + self.image.cur_x = 0; + self.image.cur_y = 0; + self.image.fmt = 0; + self.image.group_len = 0; + self.image.group_painted = 0; + self.image.code = 0; + self.image.write_code = 0; + self.image.bytes = 0; + self.image.nbytes = 0; + self.diag.image_code_reported = false; + self.dma_seq = 0; + } +} + +/// One marketed board: name, revision, memory size, banks, back-end +/// version, identity record. +pub(super) struct Product { + pub name: &'static str, + pub revision: u32, + pub memory_size: u32, + pub memory_banks: u32, + pub backend_version: u32, + pub record: &'static str, +} + +/// The four marketed boards. A driver turns widget revision 1-3 into one +/// part generation and 9-15 into the next, and the board-info tool prints +/// the minor number as the revision's offset less one: revision 2 is "A.1", +/// 10 is "B.1". The inventory names the board from those two facts and the +/// size: A at 32 or 128 MB is V6 or V8, B is V10 or V12. Public captures +/// give A.1 for a 32 MB V6, B.1 for a 32 MB V10 and a 128 MB V12, and four +/// banks for the V12. The V8's A.1 and four banks are inferred from its +/// generation and size; the V10's revision 10 is inferred from working +/// Fuel and Tezro V10s (B.2 boards, revision 11, also exist); the 32 MB +/// boards' two banks are inferred, since the tool prints a fixed "Banks: 2" +/// line for any 32 MB board. Every product is CAS latency 3 with back-end +/// (PB&J) version 1. The identity record is the Octane board's name as the +/// inventory prints it, with its part number beside each entry. Fuel and +/// Tezro boards are other assemblies; a profile answers the Octane name. +pub(super) static PRODUCTS: [Product; 4] = [ + Product { name: "v12", revision: 0xa, memory_size: 128, memory_banks: 4, backend_version: 1, record: "Name:ODY128VERSIONB" }, // 030-1611 + Product { name: "v10", revision: 0xa, memory_size: 32, memory_banks: 2, backend_version: 1, record: "Name:ODY32VERSIONB" }, // 030-1655 + Product { name: "v8", revision: 0x2, memory_size: 128, memory_banks: 4, backend_version: 1, record: "Name:ODY128" }, // 030-1404 + Product { name: "v6", revision: 0x2, memory_size: 32, memory_banks: 2, backend_version: 1, record: "Name:ODY32" }, // 030-1403 +]; + +impl Board { + pub fn resolve_product(&mut self) -> Result<(), String> { + if self.widget.product.is_empty() { + return Ok(()); + } + for p in PRODUCTS.iter() { + if self.widget.product == p.name { + self.widget.revision = p.revision; + self.widget.memory_size = p.memory_size; + self.widget.memory_banks = p.memory_banks; + self.widget.backend_version = p.backend_version; + self.widget.product_record = p.record.to_string(); + return Ok(()); + } + } + Err(format!("sgi-odyssey product must be v12, v10, v8 or v6, not {}", self.widget.product)) + } + + pub fn configure(&mut self) -> Result<(), String> { + self.resolve_product()?; + if self.widget.revision > 0xf { + return Err(format!("sgi-odyssey revision must fit in four bits, not {}", self.widget.revision)); + } + self.compose_memory_config()?; + self.reset_registers(); + Ok(()) + } + + pub fn allocate(&mut self) -> Result<(), String> { + self.attach_nic()?; + + // The colour framebuffer is two RGBA8 planes, one per colour buffer, + // each over device-backed board memory of its own: a model-owned byte + // array. It is not page-backed onto guest RAM. add_plane can only fail + // on a programming error in these fixed sizes. + let pixels = ODSY_WIDTH as u64 * ODSY_HEIGHT as u64; + self.planes.colour.init(pixels * 4); + { + let plane = Plane { + format: PlaneFormat::Rgba8, + packing: Packing::BeRedHigh, + width: ODSY_WIDTH, + height: ODSY_HEIGHT, + stride: ODSY_WIDTH * 4, + base: 0, + len: pixels * 4, + bottom_origin: false, + ..Default::default() + }; + + self.planes.colour_plane = self.planes.colour.add_plane(&plane).map_err(|e| format!("sgi-odyssey: colour plane: {e}"))?; + // Colour buffer 1, the same shape in its own memory. Two model + // planes preserve the visible contract; they do not claim the + // board's two-buffer layout. + self.planes.buffer1.init(plane.len); + self.planes.buffer1_plane = self.planes.buffer1.add_plane(&plane).map_err(|e| format!("sgi-odyssey: colour buffer 1 plane: {e}"))?; + } + + // The board-memory object store: device-backed words the objects are + // laid in by Board::object_rows(), addressed directly. + self.objects.mem.init(ODSY_OBJ_POOL_BYTES as u64); + + self.planes.did = vec![0u8; pixels as usize]; + self.planes.overlay = vec![0u8; pixels as usize]; + self.stream.poly_marks = vec![0u8; pixels as usize]; + + // The depth plane: 24-bit depth in a 32-bit word per frame pixel, in + // device-backed memory of its own. Where the board keeps the depth + // buffer, and its width and layout, are unknown, so no aperture reaches + // this plane. 24 bits matches the Z24 names in the GL library and the depth + // word (depth_word.rs). + self.planes.depth.init(pixels * 4); + { + let depth = Plane { + format: PlaneFormat::Z24, + packing: Packing::BeRedHigh, + width: ODSY_WIDTH, + height: ODSY_HEIGHT, + stride: ODSY_WIDTH * 4, + base: 0, + len: pixels * 4, + bottom_origin: false, + ..Default::default() + }; + + self.planes.depth_plane = self.planes.depth.add_plane(&depth).map_err(|e| format!("sgi-odyssey: depth plane: {e}"))?; + } + + // The stencil plane: 8 bits a pixel, in device-backed memory of its own; + // like the depth plane, no aperture reaches it. + self.planes.stencil.init(pixels); + { + let stencil = Plane { + format: PlaneFormat::S8, + packing: Packing::BeRedHigh, + width: ODSY_WIDTH, + height: ODSY_HEIGHT, + stride: ODSY_WIDTH, + base: 0, + len: pixels, + bottom_origin: false, + ..Default::default() + }; + + self.planes.stencil_plane = self.planes.stencil.add_plane(&stencil).map_err(|e| format!("sgi-odyssey: stencil plane: {e}"))?; + } + + // The display reads the planes through a view that lives as long as + // they do: they are never resized or moved after this. + if let Some(view) = PlaneView::of(self) { + let _ = self.scanout.shared.planes.set(view); + } + + // Setting the mode marks the scanout's frame all dirty and + // restarts the retrace at the mode's rate. + self.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + Ok(()) + } + + pub fn reset(&mut self) { + self.work_reset(); + // The raster unit finishes what it was given before the planes clear. + self.raster_wait(); + self.reset_registers(); + self.front.lock().update_irq(); + // Board::configure() also calls Board::reset_registers(), before + // the board memory exists; this runs after Board::allocate(), so the + // memory exists here. Zero the colour planes' bytes directly; the + // planes themselves are fixed at allocation and stay. Redraw because + // reset clears them. + self.planes.colour.bytes.fill(0); + self.planes.buffer1.bytes.fill(0); + self.objects.mem.bytes.fill(0); + self.planes.stencil.bytes.fill(0); + self.planes.did.fill(0); + self.planes.overlay.fill(0); + self.stream.poly_marks.fill(0); + // The depth plane starts at all ones (a model choice). The far plane, + // 2S, is word 0xfffffe, and every drawn depth is at most that, so all + // ones, the largest word, compares as farther than any fragment. + let depth = self.planes.depth.plane(self.planes.depth_plane).expect("the depth plane"); + for y in 0..ODSY_HEIGHT as i32 { + for x in 0..ODSY_WIDTH as i32 { + self.planes.depth.plane_store(&depth, x, y, ODSY_DEPTH24_MAX); + } + } + // Back to the boot size, then repaint the cleared plane and restart + // the phase. + if self.scanout.mode.width != ODSY_WIDTH || self.scanout.mode.height != ODSY_HEIGHT { + self.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + } + self.scanout.reset(); + } +} + +/// The synthetic one-wire serial for a component letter and index. +pub(super) fn synthetic_onewire_serial(letter: char, index: u32) -> u64 { + 0x51454d55u64 << 16 | (letter as u64) << 8 | index as u64 +} + +impl Board { + /// A board of `product` (v12, v10, v8 or v6) in crosstalk widget + /// `slot`: board revision A with a synthetic serial number in its + /// one-wire NIC, and the memory the product carries. An error names a + /// configuration the board cannot have. + pub fn new(product: &str, slot: u32) -> Result, String> { + let mut s = Box::new(Board::default()); + + s.objects.index = Default::default(); + // The fixed part of the drawable Board::gl_bind() hands over. + s.gl_bind_drawable.bound = true; + s.gl_bind_drawable.front = 0; + s.gl_bind_drawable.back = 1; + s.widget.revision = 0xa; + s.widget.memory_size = 128; + s.widget.memory_banks = 4; + s.widget.cas_latency = 3; + s.widget.backend_version = 1; + s.widget.nic_serial = synthetic_onewire_serial('G', 0); + s.widget.product = product.to_string(); + s.slot = slot; + s.configure()?; + s.allocate()?; + Ok(s) + } +} diff --git a/src/dev/odyssey/command.rs b/src/dev/odyssey/command.rs new file mode 100644 index 000000000..03864a9c5 --- /dev/null +++ b/src/dev/odyssey/command.rs @@ -0,0 +1,1425 @@ +//! The command-FIFO interpreter: word classification, operand gathering and +//! the one big dispatch of a completed command. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use super::bits::sextract32; +use super::board::{Board, ODSY_GL_LIGHTS, ODSY_GL_LIGHT_COLOURS, ODSY_TEX_LEVELS}; +use super::consts::{ + alpha, blend, cmd, cmd_arity, dest, dma, gl_cmd, image, line, mode, pattern, polygon, prog, + query, reg, stencil, swap, tex, write, ODSY_DEPTH_FUNC_MASK, ODSY_DRAW_MASK_BITS, + ODSY_DRAW_MASK_RIGHT, ODSY_F32_ONE, ODSY_FENCE_VALID, ODSY_FIFO_SETUP_7B5_INDEX, + ODSY_LOGIC_OP_MASK, ODSY_ORIGIN_BITS, ODSY_RASTER_POS_LATCH_WORD, +}; +use super::dma::{ + float_channel, integer_colour, is_integer_colour, signed_to_float, ODSY_CMD_COPY_RECT, + ODSY_CMD_GL_RECT, ODSY_CMD_IDLE, ODSY_CMD_IMAGE_BOX_LOAD, ODSY_CMD_IMAGE_BURST, + ODSY_CMD_MATRIX_LOAD, ODSY_CMD_OPERANDS, ODSY_CMD_PATTERN_LOAD, ODSY_CMD_SKIP, +}; +use super::draw::{float_to_u32, image_burst_fmt, pack_pixel}; +use super::geometry::{f32_bits, odyssey_f32}; +use super::gl::{ + blend_factor, float_index, is_program_word, polygon_mode, prog_value, stencil_op, + BLEND_EQUATION, GL_LIGHT_WORDS, +}; +use super::gl_context::{ + op, GL_BACK, GL_CLEAR, GL_CLIP_PLANES, GL_FRONT, GL_MODELVIEW, + GL_NEVER_ENUM, +}; +impl Board { + pub fn cmd_dispatch(&mut self, tag: u32) { + let mut after_tail = false; + + // Only the kernel command right after its window-origin write can be its tail. + if self.cmd.from_kernel && tag != reg::WINDOW_ORIGIN && tag != reg::WINDOW_CLIP { + self.win.origin_pending = false; + } + // And only the one right after the tail's 0x204b is its 0x840. + if self.cmd.from_kernel { + after_tail = self.win.after_tail; + self.win.after_tail = false; + } + if is_program_word(tag) { + self.gl_program_word(tag); + return; + } + if (reg::TEX_DETAIL_FUNCTION..reg::TEX_DETAIL_FUNCTION + tex::DETAIL_FUNCTION_BLOCKS).contains(&tag) + { + let block = tag - reg::TEX_DETAIL_FUNCTION; + + if self.cmd.operand_count != tex::DETAIL_FUNCTION_BLOCK_WORDS { + self.tex.detail_function_valid &= !(1u32 << block); + self.cmd_unimplemented(tag); + return; + } + let w = tex::DETAIL_FUNCTION_BLOCK_WORDS as usize; + let at = block as usize * w; + self.tex.detail_function[at..at + w].copy_from_slice(&self.cmd.operand[..w]); + self.tex.detail_function_valid |= 1u32 << block; + return; + } + if (reg::TEX_TABLE3..reg::TEX_TABLE3 + tex::TABLE3_BLOCKS).contains(&tag) { + self.cmd_unimplemented(tag); + return; + } + if (reg::TEX_COLOUR_TABLE0..reg::TEX_COLOUR_TABLE0 + tex::COLOUR_TABLE_BLOCKS).contains(&tag) + || (reg::TEX_COLOUR_TABLE1..reg::TEX_COLOUR_TABLE1 + tex::COLOUR_TABLE_BLOCKS).contains(&tag) + { + let plane = (tag >= reg::TEX_COLOUR_TABLE1) as usize; + let first_tag = if plane != 0 { reg::TEX_COLOUR_TABLE1 } else { reg::TEX_COLOUR_TABLE0 }; + let block = tag - first_tag; + + if self.cmd.operand_count != tex::COLOUR_TABLE_BLOCK_WORDS { + self.cmd_unimplemented(tag); + return; + } + let w = tex::COLOUR_TABLE_BLOCK_WORDS as usize; + let at = block as usize * w; + self.tex.colour_table[plane][at..at + w].copy_from_slice(&self.cmd.operand[..w]); + self.tex.colour_table_valid[plane] |= 1u32 << block; + return; + } + if (reg::TEX_LEVEL..reg::TEX_LEVEL + ODSY_TEX_LEVELS).contains(&tag) { + let at = (tag - reg::TEX_LEVEL) as usize * 3; + self.tex.levels[at..at + 3].copy_from_slice(&self.cmd.operand[..3]); + return; + } + if tag == gl_cmd::TEXCOORD_3F || tag == gl_cmd::TEXCOORD_4F { + self.tex.coord.copy_from_slice(&self.cmd.operand[..2]); + self.tex.coord_r = self.cmd.operand[2]; + self.tex.coord_q = if tag == gl_cmd::TEXCOORD_4F { self.cmd.operand[3] } else { ODSY_F32_ONE }; + return; + } + if tag == gl_cmd::TEXCOORD_1I + || tag == gl_cmd::TEXCOORD_2I + || tag == gl_cmd::TEXCOORD_3I + || tag == gl_cmd::TEXCOORD_4I + { + let n = cmd_arity(tag); + let mut c: [u32; 4] = [0, 0, 0, ODSY_F32_ONE]; + + for i in 0..n as usize { + c[i] = f32_bits((self.cmd.operand[i] as i32) as f32); + } + self.tex.coord[0] = c[0]; + self.tex.coord[1] = c[1]; + self.tex.coord_r = c[2]; + self.tex.coord_q = c[3]; + return; + } + if (tag & gl_cmd::TEXCOORD_SHAPE) == gl_cmd::TEXCOORD_TAG && tag != gl_cmd::TEXCOORD_2F { + // Other two-coordinate forms remain observable and change nothing. + self.cmd_unimplemented(tag); + return; + } + if (tag & gl_cmd::MATRIX_SELECT_SHAPE) == gl_cmd::MATRIX_SELECT_TAG { + self.geom.regs_20_35[((gl_cmd::MATRIX_SELECT_TAG >> prog::CLASS_SHIFT) - prog::REG_FIRST) as usize] = + prog_value(tag); + // Pick the shared matrix slot the following load targets. + self.gl_matrix_slot = match tag & 0xff { + gl_cmd::MATRIX_SELECT_COMBINED => gl_cmd::SLOT_COMBINED, + gl_cmd::MATRIX_SELECT_TEXTURE => gl_cmd::SLOT_TEXTURE, + gl_cmd::MATRIX_SELECT_MODELVIEW => GL_MODELVIEW, + gl_cmd::MATRIX_SELECT_BLOCK16 => gl_cmd::SLOT_BLOCK16, + gl_cmd::MATRIX_SELECT_NORMAL => gl_cmd::SLOT_NORMAL_MATRIX, + // Any other block: no home. + _ => gl_cmd::SLOT_NORMAL, + }; + return; + } + match tag { + gl_cmd::COLOR_3UB => { + self.stream.color = pack_pixel(self.cmd.operand[0], self.cmd.operand[1], self.cmd.operand[2]); + self.gl_color_is_index = false; + if !self.gl_index_mode() { + let opd = self.cmd.operand; + + self.gl_feed(op::COLOR3UB as u16, &opd[..3]); + self.geom.colour = self.gl().current_color(); + self.geom.colour_overridden = false; + } + } + gl_cmd::COLOR_INDEX | gl_cmd::REPLAY_INDEX => { + // The user's index word, or the kernel's replay of a saved one. + self.gl_color_index = self.cmd.operand[0]; + self.gl_color_is_index = true; + } + gl_cmd::COLOR_INDEX_F | gl_cmd::REPLAY_INDEX_F => { + self.gl_color_index = float_index(self.cmd.operand[0]); + self.gl_color_is_index = true; + } + reg::DEST_SELECT => { + // Destination-buffer select: its third operand marks an id-plane + // write when it is dest::ID_PLANES. A colour select's non-zero + // first operand names the colour buffer, the base a DMA header's + // destination word is matched against (the all-zero blocks are GL + // context setup, not a buffer). + self.dest_buffer_word = self.cmd.operand[dest::SELECT_VALUE_INDEX]; + self.dest_base = self.cmd.operand[dest::SELECT_BASE_INDEX]; + if self.dest_base == 0 || self.dest_is_id_planes() { + return; + } + let base = self.dest_base; + if !self.board_address_valid(base) || (self.dest_buffer_word & !dest::FLAGS) == 0 { + return; + } + if (self.dest_buffer_word >> dest::FORMAT_SHIFT) == dest::FORMAT_SCREEN + || self.dest_base == self.dma_colour_base + { + self.dma_colour_base = self.dest_base; + if self.screen_base == 0 { + self.screen_base = self.dest_base; + } + } else { + // An off-screen object: create it, or update its row length. + let width = self.cmd.operand[dest::WIDTH_INDEX].wrapping_mul(dest::WIDTH_UNIT); + self.object_select(base, width); + } + } + reg::PLANE_MASK => { + // Plane write mask (mask << 12), consumed by an id-plane write. + self.plane_write_mask = self.cmd.operand[0]; + } + mode::INDEX_ZEROS + | gl_cmd::INDEX_MODE_COLOR_3I + | gl_cmd::INDEX_MODE_COLOR_3F + | gl_cmd::INDEX_MODE_COLOR_4F => { + // An RGBA colour sent in index mode: GL does not draw with it. + } + gl_cmd::VERTEX_2I => { + if self.prim.begin_open { + self.prim.integer_prim = true; + } + // Bit 13 distinguishes the observed GL and X11 integer streams. + // Its hardware meaning is unknown. This is a model routing policy, + // not a claim that the bit enables hardware vertex transforms. + if self.prim.begin_open + && !self.image.active + && ((self.stream.pattern_mode & (1u32 << 13)) != 0 + || (self.tex.mode & (tex::MODE_1D | tex::MODE_2D | tex::MODE_3D)) != 0 + || self.stream.primitive == gl_cmd::TRIANGLES + || self.stream.primitive == gl_cmd::TRIANGLE_STRIP + || self.stream.primitive == gl_cmd::TRIANGLE_FAN + || self.stream.primitive == gl_cmd::QUAD_STRIP) + { + // Lighting takes the signed integers as object coordinates. + let xyz: [f32; 3] = [ + (self.cmd.operand[0] as i32) as f32, + (self.cmd.operand[1] as i32) as f32, + 0.0, + ]; + let opd = self.cmd.operand; + + if !self.prim.in_prim { + let mode = self.stream.primitive; + + self.gl_feed(op::BEGIN as u16, &[mode]); + self.prim.in_prim = true; + } + self.gl_vertex_colour(&xyz); + self.texture_vertex(); + self.gl_feed(op::VERTEX2I as u16, &opd[..2]); + } else { + // The X path uses window-relative integer coordinates. + let x = self.cmd.operand[0].wrapping_add(self.win.origin[0] as u32); + let y = self.cmd.operand[1].wrapping_add(self.win.origin[1] as u32); + self.gl_vertex(x, y); + } + } + gl_cmd::COLOR_3F + | gl_cmd::COLOR_3I + | gl_cmd::COLOR_4F + | gl_cmd::COLOR_3US + | gl_cmd::COLOR_3UI + | gl_cmd::COLOR_3B + | gl_cmd::COLOR_3S + | gl_cmd::COLOR_4UB + | gl_cmd::COLOR_4US + | gl_cmd::COLOR_4UI + | gl_cmd::COLOR_4B + | gl_cmd::COLOR_4S + | gl_cmd::COLOR_4I => { + // The current colour of the true-colour GL stream (glColor3f, + // glColor4f and the integer forms), or the kernel's replay of one + // Board::ctx_save() saved. Kept so an unlit vertex after lit ones + // takes it back, and so colour material lights with it. + let n = cmd_arity(tag); + let mut rgba = [0u32; 4]; + + rgba.copy_from_slice(&self.cmd.operand[..4]); + if tag != gl_cmd::COLOR_3F && tag != gl_cmd::COLOR_4F { + for i in 0..n as usize { + rgba[i] = integer_colour(tag, self.cmd.operand[i]); + } + } + if n == 3 { + rgba[3] = ODSY_F32_ONE; + } + self.geom.colour.copy_from_slice(&rgba); + self.geom.colour_overridden = false; + if n == 4 { + self.gl_feed(op::COLOR4F as u16, &rgba[..4]); + } else { + self.gl_feed(op::COLOR3F as u16, &rgba[..3]); + } + if !self.gl_index_mode() { + self.stream.color = pack_pixel( + float_channel(rgba[0]), + float_channel(rgba[1]), + float_channel(rgba[2]), + ); + self.gl_color_is_index = false; + } + } + gl_cmd::NORMAL_3B | gl_cmd::NORMAL_3S | gl_cmd::NORMAL_3I => { + for i in 0..3 { + self.geom.normal[i] = signed_to_float(self.cmd.operand[i], tag & 0x3f); + } + } + gl_cmd::NORMAL_3F => { + self.geom.normal.copy_from_slice(&self.cmd.operand[..3]); + } + gl_cmd::MATERIAL_AMBIENT + | gl_cmd::MATERIAL_DIFFUSE + | gl_cmd::MATERIAL_SPECULAR + | gl_cmd::MATERIAL_EMISSION => { + // The low byte counts 0x2f, 0x30, 0x31, 0x32 through the four. + let at = (((tag & 0xff) - (gl_cmd::MATERIAL_AMBIENT & 0xff)) * 4) as usize; + self.geom.material[at..at + 4].copy_from_slice(&self.cmd.operand[..4]); + } + gl_cmd::SHININESS_SQRT => { + self.geom.shininess_sqrt = self.cmd.operand[0]; + } + gl_cmd::BACK_AMBIENT | gl_cmd::BACK_DIFFUSE | gl_cmd::BACK_SPECULAR | gl_cmd::BACK_EMISSION => { + let at = (((tag & 0xff) - (gl_cmd::BACK_AMBIENT & 0xff)) * 4) as usize; + self.geom.back_material[at..at + 4].copy_from_slice(&self.cmd.operand[..4]); + } + gl_cmd::BACK_SHININESS_SQRT => { + self.geom.back_shininess_sqrt = self.cmd.operand[0]; + } + gl_cmd::SHININESS_DERIVED | prog::RUN_ONCE => { + // Consumed, not kept: the derived shininess floats and the one-shot + // run have no modelled effect. Block 0x1ea (gl_cmd::SHININESS_DERIVED) + // is also the second read path of attribute slots 448 and 544: every + // full context replay sends it with the selected slot-448 entry's + // first operand, that with bit 31 flipped, and the same for slot 544 + // (ESTABLISHED as the kernel's replay). What the block does on the + // board is UNKNOWN; the model gives neither use an effect. + } + gl_cmd::CLIP_PLANE_LOAD => { + // Public controls identify the effective S, T, R and Q planes. + if self.geom.light_vec_sel == 0x10c { + self.tex.linear_plane_q.copy_from_slice(&self.cmd.operand[..4]); + self.tex.linear_valid |= 8; + return; + } + if self.geom.light_vec_sel == 0x108 { + self.tex.linear_plane_r.copy_from_slice(&self.cmd.operand[..4]); + self.tex.linear_valid |= 4; + return; + } + if self.geom.light_vec_sel == 0x100 || self.geom.light_vec_sel == 0x104 { + let axis = ((self.geom.light_vec_sel - 0x100) / 4) as usize; + + self.tex.linear_plane[axis].copy_from_slice(&self.cmd.operand[..4]); + self.tex.linear_valid |= 1u32 << axis; + return; + } + // See gl_cmd::CLIP_PLANE_BASE's comment: accept and pass through + // loads into the shared layer's six slots, silently drop loads past + // that budget (expected library traffic, not an error), and report + // only a selector this model cannot place at all. + if self.geom.light_vec_sel >= gl_cmd::CLIP_PLANE_BASE + && (self.geom.light_vec_sel - gl_cmd::CLIP_PLANE_BASE).is_multiple_of(gl_cmd::CLIP_PLANE_STRIDE) + { + let plane = (self.geom.light_vec_sel - gl_cmd::CLIP_PLANE_BASE) / gl_cmd::CLIP_PLANE_STRIDE; + + if plane < GL_CLIP_PLANES { + let at = plane as usize * 4; + self.geom.clip_eye[at..at + 4].copy_from_slice(&self.cmd.operand[..4]); + self.gl_apply_clip_plane(plane); + } + } else if !self.diag.gl_block_logged { + self.diag.gl_block_logged = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: class 0x31 clip-plane load at index {:#x} is not modeled", + self.geom.light_vec_sel + ); + } + } + gl_cmd::LIGHT_COLOUR_LOAD => { + if self.geom.light_colour_sel < ODSY_GL_LIGHT_COLOURS { + let at = self.geom.light_colour_sel as usize * 4; + self.geom.light_colour[at..at + 4].copy_from_slice(&self.cmd.operand[..4]); + } else if !self.diag.gl_block_logged { + self.diag.gl_block_logged = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: class 0x32 load at index {:#x} is not modeled", + self.geom.light_colour_sel + ); + } + } + gl_cmd::LIGHT_VEC_LOAD => { + if self.geom.light_vec_sel == 0x100 { + self.tex.linear_plane[0].copy_from_slice(&self.cmd.operand[0..4]); + self.tex.linear_plane[1].copy_from_slice(&self.cmd.operand[4..8]); + self.tex.linear_plane_r.copy_from_slice(&self.cmd.operand[8..12]); + self.tex.linear_plane_q.copy_from_slice(&self.cmd.operand[12..16]); + self.tex.linear_valid |= 0xf; + return; + } + // Index 16 i is light i. Other context setup blocks at 0x80 + // to 0x1e0 remain unknown and are reported once, not per index. + if self.geom.light_vec_sel.is_multiple_of(gl_cmd::LIGHT_VEC_FLOATS) + && self.geom.light_vec_sel / gl_cmd::LIGHT_VEC_FLOATS < ODSY_GL_LIGHTS + { + let at = self.geom.light_vec_sel as usize; + let n = gl_cmd::LIGHT_VEC_FLOATS as usize; + self.geom.light_vec[at..at + n].copy_from_slice(&self.cmd.operand[..n]); + } else if !self.diag.gl_block_logged { + self.diag.gl_block_logged = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: class 0x31 load at index {:#x} is not modeled", + self.geom.light_vec_sel + ); + } + } + gl_cmd::VERTEX_2F | gl_cmd::VERTEX_3F | gl_cmd::VERTEX_3I | gl_cmd::VERTEX_4F => { + // A float vertex uses the shared GL path, and so does an integer 3D + // vertex once converted to float (gl_cmd::VERTEX_3I). Open the + // primitive lazily on its first vertex, as for shared integer + // geometry above. The open-primitive record keeps the word and its + // operands as sent. glVertex4f (gl_cmd::VERTEX_4F) passes its W on. + let components: u32 = if tag == gl_cmd::VERTEX_4F { + 4 + } else if tag == gl_cmd::VERTEX_2F { + 2 + } else { + 3 + }; + let mut xyz = [0u32; 4]; + xyz.copy_from_slice(&self.cmd.operand[..4]); + let mut obj = [0f32; 3]; + + if tag == gl_cmd::VERTEX_3I { + for k in 0..3 { + xyz[k] = f32_bits((self.cmd.operand[k] as i32) as f32); + } + } + if !self.prim.in_prim && self.prim.restart_absorb == 0 { + let mode = self.stream.primitive; + + self.gl_feed(op::BEGIN as u16, &[mode]); + self.prim.in_prim = true; + } + if self.prim.begin_open && !self.prim.integer_prim && self.stream.primitive <= 8 { + self.open_record_vertex(tag); + } + obj[0] = odyssey_f32(xyz[0]); + obj[1] = odyssey_f32(xyz[1]); + obj[2] = if components > 2 { odyssey_f32(xyz[2]) } else { 0.0 }; + if components == 4 && odyssey_f32(xyz[3]) != 0.0 { + // (x, y, z, w) and (x/w, y/w, z/w, 1) are the same point. + for c in &mut obj[..3] { + *c /= odyssey_f32(xyz[3]); + } + } + self.gl_vertex_colour(&obj); + self.texture_vertex(); + if self.prim.restart_absorb != 0 { + let slot = (self.prim.restart_total - self.prim.restart_absorb) as usize; + + if slot < self.prim.restart_vertex.len() { + self.prim.restart_vertex[slot] = + self.gl_ctx.capture_vertex(self.prim.restart_type, &xyz, components); + } + self.prim.restart_absorb -= 1; + self.restart_finish(); + return; + } + let op = if components == 4 { + op::VERTEX4F + } else if components == 3 { + op::VERTEX3F + } else { + op::VERTEX2F + }; + self.gl_feed(op as u16, &xyz[..components as usize]); + } + gl_cmd::LOAD_4F => { + self.gl_load_block16(); + } + reg::DEPTH_FUNC => { + if (self.cmd.operand[0] & !ODSY_DEPTH_FUNC_MASK) != 0 { + // Only bits 2:0 are known; every capture leaves the rest 0. + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: depth function word {:#x} has bits beyond 2:0, not modeled", + self.cmd.operand[0] + ); + } + self.gl_depth_func = self.cmd.operand[0]; + self.gl_apply_depth(); + } + reg::WRITE_MASK => { + let stencil_mask = (self.cmd.operand[0] >> write::STENCIL_SHIFT) & write::STENCIL_MASK; + + self.gl_write_mask = self.cmd.operand[0]; + self.gl_apply_depth(); + // The library forces it to 0 for a drawable with no stencil. + self.gl_feed(op::STENCIL_MASK as u16, &[stencil_mask]); + } + reg::ALPHA_FUNC => { + let word = self.cmd.operand[0]; + let mut args = [0u32; 2]; + + args[0] = GL_NEVER_ENUM + ((word >> alpha::FUNC_SHIFT) & alpha::FUNC_MASK); + // The reference is a 12-bit field, x4096 with 1.0 sent as 4095 + // (ESTABLISHED). Decoding it as field / 4096 is INFERRED; the + // vendor frames rule out field / 4095. + let field = word & alpha::REF_MASK; + let reference = if field == alpha::REF_ONE { 1.0 } else { field as f32 / alpha::REF_SCALE }; + args[1] = f32_bits(reference); + self.gl_feed(op::ALPHA_FUNC as u16, &args); + } + reg::STENCIL_FUNC => { + let word = self.cmd.operand[0]; + let mut args = [0u32; 3]; + + args[0] = GL_NEVER_ENUM + ((word >> stencil::FUNC_SHIFT) & stencil::FUNC_MASK); + args[1] = word & stencil::REF_MASK; + args[2] = (word >> stencil::VALUE_MASK_SHIFT) & stencil::VALUE_MASK_MASK; + self.gl_feed(op::STENCIL_FUNC as u16, &args); + + let fail = stencil_op((word >> stencil::OP_FAIL_SHIFT) & stencil::OP_MASK); + let zfail = stencil_op((word >> stencil::OP_ZFAIL_SHIFT) & stencil::OP_MASK); + let zpass = stencil_op((word >> stencil::OP_ZPASS_SHIFT) & stencil::OP_MASK); + if fail == u32::MAX || zfail == u32::MAX || zpass == u32::MAX { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: stencil op word {:#x} is not modeled", word); + } else { + args[0] = fail; + args[1] = zfail; + args[2] = zpass; + self.gl_feed(op::STENCIL_OP as u16, &args); + } + } + reg::BLEND => { + let mut args = [0u32; 4]; + + // Every operand is kept whatever bit 17 says (mode::BLEND): the + // library sends glBlendFunc's factors, and the X server its + // opaque-stipple background, before the 0x0404 that sets the bit. + self.const_colour[0] = self.cmd.operand[1]; + self.const_colour[1] = self.cmd.operand[2]; + self.blend_word = self.cmd.operand[0]; + let word = self.cmd.operand[0]; + let eq = (word >> blend::EQ_SHIFT) & blend::EQ_MASK; + let src = blend_factor((word >> blend::SRC_SHIFT) & blend::SRC_MASK, false); + let dst = blend_factor(word & blend::DST_MASK, true); + if src == u32::MAX || dst == u32::MAX { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: blend factor word {:#x} is not modeled", word); + } else { + args[0] = src; + args[1] = dst; + self.gl_feed(op::BLEND_FUNC as u16, &args[..2]); + } + if eq == 3 || eq == 4 { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: blend equation {} (the SGIX alpha min/max extension) is not modeled", + eq + ); + } else { + args[0] = BLEND_EQUATION[eq as usize]; + self.gl_feed(op::BLEND_EQUATION as u16, &args[..1]); + } + let c1 = self.cmd.operand[1]; + let c2 = self.cmd.operand[2]; + args[0] = f32_bits(((c1 >> blend::COLOUR_SHIFT) & blend::COLOUR_MASK) as f32 / blend::COLOUR_SCALE); + args[1] = f32_bits((c1 & blend::COLOUR_MASK) as f32 / blend::COLOUR_SCALE); + args[2] = f32_bits(((c2 >> blend::COLOUR_SHIFT) & blend::COLOUR_MASK) as f32 / blend::COLOUR_SCALE); + args[3] = f32_bits((c2 & blend::COLOUR_MASK) as f32 / blend::COLOUR_SCALE); + self.gl_feed(op::BLEND_COLOR as u16, &args); + } + reg::LOGIC_OP => { + let mode = GL_CLEAR + (self.cmd.operand[0] & ODSY_LOGIC_OP_MASK); + + self.gl_feed(op::LOGIC_OP as u16, &[mode]); + } + reg::LINE_POINT_SIZE => { + let mut args = [0u32; 2]; + + args[0] = f32_bits( + (self.cmd.operand[0] & line::POINT_SIZE_MASK) as f32 / line::POINT_SIZE_SCALE, + ); + self.gl_feed_raster_state(op::LINE_WIDTH as u16, &args[0..1]); + args[1] = f32_bits( + (self.cmd.operand[1] & line::POINT_SIZE_MASK) as f32 / line::POINT_SIZE_SCALE, + ); + self.gl_feed_raster_state(op::POINT_SIZE as u16, &args[1..2]); + } + reg::POLYGON_MODE => { + let mut front = [0u32; 2]; + let mut back = [0u32; 2]; + + front[0] = GL_FRONT; + front[1] = polygon_mode( + (self.cmd.operand[0] >> polygon::MODE_FRONT_SHIFT) & polygon::MODE_MASK, + ); + self.gl_feed_raster_state(op::POLYGON_MODE as u16, &front); + back[0] = GL_BACK; + back[1] = polygon_mode( + (self.cmd.operand[0] >> polygon::MODE_BACK_SHIFT) & polygon::MODE_MASK, + ); + self.gl_feed_raster_state(op::POLYGON_MODE as u16, &back); + } + reg::POLYGON_OFFSET => { + let mut args = [0u32; 2]; + + args[0] = f32_bits(0.0f32); + args[1] = f32_bits( + sextract32(self.cmd.operand[0], 0, polygon::OFFSET_BITS) as f32 * polygon::OFFSET_SCALE, + ); + self.gl_feed(op::POLYGON_OFFSET_EXT as u16, &args); + } + reg::CLIP_BOX => { + self.gl_clip_box[0] = self.cmd.operand[0]; + self.gl_clip_box[1] = self.cmd.operand[1]; + self.gl_apply_clip_box(); + } + reg::FOG => { + let n = self.gl_fog.len(); + self.gl_fog.copy_from_slice(&self.cmd.operand[..n]); + } + reg::TEX_MODE => { + self.tex.mode = self.cmd.operand[0]; + self.gl_apply_clip_planes(); + } + reg::TEX_SAMPLER => { + let n = self.tex.sampler.len(); + self.tex.sampler.copy_from_slice(&self.cmd.operand[..n]); + } + reg::TEX_ENV => { + let n = self.tex.env.len(); + self.tex.env.copy_from_slice(&self.cmd.operand[..n]); + } + reg::TEX_LEVEL0 => { + self.tex.level0 = self.cmd.operand[0]; + } + reg::TEX_LOD => { + let n = self.tex.lod.len(); + self.tex.lod.copy_from_slice(&self.cmd.operand[..n]); + } + reg::TEX_SCALE_BIAS => { + let n = self.tex.scale_bias.len(); + self.tex.scale_bias.copy_from_slice(&self.cmd.operand[..n]); + } + reg::TEX_OFFSET => { + let n = self.tex.offset.len(); + self.tex.offset.copy_from_slice(&self.cmd.operand[..n]); + } + reg::POST_FILTER_DEFAULT => { + self.cmd_unimplemented(tag); + } + reg::FIFO_SETUP_FIRST..=reg::FIFO_SETUP_LAST => { + self.widget.fifo_setup[(tag - reg::FIFO_SETUP_FIRST) as usize] = self.cmd.operand[0]; + if tag == reg::FIFO_SETUP_6B2 { + self.fifo.set_threshold_6b2(self.cmd.operand[0]); + } + } + reg::FIFO_SETUP_7B5 => { + self.widget.fifo_setup[ODSY_FIFO_SETUP_7B5_INDEX] = self.cmd.operand[0]; + } + reg::SETUP_7F0 => { + self.widget.setup_7f0 = self.cmd.operand[0]; + if self.widget.setup_7f0 != 0 && !self.diag.setup_7f0_reported { + self.diag.setup_7f0_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: register 0x7f0 written with {}; the value is stored and its role is unknown, so no timer runs", + self.widget.setup_7f0 + ); + } + } + gl_cmd::TEXCOORD_1F => { + self.tex.coord[0] = self.cmd.operand[0]; + self.tex.coord[1] = 0; + self.tex.coord_r = 0; + self.tex.coord_q = ODSY_F32_ONE; + } + gl_cmd::TEXCOORD_2F => { + self.tex.coord.copy_from_slice(&self.cmd.operand[..2]); + self.tex.coord_r = 0; + self.tex.coord_q = ODSY_F32_ONE; + } + gl_cmd::TEX_RECT => { + if !self.texture_build_level() && !self.texture_zoom_rect() && !self.diag.tex_rect_reported { + self.diag.tex_rect_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: textured rectangle into buffer {:#x} has unsupported construction state or missing backing", + self.dest_base + ); + } + } + reg::GL_BEGIN => { + // Record the primitive and, for an image, open the raster path. The + // shared GL path opens on a float vertex, an integer GL stream, + // a textured integer vertex or an assembly absent from the 2D + // interpreter. Integer X11 geometry retains the 2D path. + self.stream.primitive = self.cmd.operand[0]; + self.stream.vertex_count = 0; + self.prim.begin_open = true; + self.prim.integer_prim = false; + self.prim.open_r = 0; + self.prim.open_c = 0; + self.prim.open_m3 = 0; + self.prim.open_m4 = 0; + self.prim.open_held = 0; + // A polygon left open by a BEGIN without END encloses nothing. + self.gl_polygon_close(false); + self.image.active = self.cmd.operand[0] == gl_cmd::PRIMITIVE_IMAGE; + if self.image.active { + self.image.x1 = 0; + self.image.y1 = 0; + self.image.x2 = 0; + self.image.y2 = 0; + self.image.cur_x = 0; + self.image.cur_y = 0; + self.image.fmt = 0; + self.image.group_len = 0; + self.image.group_painted = 0; + self.image.bytes = 0; + self.image.nbytes = 0; + } + } + reg::GL_END => { + // A GL geometry primitive closes on the shared context; the greeter's + // QUADS fill (and the console text path's stray END) close on the 2D path. + self.prim.begin_open = false; + self.prim.open_held = 0; + if self.prim.in_prim { + self.gl_feed(op::END as u16, &[]); + self.prim.in_prim = false; + } else { + self.gl_end(); + } + self.prim.integer_prim = false; + } + 0x0400 => { + // Pending primitive for context restore. + self.prim.restart_type = self.cmd.operand[0]; + } + 0x0444 => { + // Drop an outgoing primitive, without draw. + if self.cmd.operand[0] == 0 && !self.prim.integer_prim && !(self.prim.begin_open && self.stream.primitive == 9) { + self.gl().discard_primitive(); + self.prim.in_prim = false; + self.prim.begin_open = false; + self.prim.open_held = 0; + self.prim.restart_absorb = 0; + self.prim.restart_armed = false; + } else if self.cmd.operand[0] != 0 { + self.cmd_unimplemented(0x10444); + } + } + 0x0850 => { + let type_ = self.cmd.operand[0] & 0xf; + + if type_ == 0xf { + if !self.prim.integer_prim && !(self.prim.begin_open && self.stream.primitive == 9) { + self.gl().discard_primitive(); + self.prim.in_prim = false; + self.prim.begin_open = false; + self.prim.open_held = 0; + self.prim.restart_absorb = 0; + self.prim.restart_armed = false; + } + } else if type_ <= 8 { + self.stream.primitive = type_; + self.prim.restart_type = type_; + self.prim.restart_absorb = (self.cmd.operand[0] >> 19) & 3; + self.prim.restart_total = self.prim.restart_absorb; + self.prim.restart_armed = true; + self.prim.restart_select = 0; + self.prim.begin_open = true; + self.prim.integer_prim = false; + self.prim.open_r = 0; + self.prim.open_c = 0; + self.prim.open_m3 = 0; + self.prim.open_m4 = 0; + self.prim.open_held = 0; + } else { + self.cmd_unimplemented(0x10850); + } + } + 0x0740 => { + self.prim.restart_select = self.cmd.operand[0]; + // The kernel selects 0x1ed as well as 0x1e0. What 0x1ed selects is + // UNKNOWN: it is accepted, and 0x0741 is still reported. + if self.prim.restart_select != 0x1e0 && self.prim.restart_select != 0x1ed { + self.cmd_unimplemented(0x740); + } + } + 0x0741 => { + if self.prim.restart_select == 0x1e0 && self.prim.restart_armed { + let word = self.cmd.operand[0]; + + self.prim.restart_c = (word >> 21) & 7; + self.prim.restart_m3 = (word >> 12) & 3; + self.prim.restart_m4 = (word >> 14) & 3; + self.restart_finish(); + } else { + self.cmd_unimplemented(0x741); + } + } + cmd::IMAGE_HEADER => { + // The transfer header opens a pixel group: its count (bits 23:20, 15 + // standing for 16, 0 meaning a full burst) is how many byte-format + // pixels or SHORT-format words the group's first burst carries; any + // more words in that burst are padding, and the bursts after it are + // full, running on to the box edge. Reset the per-group count and + // any partial pixel. + // + // That the count covers only the first burst is INFERRED from the + // X server's emitter: a colour-index image 56 pixels wide is sent + // as a header with count 0 and three 16-pixel bursts, then for each + // later group a header with count 8 and bit 16 set, an 8-pixel + // burst finishing the previous row, and three 16-pixel bursts. A + // count that bounded the whole group drew 8 pixels of every 56 and + // left the root window's background in grey bands. + let glen = (self.cmd.operand[0] >> image::HEADER_GROUP_SHIFT) & image::HEADER_GROUP_MASK; + + self.image.group_len = if glen == image::HEADER_GROUP_MASK { + image::HEADER_GROUP_FULL + } else { + glen + }; + self.image.group_painted = 0; + self.image.code = self.cmd.operand[0] & image::CODE_MASK; + self.image.write_code = self.cmd.operand[0] & write::CODE_MASK; + self.image.nbytes = 0; + self.image.bytes = 0; + } + reg::PATTERN_SHAPE => { + self.stream.pattern_shape = self.cmd.operand[0]; + self.gl_apply_polygon_stipple(); + } + reg::PATTERN_MODE => { + self.stream.pattern_mode = self.cmd.operand[0]; + self.gl_apply_mode(); + } + reg::WINDOW_ORIGIN => { + self.win.origin[0] = sextract32(self.cmd.operand[0], 0, ODSY_ORIGIN_BITS); + self.win.origin[1] = sextract32(self.cmd.operand[1], 0, ODSY_ORIGIN_BITS); + if self.cmd.from_kernel { + self.win.prior_origin = self.win.committed_origin; + self.win.committed_origin = self.win.origin; + self.win.origin_pending = true; + } + } + reg::ID_COMPARE => { + self.win.id_compare = self.cmd.operand[1]; + if self.cmd.from_kernel && !after_tail { + self.win.committed_id_compare = self.win.id_compare; + } + // A non-zero pair still ends a pending query (which clears the + // operands), as when the word was undecoded: whether the board does + // is unknown, and no traced query has one inside it. + if self.cmd.operand[0] != 0 || self.cmd.operand[1] != 0 { + self.query_cancel(); + } + } + reg::WINDOW_CLIP => { + let n = self.win.clip_words.len(); + self.win.clip_words.copy_from_slice(&self.cmd.operand[..n]); + if self.cmd.from_kernel + && self.win.origin_pending + && self.win.origin[0] == 0 + && self.win.origin[1] == 0 + && self.clip_is_offscreen() + { + // The off-screen tail: keep the window pushed before it. + self.win.committed_origin = self.win.prior_origin; + self.win.after_tail = true; + } else if self.cmd.from_kernel { + self.win.committed_clip = self.win.clip_words; + } + if self.cmd.from_kernel { + self.win.origin_pending = false; + } + self.clip_update(); + } + cmd::WINDOW_RESTORE => { + // The committed window back (cmd::WINDOW_RESTORE; INFERRED). + self.win.origin = self.win.committed_origin; + self.win.clip_words = self.win.committed_clip; + self.win.id_compare = self.win.committed_id_compare; + self.clip_update(); + } + reg::LINE_STIPPLE => { + let n = self.stream.line_stipple.len(); + self.stream.line_stipple.copy_from_slice(&self.cmd.operand[..n]); + self.gl_apply_line_stipple(); + if self.stream.line_stipple[line::STIPPLE_CONTROL] != line::STIPPLE_CONTROL_32 + && (self.stream.line_stipple[line::STIPPLE_CONTROL] & !0xffu32) != 0 + && !self.diag.line_stipple_reported + { + self.diag.line_stipple_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: line stipple control {:#x} has unclassified high bits", + self.stream.line_stipple[line::STIPPLE_CONTROL] + ); + } + } + reg::QUICK_FLUSH_ARG + | reg::STROBE + | reg::FULL_FLUSH_EXTRA + | reg::PIXEL_MODE_EXT + | reg::BLIT_PREFIX => {} + swap::SYNC_A => { + // The bound drawing and reading drawable words, kept raw and + // independently (reg::DRAW_MASK): neither changes where the + // model draws. The swap completes on the retrace hook + // (retrace()). + self.draw_word = self.cmd.operand[0]; + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: buffer word: reg {:#x} word {:#x}", tag, self.draw_word); + } + swap::SYNC_B => { + self.read_word = self.cmd.operand[0]; + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: buffer word: reg {:#x} word {:#x}", tag, self.read_word); + } + reg::READ_ORIGIN => { + self.win.read_origin[0] = sextract32(self.cmd.operand[0], 0, 16); + self.win.read_origin[1] = sextract32(self.cmd.operand[0], 16, 16); + } + 0x0000060a | 0x0000060c..=reg::DRAWABLE_LAST => { + if !self.diag.drawable_word_reported { + self.diag.drawable_word_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: drawable word {:#x} with operand {:#x} is consumed but not modeled", + tag, + self.cmd.operand[0] + ); + } + } + reg::DRAW_MASK => { + // The drawing mask: which colour buffers the next writes reach. + self.draw_mask = self.cmd.operand[0]; + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: draw mask: mask {:#x}", self.draw_mask); + if (self.draw_mask & !ODSY_DRAW_MASK_BITS) != 0 && !self.diag.draw_mask_high_reported { + self.diag.draw_mask_high_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: drawing mask {:#x} bits above 3 are not modeled; ignored", + self.draw_mask + ); + } + if (self.draw_mask & ODSY_DRAW_MASK_RIGHT) != 0 && !self.diag.draw_mask_stereo_reported { + self.diag.draw_mask_stereo_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: stereo is not modeled; drawing mask {:#x} draws its right-eye buffers into the left-eye ones", + self.draw_mask + ); + } + } + reg::HOST_WRITE => { + let op0 = self.cmd.operand[0]; + let op1 = self.cmd.operand[1]; + // The host-write command's word and the halfword it writes are + // ESTABLISHED as the kernel's accesses; that the board treats it + // as its DMA completion signal is INFERRED. The model raises the same fence the DMA engine's + // completion does, through the one fence implementation: op0's slot + // and interrupt bit and op1's token become the completion cookie, + // raised by Board::fence(). op0's interrupt + // bit and op1's token already sit at the fence word's own positions + // (dma::FENCE_INTERRUPT is the fence word's bit 15). + let word: u32 = ((op1 & 0xffff) << dma::FENCE_TOKEN_SHIFT) + | (op0 & (dma::FENCE_INTERRUPT | dma::FENCE_SLOT_MASK)); + + self.fence(ODSY_FENCE_VALID | word as u64); + } + dma::DESC_UPLOAD => { + self.dma_descriptor(false); + } + dma::DESC_READBACK => { + self.dma_descriptor(true); + } + _ => { + // A recognized register-select word (valid shape, field-derived + // arity) whose side effect this model does not track -- a mode, mask + // or state register. Consumed like the flush and pixel-mode registers + // above rather than treated as impossible. + } + } + } + + pub fn cmd_start(&mut self, tag: u32, operands: u32) { + self.cmd.tag = tag; + self.cmd.operand_count = operands; + self.cmd.operand_index = 0; + if operands == 0 { + self.query_update(tag); + if tag != 0x2f00c02e && tag != 0x3000c02e { + self.cmd_dispatch(tag); + } + } else { + self.cmd.state = ODSY_CMD_OPERANDS; + } + } + + // A register-select word (0x0001rrrr): the most common command word, since + // every flush and state write is one. No other word classify matches has + // this shape, so it is tested first rather than after every by-value match. + pub fn cmd_classify_register(&mut self, word: u32) { + let reg = word & !cmd::REGISTER_SHAPE; + let operands = cmd_arity(reg); + + // A pixel burst while an image is open: its data words paint into the + // box rather than streaming through the generic operand path. Outside + // an image the same register-select is an ordinary state word. + if let Some(fmt) = image_burst_fmt(reg).filter(|_| self.image.active && operands > 0) { + self.image.fmt = fmt; + if !self.image_burst_supported(fmt) { + self.cmd_unimplemented(word); + } + self.cmd.state = ODSY_CMD_IMAGE_BURST; + self.cmd.operand_count = operands; + self.cmd.operand_index = 0; + return; + } + + // The 32x32-bit pattern loads: a 16-word burst each, more than + // cmd_operand[] holds, so they stream straight into the pattern buffer + // (0x3c4e -> words 0-15, 0x3c4f -> words 16-31) rather than through the + // generic operand path. + if reg == reg::PATTERN_LOW || reg == reg::PATTERN_HIGH { + self.cmd.state = ODSY_CMD_PATTERN_LOAD; + self.cmd.operand_count = operands; + self.cmd.operand_index = 0; + self.stream.pattern_load_offset = if reg == reg::PATTERN_HIGH { pattern::WORDS } else { 0 }; + return; + } + self.cmd_start(reg, operands); + } + + /// Classify a word arriving while the parser is idle: a new command. + pub fn cmd_classify(&mut self, word: u32) { + if (word & cmd::REGISTER_SHAPE) == cmd::REGISTER_TAG { + self.cmd_classify_register(word); + return; + } + if word == 0x4c0 || word == 0x4c0 + query::KIND_RASTER { + self.query_begin(); + self.query.kind = word - 0x4c0; + return; + } + if word == ODSY_RASTER_POS_LATCH_WORD { + self.query_raster_latch(); + } + if word == 0x2f00c02e || word == 0x3000c02e { + self.cmd_start(word, 0); + return; + } + if word == 0x740 || word == 0x741 { + self.cmd_start(word, 1); + return; + } + if (reg::FIFO_SETUP_FIRST..=reg::FIFO_SETUP_LAST).contains(&word) + || word == reg::FIFO_SETUP_7B5 + || word == reg::SETUP_7F0 + { + self.cmd_start(word, 1); + return; + } + + if word == cmd::COPY_RECT_TRIGGER { + self.cmd.state = ODSY_CMD_COPY_RECT; + self.cmd.operand_index = 0; + return; + } + // A page-table DMA descriptor: the opcode word, then three more words the + // generic operand path gathers before Board::cmd_dispatch() runs the + // transfer (the two-doubleword descriptor reaches here as four words). + if word == dma::DESC_UPLOAD || word == dma::DESC_READBACK { + self.cmd_start(word, dma::DESC_OPERANDS); + return; + } + // The two buffer-select doublewords: each opcode word takes one operand + // word. Consumed cleanly so the parser stays word-aligned; the swap + // completion is delivered from the retrace hook, not here (see the + // swap::SYNC_* constants and retrace()). + if word == swap::SYNC_A || word == swap::SYNC_B { + self.cmd_start(word, swap::SYNC_OPERANDS); + return; + } + if word == reg::READ_ORIGIN { + self.cmd_start(word, 1); + return; + } + if (reg::DRAWABLE_FIRST..=reg::DRAWABLE_LAST).contains(&word) { + // 0x60a, 0x60c to 0x60f; the three known ones are matched above. + self.cmd_start(word, 1); + return; + } + if word == gl_cmd::COLOR_3UB { + self.cmd_start(word, 3); + return; + } + if word == gl_cmd::VERTEX_2I { + self.cmd_start(word, 2); + return; + } + if word == gl_cmd::TEXCOORD_1F + || word == gl_cmd::TEXCOORD_3F + || word == gl_cmd::TEXCOORD_4F + || word == gl_cmd::TEXCOORD_1I + || word == gl_cmd::TEXCOORD_3I + || word == gl_cmd::TEXCOORD_4I + || (word & gl_cmd::TEXCOORD_SHAPE) == gl_cmd::TEXCOORD_TAG + || word == gl_cmd::TEX_RECT + { + self.cmd_start(word, cmd_arity(word)); + return; + } + if word == cmd::WINDOW_RESTORE { + self.cmd_start(word, 0); + return; + } + if word == gl_cmd::COLOR_INDEX + || word == gl_cmd::REPLAY_INDEX + || word == gl_cmd::COLOR_INDEX_F + || word == gl_cmd::REPLAY_INDEX_F + || word == gl_cmd::INDEX_MODE_COLOR_3I + || word == gl_cmd::INDEX_MODE_COLOR_3F + || word == gl_cmd::INDEX_MODE_COLOR_4F + || word == gl_cmd::COLOR_3I + || word == gl_cmd::COLOR_4F + || is_integer_colour(word) + || word == gl_cmd::NORMAL_3B + || word == gl_cmd::NORMAL_3S + || word == gl_cmd::NORMAL_3I + { + self.cmd_start(word, cmd_arity(word)); + return; + } + // The true-colour GL words, matched by value: the float colour and vertex + // siblings, the three matrix target-selects (0 operands each) and the + // matrix load (16 floats, more than cmd_operand[] holds, so it streams into + // gl_matrix[] through its own state). + if word == gl_cmd::COLOR_3F { + self.cmd_start(word, 3); + return; + } + if word == gl_cmd::VERTEX_2F { + self.cmd_start(word, 2); + return; + } + if word == gl_cmd::VERTEX_3F || word == gl_cmd::VERTEX_3I { + self.cmd_start(word, 3); + return; + } + if word == gl_cmd::VERTEX_4F { + self.cmd_start(word, 4); + return; + } + if word == gl_cmd::LOAD_4F { + self.cmd_start(word, gl_cmd::BLOCK16_FLOATS); + return; + } + // The lighting words, matched by value; each carries its operand count + // in bits 13:10 like every other word. The program words carry none and + // are matched by class ahead of the image path's 0x29 state word, which + // is the same class (is_program_word()). + if GL_LIGHT_WORDS.contains(&word) { + self.cmd_start(word, cmd_arity(word)); + return; + } + if is_program_word(word) { + self.cmd_start(word, 0); + return; + } + if (word & gl_cmd::MATRIX_SELECT_SHAPE) == gl_cmd::MATRIX_SELECT_TAG { + self.cmd_start(word, 0); + return; + } + if word == gl_cmd::MATRIX_LOAD { + // 16 floats, more than cmd_operand[] holds, so gathered in gl_matrix[]. + self.cmd.state = ODSY_CMD_MATRIX_LOAD; + self.cmd.operand_count = gl_cmd::MATRIX_FLOATS; + self.cmd.operand_index = 0; + return; + } + if word == gl_cmd::RECT || word == gl_cmd::CLEAR_BOX { + // 10 floats, more than cmd_operand[] holds: gathered in gl_rect[]. + self.cmd.state = ODSY_CMD_GL_RECT; + self.cmd.operand_count = gl_cmd::RECT_FLOATS; + self.cmd.operand_index = 0; + return; + } + if word == mode::INDEX_ZEROS { + self.cmd_start(word, 3); + return; + } + // The BEGIN-0xa image raster path's structural words, matched by value + // rather than as register-selects. The raster-box floats need indices 4, + // 5, 7 and 8 -- past what cmd_operand[] holds -- so they stream through a + // dedicated state; the state word 0x29008000 | v and its teardown twin (0 + // operands), the transfer header (1) and its tail (0) run the generic + // path, the header setting the group length as it dispatches. + if word == cmd::IMAGE_BOX { + self.cmd.state = ODSY_CMD_IMAGE_BOX_LOAD; + self.cmd.operand_count = cmd_arity(word); + self.cmd.operand_index = 0; + return; + } + if (word & cmd::STATE_WORD_MASK) == cmd::STATE_WORD + || word == cmd::IMAGE_HEADER + || word == cmd::IMAGE_HEADER_TAIL + { + self.cmd_start(word, cmd_arity(word)); + return; + } + // A word this model does not know still carries its operand count in + // bits 13:10 (cmd_arity()), so pass over that many words and + // keep the parser on the stream's command positions. The kernel's + // context restore replays words of every class this way: 0x3cf8 to + // 0x3cfb and 0x3f41 with 16 zero operands each, 0x0f41 with 3, 0x04b0 + // and 0xc480c426 with one, 0x2400800a with none. Taking their operands + // as commands instead read zeros and floats as command words. + // + // A word of the GL classes (a non-zero top byte) with bit 23 clear takes + // none, whatever its count field says: the restore's 0x9000bda2, whose + // field reads 16, is followed at once by a flush, the window origin + // (0x0841) and the clip list, which it would otherwise swallow, leaving + // the next client drawing at the previous one's origin. Every bit-23- + // clear GL word the model knows has a zero field and no operands, and + // every one with operands has the bit set (0x9080bda2, 0x90808800). + // INFERRED from that pattern; the bit's meaning on the board is UNKNOWN. + if cmd_arity(word) != 0 && ((word >> 24) == 0 || (word & cmd::GL_OPERANDS) != 0) { + let note = format!("; skipping {} operands", cmd_arity(word)); + self.cmd_report(word, ¬e); + self.cmd.state = ODSY_CMD_SKIP; + self.cmd.operand_count = cmd_arity(word); + self.cmd.operand_index = 0; + } else if cmd_arity(word) != 0 { + self.cmd_report(word, "; bit 23 clear, no operands"); + } else { + self.cmd_report(word, "; no operands"); + } + } + + /// One command-FIFO word, wherever the parser currently is. + pub fn cmd_word(&mut self, word: u32) { + match self.query.stage { + 1 => { + self.query.count = word; + self.query.stage = 2; + return; + } + 2 => { + if word == 0x10a5a { + self.query.stage = 3; + return; + } + self.query.stage = 0; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: unpaired current-query sequence"); + self.cmd_classify(word); + return; + } + 3 => { + self.query.sync_index = word; + self.query.stage = 4; + return; + } + 4 => { + self.query_finish(word); + return; + } + _ => {} + } + match self.cmd.state { + ODSY_CMD_SKIP => { + self.cmd.operand_index += 1; + if self.cmd.operand_index == self.cmd.operand_count { + self.cmd.state = ODSY_CMD_IDLE; + } + } + ODSY_CMD_OPERANDS => { + // cmd_operand[] holds a whole burst; the bound is belt and braces. + if (self.cmd.operand_index as usize) < self.cmd.operand.len() { + self.cmd.operand[self.cmd.operand_index as usize] = word; + } + self.cmd.operand_index += 1; + if self.cmd.operand_index == self.cmd.operand_count { + let tag = self.cmd.tag; + self.query_update(tag); + self.cmd_dispatch(tag); + self.cmd.state = ODSY_CMD_IDLE; + } + } + ODSY_CMD_COPY_RECT => { + self.cmd.operand[self.cmd.operand_index as usize] = word; + match self.cmd.operand_index { + cmd::COPY_RECT_SRC_INDEX => self.copy_src = word, + cmd::COPY_RECT_SIZE_INDEX => self.copy_size = word, + cmd::COPY_RECT_DST_INDEX => self.copy_dst = word, + _ => {} + } + self.cmd.operand_index += 1; + if self.cmd.operand_index == cmd::COPY_RECT_WORDS { + self.copy_rect(); + self.cmd.state = ODSY_CMD_IDLE; + } + } + ODSY_CMD_PATTERN_LOAD => { + // A 16-word burst into the pattern buffer at the load's own offset. + if self.cmd.operand_index < pattern::WORDS { + self.stream.pattern_buffer[(self.stream.pattern_load_offset + self.cmd.operand_index) as usize] = word; + } + self.cmd.operand_index += 1; + if self.cmd.operand_index == self.cmd.operand_count { + self.gl_apply_polygon_stipple(); + self.cmd.state = ODSY_CMD_IDLE; + } + } + ODSY_CMD_IMAGE_BOX_LOAD => { + // The ten raster-box floats. Only 4, 5, 7 and 8 carry the destination + // corners; the rest are the zero the DDX's private was allocated with. + // Set the cursor to the top-left corner once all ten have arrived. + match self.cmd.operand_index { + image::BOX_X1_INDEX => self.image.x1 = float_to_u32(word), + image::BOX_Y1_INDEX => self.image.y1 = float_to_u32(word), + image::BOX_X2_INDEX => self.image.x2 = float_to_u32(word), + image::BOX_Y2_INDEX => self.image.y2 = float_to_u32(word), + _ => {} + } + self.cmd.operand_index += 1; + if self.cmd.operand_index == self.cmd.operand_count { + self.image.cur_x = if self.image.x2 < self.image.x1 { self.image.x1.wrapping_sub(1) } else { self.image.x1 }; + self.image.cur_y = if self.image.y2 < self.image.y1 { self.image.y1.wrapping_sub(1) } else { self.image.y1 }; + self.cmd.state = ODSY_CMD_IDLE; + } + } + ODSY_CMD_IMAGE_BURST => { + // Each data word paints into the open image at the raster cursor. + self.image_burst_word(word); + self.cmd.operand_index += 1; + if self.cmd.operand_index == self.cmd.operand_count { + // The header's count bounds only the group's first burst; the + // bursts after it are full (cmd::IMAGE_HEADER). + self.image.group_len = 0; + self.cmd.state = ODSY_CMD_IDLE; + } + } + ODSY_CMD_MATRIX_LOAD => { + // A board-side matrix: 16 floats, loaded once all have arrived. + if self.cmd.operand_index < gl_cmd::MATRIX_FLOATS { + self.gl_matrix[self.cmd.operand_index as usize] = word; + } + self.cmd.operand_index += 1; + if self.cmd.operand_index == self.cmd.operand_count { + self.gl_load_matrix(); + self.cmd.state = ODSY_CMD_IDLE; + } + } + ODSY_CMD_GL_RECT => { + // The rectangle's colour and corners, painted once all have arrived. + self.gl_rect[self.cmd.operand_index as usize] = word; + self.cmd.operand_index += 1; + if self.cmd.operand_index == self.cmd.operand_count { + self.gl_rect(); + self.cmd.state = ODSY_CMD_IDLE; + } + } + _ => { + self.cmd_classify(word); + } + } + } + + /// The command FIFO accepts both 32-bit and 64-bit stores. An 8-byte store + /// carries two sequential 32-bit command words; which half is logically + /// first is not independently established by any allowed source, so it is + /// modeled by analogy to the backend-selector port's own + /// (selector << 32) | data convention and this device's big-endian byte + /// order, upper half first. Other widths are not established for this + /// port, so they are reported unimplemented rather than fed into the + /// command stream and risking desync. + pub fn cmd_fifo_write(&mut self, value: u64, size: u32) { + match size { + 4 => self.cmd_word(value as u32), + 8 => { + self.cmd_word((value >> 32) as u32); + self.cmd_word(value as u32); + } + _ => self.unimplemented(cmd::FIFO, size, true), + } + } +} + +/// One step of the command-FIFO self-test preamble. +pub(super) struct CfifoPreamble { + pub port: u64, + pub size: u32, + pub value: u64, +} + +/// The kernel driver's command-FIFO initialisation self-test preamble, in +/// order, all through the command FIFO: three 4-byte command words, five +/// 8-byte pushes (a 32-bit selector in the upper half, data in the lower), +/// and one closing 4-byte word. Seeing the whole sequence makes +/// cfifo::SELFTEST answer OK. The values are ESTABLISHED as the +/// kernel's first stores; the role of the three command words (0x00010854, +/// 0 and 0x0fffffff) is UNKNOWN, and nothing found names them a +/// memory-protect command. The tracker only matches the sequence. (The real +/// driver sends the five 8-byte +/// words to 0x110000, not to the backend-select port, confirmed by a boot +/// trace.) +pub(super) static CFIFO_PREAMBLE: [CfifoPreamble; 9] = [ + CfifoPreamble { port: cmd::FIFO, size: 4, value: 0x00010854 }, + CfifoPreamble { port: cmd::FIFO, size: 4, value: 0x00000000 }, + CfifoPreamble { port: cmd::FIFO, size: 4, value: 0x0fffffff }, + CfifoPreamble { port: cmd::FIFO, size: 8, value: 0x000006b000000000 }, + CfifoPreamble { port: cmd::FIFO, size: 8, value: 0x000006b100000100 }, + CfifoPreamble { port: cmd::FIFO, size: 8, value: 0x000006b300005e50 }, + CfifoPreamble { port: cmd::FIFO, size: 8, value: 0x000006b200007dc0 }, + CfifoPreamble { port: cmd::FIFO, size: 8, value: 0x000007b500000001 }, + CfifoPreamble { port: cmd::FIFO, size: 4, value: 0x00010046 }, +]; + +impl Board { + pub fn selftest_track(&mut self, port: u64, size: u32, value: u64) { + let next = self.widget.selftest_progress as usize; + + if next < CFIFO_PREAMBLE.len() + && CFIFO_PREAMBLE[next].port == port + && CFIFO_PREAMBLE[next].size == size + && CFIFO_PREAMBLE[next].value == value + { + self.widget.selftest_progress = next as u32 + 1; + if self.widget.selftest_progress as usize == CFIFO_PREAMBLE.len() { + self.widget.cfifo_selftest_ready = true; + self.widget.selftest_progress = 0; + } + return; + } + // A break in the sequence restarts it, counting a fresh first word. + self.widget.selftest_progress = if CFIFO_PREAMBLE[0].port == port + && CFIFO_PREAMBLE[0].size == size + && CFIFO_PREAMBLE[0].value == value + { + 1 + } else { + 0 + }; + } +} diff --git a/src/dev/odyssey/consts.rs b/src/dev/odyssey/consts.rs new file mode 100644 index 000000000..23d143938 --- /dev/null +++ b/src/dev/odyssey/consts.rs @@ -0,0 +1,1209 @@ +//! Register offsets, command words, enums and the command arity helper. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + +use super::board::{ODSY_GL_WINDOW_ENTRIES, ODSY_TEX_DETAIL_FUNCTION_ENTRIES}; +// The whole aperture a driver maps. +pub(super) const ODSY_APERTURE: u64 = 0x410000; +pub(super) const ODSY_PART: u32 = 0xc013; +pub(super) const ODSY_MANUFACTURER: u32 = 0x023; + +// The one modeled scanout mode's retrace period, from the compiled 60 Hz 1280x1024 mode-description file +pub(super) const ODSY_FRAME_CLOCKS: u32 = 1688 * 1066; +pub(super) const ODSY_PIXEL_CLOCK_HZ: u32 = 107964476; + +// The display channel's active size, backend entries 0x2802 (pixels) and 0x2803 (lines). +pub(super) const ODSY_CHANNEL_WIDTH: u32 = 0x2802; +pub(super) const ODSY_CHANNEL_HEIGHT: u32 = 0x2803; + +// The crosstalk configuration block +pub(super) const ODSY_BLOCK_END: u64 = 0x60; +pub(super) const ODSY_IDENTITY: u64 = 0x00; +pub(super) const ODSY_STATUS: u64 = 0x08; +pub(super) const ODSY_ERROR_ADDR_UPPER: u64 = 0x10; +pub(super) const ODSY_ERROR_ADDR_LOWER: u64 = 0x18; +pub(super) const ODSY_CONTROL: u64 = 0x20; +pub(super) const ODSY_XT_REQ_TIMEOUT: u64 = 0x28; +pub(super) const ODSY_INT_DEST_UPPER: u64 = 0x30; +pub(super) const ODSY_INT_DEST_LOWER: u64 = 0x38; +pub(super) const ODSY_ERROR_COMMAND: u64 = 0x40; + +// int_dest_upper/int_dest_lower +pub(super) const ODSY_INT_DEST_WIDGET_BITS: u32 = 4; + +pub(super) mod mem { + // +0x1004, the memory controller configuration. + pub const CONTROL: u64 = 0x1004; + pub const CONTROL_LATENCY_2: u32 = 1 << 1; + pub const CONTROL_FOUR_BANKS: u32 = 1 << 2; + pub const CONTROL_SIZE_SHIFT: u32 = 3; + pub const CONTROL_SIZE_MASK: u32 = 0x3; + pub const CONFIG: u64 = 0x106c; +} + +// +0x100c, the low-level reset +pub(super) const ODSY_RESET_CONTROL: u64 = 0x100c; + +pub(super) mod intr { + // The flag interrupt's registers, as the shipped guest OS kernel uses them + pub const FLAGS: u64 = 0x1014; + pub const CLEAR: u64 = 0x101c; + pub const ENABLE_SET: u64 = 0x1034; + pub const ENABLE_CLEAR: u64 = 0x103c; + + // FLAGS' low-water bit, the command-FIFO low-water interrupt (the kernel's flags decode names it as the low-water flag bit). + pub const FIFO_LOW_WATER: u32 = 0x00000200; + + // Flags bit 25, raised as the level rises to the threshold the high mark gives. + // MODEL CHOICE: the flag's meaning rests on its name alone, and its link to + // 0x6b2 and its threshold and edge are UNKNOWN; no driver enables it, and the kernel's fatal-flag mask + // leaves it out. + pub const FIFO_HIGH_WATER: u32 = 0x02000000; +} + +pub(super) mod route { + // The routed-interrupt destination and per-source vector table + pub const DEST_UPPER: u64 = 0x1044; + pub const DEST_LOWER: u64 = 0x104c; + pub const VECTORS: u64 = 0x1054; + + // bits 28:25 of ROUTE_DEST_UPPER, and two of the four source bytes within ROUTE_VECTORS. + pub const DEST_WIDGET_SHIFT: u32 = 25; + pub const VECTOR_SOFTSYNC_SHIFT: u32 = 24; + pub const VECTOR_RETRACE_SHIFT: u32 = 16; + pub const VECTOR_DMA_SHIFT: u32 = 8; + pub const VECTOR_FLAG_SHIFT: u32 = 0; + + // Bits 18:16 of ROUTE_DEST_UPPER enable the three non-flag sources. The + // three bits are inferred to be the enables, but not which bit belongs to + // which source; this assignment is a model choice (an approximation): + // ascending with the vector bytes. + pub const ENABLE_DMA: u32 = 1 << 16; + pub const ENABLE_RETRACE: u32 = 1 << 17; + pub const ENABLE_SOFTSYNC: u32 = 1 << 18; +} +pub(super) const ODSY_STATUS0: u64 = 0x1064; +pub(super) mod flctl { + pub const CONFIG: u64 = 0x1070; + pub const TARGET: u64 = 0x1074; + pub const CONFIG_RESET: u32 = 0x0001c000; + + // 0x1074, flow-control attach. + pub const TARGET_SHIFT: u32 = 12; + pub const TARGET_MASK: u32 = 0x1f; + pub const INTR_TARGET_SHIFT: u32 = 17; +} +pub(super) const ODSY_SW_TOKEN: u64 = 0x107c; +pub(super) mod nic { + pub const PROM: u64 = 0x10d8; + pub const KERNEL: u64 = 0x10dc; + + // SGI's MicroLAN control register, the layout the Bridge, the XBridge and the Hub all carry + pub const LEVEL: u32 = 1 << 0; + pub const DONE: u32 = 1 << 1; + pub const SAMPLE_SHIFT: u32 = 2; + pub const SAMPLE_MASK: u32 = 0xff << SAMPLE_SHIFT; + pub const PULSE_SHIFT: u32 = 10; + pub const PULSE_MASK: u32 = 0x3ff << PULSE_SHIFT; + + // Board-identity part family 0x0b, one 32-byte page, 48-bit serial. + pub const FAMILY: u32 = 0x0b; + pub const PAGE: u32 = 32; + pub const ROM_SIZE: usize = 160; + pub const SERIAL_MASK: u64 = (1u64 << 48) - 1; + pub const RECORD_DEFAULT: &str = "Name:ODY128VERSIONB"; +} + +pub(super) mod host { + // The DMA-sync command's (super::reg::HOST_WRITE, below) host-write base + pub const WRITE_ADDR_UPPER: u64 = 0x4034; + pub const WRITE_ADDR_LOWER: u64 = 0x403c; +} + +pub(super) mod stats { + // The statistics region. + pub const ADDR_UPPER: u64 = 0x401c; + pub const ADDR_LOWER: u64 = 0x4024; + pub const CONTROL: u64 = 0x402c; +} + +pub(super) mod ctx { + // The context-shadow address + pub const SHADOW_ADDR_UPPER: u64 = 0x4004; + pub const SHADOW_ADDR_LOWER: u64 = 0x400c; + + // The per-context command port + pub const SWITCH_PORT: u64 = 0x118000; + pub const SWITCH_OPCODE_SHIFT: u32 = 40; + pub const SWITCH_OPCODE: u32 = 0x7; + pub const SWITCH_VALID: u64 = 1 << 0; + pub const SWITCH_DONE_OFFSET: u64 = 0x8fc; +} + +// The "Buzz registers" the stock IP35 BaseIO PROM's graphics diagnostic tests, adjoining but distinct from the kernel driver's own register scan. +pub(super) const ODSY_BUZZ_1: u64 = 0x1078; +pub(super) const ODSY_BUZZ_2: u64 = 0x1088; +pub(super) const ODSY_BUZZ_3: u64 = 0x1098; +pub(super) const ODSY_BUZZ_4: u64 = 0x10a8; +pub(super) const ODSY_BUZZ_5: u64 = 0x10b8; +pub(super) const ODSY_BUZZ_6: u64 = 0x10c0; + +pub(super) mod swap { + // The swap-control register bank, as the guest driver's buffer swap and vertical-retrace handler use it, interleaved with the Buzz registers above but ... + pub const ARM: u64 = 0x10b4; + pub const PENDING: u64 = 0x10bc; + pub const CONTROL: u64 = 0x10c4; + pub const CONTROL_COMPANION: u64 = 0x10cc; + + // The two buffer-select doublewords the kernel driver's buffer-select push sends ahead of a swap, 0x608 and 0x609 each with one operand word taken from ... + pub const SYNC_A: u32 = 0x00000608; + pub const SYNC_B: u32 = 0x00000609; + pub const SYNC_OPERANDS: u32 = 1; +} +pub(super) const ODSY_DISPLAYED_BUFFER: u64 = 0x10d4; + +pub(super) mod reg { + // Front and back storage. + pub const DRAW_MASK: u32 = 0x0424; + + // FIFO set-up registers. The IRIX 6.5.30 kernels' command-FIFO set-up stores + // five doublewords, read as a register number in the high word (weakly + // INFERRED) and a value in the low word: 0x6b0 = 0, 0x6b1 = 256, + // 0x6b3 = 24144, 0x6b2 = 32192 and 0x7b5 = 1, written in the order 0x6b0, + // 0x6b1, 0x6b3, 0x6b2, 0x7b5. The values and the order are ESTABLISHED; the + // roles and units of the four 0x6bX words are UNKNOWN. The model stores all five + // and gives no meaning to 0x6b0, 0x6b1, 0x6b3 or 0x7b5. + // + // MODEL CHOICE, not a hardware fact: 0x6b2 is applied as the threshold for + // status bit 15 and flag bit 25 -- the level reaching a nonzero 0x6b2 sets + // the bit and raises the flag, and zero, as out of reset, is no threshold. + // Neither the roles nor the link between bit 15, flag 25 and any 0x6bX value + // is established. The level's unit (32-bit words, a doubleword store counting + // two) is a model choice, as is the FIFO's size (super::cfifo::WORDS), + // independent of the stored values, and so is the low-water threshold (super::cfifo::LOW_WATER). The model contract is pinned by + // the_level_counts_words_against_the_model_fifo_and_the_6b2_threshold and + // the_threshold_follows_6b2. + pub const FIFO_SETUP_FIRST: u32 = 0x6b0; + pub const FIFO_SETUP_LAST: u32 = 0x6b3; + + // The model's own name for the 0x6b2 slot, see above. + pub const FIFO_SETUP_6B2: u32 = 0x6b2; + pub const FIFO_SETUP_7B5: u32 = 0x7b5; + + // 0x7f0. The kernels' FIFO set-up writes the doubleword 0x7f0 = + // 1,000,000,000 once, after the program load (the doubleword and the single + // write are ESTABLISHED). That the register is 0x3f0 is INFERRED; its unit + // and meaning are UNKNOWN: a sync timeout in board clocks, in nanoseconds or + // a non-timeout limit all fit. The model accepts the write and records the value, runs no + // timer, and raises no flag because of it. + pub const SETUP_7F0: u32 = 0x7f0; + pub const QUICK_FLUSH_ARG: u32 = 0x0443; // 1 operand: 0xfa or 0x96 seen + pub const STROBE: u32 = 0x0046; // 0 operands, repeated in flush + pub const FULL_FLUSH_EXTRA: u32 = 0x0019; // 0 operands, full flush only + pub const GL_BEGIN: u32 = 0x4400; // 1 operand: the GL primitive + pub const GL_END: u32 = 0x4001; // 0 operands + + // 0x0405, glyph pixel setup, is also the texturing mode + pub const PIXEL_MODE_EXT: u32 = 0x1453; // 5 operands: glyph pixel setup + + // Block 0x258 with one operand, 0x00120000 + pub const BLIT_PREFIX: u32 = 0x0658; + + // The host-write/sync command the kernel driver's DMA completion wait uses + pub const HOST_WRITE: u32 = 0x0a5a; + + // The bound reading drawable's packed y:x origin, distinct from 0x841. + pub const READ_ORIGIN: u32 = 0x0000060b; + + // The rest of that block + pub const DRAWABLE_FIRST: u32 = 0x00000608; + pub const DRAWABLE_LAST: u32 = 0x0000060f; + pub const DEPTH_FUNC: u32 = 0x0420; // 1 operand + pub const WRITE_MASK: u32 = 0x0452; // 1 operand + + // Texture state (sections 1 and 2 + pub const TEX_MODE: u32 = 0x0405; // 1 operand + pub const TEX_SAMPLER: u32 = 0x1c16; // 7 operands + pub const TEX_ENV: u32 = 0x141b; // 5 operands + pub const TEX_LEVEL: u32 = 0x0e80; // 3 operands, 0x0e80 .. 0x0ebf + pub const TEX_LEVEL0: u32 = 0x0406; // 1 operand + pub const TEX_LOD: u32 = 0x1013; // 4 operands + pub const TEX_SCALE_BIAS: u32 = 0x203b; // 8 operands + + // The GL library's other two post-filter layouts + pub const POST_FILTER_DEFAULT: u32 = 0x083d; + pub const TEX_COLOUR_TABLE0: u32 = 0x3e00; + pub const TEX_COLOUR_TABLE1: u32 = 0x3e10; + pub const TEX_DETAIL_FUNCTION: u32 = 0x3e40; + + // The texture offset block, three words the GL library copies from the texture object at activation. + pub const TEX_OFFSET: u32 = 0x0c3e; // 3 operands + + // A fourth 256-entry table, sixteen 16-word blocks. + pub const TEX_TABLE3: u32 = 0x3e30; + + // Six retained words + pub const FOG: u32 = 0x181a; + + // The one-bit pattern-quad text path. + pub const PATTERN_LOW: u32 = 0x3c4e; // pattern buffer words 0-15 + pub const PATTERN_HIGH: u32 = 0x3c4f; // pattern buffer words 16-31 + pub const PATTERN_SHAPE: u32 = 0x043f; // 1 operand: 0x4/0x5/0x6, 0x0 = off + pub const PATTERN_MODE: u32 = 0x0404; // 1 operand: pattern, GL mode bits + + // The line stipple. + pub const LINE_STIPPLE: u32 = 0x0c48; // 3 operands + + // The drawable-id plane write. + pub const DEST_SELECT: u32 = 0x1017; // 4 operands: destination-buffer select + pub const PLANE_MASK: u32 = 0x1023; // 4 operands: plane write mask << 12 + pub const ALPHA_FUNC: u32 = 0x041e; // 1 operand + pub const BLEND: u32 = 0x0c21; // 3 operands + pub const LOGIC_OP: u32 = 0x0422; // 1 operand + pub const STENCIL_FUNC: u32 = 0x041f; // 1 operand + pub const POLYGON_OFFSET: u32 = 0x0458; // 1 operand + + // 0x0415 packs glPolygonMode's two faces in one word + pub const POLYGON_MODE: u32 = 0x0415; // 1 operand + + // 0x084a packs the line width and point size together + pub const LINE_POINT_SIZE: u32 = 0x084a; // 2 operands: line width, point size + + // The scissor and window clip box, 0x081d, two operands + pub const CLIP_BOX: u32 = 0x081d; // 2 operands + pub const WINDOW_ORIGIN: u32 = 0x0841; // 2 operands: x, y + pub const ID_COMPARE: u32 = 0x0840; // 2 operands: id-plane base, test + pub const WINDOW_CLIP: u32 = 0x204b; // 8 operands: four rectangles +} +pub(super) const ODSY_BUFFERS: u32 = 2; +pub(super) const ODSY_DRAW_MASK_BITS: u32 = 0xf; +pub(super) const ODSY_DRAW_MASK_BUFFER0: u32 = 0x3; // left and right eye of buffer 0 +pub(super) const ODSY_DRAW_MASK_BUFFER1: u32 = 0xc; // left and right eye of buffer 1 +pub(super) const ODSY_DRAW_MASK_RIGHT: u32 = 0xa; // the right-eye bits +pub(super) const ODSY_DRAW_MASK_RESET: u32 = 0x1; // buffer 0, left: an approximation + +// Reset values a real Tezro V12's firmware diagnostics printed for a healthy board +pub(super) const ODSY_XT_REQ_TIMEOUT_RESET: u32 = 0x000fffff; + +pub(super) mod status0 { + // Status register 0x1064 (odsy status0). + pub const FIFO_ENABLED: u32 = 0x10000000; + pub const FIFO_LOW_WATER: u32 = 0x00020000; + pub const FIFO_EMPTY: u32 = 0x00010000; + pub const FIFO_HIGH_WATER: u32 = 0x00008000; +} + +pub(super) mod cfifo { + // The command FIFO's low-water threshold, in 32-bit words: below it STATUS0 + // bit 17 reads set, and the level falling below it latches the low-water + // flag. That bit 17 and flag 9 report a low-water level is INFERRED, and + // their link to any 0x6bX value is UNKNOWN. + // MODEL CHOICE, not a hardware fact: the threshold is 16 words, independent + // of the 0x6bX values the kernel stores (24,144 in 0x6b3). The edge (bit 17 + // set while the level is below it, the flag latched on falling below it) is a + // model choice too. Tests, a model contract and no evidence for the hardware: + // the_status_shows_waiting_stores_until_they_have_run and + // low_water_is_raised_as_the_queue_falls_below_the_mark. + pub const LOW_WATER: u32 = 16; + + // The command-FIFO self-test read-back. + pub const SELFTEST: u64 = 0x201594; + pub const SELFTEST_OK: u32 = 0x003d0046; + + // The command FIFO's size in 32-bit words that the model clamps the level to. + // MODEL CHOICE: the + // FIFO's depth, in any unit, and the figure 32,768 are UNKNOWN (no object + // names a FIFO size). The model + // keeps this figure whatever the stored 0x6bX values say, so the level the + // guest reads never exceeds it, which is a limitation: a real FIFO may be + // deeper or shallower. Test, a model contract: + // the_fifo_size_ignores_the_set_up_words. + pub const WORDS: u32 = 32768; +} + +pub(super) const ODSY_FIFO_SETUP_7B5_INDEX: usize = 4; + +pub(super) mod backend { + // The display back end's indexed register file is a 32-bit word FIFO, not a selector-plus-data register. + pub const SELECT: u64 = 0x400000; + pub const READ_BASE: u64 = 0x220000; + pub const SELECTOR_INDEX_SHIFT: u32 = 14; + + // The header's index (bits 27:14) and count (bits 13:0) fields. + pub const SELECTOR_INDEX_MASK: u32 = 0x3fff; + pub const COUNT_MASK: u32 = 0x3fff; + + // A header word's tag, bits 29:28, always 0b11 in every observed record. + pub const TAG_SHIFT: u32 = 28; + pub const TAG_MASK: u32 = 0x3; + pub const TAG_HEADER: u32 = 0x3; + + // The back-end (PB&J) version, bits 2:0. + pub const VERSION: u64 = 0x23380c; + + // Back-end entries 0x2000-0x2fff + pub const CTL_BASE: u32 = 0x2000; + pub const CTL_ENTRIES: u32 = 0x1000; +} + +pub(super) mod lut { + // The colour lookup RAM + pub const SIZE: u32 = 0x2000; + pub const ENTRY_MASK: u32 = 0x3fffffff; + pub const RED_SHIFT: u32 = 22; + pub const GREEN_SHIFT: u32 = 12; + pub const BLUE_SHIFT: u32 = 2; +} + +pub(super) mod dispmode { + // The per-drawable display-mode registers at BASE + DID (drawable id 0..31). + pub const BASE: u32 = 0x2900; + pub const COUNT: u32 = 32; + pub const GROUP_SHIFT: u32 = 4; + pub const GROUP_MASK: u32 = 0x1f; + + // Bits 11:9 of an RGB display mode follow the visual's channel width. + pub const WIDTH_SHIFT: u32 = 9; + pub const WIDTH_MASK: u32 = 0x7; + pub const WIDTH_4: u32 = 4; + pub const WIDTH_5: u32 = 3; +} + +// Colour resolution at scanout. +pub(super) const ODSY_GAMMA_GROUP: u32 = 26; // base 0x1a00: groups >= 26 are RGB +pub(super) const ODSY_GAMMA_BASE: u32 = ODSY_GAMMA_GROUP * 256; +pub(super) const ODSY_CMAP_UNVERIFIED_GROUP: u32 = 25; // base 0x1900, no software gamma +pub(super) const ODSY_DID_MAIN_SHIFT: u32 = 3; +pub(super) const ODSY_DID_OVERLAY_MASK: u32 = 0x7; + +pub(super) mod overlay { + // The overlay. + pub const MODE_BASE: u32 = 0x2920; + pub const IDS: u32 = 8; + pub const MODE_ENABLE: u32 = 0x1; + pub const MODE_GROUP_SHIFT: u32 = 3; + pub const MODE_GROUP_MASK: u32 = 0x1f; + pub const MODE_KNOWN: u32 = 0xf9; +} + +// The id-plane destination's drawing-mask buffers +pub(super) const ODSY_AUX_ID_BUFFER: u32 = 0; +pub(super) const ODSY_AUX_OVERLAY_BUFFER: u32 = 1; + +// 0x608's bit 31, the software buffer state, beside the drawable's base. +pub(super) const ODSY_DRAW_WORD_STATE: u32 = 0x80000000; + +// The colour plane word of an index pixel +pub(super) const ODSY_INDEX_MASK: u32 = 0xfff; +pub(super) mod i2c { + pub const MAIN_CMD_INDEX: u32 = 0x2780; + pub const OPT_CMD_INDEX: u32 = 0x2740; + pub const MAIN_CMD_READ: u64 = 0x233c00; + pub const MAIN_STATUS_READ: u64 = 0x233c08; + pub const OPT_CMD_READ: u64 = 0x233a00; + pub const OPT_STATUS_READ: u64 = 0x233a08; + + // Status bits this model computes at read time + pub const STATUS_DONE: u32 = 1 << 9; + pub const STATUS_READY: u32 = 1 << 22; +} + +pub(super) mod cmd { + // The command FIFO + pub const FIFO: u64 = 0x110000; + + // A "register-select" word, 0x0001nnnn with the register in the low 16 bits. + pub const REGISTER_SHAPE: u32 = 0xffff0000; + pub const REGISTER_TAG: u32 = 0x00010000; + + // The operand-count field every command word carries + pub const ARITY_SHIFT: u32 = 10; + pub const ARITY_MASK: u32 = 0xf; + pub const ARITY_BURST: u32 = 16; + + // A GL-class word's operands-follow bit (Board::cmd_classify()). + pub const GL_OPERANDS: u32 = 0x00800000; + + // The screen-to-screen copy command is a fixed 8-word literal template following a bare trigger, not decomposed through the generic register-select ... + pub const COPY_RECT_TRIGGER: u32 = 0x00002031; + pub const COPY_RECT_WORDS: u32 = 8; + pub const COPY_RECT_SRC_INDEX: u32 = 1; + pub const COPY_RECT_SIZE_INDEX: u32 = 3; + pub const COPY_RECT_DST_INDEX: u32 = 6; + + // The BEGIN-0xa image raster path + pub const STATE_WORD: u32 = 0x29008000; + pub const STATE_WORD_MASK: u32 = 0xfffffc00; + pub const IMAGE_BOX: u32 = 0xcf80a92f; // 10 operands: raster-box floats + pub const IMAGE_HEADER: u32 = 0x00004570; // 1 operand: transfer header + pub const IMAGE_HEADER_TAIL: u32 = 0x00000071; // 0 operands + + // The window restore. + pub const WINDOW_RESTORE: u32 = 0x90008116; // 0 operands +} + +// The per-context USER command FIFO. +pub(super) const ODSY_USER_CMD_FIFO: u64 = 0x100000; + +pub(super) mod dma { + // The page-table pixel/texture DMA descriptor. + pub const DESC_UPLOAD: u32 = 0x00000e01; // opcode 0xe01: either direction + pub const DESC_READBACK: u32 = 0x00000e03; // opcode 0xe03: region path only + pub const DESC_OPERANDS: u32 = 3; // the three words after the opcode + pub const DESC_ADDR_LOW_MASK: u32 = 0x1ffff; // bits 16:0 of a packed half + pub const DESC_ADDR_HIGH_SHIFT: u32 = 23; // bits 29:23: half bits 23:17 + pub const DESC_ADDR_HIGH_MASK: u32 = 0x7f; + + // The fence word the 0x0a5a host-write command's operands form (see super::reg::HOST_WRITE). + pub const FENCE_SLOT_MASK: u32 = 0x7fff; + pub const FENCE_INTERRUPT: u32 = 1 << 15; + pub const FENCE_TOKEN_SHIFT: u32 = 16; + + // The translation table + pub const PAGE_SIZE: u64 = 0x4000; + pub const TABLE_HEADER: u64 = 0x20; + pub const HDR_WORDS: usize = 8; + pub const HDR_DEST: usize = 0; + pub const HDR_ORIGIN: usize = 1; + pub const HDR_WIDTH: usize = 2; + pub const HDR_SIZE: usize = 3; + pub const HDR_SKIP: usize = 5; + pub const HDR_ROWBYTES: usize = 6; + pub const HDR_CONTROL: usize = 7; + pub const ROWBYTES_MASK: u32 = 0xfffff; + pub const BPP_SHIFT: u32 = 16; + pub const BPP_MASK: u32 = 0x7; + pub const IRQ_REQUEST: u32 = 1 << 31; + pub const PTE_LEN_MASK: u32 = 0xffff; + pub const PTE_ADDR_SHIFT: u32 = 16; + + // One kick names one 128-byte work-list entry + pub const ENTRY_PAGES: usize = 12; + pub const MAX_PAGES: usize = ENTRY_PAGES; + + // Board-to-host transfers. + pub const HDR_FORMAT: usize = 4; + pub const TO_HOST: u32 = 1 << 24; // word 7 + pub const BOTTOM_UP: u32 = 1 << 30; // word 7 + pub const SENTINEL: u32 = 1 << 29; // word 0 + pub const SENTINEL_READ: u32 = 0x30000019; + pub const SENTINEL_DRAW: u32 = 0x30000010; + pub const FMT_FRONT: u32 = 1 << 31; // word 4 + pub const FMT_SELECTOR_SHIFT: u32 = 28; // word 4 bits 30:28 + pub const FMT_SELECTOR_MASK: u32 = 0x7; + pub const FMT_SELECTOR_READ: u32 = 1; + pub const FMT_SELECTOR_DRAW: u32 = 0; + pub const FMT_SIZE_SHIFT: u32 = 13; // word 4 bits 15:13 + pub const FMT_SIZE_MASK: u32 = 0x7; + pub const FMT_SIZE_STENCIL: u32 = 0; // a write's size field: stencil + pub const FMT_SIZE_DEPTH: u32 = 1; // and depth + pub const FMT_CLASS_MASK: u32 = 0x7; // word 4 bits 2:0 + pub const FMT_CODE_SHIFT: u32 = 3; // word 4 bits 7:3 + pub const FMT_CODE_MASK: u32 = 0x1f; + + // The GL library's source classes for 8- and 12-bit RGBA visuals. + pub const GL_CLASS_RGBA8: u32 = 2; + pub const GL_CLASS_RGBA12: u32 = 3; + + // The kernel's board-memory manager pages an object out to its backing store in host memory and back in with raw copies of the object's texels. + pub const FMT_RAW16: u32 = 0x1001; + pub const FMT_RAW32: u32 = 0x13002; + pub const FMT_RAW64: u32 = 0x25004; + + // Source format code 0x6c + pub const ABGR_CONTROL: u32 = 0x4001016c; + pub const ABGR_CONTROL_IGNORED: u32 = 0x3e000000; + + // dmplay's movie frames + pub const MOVIE_FORMAT: u32 = 0x82233000; + pub const MOVIE_CONTROL: u32 = 0x4001004c; + + // A read's host bytes for one pixel + pub const BYTE_ZERO: u32 = 4; + pub const BYTE_INDEX: u32 = 5; + + /// The index word's bits 15:8. + pub const BYTE_INDEX_HIGH: u32 = 6; + + /// The index when the pixel is shown through a colour map, else the red byte. + pub const BYTE_INDEX_OR_RED: u32 = 7; + + // The 16 KiB pages an entry's longest piece can touch. + pub const PIECE_PAGES: usize = (PTE_LEN_MASK as u64 + PAGE_SIZE).div_ceil(PAGE_SIZE) as usize; + + // Board-to-host and sentinel reports, each made once (dma_read_reported). + pub const REPORT_DRAW_SENTINEL: u32 = 1 << 0; + pub const REPORT_SENTINEL: u32 = 1 << 1; + pub const REPORT_LAYOUT: u32 = 1 << 2; + pub const REPORT_SOURCE: u32 = 1 << 3; + pub const REPORT_BOUNDS: u32 = 1 << 4; + pub const REPORT_INDEX: u32 = 1 << 5; + pub const REPORT_SHAPE: u32 = 1 << 6; + pub const REPORT_WRITE: u32 = 1 << 7; + pub const REPORT_UPLOAD_SENTINEL: u32 = 1 << 8; + pub const REPORT_INDEX_OBJECT: u32 = 1 << 9; + pub const REPORT_COLOUR_OVER_INDEX: u32 = 1 << 10; + pub const REPORT_RAW64: u32 = 1 << 11; + pub const REPORT_SHARED: u32 = 1 << 12; + pub const REPORT_STENCIL_OR_INDEX: u32 = 1 << 13; + pub const REPORT_UNLISTED_FIELDS: u32 = 1 << 14; +} + +// The completion cookie a head-ops publish() carries +pub(super) const ODSY_FENCE_VALID: u64 = 1 << 32; +pub(super) const ODSY_FENCE_WORD_MASK: u32 = 0xffffffff; + +pub(super) mod dest { + // Off-screen board-memory objects. + pub const WIDTH_INDEX: usize = 1; // 0x1017's row length, 16-pixel units + pub const WIDTH_UNIT: u32 = 16; + + // The select's format word + pub const FORMAT_SHIFT: u32 = 28; + pub const FORMAT_SCREEN: u32 = 0x2; + pub const ID_PLANES: u32 = 0x20080080; // 0x1017 third word: write id planes + + // Two bits of the 0x1017 third word that the X server changes from one draw to the next for the same buffer, so they do not name the buffer. + pub const NO_READ: u32 = 1 << 29; + pub const BIT23: u32 = 1 << 23; + pub const FLAGS: u32 = NO_READ | BIT23; + pub const SELECT_VALUE_INDEX: usize = 2; // the third operand + pub const SELECT_BASE_INDEX: usize = 0; // the buffer's base, 0x2000 +} + +pub(super) mod gl_cmd { + // The 2D stream's GL primitives. + pub const POINTS: u32 = 0x00000000; + pub const LINES: u32 = 0x00000001; + pub const LINE_LOOP: u32 = 0x00000002; + pub const LINE_STRIP: u32 = 0x00000003; + pub const TRIANGLES: u32 = 0x00000004; + pub const TRIANGLE_STRIP: u32 = 0x00000005; + pub const TRIANGLE_FAN: u32 = 0x00000006; + pub const QUADS: u32 = 0x00000007; + pub const QUAD_STRIP: u32 = 0x00000008; + pub const POLYGON: u32 = 0x00000009; + + // Two GL vertex-stream words carry their own tag instead of the register-select shape above (same primary source, section 4). + pub const COLOR_3UB: u32 = 0xc580cc08; // 3 operands: R, G, B, one byte each + pub const VERTEX_2I: u32 = 0x8080c800; // 2 operands: X, Y + + // The greeter draws in PseudoColor + pub const COLOR_INDEX: u32 = 0xc580c406; // 1 operand: PseudoColor index + pub const COLOR_INDEX_F: u32 = 0xc580c407; // 1 operand: the index as a float + pub const REPLAY_INDEX_F: u32 = 0xce80c407; // 1 operand: the index as a float + pub const INDEX_MODE_COLOR_3F: u32 = 0xce80cc0e; // 3 float operands + pub const INDEX_MODE_COLOR_4F: u32 = 0xce80d015; // 4 float operands + + // The other current-colour and normal words of the guest OS GL library (software expectations, not measured hardware). + pub const COLOR_3I: u32 = 0xc580cc0d; // 3 signed integers + pub const INDEX_MODE_COLOR_3I: u32 = 0xce80cc0d; + pub const COLOR_4F: u32 = 0xc580d015; // 4 floats: R, G, B, A + + // The integer colour words, one per glColor variant, in the same shape + pub const COLOR_3US: u32 = 0xc580cc09; + pub const COLOR_3UI: u32 = 0xc580cc0a; + pub const COLOR_3B: u32 = 0xc580cc0b; + pub const COLOR_3S: u32 = 0xc580cc0c; + pub const COLOR_4UB: u32 = 0xc580d00f; + pub const COLOR_4US: u32 = 0xc580d010; + pub const COLOR_4UI: u32 = 0xc580d011; + pub const COLOR_4B: u32 = 0xc580d012; + pub const COLOR_4S: u32 = 0xc580d013; + pub const COLOR_4I: u32 = 0xc580d014; + pub const COLOR_TYPE_MASK: u32 = 0x3f; + pub const COLOR_TYPE_3: u32 = 8; // glColor3ub's type code + pub const COLOR_TYPE_4: u32 = 15; // glColor4ub's type code + pub const NORMAL_3B: u32 = 0xc180cc16; // 3 signed bytes, one per word + pub const NORMAL_3S: u32 = 0xc180cc17; // 3 signed shorts, one per word + pub const NORMAL_3I: u32 = 0xc180cc18; // 3 signed integers + + // The true-colour immediate-mode GL words a real OpenGL client draws with, the float siblings of the integer 2D words above. + pub const COLOR_3F: u32 = 0xc580cc0e; // 3 operands: R, G, B as IEEE floats + pub const VERTEX_2F: u32 = 0x8080c801; // 2 operands: X, Y as IEEE floats + + // The GL library's glVertex3f, object coordinates (ESTABLISHED). + pub const VERTEX_3F: u32 = 0x8080cc03; // 3 operands: X, Y, Z as IEEE floats + + // The GL library's glVertex3i, glVertex3s and their vector forms send this word and the three coordinates as raw signed 32-bit integers, shorts ... + pub const VERTEX_3I: u32 = 0x8080cc02; // 3 operands: X, Y, Z as int32 + + // glVertex4f + pub const VERTEX_4F: u32 = 0x8080d005; + + // Lighting data (section 1, ESTABLISHED from the entry points and two captures with distinct values). + pub const NORMAL_3F: u32 = 0xc180cc19; // 3 operands + pub const MATERIAL_AMBIENT: u32 = 0xc680d02f; // 4 operands, as the next 7 + pub const MATERIAL_DIFFUSE: u32 = 0xc780d030; + pub const MATERIAL_SPECULAR: u32 = 0xc880d031; + pub const MATERIAL_EMISSION: u32 = 0xc980d032; + pub const BACK_AMBIENT: u32 = 0xca80d035; + pub const BACK_DIFFUSE: u32 = 0xcb80d036; + pub const BACK_SPECULAR: u32 = 0xcc80d037; + pub const BACK_EMISSION: u32 = 0xcd80d038; + pub const SHININESS_SQRT: u32 = 0xd080c433; // 1 operand + pub const BACK_SHININESS_SQRT: u32 = 0xd080c439; // 1 operand + pub const SHININESS_DERIVED: u32 = 0x908091ea; // 4 operands + pub const LIGHT_COLOUR_LOAD: u32 = 0x8e809015; // 4 operands + pub const LIGHT_VEC_LOAD: u32 = 0x9080bda2; // 16 operands + pub const CLIP_PLANE_LOAD: u32 = 0x908091bb; // 4 operands + pub const LIGHT_MODEL_AMBIENT: u32 = 24; + pub const LIGHT_VEC_FLOATS: u32 = 16; + pub const MATRIX_SELECT_NORMAL: u32 = 0x34; + pub const TEXCOORD_1F: u32 = 0xc380c41f; // 1 operand: s + pub const TEXCOORD_2F: u32 = 0xc380c821; // 2 operands: s, t + pub const TEXCOORD_3F: u32 = 0xc380cc23; // 3 operands: s, t, r + pub const TEXCOORD_4F: u32 = 0xc380d025; // 4 operands: s, t, r, q + pub const TEXCOORD_SHAPE: u32 = 0xffffff00; + pub const TEXCOORD_TAG: u32 = 0xc380c800; + + // glTexCoord1i to 4i + pub const TEXCOORD_1I: u32 = 0xc380c41e; + pub const TEXCOORD_2I: u32 = 0xc380c820; + pub const TEXCOORD_3I: u32 = 0xc380cc22; + pub const TEXCOORD_4I: u32 = 0xc380d024; + pub const TEX_RECT: u32 = 0x8f80b968; // 14 operands + + // The board-side geometry transform. + pub const MATRIX_SELECT_SHAPE: u32 = 0xffffff00; + pub const MATRIX_SELECT_TAG: u32 = 0x34008000; + pub const MATRIX_SELECT_COMBINED: u32 = 0x00; // the select word's low byte + pub const MATRIX_SELECT_TEXTURE: u32 = 0x14; + pub const MATRIX_SELECT_MODELVIEW: u32 = 0x24; + pub const MATRIX_LOAD: u32 = 0x9080bdc8; + pub const MATRIX_FLOATS: u32 = 16; + + // The GL library's combined-matrix validation follows each slot-0x00 load with a select of index 16 and this word carrying four floats (ESTABLISHED) + pub const MATRIX_SELECT_BLOCK16: u32 = 0x10; + pub const LOAD_4F: u32 = 0x908091df; + pub const BLOCK16_FLOATS: u32 = 4; + pub const BLOCK16_SIGN: u32 = 3; + + // The select's own slot, so the four-float load knows its target. + pub const SLOT_BLOCK16: u32 = 0x10002; + + // The normal transform's slot (MATRIX_SELECT_NORMAL). + pub const SLOT_NORMAL_MATRIX: u32 = 0x10003; + + // The board's depth unit + pub const DEPTH_S: f64 = 4294950912.0; + + // A GL matrix mode the shared context does not keep (neither modelview nor projection), so GlContext::set_matrix() logs and refuses a load aimed at it. + pub const SLOT_NORMAL: u32 = 0x1702; + + // This model's own slots for the combined and texture matrices. + pub const SLOT_COMBINED: u32 = 0x10000; + pub const SLOT_TEXTURE: u32 = 0x10001; + + // Half the extent of the fixed viewport the shared context draws through (Board::gl_load_combined()) + // It is iris's own limit, set by its integer edge arithmetic, not a fact about the board. It matches the library's class-3 guard + // (|x| or |y| > 8192 |w|) and is twice the largest drawable (under 4096). The window-matrix path's GL_WINDOW_GUARD (16384) is + // separate: that path measures from the drawable's origin. The hard limit is the +-65536 check in project(). + pub const WINDOW_REACH: i32 = 8192; + + // The screen-aligned rectangle. + pub const RECT: u32 = 0x8f80a944; + pub const RECT_FLOATS: u32 = 10; + + // The kernel driver's buffer-clear request, made by a GL client with its parameters, sends the same ten-float record under this word inside BEGIN 0xa + pub const CLEAR_BOX: u32 = 0x8f80a92f; + + // The identity the shared GL context keys draws by + pub const CONTEXT_HW_ID: u32 = 1; + + // Proposed shape-4 screen-aligned rectangle policy. + pub const RECT_PATTERN_ENABLE: u32 = 1 << 4; + + // User clip planes, class 0x31 index 0x120 + 4j, reusing the same index-select field as the light-vector loads (super::prog::LIGHT_VEC_SELECT, ... + pub const CLIP_PLANE_BASE: u32 = 0x120; + pub const CLIP_PLANE_STRIDE: u32 = 4; + + // BEGIN's operand for an image (as opposed to 0x7, QUADS). + pub const PRIMITIVE_IMAGE: u32 = 0xa; + + // The index word the kernel's template carries + pub const REPLAY_INDEX: u32 = 0xce80c406; // 1 operand + + // How far past [0, 1] a fragment's depth may round and still be drawn, clamped + pub const DEPTH_SLACK: f64 = 1.0f64 / (1u32 << 20) as f64; +} + +pub(super) mod mode { + pub const INDEX_ZEROS: u32 = 0xce80cc08; // 3 operands: three zero words + + // Depth state (ESTABLISHED from the GL library's entry points and the captures) + pub const DEPTH_TEST: u32 = 1 << 16; + pub const FOG: u32 = 1 << 22; + pub const GL_LINE_STIPPLE: u32 = 1 << 3; + pub const POINT_SMOOTH: u32 = 1 << 0; + pub const LINE_SMOOTH: u32 = 1 << 1; + + // The GL library keeps the shade model in the same register + pub const FLAT_SHADE: u32 = 1 << 12; + + // Bit 18 of the same register is GL_DITHER (ESTABLISHED) + pub const DITHER: u32 = 1 << 18; + + // Bit 14 of the same register is GL_ALPHA_TEST + pub const ALPHA_TEST: u32 = 1 << 14; + + // Bit 17 of the same register is a computed enable, not a plain mirror of glEnable(GL_BLEND) + pub const BLEND: u32 = 1 << 17; + + // Bits 19 and 20 of the same register are GL_INDEX_LOGIC_OP and GL_COLOR_LOGIC_OP. + pub const INDEX_LOGIC_OP: u32 = 1 << 19; + pub const COLOR_LOGIC_OP: u32 = 1 << 20; + + // Bit 15 of the same register is GL_STENCIL_TEST, which the library sets only for a drawable with stencil planes. + pub const STENCIL_TEST: u32 = 1 << 15; + + // Bit 8 of the 0x0404 mode register is GL_CULL_FACE, bits 10:9 the face it culls -- 01 GL_BACK, 10 GL_FRONT, 11 GL_FRONT_AND_BACK, with 00 never sent ... + pub const CULL_FACE: u32 = 1 << 8; + pub const CULL_MODE_SHIFT: u32 = 9; + pub const CULL_MODE_MASK: u32 = 0x3; + pub const FRONT_FACE_CW: u32 = 1 << 23; + + // Bits 5, 6 and 7 of the same register are GL_POLYGON_OFFSET_POINT, _LINE and _FILL + pub const OFFSET_POINT: u32 = 1 << 5; + pub const OFFSET_LINE: u32 = 1 << 6; + pub const OFFSET_FILL: u32 = 1 << 7; +} + +pub(super) const ODSY_DEPTH_FUNC_MASK: u32 = 0x7; +pub(super) mod write { + pub const DEPTH: u32 = 1 << 24; + pub const COLOUR_SHIFT: u32 = 20; + pub const COLOUR_MASK: u32 = 0xf; + pub const MASK_RESET: u32 = 0x01f00000; + pub const STENCIL_SHIFT: u32 = 12; + pub const STENCIL_MASK: u32 = 0xff; + + // 0x452 bits 11:0 are glIndexMask ANDed with the drawable's index-plane mask + pub const INDEX_MASK: u32 = 0xfff; + + /// A GL write's format (bits 4:0) and type (8:5) together. + pub const CODE_MASK: u32 = 0x1ff; +} + +// Out of reset +pub(super) const ODSY_DEPTH_FUNC_RESET: u32 = 0x1; + +pub(super) mod light { + // Light block floats + pub const POS: usize = 0; + pub const HALF: usize = 3; + pub const ATTEN: usize = 7; + pub const SPOT_DIR: usize = 10; + pub const SPOT_COS: usize = 13; + pub const SPOT_EXP: usize = 14; +} +pub(super) mod prog { + pub const SHAPE: u32 = 0x007ffc00; + pub const TAG: u32 = 0x00008000; + pub const LOW_MASK: u32 = 0x3ff; + pub const HIGH_SHIFT: u32 = 23; + pub const CLASS_SHIFT: u32 = 24; + + // The colour mode, in the same word shape. + pub const COLOUR_MODE: u32 = 0x22; + pub const COLOUR_MODE_B: u32 = 0x23; + pub const SET_UCODE: u32 = 0x25; + pub const PROGRAM_ADDR: u32 = 0x27; + pub const CONST_SELECT: u32 = 0x28; + pub const ENTRY_29: u32 = 0x29; + pub const ENTRY_2A: u32 = 0x2a; + pub const ENTRY_2C: u32 = 0x2c; + pub const LIGHT_VEC_SELECT: u32 = 0x31; + pub const LIGHT_COLOUR_SELECT: u32 = 0x32; + pub const LIST_INDEX: u32 = 0x33; + + // The colour-material word, class 0x35 + pub const COLOUR_MATERIAL: u32 = 0x35; + + // Classes 0x20 to 0x35 are board registers the host never reads back; the GL + // library keeps a copy of each at a fixed stride, so the numbering is + // contiguous. "Sequencer" is not an established name for them. Class 0x27 is + // the microcode write address (ESTABLISHED; PROGRAM_ADDR). The + // kernel writes 0x2e = 0x380 and 0x26 = 0x7f at set-up: the values are + // ESTABLISHED and their roles UNKNOWN. The model acts on the + // classes it names, keeps every value in gl_regs_20_35[] and accepts the rest, whose + // meaning is UNKNOWN, without effect. + pub const REG_FIRST: u32 = 0x20; + pub const REG_LAST: u32 = 0x35; + pub const PROGRAM_HALFWORD: u32 = 0x40; + pub const PROGRAM_HALFWORD_LAST: u32 = 0x5f; + pub const CONST_STORE: u32 = 0x60; + pub const HALFWORD_HIGH_MASK: u32 = 0x3f; + pub const ENTRY_HALFWORDS: u32 = 6; + pub const RUN_ONCE: u32 = 0x90808800; // 2 operands + + // How far a program list is read before it counts as unrecognised. + pub const LIST_MAX: u32 = 64; + pub const WINDOW_BASE: u32 = 0x738; + pub const WINDOW_ENTRIES: u32 = super::ODSY_GL_WINDOW_ENTRIES; +} + +pub(super) mod colour { + pub const MODE_INDEX: u32 = 0x1; + pub const MATERIAL_MASK: u32 = 0xff; + pub const MATERIAL_AMBIENT: u32 = 0x1; + pub const MATERIAL_DIFFUSE: u32 = 0x2; + pub const MATERIAL_SPECULAR: u32 = 0x4; + pub const MATERIAL_EMISSION: u32 = 0x8; +} + +pub(super) mod ucode { + // The older GL libraries' program image lacks the 99-record spot routine at + // records 792 to 890 and every record from 2365 on, so they number each + // routine from 891 on 99 lower than the current libraries do. + pub const SPOT_FIRST: u32 = 792; + pub const SPOT_RECORDS: u32 = 99; + + // The older image's length: records 0-791 and 891-2364 of the current one. + pub const OLDER_RECORDS: u32 = 2266; + pub const END: u32 = 0x4c5; + pub const UNLIT: u32 = 0x0d6; + pub const UNLIT_2A: u32 = 0x0e0; + pub const UNLIT_2C: u32 = 0x0ea; + pub const UNLIT_CLIP: u32 = 0x0f3; + pub const UNLIT_CLIP_2A: u32 = 0x100; + pub const UNLIT_CLIP_2C: u32 = 0x10c; + + // The assembled unlit list + pub const HEAD_UNLIT: u32 = 0x4ae; + pub const UNLIT_TEXTURE: u32 = 0x4bb; + pub const UNLIT_FINISH: u32 = 0x4c0; + + // Assembled lit list heads + pub const HEAD_INF_DIR: u32 = 0x290; + pub const HEAD_INF_POS: u32 = 0x272; + pub const HEAD_LOCAL_DIR: u32 = 0x281; + pub const HEAD_LOCAL_POS: u32 = 0x263; + + // The normal transform without and with GL_NORMALIZE. + pub const NORMAL: u32 = 0x2b6; + pub const NORMALIZE: u32 = 0x29c; + + // Per-light fragments + pub const LIGHT_DIR: u32 = 0x300; + pub const LIGHT_POS: u32 = 0x398; + pub const LIGHT_PLAIN: u32 = 0x2cf; + pub const LIGHT_ATTEN: u32 = 0x2de; + + // The lighting finish + pub const FINISH_A: u32 = 0x3cf; + pub const FINISH_A_TWO_SIDED: u32 = 0x3e5; + pub const FINISH_A_TEXTURE: u32 = 0x3da; + pub const FINISH_ONE_SIDED: u32 = 0x3f5; + pub const FINISH_TWO_SIDED: u32 = 0x415; + pub const FINISH_TEXTURE: u32 = 0x40b; + pub const END_ONE_SIDED: u32 = 0x49a; + pub const END_TWO_SIDED: u32 = 0x49f; + + // The list's tail + pub const TAIL_ONE_SHOT: u32 = 0x203; + pub const TAIL: u32 = 0x25e; + + // A hardware clip-plane stage is a 0 word followed by one of these. + pub const CLIP: u32 = 0x00c; + pub const CLIP_TEXTURED: u32 = 0x01a; + pub const CLIP_TEXGEN: u32 = 0x028; +} + +pub(super) mod tex { + pub const MODE_1D: u32 = 1 << 3; + pub const MODE_2D: u32 = 1 << 4; + pub const MODE_3D: u32 = 1 << 5; + pub const COLOUR_TABLE_BLOCKS: u32 = 16; + pub const COLOUR_TABLE_BLOCK_WORDS: u32 = 16; + pub const TABLE3_BLOCKS: u32 = 16; + pub const DETAIL_FUNCTION_BLOCKS: u32 = 16; + pub const DETAIL_FUNCTION_BLOCK_WORDS: u32 = 16; + pub const MODE_CLIP_COUNT_MASK: u32 = 0x7; + + // The stock driver's two-point function (-4, 1/4), (0, 1/2) fills entry 0 with 1/2, falls by 1/512 per entry to 1/4 at entry 128 and holds 1/4 through ... + pub const DETAIL_FUNCTION_LAST_LOD: f64 = -((super::ODSY_TEX_DETAIL_FUNCTION_ENTRIES as f64) - 1.0) / 32.0; + + // Post-filter scale and bias (0x203b, where texture validation sends a texture's words) + pub const SCALE_BIAS_WORDS: u32 = 8; + pub const SCALE_BIAS_BITS: u32 = 16; + pub const SCALE_BIAS_ONE: u32 = 4096; + + // Sampler word 4 bits 7:5 are the minification code (0 nearest, 1 linear, 2 to 5 the four mipmap modes in GL order) and bits 4:1 the magnification code ... + pub const MIN_MASK: u32 = 0xe0; + pub const MIN_NEAREST_MIPMAP_NEAREST: u32 = 0x40; + pub const MIN_LINEAR_MIPMAP_LINEAR: u32 = 0xa0; + + // Sampler word 4 bits 25:23, the texel class + pub const COMPONENTS_MASK: u32 = 0x03800000; + + // Word 4 bit 17 + pub const SAMPLER_BIT17: u32 = 1 << 17; + + // The four 0x1013 words are 14-bit two's complement with 8 fraction bits + pub const LOD_MASK: u32 = 0x3fff; + pub const LOD_BITS: u32 = 14; + pub const LOD_ONE: u32 = 256; + pub const LOD_LIMIT: u32 = 15 * LOD_ONE; +} + +pub(super) const ODSY_F32_ONE: u32 = 0x3f800000; +pub(super) const ODSY_F32_POINT_TWO: u32 = 0x3e4ccccd; +pub(super) const ODSY_F32_POINT_EIGHT: u32 = 0x3f4ccccd; +pub(super) const ODSY_DEPTH24_MAX: u32 = 0xffffff; + +pub(super) const ODSY_COPY_RECT_SOURCE_SHIFT: u32 = 12; +pub(super) const ODSY_COPY_RECT_DEST_SHIFT: u32 = 20; +pub(super) const ODSY_COPY_RECT_FORMAT_SOURCE_SHIFT: u32 = 24; + +pub(super) mod pattern { + pub const WORDS: u32 = 16; + + // The 0x043f shape operand. + pub const SHAPE_64X16: u32 = 0x5; + pub const SHAPE_32X32: u32 = 0x4; + pub const SHAPE_16X64: u32 = 0x6; + + // Bit 4 of the 0x043f operand makes a pattern opaque + pub const SHAPE_OPAQUE: u32 = 0x10; + + // The 0x0404 mode operand's pattern-enable bits + pub const MODE_ENABLE: u32 = 0x18; +} + +pub(super) mod line { + pub const STIPPLE_PATTERN: usize = 1; // the 32-bit rotated row + pub const STIPPLE_CONTROL: usize = 2; + pub const STIPPLE_CONTROL_32: u32 = 0x30f; + pub const POINT_SIZE_MASK: u32 = 0xffffff; + pub const POINT_SIZE_SCALE: f32 = 128.0; +} + +pub(super) const ODSY_PLANE_MASK_SHIFT: u32 = 12; // the 0x1023 operand holds mask << 12 + +pub(super) mod alpha { + pub const FUNC_SHIFT: u32 = 12; + pub const FUNC_MASK: u32 = 0x7; + pub const REF_MASK: u32 = 0xfff; + /// The field is trunc(x * 4096), with 1.0 sent as 4095 (the clamp) and + /// 0x800 = 0.5. INFERRED (graded): the board compares min(4095, + /// floor(4096 a)), and the vendor frames rule out field / 4095. + pub const REF_SCALE: f32 = 4096.0; + pub const REF_ONE: u32 = 0xfff; +} + +pub(super) mod blend { + pub const DST_MASK: u32 = 0x3f; + pub const SRC_SHIFT: u32 = 6; + pub const SRC_MASK: u32 = 0x7f; + pub const EQ_SHIFT: u32 = 13; + pub const EQ_MASK: u32 = 0x7; + pub const COLOUR_MASK: u32 = 0xfff; + pub const COLOUR_SHIFT: u32 = 12; + pub const COLOUR_SCALE: f32 = 4096.0; + + // The X server's opaque-span 0x0c21 operand 0 + pub const X_OPAQUE: u32 = 0x200; +} + +pub(super) const ODSY_LOGIC_OP_MASK: u32 = 0xf; + +pub(super) mod stencil { + pub const FUNC_SHIFT: u32 = 25; + pub const FUNC_MASK: u32 = 0x7; + pub const REF_MASK: u32 = 0xff; + pub const VALUE_MASK_SHIFT: u32 = 8; + pub const VALUE_MASK_MASK: u32 = 0xff; + pub const OP_FAIL_SHIFT: u32 = 22; + pub const OP_ZFAIL_SHIFT: u32 = 19; + pub const OP_ZPASS_SHIFT: u32 = 16; + pub const OP_MASK: u32 = 0x7; + pub const MAX: u32 = 0xff; +} +/// Where a depth or stencil burst's word holds the stencil index. +pub(super) const ODSY_BURST_STENCIL_SHIFT: u32 = 16; + +pub(super) mod polygon { + pub const OFFSET_BITS: u32 = 25; + + // The field is a signed count of steps of the encoded 24-bit depth word + // (depth_word.rs); the shared context carries it as that count over 2^24, which + // RasterRules::split_offset() turns back into steps. + pub const OFFSET_SCALE: f32 = 1.0f32 / (1u32 << 24) as f32; + pub const MODE_FRONT_SHIFT: u32 = 2; + pub const MODE_BACK_SHIFT: u32 = 0; + pub const MODE_MASK: u32 = 0x3; +} + +pub(super) mod clip { + pub const BOX_MIN_SHIFT: u32 = 12; + pub const BOX_FIELD_MASK: u32 = 0xfff; + pub const FIELD_MASK: u32 = 0xfff; + pub const X_SHIFT: u32 = 12; + + // The off-screen 0x204b + pub const OFFSCREEN_HI: u32 = 0x00ffffff; + pub const WORD_MASK: u32 = 0x00ffffff; + + // The clip rectangles out of reset + pub const RESET_FULL: u32 = 0x00ffffff; + pub const RESET_EMPTY: u32 = 0x00001001; +} + +pub(super) mod image { + // The floats 4, 5, 7, 8 of the 0xcf80a92f box record are the destination corners + pub const BOX_X1_INDEX: u32 = 4; + pub const BOX_Y1_INDEX: u32 = 5; + pub const BOX_X2_INDEX: u32 = 7; + pub const BOX_Y2_INDEX: u32 = 8; + + // A pixel burst is 0x00014000 | (words - words/16) << 10 | fmt + pub const BURST_BASE: u32 = 0x4000; + pub const BURST_FMT_MASK: u32 = 0x1f; + pub const BURST_RESERVED_MASK: u32 = 0x3e0; + pub const FMT_CI: u32 = 0x10; + pub const FMT_RGB: u32 = 0x11; + pub const FMT_RGBA16: u32 = 0x12; + + // The source pixel format + pub const CODE_MASK: u32 = 0xff; + + /// Format 0 (depth or stencil) with type 4 (four bytes a pixel). + pub const CODE_DEPTH_STENCIL: u32 = 0x80; + pub const CODE_CI: u32 = 0x41; + pub const CODE_CI16: u32 = 0x61; + pub const CODE_LUM: u32 = 0x42; + pub const CODE_LUM_ALPHA: u32 = 0x48; + pub const CODE_RGB: u32 = 0x49; + pub const CODE_RGBA: u32 = 0x4b; + pub const CODE_ABGR: u32 = 0x4c; + + // Not a board code + pub const CODE_FALLBACK_XRGB: u32 = 0x100; + pub const CODE_RGBA12: u32 = 0x6b; + pub const CODE_LUM12: u32 = 0x62; + pub const CODE_LUM_ALPHA12: u32 = 0x68; + pub const CODE_RGB12: u32 = 0x69; + pub const CODE_ABGR12: u32 = 0x6c; + + // The transfer header's first-burst count, bits 23:20, 15 standing for 16. + pub const HEADER_GROUP_SHIFT: u32 = 20; + pub const HEADER_GROUP_MASK: u32 = 0xf; + pub const HEADER_GROUP_FULL: u32 = 16; +} + +pub(super) const ODSY_ID_COMPARE_ENABLE: u32 = 1 << 16; +pub(super) const ODSY_ID_COMPARE_MASK_SHIFT: u32 = 8; +pub(super) const ODSY_ORIGIN_BITS: u32 = 13; + +pub(super) const ODSY_CONTROL_WIDGET_MASK: u32 = 0xf; + +pub(super) mod config { + // Memory configuration + pub const SIZE_SHIFT: u32 = 30; + pub const FOUR_BANKS: u32 = 1 << 29; + pub const LATENCY_2: u32 = 1 << 28; + + // Bits 27:24 the live V12 capture read back as 0x1 + pub const COPY_FIELD: u32 = 1 << 24; +} + +// Reads and writes are reported apart +pub(super) const ODSY_UNIMP_WRITE: u32 = 1 << 31; + +pub(super) mod cursor { + // The display back end's 32 by 32 hardware cursor, drawn over the resolved picture + pub const BASE: u32 = 0x2500; + pub const ENTRIES: u32 = 0x100; + pub const GLYPH: u32 = 0x00; + pub const POSITION: u32 = 0x80; + pub const CONTROL: u32 = 0x81; + pub const COLOURS: u32 = 0x90; + pub const COLOUR_COUNT: u32 = 15; + pub const PLANE: u32 = 0xc0; + pub const SIZE: u32 = 32; + + // Control bits. + pub const CTL_ENABLE: u32 = 1 << 0; + pub const CTL_CROSSHAIR: u32 = 1 << 2; + pub const CTL_CHANNEL1: u32 = 1 << 5; + pub const CTL_KNOWN: u32 = 0xc000003f; + + // Once-only reports, bits of cursor_reported. + pub const REPORT_GLYPH15: u32 = 1 << 0; + pub const REPORT_PLANE: u32 = 1 << 1; + pub const REPORT_CROSSHAIR: u32 = 1 << 2; + pub const REPORT_CHANNEL1: u32 = 1 << 3; + pub const REPORT_CONTROL: u32 = 1 << 4; + pub const REPORT_ENTRY: u32 = 1 << 5; +} + +pub(super) mod hw { + // The hardware shadow page + pub const PIPE_STATE: u32 = 0x80; + pub const PIPE_IDLE: u32 = 0xf; + + // Pipe-state bit 11 of an open triangle strip + pub const PIPE_STRIP_ODD: u32 = 1 << 11; + pub const CURRENT_ATTRIBUTES: u32 = 0x84; + pub const DESCRIPTOR_WORDS: u32 = 10; // +0x84 to +0xab + pub const INDEX_WORD: u32 = 0x240; + pub const COLOUR_WORD: u32 = 0x250; + pub const INDEX_OPERANDS: u32 = 0x800; + pub const COLOUR_OPERANDS: u32 = 0x840; + pub const RING_WORD: u32 = 0x100; + pub const RING_WORD_STRIDE: u32 = 16; + pub const RING_OPERANDS: u32 = 0x300; + pub const RING_OPERAND_STRIDE: u32 = 64; + pub const ATTR_INDEX: usize = 1; + pub const ATTR_COLOUR: usize = 2; + pub const ATTR_NORMAL: usize = 3; + pub const ATTR_TEXTURE: usize = 4; + pub const ATTR_EDGE: usize = 7; + pub const ATTR_MATERIAL: usize = 8; + pub const ATTR_SHININESS: usize = 12; + pub const ATTR_BACK_MATERIAL: usize = 14; + pub const ATTR_BACK_SHININESS: usize = 18; +} + +// A volume level's slices sit 64 << word 2 address units apart. +pub(super) const ODSY_VOLUME_SLICE_SHIFT_MAX: u32 = 20; + +pub(super) const ODSY_CRC64_ECMA_POLY: u64 = 0x42f0e1eba9ea3693; + +pub(super) mod query { + // The raster-position query. + pub const KIND_RASTER: u32 = 3; + pub const RASTER_INDEX: u32 = 0x928; + pub const RASTER_COLOUR: u32 = 0x93c; +} +/// The last of the four zero-operand blocks (0x3cf8 to 0x3cfb) that end a +/// glRasterPos stream. INFERRED as the point where the board latches the +/// raster index, colour and texture coordinate. +pub(super) const ODSY_RASTER_POS_LATCH_WORD: u32 = 0x3cfb; + +// The GL library's conversion of a signed-integer colour or normal operand to a float, in single precision as its query decoder does. +pub(super) const ODSY_F32_SCALE_8: u32 = 0x3b808081; +pub(super) const ODSY_F32_SCALE_16: u32 = 0x37800080; +pub(super) const ODSY_F32_SCALE_32: u32 = 0x2f800005; + +// The host ring under the command FIFO, in entries (fifo.rs). A store is one +// entry whatever its width, so it holds at least twice the model's FIFO in +// words (cfifo::WORDS, a model choice); a store is refused only when it is full. +pub(super) const ODSY_WORK_QUEUE_MAX: u32 = 65536; + +// gl_entry[] slots. +pub(super) const ODSY_ENTRY_29: usize = 0; +pub(super) const ODSY_ENTRY_2A: usize = 1; +pub(super) const ODSY_ENTRY_2C: usize = 2; +pub(super) const ODSY_ENTRY_33: usize = 3; + +// Every command word carries its own operand count in bits 13:10, the field +// value 15 encoding a 16-word burst. A register-select's low 16 bits carry +// the same field, so this reads correctly given the whole word or just the +// register. +pub(super) fn cmd_arity(word: u32) -> u32 { + let count = (word >> cmd::ARITY_SHIFT) & cmd::ARITY_MASK; + + if count == cmd::ARITY_MASK { cmd::ARITY_BURST } else { count } +} diff --git a/src/dev/odyssey/context.rs b/src/dev/odyssey/context.rs new file mode 100644 index 000000000..64509f773 --- /dev/null +++ b/src/dev/odyssey/context.rs @@ -0,0 +1,356 @@ + +use crate::devlog::LogModule; +use super::board::{Board, OpenVertex}; +use super::consts::{cmd_arity, ctx, gl_cmd, hw, tex, ODSY_F32_ONE}; +use super::pixel_dma::write_guest; +use super::registers::BACK_MATERIAL_DEFAULTS; +// Anonymous enum: current-value query kinds. +pub(super) const ODSY_QUERY_INDEX: u32 = 0; + +pub(super) const ODSY_QUERY_COLOUR: u32 = 1; + +pub(super) const ODSY_QUERY_NORMAL: u32 = 2; + +pub(super) const ODSY_QUERY_TEXCOORD: u32 = 3; + +pub(super) const ODSY_QUERY_EDGE: u32 = 4; + +impl Board { + pub fn back_material_reset(&mut self) { + self.geom.back_material.copy_from_slice(&BACK_MATERIAL_DEFAULTS); + self.geom.back_shininess_sqrt = 0; + } + + pub fn shadow_put(&mut self, base: u64, off: u32, word: u32) { + let at = base.wrapping_add(off as u64); + if !write_guest(&self.guest, at, &word.to_be_bytes()) && !self.diag.ctx_shadow_fail_reported { + self.diag.ctx_shadow_fail_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: context shadow store to {:#x} failed", at); + } + } + + /// One selected ring entry: attribute a's word and its raw operands. + pub fn shadow_ring(&mut self, base: u64, a: u32, slot: u32, word: u32, ops: &[u32]) { + self.shadow_put(base, hw::RING_WORD + hw::RING_WORD_STRIDE * a + 4 * slot, word); + for (i, op) in ops.iter().enumerate() { + self.shadow_put( + base, + hw::RING_OPERANDS + hw::RING_OPERAND_STRIDE * a + 16 * slot + 4 * i as u32, + *op, + ); + } + } + + /// One two-word descriptor of an open-primitive save. Attribute A's 2-bit + /// ring-slot selector is at bit 2 * (16 - A) of the first word for A 1..16 + /// and at bit 2 * (32 - A) of the second for A 17 and 18. An attribute the + /// model keeps no record of (record[A][0] zero) selects slot 0, which this + /// save never writes for it, so the replay sends back what that slot already + /// holds. This is an approximation: the board's values for these attributes, + /// and the slots it saves them in, are unknown. + pub fn shadow_descriptor(&mut self, base: u64, off: u32, record: &[[u32; 5]; 19], slot: u32) { + let mut word = [0u32; 2]; + + for (a, rec) in record.iter().enumerate().skip(1) { + if rec[0] == 0 { + continue; + } + if a <= 16 { + word[0] |= slot << (2 * (16 - a)); + } else { + word[1] |= slot << (2 * (32 - a)); + } + } + self.shadow_put(base, off, word[0]); + self.shadow_put(base, off + 4, word[1]); + } + + pub fn open_current_records(&mut self, record: &mut [[u32; 5]; 19]) { + const MATERIAL: [u32; 4] = [ + gl_cmd::MATERIAL_AMBIENT, + gl_cmd::MATERIAL_DIFFUSE, + gl_cmd::MATERIAL_SPECULAR, + gl_cmd::MATERIAL_EMISSION, + ]; + const BACK_MATERIAL: [u32; 4] = [gl_cmd::BACK_AMBIENT, gl_cmd::BACK_DIFFUSE, gl_cmd::BACK_SPECULAR, gl_cmd::BACK_EMISSION]; + + *record = [[0u32; 5]; 19]; + record[hw::ATTR_INDEX] = self.query.current[ODSY_QUERY_INDEX as usize]; + record[hw::ATTR_COLOUR] = self.query.current[ODSY_QUERY_COLOUR as usize]; + record[hw::ATTR_NORMAL] = self.query.current[ODSY_QUERY_NORMAL as usize]; + record[hw::ATTR_TEXTURE] = self.query.current[ODSY_QUERY_TEXCOORD as usize]; + record[hw::ATTR_EDGE] = self.query.current[ODSY_QUERY_EDGE as usize]; + for i in 0..4usize { + record[hw::ATTR_MATERIAL + i][0] = MATERIAL[i]; + record[hw::ATTR_MATERIAL + i][1..5].copy_from_slice(&self.geom.material[4 * i..4 * i + 4]); + record[hw::ATTR_BACK_MATERIAL + i][0] = BACK_MATERIAL[i]; + record[hw::ATTR_BACK_MATERIAL + i][1..5].copy_from_slice(&self.geom.back_material[4 * i..4 * i + 4]); + } + record[hw::ATTR_SHININESS][0] = gl_cmd::SHININESS_SQRT; + record[hw::ATTR_SHININESS][1] = self.geom.shininess_sqrt; + record[hw::ATTR_BACK_SHININESS][0] = gl_cmd::BACK_SHININESS_SQRT; + record[hw::ATTR_BACK_SHININESS][1] = self.geom.back_shininess_sqrt; + } + + pub fn open_record_vertex(&mut self, word: u32) { + let mut v = OpenVertex { word, slot: self.prim.open_r, ..Default::default() }; + let mut n = self.prim.open_held as usize; + let arity = (cmd_arity(word) as usize).min(4); + + v.operands[..arity].copy_from_slice(&self.cmd.operand[..arity]); + self.open_current_records(&mut v.attribute); + if self.stream.primitive == 2 || self.stream.primitive == 6 { + self.prim.open_r = if self.prim.open_r == 3 { 1 } else { self.prim.open_r + 1 }; + } else { + self.prim.open_r = (self.prim.open_r + 1) & 3; + } + self.prim.open_c = (self.prim.open_c + 1).min(7); + self.prim.open_m3 = (self.prim.open_m3 + 1) % 3; + self.prim.open_m4 = (self.prim.open_m4 + 1) & 3; + + match self.stream.primitive { + 0 => { + // Points retain no vertices. + n = 0; + } + 1 => { + // Lines retain one of each pair. + if n != 0 { + n = 0; + } else { + self.prim.open_vertex[0] = v; + n = 1; + } + } + 2 | 6 => { + // Anchor and latest vertex. + self.prim.open_vertex[if n != 0 { 1 } else { 0 }] = v; + n = (n + 1).min(2); + } + 3 => { + // Line strip. + self.prim.open_vertex[0] = v; + n = 1; + } + 4 | 7 => { + // Triangles and quads. + if n == (if self.stream.primitive == 4 { 2 } else { 3 }) { + n = 0; + } else { + self.prim.open_vertex[n] = v; + n += 1; + } + } + 5 => { + // Triangle strip. + if n == 2 { + self.prim.open_vertex[0] = self.prim.open_vertex[1]; + self.prim.open_vertex[1] = v; + } else { + self.prim.open_vertex[n] = v; + n += 1; + } + } + 8 => { + // Quad strip. + if n == 3 { + self.prim.open_vertex[0] = self.prim.open_vertex[2]; + self.prim.open_vertex[1] = v; + n = 2; + } else { + self.prim.open_vertex[n] = v; + n += 1; + } + } + _ => {} + } + self.prim.open_held = n as u32; + } + + pub fn restart_finish(&mut self) { + let n = self.prim.open_held; + let mut count; + + if !self.prim.restart_armed || self.prim.restart_absorb != 0 { + return; + } + if self.prim.restart_m3 >= 3 || n != self.prim.restart_total { + self.cmd_unimplemented(0x741); + self.prim.restart_armed = false; + return; + } + self.prim.open_c = if self.prim.restart_c == 7 { 7 } else { (self.prim.restart_c + n).min(7) }; + self.prim.open_m3 = (self.prim.restart_m3 + n) % 3; + self.prim.open_m4 = (self.prim.restart_m4 + n) & 3; + count = self.prim.open_c; + if count == 7 { + while (count % 3 != self.prim.open_m3 || count % 4 != self.prim.open_m4) && count < 19 { + count += 1; + } + } + self.prim.in_prim = self.gl_ctx.resume_primitive(self.prim.restart_type, Some(&self.prim.restart_vertex[..]), n, count); + self.prim.restart_armed = false; + } + + /// The board's save of the outgoing context's geometry-engine state into the + /// hardware shadow, before the completion word. Register state needs no save: + /// the kernel re-pushes it from the owner's software shadow on every + /// switch-in, so the model keeps its single copy. What the kernel cannot + /// restore itself is the current vertex attributes, which it replays from this + /// page. Each record goes out as the last command word it retained with its + /// raw operands; nothing is converted between a setter, this save and a later + /// query. This is an approximation: the physical board's saved words and + /// operand encodings are unknown. Every save writes all ten descriptor words. + /// Idle selects ring slot 0. A float primitive of type 0..8 packs its retained + /// raw attribute records into ring slots 0..N-1 at save time, and current + /// records into slot 3. Integer 2D, type 10 and type 9 polygon assembly still + /// report an unsupported switch and save idle. This producer and its restart + /// decode are model approximations, not established board behaviour. + pub fn ctx_save(&mut self, base: u64) { + const MATERIAL: [u32; 4] = [ + gl_cmd::MATERIAL_AMBIENT, + gl_cmd::MATERIAL_DIFFUSE, + gl_cmd::MATERIAL_SPECULAR, + gl_cmd::MATERIAL_EMISSION, + ]; + const BACK_MATERIAL: [u32; 4] = [gl_cmd::BACK_AMBIENT, gl_cmd::BACK_DIFFUSE, gl_cmd::BACK_SPECULAR, gl_cmd::BACK_EMISSION]; + let index = self.query.current[ODSY_QUERY_INDEX as usize]; + let colour = self.query.current[ODSY_QUERY_COLOUR as usize]; + let normal = self.query.current[ODSY_QUERY_NORMAL as usize]; + let edge = self.query.current[ODSY_QUERY_EDGE as usize][0]; + let open = self.prim.begin_open && !self.prim.integer_prim && self.stream.primitive <= 8; + let ar = |w: u32| (cmd_arity(w) as usize).min(4); + + if self.prim.begin_open && !open && !self.diag.ctx_open_prim_logged { + self.diag.ctx_open_prim_logged = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: context switch inside primitive {:#x}; its buffered vertices are not saved", + self.stream.primitive + ); + } + self.shadow_put(base, hw::PIPE_STATE, hw::PIPE_IDLE); + for i in 0..hw::DESCRIPTOR_WORDS { + self.shadow_put(base, hw::CURRENT_ATTRIBUTES + 4 * i, 0); + } + self.shadow_put(base, hw::INDEX_WORD, index[0]); + for i in 0..cmd_arity(index[0]) { + self.shadow_put(base, hw::INDEX_OPERANDS + 4 * i, index[1 + (i as usize).min(3)]); + } + self.shadow_put(base, hw::COLOUR_WORD, colour[0]); + for i in 0..cmd_arity(colour[0]) { + self.shadow_put(base, hw::COLOUR_OPERANDS + 4 * i, colour[1 + (i as usize).min(3)]); + } + self.shadow_ring(base, hw::ATTR_INDEX as u32, 0, index[0], &index[1..1 + ar(index[0])]); + self.shadow_ring(base, hw::ATTR_COLOUR as u32, 0, colour[0], &colour[1..1 + ar(colour[0])]); + self.shadow_ring(base, hw::ATTR_NORMAL as u32, 0, normal[0], &normal[1..1 + ar(normal[0])]); + self.shadow_ring(base, hw::ATTR_EDGE as u32, 0, edge, &[]); + if self.tex.coord_q != ODSY_F32_ONE { + let coordinate = [self.tex.coord[0], self.tex.coord[1], self.tex.coord_r, self.tex.coord_q]; + + self.shadow_ring(base, hw::ATTR_TEXTURE as u32, 0, gl_cmd::TEXCOORD_4F, &coordinate); + } else { + let three = (self.tex.mode & tex::MODE_3D) != 0 || self.tex.coord_r != 0; + let three_ops = [self.tex.coord[0], self.tex.coord[1], self.tex.coord_r]; + let two_ops = self.tex.coord; + + self.shadow_ring( + base, + hw::ATTR_TEXTURE as u32, + 0, + if three { gl_cmd::TEXCOORD_3F } else { gl_cmd::TEXCOORD_2F }, + if three { &three_ops[..] } else { &two_ops[..] }, + ); + } + for i in 0..4usize { + let m: [u32; 4] = self.geom.material[i * 4..i * 4 + 4].try_into().unwrap(); + + self.shadow_ring(base, (hw::ATTR_MATERIAL + i) as u32, 0, MATERIAL[i], &m); + } + let sh = [self.geom.shininess_sqrt]; + self.shadow_ring(base, hw::ATTR_SHININESS as u32, 0, gl_cmd::SHININESS_SQRT, &sh); + for i in 0..4usize { + let m: [u32; 4] = self.geom.back_material[i * 4..i * 4 + 4].try_into().unwrap(); + + self.shadow_ring(base, (hw::ATTR_BACK_MATERIAL + i) as u32, 0, BACK_MATERIAL[i], &m); + } + let bsh = [self.geom.back_shininess_sqrt]; + self.shadow_ring(base, hw::ATTR_BACK_SHININESS as u32, 0, gl_cmd::BACK_SHININESS_SQRT, &bsh); + if open { + let mut current = [[0u32; 5]; 19]; + let mut state = self.stream.primitive + | (self.prim.open_held << 4) + | (self.prim.open_r << 6) + | (self.prim.open_m3 << 12) + | (self.prim.open_m4 << 14) + | (self.prim.open_c << 21); + + // After c >= 2 vertices the next triangle is number c - 2. + if self.stream.primitive == gl_cmd::TRIANGLE_STRIP && self.prim.open_c >= 2 && (self.prim.open_m4 & 1) != 0 { + state |= hw::PIPE_STRIP_ODD; + } + + self.open_current_records(&mut current); + self.shadow_put(base, hw::PIPE_STATE, state); + for i in 0..self.prim.open_held.min(3) { + let v = self.prim.open_vertex[i as usize]; + let slot = v.slot; + + self.shadow_ring(base, 0, slot, v.word, &v.operands[..ar(v.word)]); + self.shadow_descriptor(base, 0x8c + 8 * slot, &v.attribute, i); + for a in 1..19usize { + if v.attribute[a][0] == 0 { + continue; + } + self.shadow_ring( + base, + a as u32, + i, + v.attribute[a][0], + &v.attribute[a][1..1 + ar(v.attribute[a][0])], + ); + } + } + self.shadow_descriptor(base, hw::CURRENT_ATTRIBUTES, ¤t, 3); + for i in 0..19usize { + if current[i][0] == 0 { + continue; + } + self.shadow_ring(base, i as u32, 3, current[i][0], ¤t[i][1..1 + ar(current[i][0])]); + } + } + } + + /// The kernel driver's context switch: a single 8-byte store to + /// ctx::SWITCH_PORT, not a command-FIFO word. The new context id in bits + /// 31:16 is not modelled: the board learns the context from the shadow address + /// pair, which names where the outgoing state is saved (Board::ctx_save()), + /// and the kernel restores the rest. A store naming context 0x7f is the + /// kernel driver's shadow snapshot and saves the same way. Any other shape at + /// this port has no established meaning and is reported unimplemented instead + /// of guessed. + pub fn ctx_switch(&mut self, value: u64) { + if ((value >> ctx::SWITCH_OPCODE_SHIFT) & 0xff) != ctx::SWITCH_OPCODE as u64 + || (value & ctx::SWITCH_VALID) == 0 + { + self.unimplemented(ctx::SWITCH_PORT, 8, true); + return; + } + // The done word says the board has finished the work before the + // switch, the raster unit's included. + self.raster_wait(); + if self.widget.ctx_shadow_addr_upper == 0 && self.widget.ctx_shadow_addr_lower == 0 { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: context switch ({:#x}) with no shadow address programmed; no completion written", + value + ); + return; + } + self.query_cancel(); + let shadow_base = (((self.widget.ctx_shadow_addr_upper & 0xffff) as u64) << 32) | self.widget.ctx_shadow_addr_lower as u64; + // A plain guest-physical address: interrupt packets go elsewhere. + self.ctx_save(shadow_base); + self.shadow_put(shadow_base, ctx::SWITCH_DONE_OFFSET as u32, 1); + } +} diff --git a/src/dev/odyssey/depth_word.rs b/src/dev/odyssey/depth_word.rs new file mode 100644 index 000000000..90957f352 --- /dev/null +++ b/src/dev/odyssey/depth_word.rs @@ -0,0 +1,382 @@ +//! The board's depth word: a 24-bit unsigned float. +//! +//! Bits 23:18 are the exponent e, bits 17:0 the mantissa m with an implied +//! leading one and no sign, so the value is (1 + m / 2^18) * 2^(e - 31), +//! from 2^-31 to just under 2^33. The stored value is 2S * (d - n) / (f - n) +//! for eye distance d and frustum distances n and f, with 2S = 2^33 - 2^15: +//! linear in eye distance, 0 at the near plane and 2S at the far plane. +//! Words order as unsigned integers do, so a depth compare is an integer +//! compare. Bits 31:24 of a 32-bit word are not depth. +//! +//! What is established is the library's reading of the plane after a DMA +//! read: the encoding (`encode_f64`), the 16-bit form and the polygon-offset +//! field. That the plane holds exactly these bits is INFERRED. The rounding +//! of a rasterised depth into the word is unknown; this model truncates, as +//! the library's encoder does. + +use super::consts::ODSY_DEPTH24_MAX; + +/// The depth range's far value, 2S = 2^33 - 2^15. +pub(super) const DEPTH_2S: f64 = 8589901824.0; + +/// What the library's encoder returns for an exponent of 160 or more. Wider than +/// the word: the plane keeps the low 24 bits. +pub(super) const DEPTH_HOST_SATURATED: u32 = 0x0fffffff; + +/// A value below 2^-31 has no exponent; the encoder maps it to 0. +const DEPTH_MIN_VALUE: f64 = 1.0 / 2147483648.0; +const DEPTH_EXP_BIAS: i32 = 31; +const DEPTH_MANTISSA_BITS: u32 = 18; +const DEPTH_MANTISSA_MASK: u32 = (1 << DEPTH_MANTISSA_BITS) - 1; + +/// The library's encoder (ESTABLISHED), for a double: the exponent rebased +/// so that 2^-31 is 0, and the top 18 mantissa bits, the rest truncated. A +/// value below 2^-31 gives 0, one of 2^33 or more gives 0x0fffffff, and the +/// sign is ignored. +pub(super) fn encode_f64(v: f64) -> u32 { + let v = v.abs(); + + // NaN fails both tests and is unordered; treat it as below range. + if !(v >= DEPTH_MIN_VALUE) { + return 0; + } + let bits = v.to_bits(); + let exp = ((bits >> 52) & 0x7ff) as i32 - 1023 + DEPTH_EXP_BIAS; + + if exp >= 64 { + return DEPTH_HOST_SATURATED; + } + ((exp as u32) << DEPTH_MANTISSA_BITS) | ((bits >> (52 - DEPTH_MANTISSA_BITS)) as u32 & DEPTH_MANTISSA_MASK) +} + +/// The word for an eye-linear depth fraction z in [0, 1] (0 at the near +/// plane, 1 at the far plane): the encoding of 2S * z. A fraction outside +/// [0, 1] clamps to it. +pub(super) fn word_for_fraction(z: f64) -> u32 { + let z = if z > 1.0 { 1.0 } else if z > 0.0 { z } else { 0.0 }; + + encode_f64(z * DEPTH_2S) & ODSY_DEPTH24_MAX +} + +/// The 16-bit form (ESTABLISHED): the exponent in bits 15:10 and the top ten +/// mantissa bits in 9:0, which is the 24-bit word shifted right by eight. The +/// library reads it from windows whose visual is RGBA 12/12/12/12 and the +/// 24-bit form from every other visual with depth. Why those visuals get the +/// shorter word is not shown; the model has no depth plane of that width, so +/// a 16-bit depth read converts the 24-bit word (`ReadPlane::Depth16`). +pub(super) fn word16_from_word24(word: u32) -> u32 { + (word & ODSY_DEPTH24_MAX) >> 8 +} + +/// The polygon-offset field (block 0x058) is a signed 25-bit count of steps +/// of the encoded 24-bit word, the limits the library clamps to. +pub(super) const OFFSET_STEPS_MIN: i64 = -(1 << 24); +pub(super) const OFFSET_STEPS_MAX: i64 = (1 << 24) - 1; + +/// The word after a polygon offset. INFERRED placement: the board adds the field to the encoded word, so the offset +/// counts mantissa steps and shrinks toward the near plane in proportion to +/// d - n. The sum clamps to 0 and the largest word, a model choice: what the +/// board does at either end is not shown. The field alone is the step count; +/// the library has already folded factor and units into it ("Polygon offset"). +pub(super) fn word_with_offset(word: u32, steps: i64) -> u32 { + (word as i64 + steps).clamp(0, ODSY_DEPTH24_MAX as i64) as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::odyssey::raster_bench; + use crate::dev::odyssey::consts::polygon; + use crate::dev::odyssey::gl_context::{ + vtx, GL_COLOUR_MASK_ALL, GL_FLAT, GL_LEQUAL, GL_LESS, GL_OFFSET_FILL, GL_TRIANGLES, GlDraw, + GlDrawState, + }; + use crate::dev::odyssey::memory::ClipRect; + use crate::dev::odyssey::raster::{LineRule, NoTexels, RasterRules}; + + + + + const WIDTH: usize = 32; + + /// The value a word stands for: (1 + m / 2^18) * 2^(e - 31). + fn word_value(word: u32) -> f64 { + let e = (word >> 18) & 0x3f; + let m = word & 0x3ffff; + + (1.0 + m as f64 / 262144.0) * 2f64.powi(e as i32 - 31) + } + + #[test] + fn the_encoder_truncates_to_18_mantissa_bits() { + assert_eq!(encode_f64(1.0), 31 << 18); + assert_eq!(encode_f64(1.5), (31 << 18) | (1 << 17)); + // Truncation, not rounding: just below 1 + 2^-18 is still the word for 1. + assert_eq!(encode_f64(1.0 + 2f64.powi(-18) * 0.999), 31 << 18); + assert_eq!(encode_f64(1.0 + 2f64.powi(-18)), (31 << 18) | 1); + // Every word below the saturation point survives a round trip. + for w in (0..=0xffffffu32).step_by(4099) { + assert_eq!(encode_f64(word_value(w)), w, "word {w:#x}"); + } + } + + #[test] + fn a_value_below_2_to_the_minus_31_encodes_to_zero_and_the_sign_is_ignored() { + assert_eq!(encode_f64(0.0), 0); + assert_eq!(encode_f64(2f64.powi(-31) * 0.999), 0, "just under 2^-31"); + assert_eq!(encode_f64(2f64.powi(-31)), 0, "2^-31 is word 0 too"); + assert_eq!(encode_f64(2f64.powi(-31) * (1.0 + 2f64.powi(-18))), 1, "and its first mantissa step is word 1"); + assert_eq!(encode_f64(f64::NAN), 0); + assert_eq!(encode_f64(-1.0), encode_f64(1.0)); + assert_eq!(encode_f64(-1.0e-12), 0); + } + + #[test] + fn a_value_of_2_to_the_33_or_more_saturates_at_0x0fffffff() { + assert_eq!(encode_f64(8589934592.0), DEPTH_HOST_SATURATED); + assert_eq!(encode_f64(1.0e20), DEPTH_HOST_SATURATED); + assert_eq!(encode_f64(f64::INFINITY), DEPTH_HOST_SATURATED); + assert_eq!(encode_f64(-8589934592.0), DEPTH_HOST_SATURATED); + // The largest value below 2^33 is a real word, not the saturated one. + assert_eq!(encode_f64(8589934591.0), 0xffffff); + // The plane keeps the low 24 bits of a saturated word. + assert_eq!(DEPTH_HOST_SATURATED & ODSY_DEPTH24_MAX, 0xffffff); + } + + #[test] + fn the_near_plane_is_word_zero_and_the_far_plane_is_2s() { + assert_eq!(word_for_fraction(0.0), 0); + // 2S = 2^33 - 2^15: exponent 32 (e = 63), mantissa 2^18 - 2. + assert_eq!(word_for_fraction(1.0), 0xfffffe); + assert!((word_value(word_for_fraction(1.0)) - DEPTH_2S).abs() < DEPTH_2S * 2f64.powi(-17)); + // Outside the frustum clamps to it. + assert_eq!(word_for_fraction(-0.5), 0); + assert_eq!(word_for_fraction(2.0), 0xfffffe); + // Halfway is linear in eye distance, so the value is S, not a log scale. + assert!((word_value(word_for_fraction(0.5)) - DEPTH_2S / 2.0).abs() < DEPTH_2S * 2f64.powi(-18)); + } + + #[test] + fn words_order_as_integers_do_so_depth_compares_without_decoding() { + let mut last = 0; + + for i in 0..=2000 { + let w = word_for_fraction(i as f64 / 2000.0); + + assert!(w >= last, "fraction {i}/2000 gave {w:#x} below {last:#x}"); + last = w; + } + // Two depths one mantissa step apart in the top binade (relative 2^-18) + // differ by one word; the same step near the bottom is the same word + // step, where a fixed-point depth would see no difference. + let hi = word_for_fraction(0.75); + let lo = word_for_fraction(1.0e-6); + assert!(word_for_fraction(0.75 * (1.0 + 2f64.powi(-16))) > hi); + assert!(word_for_fraction(1.0e-6 * (1.0 + 2f64.powi(-16))) > lo); + } + + #[test] + fn the_16_bit_form_is_the_top_16_bits_of_the_word() { + for w in [0u32, 31 << 18, (31 << 18) | 0x3ffff, 0xfffffe, 0x123456] { + let w16 = word16_from_word24(w); + + assert_eq!(w16, w >> 8); + assert_eq!(w16 >> 10, w >> 18, "exponent in bits 15:10"); + assert_eq!(w16 & 0x3ff, (w >> 8) & 0x3ff, "top ten mantissa bits in 9:0"); + } + } + + #[test] + fn an_offset_adds_to_the_encoded_word_and_clamps_to_the_words_range() { + assert_eq!(word_with_offset(1000, 24), 1024); + assert_eq!(word_with_offset(1000, -1000), 0); + assert_eq!(word_with_offset(1000, -(1 << 24)), 0); + assert_eq!(word_with_offset(0xfffffe, (1 << 24) - 1), 0xffffff); + assert_eq!(OFFSET_STEPS_MIN, -16777216); + assert_eq!(OFFSET_STEPS_MAX, 16777215); + } + + // The offset the guest library computes for glPolygonOffset(factor, units) + // under a frustum: factor * tan(L) * (eye + // width of a pixel at f) * 2^19 / (f - n) + 2 * units, in steps of the + // word. It is the guest's arithmetic, not the board's; the register + // receives the result. + fn library_offset_steps(factor: f64, units: f64, limit_deg: f64, pixel_at_far: f64, n: f64, f: f64) -> i64 { + let v = factor * limit_deg.to_radians().tan() * pixel_at_far * 524288.0 / (f - n) + 2.0 * units; + + (v as i64).clamp(OFFSET_STEPS_MIN, OFFSET_STEPS_MAX) + } + + #[test] + fn the_library_offset_for_the_default_limit_angle_counts_word_steps_of_the_far_plane() { + // tan(70 deg) = 2.747: factor 1, a pixel the width of (f - n) / 2^19 + // is 2.747 steps. + let steps = library_offset_steps(1.0, 0.0, 70.0, 1.0 / 524288.0, 0.0, 1.0); + + assert_eq!(steps, 2); + assert_eq!(library_offset_steps(0.0, 3.0, 70.0, 1.0, 1.0, 100.0), 6, "units count double"); + assert_eq!(library_offset_steps(1.0e9, 0.0, 70.0, 1.0, 1.0, 2.0), OFFSET_STEPS_MAX, "limited to 24 bits"); + assert_eq!(library_offset_steps(0.0, -1.0e9, 70.0, 1.0, 1.0, 2.0), OFFSET_STEPS_MIN); + } + + /// Draw one triangle covering the plane at window depth `zw` with the + /// board's depth rules, depth test `func`, and `steps` of polygon offset + /// through the register's path (the shared context's bias). + fn draw_flat(depth: &mut [u32], zw: f64, func: u32, steps: i64) { + draw_sloped(depth, zw, 0.0, func, steps, (0.0, 0.0)); + } + + /// [`draw_flat`] with the third vertex `tilt` further in window depth, so + /// the triangle has a depth slope, and the context's glPolygonOffset + /// factor and units set to `(factor, units)`. + fn draw_sloped(depth: &mut [u32], zw: f64, tilt: f64, func: u32, steps: i64, (factor, units): (f32, f32)) { + let rules = RasterRules { + subpixel_bits: 4, + color_bits: 8, + depth_bits: 24, + line_rule: LineRule::Diamond, + depth_float: true, + ..Default::default() + }; + assert!(rules.rules_supported()); + let mut id = [0.0f32; 16]; + for k in 0..4 { + id[k * 5] = 1.0; + } + let mut st = GlDrawState { + modelview: id, + projection: id, + viewport: [0, 0, WIDTH as i32, WIDTH as i32], + shade_model: GL_FLAT, + depth_test: true, + depth_func: func, + depth_mask: true, + depth_range: [0.0, 1.0], + draw_buffers: 1, + colour_mask: GL_COLOUR_MASK_ALL, + polygon_offset: GL_OFFSET_FILL, + // What the 0x058 write puts in the shared context. + offset_bias: steps as f32 * polygon::OFFSET_SCALE, + offset_factor: factor, + offset_units: units, + ..Default::default() + }; + st.drawable.bound = true; + st.drawable.front = 1; + st.drawable.clip.n = 1; + st.drawable.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: WIDTH as i32 - 1, y1: WIDTH as i32 - 1 }; + let mut verts = vec![0.0f32; 3 * vtx::FLOATS]; + let ndc_z = (zw * 2.0 - 1.0) as f32; + + for (i, (x, y)) in [(-1.0f32, -1.0f32), (3.0, -1.0), (-1.0, 3.0)].iter().enumerate() { + let v = &mut verts[i * vtx::FLOATS..(i + 1) * vtx::FLOATS]; + let z = if i == 2 { ndc_z + (tilt * 2.0) as f32 } else { ndc_z }; + v[vtx::POSITION..vtx::POSITION + 4].copy_from_slice(&[*x, *y, z, 1.0]); + v[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&[1.0; 4]); + } + let stipple = [0u32; 32]; + let mut d = GlDraw::new(GL_TRIANGLES, 3, &verts, &st, &stipple); + let mut b = raster_bench::Bench::empty(); + + b.rules = rules; + b.set_depth(WIDTH as i32, depth); + let t = b.board.gl_target(); + b.draw(&t, &NoTexels, &mut d); + depth.copy_from_slice(&b.depth(WIDTH as i32)); + } + + fn plane(initial: u32) -> Vec { + vec![initial; WIDTH * WIDTH] + } + + #[test] + fn a_rasterised_depth_is_stored_as_the_board_word() { + let mut fb = plane(ODSY_DEPTH24_MAX); + + draw_flat(&mut fb, 0.25, GL_LESS, 0); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.25)), "{:#x}", fb[0]); + assert!((word_value(fb[0]) - DEPTH_2S * 0.25).abs() < DEPTH_2S * 2f64.powi(-20)); + } + + #[test] + fn depth_compares_in_the_board_format_pass_nearer_and_fail_farther() { + let mut fb = plane(word_for_fraction(0.5)); + + draw_flat(&mut fb, 0.75, GL_LESS, 0); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.5)), "a farther polygon fails GL_LESS"); + draw_flat(&mut fb, 0.5, GL_LESS, 0); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.5)), "an equal one fails GL_LESS"); + draw_flat(&mut fb, 0.5, GL_LEQUAL, 0); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.5))); + draw_flat(&mut fb, 0.25, GL_LESS, 0); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.25)), "a nearer one passes"); + // One mantissa step nearer in the top binade still passes: the + // format resolves 2^-18 relative at every depth. + let near = word_for_fraction(0.25) - 1; + let mut fb = plane(word_for_fraction(0.25)); + draw_flat(&mut fb, word_value(near) / DEPTH_2S, GL_LESS, 0); + assert!(fb.iter().all(|&w| w == near), "{:#x} against {near:#x}", fb[0]); + } + + #[test] + fn a_polygon_offset_adds_the_same_steps_to_the_word_at_any_depth() { + // INFERRED placement: the field adds to the encoded word. Added to + // window depth instead, 100 steps would be a different count of words + // at 0.25 and at 0.75, because a word step is relative. + for zw in [0.25, 0.75] { + let mut fb = plane(ODSY_DEPTH24_MAX); + + draw_flat(&mut fb, zw, GL_LESS, 100); + assert!(fb.iter().all(|&w| w == word_for_fraction(zw) + 100), "zw {zw}: {:#x}", fb[0]); + } + let mut fb = plane(ODSY_DEPTH24_MAX); + draw_flat(&mut fb, 0.25, GL_LESS, -100); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.25) - 100), "a negative offset pulls nearer"); + } + + #[test] + fn the_offset_field_is_the_whole_offset_whatever_the_slope_factor_and_units() { + // The library folds factor and 2 units into the one field, and the board + // has no slope term, so a context carrying a factor and units, on a + // triangle with a depth slope, offsets exactly as the field says. + let mut plain = plane(ODSY_DEPTH24_MAX); + let mut loaded = plane(ODSY_DEPTH24_MAX); + + draw_sloped(&mut plain, 0.25, 0.5, GL_LESS, 40, (0.0, 0.0)); + draw_sloped(&mut loaded, 0.25, 0.5, GL_LESS, 40, (1.5, 2000.0)); + assert_ne!(plain[0], ODSY_DEPTH24_MAX, "the triangle drew"); + assert!(plain.iter().zip(&loaded).all(|(a, b)| a == b), "factor and units changed the stored depth"); + // Same word delta from the unoffset draw at two depths. + for zw in [0.125, 0.5] { + let mut off = plane(ODSY_DEPTH24_MAX); + let mut none = plane(ODSY_DEPTH24_MAX); + + draw_sloped(&mut off, zw, 0.25, GL_LESS, 40, (1.5, 2000.0)); + draw_sloped(&mut none, zw, 0.25, GL_LESS, 0, (0.0, 0.0)); + assert!(off.iter().zip(&none).all(|(a, b)| *b == ODSY_DEPTH24_MAX || *a == *b + 40), "zw {zw}"); + } + } + + #[test] + fn a_decal_offset_towards_the_viewer_wins_the_depth_test_over_coplanar_geometry() { + let mut fb = plane(ODSY_DEPTH24_MAX); + + draw_flat(&mut fb, 0.5, GL_LESS, 0); + draw_flat(&mut fb, 0.5, GL_LESS, -4); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.5) - 4)); + // And a positive one loses. + draw_flat(&mut fb, 0.5, GL_LESS, 4); + assert!(fb.iter().all(|&w| w == word_for_fraction(0.5) - 4)); + } + + #[test] + fn an_offset_that_would_pass_the_ends_of_the_word_range_clamps() { + let mut fb = plane(ODSY_DEPTH24_MAX); + + draw_flat(&mut fb, 0.0, GL_LESS, -(1 << 24)); + assert!(fb.iter().all(|&w| w == 0)); + let mut fb = plane(ODSY_DEPTH24_MAX); + draw_flat(&mut fb, 1.0, GL_LEQUAL, (1 << 24) - 1); + assert!(fb.iter().all(|&w| w == 0xffffff)); + } +} diff --git a/src/dev/odyssey/display.rs b/src/dev/odyssey/display.rs new file mode 100644 index 000000000..b99d45903 --- /dev/null +++ b/src/dev/odyssey/display.rs @@ -0,0 +1,599 @@ +//! What the board shows: the scanout's colour lookup and gamma, the retrace +//! with its swap latching, and the window iris presents the frame in. + + +use super::board::{Board, ODSY_WIDTH}; +use super::consts::{ + dispmode, lut, overlay, ODSY_CMAP_UNVERIFIED_GROUP, ODSY_DID_MAIN_SHIFT, ODSY_DID_OVERLAY_MASK, + ODSY_GAMMA_GROUP, ODSY_INDEX_MASK, +}; +use super::fifo::display_fifo_pending; +use super::reader::{DisplayRegs, DisplayShared, PlaneView, RowOf, ScanoutReader}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; +use std::sync::{Arc, OnceLock}; + +use parking_lot::Mutex; + +use crate::cpu::mips_core::CyclesPtr; +#[cfg(test)] +use super::memory::plane_bpp; +#[cfg(test)] +use super::mmio::BoardLocks; +use crate::dev::ng1::rex3::Renderer; +use crate::disp::{Rex3Screen, FB_STRIDE}; + +/// Idle frames between forced presents. +const IDLE_REFRESH: u32 = 6; + +/// The window the board shows the guest: what `GfxDisplay` hands out. +pub(super) struct Display { + pub renderer: Mutex>>, + pub screen: Mutex, + pub screenshot: AtomicBool, + pub frame: AtomicBool, + /// Whether the display thread releases the renderer when it ends. + pub(super) display_stop: crate::gfx_display::DisplayStop, + local: Mutex>, + /// What the display keeps of the scanout: the presented frame, the + /// retrace clock and the update in progress. Held for each frame. + pub reader: Mutex, + /// Frames since the last present. + idle: AtomicU32, + /// The frame size last copied to the screen; (0, 0) before the first. + shown: Mutex<(usize, usize)>, + /// The machine's status-bar inputs. + status: Arc, + /// The registers the display reads, for the host's cursor query; set + /// when the board hands the display out. + shared: OnceLock>, +} + +struct Local { + overlay: crate::debug_overlay::DebugOverlay, + status: crate::disp::StatusBar, + texture: crate::disp::StatusBarTexture, + size: (usize, usize), +} + +impl Display { + pub(super) fn new(status: Arc) -> Self { + Self { + renderer: Mutex::new(None), + screen: Mutex::new(Rex3Screen::new()), + screenshot: AtomicBool::new(false), + frame: AtomicBool::new(false), + display_stop: Default::default(), + local: Mutex::new(None), + reader: Mutex::new(ScanoutReader::default()), + idle: AtomicU32::new(0), + shown: Mutex::new((0, 0)), + status, + shared: OnceLock::new(), + } + } + + /// Read the cursor from `shared` from now on. + pub(super) fn attach_registers(&self, shared: Arc) { + let _ = self.shared.set(shared); + } + + pub(super) fn shown(&self) -> (usize, usize) { + *self.shown.lock() + } + + /// Whether this frame is presented: when a line changed, when a + /// screenshot or frame is wanted, or on every sixth idle frame, as IMPACT + /// does, so a window or VNC client that missed a frame catches up. + pub(super) fn due(&self, changed: bool) -> bool { + let wanted = self.screenshot.load(Ordering::Acquire) || self.frame.load(Ordering::Acquire); + let idle = self.idle.fetch_add(1, Ordering::Relaxed) + 1; + if changed || wanted || idle >= IDLE_REFRESH { + self.idle.store(0, Ordering::Relaxed); + return true; + } + false + } + + /// Convert `rows` (0x00RRGGBB lines of `width` pixels, the first being + /// line `first` of a frame `height` lines tall) into the screen and + /// present it. No rows: present the screen as it is. + pub(super) fn present(&self, rows: &[u32], width: usize, height: usize, first: usize) { + self.present_lines(rows.chunks_exact(width.max(1)).enumerate().map(|(i, row)| (first + i, row)), width, height); + } + + /// As present(), for lines given with their numbers. + pub(super) fn present_lines<'a>(&self, lines: impl Iterator, width: usize, height: usize) { + let mut screen = self.screen.lock(); + let w = width.min(FB_STRIDE); + let h = height.min(screen.rgba.len() / FB_STRIDE); + screen.width = w; + screen.height = h; + screen.prebuilt = true; + screen.status_bar_only = false; + let planes = &mut *screen; + for (y, src) in lines { + if y >= h { + continue; + } + // 0x00RRGGBB to opaque 0xAABBGGRR: a byte swap with alpha set, + // written to both copies in one pass. + let at = y * FB_STRIDE; + let line = &mut planes.fb_rgb[at..at + w]; + let rgba = &mut planes.rgba[at..at + w]; + for ((d, e), &rgb) in line.iter_mut().zip(rgba.iter_mut()).zip(&src[..w]) { + let v = 0xff00_0000 | rgb.swap_bytes() >> 8; + *d = v; + *e = v; + } + } + *self.shown.lock() = (width, height); + screen.frame_seq = screen.frame_seq.wrapping_add(1); + 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.snapshot(std::time::Instant::now(), display_fifo_pending()); + renderer.present(&mut screen, &mut local.overlay, &mut local.status, &mut local.texture, &stats, false, None, None); + } + self.screenshot.store(false, Ordering::Release); + self.frame.store(false, Ordering::Release); + } + + /// Drop the frame state but leave the renderer as it is. + /// 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(super) fn stop(&self) { + self.display_stop.finish(&self.renderer); + self.local.lock().take(); + } +} + +impl Board { + /// Pixel (x, y)'s main drawable id, bits 7:3 of its id-plane byte. + pub fn main_id(&mut self, x: u32, y: u32) -> u32 { + ((self.planes.did[(y * ODSY_WIDTH + x) as usize] as u32) >> ODSY_DID_MAIN_SHIFT) & (dispmode::COUNT - 1) + } + + /// The colormap group main drawable main_id shows through: bits 8:4 of its + /// display mode. Below ODSY_GAMMA_GROUP the plane word is an index. + pub fn display_group(&mut self, main_id: u32) -> u32 { + self.scanout.shared.regs.display_group(main_id) + } + + /// A vertical retrace: the host interface latches the swaps armed and + /// raises the retrace source (HostInterface::retrace()). + #[cfg(test)] + pub fn retrace(&mut self) { + self.scanout.shared.retrace(); + } +} + +/// The scanout resolve as the tests and the benchmark run it on a board +/// they hold, without a display thread: the same reader the display uses. +#[cfg(test)] +impl Board { + fn test_reader(&mut self) -> &mut ScanoutReader { + self.reader.attach(&self.scanout.shared); + &mut self.reader + } + + /// Stage, resolve and commit the dirty lines into `reader.frame`. + pub fn scanout_update(&mut self) -> Option<(usize, usize)> { + self.test_reader().update() + } + + /// Take the raster unit's published rows into the test reader. + pub fn test_take_rows(&mut self, h: &BoardLocks, displayed: u32) { + let marks = self.scanout.marks.clone(); + let differ = &mut self.test_reader().differ; + h.raster.take_published_rows(displayed, differ, &marks); + } + + pub fn scanout_stage(&mut self) { + self.test_reader().stage(); + } + + pub fn scanout_resolve(&mut self) -> Option<(usize, usize)> { + self.test_reader().resolve() + } + + /// Resolve the colour buffers at scanout. Per pixel: the id plane's main + /// drawable (bits 7:3) selects the buffer shown, by its displayed-buffer bit, + /// and display mode 0x2900 + id, whose bits 8:4 are the group. Inferred: that + /// the displayed-buffer bits index windows by main drawable id. A group at or + /// above 26 is an RGB mode: each channel of the plane word goes through the + /// gamma table. Below it the plane word is an index: lut[256 * group + (index + /// & 0xff)], or lut[index & 0xfff] for group 0; no second gamma pass. Group 25 + /// resolves as an index table and is reported once as unverified. Over all of + /// that, a pixel whose overlay id's mode is enabled and whose overlay pixel is + /// not zero shows the overlay colormap entry instead. + /// + /// This is the per-pixel form, which any plane shape can use. resolve_row() + /// is the same function for the colour planes the board allocates, and the + /// tests hold the two to the same pixels. + pub fn resolve_row_generic(&mut self, y: u32, dst: &mut [u32]) { + let width = self.scanout.mode.width.min(ODSY_WIDTH); + let displayed = self.front.lock().displayed_buffer; + let shared = self.scanout.shared.clone(); + + self.raster_wait(); + let regs = shared.regs.read(); + if shared.planes.get().is_some_and(|view| view.resolve_generic(®s, displayed, y, width, dst)) { + shared.report_group25(); + } + } + + pub(super) fn resolve_ctx(&mut self) -> ResolveCtx { + let displayed = self.front.lock().displayed_buffer; + + ResolveCtx::new(&self.scanout.shared.regs.read(), displayed) + } + + /// The bytes of the colour buffer b's line y, `width` pixels of four + /// bytes, and whether they are stored big-endian. None unless the line is + /// whole in a four-byte plane. + fn colour_line(&self, b: u32, y: u32, width: usize) -> Option<(&[u8], bool)> { + let p = self.buffer_plane(b)?; + let at = usize::try_from(p.offset(0, y as i32, 4)).ok()?; + + if plane_bpp(p.format) != 4 || !p.contains(width as i32 - 1, y as i32) { + return None; + } + Some((self.buffer_mem(b).bytes.get(at..)?.get(..width * 4)?, !p.is_le())) + } + + /// resolve_row_generic() for the board's own colour planes, with the + /// per-pixel decisions that depend only on a pixel's drawable id made once + /// per update (ResolveCtx) and the planes read as lines of words. + fn resolve_row_with(&mut self, ctx: &ResolveCtx, y: u32, dst: &mut [u32]) { + let width = self.scanout.mode.width.min(ODSY_WIDTH); + let Some(line) = self.scanout_line(y, width as usize) else { + return self.resolve_row_generic(y, dst); + }; + let saw_unverified = resolve_line(ctx, line, dst); + let shared = self.scanout.shared.clone(); + + if saw_unverified { + shared.report_group25(); + } + shared.regs.read().cursor_row(y, dst, width); + } + + /// The plane bytes a row's resolve reads, when the row takes + /// resolve_line(): both colour buffers in one byte order, the id + /// bytes and the overlay bytes, each `width` pixels. + pub(super) fn scanout_line(&self, y: u32, w: usize) -> Option> { + let at = y as usize * ODSY_WIDTH as usize; + let (l0, be0) = self.colour_line(0, y, w)?; + let (l1, be1) = self.colour_line(1, y, w)?; + if be0 != be1 { + return None; + } + Some(ScanoutLine { colour: [l0, l1], be: be0, did: self.planes.did.get(at..at + w)?, overlay: self.planes.overlay.get(at..at + w)? }) + } + + /// Resolve the colour buffers at scanout; see resolve_row_generic(). + pub fn resolve_row(&mut self, y: u32, dst: &mut [u32]) { + let ctx = self.resolve_ctx(); + + self.resolve_row_with(&ctx, y, dst); + } +} + +/// The channel byte c of plane word 0xRRGGBBAA as RGB display mode `mode` +/// shows it: a 4- or 5-bit visual's channel keeps its top bits, repeated down +/// to fill the byte as a full-scale value. How the board itself stores a +/// narrow pixel is unknown. +pub(super) fn display_channel(mode: u32, c: u32) -> u32 { + match (mode >> dispmode::WIDTH_SHIFT) & dispmode::WIDTH_MASK { + dispmode::WIDTH_4 => (c & 0xf0) | (c >> 4), + dispmode::WIDTH_5 => (c & 0xf8) | (c >> 5), + _ => c, + } +} + +/// What one main drawable id shows, decided once per update instead of per +/// pixel: which colour buffer, and how its plane word becomes a colour. +#[derive(Clone, Copy, Default)] +struct IdShow { + /// The displayed colour buffer, 0 or 1. + buf: usize, + /// Whether the plane word is RGB through the gamma table. + rgb: bool, + /// An RGB mode's channel fold: (c & keep) | (c >> down). + keep: u32, + down: u32, + /// An index mode: lut entry = base + (word & mask). + base: u32, + mask: u32, + /// The mode is colour map group 25, reported once when shown. + unverified: bool, +} + +/// Everything a row's resolve reads outside the planes, gathered once from a +/// copy of the registers, so none of it can change meanwhile. +pub(super) struct ResolveCtx { + ids: [IdShow; dispmode::COUNT as usize], + /// Per overlay id: whether it is enabled, and its entry base. + overlay: [(bool, u32); overlay::IDS as usize], + /// Every lookup-RAM entry as 0x00RRGGBB (DisplayRegs::lut_rgb()). + rgb: Box<[u32; lut::SIZE as usize]>, + /// DisplayRegs::gamma() of each 8-bit channel value, placed in its byte of + /// the pixel: red, green, blue. + gamma: Box<[[u32; 256]; 3]>, +} + +impl ResolveCtx { + /// What the registers `regs` make of each drawable, overlay id, lookup + /// entry and gamma step, with colour buffer (displayed >> id) & 1 + /// showing for each drawable. + pub(super) fn new(regs: &DisplayRegs, displayed: u32) -> ResolveCtx { + let mut ids = [IdShow::default(); dispmode::COUNT as usize]; + let mut overlay = [(false, 0u32); overlay::IDS as usize]; + let mut rgb = Box::new([0u32; lut::SIZE as usize]); + let mut gamma = Box::new([[0u32; 256]; 3]); + + for (i, v) in rgb.iter_mut().enumerate() { + *v = regs.lut_rgb(i as u32); + } + for c in 0..256u32 { + gamma[0][c as usize] = regs.gamma(c, lut::RED_SHIFT) << 16; + gamma[1][c as usize] = regs.gamma(c, lut::GREEN_SHIFT) << 8; + gamma[2][c as usize] = regs.gamma(c, lut::BLUE_SHIFT); + } + for (id, show) in ids.iter_mut().enumerate() { + let mode = regs.dispmode[id]; + let group = regs.display_group(id as u32); + + show.buf = ((displayed >> id) & 1) as usize; + if group >= ODSY_GAMMA_GROUP { + show.rgb = true; + // display_channel() as a mask and a shift; a shift of 8 or + // more leaves a byte's high part empty. + (show.keep, show.down) = match (mode >> dispmode::WIDTH_SHIFT) & dispmode::WIDTH_MASK { + dispmode::WIDTH_4 => (0xf0, 4), + dispmode::WIDTH_5 => (0xf8, 5), + _ => (0xff, 8), + }; + } else if group != 0 { + show.base = 256 * group; + show.mask = 0xff; + show.unverified = group == ODSY_CMAP_UNVERIFIED_GROUP; + } else { + show.mask = ODSY_INDEX_MASK; + } + } + for (id, o) in overlay.iter_mut().enumerate() { + let mode = regs.overlay_mode[id]; + + *o = ( + mode & overlay::MODE_ENABLE != 0, + 256 * ((mode >> overlay::MODE_GROUP_SHIFT) & overlay::MODE_GROUP_MASK), + ); + } + ResolveCtx { ids, overlay, rgb, gamma } + } +} + +/// A run of RGB pixels through the gamma table: `src` is their plane words +/// (`BE`: big-endian), `FOLD`: the mode has narrow channels to fold first. +#[inline(always)] +fn gamma_run(ctx: &ResolveCtx, show: &IdShow, src: &[u8], out: &mut [u32]) { + let [red, green, blue] = &*ctx.gamma; + + for (out, c) in out.iter_mut().zip(src.chunks_exact(4)) { + let raw = u32::from_le_bytes([c[0], c[1], c[2], c[3]]); + let word = if BE { raw.swap_bytes() } else { raw }; + let (mut r, mut g, mut b) = (word >> 24, (word >> 16) & 0xff, (word >> 8) & 0xff); + + if FOLD { + r = (r & show.keep) | (r >> show.down); + g = (g & show.keep) | (g >> show.down); + b = (b & show.keep) | (b >> show.down); + } + *out = red[r as usize & 0xff] | green[g as usize & 0xff] | blue[b as usize & 0xff]; + } +} + +/// One row's plane bytes, as resolve_line() reads them. +pub(super) struct ScanoutLine<'a> { + colour: [&'a [u8]; 2], + /// The colour words are big-endian. + be: bool, + did: &'a [u8], + overlay: &'a [u8], +} + +/// resolve_row_generic() for the board's own colour planes, with the +/// per-pixel decisions that depend only on a pixel's drawable id made once +/// per update (ResolveCtx) and the planes read as lines of words. Whether a +/// pixel showed through colour map group 25. +pub(super) fn resolve_line(ctx: &ResolveCtx, line: ScanoutLine<'_>, dst: &mut [u32]) -> bool { + let ScanoutLine { colour: lines, be: be0, did, overlay: ovl } = line; + let w = did.len(); + let mut saw_unverified = false; + let mut x = 0; + + // A window is a run of pixels with one id byte, so the lookups that + // depend only on the id are made per run and the run is a plain loop. + while x < w { + let d = did[x]; + let end = x + 1 + did[x + 1..].iter().take_while(|&&v| v == d).count(); + let (enabled, obase) = ctx.overlay[(d as u32 & ODSY_DID_OVERLAY_MASK) as usize]; + let show = &ctx.ids[(d >> ODSY_DID_MAIN_SHIFT) as usize & (dispmode::COUNT as usize - 1)]; + let out = &mut dst[x..end]; + let src = &lines[show.buf][x * 4..end * 4]; + let ov = &ovl[x..end]; + + if show.rgb { + match (be0, show.keep != 0xff) { + (true, true) => gamma_run::(ctx, show, src, out), + (false, true) => gamma_run::(ctx, show, src, out), + (true, false) => gamma_run::(ctx, show, src, out), + (false, false) => gamma_run::(ctx, show, src, out), + } + } else { + for (out, c) in out.iter_mut().zip(src.chunks_exact(4)) { + let raw = u32::from_le_bytes([c[0], c[1], c[2], c[3]]); + let word = if be0 { raw.swap_bytes() } else { raw }; + *out = ctx.rgb[(show.base + (word & show.mask)) as usize & (lut::SIZE as usize - 1)]; + } + // Reported only if a pixel of the run is shown through it. + saw_unverified |= show.unverified && (!enabled || ov.contains(&0)); + } + if enabled { + for (out, &o) in out.iter_mut().zip(ov) { + if o != 0 { + *out = ctx.rgb[(obase + o as u32) as usize & (lut::SIZE as usize - 1)]; + } + } + } + x = end; + } + saw_unverified +} + +/// A frame's staged lines on their way to the presented frame +/// (ScanoutReader::take()). +pub(super) struct ScanoutJob { + pub(super) stage: ScanoutStage, + pub(super) ctx: Option, + /// The frame's width, and the pixels of it the planes cover. + pub(super) w: usize, + pub(super) width: u32, + /// The frame's size when the lines were staged. + pub(super) frame: (u32, u32), + /// The staged lines resolved, `w` pixels each. + pub(super) rows: Vec, + pub(super) saw_unverified: bool, + /// ScanoutReader::commit() put the rows in the frame. + pub(super) committed: bool, +} + +impl ScanoutJob { + /// The lines of a committed job, as (line, `w` pixels) in order; none + /// before ScanoutReader::commit() or if it dropped them. + pub fn lines(&self) -> impl Iterator { + let committed = self.committed; + self.stage.lines.iter().zip(self.rows.chunks_exact(self.w.max(1))).filter(move |_| committed).map(|(&(y, _), row)| (y as usize, row)) + } + + pub fn committed(&self) -> bool { + self.committed + } + + /// Resolve the staged lines. Needs nothing of the board: the display + /// runs it with no lock held, while the front end and the raster unit + /// go on. + pub fn resolve(&mut self) { + let (w, width) = (self.w, self.width as usize); + if w == 0 { + return; + } + // A row for every line, in order; a line not staged is filled in + // by ScanoutReader::commit(). + self.rows.clear(); + self.rows.resize(self.stage.lines.len() * w, 0); + let Some(ctx) = &self.ctx else { return }; + let mut at = 0; + for (&(_, staged), out) in self.stage.lines.iter().zip(self.rows.chunks_exact_mut(w)) { + if staged { + let (line, next) = self.stage.line(at, width); + at = next; + self.saw_unverified |= resolve_line(ctx, line, out); + } + } + } +} + +/// The dirty lines of a frame's update, with the plane bytes of those that +/// resolve_line() resolves copied aside (ScanoutReader::stage()). +#[derive(Default)] +pub(super) struct ScanoutStage { + /// Each dirty line, and whether its bytes are staged; a line that is + /// not was resolved at once. + pub(super) lines: Vec<(u32, bool)>, + /// Per staged line: colour buffer 0 and 1 words, id bytes, overlay + /// bytes. Kept between frames for its allocation. + pub(super) bytes: Vec, + /// The staged lines' colour words are big-endian. + pub(super) be: Vec, +} + +impl ScanoutStage { + /// Copy line y's plane bytes from the view, `w` pixels: both colour + /// buffers, the id bytes and the overlay bytes. False, with nothing + /// kept, unless the line is whole in four-byte planes of one byte order. + pub(super) fn push_row(&mut self, view: &PlaneView, y: u32, w: usize) -> bool { + let start = self.bytes.len(); + + self.bytes.resize(start + 10 * w, 0); + let (colour, rest) = self.bytes[start..].split_at_mut(8 * w); + let (c0, c1) = colour.split_at_mut(4 * w); + let (did, overlay) = rest.split_at_mut(w); + let be = [view.copy_row(RowOf::Colour(0), y, c0), view.copy_row(RowOf::Colour(1), y, c1)]; + + if let (Some(a), Some(b)) = (be[0], be[1]) { + if a == b && view.copy_row(RowOf::Did, y, did).is_some() && view.copy_row(RowOf::Overlay, y, overlay).is_some() { + self.be.push(a); + return true; + } + } + self.bytes.truncate(start); + false + } + + /// The staged line at byte `at`, `w` pixels wide, and where the next + /// one starts. + fn line(&self, at: usize, w: usize) -> (ScanoutLine<'_>, usize) { + let b = &self.bytes[at..at + 10 * w]; + let index = at / (10 * w); + let line = ScanoutLine { + colour: [&b[..4 * w], &b[4 * w..8 * w]], + be: self.be[index], + did: &b[8 * w..9 * w], + overlay: &b[9 * w..], + }; + (line, at + 10 * w) + } +} + +impl crate::gfx_display::GfxDisplay for Display { + fn display_stop(&self) -> &crate::gfx_display::DisplayStop { + &self.display_stop + } + fn renderer_slot(&self) -> &Mutex>> { + &self.renderer + } + fn screen(&self) -> &Mutex { + &self.screen + } + fn request_screenshot(&self) { + self.screenshot.store(true, Ordering::Release); + } + fn screenshot_pending(&self) -> bool { + self.screenshot.load(Ordering::Acquire) + } + fn cycles(&self) -> CyclesPtr { + self.status.cycles() + } + fn request_frame(&self) { + self.frame.store(true, Ordering::Release); + } + fn frame_pending(&self) -> bool { + self.frame.load(Ordering::Acquire) + } + fn hw_cursor(&self) -> Option { + // A racy read of the registers, as the display thread's frame does. + self.shared.get()?.regs.hw_cursor() + } +} diff --git a/src/dev/odyssey/display_bench.rs b/src/dev/odyssey/display_bench.rs new file mode 100644 index 000000000..63f04dd05 --- /dev/null +++ b/src/dev/odyssey/display_bench.rs @@ -0,0 +1,294 @@ +//! The scanout resolve: frames it must produce exactly, and a benchmark of +//! how long it takes. +//! +//! Each scene is a board loaded with deterministic planes, lookup RAM, +//! display modes, overlay and cursor, then resolved whole. The tests hold +//! the resolved frame to a stored hash, so a change to the resolve that moves +//! one pixel fails. The benchmark reports the resolve of a full 1280x1024 +//! screen, a whole display frame, a swap of a window that differs from its other buffer in part of +//! the screen, and the conversion of that frame into the window's buffers: +//! +//! cargo test --release --lib odyssey::display_bench -- --ignored --nocapture --test-threads=1 +//! +//! BENCH_SECS is the time each case takes (default one second). + +use super::{raster_bench, Odyssey}; +use super::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use super::consts::{ + cursor, dispmode, lut, overlay, ODSY_CMAP_UNVERIFIED_GROUP, ODSY_DID_MAIN_SHIFT, + ODSY_GAMMA_BASE, +}; +use super::display::{resolve_line, Display}; +use super::mmio::BoardLocks; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use raster_bench::Lcg; + +/// What a scene puts on the screen. +#[derive(Clone, Copy)] +enum Scene { + /// Every drawable an 8-bit RGB visual; no overlay, no cursor. + Rgb, + /// Drawables in every kind of mode on both colour buffers, an overlay + /// that covers some pixels, and the glyph cursor. + Mixed, + /// As Mixed, with the crosshair cursor in place of the glyph. + Crosshair, + /// Mixed, in a smaller mode than the planes. + Small, +} + +const SCENES: [(Scene, &str); 4] = + [(Scene::Rgb, "rgb"), (Scene::Mixed, "mixed"), (Scene::Crosshair, "crosshair"), (Scene::Small, "small")]; + +fn fill_random(bytes: &mut [u8], rng: &mut Lcg) { + for c in bytes.chunks_exact_mut(4) { + c.copy_from_slice(&rng.next_u32().to_be_bytes()); + } +} + +fn scene_board(scene: Scene) -> Box { + let mut s = Board::new("v12", 11).expect("board"); + let mut rng = Lcg(0x5eed_0001); + let pixels = (ODSY_WIDTH * ODSY_HEIGHT) as usize; + + fill_random(&mut s.planes.colour.bytes, &mut rng); + fill_random(&mut s.planes.buffer1.bytes, &mut rng); + for i in 0..lut::SIZE as usize { + s.regs().set_lut(i, rng.next_u32() & lut::ENTRY_MASK); + } + if let Scene::Rgb = scene { + // The identity-like ramp a guest loads, so the picture is not noise. + for i in 0..0x600u32 { + let v = (i * 0x3ff / 0x5ff) & 0x3ff; + s.regs().set_lut((ODSY_GAMMA_BASE + i) as usize, v << 20 | v << 10 | v); + } + for id in 0..dispmode::COUNT { + s.regs().set_dispmode(id as usize, 26 << dispmode::GROUP_SHIFT); + } + return s; + } + // Drawable modes: 8-bit RGB, 4-bit, 5-bit, index groups 17 and 0 and 25, + // another RGB group, repeating over the 32 ids. + for id in 0..dispmode::COUNT { + let mode = match id % 7 { + 0 => 26 << dispmode::GROUP_SHIFT, + 1 => 27 << dispmode::GROUP_SHIFT | dispmode::WIDTH_4 << dispmode::WIDTH_SHIFT, + 2 => 28 << dispmode::GROUP_SHIFT | dispmode::WIDTH_5 << dispmode::WIDTH_SHIFT, + 3 => 17 << dispmode::GROUP_SHIFT, + 4 => 0, + 5 => ODSY_CMAP_UNVERIFIED_GROUP << dispmode::GROUP_SHIFT, + _ => 31 << dispmode::GROUP_SHIFT | 1 << dispmode::WIDTH_SHIFT, + }; + s.regs().set_dispmode(id as usize, mode); + } + for id in 0..overlay::IDS { + // Ids 0, 3, 5 and 6 show the overlay, from different groups. + let mode = if matches!(id, 0 | 3 | 5 | 6) { + overlay::MODE_ENABLE | (id + 20) << overlay::MODE_GROUP_SHIFT + } else { + id << overlay::MODE_GROUP_SHIFT + }; + s.regs().set_overlay_mode(id as usize, mode); + } + // Windows: 64-pixel blocks of one drawable id, each with an overlay id; + // the overlay is set on about one pixel in five. + for y in 0..ODSY_HEIGHT as usize { + for x in 0..ODSY_WIDTH as usize { + let at = y * ODSY_WIDTH as usize + x; + let block = (x / 64 + 3 * (y / 48)) as u32; + + s.planes.did[at] = ((block % 32) << ODSY_DID_MAIN_SHIFT | (block / 3) % 8) as u8; + s.planes.overlay[at] = if rng.next_u32() % 5 == 0 { rng.next_u32() as u8 } else { 0 }; + } + } + assert_eq!(s.planes.did.len(), pixels); + s.front.lock().displayed_buffer = 0x6d5a_b3c9; + s.regs().set_cursor(cursor::POSITION as usize, 100 << 16 | 40); + for i in 0..cursor::COLOUR_COUNT { + s.regs().set_cursor((cursor::COLOURS + 1 + i) as usize, rng.next_u32() & lut::ENTRY_MASK); + } + for w in 0..64u32 { + s.regs().set_cursor((cursor::PLANE + w) as usize, rng.next_u32()); + } + for w in 0..128u32 { + s.regs().set_cursor((cursor::GLYPH + w) as usize, rng.next_u32()); + } + let control = if let Scene::Crosshair = scene { cursor::CTL_ENABLE | cursor::CTL_CROSSHAIR } else { cursor::CTL_ENABLE }; + s.regs().set_cursor(cursor::CONTROL as usize, control); + if let Scene::Small = scene { + s.set_scanout_size(1000, 700); + } + s +} + +/// An RGB screen whose two colour buffers differ in the `rows` rows from the +/// middle out: what a swap of a window that size changes. +fn swap_board(rows: usize) -> Box { + let mut s = scene_board(Scene::Rgb); + let stride = ODSY_WIDTH as usize * 4; + let mut rng = Lcg(0x5eed_0002); + let first = (ODSY_HEIGHT as usize - rows) / 2; + + let same = s.planes.colour.bytes.clone(); + s.planes.buffer1.bytes.copy_from_slice(&same); + fill_random(&mut s.planes.buffer1.bytes[first * stride..(first + rows) * stride], &mut rng); + s +} + +fn fnv(frame: &[u32]) -> u64 { + frame.iter().fold(0xcbf2_9ce4_8422_2325u64, |h, &p| (h ^ p as u64).wrapping_mul(0x100_0000_01b3)) +} + +/// The frame of a scene, resolved whole. +fn resolved(scene: Scene) -> Vec { + let mut s = scene_board(scene); + + s.scanout.invalidate_all(); + assert!(s.scanout_update().is_some()); + s.reader.frame.clone() +} + +/// Stored from the per-pixel resolve this change replaced. +const EXPECTED: [u64; 4] = [0x811e_c4e1_b153_56b7, 0x48e6_3c81_0278_8e71, 0x875a_cf9e_98b9_71c3, 0x3de4_d246_d889_9652]; + +#[test] +fn scenes_resolve_to_the_stored_frames() { + let got: Vec = SCENES.iter().map(|(scene, _)| fnv(&resolved(*scene))).collect(); + + assert_eq!(got, EXPECTED, "hashes {got:#018x?}"); +} + +/// The rows resolve_row() makes are the per-pixel resolve's, on every scene +/// and at a few lines of each, with the board left as the scene made it. +#[test] +fn the_row_resolve_matches_the_per_pixel_resolve() { + for (scene, name) in SCENES { + let mut s = scene_board(scene); + let w = s.scanout.mode.width as usize; + let mut fast = vec![0u32; w]; + let mut slow = vec![0u32; w]; + + for y in 0..s.scanout.mode.height { + s.resolve_row(y, &mut fast); + s.resolve_row_generic(y, &mut slow); + assert_eq!(fast, slow, "scene {name}, line {y}"); + } + } +} + +/// A line the planes do not cover whole is resolved per pixel, not skipped. +#[test] +fn a_mode_wider_than_the_planes_still_resolves() { + let mut s = scene_board(Scene::Mixed); + let mut fast = vec![0u32; 1280]; + let mut slow = vec![0u32; 1280]; + + s.planes.colour.bytes.truncate(1280 * 4 * 10); + s.resolve_row(9, &mut fast); + s.resolve_row_generic(9, &mut slow); + assert_eq!(fast, slow); + s.resolve_row(500, &mut fast); + s.resolve_row_generic(500, &mut slow); + assert_eq!(fast, slow, "a line past the end of the board memory"); +} + +/// The window's buffers hold each pixel as opaque 0xAABBGGRR, in both copies, +/// for the lines presented and nothing else. +#[test] +fn present_swaps_each_pixel_into_both_buffers() { + let display = Display::new(crate::disp::StatusSource::detached()); + let (w, h) = (ODSY_WIDTH as usize, ODSY_HEIGHT as usize); + let frame = resolved(Scene::Mixed); + let rows = &frame[100 * w..300 * w]; + + display.present(rows, w, h, 100); + let screen = display.screen.lock(); + assert_eq!((screen.width, screen.height), (w, h)); + for y in 0..h { + for x in 0..w { + let want = if (100..300).contains(&y) { + let rgb = frame[y * w + x]; + 0xff00_0000 | (rgb >> 16) & 0xff | rgb & 0xff00 | (rgb & 0xff) << 16 + } else { + 0 + }; + let at = y * crate::disp::FB_STRIDE + x; + assert_eq!((screen.fb_rgb[at], screen.rgba[at]), (want, want), "({x}, {y})"); + } + } +} + +/// The fastest of the runs of `f` in the time budget, in ms: a shared host's +/// load only ever slows a run. +fn bench(mut f: F) -> f64 { + let secs: f64 = std::env::var("BENCH_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(1.0); + let budget = Duration::from_secs_f64(secs); + let start = Instant::now(); + let mut best = f64::MAX; + + f(); + while start.elapsed() < budget { + let t = Instant::now(); + f(); + best = best.min(t.elapsed().as_secs_f64() * 1e3); + } + best +} + +#[test] +#[ignore = "benchmark"] +fn resolve_and_present_timings() { + for (scene, name) in SCENES { + let mut s = scene_board(scene); + let ms = bench(|| { + s.scanout.invalidate_all(); + s.scanout_update(); + }); + println!("resolve {name:10} {}x{}: {ms:.3} ms per full resolve (best run)", s.scanout.mode.width, s.scanout.mode.height); + } + for (scene, name) in SCENES { + let mut s = scene_board(scene); + let ctx = s.resolve_ctx(); + let (w, h) = (s.scanout.mode.width as usize, s.scanout.mode.height); + let width = s.scanout.mode.width.min(ODSY_WIDTH) as usize; + let mut dst = vec![0u32; w]; + let ms = bench(|| { + for y in 0..h { + if let Some(line) = s.scanout_line(y, width) { + std::hint::black_box(resolve_line(&ctx, line, &mut dst)); + } + } + }); + println!("lines {name:10} {w}x{h}: {ms:.3} ms to resolve the planes' lines alone"); + } + for (rows, name) in [(480, "a 480-row window"), (1024, "every row")] { + let mut s = swap_board(rows); + s.scanout_update(); + let ms = bench(|| { + s.front.lock().swap_arm = u32::MAX; + s.retrace(); + s.scanout_update(); + }); + println!("swap {name:18} {}x{}: {ms:.3} ms per swap and update", s.scanout.mode.width, s.scanout.mode.height); + } + for (rows, name) in [(480, "480 lines"), (1024, "every line")] { + let locks = BoardLocks::new(scene_board(Scene::Rgb)); + let display = Display::new(crate::disp::StatusSource::detached()); + let first = (ODSY_HEIGHT as i32 - rows) / 2; + + Odyssey::frame(&locks, &display); + let ms = bench(|| { + locks.test_board().scanout.invalidate(first, rows); + Odyssey::frame(&locks, &display); + }); + println!("frame {name:18} dirty, resolved and presented: {ms:.3} ms"); + } + let display = Display::new(crate::disp::StatusSource::detached()); + let frame = resolved(Scene::Mixed); + let (w, h) = (ODSY_WIDTH as usize, ODSY_HEIGHT as usize); + let ms = bench(|| display.present(&frame, w, h, 0)); + println!("present {w}x{h}: {ms:.3} ms per full frame"); +} diff --git a/src/dev/odyssey/dma.rs b/src/dev/odyssey/dma.rs new file mode 100644 index 000000000..f87571529 --- /dev/null +++ b/src/dev/odyssey/dma.rs @@ -0,0 +1,1767 @@ +//! Pixel DMA, queries, colour conversions and the texture level build. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + + +use super::bits::{be16, be32, be64}; +use crate::devlog::LogModule; +use std::sync::atomic::Ordering; +use super::board::{Board, ODSY_CMD_OPERANDS_MAX, ODSY_HEIGHT, ODSY_OBJECTS, ODSY_WIDTH}; +use super::consts::{ + clip, cmd_arity, dest, dma, gl_cmd, image, mode, query, route, tex, write, ODSY_BUFFERS, + ODSY_DEPTH24_MAX, ODSY_DRAW_WORD_STATE, ODSY_F32_ONE, ODSY_F32_SCALE_16, ODSY_F32_SCALE_32, + ODSY_F32_SCALE_8, ODSY_GAMMA_GROUP, +}; +use super::context::{ + ODSY_QUERY_COLOUR, ODSY_QUERY_EDGE, ODSY_QUERY_INDEX, ODSY_QUERY_NORMAL, ODSY_QUERY_TEXCOORD, +}; +use super::draw::{code_is_index, decode_texel12, pixel_bytes, texel12_components}; +use super::geometry::{f32_bits, odyssey_f32}; +use super::pixel_dma::{write_guest, DmaBus, DmaDirection, DmaError, DmaMap, DmaPageList}; +use super::pixel_write::{write_form, WriteForm}; +use super::sampler::{texture_level_texel_format, texture_mip_build_word, LEVEL_FORMAT_MASK}; +use super::scanout::DirtySite; +#[derive(Clone, Copy, Default)] +pub(super) struct DmaEntry { + pub addr: u64, + pub len: u32, +} + +/// One field of a packed pixel word: channel `chan` (0 red, 1 green, 2 blue, +/// 3 alpha) narrowed to `width` bits and placed at bit `shift`. Width 0 ends +/// the list. +#[derive(Clone, Copy, Default)] +pub(super) struct DmaField { + pub chan: u8, + pub width: u8, + pub shift: u8, +} + +#[derive(Clone, Copy, Default)] +pub(super) struct DmaLayout { + pub code: u8, + pub bytes: u8, + /// Host bytes, each a channel of the plane word or one of the + /// dma::BYTE_* sources. Unused when `fields` is not empty. + pub from: [u8; 8], + /// A packed big-endian word of `bytes` bytes, most significant field + /// listed by its shift. Empty for a layout of whole bytes. + pub fields: [DmaField; 4], + /// Which plane the read takes its pixels from. + pub plane: ReadPlane, +} + +/// The plane a read comes from and the form of its word. +#[derive(Clone, Copy, Default, PartialEq)] +pub(super) enum ReadPlane { + #[default] + Colour, + /// The depth word's 16-bit form: exponent and top ten mantissa bits. + Depth16, + /// The depth word as a 32-bit word, bits 31:24 zero. + Depth32, + /// The stencil byte. + Stencil, +} + +#[derive(Clone, Copy, Default)] +pub(super) struct DmaSource { + /// The board-memory object read, or None for the colour buffer. + pub obj: Option, + pub front: bool, + pub x: i64, + pub y: i64, +} + +#[derive(Clone, Default)] +pub(super) struct DmaPiece { + pub map: DmaMap, + pub at: u64, + pub src: u64, + pub len: u32, +} + +// Anonymous enum: command-interpreter states. +pub(super) const ODSY_CMD_IDLE: u32 = 0; +pub(super) const ODSY_CMD_OPERANDS: u32 = 1; +pub(super) const ODSY_CMD_COPY_RECT: u32 = 2; +pub(super) const ODSY_CMD_PATTERN_LOAD: u32 = 3; +pub(super) const ODSY_CMD_IMAGE_BOX_LOAD: u32 = 4; +pub(super) const ODSY_CMD_IMAGE_BURST: u32 = 5; +pub(super) const ODSY_CMD_MATRIX_LOAD: u32 = 6; +pub(super) const ODSY_CMD_GL_RECT: u32 = 7; +pub(super) const ODSY_CMD_SKIP: u32 = 8; + +/// The table's physical address half, unpacked from a descriptor word. +pub(super) fn dma_desc_half(word: u32) -> u64 { + ((word & dma::DESC_ADDR_LOW_MASK) as u64) + | ((((word >> dma::DESC_ADDR_HIGH_SHIFT) & dma::DESC_ADDR_HIGH_MASK) as u64) << 17) +} + +pub(super) fn dma_table_address(w1: u32, w3: u32) -> u64 { + (dma_desc_half(w1) << 24) | dma_desc_half(w3) +} + +impl Board { + /// The hard DMA-done interrupt: the routed source whose vector is byte 15:8 + /// of the route-vectors register. A DMA the driver asks to be told about + /// completes on two events, the fence and this interrupt; the model raises it + /// for exactly the descriptors whose header word 7 bit 31 asks for it, once + /// the transfer is finished. Nothing is delivered before the driver routes it. + pub fn dma_done(&mut self) { + let mut f = self.front.lock(); + crate::pf_instant!(crate::perfetto::events::ODSY_DMA_DONE); + if f.routed() { + f.deliver_routed(route::VECTOR_DMA_SHIFT, "DMA-done", true); + } + } +} + +/// Bytes per pixel, indexed by word 7's bits 18:16. +pub(super) static DMA_BPP: [u8; (dma::BPP_MASK + 1) as usize] = [2, 4, 8, 16, 12, 1, 6, 3]; + +impl DmaBus { + /// The entry's page pairs, up to a zero pair or the twelfth, into e[]. + /// Returns how many, or a bus error when a read fails. + pub fn entries(&self, table: u64, seq: u64, e: &mut [DmaEntry]) -> Result { + let mut n: usize = 0; + + while n < dma::MAX_PAGES { + let mut raw = [0u8; 8]; + + if !self.read(table + dma::TABLE_HEADER + (n as u64) * 8, &mut raw, true, seq) { + return Err(DmaError::Bus); + } + let pte = be64(&raw[..8]); + if pte & (dma::PTE_LEN_MASK as u64) == 0 { + return Ok(n); + } + e[n].addr = pte >> dma::PTE_ADDR_SHIFT; + e[n].len = (pte & (dma::PTE_LEN_MASK as u64)) as u32; + n += 1; + } + Ok(n) + } +} + +impl Board { + /// One pixel of a transfer at (x, y) of the colour buffers the drawing + /// mask selects, decoded by the header's source format as an image + /// transfer's pixel is. Clipped to the plane only. + pub fn dma_put(&mut self, x: i64, y: i64, src: &[u8], code: u32) { + let pixel = self.decode_pixel(code, src).unwrap_or(0); + if x < 0 || y < 0 || x >= ODSY_WIDTH as i64 || y >= ODSY_HEIGHT as i64 { + return; + } + for b in 0..ODSY_BUFFERS { + if self.draw_buffers() & (1u32 << b) != 0 { + let word = if code_is_index(code) { pixel } else { (pixel & !0xffu32) | 0xff }; + self.buffer_store(b, x, y, word); + } + } + } + + /// The source format of a DMA: word 7's low byte, the same code as an image + /// transfer header's. Another code keeps the earlier rule (4 bytes as + /// 0x00RRGGBB, 1 byte as a colour index), reported once. + pub fn dma_code(&mut self, hdr: &[u32], bpp: u32) -> u32 { + let code = hdr[dma::HDR_CONTROL] & image::CODE_MASK; + + if pixel_bytes(code) == bpp { + return code; + } + if !self.diag.dma_code_reported { + self.diag.dma_code_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA source format {:#x} at {} bytes a pixel is not modeled", code, bpp); + } + if bpp == 4 { image::CODE_FALLBACK_XRGB } else { image::CODE_CI } + } +} + +/// True when hdr is a backing-store page the model copies, bpp a texel. +pub(super) fn dma_raw(hdr: &[u32], bpp: u32) -> bool { + let fmt = hdr[dma::HDR_FORMAT]; + + (hdr[dma::HDR_CONTROL] & image::CODE_MASK) == 0 + && ((fmt == dma::FMT_RAW16 && bpp == 2) || (fmt == dma::FMT_RAW32 && bpp == 4)) +} + +impl Board { + /// True, reported once, when hdr is an 8-byte backing-store page. + pub fn dma_raw64(&mut self, hdr: &[u32], bpp: u32) -> bool { + if hdr[dma::HDR_FORMAT] != dma::FMT_RAW64 + || bpp != 8 + || (hdr[dma::HDR_CONTROL] & image::CODE_MASK) != 0 + { + return false; + } + if self.dma_report(dma::REPORT_RAW64) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: an 8-byte backing-store page of object {:#x} is not modeled", + hdr[dma::HDR_DEST] + ); + } + true + } +} + +pub(super) fn raw_nibble(byte: u32) -> u32 { + (byte * 15 + 127) / 255 +} + +/// A stored texel as a backing-store page's bpp raw bytes. +pub(super) fn raw_pack(rgba: u32, bpp: u32, dst: &mut [u8]) { + if bpp == 4 { + dst[..4].copy_from_slice(&rgba.to_be_bytes()); + return; + } + let word = raw_nibble(rgba >> 24) << 12 + | raw_nibble((rgba >> 16) & 0xff) << 8 + | raw_nibble((rgba >> 8) & 0xff) << 4 + | raw_nibble(rgba & 0xff); + dst[..2].copy_from_slice(&(word as u16).to_be_bytes()); +} + +/// A backing-store page's bpp raw bytes back as a stored texel. +pub(super) fn raw_unpack(src: &[u8], bpp: u32) -> u32 { + if bpp == 4 { + return be32(&src[..4]); + } + let v = be16(&src[..2]) as u32; + (((v >> 12) & 0xf) * 0x11u32) << 24 + | (((v >> 8) & 0xf) * 0x11u32) << 16 + | (((v >> 4) & 0xf) * 0x11u32) << 8 + | ((v & 0xf) * 0x11u32) +} + +impl Board { + /// A transfer into the colour buffer the screen shows (obj -1) or into + /// board-memory object obj: paint the header's rectangle from the source. + /// The source is a stream of pixel bytes spread over the entries: each row + /// takes rowbytes bytes, moving to the next entry whenever the current one's + /// count runs out, then the cursor steps over skip uncounted bytes. No byte + /// outside the entries is read. + pub fn dma_to_buffer(&mut self, bus: &DmaBus, table: u64, seq: u64, hdr: &[u32], obj: Option) { + let mut e = [DmaEntry::default(); dma::MAX_PAGES]; + let x = hdr[dma::HDR_ORIGIN] & 0xffff; + let y = hdr[dma::HDR_ORIGIN] >> 16; + let w = hdr[dma::HDR_SIZE] & 0xffff; + let h = if hdr[dma::HDR_SIZE] >> 16 != 0 { hdr[dma::HDR_SIZE] >> 16 } else { 0x10000 }; + let skip = hdr[dma::HDR_SKIP]; + let rowbytes = hdr[dma::HDR_ROWBYTES] & dma::ROWBYTES_MASK; + let bpp = DMA_BPP[((hdr[dma::HDR_CONTROL] >> dma::BPP_SHIFT) & dma::BPP_MASK) as usize] as u32; + let comps = texel12_components(hdr[dma::HDR_CONTROL] & image::CODE_MASK); + // A 12-bit texture source into an object: a byte or two a component. + let texel12 = obj.is_some() && comps != 0 && (bpp == comps || bpp == 2 * comps); + // An 8-bit luminance-alpha source into an object, two bytes a pixel. + let lum_alpha = obj.is_some() && bpp == 2 && (hdr[dma::HDR_CONTROL] & image::CODE_MASK) == image::CODE_LUM_ALPHA; + let raw = obj.is_some() && dma_raw(hdr, bpp); + let mut payload: u64 = 0; + let mut pos: u64 = 0; + + if obj.is_some() && self.dma_raw64(hdr, bpp) { + return; + } + if bpp != 4 && bpp != 3 && bpp != 1 && !texel12 && !lum_alpha && !raw { + if !self.diag.dma_bpp_reported { + self.diag.dma_bpp_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA to buffer {:#x} at {} bytes a pixel is not modeled", + hdr[dma::HDR_DEST], + bpp + ); + } + return; + } + let code = if texel12 || raw { + hdr[dma::HDR_CONTROL] & image::CODE_MASK + } else { + self.dma_code(hdr, bpp) + }; + let Ok(n) = bus.entries(table, seq, &mut e) else { + return; + }; + payload += e[..n].iter().map(|e| e.len as u64).sum::(); + if rowbytes as u64 != (w as u64) * (bpp as u64) || payload != (rowbytes as u64) * (h as u64) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA header at {:#x} disagrees with itself: {}x{} at {} bytes a pixel, {} bytes a row, {} bytes in its entries", + table, w, h, bpp, rowbytes, payload + ); + return; + } + let mut row = vec![0u8; rowbytes as usize]; + let mut staged = Vec::new(); + let mut i: usize = 0; + let mut left: u32 = if n > 0 { e[0].len } else { 0 }; + 'done: { + for r in 0..h { + let mut got: u32 = 0; + + while got < rowbytes { + if left == 0 { + // Checked above: the entries hold exactly h rows. + i += 1; + pos = 0; + left = e[i].len; + } + let take = left.min(rowbytes - got); + if !bus.read( + e[i].addr.wrapping_add(pos), + &mut row[got as usize..(got + take) as usize], + true, + seq, + ) { + break 'done; + } + got += take; + pos += take as u64; + left -= take; + } + if left != 0 { + pos += skip as u64; + } + for c in 0..w { + let px = &row[(c * bpp) as usize..]; + // An object's texels land on the raster unit, in order + // behind the draws that may still read the old ones. + let (ox, oy) = (x.wrapping_add(c), y.wrapping_add(r)); + if let (true, Some(obj)) = (raw, obj) { + self.object_stage(&mut staged, obj, ox, oy, raw_unpack(px, bpp)); + continue; + } + if let (true, Some(obj)) = (texel12, obj) { + self.object_stage(&mut staged, obj, ox, oy, decode_texel12(code, px, bpp != comps)); + continue; + } + if let Some(obj) = obj { + let pixel = self.decode_pixel(code, px).unwrap_or(0); + self.object_stage(&mut staged, obj, ox, oy, pixel); + continue; + } + self.dma_put((x as i64) + (c as i64), (y as i64) + (r as i64), px, code); + } + } + } + self.object_flush(&mut staged); + // An object is board memory the raster unit samples, not a line the display scans. + if obj.is_none() { + self.scanout.invalidate_rows_for(DirtySite::Dma, y, h); + } + } +} + +// Source format code 0x6c: a GL DMA image write of A, B, G, R bytes, one +// pixel a 4-byte word. ESTABLISHED from a model run: the software's payload +// carries the bytes in that order. INFERRED: that the board decodes the word +// in that order, from the image coming out right. Word 7 of the two observed +// writes differs in bits 29:25, which are ignored; every other bit must +// match, so an unseen form stays reported. +// +// Movie frames: the same A, B, G, R bytes under source format code 0x4c, +// with word 4 0x82233000. OBSERVED on the model: each 320x240 frame arrives +// as nine tiles covering it exactly, rows of 4 * width bytes with the rest +// of each source row skipped, first tile sent holding the first source rows. +// INFERRED from the picture: the byte order, that the source's first row is +// the top of the picture, and that the header's y counts up from the bottom +// of the window. What tells the board the rows run top down, and the window +// height its bottom-up y needs, remain unknown. + +/// What a depth or stencil image write names (see dma_depth_stencil_write()). +#[derive(Clone, Copy, PartialEq)] +pub(super) enum DepthStencil { + Depth, + Stencil, +} + +/// Whether hdr is the library's depth or stencil image write: format 0 and +/// type 4 (word 7 low bits 0x080, four bytes a pixel) with word 4's size +/// field 1 for depth and 0 for stencil. +/// +/// ESTABLISHED as the library's request. How the board takes the words is unknown, so the model's +/// choices, each pinned by a test, are: +/// - stencil lives in a plane of its own, so a stencil write leaves the +/// colour and the depth word alone; +/// - a write passes the same clip-rectangle and window-ID tests as a GL +/// fragment; +/// - a write ignores the stencil write mask (register 0x0452). The library's +/// burst does not: it sends the mask (draw.rs, image_depth_stencil_word()). +/// The library sends no depth or stencil by DMA (every depth and stencil +/// step takes the burst: ESTABLISHED), so this path has no +/// software evidence and the mask's effect on it is UNKNOWN; the choice +/// stands; +/// - a depth write is always the 24-bit word, bits 23:0 of each value. +pub(super) fn dma_depth_stencil_write(hdr: &[u32]) -> Option { + if hdr[dma::HDR_CONTROL] & write::CODE_MASK != image::CODE_DEPTH_STENCIL { + return None; + } + match (hdr[dma::HDR_FORMAT] >> dma::FMT_SIZE_SHIFT) & dma::FMT_SIZE_MASK { + dma::FMT_SIZE_DEPTH => Some(DepthStencil::Depth), + dma::FMT_SIZE_STENCIL => Some(DepthStencil::Stencil), + _ => None, + } +} + +/// The draw-sentinel upload's pixel form: word 7's format and type through +/// the write table (pixel_write.rs), at the bytes a pixel its bpp field +/// names. None for a form the table does not take. Word 4's low byte is zero +/// in every write the library builds. The movie write (dmplay's frames: +/// code 0x4c under its own word 4) also sets *top_down. +/// +/// The movie's picture is OBSERVED on the model: each 320x240 frame arrives +/// as nine tiles covering it exactly, rows of 4 * width bytes with the rest +/// of each source row skipped, first tile sent holding the first source +/// rows. INFERRED from the picture: the byte order, that the source's first +/// row is the top of the picture, and that the header's y counts up from +/// the bottom of the window. What tells the board the rows run top down, +/// and the window height its bottom-up y needs, remain unknown. +pub(super) fn dma_draw_sentinel_form(hdr: &[u32], top_down: &mut bool) -> Option { + let control = hdr[dma::HDR_CONTROL]; + let bpp = DMA_BPP[((control >> dma::BPP_SHIFT) & dma::BPP_MASK) as usize] as u32; + + *top_down = hdr[dma::HDR_FORMAT] == dma::MOVIE_FORMAT + && (control & !dma::ABGR_CONTROL_IGNORED) == dma::MOVIE_CONTROL; + if hdr[dma::HDR_FORMAT] & 0xff != 0 { + return None; + } + write_form(control & write::CODE_MASK).filter(|form| form.bytes == bpp) +} + +impl Board { + /// The depth plane's word at plane pixel (x, y), which is in the plane. + pub fn depth_load(&mut self, x: i64, y: i64) -> u32 { + self.raster_wait(); + let p = self.planes.depth.plane(self.planes.depth_plane).expect("the depth plane"); + self.planes.depth.plane_load(&p, x as i32, y as i32) + } + + pub fn depth_store(&mut self, x: i64, y: i64, word: u32) { + self.raster_wait(); + let p = self.planes.depth.plane(self.planes.depth_plane).expect("the depth plane"); + self.planes.depth.plane_store(&p, x as i32, y as i32, word); + } + + pub fn stencil_load(&mut self, x: i64, y: i64) -> u32 { + self.raster_wait(); + let p = self.planes.stencil.plane(self.planes.stencil_plane).expect("the stencil plane"); + self.planes.stencil.plane_load(&p, x as i32, y as i32) + } + + pub fn stencil_store(&mut self, x: i64, y: i64, value: u8) { + self.raster_wait(); + let p = self.planes.stencil.plane(self.planes.stencil_plane).expect("the stencil plane"); + self.planes.stencil.plane_store(&p, x as i32, y as i32, value as u32); + } + + /// Stage the pixel bytes first so a failed source read cannot leave a + /// partially painted rectangle. Entry lengths count pixels; row gaps advance + /// the address cursor only while the current entry has bytes remaining. + pub fn dma_draw_sentinel_short(&mut self, bus: &DmaBus, table: u64, seq: u64, hdr: &[u32]) { + let mut e = [DmaEntry::default(); dma::MAX_PAGES]; + let x = hdr[dma::HDR_ORIGIN] & 0xffff; + let y = hdr[dma::HDR_ORIGIN] >> 16; + let w = hdr[dma::HDR_SIZE] & 0xffff; + let h = if hdr[dma::HDR_SIZE] >> 16 != 0 { hdr[dma::HDR_SIZE] >> 16 } else { 0x10000 }; + let skip = hdr[dma::HDR_SKIP]; + let rowbytes = hdr[dma::HDR_ROWBYTES] & dma::ROWBYTES_MASK; + let mut payload_len: u64 = 0; + let mut pos: u64 = 0; + let mut out: u64 = 0; + let mut top_down = false; + let depth_stencil = dma_depth_stencil_write(hdr); + let form = dma_draw_sentinel_form(hdr, &mut top_down); + let psize = if depth_stencil.is_some() { 4 } else { form.as_ref().map_or(0, |f| f.bytes) }; + // APPROXIMATION: the window's height, for a top-down write's bottom-up y, + // taken from the clip box's last row; the header carries no height. A + // capture with a scissor box smaller than the window would tell this + // rule from the board's own. + let rows: i64 = (self.gl_clip_box[1] & clip::BOX_FIELD_MASK) as i64 + 1; + + if psize == 0 { + if self.dma_report(dma::REPORT_UPLOAD_SENTINEL) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA upload to sentinel {:#x} with format {:#x} and control {:#x} is not modeled", + hdr[dma::HDR_DEST], + hdr[dma::HDR_FORMAT], + hdr[dma::HDR_CONTROL] + ); + } + return; + } + + let Ok(n) = bus.entries(table, seq, &mut e) else { + return; + }; + payload_len += e[..n].iter().map(|e| e.len as u64).sum::(); + if w == 0 + || h == 0 + || rowbytes as u64 != (w as u64) * (psize as u64) + || payload_len != (rowbytes as u64) * (h as u64) + { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: draw-sentinel DMA header at {:#x} disagrees with its payload", table); + return; + } + + let mut payload = vec![0u8; payload_len as usize]; + let mut i: usize = 0; + let mut left: u32 = if n > 0 { e[0].len } else { 0 }; + for _r in 0..h { + let mut got: u32 = 0; + + while got < rowbytes { + if left == 0 { + i += 1; + pos = 0; + left = e[i].len; + } + let take = left.min(rowbytes - got); + let at = (out + got as u64) as usize; + if pos > u64::MAX - e[i].addr + || !bus.read(e[i].addr + pos, &mut payload[at..at + take as usize], true, seq) + { + return; + } + got += take; + pos += take as u64; + left -= take; + } + if left != 0 { + pos += skip as u64; + } + out += rowbytes as u64; + } + + for r in 0..h { + for c in 0..w { + let off = ((r as u64) * (rowbytes as u64) + (c as u64) * (psize as u64)) as usize; + let src = &payload[off..]; + let px = (x as i64) + (c as i64) + self.win.origin[0] as i64; + let py = (if top_down { + rows - (y as i64) - (h as i64) + (r as i64) + } else { + (y as i64) + (h as i64) - 1 - (r as i64) + }) + self.win.origin[1] as i64; + if let Some(plane) = depth_stencil { + let value = be32(&src[..4]); + + if self.pixel_allowed(px, py) { + if plane == DepthStencil::Depth { + self.depth_store(px, py, value & ODSY_DEPTH24_MAX); + } else { + self.stencil_store(px, py, value as u8); + } + } + continue; + } + let rgba = form.as_ref().expect("a form when not depth or stencil").decode(src); + self.paint_pixel(px, py, rgba); + } + } + } + + /// True the first time reason (dma::REPORT_*) comes up: report it then. + pub fn dma_report(&mut self, reason: u32) -> bool { + if self.diag.dma_read_reported & reason != 0 { + return false; + } + self.diag.dma_read_reported |= reason; + true + } +} + +const fn lay(code: u32, bytes: u8, from: [u8; 8]) -> DmaLayout { + DmaLayout { code: code as u8, bytes, from, fields: [DmaField { chan: 0, width: 0, shift: 0 }; 4], plane: ReadPlane::Colour } +} + +const fn fld(chan: u8, width: u8, shift: u8) -> DmaField { + DmaField { chan, width, shift } +} + +/// A packed layout: the fields of a `bytes`-byte big-endian word. +const fn pack(code: u32, bytes: u8, fields: [DmaField; 4]) -> DmaLayout { + DmaLayout { code: code as u8, bytes, from: [0; 8], fields, plane: ReadPlane::Colour } +} + +const NF: DmaField = fld(0, 0, 0); +const BZ: u8 = dma::BYTE_ZERO as u8; +const BI: u8 = dma::BYTE_INDEX as u8; +const BH: u8 = dma::BYTE_INDEX_HIGH as u8; +const BR: u8 = dma::BYTE_INDEX_OR_RED as u8; + +/// Which sources a GL read header row applies to. +#[derive(Clone, Copy, PartialEq)] +pub(super) enum ReadFrom { + Any, + /// A texture object, named by its board address. + Object, +} + +#[derive(Clone, Copy)] +pub(super) struct GlRead { + pub class: u8, + pub code: u8, + pub from: ReadFrom, + pub layout: DmaLayout, +} + +const fn row(class: u8, code: u8, layout: DmaLayout) -> GlRead { + GlRead { class, code, from: ReadFrom::Any, layout } +} + +const fn object_row(class: u8, code: u8, layout: DmaLayout) -> GlRead { + GlRead { class, code, from: ReadFrom::Object, layout } +} + +/// GL reads by word 4's class and code (established as the library's +/// request; that the board returns the GL packing of the named format and +/// type is inferred). A row lists every format and type whose bytes are the +/// same under one header. +/// +/// Packed rows quantise each channel of the plane word to the field's width +/// by keeping its top bits, which is how the model shows a narrow visual at +/// scanout (display_channel()). APPROXIMATION: the planes keep the byte that +/// was drawn, so for a 4/4/4/4 or 5/5/5/1 visual the stored value narrows +/// only here, and 10-bit fields repeat the byte's top bits into the low two; +/// how the board stores and widens a narrow pixel is unknown. A byte read +/// from a narrow visual (class 1 code 1, class 2 code 8) returns the plane's +/// byte as drawn, for the same reason. +/// +/// A one- or two-channel luminance, alpha or red texture is stored with the +/// value in red, green and blue, so those rows return the red byte. +/// +/// Class 0 code 0 and class 2 code 2 from a drawable are shared with stencil +/// and depth reads and are told apart by the field codes +/// (dma_read_layout()). +pub(super) static DMA_GL_READS: &[GlRead] = &[ + // Header class 0: one byte a pixel. A pixel the display shows as an + // index returns its index, any other its red byte. + row(0, 1, lay(1, 1, [BR, 0, 0, 0, 0, 0, 0, 0])), + // Code 0: a texture's alpha, and a colour read when a drawable's field + // codes call it one (dma_read_layout()). The red byte, as for any single + // channel. + row(0, 0, lay(0, 1, [BR, 0, 0, 0, 0, 0, 0, 0])), + // Header class 1: two bytes a pixel, but code 1 is four. + object_row(1, 0, lay(0, 2, [0, 3, 0, 0, 0, 0, 0, 0])), // LUMINANCE_ALPHA, UNSIGNED_BYTE + row(1, 1, lay(1, 4, [0, 1, 2, 3, 0, 0, 0, 0])), // RGBA, UNSIGNED_BYTE (5/5/5/1) + row(1, 2, lay(2, 2, [0, 0, 0, 0, 0, 0, 0, 0])), // ALPHA, LUMINANCE, RED: UNSIGNED_SHORT + row(1, 3, pack(3, 2, [fld(3, 1, 15), fld(0, 5, 10), fld(1, 5, 5), fld(2, 5, 0)])), // BGRA, 1_5_5_5_REV + row(1, 4, pack(4, 2, [fld(2, 5, 11), fld(1, 6, 5), fld(0, 5, 0), NF])), // BGR, 5_6_5 + row(1, 5, pack(5, 2, [fld(2, 5, 11), fld(1, 5, 6), fld(0, 5, 1), fld(3, 1, 0)])), // BGRA, 5_5_5_1 + row(1, 6, pack(6, 2, [fld(3, 4, 12), fld(2, 4, 8), fld(1, 4, 4), fld(0, 4, 0)])), // RGBA, 4_4_4_4_REV + row(1, 7, pack(7, 2, [fld(3, 4, 12), fld(0, 4, 8), fld(1, 4, 4), fld(2, 4, 0)])), // BGRA, 4_4_4_4_REV + row(1, 8, pack(8, 2, [fld(2, 4, 12), fld(1, 4, 8), fld(0, 4, 4), fld(3, 4, 0)])), // BGRA, 4_4_4_4 + row(1, 9, pack(9, 2, [fld(3, 1, 15), fld(2, 5, 10), fld(1, 5, 5), fld(0, 5, 0)])), // RGBA, 1_5_5_5_REV + row(1, 10, pack(10, 2, [fld(0, 5, 11), fld(1, 6, 5), fld(2, 5, 0), NF])), // RGB, 5_6_5 + row(1, 12, pack(12, 2, [fld(0, 5, 11), fld(1, 5, 6), fld(2, 5, 1), fld(3, 1, 0)])), // RGBA, 5_5_5_1 + row(1, 13, pack(13, 2, [fld(0, 4, 12), fld(1, 4, 8), fld(2, 4, 4), fld(3, 4, 0)])), // RGBA, 4_4_4_4 + row(1, 14, lay(14, 2, [BH, BI, 0, 0, 0, 0, 0, 0])), // COLOR_INDEX, UNSIGNED_SHORT + // Header class 2: the RGBA 8/8/8/8 and 10/10/10/2 windows and + // luminance-alpha 12 textures. + row(2, 0, pack(0, 4, [fld(0, 10, 22), fld(1, 10, 12), fld(2, 10, 2), fld(3, 2, 0)])), // RGBA, 10_10_10_2 + row(2, 1, lay(1, 3, [0, 1, 2, 0, 0, 0, 0, 0])), // RGB, UNSIGNED_BYTE + row(2, 2, lay(2, 4, [3, 0, 1, 2, 0, 0, 0, 0])), // BGRA, 8_8_8_8_REV + row(2, 3, lay(3, 3, [2, 1, 0, 0, 0, 0, 0, 0])), // BGR, UNSIGNED_BYTE + row(2, 4, lay(4, 4, [2, 1, 0, 3, 0, 0, 0, 0])), // BGRA, UNSIGNED_BYTE + row(2, 5, lay(5, 4, [0, 0, 3, 3, 0, 0, 0, 0])), // LUMINANCE_ALPHA, UNSIGNED_SHORT + row(2, 6, lay(6, 2, [0, 3, 0, 0, 0, 0, 0, 0])), // LUMINANCE_ALPHA, UNSIGNED_BYTE + row(2, 8, lay(8, 4, [0, 1, 2, 3, 0, 0, 0, 0])), // RGBA, UNSIGNED_BYTE + row(2, 9, pack(9, 2, [fld(2, 5, 11), fld(1, 6, 5), fld(0, 5, 0), NF])), // BGR, 5_6_5 + row(2, 10, pack(10, 2, [fld(0, 5, 11), fld(1, 6, 5), fld(2, 5, 0), NF])), // RGB, 5_6_5 + row(2, 11, lay(11, 4, [3, 2, 1, 0, 0, 0, 0, 0])), // ABGR, UNSIGNED_BYTE + row(2, 12, pack(12, 4, [fld(3, 2, 30), fld(2, 10, 20), fld(1, 10, 10), fld(0, 10, 0)])), // RGBA, 2_10_10_10_REV + // Header class 3: the RGBA 12/12/12/12 window. The model keeps eight + // bits a channel, so a byte read returns them as stored and a 16-bit + // read replicates each byte. APPROXIMATION: how the board narrows a + // 12-bit channel to eight bits is unknown. + row(3, 1, lay(1, 4, [0, 1, 2, 3, 0, 0, 0, 0])), // RGBA, UNSIGNED_BYTE + row(3, 2, lay(2, 3, [0, 1, 2, 0, 0, 0, 0, 0])), // RGB, UNSIGNED_BYTE + row(3, 3, pack(3, 4, [fld(3, 2, 30), fld(2, 10, 20), fld(1, 10, 10), fld(0, 10, 0)])), // RGBA, 2_10_10_10_REV + row(3, 5, lay(5, 8, [3, 3, 2, 2, 1, 1, 0, 0])), // ABGR, UNSIGNED_SHORT + row(3, 6, lay(6, 4, [3, 2, 1, 0, 0, 0, 0, 0])), // ABGR, UNSIGNED_BYTE + row(3, 7, lay(7, 3, [2, 1, 0, 0, 0, 0, 0, 0])), // BGR, UNSIGNED_BYTE + row(3, 8, lay(8, 6, [0, 0, 1, 1, 2, 2, 0, 0])), // RGB, UNSIGNED_SHORT + row(3, 9, lay(9, 8, [0, 0, 1, 1, 2, 2, 3, 3])), // RGBA, UNSIGNED_SHORT + // Header class 4: RGBA textures of 13 channel bits. + row(4, 0, lay(0, 8, [0, 0, 1, 1, 2, 2, 3, 3])), // RGBA, UNSIGNED_SHORT + row(4, 2, lay(2, 4, [0, 1, 2, 3, 0, 0, 0, 0])), // RGBA, UNSIGNED_BYTE +]; + +/// A depth read from a drawable under header class 1 code 0, two bytes a +/// pixel (DEPTH_COMPONENT, UNSIGNED_SHORT). The library sends it for windows +/// whose visual is RGBA 12/12/12/12. The bytes are the 16-bit form of the +/// plane's depth word: exponent in bits 15:10, the top ten mantissa bits +/// below. +/// +/// ESTABLISHED as the library's request; that the board returns the plane's own 16-bit form is INFERRED. +/// MODEL CHOICE: the plane holds a 24-bit word whatever the visual, so a +/// depth written by the 16-bit form (always stored as 24 bits, see +/// dma_depth_stencil_write()) reads back near zero, and that visual's depth +/// does not round-trip; the model does not identify those windows. +pub(super) static DMA_DEPTH16: DmaLayout = + DmaLayout { code: 0, bytes: 2, from: [0; 8], fields: [NF; 4], plane: ReadPlane::Depth16 }; + +/// A depth read under class 2 code 2 with field codes (0,0) or (4,0), four +/// bytes a pixel (DEPTH_COMPONENT, UNSIGNED_INT): the plane's 24-bit word. +pub(super) static DMA_DEPTH32: DmaLayout = + DmaLayout { code: 2, bytes: 4, from: [0; 8], fields: [NF; 4], plane: ReadPlane::Depth32 }; + +/// A stencil read under class 0 code 0 with field codes (4,0), one byte a +/// pixel (STENCIL_INDEX, UNSIGNED_BYTE). MODEL CHOICE: the stencil byte +/// comes from the model's own stencil plane; where the board keeps it inside +/// a stored pixel is unknown. +pub(super) static DMA_STENCIL: DmaLayout = + DmaLayout { code: 0, bytes: 1, from: [0; 8], fields: [NF; 4], plane: ReadPlane::Stencil }; + +/// X reads, by word 7's format byte: the upload decode run backwards. The +/// luminance code is not invertible and no read of it has been seen. +pub(super) static DMA_X_LAYOUTS: [DmaLayout; 4] = [ + lay(image::CODE_ABGR, 4, [BZ, 2, 1, 0, 0, 0, 0, 0]), + lay(image::CODE_RGBA, 4, [0, 1, 2, 3, 0, 0, 0, 0]), + lay(image::CODE_RGB, 3, [0, 1, 2, 0, 0, 0, 0, 0]), + lay(image::CODE_CI, 1, [BI, 0, 0, 0, 0, 0, 0, 0]), +]; + +/// Whether a read header's class and code are sent both for a colour read +/// and for a depth or stencil read. +fn shared_depth_code(cls: u32, code: u32) -> bool { + (cls == 0 && code == 0) || (cls == 2 && code == 2) +} + +/// A one-byte colour index read: the plane word's low byte, refused for a +/// pixel shown as a colour. +pub(super) static DMA_GL_INDEX_BYTE: DmaLayout = lay(0, 1, [BI, 0, 0, 0, 0, 0, 0, 0]); + +impl Board { + /// The read's layout, or None (reported once) when it is not modeled. + pub fn dma_read_layout(&mut self, hdr: &[u32], bpp: u32) -> Option<&'static DmaLayout> { + let xcode = hdr[dma::HDR_CONTROL] & image::CODE_MASK; + let fmt = hdr[dma::HDR_FORMAT]; + let cls = fmt & dma::FMT_CLASS_MASK; + let code = (fmt >> dma::FMT_CODE_SHIFT) & dma::FMT_CODE_MASK; + let drawable = hdr[dma::HDR_DEST] & dma::SENTINEL != 0; + let mut l: Option<&'static DmaLayout> = None; + + if xcode != 0 { + for t in DMA_X_LAYOUTS.iter() { + if t.code as u32 == xcode { + l = Some(t); + } + } + } else if drawable && shared_depth_code(cls, code) { + // Class 0 code 0 and class 2 code 2 are each sent for a colour + // read and for a stencil or 32-bit depth read. Word 4's two field + // codes (bits 23:20, 27:24) tell them apart: (4,0) is a stencil byte or a depth word, (0,0) a depth + // word, and the colour pairs are (6,2), (2,2), (3,2), (7,3) and + // (2,0). + let fields = ((fmt >> 20) & 0xf, (fmt >> 24) & 0xf); + + if cls == 2 && (fields == (4, 0) || fields == (0, 0)) { + l = Some(&DMA_DEPTH32); + } else if cls == 0 && fields == (4, 0) { + l = Some(&DMA_STENCIL); + } else if cls == 0 && fields == (0, 0) { + // UNKNOWN: this is a stencil read of a storage-2 window or a + // colour index read of a storage-0 window, and only the + // header's word 2 would separate them. The model keeps no + // storage code, so it serves a colour index and says so once. + // On an RGB window the index read is refused (dma_read_pack()). + if self.dma_report(dma::REPORT_STENCIL_OR_INDEX) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA read with word 4 {:#x} is a colour index read of a storage-0 window or a stencil read, which the header does not separate; served as colour index", + fmt + ); + } + l = Some(&DMA_GL_INDEX_BYTE); + } else { + if !(cls == 2 && matches!(fields, (6, 2) | (2, 2) | (3, 2) | (7, 3) | (2, 0))) && self.dma_report(dma::REPORT_UNLISTED_FIELDS) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA read with word 4 {:#x} (class {}, code {}) has field codes that are not known; served as colour", + fmt, cls, code + ); + } + l = DMA_GL_READS.iter().find(|t| t.class as u32 == cls && t.code as u32 == code).map(|t| &t.layout); + } + } else if drawable && cls == 1 && code == 0 { + l = Some(&DMA_DEPTH16); + } else { + for t in DMA_GL_READS.iter() { + if t.class as u32 == cls && t.code as u32 == code && (t.from == ReadFrom::Any || !drawable) { + l = Some(&t.layout); + } + } + } + if let Some(t) = l { + if t.bytes as u32 == bpp { + return Some(t); + } + } + if self.dma_report(dma::REPORT_LAYOUT) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA read with format byte {:#x}, word 4 {:#x} (class {}, code {}) at {} bytes a pixel is not modeled", + xcode, fmt, cls, code, bpp + ); + } + None + } + + /// Resolve word 0 to the read's source. False, reported once, when it names + /// nothing the model can read. A sentinel never creates or names a + /// board-memory object. + pub fn dma_read_source(&mut self, hdr: &[u32], src: &mut DmaSource) -> bool { + let base = hdr[dma::HDR_DEST]; + let fmt = hdr[dma::HDR_FORMAT]; + let selector = (fmt >> dma::FMT_SELECTOR_SHIFT) & dma::FMT_SELECTOR_MASK; + + src.obj = None; + // Front and back per pixel (Board::dma_read_pixel()). + src.front = true; + src.x = (hdr[dma::HDR_ORIGIN] & 0xffff) as i64; + src.y = (hdr[dma::HDR_ORIGIN] >> 16) as i64; + + if base & dma::SENTINEL != 0 { + if base == dma::SENTINEL_DRAW && selector == dma::FMT_SELECTOR_DRAW { + if self.dma_report(dma::REPORT_DRAW_SENTINEL) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA read of the draw drawable ({:#x}); never captured, read as the read drawable is", + base + ); + } + } else if base != dma::SENTINEL_READ || selector != dma::FMT_SELECTOR_READ { + if self.dma_report(dma::REPORT_SENTINEL) { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA read of sentinel {:#x} with word 4 {:#x} is not modeled", base, fmt); + } + return false; + } + // INFERRED: both sentinels are the current window, whose origin the + // model tracks; it keeps no separate read and draw drawable. + src.x += self.win.origin[0] as i64; + src.y += self.win.origin[1] as i64; + src.front = fmt & dma::FMT_FRONT != 0; + return true; + } + if base != 0 && base == self.dma_colour_base { + return true; + } + src.obj = self.object_find(base); + if src.obj.is_none() { + if self.dma_report(dma::REPORT_SOURCE) { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA read of board base {:#x}, which holds no buffer", base); + } + return false; + } + true + } + + /// Source pixel (x, y) as the plane's 0xRRGGBBAA word, or a raw index. A + /// colour-buffer pixel comes from the buffer its main drawable shows, or from + /// the other buffer for a back read. (x, y) is inside the plane. + pub fn dma_read_pixel(&mut self, src: &DmaSource, x: i64, y: i64) -> u32 { + if let Some(obj) = src.obj { + return self.object_load(obj, x as u32, y as u32); + } + let buffer = self.read_buffer(src.front, x, y); + self.buffer_load(buffer, x, y) + } + + /// Whether the bound read drawable is an off-screen object (a pixel + /// buffer) and not the screen: the base 0x609 names is not the first + /// colour buffer the X server selected, which is the screen's. + pub fn read_drawable_offscreen(&self) -> bool { + let base = self.read_word & !ODSY_DRAW_WORD_STATE; + + self.screen_base != 0 && base != 0 && base != self.screen_base + } + + /// The colour buffer, 0 or 1, that a GL read of pixel (x, y) names by + /// its front bit (word 4 bit 31 of the read header). `front` is logical. + /// + /// A window's logical front is the buffer its id displays and its back + /// the other: the draw mask the kernel sends is absolute, with front and + /// back exchanged once the window has swapped, so a front draw lands in + /// the displayed buffer. + /// + /// ESTABLISHED: the + /// kernel alone writes bit 31 of the buffer registers 0x608 and 0x609, and + /// sets it when the drawable's pane has a zero swap halfword; it sends the + /// draw mask unchanged then, and with front and back exchanged (bit 31 + /// clear) once the halfword is nonzero. A pixel buffer's pane is an + /// ordinary pane, so a double-buffered pixel buffer swaps like a window. + /// INFERRED: a read with header bit 31 set returns the buffer a front draw + /// mask writes, for a window and a pixel buffer alike. A pixel buffer has + /// no on-screen id, so the model follows bit 31 of 0x609: set means front + /// is buffer 0, clear means buffer 1, whatever the ids under the pixel + /// buffer's plane coordinates display. UNKNOWN: how the board encodes bit + /// 31 of 0x609 together with the header's bit 31. Windows keep the + /// displayed-buffer rule, which gives the same buffer as the swap state + /// does; the model has no swap state of its own. + pub fn read_buffer(&mut self, front: bool, x: i64, y: i64) -> u32 { + if self.read_drawable_offscreen() { + let buffer_0_is_front = self.read_word & ODSY_DRAW_WORD_STATE != 0; + + return if front == buffer_0_is_front { 0 } else { 1 }; + } + let id = self.main_id(x as u32, y as u32); + let shown = self.front.lock().displayed_buffer.checked_shr(id).unwrap_or(0) & 1; + + if front { shown } else { shown ^ 1 } + } +} + +impl DmaBus { + /// Map piece p at guest address addr: an entry is one physical extent, so + /// the pages it runs over are consecutive. + pub fn map_piece(&self, p: &mut DmaPiece, addr: u64) -> Result<(), DmaError> { + let first = addr & !(dma::PAGE_SIZE - 1); + let npages = (addr - first + p.len as u64).div_ceil(dma::PAGE_SIZE) as u32; + let mut pages = vec![0u64; npages as usize]; + assert!(npages as usize <= dma::PIECE_PAGES); + for (i, pg) in pages.iter_mut().enumerate() { + *pg = first + (i as u64) * dma::PAGE_SIZE; + } + let pl = DmaPageList { addr: pages, npages, page_size: dma::PAGE_SIZE as u32 }; + p.at = addr - first; + self.map(&mut p.map, &pl, p.at, p.len as u64, DmaDirection::FromDevice, false, 0) + } +} + +/// Channel `chan` of plane word 0xRRGGBBAA as a `width`-bit field: the top +/// bits for a narrower field, the byte's top bits repeated below for a wider +/// one (see DMA_GL_READS). +pub(super) fn field_value(word: u32, chan: u32, width: u32) -> u32 { + let c = (word >> (24 - 8 * chan)) & 0xff; + + if width <= 8 { c >> (8 - width) } else { (c << (width - 8)) | (c >> (16 - width)) } +} + +impl Board { + /// Pack the read's rectangle into out, host row by host row, rowbytes each. + /// A colour-index byte is the plane word itself, taken only where scanout + /// would take that word as an index: a colour-buffer pixel whose main + /// drawable's display mode is an index group, with a word that fits eight + /// bits. Anything else refuses the whole read, reported once. A colour + /// layout over an index-mode pixel returns the raw index word's bytes as + /// colour and is reported once: no inverse colormap is applied. + pub fn dma_read_pack(&mut self, src: &DmaSource, l: &DmaLayout, w: u32, h: u32, bottom_up: bool, out: &mut [u8]) -> bool { + let packed = l.fields[0].width != 0; + let first = l.from[0] as u32; + let index_read = !packed && (first == dma::BYTE_INDEX || first == dma::BYTE_INDEX_HIGH); + let index_or_red = !packed && first == dma::BYTE_INDEX_OR_RED; + let index_max: u32 = if first == dma::BYTE_INDEX_HIGH { 0xfff } else { 0xff }; + let mut o: usize = 0; + + if let (true, Some(obj)) = (index_read, src.obj) { + if self.dma_report(dma::REPORT_INDEX_OBJECT) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA index read of board object {:#x}, which records no display mode", + self.objects.base[obj] + ); + } + return false; + } + for r in 0..h { + let board_row = if bottom_up { h - 1 - r } else { r }; + + for c in 0..w { + let x = src.x + c as i64; + let y = src.y + board_row as i64; + if l.plane != ReadPlane::Colour { + let n = l.bytes as usize; + let word = match l.plane { + ReadPlane::Depth16 => super::depth_word::word16_from_word24(self.depth_load(x, y)), + ReadPlane::Depth32 => self.depth_load(x, y) & ODSY_DEPTH24_MAX, + _ => self.stencil_load(x, y), + }; + + out[o..o + n].copy_from_slice(&word.to_be_bytes()[4 - n..]); + o += n; + continue; + } + let word = self.dma_read_pixel(src, x, y); + let mut group = ODSY_GAMMA_GROUP; + + if src.obj.is_none() { + let id = self.main_id(x as u32, y as u32); + group = self.display_group(id); + } + let indexed = index_read || (index_or_red && group < ODSY_GAMMA_GROUP); + + if indexed && (group >= ODSY_GAMMA_GROUP || word > index_max) { + if self.dma_report(dma::REPORT_INDEX) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA index read at ({}, {}) finds word {:#x} in display-mode group {}, not an eight-bit index", + x, y, word, group + ); + } + return false; + } + if !indexed && group < ODSY_GAMMA_GROUP && self.dma_report(dma::REPORT_COLOUR_OVER_INDEX) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA colour read at ({}, {}) finds an index pixel (display-mode group {}); its word {:#x} is returned as colour", + x, y, group, word + ); + } + if packed { + let mut v: u32 = 0; + + for f in l.fields.iter().filter(|f| f.width != 0) { + v |= field_value(word, f.chan as u32, f.width as u32) << f.shift; + } + out[o..o + l.bytes as usize].copy_from_slice(&v.to_be_bytes()[4 - l.bytes as usize..]); + o += l.bytes as usize; + continue; + } + for &from in &l.from[..l.bytes as usize] { + let from = from as u32; + + out[o] = if from == dma::BYTE_INDEX { + word as u8 + } else if from == dma::BYTE_INDEX_HIGH { + (word >> 8) as u8 + } else if from == dma::BYTE_INDEX_OR_RED { + if indexed { word as u8 } else { (word >> 24) as u8 } + } else if from == dma::BYTE_ZERO { + 0 + } else { + (word >> (24 - 8 * from)) as u8 + }; + o += 1; + } + } + } + true + } + + /// Write the packed payload into the table's entries: rowbytes bytes a host + /// row, moving to the next entry when one is used up, then skipping skip + /// uncounted bytes, exactly as an upload reads its source. Every piece is + /// mapped before any byte is written, so a table that fails to map writes + /// nothing. No byte outside the pieces is touched. + pub fn dma_read_write(&mut self, bus: &DmaBus, e: &[DmaEntry], rowbytes: u32, h: u32, skip: u32, payload: &mut [u8]) -> bool { + let n = e.len(); + let mut pieces: Vec = Vec::new(); + let mut pos: u64 = 0; + let mut done: u64 = 0; + let mut left: u32 = if n > 0 { e[0].len } else { 0 }; + let mut ok = true; + let mut i: usize = 0; + + let mut r: u32 = 0; + while r < h && ok { + let mut got: u32 = 0; + + while got < rowbytes { + let mut p = DmaPiece::default(); + + if left == 0 { + // Checked by the caller: the entries hold exactly h rows. + i += 1; + pos = 0; + left = e[i].len; + } + p.len = left.min(rowbytes - got); + p.src = done; + let plen = p.len; + let mapped = bus.map_piece(&mut p, e[i].addr + pos); + pieces.push(p); + if mapped.is_err() { + ok = false; + break; + } + got += plen; + done += plen as u64; + pos += plen as u64; + left -= plen; + } + if left != 0 { + pos += skip as u64; + } + r += 1; + } + for p in pieces.iter_mut() { + if ok { + let a = p.src as usize; + if !p.map.copy(p.at, &mut payload[a..a + p.len as usize], true) { + ok = false; + } + } + p.map.unmap(); + } + if !ok && self.dma_report(dma::REPORT_WRITE) { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA read's host pages could not all be written"); + } + ok + } + + /// A board-to-host transfer (word 7 bit 24): read the header's rectangle from + /// its source and write it, laid out as the header says, into the table's + /// entries. Any refusal is reported once and writes nothing to the guest; the + /// caller still raises DMA-done when bit 31 asks for it. The payload is in + /// guest memory before this returns. + pub fn dma_from_board(&mut self, bus: &DmaBus, table: u64, seq: u64, hdr: &[u32]) { + let mut e = [DmaEntry::default(); dma::MAX_PAGES]; + let w = hdr[dma::HDR_SIZE] & 0xffff; + let h = if hdr[dma::HDR_SIZE] >> 16 != 0 { hdr[dma::HDR_SIZE] >> 16 } else { 0x10000 }; + let skip = hdr[dma::HDR_SKIP]; + let rowbytes = hdr[dma::HDR_ROWBYTES] & dma::ROWBYTES_MASK; + let bpp = DMA_BPP[((hdr[dma::HDR_CONTROL] >> dma::BPP_SHIFT) & dma::BPP_MASK) as usize] as u32; + let mut src = DmaSource::default(); + let mut total: u64 = 0; + + let raw = dma_raw(hdr, bpp); + + if self.dma_raw64(hdr, bpp) { + return; + } + let l = if raw { None } else { self.dma_read_layout(hdr, bpp) }; + if (!raw && l.is_none()) || !self.dma_read_source(hdr, &mut src) { + return; + } + if raw && src.obj.is_none() { + if self.dma_report(dma::REPORT_SOURCE) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: raw DMA read of {:#x}, which is not a board-memory object, is not modeled", + hdr[dma::HDR_DEST] + ); + } + return; + } + let Ok(n) = bus.entries(table, seq, &mut e) else { + return; + }; + total += e[..n].iter().map(|e| e.len as u64).sum::(); + if rowbytes as u64 != (w as u64) * (bpp as u64) || total != (rowbytes as u64) * (h as u64) { + if self.dma_report(dma::REPORT_SHAPE) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA read header at {:#x} disagrees with itself: {}x{} at {} bytes a pixel, {} bytes a row, {} bytes in its entries", + table, w, h, bpp, rowbytes, total + ); + } + return; + } + let inside = if let Some(obj) = src.obj { + // Rows past an object's store were never written: the model holds no + // value for them, so a read reaching them is refused, not zero-filled. + src.x + (w as i64) <= self.objects.width[obj] as i64 && src.y + (h as i64) <= self.objects.rows[obj] as i64 + } else { + src.x >= 0 + && src.y >= 0 + && src.x + (w as i64) <= ODSY_WIDTH as i64 + && src.y + (h as i64) <= ODSY_HEIGHT as i64 + }; + if !inside { + if self.dma_report(dma::REPORT_BOUNDS) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA read of {}x{} at ({}, {}) from {:#x} leaves its source", + w, h, src.x, src.y, hdr[dma::HDR_DEST] + ); + } + return; + } + let mut payload = vec![0u8; total as usize]; + if let (true, Some(obj)) = (raw, src.obj) { + for r in 0..h { + for c in 0..w { + let word = self.object_load(obj, (src.x + c as i64) as u32, (src.y + r as i64) as u32); + let off = ((r as u64) * (rowbytes as u64) + (c as u64) * (bpp as u64)) as usize; + raw_pack(word, bpp, &mut payload[off..]); + } + } + if self.dma_read_write(bus, &e[..n], rowbytes, h, skip, &mut payload) { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA read: seq {} source {:#x} at {},{} {}x{} wrote {} bytes", seq, hdr[dma::HDR_DEST], src.x, src.y, w, h, total); + } + return; + } + let l = l.unwrap(); + if !self.dma_read_pack(&src, l, w, h, hdr[dma::HDR_CONTROL] & dma::BOTTOM_UP != 0, &mut payload) { + return; + } + if !self.dma_read_write(bus, &e[..n], rowbytes, h, skip, &mut payload) { + return; + } + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA read: seq {} source {:#x} at {},{} {}x{} wrote {} bytes", seq, hdr[dma::HDR_DEST], src.x, src.y, w, h, total); + } + + /// A 0xe01/0xe03 descriptor: read the table's header, move the pixels, and + /// raise the DMA-done interrupt when the header asks for it. A header with + /// word 7 bit 24 set is a read; any other names its destination in word 0: + /// the colour buffer the screen shows, or an off-screen object whose row + /// length is word 2's (16-pixel units), created when no select named it + /// first. A zero word 0 names no buffer and is refused. An upload to a + /// sentinel (GL's DMA image write) is accepted only by the draw-sentinel + /// adapter. The 0xe03 opcode is reported and not performed. + pub fn dma_descriptor(&mut self, readback: bool) { + let table = dma_table_address(self.cmd.operand[0], self.cmd.operand[1]); + crate::pf_scope!(crate::perfetto::events::ODSY_DMA, table); + self.dma_seq += 1; + let seq = self.dma_seq; + let bus = DmaBus { + mem: self.guest.clone(), + trace_sequence: self.trace_sequence.load(Ordering::Relaxed).wrapping_sub(1), + }; + let mut raw = [0u8; dma::TABLE_HEADER as usize]; + let mut hdr = [0u32; dma::HDR_WORDS]; + + if !bus.read(table, &mut raw, true, seq) { + return; + } + for i in 0..dma::HDR_WORDS { + hdr[i] = be32(&raw[i * 4..][..4]); + } + if readback { + if !self.diag.dma_readback_reported { + self.diag.dma_readback_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA opcode 0xe03 (buffer {:#x}) is not modeled", hdr[dma::HDR_DEST]); + } + } else if hdr[dma::HDR_CONTROL] & dma::TO_HOST != 0 { + self.dma_from_board(&bus, table, seq, &hdr); + } else if hdr[dma::HDR_DEST] & dma::SENTINEL != 0 { + if hdr[dma::HDR_DEST] == dma::SENTINEL_DRAW { + self.dma_draw_sentinel_short(&bus, table, seq, &hdr); + } else if self.dma_report(dma::REPORT_UPLOAD_SENTINEL) { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: DMA upload to sentinel {:#x} with word 4 {:#x} is not modeled", + hdr[dma::HDR_DEST], + hdr[dma::HDR_FORMAT] + ); + } + } else if self.dma_colour_base != 0 && hdr[dma::HDR_DEST] == self.dma_colour_base { + self.dma_to_buffer(&bus, table, seq, &hdr, None); + } else if hdr[dma::HDR_DEST] == 0 { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: DMA header at {:#x} names no destination buffer", table); + } else { + // The header's row length is the object's, as a select's. + let obj = self.object_select( + hdr[dma::HDR_DEST], + hdr[dma::HDR_WIDTH].wrapping_mul(dest::WIDTH_UNIT), + ); + + self.dma_to_buffer(&bus, table, seq, &hdr, Some(obj)); + } + if hdr[dma::HDR_CONTROL] & dma::IRQ_REQUEST != 0 { + self.dma_done(); + } + } +} + +// The raster-position query: the library sends 0x4c0 + kind with the same +// count, fence and sequence as the current-value query (kind 0); kind 3 is +// glRasterPos's update. After it completes the library copies the record at +// query base + 0x93c into the raster colour (RGBA context) or the one at +// + 0x928 (colour-index context), and the record at + 0x950 into the raster +// texture coordinate. +// +// The raster index, colour and texture coordinate are a latch, taken when +// the last block (0x3cfb) of glRasterPos's position stream arrives, and +// kind 3 answers from the latch, not from the colour current when the +// query is processed: the library defers the query past later glColor +// calls. That the +// board latches, and at 0x3cfb, is INFERRED; the library's reliance on it +// is ESTABLISHED. Before any latch the records hold the GL initial values +// (colour white, index 1). Kind 0 answers the live records. +// +// APPROXIMATION: the lit case is UNKNOWN. The library computes a lit raster +// position on the host and sends the board nothing to light, so the latch +// takes the current colour unlit, and the texture coordinate untransformed. +// Kinds 1 and 2 are not modeled. + +pub(super) static QUERY_OFFSET: [u32; 5] = [0x900, 0x914, 0x928, 0x950, 0x978]; + +/// Public initial values: index 1, colour white, normal +Z, edge true. +pub(super) static QUERY_DEFAULTS: [[u32; 5]; 5] = [ + [gl_cmd::COLOR_INDEX, 1, 0, 0, 0], + [gl_cmd::COLOR_3F, ODSY_F32_ONE, ODSY_F32_ONE, ODSY_F32_ONE, 0], + [gl_cmd::NORMAL_3F, 0, 0, ODSY_F32_ONE, 0], + [gl_cmd::TEXCOORD_2F, 0, 0, 0, 0], + [0x2f00c02e, 0, 0, 0, 0], +]; + +impl Board { + pub fn query_records_reset(&mut self) { + self.query.current = QUERY_DEFAULTS; + self.query.current_valid = 0x1f; + self.query.latched = QUERY_DEFAULTS; + self.query.latched_valid = 0x1f; + } + + /// The raster position's stream has arrived: keep the current index, + /// colour and texture coordinate for the kind-3 query. + pub fn query_raster_latch(&mut self) { + for rec in [ODSY_QUERY_INDEX, ODSY_QUERY_COLOUR, ODSY_QUERY_TEXCOORD] { + self.query.latched[rec as usize] = self.query.current[rec as usize]; + let bit = 1u32 << rec; + self.query.latched_valid = (self.query.latched_valid & !bit) | (self.query.current_valid & bit); + } + } + + pub fn query_cancel(&mut self) { + if self.query.stage != 0 { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: interrupted current-query sequence"); + } + self.query.epoch += 1; + self.query.stage = 0; + self.query.count = 0; + self.query.sync_index = 0; + self.query.kind = 0; + self.query.base = 0; + self.query.sync_base = 0; + self.query.captured_epoch = 0; + self.query.captured_valid = 0; + self.query.captured = [[0; 5]; 5]; + self.cmd.state = ODSY_CMD_IDLE; + self.cmd.tag = 0; + self.cmd.operand_count = 0; + self.cmd.operand_index = 0; + self.cmd.operand = [0; ODSY_CMD_OPERANDS_MAX as usize]; + } + + /// Retain a completed setter in its public record: the whole command word + /// and its raw operands, never a converted value. The 0xce80 twins of the + /// index and colour words set the same records as their 0xc580 siblings (a + /// model choice). A byte colour operand above 255 has no defined answer: the + /// record is retained for the context save but makes queries fail + /// observably until the next colour setter. The current texture record + /// admits the captured two- and four-float setters; other formats stay + /// unsupported. + pub fn query_update(&mut self, tag: u32) { + let record: u32; + let mut supported = true; + + match tag { + gl_cmd::COLOR_INDEX | gl_cmd::REPLAY_INDEX | gl_cmd::COLOR_INDEX_F | gl_cmd::REPLAY_INDEX_F => { + record = ODSY_QUERY_INDEX; + } + gl_cmd::COLOR_3UB | mode::INDEX_ZEROS => { + record = ODSY_QUERY_COLOUR; + supported &= self.cmd.operand[..3].iter().all(|&c| c <= 0xff); + } + gl_cmd::COLOR_4UB => { + record = ODSY_QUERY_COLOUR; + supported &= self.cmd.operand[..4].iter().all(|&c| c <= 0xff); + } + gl_cmd::COLOR_3I + | gl_cmd::INDEX_MODE_COLOR_3I + | gl_cmd::COLOR_3F + | gl_cmd::INDEX_MODE_COLOR_3F + | gl_cmd::COLOR_4F + | gl_cmd::INDEX_MODE_COLOR_4F + | gl_cmd::COLOR_3US + | gl_cmd::COLOR_3UI + | gl_cmd::COLOR_3B + | gl_cmd::COLOR_3S + | gl_cmd::COLOR_4US + | gl_cmd::COLOR_4UI + | gl_cmd::COLOR_4B + | gl_cmd::COLOR_4S + | gl_cmd::COLOR_4I => { + record = ODSY_QUERY_COLOUR; + } + gl_cmd::NORMAL_3B | gl_cmd::NORMAL_3S | gl_cmd::NORMAL_3I | gl_cmd::NORMAL_3F => { + record = ODSY_QUERY_NORMAL; + } + gl_cmd::TEXCOORD_1F | gl_cmd::TEXCOORD_2F | gl_cmd::TEXCOORD_3F | gl_cmd::TEXCOORD_4F => { + record = ODSY_QUERY_TEXCOORD; + } + 0x2f00c02e | 0x3000c02e => { + record = ODSY_QUERY_EDGE; + } + // Kept raw like the float forms: the kernel replays the record + // through the command FIFO at a context restore. + gl_cmd::TEXCOORD_1I | gl_cmd::TEXCOORD_2I | gl_cmd::TEXCOORD_3I | gl_cmd::TEXCOORD_4I => { + record = ODSY_QUERY_TEXCOORD; + } + _ => { + if (tag & gl_cmd::TEXCOORD_SHAPE) != gl_cmd::TEXCOORD_TAG { + return; + } + record = ODSY_QUERY_TEXCOORD; + supported = false; + } + } + let rec = record as usize; + self.query.current_valid &= !(1u32 << record); + if record != ODSY_QUERY_TEXCOORD || supported { + // Every word mapped to a record above has at most four operands. + let arity = cmd_arity(tag) as usize; + assert!(arity < self.query.current[rec].len()); + self.query.current[rec] = [0; 5]; + self.query.current[rec][0] = tag; + for k in 0..arity { + self.query.current[rec][1 + k] = self.cmd.operand[k]; + } + } + if supported { + self.query.current_valid |= 1u32 << record; + } else { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: unsupported current-query record {} tag {:#010x}", record, tag); + } + } +} + +impl DmaBus { + /// A query's completion words must not report failure after changing guest + /// memory. Admit only a whole aligned scalar, big-endian. Guest memory is + /// written through the bus. + pub fn publish_word(&self, addr: u64, value: u32, size: u32) -> Result<(), DmaError> { + let size64 = size as u64; + + if (addr & (size64 - 1)) != 0 || addr > u64::MAX - size64 { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: invalid publication address {:#x}", addr); + return Err(DmaError::Bounds); + } + let written = if size == 2 { + write_guest(&self.mem, addr, &(value as u16).to_be_bytes()) + } else { + write_guest(&self.mem, addr, &value.to_be_bytes()) + }; + if !written { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: publication at {:#x} is not aligned writable RAM", addr); + return Err(DmaError::Bus); + } + Ok(()) + } + + pub fn publish_be16(&self, addr: u64, value: u16) -> Result<(), DmaError> { + self.publish_word(addr, value as u32, 2) + } + + pub fn publish_be32(&self, addr: u64, value: u32) -> Result<(), DmaError> { + self.publish_word(addr, value, 4) + } +} + +impl Board { + pub fn query_begin(&mut self) { + self.query.stage = 1; + self.query.base = (((self.widget.ctx_shadow_addr_upper & 0xffff) as u64) << 32) | self.widget.ctx_shadow_addr_lower as u64; + self.query.sync_base = (((self.widget.host_write_addr_upper & 0xffff) as u64) << 32) | self.widget.host_write_addr_lower as u64; + self.query.captured_epoch = self.query.epoch; + self.query.captured_valid = self.query.current_valid; + self.query.captured = self.query.current; + } + + /// The query's last word: publish the snapshot query_begin() took, + /// its counter and its sync word. The sync word completes like a fence, + /// after the raster work before it, so the publication goes to the + /// raster unit as a job behind that work and the front end goes on. It + /// waited for the raster queue to empty instead, which in perfly held + /// the front end up for most of a frame's drawing once a frame. + pub fn query_finish(&mut self, sequence: u32) { + let sync_offset: u64 = 2 * (self.query.sync_index & dma::FENCE_SLOT_MASK) as u64; + let raster = self.query.kind == query::KIND_RASTER; + // Kind 3 reads the raster records and the texture coordinate from the + // latch; the rest of the page is the live snapshot. + let mut page = self.query.captured; + let mut page_valid = self.query.captured_valid; + + self.query.stage = 0; + if raster { + let tex = ODSY_QUERY_TEXCOORD as usize; + + page[tex] = self.query.latched[tex]; + page_valid = (page_valid & !(1u32 << tex)) | (self.query.latched_valid & (1u32 << tex)); + } + for i in 0..QUERY_OFFSET.len() { + if page_valid & (1u32 << i) == 0 { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: unsupported current-query record {} in snapshot", i); + } + } + if raster && self.query.latched_valid & 0b1011 != 0b1011 { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: unsupported raster-position latch"); + return; + } + if self.query.base == 0 + || self.query.base > u64::MAX - 0x980 + || self.query.captured_epoch != self.query.epoch + || page_valid != 0x1f + { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: unsupported current-query snapshot"); + return; + } + // Each word in publication order, with what a failed write reports. + let mut words: Vec<(u64, u32, &'static str)> = Vec::new(); + for (record, &offset) in page.iter().zip(QUERY_OFFSET.iter()) { + let n = 1 + cmd_arity(record[0]) as usize; + + for (j, &word) in record[..n].iter().enumerate() { + let addr = self.query.base + offset as u64 + 4 * j as u64; + words.push((addr, word, "sgi-odyssey: current-query payload failed")); + } + } + let mut i = 0; + while self.query.kind == query::KIND_RASTER && i < 2 { + let rec = self.query.latched[if i != 0 { ODSY_QUERY_COLOUR } else { ODSY_QUERY_INDEX } as usize]; + let at = self.query.base + (if i != 0 { query::RASTER_COLOUR } else { query::RASTER_INDEX }) as u64; + + for (j, &word) in rec.iter().enumerate().take(1 + cmd_arity(rec[0]) as usize) { + words.push((at + 4 * j as u64, word, "sgi-odyssey: raster query payload failed")); + } + i += 1; + } + words.push((self.query.base + 0x97c, self.query.count, "sgi-odyssey: current-query counter failed")); + let sync = (self.query.sync_base <= u64::MAX - sync_offset).then(|| self.query.sync_base + sync_offset); + let softsync = self.query.sync_index & dma::FENCE_INTERRUPT != 0; + let bus = DmaBus { + mem: self.guest.clone(), + trace_sequence: self.trace_sequence.load(Ordering::Relaxed).wrapping_sub(1), + }; + let front = self.front.clone(); + + self.raster.submit_completion(move |_| { + for (addr, word, failed) in words { + if bus.publish_be32(addr, word).is_err() { + crate::dlog!(LogModule::Odyssey, "{}", failed); + return; + } + } + if sync.is_none_or(|at| bus.publish_be16(at, (sequence & 0xffff) as u16).is_err()) { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: current-query sync failed"); + return; + } + if softsync { + front.lock().deliver_softsync(); + } + }); + } +} + +/// Match the shared rasterizer's eight-bit channel conversion. +pub(super) fn float_channel(bits: u32) -> u32 { + let c = f64::from(odyssey_f32(bits)); + + if !(c > 0.0) { + return 0; + } + if c >= 1.0 { + return 255; + } + (c * 255.0 + 0.5) as u32 +} + +/// The GL library's conversion of a signed-integer colour or normal operand +/// to a float, in single precision as its query decoder does. The exact +/// scales are 1/255, 1/65535 and 1/(2^32 - 2304); ODSY_F32_SCALE_32 is the +/// single-precision quotient of the last, exactly 1/(2^32 - 2560). Formats +/// 22 and 23 form 2v + 1 in 32-bit integer arithmetic before converting; 13 +/// and 24 convert v first, then form v * 2 + 1 with a fused multiply-add. +/// This is the software's expectation for what the current value means; the +/// board's own arithmetic is unknown. +pub(super) fn signed_to_float(word: u32, format: u32) -> u32 { + let scale_bits: u32; + let mut f: f32; + + match format { + 22 => { + scale_bits = ODSY_F32_SCALE_8; + f = word.wrapping_mul(2).wrapping_add(1) as i32 as f32; + } + 23 => { + scale_bits = ODSY_F32_SCALE_16; + f = word.wrapping_mul(2).wrapping_add(1) as i32 as f32; + } + _ => { + scale_bits = ODSY_F32_SCALE_32; + f = (word as i32 as f32).mul_add(2.0, 1.0); + } + } + f *= f32::from_bits(scale_bits); + f.to_bits() +} + +/// One component of an integer colour word as float bits. The operand is +/// taken to its type's width and converted by OpenGL's rule, c / (2^n - 1) +/// for an unsigned type and (2c + 1) / (2^n - 1) for a signed one; the int +/// type keeps glColor3i's existing conversion. INFERRED: that the board +/// converts by OpenGL's rule; the library sends the raw components. +pub(super) fn integer_colour(tag: u32, word: u32) -> u32 { + let mut code = tag & gl_cmd::COLOR_TYPE_MASK; + + code = code.wrapping_sub(if code >= gl_cmd::COLOR_TYPE_4 { gl_cmd::COLOR_TYPE_4 } else { gl_cmd::COLOR_TYPE_3 }); + let c: f64 = match code { + 0 => (word as u8) as f64 / 255.0, + 1 => (word as u16) as f64 / 65535.0, + 2 => word as f64 / 4294967295.0, + 3 => (2.0 * ((word as u8 as i8) as f64) + 1.0) / 255.0, + 4 => (2.0 * ((word as u16 as i16) as f64) + 1.0) / 65535.0, + _ => return signed_to_float(word, gl_cmd::COLOR_3I & gl_cmd::COLOR_TYPE_MASK), + }; + f32_bits(c as f32) +} + +/// True for the integer colour words other than glColor3ub and glColor3i. +pub(super) fn is_integer_colour(word: u32) -> bool { + matches!( + word, + gl_cmd::COLOR_3US + | gl_cmd::COLOR_3UI + | gl_cmd::COLOR_3B + | gl_cmd::COLOR_3S + | gl_cmd::COLOR_4UB + | gl_cmd::COLOR_4US + | gl_cmd::COLOR_4UI + | gl_cmd::COLOR_4B + | gl_cmd::COLOR_4S + | gl_cmd::COLOR_4I + ) +} + +impl Board { + /// True when base lies within the board memory the active product reports + /// (the "memory-size" property, in megabytes), false and reported, once per + /// reset, otherwise. A destination select's base is in 16-byte units, so the + /// capacity is converted into that unit first. A guest that addresses past + /// the reported size is a guest bug, kept observable. + pub fn board_address_valid(&mut self, base: u32) -> bool { + let limit: u64 = (self.widget.memory_size as u64) * (1u64 << 20) / 16; + + if (base as u64) < limit { + return true; + } + if !self.diag.board_oob_reported { + self.diag.board_oob_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: destination base {:#x} is beyond the {} MiB this board reports", base, self.widget.memory_size); + } + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: board memory out of bounds: destination base {:#x} exceeds the {} MiB this board reports", base, self.widget.memory_size); + false + } + + /// Build the next mipmap level from the level above by taking every second + /// texel (a model policy), for the one construction template the model has + /// seen. Sampler word 4 must be the builder's (texture_mip_build_word()) + /// and the source level's entry must name a listed format index with its + /// own size code, with no other bits but the row; the class is not + /// checked against the index. The store keeps + /// whole texel words whatever the level's format, so the copy is the same. + pub fn texture_build_level(&mut self) -> bool { + let p = self.cmd.operand; + let entry = [self.tex.levels[60 * 3], self.tex.levels[60 * 3 + 1], self.tex.levels[60 * 3 + 2]]; + let ws = self.tex.sampler[1]; + let hs = self.tex.sampler[2]; + + if !texture_mip_build_word(self.tex.sampler[4]) || texture_level_texel_format(entry[1]).is_none() { + return false; + } + if ws == 0 || hs == 0 || ws > i32::MAX as u32 || hs > i32::MAX as u32 || !(ws as u64).is_power_of_two() || !(hs as u64).is_power_of_two() { + return false; + } + let wd = (ws / 2).max(1); + let hd = (hs / 2).max(1); + let source_units = (ws.div_ceil(16) + 1) & !1u32; + let dest_units = (wd.div_ceil(16) + 1) & !1u32; + if self.stream.primitive != gl_cmd::QUADS + || (self.tex.mode & tex::MODE_2D) == 0 + || self.tex.level0 != 60 + || (entry[1] & !LEVEL_FORMAT_MASK) != source_units + || entry[2] != 0 + || self.tex.sampler[0] != ((ws.trailing_zeros() << 10) | (hs.trailing_zeros() << 5)) + || self.tex.sampler[3] != 0 + || self.tex.sampler[5] != 0 + || self.tex.sampler[6] != 0 + || p[0] != 0 + || p[1] != 0 + || p[2] != 0 + || p[3] != 0 + || p[4] != 0 + || p[5] != 0 + || p[6] != 0 + || p[7] != f32_bits(wd as f32) + || p[8] != f32_bits(hd as f32) + || p[9] != 0 + || p[10] != 0 + || p[11] != 0 + || p[12] != f32_bits(1.0f32 - 0.5f32 / wd as f32) + || p[13] != f32_bits(1.0f32 - 0.5f32 / hd as f32) + { + return false; + } + let source_base = entry[0] & 0x1fffffff; + if source_base == 0 || source_base == self.dest_base || self.objects.count > ODSY_OBJECTS { + return false; + } + let (Some(su), Some(du)) = (self.object_find(source_base), self.image_object()) else { + return false; + }; + if self.objects.width[su] != source_units * 16 || self.objects.rows[su] < hs || self.objects.width[du] != dest_units * 16 { + return false; + } + let source_end = ((hs - 1) as u64) + .checked_mul(self.objects.width[su] as u64) + .and_then(|v| v.checked_add(self.objects.offset[su] as u64)) + .and_then(|v| v.checked_add(ws as u64)); + let Some(source_end) = source_end else { return false }; + if source_end > self.objects.used as u64 || source_end > self.objects.mem.size / 4 || !self.object_rows(du, hd) { + return false; + } + for y in 0..hd { + for x in 0..wd { + let v = self.object_load(su, 2 * x, 2 * y); + self.object_store(du, x, y, v); + } + } + true + } + + /// One rectangle of the library's texture move drawn as a pixel zoom: + /// fourteen floats, a constant colour (0, 0, 0, 1), the destination + /// corners (x0, ya, z) and (x1, yb, z), then the source corners (s0, t0) + /// and (s1, t1). The mip-level builder (texture_build_level()) sends + /// the same word with an alpha of 0, so a word that is not one of those + /// builds is a zoom rectangle when the colour is exactly (0, 0, 0, 1). + /// + /// The library sends the move as BEGIN of type 7 (quads), one such word per + /// source row (per whole destination row under a y zoom), and END, after + /// it has set the sampler, the level-60 descriptor and a stored raster + /// state, all of which arrive as ordinary commands. ESTABLISHED: the word + /// layout and the begin and end around it. INFERRED: the first four + /// floats are a constant colour and the repeated z gives both corners' + /// depth. UNKNOWN: the count-14 word's fields, and the magnification + /// filter, since the move sends no filter word. + /// + /// The rectangle goes down the ordinary textured vertex path as four + /// float vertices of a quad, so it takes the sampler, raster state and + /// clip the guest sent. Returns whether the word was taken as a zoom + /// rectangle. The colour words are not applied: with a replacing texture + /// environment they do not reach the pixel, and the environment is the + /// stored raster state's to say. + pub fn texture_zoom_rect(&mut self) -> bool { + let p = self.cmd.operand; + + if p[..3] != [0; 3] || p[3] != ODSY_F32_ONE || self.stream.primitive != gl_cmd::QUADS || !self.prim.begin_open { + return false; + } + let (tag, operands) = (self.cmd.tag, self.cmd.operand); + let (x0, ya, z, x1, yb) = (p[4], p[5], p[6], p[7], p[8]); + let (s0, t0, s1, t1) = (p[10], p[11], p[12], p[13]); + + // The corners are window pixels, so the combined matrix is the + // identity for the rectangle and the guest's comes back after. + let identity = [1f32, 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]; + + self.gl_load_combined(&identity); + self.tex.coord_r = 0; + self.tex.coord_q = ODSY_F32_ONE; + for (x, y, s, t) in [(x0, ya, s0, t0), (x1, ya, s1, t0), (x1, yb, s1, t1), (x0, yb, s0, t1)] { + self.tex.coord = [s, t]; + self.cmd.operand[..4].copy_from_slice(&[x, y, z, 0]); + self.cmd.tag = gl_cmd::VERTEX_3F; + self.cmd_dispatch(gl_cmd::VERTEX_3F); + } + self.gl_reload_combined(); + self.cmd.tag = tag; + self.cmd.operand = operands; + true + } +} diff --git a/src/dev/odyssey/draw.rs b/src/dev/odyssey/draw.rs new file mode 100644 index 000000000..b84e6dedb --- /dev/null +++ b/src/dev/odyssey/draw.rs @@ -0,0 +1,1269 @@ +//! The board's 2D drawing: colour-buffer access, clipping and id-plane tests, +//! integer pixel paints, rectangle fill and copy, and pixel bursts. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + + +use super::bits::{be16, be32}; +use crate::devlog::LogModule; +use super::board::{Board, ODSY_CLIP_RECTS, ODSY_HEIGHT, ODSY_WIDTH}; +use super::consts::{ + clip, cmd, dest, dma, image, mode, pattern, write, ODSY_AUX_ID_BUFFER, ODSY_AUX_OVERLAY_BUFFER, + ODSY_BUFFERS, ODSY_BURST_STENCIL_SHIFT, ODSY_COPY_RECT_DEST_SHIFT, + ODSY_COPY_RECT_FORMAT_SOURCE_SHIFT, ODSY_COPY_RECT_SOURCE_SHIFT, ODSY_DEPTH24_MAX, + ODSY_DRAW_MASK_BUFFER0, ODSY_DRAW_MASK_BUFFER1, ODSY_FENCE_WORD_MASK, ODSY_ID_COMPARE_ENABLE, + ODSY_ID_COMPARE_MASK_SHIFT, ODSY_INDEX_MASK, ODSY_PLANE_MASK_SHIFT, +}; +use super::gl_context::GL_CLEAR; +use super::memory::{logic_op, plane_bpp, plane_bytes_per_pixel, BoardMemory, ClipRect, LOGIC_COPY, Plane}; +use super::objects::CopyOperands; +use super::pixel_dma::write_guest; +use super::pixel_write::{write_form, WriteForm}; +use super::raster::{LineRule, RasterRules}; +use super::scanout::DirtySite; +/// The clip-rectangle runs of one row (Board::row_runs). +#[derive(Clone, Copy)] +pub(super) struct RowRuns { + pub n: u32, + pub start: [i64; ODSY_CLIP_RECTS as usize], + pub end: [i64; ODSY_CLIP_RECTS as usize], +} + +impl Default for RowRuns { + fn default() -> Self { + RowRuns { n: 0, start: [0; ODSY_CLIP_RECTS as usize], end: [0; ODSY_CLIP_RECTS as usize] } + } +} + +/// 2D fills carry no alpha channel in this grammar; the framebuffer's fourth +/// byte is modelled fully opaque (the colour plane's red-high R,G,B,A packing, +/// resolved by resolve_row() below). +pub(super) fn pack_pixel(r: u32, g: u32, b: u32) -> u32 { + (r << 24) | (g << 16) | (b << 8) | 0xff +} + +/// The 0x840 ownership test at an in-plane pixel, against id plane `did` +/// and the 0x840 word `id_compare`. +pub(super) fn id_compare_passes(did: &[u8], id_compare: u32, x: i64, y: i64) -> bool { + let mask = (id_compare >> ODSY_ID_COMPARE_MASK_SHIFT) as u8; + + id_compare & ODSY_ID_COMPARE_ENABLE == 0 + || ((did[(y * ODSY_WIDTH as i64 + x) as usize] ^ id_compare as u8) & mask) == 0 +} + +impl Board { + /// Colour buffer b's board memory and plane (set up by Board::allocate()), b being 0 or + /// 1 in the displayed-buffer bits' sense (reg::DRAW_MASK). + /// + /// The planes are the raster unit's: the accessors here and below wait + /// for its queued jobs before the front end reads or writes them + /// (raster_unit.rs). + pub fn buffer_mem(&self, b: u32) -> &BoardMemory { + self.raster_wait(); + if b != 0 { &self.planes.buffer1 } else { &self.planes.colour } + } + + pub fn buffer_mem_mut(&mut self, b: u32) -> &mut BoardMemory { + self.raster_wait(); + if b != 0 { &mut self.planes.buffer1 } else { &mut self.planes.colour } + } + + pub fn buffer_plane(&self, b: u32) -> Option { + if b != 0 { + self.planes.buffer1.plane(self.planes.buffer1_plane) + } else { + self.planes.colour.plane(self.planes.colour_plane) + } + } + + pub fn buffer_load(&self, b: u32, x: i64, y: i64) -> u32 { + match self.buffer_plane(b) { + Some(p) => self.buffer_mem(b).plane_load(&p, x as i32, y as i32), + None => 0, + } + } + + pub fn buffer_store(&mut self, b: u32, x: i64, y: i64, word: u32) { + if let Some(p) = self.buffer_plane(b) { + self.buffer_mem_mut(b).plane_store(&p, x as i32, y as i32, word); + } + } + + /// Whether the last 0x1017 select named the id-plane destination, whatever + /// dest::FLAGS it carried. + pub fn dest_is_id_planes(&self) -> bool { + (self.dest_buffer_word & !dest::FLAGS) == (dest::ID_PLANES & !dest::FLAGS) + } + + /// The buffers a colour write lands in, bit b for buffer b, from the last + /// 0x0424 drawing mask. A right-eye bit folds into its buffer: stereo is not + /// modelled. + pub fn draw_buffers(&self) -> u32 { + (if self.draw_mask & ODSY_DRAW_MASK_BUFFER0 != 0 { 1 } else { 0 }) + | (if self.draw_mask & ODSY_DRAW_MASK_BUFFER1 != 0 { 2 } else { 0 }) + } + + /// The one completion fence, shared by the 0x0a5a host-write command and + /// the DMA engine's publish(): write the cookie's 16-bit token, + /// big-endian, to the guest fence slot at the host-write base plus twice the + /// slot, and pulse the softsync interrupt when the cookie asks. + /// + /// The raster unit writes it, behind the raster jobs queued before it, so + /// the token lands only once the drawing before it is in the planes. + pub fn fence(&mut self, cookie: u64) { + let word = (cookie & ODSY_FENCE_WORD_MASK as u64) as u32; + let slot = word & dma::FENCE_SLOT_MASK; + let token = (word >> dma::FENCE_TOKEN_SHIFT) & 0xffff; + let base = (((self.widget.host_write_addr_upper & 0xffff) as u64) << 32) | self.widget.host_write_addr_lower as u64; + let (guest, front) = (self.guest.clone(), self.front.clone()); + + self.raster.submit_completion(move |_| { + write_guest(&guest, base.wrapping_add(2 * slot as u64), &(token as u16).to_be_bytes()); + if word & dma::FENCE_INTERRUPT != 0 { + front.lock().deliver_softsync(); + } + }); + } +} + +impl Board { + /// Rebuild the kept (non-empty) clip rectangles from the raw 0x204b words. + pub fn clip_update(&mut self) { + self.win.clip_kept = 0; + for i in 0..ODSY_CLIP_RECTS as usize { + let lo = self.win.clip_words[2 * i]; + let hi = self.win.clip_words[2 * i + 1]; + let r = ClipRect { + x0: ((lo >> clip::X_SHIFT) & clip::FIELD_MASK) as i32, + y0: (lo & clip::FIELD_MASK) as i32, + x1: ((hi >> clip::X_SHIFT) & clip::FIELD_MASK) as i32, + y1: (hi & clip::FIELD_MASK) as i32, + }; + + if r.x0 <= r.x1 && r.y0 <= r.y1 { + self.win.clip_rect[self.win.clip_kept as usize] = r; + self.win.clip_kept += 1; + } + } + } + + /// Whether clip_words is the off-screen form (cmd::WINDOW_RESTORE). + pub fn clip_is_offscreen(&self) -> bool { + for (i, &word) in self.win.clip_words.iter().enumerate().take((2 * ODSY_CLIP_RECTS) as usize) { + let want = if i == 1 { clip::OFFSCREEN_HI } else { 0 }; + + if (word & clip::WORD_MASK) != want { + return false; + } + } + true + } + + /// The plane bound. The plane itself drops an out-of-plane store, but the + /// board checks first so the drawing code never has to guess whose error + /// it is. Board::pixel_allowed() adds the window tests for every write except a + /// copy destination, which calls this helper directly. + pub fn pixel_in_bounds(&self, x: i64, y: i64) -> bool { + x >= 0 && y >= 0 && x < ODSY_WIDTH as i64 && y < ODSY_HEIGHT as i64 + } + + pub fn id_compare_active(&self) -> bool { + self.win.id_compare & ODSY_ID_COMPARE_ENABLE != 0 + } + + /// The 0x840 ownership test at an in-plane pixel. + pub fn id_compare_passes(&self, x: i64, y: i64) -> bool { + self.raster_wait(); + id_compare_passes(&self.planes.did, self.win.id_compare, x, y) + } + + /// The one pixel test every write but a copy destination passes: the plane + /// bound and the 0x840 id test, then inside at least one kept clip rectangle + /// -- with none kept nothing is drawn. + pub fn pixel_allowed(&self, x: i64, y: i64) -> bool { + if !self.pixel_in_bounds(x, y) || !self.id_compare_passes(x, y) { + return false; + } + for r in &self.win.clip_rect[..self.win.clip_kept as usize] { + if x >= r.x0 as i64 && x <= r.x1 as i64 && y >= r.y0 as i64 && y <= r.y1 as i64 { + return true; + } + } + false + } + + /// The pixels of one row that Board::pixel_allowed() passes before its id + /// test: the plane bound and the union of the kept clip rectangles, as + /// ascending, disjoint, non-touching [start, end) runs. The 2D fills walk + /// these a row at a time; they still apply the id test per pixel. + pub fn row_runs(&self, y: i64) -> RowRuns { + let mut runs = RowRuns::default(); + let mut n = 0usize; + let mut kept = 0usize; + + if y < 0 || y >= ODSY_HEIGHT as i64 { + return runs; + } + for &r in &self.win.clip_rect[..self.win.clip_kept as usize] { + let a = (r.x0 as i64).max(0); + let b = (r.x1 as i64 + 1).min(ODSY_WIDTH as i64); + let mut j = n; + + n += 1; + if y < r.y0 as i64 || y > r.y1 as i64 || a >= b { + n -= 1; + continue; + } + // Insertion by start: there are at most four. + while j != 0 && runs.start[j - 1] > a { + runs.start[j] = runs.start[j - 1]; + runs.end[j] = runs.end[j - 1]; + j -= 1; + } + runs.start[j] = a; + runs.end[j] = b; + } + for i in 0..n { + if kept != 0 && runs.start[i] <= runs.end[kept - 1] { + runs.end[kept - 1] = runs.end[kept - 1].max(runs.end[i]); + } else { + runs.start[kept] = runs.start[i]; + runs.end[kept] = runs.end[i]; + kept += 1; + } + } + runs.n = kept as u32; + runs + } + + /// Where colour buffer b keeps the n pixels of row y from x, when all of + /// them lie in its plane and its memory: the plane, and the byte offset + /// and length of the run in Board::buffer_mem(), the same bytes + /// BoardMemory::plane_store() and BoardMemory::plane_load() reach one + /// pixel at a time. None otherwise, and the caller goes a pixel at a time. + pub fn buffer_row(&self, b: u32, x: i64, y: i64, n: i64) -> Option<(Plane, std::ops::Range)> { + let m = self.buffer_mem(b); + let p = self.buffer_plane(b)?; + let size = plane_bytes_per_pixel(p.format); + + if m.bytes.is_empty() || n <= 0 || x < 0 || y < 0 || x + n > p.width as i64 || y >= p.height as i64 { + return None; + } + let at = p.base.wrapping_add((y as u64).wrapping_mul(p.stride as u64)).wrapping_add((x as u64).wrapping_mul(size as u64)); + let len = (n as u64).wrapping_mul(size as u64); + if at > m.size || len > m.size - at { + return None; + } + Some((p, at as usize..(at + len) as usize)) + } + + /// A colour write, into each buffer the drawing mask selects. + /// + /// Marks the pixel's display line to be resolved again. + pub fn paint_pixel(&mut self, x: i64, y: i64, pixel: u32) { + let buffers = self.draw_buffers(); + + if !self.pixel_allowed(x, y) { + return; + } + for b in 0..ODSY_BUFFERS { + if buffers & (1u32 << b) != 0 { + self.buffer_store(b, x, y, pixel); + } + } + // A store from the front end, not a raster job: the display line is + // marked here, since no job records the row (see RasterQueue::submit()). + self.scanout.invalidate_for(DirtySite::Pixel, y as i32, 1); + } +} + +/// The plane word a fill stores: the packed RGB pixel of an RGB fill, or the +/// raw index of a colour-index fill. No colormap is applied here: scanout +/// resolves each pixel through its own drawable. +pub(super) fn fill_pixel(is_index: bool, value: u32) -> u32 { + if is_index { value & ODSY_INDEX_MASK } else { value } +} + +impl Board { + /// One pixel's id-plane write: the bits mask selects of the id byte when the + /// drawing mask selects buffer 0, and of the overlay pixel byte when it + /// selects buffer 1. + pub fn aux_store(&mut self, at: u64, buffers: u32, value: u8, mask: u8) { + let at_i = at as usize; + + self.raster_wait(); + + if buffers & (1u32 << ODSY_AUX_ID_BUFFER) != 0 { + self.planes.did[at_i] = (self.planes.did[at_i] & !mask) | (value & mask); + } + if buffers & (1u32 << ODSY_AUX_OVERLAY_BUFFER) != 0 { + self.planes.overlay[at_i] = (self.planes.overlay[at_i] & !mask) | (value & mask); + } + if buffers & ((1u32 << ODSY_AUX_ID_BUFFER) | (1u32 << ODSY_AUX_OVERLAY_BUFFER)) == 0 && !self.diag.aux_mask_reported { + self.diag.aux_mask_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: id-plane write with drawing mask {:#x} selects neither the id nor the overlay byte; dropped", + self.draw_mask + ); + } + } + + /// One pixel the X server draws at the id-plane destination: the index's low + /// byte combined through op with the byte it lands on -- the overlay byte when + /// the drawing mask selects buffer 1, else the id byte -- and stored under the + /// 0x1023 plane mask. + pub fn paint_aux_at(&mut self, at: u64, index: u32, op: u8) { + let buffers = self.draw_buffers(); + let mut byte = index as u8; + + self.raster_wait(); + if op as u32 != LOGIC_COPY { + let dst = if buffers & (1u32 << ODSY_AUX_OVERLAY_BUFFER) != 0 { self.planes.overlay[at as usize] } else { self.planes.did[at as usize] }; + + byte = logic_op(op, index, dst as u32) as u8; + } + self.aux_store(at, buffers, byte, (self.plane_write_mask >> ODSY_PLANE_MASK_SHIFT) as u8); + } + + pub fn paint_aux(&mut self, x: i64, y: i64, index: u32, op: u8) { + if self.pixel_allowed(x, y) { + self.paint_aux_at(y as u64 * ODSY_WIDTH as u64 + x as u64, index, op); + } + } + + /// One pixel of an integer primitive -- a QUADS rectangle, a pattern quad, a + /// line, point or polygon pixel -- combined with each selected buffer through + /// the logical operation 0x0422 names, when 0x0404 enables one for the + /// colour's kind: bit 19 for an index, bit 20 for an RGB colour. An index + /// result keeps the index bits; an RGB result keeps the fourth byte opaque. + /// Images and copies do not come through here: whether the board applies the + /// operation to them is unknown. + /// + /// At the id-plane destination the pixel goes to the id or overlay byte + /// instead, under the 0x1023 plane mask. Its logical operation reads the + /// overlay byte when buffer 1 is selected and the id byte otherwise. + pub fn paint_integer(&mut self, x: i64, y: i64, is_index: bool, value: u32) { + let src = fill_pixel(is_index, value); + let enable = if is_index { mode::INDEX_LOGIC_OP } else { mode::COLOR_LOGIC_OP }; + let op = self.gl_ref().cur.logic_op_mode.wrapping_sub(GL_CLEAR) as u8; + let buffers = self.draw_buffers(); + + if self.dest_is_id_planes() { + let use_op = (self.stream.pattern_mode & enable) != 0 && op as u32 != LOGIC_COPY; + + self.paint_aux(x, y, src, if use_op { op } else { LOGIC_COPY as u8 }); + self.scanout.invalidate_for(DirtySite::Integer, y as i32, 1); + return; + } + if (self.stream.pattern_mode & enable) == 0 || op as u32 == LOGIC_COPY { + self.paint_pixel(x, y, src); + return; + } + if !self.pixel_allowed(x, y) { + return; + } + for b in 0..ODSY_BUFFERS { + if buffers & (1u32 << b) != 0 { + let out = logic_op(op, src, self.buffer_load(b, x, y)); + + self.buffer_store(b, x, y, if is_index { out & ODSY_INDEX_MASK } else { out | 0xff }); + } + } + self.scanout.invalidate_for(DirtySite::Integer, y as i32, 1); + } + + /// Board::paint_integer() at each pixel of row y from x0 up to x1, given the + /// row's runs, a run of the buffer's bytes at a time. Each pixel is tested, + /// combined and stored exactly as the per-pixel path does it. Pixels are + /// independent, so going a buffer at a time rather than a pixel at a time + /// stores the same bytes. + pub fn paint_integer_span(&mut self, runs: &RowRuns, y: i64, x0: i64, x1: i64, is_index: bool, value: u32) { + let src = fill_pixel(is_index, value); + let enable = if is_index { mode::INDEX_LOGIC_OP } else { mode::COLOR_LOGIC_OP }; + let op = self.gl_ref().cur.logic_op_mode.wrapping_sub(GL_CLEAR) as u8; + let logic = (self.stream.pattern_mode & enable) != 0 && op as u32 != LOGIC_COPY; + let id_test = self.id_compare_active(); + let buffers = self.draw_buffers(); + let aux = self.dest_is_id_planes(); + + for r in 0..runs.n as usize { + let a = x0.max(runs.start[r]); + let e = x1.min(runs.end[r]); + + if a >= e { + continue; + } + if aux { + for x in a..e { + if !id_test || self.id_compare_passes(x, y) { + self.paint_aux_at( + y as u64 * ODSY_WIDTH as u64 + x as u64, + src, + if logic { op } else { LOGIC_COPY as u8 }, + ); + } + } + continue; + } + for b in 0..ODSY_BUFFERS { + let mut word = [0u8; 4]; + + if buffers & (1u32 << b) == 0 { + continue; + } + let Some((plane, run)) = self.buffer_row(b, a, y, e - a) else { + for x in a..e { + let mut out = src; + + if id_test && !self.id_compare_passes(x, y) { + continue; + } + if logic { + out = logic_op(op, src, self.buffer_load(b, x, y)); + out = if is_index { out & ODSY_INDEX_MASK } else { out | 0xff }; + } + self.buffer_store(b, x, y, out); + } + continue; + }; + let bpp = plane_bpp(plane.format) as usize; + let (did, id_compare) = (&self.planes.did, self.win.id_compare); + let mem = if b != 0 { &mut self.planes.buffer1 } else { &mut self.planes.colour }; + + plane.encode(src, &mut word); + for (x, px) in (a..e).zip(mem.bytes[run].chunks_exact_mut(bpp)) { + if id_test && !id_compare_passes(did, id_compare, x, y) { + continue; + } + if !logic { + px.copy_from_slice(&word[..bpp]); + continue; + } + let out = logic_op(op, src, plane.decode(px)); + plane.encode(if is_index { out & ODSY_INDEX_MASK } else { out | 0xff }, px); + } + } + } + } + + /// The solid rectangle fill: BEGIN(QUADS)/COLOR_3UB or COLOR_INDEX/four VERTEX_2I + /// corners/END drives the solid rectangle fill. Board::gl_end() routes a + /// QUADS fill here when pattern mode is off, and to Board::pattern_fill() + /// when it is on. is_index selects whether value is a packed RGB pixel or a + /// PseudoColor index resolved per pixel. + pub fn fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, is_index: bool, value: u32) { + for row in 0..h { + let runs = self.row_runs(y as i64 + row as i64); + self.paint_integer_span(&runs, y as i64 + row as i64, x as i64, x as i64 + w as i64, is_index, value); + } + self.scanout.invalidate_rows_for(DirtySite::FillRect, y, h); + } + + /// Board::clear_planes(): the X server's write to the id-plane destination. + /// For each pixel of the rectangle, replace the plane bits the 0x1023 mask + /// selects with the value the COLOR_INDEX word carried, in the id byte when + /// the drawing mask selects buffer 0 and in the overlay pixel byte when it + /// selects buffer 1, leaving the colour framebuffer untouched. + pub fn clear_planes(&mut self, x: u32, y: u32, w: u32, h: u32, value: u8, mask: u8) { + let buffers = self.draw_buffers(); + let id_test = self.id_compare_active(); + for row in 0..h { + let py = y as i64 + row as i64; + let runs = self.row_runs(py); + for r in 0..runs.n as usize { + let a = (x as i64).max(runs.start[r]); + let e = (x as i64 + w as i64).min(runs.end[r]); + + for px in a..e { + if !id_test || self.id_compare_passes(px, py) { + self.aux_store(py as u64 * ODSY_WIDTH as u64 + px as u64, buffers, value, mask); + } + } + } + } + // A pixel's drawable picks its colour at scanout. + self.scanout.invalidate_rows_for(DirtySite::ClearPlanes, y, h); + } + + /// The screen-to-screen blit (Board::copy_rect()), the console-scroll primitive. + /// Copies in whichever row/column direction keeps a read ahead of the matching + /// write, so an overlapping source and destination in the same framebuffer -- + /// the case a scroll relies on -- reads the old contents before they are + /// overwritten. + /// + /// Approximation: which buffer the board reads the source from is part of the + /// uncleared read selection, so the copy runs within each buffer the drawing + /// mask selects, source and destination in the same buffer. + /// + /// Approximation, inferred: a destination pixel passes Board::pixel_in_bounds() + /// only, not the window-clip rectangles Board::pixel_allowed() also tests. + pub fn copy_rect_buffer(&mut self, source: u32, dest: u32) { + let sx = self.copy_src & 0xffff; + let sy = (self.copy_src >> 16) & 0xffff; + let w = self.copy_size & 0xffff; + let h = (self.copy_size >> 16) & 0xffff; + let dx = self.copy_dst & 0xffff; + let (dy, h_clip) = self.copy_dest_rows(); + let w_clip = if dx < ODSY_WIDTH { w.min(ODSY_WIDTH - dx) } else { 0 }; + let reverse_rows = dy > sy; + let reverse_cols = dx > sx; + + for row in 0..h_clip { + let sr = if reverse_rows { h_clip - 1 - row } else { row }; + let src_row = if w_clip != 0 { + self.buffer_row(source, sx as i64, sy as i64 + sr as i64, w_clip as i64) + } else { + None + }; + let dst_row = if src_row.is_some() { + self.buffer_row(dest, dx as i64, dy as i64 + sr as i64, w_clip as i64) + } else { + None + }; + + // A row wholly inside both planes, in the same encoding, is the byte + // copy the pixel loop below makes. The loop runs its columns so that + // a read stays ahead of the write it could meet, which is what a + // memmove does for the one overlapping case, a row copied along + // itself. + if let (Some((src_plane, src)), Some((dst_plane, dst))) = (src_row, dst_row) { + if src_plane.format == dst_plane.format && src_plane.packing == dst_plane.packing { + if (source != 0) == (dest != 0) { + self.buffer_mem_mut(dest).bytes.copy_within(src, dst.start); + } else { + let (from, to) = if source != 0 { + (&self.planes.buffer1, &mut self.planes.colour) + } else { + (&self.planes.colour, &mut self.planes.buffer1) + }; + to.bytes[dst].copy_from_slice(&from.bytes[src]); + } + continue; + } + } + for col in 0..w_clip { + let sc = if reverse_cols { w_clip - 1 - col } else { col }; + // An out-of-plane source reads zero, as the guarded read did. + let pixel = self.buffer_load(source, sx.wrapping_add(sc) as i64, sy.wrapping_add(sr) as i64); + + if self.pixel_in_bounds(dx.wrapping_add(sc) as i64, dy.wrapping_add(sr) as i64) { + self.buffer_store(dest, dx.wrapping_add(sc) as i64, dy.wrapping_add(sr) as i64, pixel); + } + } + } + } + + /// The rectangle copy whose source and destination are both the id-plane + /// base the X server last selected: the id or overlay byte the drawing + /// mask names, under the 0x1023 plane mask, as every other write there. + /// The server draws a menu's stipple by copying a seed over itself with + /// these, so running them through the colour planes smeared whatever the + /// menu covered. + /// + /// INFERRED: that these copies belong to the overlay is read from a trace + /// of the words and the menus' appearance; no other source records it. + /// + /// Each row is read whole before it is written, and the rows run in the + /// direction that keeps a source row ahead of the writes, so an overlapping + /// copy reads the bytes it had before. Approximation, as for the colour + /// copy: a destination byte passes the plane bounds only, and a source + /// outside the plane reads as zero (model policy; the hardware behaviour is + /// unknown). + pub fn copy_rect_id_planes(&mut self) { + let (sx, sy) = (self.copy_src & 0xffff, (self.copy_src >> 16) & 0xffff); + let dx = self.copy_dst & 0xffff; + let (dy, h_clip) = self.copy_dest_rows(); + let w = self.copy_size & 0xffff; + let w_clip = w.min(ODSY_WIDTH.saturating_sub(dx)) as usize; + let rows = h_clip; + // The source pixels inside the plane on a row; the rest read as zero. + let src_w = w_clip.min(ODSY_WIDTH.saturating_sub(sx) as usize); + let buffers = self.draw_buffers(); + let mask = (self.plane_write_mask >> ODSY_PLANE_MASK_SHIFT) as u8; + let mut staged = vec![0u8; w_clip]; + + if buffers == 0 && !self.diag.aux_mask_reported { + self.diag.aux_mask_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: id-plane copy with drawing mask {:#x} selects neither the id nor the overlay byte; dropped", + self.draw_mask + ); + } + self.raster_wait(); + for row in 0..rows { + let r = if dy > sy { rows - 1 - row } else { row }; + let from = ((sy + r) * ODSY_WIDTH + sx) as usize; + let to = ((dy + r) * ODSY_WIDTH + dx) as usize; + + for b in [ODSY_AUX_ID_BUFFER, ODSY_AUX_OVERLAY_BUFFER] { + if buffers & (1u32 << b) == 0 { + continue; + } + let plane = if b == ODSY_AUX_ID_BUFFER { &mut self.planes.did } else { &mut self.planes.overlay }; + + staged.fill(0); + if sy + r < ODSY_HEIGHT { + staged[..src_w].copy_from_slice(&plane[from..from + src_w]); + } + for (d, s) in plane[to..to + w_clip].iter_mut().zip(&staged) { + *d = (*d & !mask) | (*s & mask); + } + } + } + } + + /// The first row and count of the rows a copy writes, clipped to the plane. + fn copy_dest_rows(&self) -> (u32, u32) { + let h = (self.copy_size >> 16) & 0xffff; + let dy = (self.copy_dst >> 16) & 0xffff; + + (dy, if dy < ODSY_HEIGHT { h.min(ODSY_HEIGHT - dy) } else { 0 }) + } + + pub fn copy_rect(&mut self) { + let source = (self.cmd.operand[2] >> ODSY_COPY_RECT_SOURCE_SHIFT) & 0xf; + let dest = (self.cmd.operand[7] >> ODSY_COPY_RECT_DEST_SHIFT) & 0xf; + let format_source = (self.cmd.operand[4] >> ODSY_COPY_RECT_FORMAT_SOURCE_SHIFT) & 0xf; + let format_dest = (self.cmd.operand[4] >> ODSY_COPY_RECT_DEST_SHIFT) & 0xf; + + let operands = CopyOperands::decode(&self.cmd.operand); + + if operands.src_base == dma::SENTINEL_READ || operands.dest_base == dma::SENTINEL_DRAW { + if !self.copy_rect_gl(&operands) { + self.cmd_unimplemented(cmd::COPY_RECT_TRIGGER); + } + return; + } + + if source != format_source || dest != format_dest { + self.cmd_unimplemented(cmd::COPY_RECT_TRIGGER); + return; + } + + // The two observed copy blocks select 0 then 4 in both their source and + // destination fields. This is an experimental model policy: the selector + // values choose model buffers 0 and 1, but do not identify physical + // memory banks. Source and destination choices stay separate so a copy + // never changes another colour buffer. + // dest_base is 0 until a select names a buffer, and the all-zero GL context + // selects leave it 0 while a copy's own base words can be 0 too: without + // the test two zero words would read as "both at the id-plane base". + let id_base = self.dest_base != 0 && self.dest_is_id_planes(); + let (src_at_id, dest_at_id) = (operands.src_base == self.dest_base, operands.dest_base == self.dest_base); + + if id_base && src_at_id != dest_at_id { + // Not traced: a copy between the overlay and a colour buffer. Running + // it through colour buffer 0 is the smear this path exists to avoid. + if !self.diag.copy_mixed_base_reported { + self.diag.copy_mixed_base_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: rectangle copy from base {:#x} to base {:#x} with the id-plane base {:#x} selected is not modeled; dropped", + operands.src_base, + operands.dest_base, + self.dest_base + ); + } + self.cmd_unimplemented(cmd::COPY_RECT_TRIGGER); + return; + } + if id_base && src_at_id && dest_at_id { + self.copy_rect_id_planes(); + } else if (source == 0 || source == 4) && (dest == 0 || dest == 4) { + self.copy_rect_buffer(source >> 2, dest >> 2); + } else if source == dest && (source == 2 || source == 3) { + // Existing public and modelled templates retain draw-mask behaviour. + let buffers = self.draw_buffers(); + + for b in 0..ODSY_BUFFERS { + if buffers & (1u32 << b) != 0 { + self.copy_rect_buffer(b, b); + } + } + } else { + self.cmd_unimplemented(cmd::COPY_RECT_TRIGGER); + return; + } + let (dy, rows) = self.copy_dest_rows(); + self.scanout.invalidate_rows_for(DirtySite::CopyRect, dy, rows); + } + + /// Pattern mode is on when the 0x0404 mode's bits 3,4 are set and a shape is + /// selected. A quad drawn in this state paints the current colour through the + /// pattern instead of as a solid rectangle. + pub fn pattern_enabled(&mut self) -> bool { + if (self.stream.pattern_mode & pattern::MODE_ENABLE) != pattern::MODE_ENABLE { + return false; + } + matches!( + self.stream.pattern_shape & !pattern::SHAPE_OPAQUE, + pattern::SHAPE_64X16 | pattern::SHAPE_32X32 | pattern::SHAPE_16X64 + ) + } + + /// The pattern bit at quad-relative column u, row v, for the current shape. + /// Row 0 column 0 is the quad's top-left vertex and bit 31 of a row's first + /// word is its leftmost pixel. A coordinate past the shape's own extent is + /// treated as clear: the X server never draws a quad larger than its pattern. + pub fn pattern_bit(&mut self, u: u32, v: u32) -> bool { + + let (word, bit) = match self.stream.pattern_shape & !pattern::SHAPE_OPAQUE { + pattern::SHAPE_64X16 => { + if u >= 64 || v >= 16 { + return false; + } + (self.stream.pattern_buffer[(2 * v + (u >> 5)) as usize], 31 - (u & 31)) + } + pattern::SHAPE_32X32 => { + if u >= 32 || v >= 32 { + return false; + } + (self.stream.pattern_buffer[v as usize], 31 - u) + } + pattern::SHAPE_16X64 => { + if u >= 16 || v >= 64 { + return false; + } + (self.stream.pattern_buffer[(v >> 1) as usize], 31 - 16 * (v & 1) - u) + } + _ => return false, + }; + (word >> bit) & 1 != 0 + } + + /// The opaque pattern's background: the 0x0c21 constant colour, an index from + /// operand 1 bits 23:12, or an RGB pixel from the 12-bit red, green and blue + /// fields narrowed to 8 bits. + pub fn pattern_background(&mut self, is_index: bool) -> u32 { + let r = (self.const_colour[0] >> 12) & 0xfff; + let g = self.const_colour[0] & 0xfff; + let b = (self.const_colour[1] >> 12) & 0xfff; + + if is_index { + return r; + } + pack_pixel(r >> 4, g >> 4, b >> 4) + } + + /// A pattern quad: paint the current colour at each pixel whose pattern bit is + /// set, and the background at each clear one when the pattern is opaque, + /// leaving the rest as it was otherwise. Only the rows and columns the quad + /// covers are read, so the stale part of the persistent pattern buffer beyond + /// the tile never contributes. + #[allow(clippy::too_many_arguments, reason = "a fill rectangle, its pattern origin and its colour")] + pub fn pattern_fill(&mut self, x: u32, y: u32, w: u32, h: u32, pat_u0: u32, pat_v0: u32, is_index: bool, value: u32) { + let opaque = self.stream.pattern_shape & pattern::SHAPE_OPAQUE != 0; + let background = self.pattern_background(is_index); + // Each row as spans of equal pattern bits, painted a span at a time. + for v in 0..h { + let py = y as i64 + v as i64; + + let runs = self.row_runs(py); + if runs.n == 0 { + continue; + } + let mut u = 0u32; + while u < w { + let bit = self.pattern_bit(pat_u0.wrapping_add(u), pat_v0.wrapping_add(v)); + let first = u; + + loop { + u += 1; + if !(u < w && self.pattern_bit(pat_u0.wrapping_add(u), pat_v0.wrapping_add(v)) == bit) { + break; + } + } + if bit || opaque { + self.paint_integer_span( + &runs, + py, + x as i64 + first as i64, + x as i64 + u as i64, + is_index, + if bit { value } else { background }, + ); + } + } + } + self.scanout.invalidate_rows_for(DirtySite::PatternFill, y, h); + } +} + +/// A raster-box coordinate arrives as an IEEE-754 single the X server built +/// with an integer-to-float convert, so the 32-bit operand is a float bit +/// pattern, not an integer. Truncate toward zero; a NaN or negative clamps to +/// the screen origin. +pub(super) fn float_to_u32(bits: u32) -> u32 { + let f = f32::from_bits(bits); + + if !(f > 0.0f32) { + return 0; + } + f as u32 +} + +impl Board { + + /// The object an image transfer writes, or None for the screen: the last + /// 0x1017 select, when it named a base other than the colour buffer's. + pub fn image_object(&mut self) -> Option { + if self.dest_base == 0 || self.dest_base == self.dma_colour_base || self.dest_is_id_planes() { + return None; + } + self.object_find(self.dest_base) + } + + /// Lay one image pixel, as 0xRRGGBBAA, at the raster cursor in the selected + /// buffer (Board::image_object()) and advance it. Each axis is a directed + /// half-open interval: increasing endpoints visit [first, last), while + /// decreasing endpoints visit [first - 1, last]. Payload advances along X, + /// then along Y. + pub fn image_put(&mut self, rgba: u32, is_index: bool, preserve_alpha: bool) { + let x_decreasing = self.image.x2 < self.image.x1; + let y_decreasing = self.image.y2 < self.image.y1; + let y_finished = if y_decreasing { + self.image.cur_y == u32::MAX || self.image.cur_y < self.image.y2 + } else { + self.image.cur_y >= self.image.y2 + }; + + if self.image.x2 == self.image.x1 || self.image.y2 == self.image.y1 || y_finished { + return; + } + if let Some(obj) = self.image_object() { + // An off-screen object: box coordinates, no window or id tests. + self.object_store(obj, self.image.cur_x, self.image.cur_y, rgba); + } else if self.dest_is_id_planes() { + // Menu cascade arrows: index images into the overlay. + if is_index { + self.paint_aux( + self.image.cur_x as i64 + self.win.origin[0] as i64, + self.image.cur_y as i64 + self.win.origin[1] as i64, + rgba, + LOGIC_COPY as u8, + ); + self.scanout.invalidate_for(DirtySite::Image, self.image.cur_y as i32 + self.win.origin[1], 1); + } else if !self.diag.aux_image_reported { + self.diag.aux_image_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: an RGB image to the id-plane destination is not modeled; dropped"); + } + } else { + // Screen pixels are window-relative; object pixels are not. + self.paint_pixel( + self.image.cur_x as i64 + self.win.origin[0] as i64, + self.image.cur_y as i64 + self.win.origin[1] as i64, + if is_index || preserve_alpha { rgba } else { (rgba & !0xffu32) | 0xff }, + ); + } + self.image_advance(x_decreasing, y_decreasing); + } + + /// Step the raster cursor to the next pixel of the open image. + fn image_advance(&mut self, x_decreasing: bool, y_decreasing: bool) { + if (x_decreasing && self.image.cur_x == self.image.x2) || (!x_decreasing && self.image.cur_x == self.image.x2.wrapping_sub(1)) { + self.image.cur_x = if x_decreasing { self.image.x1.wrapping_sub(1) } else { self.image.x1 }; + if y_decreasing { + self.image.cur_y = if self.image.cur_y == self.image.y2 { u32::MAX } else { self.image.cur_y.wrapping_sub(1) }; + } else { + self.image.cur_y = self.image.cur_y.wrapping_add(1); + } + } else if x_decreasing { + self.image.cur_x = self.image.cur_x.wrapping_sub(1); + } else { + self.image.cur_x = self.image.cur_x.wrapping_add(1); + } + } + + /// One byte-stream texel of the open image at the raster cursor, bounded by + /// the header's group length in pixels. A source format that carries + /// alpha stores it (code_carries_alpha()); any other stores 255. + pub fn image_texel(&mut self, rgba: u32, code: u32) { + if self.image.group_len != 0 && self.image.group_painted >= self.image.group_len { + return; + } + self.image_put(rgba, code_is_index(code), code_carries_alpha(code)); + self.image.group_painted += 1; + } + + /// The same for a pixel of the library's write table: `b` holds its bytes + /// first byte first. It stores the alpha the source carries. + pub fn image_texel_form(&mut self, form: &WriteForm, b: &[u8]) { + if self.image.group_len != 0 && self.image.group_painted >= self.image.group_len { + return; + } + self.image_put(form.decode(b), form.is_index(), form.has_alpha()); + self.image.group_painted += 1; + } +} + +/// Whether a GL image write under source format `code` stores the source's +/// alpha rather than 255. +/// +/// ESTABLISHED: the library sends +/// the source's alpha bytes unchanged under 0x4b, 0x48, 0x6b, 0x4c and +/// 0x6c, and no header bit asks the board to replace them. INFERRED: +/// the board stores that alpha. The write mask's alpha enable (block +/// 0x052) is not applied to image writes in this model. +/// +/// The X server's on-screen images under 0x4c come by DMA to the colour +/// buffer (Board::dma_put()), which still stores 255: they would, by inference, write the A byte, 0 in every traced upload, but the X server +/// passes client pixels through, and whether alpha is stored at all for a +/// window whose visual has none is unknown. The model keeps one plane for +/// every visual, so it cannot tell such a window apart. +pub(super) fn code_carries_alpha(code: u32) -> bool { + matches!( + code, + image::CODE_RGBA + | image::CODE_LUM_ALPHA + | image::CODE_ABGR + | image::CODE_RGBA12 + | image::CODE_ABGR12 + ) +} + +pub(super) fn unorm16_to_8(v: u32) -> u32 { + (v.wrapping_mul(255).wrapping_add(65535 / 2)) / 65535 +} + +/// Components of a 12-bit texture source code, or 0 for any other code. +pub(super) fn texel12_components(code: u32) -> u32 { + match code { + image::CODE_LUM12 => 1, + image::CODE_LUM_ALPHA12 => 2, + image::CODE_RGB12 => 3, + image::CODE_RGBA12 | image::CODE_ABGR12 => 4, + _ => 0, + } +} + +/// A 12-bit texture source pixel as 0xRRGGBBAA: its components, each a byte or +/// (wide) a big-endian 16-bit word, in the order its code names. +pub(super) fn decode_texel12(code: u32, b: &[u8], wide: bool) -> u32 { + let mut c = [0u32; 4]; + let n = texel12_components(code) as usize; + + for i in 0..n { + c[i] = if wide { unorm16_to_8(be16(&b[2 * i..][..2]) as u32) } else { b[i] as u32 }; + } + match code { + image::CODE_LUM12 => pack_pixel(c[0], c[0], c[0]), + image::CODE_LUM_ALPHA12 => (pack_pixel(c[0], c[0], c[0]) & !0xffu32) | c[1], + image::CODE_RGB12 => pack_pixel(c[0], c[1], c[2]), + image::CODE_ABGR12 => (pack_pixel(c[3], c[2], c[1]) & !0xffu32) | c[0], + _ => (pack_pixel(c[0], c[1], c[2]) & !0xffu32) | c[3], + } +} + +impl Board { + /// Decode one pixel of a byte-stream source format whose bytes are b[0 ..]: + /// its 0xRRGGBBAA word, or for 0x41 and 0x61 the raw index. None for a code + /// this model does not know. + pub fn decode_pixel(&mut self, code: u32, b: &[u8]) -> Option { + match code { + image::CODE_ABGR => Some((pack_pixel(b[3] as u32, b[2] as u32, b[1] as u32) & !0xffu32) | b[0] as u32), + image::CODE_FALLBACK_XRGB => Some(pack_pixel(b[1] as u32, b[2] as u32, b[3] as u32)), + image::CODE_RGBA | image::CODE_RGBA12 => Some(be32(&b[..4])), + image::CODE_RGB => Some(pack_pixel(b[0] as u32, b[1] as u32, b[2] as u32)), + image::CODE_LUM => Some(pack_pixel(b[0] as u32, b[0] as u32, b[0] as u32)), + image::CODE_LUM_ALPHA => Some((pack_pixel(b[0] as u32, b[0] as u32, b[0] as u32) & !0xffu32) | b[1] as u32), + image::CODE_LUM12 | image::CODE_LUM_ALPHA12 => { + // A burst's 16-bit components (pixel_bytes()). + Some(decode_texel12(code, b, true)) + } + image::CODE_CI => { + // The raw index: scanout resolves it through the pixel's drawable. + Some(b[0] as u32) + } + image::CODE_CI16 => Some(be16(&b[..2]) as u32 & ODSY_INDEX_MASK), + _ => None, + } + } +} + +/// Whether a source format carries colour indices rather than colours. +pub(super) fn code_is_index(code: u32) -> bool { + code == image::CODE_CI || code == image::CODE_CI16 +} + +/// Bytes a pixel of a source format takes, 0 for an unknown code. +pub(super) fn pixel_bytes(code: u32) -> u32 { + match code { + image::CODE_ABGR | image::CODE_FALLBACK_XRGB | image::CODE_RGBA | image::CODE_RGBA12 => 4, + image::CODE_RGB => 3, + image::CODE_LUM | image::CODE_CI => 1, + image::CODE_LUM_ALPHA | image::CODE_LUM12 | image::CODE_CI16 => 2, + image::CODE_LUM_ALPHA12 => 4, + _ => 0, + } +} + +pub(super) fn image_pairing_supported(code: u32, fmt: u32) -> bool { + if code == image::CODE_RGBA12 { + return fmt == image::FMT_RGB || fmt == image::FMT_RGBA16; + } + if code == image::CODE_RGB12 || code == image::CODE_ABGR12 { + return fmt == image::FMT_RGBA16; + } + // The library sends 8-bit luminance-alpha under the colour-index or the + // RGB tag, and its 16-bit luminance and luminance-alpha as 16-bit + // components under the RGBA16 tag as well as the other two (observed on + // 13x5 draws of the GL test suite). + if code == image::CODE_LUM_ALPHA { + return fmt != image::FMT_RGBA16; + } + if code == image::CODE_LUM12 || code == image::CODE_LUM_ALPHA12 { + return true; + } + fmt != image::FMT_RGBA16 +} + +impl Board { + /// Whether the open image's header and a burst's format tag go together: + /// image_pairing_supported() for the codes the byte-stream tables know, + /// and for any other code, whatever the write table (pixel_write.rs) + /// takes, under any tag. + pub fn image_burst_supported(&self, fmt: u32) -> bool { + image_pairing_supported(self.image.code, fmt) + || (pixel_bytes(self.image.code) == 0 && texel12_components(self.image.code) == 0 && write_form(self.image.write_code).is_some()) + } + + /// One word of a format-0, type-4 burst: a pixel of the depth and + /// stencil word, four bytes. + /// + /// ESTABLISHED: the + /// burst's operand carries no size field and no field codes, so the + /// header does not say which plane; the library selects the plane by + /// the write-mask block (0x052) it sends just before the burst. Its depth + /// download clears the stencil mask (bits 19:12) and keeps the depth-write + /// bit (24) as the application has it; its stencil download clears bit 24 + /// and leaves the stencil mask. INFERRED: the enables decide which part of + /// the stored pixel changes, so the model writes the depth plane's word + /// (bits 23:0) when bit 24 is set, and the stencil plane's bits 19:12 of + /// block 0x052 select as `(data & mask) | (old & !mask)`. ESTABLISHED: + /// the library packs + /// the stencil index at bits 23:16 with no mask (index 0x11 is + /// 0x0011_0000) and the burst copies the word, so that byte is the data; + /// the 8-bit mask in bits 19:12 of block 0x052 applies to it, not to the + /// word as sent. UNKNOWN: how the board aligns the two, and where it + /// keeps the index inside a stored pixel. UNKNOWN: whether + /// the board runs the burst through the depth and stencil tests, and the + /// effect of their enables on it; the model applies neither. The same + /// clip-rectangle and window-ID tests as a GL fragment apply (model + /// choice, as for the DMA write). + pub fn image_depth_stencil_word(&mut self, word: u32) { + let x_decreasing = self.image.x2 < self.image.x1; + let y_decreasing = self.image.y2 < self.image.y1; + let y_finished = if y_decreasing { + self.image.cur_y == u32::MAX || self.image.cur_y < self.image.y2 + } else { + self.image.cur_y >= self.image.y2 + }; + + if (self.image.group_len != 0 && self.image.group_painted >= self.image.group_len) + || self.image.x2 == self.image.x1 + || self.image.y2 == self.image.y1 + || y_finished + { + return; + } + self.image.group_painted += 1; + let x = self.image.cur_x as i64 + self.win.origin[0] as i64; + let y = self.image.cur_y as i64 + self.win.origin[1] as i64; + + // An off-screen object and the id planes hold no depth or stencil. + if self.image_object().is_none() && !self.dest_is_id_planes() && self.pixel_allowed(x, y) { + let mask = (self.gl_write_mask >> write::STENCIL_SHIFT) & write::STENCIL_MASK; + + if self.gl_write_mask & write::DEPTH != 0 { + self.depth_store(x, y, word & ODSY_DEPTH24_MAX); + } + if mask != 0 { + let old = self.stencil_load(x, y); + let data = (word >> ODSY_BURST_STENCIL_SHIFT) & write::STENCIL_MASK; + + self.stencil_store(x, y, ((data & mask) | (old & !mask)) as u8); + } + } + self.image_advance(x_decreasing, y_decreasing); + } + + /// One data word of a pixel burst, laid out by the transfer header's format + /// code: its bytes join a stream that yields a pixel every + /// pixel_bytes() bytes, so a three-byte pixel may span two words. A + /// bounded group's trailing padding (past image_group_len) is dropped with any + /// partial pixel, so the raster position stays exact. A header with a code + /// this model does not know falls back on the burst tag's format, 0x11 one + /// 0x00RRGGBB pixel a word and 0x10 four indices, and is reported once. + pub fn image_burst_word(&mut self, word: u32) { + let mut code = self.image.code; + let mut n = pixel_bytes(code); + let mut b = [0u8; 4]; + + if !self.image_burst_supported(self.image.fmt) { + return; + } + if code == image::CODE_DEPTH_STENCIL { + self.image_depth_stencil_word(word); + return; + } + if self.image.fmt == image::FMT_RGBA16 && texel12_components(code) != 0 { + // A stream of 16-bit components, high half first, a pixel every + // texel12_components() halves: two words for 0x6b and 0x6c, a + // word and a half for 0x69. At most two halves wait between words; + // image_nbytes counts them, image_bytes holds them. + let comps = texel12_components(code) as usize; + let mut halves = [0u16; 4]; + let mut held = self.image.nbytes.min(2) as usize; + + if self.image.group_len != 0 && self.image.group_painted >= self.image.group_len { + return; + } + for (i, half) in halves.iter_mut().enumerate().take(held) { + *half = (self.image.bytes >> (16 * (held - 1 - i))) as u16; + } + for h in 0..2 { + let mut px = [0u8; 8]; + + halves[held] = if h != 0 { word as u16 } else { (word >> 16) as u16 }; + held += 1; + if held < comps { + continue; + } + for (pair, half) in px.as_chunks_mut::<2>().0.iter_mut().zip(&halves[..comps]) { + pair.copy_from_slice(&half.to_be_bytes()); + } + let row = self.image.cur_y; + + self.image_put(decode_texel12(code, &px, true), false, true); + held = 0; + if h == 0 && self.image.cur_y != row { + // The row ended in the word's first half: the second + // is pad, and the next row starts on the next word. + break; + } + } + self.image.bytes = if held > 1 { + ((halves[0] as u32) << 16) | halves[1] as u32 + } else if held != 0 { + halves[0] as u32 + } else { + 0 + }; + self.image.nbytes = held as u32; + self.image.group_painted += 1; + if self.image.group_len != 0 && self.image.group_painted >= self.image.group_len && self.image.nbytes != 0 { + self.image.bytes = 0; + self.image.nbytes = 0; + } + return; + } + + let mut form = None; + + if n == 0 { + // A code the byte-stream tables above do not know: the library's + // write table (pixel_write.rs), for pixels of up to sixteen bytes. + form = write_form(self.image.write_code).filter(|f| f.bytes <= 16); + if let Some(f) = &form { + n = f.bytes; + } else { + if !self.diag.image_code_reported { + self.diag.image_code_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: image source format {:#x} is not modeled; laid out by the burst tag", + self.image.write_code + ); + } + code = if self.image.fmt == image::FMT_RGB { image::CODE_FALLBACK_XRGB } else { image::CODE_CI }; + n = pixel_bytes(code); + } + } + b[..4].copy_from_slice(&word.to_be_bytes()); + if let Some(f) = &form { + // The write table's pixels: any number of bytes up to a pixel of + // four 32-bit components, in arrival order. + for (i, &byte) in b[..4].iter().enumerate() { + self.image.pixel[self.image.nbytes as usize] = byte; + self.image.nbytes += 1; + if self.image.nbytes < f.bytes { + continue; + } + self.image.nbytes = 0; + let px = self.image.pixel; + let row = self.image.cur_y; + + self.image_texel_form(f, &px); + if i < 3 && self.image.cur_y != row { + // The row ended inside the word: the rest is pad. + break; + } + } + return; + } + // OBSERVED (guest traces of GL draws): each row of 16- and 24-bit + // pixels ends on a word, padded with 0x5a, and the next row starts + // on the next word. Eight-bit pixels (the X server's index images) + // are left as they were: no trace shows their rows padded. + let padded_rows = (n == 2 || n == 3) && code != image::CODE_CI16; + + for (i, &byte) in b[..4].iter().enumerate() { + let mut px = [0u8; 4]; + + self.image.bytes = (self.image.bytes << 8) | byte as u32; + self.image.nbytes += 1; + if self.image.nbytes < n { + continue; + } + self.image.nbytes = 0; + // The pixel's bytes, first byte highest, at the start of px[]. + px[..4].copy_from_slice(&(self.image.bytes << (8 * (4 - n))).to_be_bytes()); + let rgba = self.decode_pixel(code, &px).unwrap_or(0); + let row = self.image.cur_y; + + self.image_texel(rgba, code); + if padded_rows && i < 3 && self.image.cur_y != row { + break; + } + } + } +} + +/// The format of a pixel-burst register-select word, None for any other. +pub(super) fn image_burst_fmt(reg: u32) -> Option { + let f = reg & image::BURST_FMT_MASK; + + if (reg & image::BURST_BASE) == 0 || (reg & image::BURST_RESERVED_MASK) != 0 { + return None; + } + match f { + image::FMT_CI | image::FMT_RGB | image::FMT_RGBA16 => Some(f), + // Other documented pixel-burst formats remain unsupported. + _ => None, + } +} + +/// The board's raster rules. Window positions sit on a grid of 8 subpixel +/// bits, truncated (inferred; the vendor frames accept truncation at 1/16 +/// pixel or finer). The interpolated-colour depth is unmeasured. The depth word is the board's +/// 24-bit float (depth_word.rs). (A function, not a static: the struct is not const +/// constructible.) +/// +/// One-pixel lines take the centre-sampled rule (LineRule::CentreSampled). +/// The x-major tie and the end rule are model choices. +pub(super) fn gl_rules() -> RasterRules { + RasterRules { + subpixel_bits: 8, + color_bits: 8, + depth_bits: 24, + stencil_bits: 8, + line_rule: LineRule::CentreSampled, + alpha_test_bits: 12, + smooth_primitives: true, + // The board's depth word; Board::gl_load_combined(). + depth_float: true, + ..Default::default() + } +} diff --git a/src/dev/odyssey/fifo.rs b/src/dev/odyssey/fifo.rs new file mode 100644 index 000000000..87b128b63 --- /dev/null +++ b/src/dev/odyssey/fifo.rs @@ -0,0 +1,468 @@ +//! The command FIFO between the processor and the board's thread. +//! +//! The PROM's and IRIX's textports send hundreds of thousands of +//! command-FIFO words per scrolled line, so a store must cost little. A +//! store goes into the shared graphics ring (`GFifo`, gfifo.rs) without +//! taking the host interface's lock; the board's thread is woken only when +//! it is asleep, and otherwise finds the store when it next looks. +//! +//! The guest sees the board's FIFO, never the host ring. The FIFO's depth, +//! in any unit, is UNKNOWN; the +//! model gives it a size of 32,768 words (`cfifo::WORDS`), a MODEL +//! CHOICE independent of the `0x6bX` values the kernel stores. The level +//! STATUS0 reports is the 32-bit words of the stores pushed and not yet +//! taken out, a doubleword store counting two, clamped to that 32,768 +//! (`level`); the high-water bit compares it against the `0x6b2` value +//! (a model choice: the register's role is unknown), and the low-water bit +//! against the model's 16-word threshold. Level 0 is the FIFO empty and +//! every store run. +//! +//! Where a store is refused is a model choice, not a recorded hardware +//! fact. What a full FIFO does to the processor is not known (software is +//! inferred to avoid overflow by credits and descheduling, and a board-side +//! overflow to be a flagged error), and the FIFO has no wrap protection of +//! its own, the kernel's flow control keeping it from filling. On the +//! Octane that flow control (the node's counted graphics credits and the +//! kernel's high-water path) is not modelled. IMPACT and GR2 do not stall +//! at their FIFO's size either: their host ring is deep, a store is refused +//! with the bus busy only when the ring is full, and the guest-visible +//! level is the model's, clamped to the model's FIFO size (a model choice, +//! `cfifo::WORDS`). The VPro does the same: the ring holds +//! `ODSY_WORK_QUEUE_MAX` entries, a store is one entry whatever its width, +//! so it takes at least twice the model's FIFO in words, and the processor +//! runs ahead of the board by that much. Everything the guest can read +//! comes from the clamped level, so it never sees a level above the +//! model's size: past the brim it reads a full FIFO (and a high-water bit, +//! once a `0x6b2` threshold is set) while its stores still succeed. A store +//! the full ring refuses is retried by the processor while the board's +//! thread makes room (`BoardLocks::bus_write`). The ring is 64K entries as +//! IMPACT's and GR2's are. +//! +//! The processor's stores are the producer; whoever runs the queue (the +//! board's thread, or an access that drains it with that thread stopped) +//! is the consumer. + + +use super::consts::{cfifo, intr, ODSY_WORK_QUEUE_MAX}; +use super::interface::HostInterface; +use super::worker::GAP; +use std::cell::Cell; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering::{Relaxed, SeqCst}}; +use std::thread::JoinHandle; + +use crossbeam_utils::CachePadded; +use parking_lot::Mutex; + +use crate::gfifo::GFifo; + +/// The host ring under the command FIFO. +pub(super) type CommandRing = GFifo<{ ODSY_WORK_QUEUE_MAX as usize }>; + +/// A command-FIFO store waiting for the board's thread. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct QueuedStore { + pub value: u64, + pub addr: u32, + pub size: u8, +} + +// A ring entry is (address word, value). The address word carries the +// aperture offset in its low 24 bits and the store's size above them. +const ENTRY_OFFSET_MASK: u32 = 0x00ff_ffff; +const ENTRY_SIZE_SHIFT: u32 = 24; +const ENTRY_SIZE_MASK: u32 = 0xf; + +impl QueuedStore { + fn entry(&self) -> u32 { + debug_assert!(self.addr <= ENTRY_OFFSET_MASK && self.size as u32 <= ENTRY_SIZE_MASK); + self.addr | (self.size as u32) << ENTRY_SIZE_SHIFT + } + + fn from_entry(addr: u32, value: u64) -> Self { + QueuedStore { + value, + addr: addr & ENTRY_OFFSET_MASK, + size: ((addr >> ENTRY_SIZE_SHIFT) & ENTRY_SIZE_MASK) as u8, + } + } + + /// The FIFO words the store fills: two for a doubleword. + pub fn words(&self) -> u32 { + store_words(self.size as u32) + } +} + +/// The 32-bit FIFO words a store of `size` bytes fills. +pub(super) fn store_words(size: u32) -> u32 { + if size == 8 { 2 } else { 1 } +} + +/// The most stores the board's thread takes out of the FIFO at a time. +pub(super) const RUN_MAX: usize = 32; + +/// Stores the board's thread has looked at and is running (`take_run`). +pub(super) struct Run { + stores: [QueuedStore; RUN_MAX], + len: usize, +} + +impl Run { + pub fn new() -> Self { + Run { stores: [QueuedStore { value: 0, addr: 0, size: 0 }; RUN_MAX], len: 0 } + } + + /// The stores to run, oldest first. + pub fn stores(&self) -> &[QueuedStore] { + &self.stores[..self.len] + } +} + +/// The stores the host ring holds: it keeps one slot empty. +pub(super) const RING_STORES: u32 = ODSY_WORK_QUEUE_MAX - 1; + +/// The counts the processor's pushes raise and the board's thread's +/// retirements lower. +struct Levels { + /// The level: FIFO words pushed and not yet taken out. + words: AtomicU32, + /// Stores pushed and not yet taken out. + stores: AtomicU32, +} + +pub(super) struct CommandFifo { + ring: Box, + /// Serialises the consumers' accounting against reset's clear. The + /// producers need no lock of their own: the ring's spinlock orders them. + pop: Mutex<()>, + /// What both sides count, on a line of its own. + levels: CachePadded, + /// The threshold for status bit 15 and flag 25 from `0x6b2`, in words + /// (a model choice: the register's role is unknown); zero until set up. This + /// and `running` are read on every store and written rarely, so each has + /// a line no counter shares. + threshold_6b2: CachePadded, + /// The board's thread runs, so command stores queue. + pub running: CachePadded, + /// The board's thread is parked (`wait_for_work`): a producer unparks it + /// only then, so a store reads a line nobody writes instead of locking. + asleep: CachePadded, + /// The processor waits for the board's thread, for a refused access: its + /// next slice starts without the pause for a burst of stores, and a + /// pause under way ends. + hurry: CachePadded, + /// The board's thread is running stores. + pub slice: CachePadded, + exit: AtomicBool, + sleeper: Mutex>, + pub thread: Mutex>>, + /// Edge filter for the traced level; zero-sized without the feature. + pub pf_level: crate::PfValue, + /// Tests: queue every command store even with no thread running, and + /// run this many per slice. + #[cfg(test)] + pub test_words: AtomicU32, + /// Tests: the board's thread's pause in milliseconds instead of `GAP`. + #[cfg(test)] + pub test_gap_ms: AtomicU32, + /// Tests: how many times the board's thread has gone to sleep in `gap`, + /// counted after its last look at the hurry request. + #[cfg(test)] + pub test_parked: AtomicU32, + /// Tests: the next push calls this, once, as soon as its + /// store is in the ring: a consumer that takes the store at that + /// moment, before the push has returned. + #[cfg(test)] + pub test_after_ring_push: Mutex)>>, +} + +impl Default for CommandFifo { + fn default() -> Self { + CommandFifo { + // SAFETY: a zeroed GFifo is an empty one (GFifo::new); built in + // place because the ring is too big for the stack. + ring: unsafe { Box::new_zeroed().assume_init() }, + pop: Mutex::new(()), + levels: CachePadded::new(Levels { words: AtomicU32::new(0), stores: AtomicU32::new(0) }), + threshold_6b2: CachePadded::new(AtomicU32::new(0)), + running: CachePadded::new(AtomicBool::new(false)), + asleep: CachePadded::new(AtomicBool::new(false)), + hurry: CachePadded::new(AtomicBool::new(false)), + slice: CachePadded::new(AtomicBool::new(false)), + exit: AtomicBool::new(false), + sleeper: Mutex::new(None), + thread: Mutex::new(None), + pf_level: crate::PfValue::new(), + #[cfg(test)] + test_words: AtomicU32::new(0), + #[cfg(test)] + test_gap_ms: AtomicU32::new(0), + #[cfg(test)] + test_parked: AtomicU32::new(0), + #[cfg(test)] + test_after_ring_push: Mutex::new(None), + } + } +} + +impl CommandFifo { + /// The FIFO words waiting, as the guest sees them: the model's FIFO + /// holds no more than `cfifo::WORDS` (a model choice), so a deeper + /// backlog in the host ring reads as a full FIFO. Zero only when nothing waits. + pub fn level(&self) -> u32 { + self.queued_words().min(cfifo::WORDS) + } + + /// The FIFO words waiting in the host ring, unclamped. + pub fn queued_words(&self) -> u32 { + self.levels.words.load(SeqCst) + } + + /// Whether the host ring has room for another store. + fn has_room(&self) -> bool { + self.stores() < RING_STORES + } + + /// The stores waiting. + pub fn stores(&self) -> u32 { + self.levels.stores.load(SeqCst) + } + + pub fn pending(&self) -> bool { + self.level() != 0 + } + + /// Whether the level is at or above the model's high-water threshold. + pub fn high_water(&self) -> bool { + let mark = self.threshold_6b2.load(SeqCst); + mark != 0 && self.level() >= mark + } + + /// The stored `0x6b2`, applied as the high-water threshold in words. MODEL + /// CHOICE (see `reg::FIFO_SETUP_FIRST`). + pub fn set_threshold_6b2(&self, words: u32) { + self.threshold_6b2.store(words, SeqCst); + } + + /// Whether a store of `size` bytes finds the board's FIFO full while + /// the board's thread runs, so it must wait for room. With two + /// processors the room it sees can go to the other one's store first; + /// `push` then refuses, and the store is not lost. + pub fn full_for(&self, size: u32) -> bool { + self.running.load(SeqCst) && !self.has_room() + } + + /// The processor waits for the board's thread to run what is queued, as + /// a refused access does. A thread already in its pause (`gap`) is woken + /// to run at once. + pub fn hurry(&self) { + // A refused store retries in a loop; write the line only when it + // changes, as IMPACT does for its asleep flag. + if !self.hurry.load(SeqCst) { + self.hurry.store(true, SeqCst); + if let Some(t) = self.sleeper.lock().as_ref() { + t.unpark(); + } + } + } + + /// The board's thread's pause before a slice (`GAP`), cut short when the + /// processor asks it to hurry. + pub fn gap(&self) { + #[cfg(test)] + let gap = match self.test_gap_ms.load(SeqCst) { + 0 => GAP, + ms => std::time::Duration::from_millis(ms as u64), + }; + #[cfg(not(test))] + let gap = GAP; + let end = std::time::Instant::now() + gap; + loop { + if self.take_hurry() { + return; + } + let now = std::time::Instant::now(); + if now >= end { + return; + } + #[cfg(test)] + self.test_parked.fetch_add(1, SeqCst); + std::thread::park_timeout(end - now); + } + } + + /// Whether the board's thread's next slice is awaited, clearing it. + pub fn take_hurry(&self) -> bool { + self.hurry.swap(false, SeqCst) + } + + /// Tests: every command store queues, to be run by hand. + pub fn held(&self) -> bool { + #[cfg(test)] + return self.test_words.load(SeqCst) != 0; + #[cfg(not(test))] + false + } + + /// Whether a command store waits in the queue: always while the board's + /// thread runs, so decoding and drawing never run on the processor's + /// thread, and otherwise behind any store that already waits. + pub fn queues(&self) -> bool { + self.running.load(SeqCst) || self.pending() || self.held() + } + + /// Queue a store. False when the host ring has no room for it. + pub fn push(&self, front: &Mutex, addr: u64, value: u64, size: u32) -> bool { + let words = store_words(size); + if !self.has_room() { + return false; + } + let q = QueuedStore { value, addr: addr as u32, size: size as u8 }; + // Count the store before the ring shows it. The board's thread can + // take it and retire it the moment it is in the ring, and a retire + // must find every store it takes already counted, or the level falls + // below zero. Until the push lands the counts are a store high, + // which reads as a store still waiting, never as one gone. + self.levels.stores.fetch_add(1, SeqCst); + let before = self.levels.words.fetch_add(words, SeqCst); + if !self.ring.try_push(q.entry(), q.value) { + // With one processor this cannot happen: has_room counted the + // ring's stores and nothing else pushes. With two, the other + // processor's store can hold the ring's lock, or take the last + // place between has_room and here. Refused, the store is pushed + // again (`write_command`). Nothing took it out, so its counts + // come back off. + self.levels.words.fetch_sub(words, SeqCst); + self.levels.stores.fetch_sub(1, SeqCst); + return false; + } + #[cfg(test)] + if let Some(consumer) = self.test_after_ring_push.lock().take() { + consumer(self, front); + } + let after = before + words; + let mark = self.threshold_6b2.load(SeqCst); + if mark != 0 && before < mark && after >= mark { + // Reaching the model's 0x6b2 threshold raises flag 25, whatever + // the enable mask says. + let mut f = front.lock(); + f.intr_flags |= intr::FIFO_HIGH_WATER; + f.update_irq(); + } + self.wake(); + true + } + + /// Look at the oldest waiting stores, up to `RUN_MAX`, without taking + /// them out; the caller runs them, then `retire`s the ones it ran. + pub fn take_run(&self, run: &mut Run) { + let mut raw = [(0u32, 0u64); RUN_MAX]; + run.len = self.ring.peek_run_at(0, &mut raw); + for (store, (addr, value)) in run.stores.iter_mut().zip(&raw[..run.len]) { + *store = QueuedStore::from_entry(*addr, *value); + } + } + + /// Take the first `n` stores of `run` out of the FIFO: they have run. The + /// level and the stores waiting move once for the run, not + /// once per store, which is what the board's thread spends most of its + /// time on when each store costs little to decode. A store the thread has + /// run but not retired still counts in the level, so the level never + /// reads lower than the words the board has yet to take. + /// + /// The low-water flag latches if the run takes the level from the mark + /// or above to below it. The flag is set before the level falls, under the + /// front lock a status read also takes, so a read sees the flag whenever + /// it sees the level below the mark. + pub fn retire(&self, front: &Mutex, run: &Run, n: usize) { + if n == 0 { + return; + } + let ran = &run.stores[..n]; + let words: u32 = ran.iter().map(QueuedStore::words).sum(); + let _c = self.pop.lock(); + self.ring.consume_n_at(0, n); + if self.levels.words.load(SeqCst) < cfifo::LOW_WATER + words { + let mut f = front.lock(); + let before = self.levels.words.fetch_sub(words, SeqCst); + debug_assert!(before >= words, "the level counts every store before the ring shows it (push)"); + if before >= cfifo::LOW_WATER && before - words < cfifo::LOW_WATER { + f.intr_flags |= intr::FIFO_LOW_WATER; + crate::pf_instant!(crate::perfetto::events::cfifo::LOW_WATER, before - words); + f.update_irq(); + } + } else { + self.levels.words.fetch_sub(words, SeqCst); + } + self.levels.stores.fetch_sub(n as u32, SeqCst); + } + + /// Abandon every waiting store. + pub fn clear(&self) { + let _c = self.pop.lock(); + while let Some((addr, value)) = self.ring.peek() { + self.ring.consume(); + let q = QueuedStore::from_entry(addr, value); + self.levels.words.fetch_sub(q.words(), SeqCst); + self.levels.stores.fetch_sub(1, SeqCst); + } + } + + /// After a push or a stop: unpark the board's thread if it is asleep. + /// The fence pairs with `wait_for_work`'s: either this sees the thread + /// asleep, or the thread sees the push before it parks. + fn wake(&self) { + std::sync::atomic::fence(SeqCst); + if self.asleep.load(Relaxed) { + if let Some(t) = self.sleeper.lock().as_ref() { + t.unpark(); + } + } + } + + /// The board's thread: sleep until a store waits or `stop` asks. + /// Returns false when the thread is to finish. The timeout is only a + /// backstop for a wake the fence protocol should never lose. + pub fn wait_for_work(&self) -> bool { + *self.sleeper.lock() = Some(std::thread::current()); + while !self.pending() && !self.exit.load(SeqCst) { + self.asleep.store(true, Relaxed); + std::sync::atomic::fence(SeqCst); + if !self.pending() && !self.exit.load(SeqCst) { + std::thread::park_timeout(std::time::Duration::from_millis(50)); + } + self.asleep.store(false, Relaxed); + } + !self.exit.load(SeqCst) + } + + pub fn exiting(&self) -> bool { + self.exit.load(SeqCst) + } + + /// Ask the board's thread to finish. + pub fn stop(&self) { + self.exit.store(true, SeqCst); + self.wake(); + } + + pub fn start(&self) { + self.exit.store(false, SeqCst); + } +} + +thread_local! { + /// The command words waiting, as the display thread last sampled them + /// for its status bar (Odyssey::frame). Per thread, because each board + /// presents from a display thread of its own. + static DISPLAY_FIFO_PENDING: Cell = const { Cell::new(0) }; +} + +/// The display thread's sample of the words waiting. +pub(super) fn display_fifo_pending() -> usize { + DISPLAY_FIFO_PENDING.with(Cell::get) +} + +/// Record the words waiting for this display thread's next present. +pub(super) fn set_display_fifo_pending(words: usize) { + DISPLAY_FIFO_PENDING.with(|p| p.set(words)); +} diff --git a/src/dev/odyssey/geometry.rs b/src/dev/odyssey/geometry.rs new file mode 100644 index 000000000..0f3870532 --- /dev/null +++ b/src/dev/odyssey/geometry.rs @@ -0,0 +1,1399 @@ +//! GL vertex work for the board: the vertex-program upload and its lit and +//! window records, per-vertex lighting and colour, the depth scale and the +//! combined matrix loads. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use super::raster::{cmax, cmin}; +use super::board::{ + Board, ODSY_GL_CONSTANTS, ODSY_GL_LIGHTS, ODSY_GL_PROGRAM_ENTRIES, ODSY_GL_PROGRAM_REPORTS, + ODSY_HEIGHT, ODSY_WIDTH, +}; +use super::consts::{ + blend, colour, gl_cmd, light, line, mode, pattern, prog, ucode, write, ODSY_BUFFERS, + ODSY_CRC64_ECMA_POLY, ODSY_DEPTH_FUNC_MASK, ODSY_ENTRY_29, ODSY_ENTRY_2A, ODSY_ENTRY_2C, + ODSY_ENTRY_33, ODSY_PLANE_MASK_SHIFT, +}; +use super::gl::{float_index, gl_index_rgba, prog_value, ucode_is_texgen}; +use super::gl_context::{ + op, GL_ALWAYS, GL_CLIP_PLANES, GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_LINES, + GL_MODELVIEW, GL_PROJECTION, GL_REPLACE, GL_STENCIL_BUFFER_BIT, RASTER_CLEAR, RasterOp, +}; +use crate::dev::gl::light::Lighting; + +/// What a program asks for, as far as the model recognises it. +#[derive(Clone, Copy, Default)] +pub(super) struct Program { + pub known: bool, + pub lit: bool, + pub local_viewer: bool, + /// The program renormalises transformed normals (GL_NORMALIZE). + pub normalize: bool, + pub two_sided: bool, + pub texgen: bool, + pub clip: bool, + pub texture: bool, + pub linear_texgen: u32, +} + +impl Board { + /// Walk the list the engine continues through, constants[k] up to the + /// terminator. With strict, every word must be a routine this model names + /// (an assembled lit list); otherwise only the texgen and clip-plane stages + /// are picked out (a precompiled program's list, which its entries already + /// identify, so the result is not needed). False when the terminator is not + /// reached or, when strict, a word is not recognised. With `older` the + /// list is in the older libraries' numbering (see `ucode_current`). + pub fn gl_walk_list(&mut self, k: u32, strict: bool, older: bool, p: &mut Program) -> bool { + let nc = ODSY_GL_CONSTANTS; + let at = |s: &Board, i: u32| -> u32 { + let a = s.geom.constants[(k.wrapping_add(i) % nc) as usize]; + if older { ucode_current(a) } else { a } + }; + let mut n: u32 = 0; + while n < prog::LIST_MAX { + let a = at(self, n); + let next = at(self, n + 1); + + if a == ucode::END { + return true; + } + if a == 0 && (next == ucode::CLIP || next == ucode::CLIP_TEXTURED || next == ucode::CLIP_TEXGEN) { + p.clip = true; + n += 2; + continue; + } + // Matched public S, T, R and Q controls identify these numeric list + // selectors as generation stages, an experimental model policy. + // Other fragments retain their diagnostic; no whole program is matched. + if a == 0x033 { + let third = at(self, n + 2); + let fourth = at(self, n + 3); + + if next == 0x04d && third == 0x057 && fourth == 0x061 && at(self, n + 4) == 0x066 && at(self, n + 5) == 0x06c && at(self, n + 6) == 0x077 { + p.linear_texgen = 0xf; + n += 7; + continue; + } + if next == 0x04d && third == 0x061 && fourth == 0x066 && at(self, n + 4) == 0x06c && at(self, n + 5) == 0x077 { + p.linear_texgen = 0xd; + n += 6; + continue; + } + if next == 0x06b && third == 0x077 { + p.linear_texgen = 3; + n += 3; + continue; + } + if (next == 0x04d || next == 0x057 || next == 0x061 || next == 0x066) && third == 0x06c && fourth == 0x077 { + p.linear_texgen = if next == 0x04d { + 1 + } else if next == 0x057 { + 2 + } else if next == 0x061 { + 4 + } else { + 8 + }; + n += 4; + continue; + } + } + if a == ucode::NORMALIZE { + p.normalize = true; + } + if ucode_is_texgen(a) { + p.texgen = true; + n += 1; + continue; + } + match a { + ucode::FINISH_A_TWO_SIDED | ucode::FINISH_TWO_SIDED | ucode::END_TWO_SIDED => { + p.two_sided = true; + } + ucode::FINISH_A_TEXTURE | ucode::FINISH_TEXTURE | ucode::UNLIT_TEXTURE => { + p.texture = true; + } + ucode::NORMAL + | ucode::NORMALIZE + | ucode::LIGHT_DIR + | ucode::LIGHT_POS + | ucode::LIGHT_PLAIN + | ucode::LIGHT_ATTEN + | ucode::FINISH_A + | ucode::FINISH_ONE_SIDED + | ucode::END_ONE_SIDED + | ucode::TAIL_ONE_SHOT + | ucode::TAIL + | ucode::UNLIT_FINISH => {} + _ => { + if strict { + return false; + } + } + } + n += 1; + } + false + } + + /// One program-memory halfword (prog::SHAPE): halfwords 5 down to 0 of + /// the selected entry, then the next entry, kept in the image's order. + pub fn gl_program_halfword(&mut self, word: u32) { + let h: u16 = ((word & prog::LOW_MASK) | (((word >> prog::HIGH_SHIFT) & prog::HALFWORD_HIGH_MASK) << 10)) as u16; + + if self.geom.program_addr >= ODSY_GL_PROGRAM_ENTRIES { + if !self.diag.gl_program_mem_reported { + self.diag.gl_program_mem_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: program-memory store past entry {:#x} is not modeled", ODSY_GL_PROGRAM_ENTRIES - 1); + } + return; + } + let idx = self.geom.program_addr * prog::ENTRY_HALFWORDS + prog::ENTRY_HALFWORDS - 1 - self.geom.program_half; + self.geom.program_mem[idx as usize] = h; + self.geom.program_half += 1; + if self.geom.program_half == prog::ENTRY_HALFWORDS { + self.geom.program_half = 0; + self.geom.program_addr += 1; + } + } +} + +pub(super) fn prog_in_window(a: u32) -> bool { + (prog::WINDOW_BASE..prog::WINDOW_BASE + prog::WINDOW_ENTRIES).contains(&a) +} + +/// The GL library's precompiled lit programs whose three entries are all +/// resident. The library picks a precompiled program from a 24-record table +/// by a 64-bit feature key and assembles a list only when no record covers +/// the context (ESTABLISHED from the library's table and a capture). Each key +/// below has lighting on, the infinite viewer and only directional lights, +/// and no fragment lighting; a key covers every context whose features are a +/// subset of it, so the texture, texture-generation and two-sided bits it +/// also carries do not say the context uses them. The model lights every +/// program here from the light blocks as it does an assembled list +/// (Board::gl_light()). +pub(super) static UCODE_LIT_RECORDS: [[u32; 3]; 6] = [ + [0x5cb, 0x5d8, 0x5e5], // record 3: one light + [0x62c, 0x63d, 0x64d], // record 6: two lights, texgen + [0x6d8, 0x6eb, 0x6fd], // record 7: one light, two-sided, texgen + [0x5f0, 0x605, 0x619], // record 9: one light, GL_NORMALIZE + [0x657, 0x66e, 0x684], // record 10: two lights, GL_NORMALIZE + [0x699, 0x6b4, 0x6ce], // record 11: four lights, texgen +]; + +/// The lit records above that renormalise normals (GL_NORMALIZE). +pub(super) static UCODE_NORMALIZE_RECORDS: [[u32; 3]; 2] = [[0x5f0, 0x605, 0x619], [0x657, 0x66e, 0x684]]; + +pub(super) fn gl_lit_record(e: &[u32; 3]) -> bool { + for r in UCODE_LIT_RECORDS.iter() { + if e == r { + return true; + } + } + false +} + +/// The precompiled lit programs the GL library runs from the per-context +/// program window. The library holds the geometry engine's whole shipped +/// program image, 12 bytes an entry; the table below fits only the 2,794 +/// entry image of the IP30 library, SHA-256 +/// a0201d88cf23752cd35ac60c25f2c1b05cfab3253e5991db09006bd1b85b0375. For a +/// record it takes the lowest address b among the record's entries and list +/// that reaches the window (a + 0x32 > 0x738), uploads min(image length - b, +/// 0xc7) image entries from b into the window at 0x738, and when b >= 0x738 +/// rebases each address a >= b to a - b + 0x738. When b is lower, nothing is +/// rebased and the window gets the image from 0x738 (record 13). ESTABLISHED +/// from the library's program selection and upload. Rebased entries do not +/// identify a record -- records 17, 18 and 19 all arrive as 738 75b 77d -- so +/// a record is recognised only when its entries arrive and the window holds +/// exactly the body the library uploads for it, checked by the body's +/// CRC-64/ECMA-182 over its halfwords in image order. Any other window body +/// stays unrecognised and reported. +/// +/// Lit: records whose list ends in the finish word 0x4ca or 0x50f, as the +/// resident lit records' do, and whose key has no fragment-lighting bit +/// (INFERRED from the resident records: the unlit ones end in 0x4c5, the +/// fragment-lighting ones in 0x518). Records 12, 20, 21 (fragment lighting) +/// and 22 (finish 0x4c5) are left out. +/// +/// Each record also carries what its key says about the viewer and the +/// faces: key bit 0x2 is the light model's local viewer, and only a +/// local-viewer context selects records 18 and 19; key bit 0x1 is two-sided +/// lighting (INFERRED: it is set in resident record 7, the two-sided one). +#[derive(Clone, Copy, Default)] +pub(super) struct WindowRecord { + pub record: u8, + pub local_viewer: bool, + pub two_sided: bool, + pub entry: [u16; 3], + pub entries: u16, + pub crc: u64, +} + +pub(super) static WINDOW_RECORDS: [WindowRecord; 8] = [ + WindowRecord { record: 13, local_viewer: false, two_sided: true, entry: [0x706, 0x720, 0x73b], entries: 199, crc: 0xde5dd7f66d4e70cf }, + WindowRecord { record: 14, local_viewer: false, two_sided: false, entry: [0x4c5, 0x4c5, 0x4c5], entries: 199, crc: 0x5aa6880e8d77c0b2 }, + WindowRecord { record: 15, local_viewer: false, two_sided: true, entry: [0x4c5, 0x4c5, 0x4c5], entries: 199, crc: 0x9391fc6eed0accdc }, + WindowRecord { record: 16, local_viewer: false, two_sided: false, entry: [0x738, 0x75a, 0x77b], entries: 199, crc: 0xb1da870f5523467f }, + WindowRecord { record: 17, local_viewer: false, two_sided: false, entry: [0x738, 0x75b, 0x77d], entries: 199, crc: 0xc4bf2cd3423da137 }, + WindowRecord { record: 18, local_viewer: true, two_sided: false, entry: [0x738, 0x75b, 0x77d], entries: 199, crc: 0xe15a6186144f9887 }, + WindowRecord { record: 19, local_viewer: true, two_sided: false, entry: [0x738, 0x75b, 0x77d], entries: 199, crc: 0xa419c456a8238b77 }, + WindowRecord { record: 23, local_viewer: false, two_sided: false, entry: [0x738, 0x760, 0x787], entries: 199, crc: 0x8d6174822a99856f }, +]; + + +impl Board { + /// CRC-64/ECMA-182 (no reflection, zero initial value) of n window entries. + pub fn window_crc(&self, n: u32) -> u64 { + let base = (prog::WINDOW_BASE * prog::ENTRY_HALFWORDS) as usize; + let h = &self.geom.program_mem[base..]; + let mut crc: u64 = 0; + + for &half in &h[..(n * prog::ENTRY_HALFWORDS) as usize] { + crc ^= (half as u64) << 48; + for _bit in 0..16 { + crc = if crc & (1u64 << 63) != 0 { (crc << 1) ^ ODSY_CRC64_ECMA_POLY } else { crc << 1 }; + } + } + crc + } + + /// The lit window record the entries and window body identify, or None. + pub fn gl_window_record(&self, e: &[u32; 3]) -> Option<&'static WindowRecord> { + WINDOW_RECORDS.iter().find(|w| { + e[0] == w.entry[0] as u32 + && e[1] == w.entry[1] as u32 + && e[2] == w.entry[2] as u32 + && self.window_crc(w.entries as u32) == w.crc + }) + } +} + +/// Uploaded program bodies have no reviewed decoder. +pub(super) fn gl_resident(e: &[u32; 3]) -> bool { + !e.iter().any(|&w| prog_in_window(w)) +} + +/// A routine number of the older libraries' numbering in the current one. +/// The older libraries run a program image without the spot routine, so +/// they number every routine from where it starts 99 lower; a number at or +/// past that point moves up past the routine, and none lands inside it. +fn ucode_current(a: u32) -> u32 { + if a >= ucode::SPOT_FIRST { a + ucode::SPOT_RECORDS } else { a } +} + +impl Board { + /// Recognise a resident program from its three entries and the list at + /// `k`, all in the current numbering or, with `older`, all in the older + /// libraries'. An entry past the older image's end is no entry of it, so + /// nothing is recognised. + fn gl_resident_program(&mut self, raw: [u32; 3], k: u32, older: bool, p: &mut Program) { + let mut r = raw; + if older { + if raw.iter().any(|&a| a >= ucode::OLDER_RECORDS) { + return; + } + r = raw.map(ucode_current); + } + if r == [0, 0, 0] { + p.known = true; + } else if r == [ucode::UNLIT, ucode::UNLIT_2A, ucode::UNLIT_2C] + || r == [ucode::UNLIT_CLIP, ucode::UNLIT_CLIP_2A, ucode::UNLIT_CLIP_2C] + { + self.gl_walk_list(k, false, older, p); + p.known = true; + } else if gl_lit_record(&r) { + self.gl_walk_list(k, false, older, p); + p.known = true; + p.lit = true; + p.normalize |= UCODE_NORMALIZE_RECORDS.contains(&r); + } else if r[1] == ucode::END && r[2] == ucode::END { + match r[0] { + ucode::HEAD_LOCAL_DIR | ucode::HEAD_LOCAL_POS => { + p.local_viewer = true; + p.known = self.gl_walk_list(k, true, older, p); + p.lit = true; + } + ucode::HEAD_INF_DIR | ucode::HEAD_INF_POS => { + p.known = self.gl_walk_list(k, true, older, p); + p.lit = true; + } + ucode::HEAD_UNLIT => { + // Lighting off: unlit is the correct rendering, not a guess. + p.known = self.gl_walk_list(k, true, older, p); + } + _ => {} + } + } + } +} + +impl Board { + /// Recognise the program the library just set (on every 0x2c write, the last + /// word of each program set). APPROXIMATION, see + /// ucode::END: a precompiled program is recognised by its three entries, + /// an assembled one (0x2a and 0x2c both the terminator) by the routines in + /// its list, the head (0x29) giving the viewer and saying lighting is on, or, + /// for the unlit head 0x4ae, that it is off. + /// Which lights are enabled and of what kind is not taken from the program: + /// the library zeroes a disabled light's colours, so lighting all eight + /// blocks gives the same colour, and how the engine maps a sparse set of + /// enabled lights onto the list is unknown. All-zero entries are the state + /// the GL library's context default-state setup pushes before any program + /// exists. + /// + /// A resident program the current numbering does not recognise is tried + /// again in the older libraries' (`ucode::SPOT_FIRST`). APPROXIMATION: + /// the model does not know which library or program image the guest runs, + /// so a program is taken in the older numbering only when it means nothing + /// in the current one. Window records are matched by the current bodies' + /// fingerprints only; the older libraries' window uploads are not. + pub fn gl_program(&mut self) { + let r: [u32; 3] = [self.geom.entry[ODSY_ENTRY_29], self.geom.entry[ODSY_ENTRY_2A], self.geom.entry[ODSY_ENTRY_2C]]; + let (e29, e2a, e2c) = (r[0], r[1], r[2]); + let k = self.geom.entry[ODSY_ENTRY_33]; + let mut p = Program::default(); + let resident = gl_resident(&r); + let window = self.gl_window_record(&r); + + if let Some(window) = window { + // A precompiled lit record the library ran from the window. + self.gl_walk_list(k, false, false, &mut p); + p.known = true; + p.lit = true; + p.local_viewer = window.local_viewer; + p.two_sided |= window.two_sided; + } else if resident { + self.gl_resident_program(r, k, false, &mut p); + if !p.known { + p = Program::default(); + self.gl_resident_program(r, k, true, &mut p); + } + } + // Any other uploaded program is unsupported and reported below. + + if !p.known { + p.lit = false; + let mut i = 0u32; + while i < self.diag.gl_program_logged_count { + let b = (i * 3) as usize; + if self.diag.gl_program_logged[b] == e29 && self.diag.gl_program_logged[b + 1] == e2a && self.diag.gl_program_logged[b + 2] == e2c { + break; + } + i += 1; + } + if i == self.diag.gl_program_logged_count && i < ODSY_GL_PROGRAM_REPORTS { + let m = ODSY_GL_CONSTANTS - 1; + let c = |j: u32| -> u32 { self.geom.constants[(k.wrapping_add(j) & m) as usize] }; + let (c0, c1, c2, c3, c4, c5, c6, c7) = (c(0), c(1), c(2), c(3), c(4), c(5), c(6), c(7)); + + let b = (i * 3) as usize; + self.diag.gl_program_logged[b] = e29; + self.diag.gl_program_logged[b + 1] = e2a; + self.diag.gl_program_logged[b + 2] = e2c; + self.diag.gl_program_logged_count += 1; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: geometry program {:#x} {:#x} {:#x} from list index {} ({:#x} {:#x} {:#x} {:#x} {:#x} {:#x} {:#x} {:#x}) is not recognised{}; drawn unlit", + e29, e2a, e2c, k, c0, c1, c2, c3, c4, c5, c6, c7, + if resident { "" } else { " (uploaded program is unsupported)" } + ); + } + } + if p.two_sided && !self.diag.gl_two_sided_logged { + self.diag.gl_two_sided_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: two-sided lighting is not modeled; back faces are lit with the front material"); + } + if p.texgen && !self.diag.gl_texgen_logged { + self.diag.gl_texgen_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: texture-coordinate generation in geometry program {:#x} is not modeled", e29); + } + if p.clip && !self.diag.gl_clip_logged { + self.diag.gl_clip_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: clip planes in geometry program {:#x} are not modeled; geometry is not clipped to them", e29); + } + if p.texture && !self.diag.gl_texture_stage_logged { + self.diag.gl_texture_stage_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: texture-mode lighting finish in geometry program {:#x} is not modeled", e29); + } + self.geom.linear_texgen = if p.known { p.linear_texgen } else { 0 }; + self.geom.lit = p.lit; + self.geom.local_viewer = p.local_viewer; + self.geom.normalize = p.normalize; + } + + /// A colour-mode word (prog::COLOUR_MODE). Only the captured pairs are + /// known: 0x22 = 0 or 1, 0x23 = 2 or 3 (0 from the kernel before a context + /// has drawn). Anything else, or a pair whose index bits disagree once + /// 0x23, which follows 0x22 in every capture, has arrived, is kept and + /// reported once. + pub fn colour_mode(&mut self, cls: u32, v: u32) { + let known: u32 = if cls == prog::COLOUR_MODE { 0x1 } else { 0x3 }; + let index = self.gl_index_mode(); + + self.colour_mode[(cls - prog::COLOUR_MODE) as usize] = v; + if self.gl_index_mode() != index && !self.prim.in_prim { + self.gl_apply_blend(); + } + let disagree = cls == prog::COLOUR_MODE_B && ((self.colour_mode[0] ^ self.colour_mode[1]) & colour::MODE_INDEX) != 0; + if ((v & !known) != 0 || disagree) && !self.diag.colour_mode_reported { + self.diag.colour_mode_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: colour mode word class {:#x} value {:#x} (0x22 value {:#x}) is not modeled", cls, v, self.colour_mode[0]); + } + } + + /// One program word (is_program_word()). + pub fn gl_program_word(&mut self, word: u32) { + let v = prog_value(word); + let cls = word >> prog::CLASS_SHIFT; + + if (prog::REG_FIRST..=prog::REG_LAST).contains(&cls) { + self.geom.regs_20_35[(cls - prog::REG_FIRST) as usize] = v; + } + match cls { + prog::COLOUR_MODE | prog::COLOUR_MODE_B => { + self.colour_mode(word >> prog::CLASS_SHIFT, v); + } + prog::CONST_SELECT => { + self.geom.constant_index = v; + } + prog::CONST_STORE => { + self.geom.constants[(self.geom.constant_index % ODSY_GL_CONSTANTS) as usize] = v; + self.geom.constant_index = (self.geom.constant_index + 1) % ODSY_GL_CONSTANTS; + } + prog::ENTRY_29 => { + // Alone, 0x29 selects the stream for light loads (0x19a) or pixel + // transfers (0x12f) and runs the one-shot: not a program change. It + // is kept for the next 0x2c. + self.geom.entry[ODSY_ENTRY_29] = v; + } + prog::ENTRY_2A => { + self.geom.entry[ODSY_ENTRY_2A] = v; + } + prog::ENTRY_2C => { + self.geom.entry[ODSY_ENTRY_2C] = v; + self.gl_program(); + } + prog::LIST_INDEX => { + self.geom.entry[ODSY_ENTRY_33] = v; + } + prog::COLOUR_MATERIAL => { + self.geom.colour_material = v & colour::MATERIAL_MASK; + if (v & !colour::MATERIAL_MASK) != 0 { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: colour-material word value {:#x} has bits beyond the eight known ones", v); + } + } + prog::LIGHT_VEC_SELECT => { + self.geom.light_vec_sel = v; + } + prog::LIGHT_COLOUR_SELECT => { + self.geom.light_colour_sel = v; + } + prog::PROGRAM_ADDR => { + self.geom.program_addr = v; + self.geom.program_half = 0; + } + _ => { + if (word >> prog::CLASS_SHIFT) >= prog::PROGRAM_HALFWORD { + self.gl_program_halfword(word); + } + // The other classes of 0x20 to 0x35: kept in gl_regs_20_35[] only. + } + } + } +} + +/// The single a command word holds. Arithmetic that is done in double +/// widens it at the use (`f64::from`). +pub(super) fn odyssey_f32(bits: u32) -> f32 { + f32::from_bits(bits) +} + +pub(super) fn f32_bits(f: f32) -> u32 { + f.to_bits() +} + +/// What `Board::gl_light_fill` hands on to the lighting equation. +pub(super) struct LitVertex { + pub eye: [f32; 4], + pub normal: [f32; 3], + pub colour: [f32; 4], + /// The diffuse alpha. + pub alpha: f32, + /// The lights the fill set up, a bit each. + pub lit: u8, +} + +/// The lighting state `Board::gl_light` fills for each vertex, kept so that +/// only what the last vertex changed is put back. +pub(super) struct LightScratch { + pub state: Lighting, + fresh: Lighting, + /// The lights the last fill set up. + pub lit: u8, +} + +impl LightScratch { + pub fn new() -> Box { + let mut fresh = Lighting::default(); + fresh.init(); + Box::new(LightScratch { state: fresh, fresh, lit: 0 }) + } + + /// Put the lights the last fill set up back to their initial state: the + /// fill sets everything else, the light chain included, itself. + pub fn restore(&mut self) { + let mut lit = self.lit; + + while lit != 0 { + let i = lit.trailing_zeros() as usize; + + self.state.lights[i] = self.fresh.lights[i]; + lit &= lit - 1; + } + self.lit = 0; + } +} + +impl Board { + /// OpenGL's lighting equation (section 2.13.1) at one vertex, front + /// material, from the kept light, material and matrix words, evaluated by + /// dev::gl's lighting core. Under colour material (section 2.13.3) the + /// current colour replaces each front material colour the class 0x35 word + /// names (prog::COLOUR_MATERIAL); the back half of that word is kept + /// but, like the back material, unused. + /// + /// APPROXIMATION: the board's geometry engine computes this in microcode + /// the model does not run, with a table function for the specular term + /// whose exact falloff is unknown; GL's equation reproduces both lighting + /// captures. The eye position is the model-view (slot 0x24) applied to the + /// vertex, the normal the upper 3x3 of slot 0x34 applied to the current + /// normal, renormalised only when the program is one of the GL_NORMALIZE + /// ones. The library precomputes a half vector (floats 3-5) for each + /// infinite light and leaves it zero for a positional one, so a light + /// block is directional when its half vector is not zero (INFERRED; a + /// directional light straight down -z would have a zero half vector and + /// is then read as positional). A light with zero ambient, diffuse and + /// specular colour, every disabled one as the library sends it, adds + /// nothing and is left out. Shininess arrives as its square root. + pub fn gl_light(&mut self, obj: &[f32; 4]) -> [f32; 4] { + // The lighting state is kept between vertices (LightScratch): building + // its 3 KB afresh for every vertex was a visible share of the front + // end's time in Smoke & Mirrors. + let mut scratch = self.gl_light_scratch.take().unwrap_or_else(LightScratch::new); + + scratch.restore(); + let v = self.gl_light_fill(&mut scratch.state, obj); + scratch.lit = v.lit; + let (mut front, _) = scratch.state.light_vertex(v.eye, v.normal, v.colour); + // The board passes the diffuse alpha through unclamped. + front[3] = v.alpha; + self.gl_light_scratch = Some(scratch); + front + } + + /// Fill `lt`, whose lights hold their initial state, from the kept + /// words, and link the lights it sets up as the enabled ones. + pub fn gl_light_fill(&self, lt: &mut Lighting, obj: &[f32; 4]) -> LitVertex { + let f = |w: u32| f32::from_bits(w); + // The model-view's first three rows; the board ignores w. + let mv = &self.geom.modelview; + let mut eye = [0f32; 4]; + for r in 0..3usize { + eye[r] = (0..4usize).map(|k| f(mv[r * 4 + k]) * obj[k]).sum(); + } + eye[3] = 1.0; + + lt.on = 1; + lt.local_viewer = self.geom.local_viewer as u32; + lt.normalize = self.geom.normalize as u32; + for r in 0..3usize { + for c in 0..3usize { + lt.normal_matrix[c * 3 + r] = f(self.geom.normal_matrix[r * 4 + c]); + } + } + let colour = self.geom.colour.map(f); + let cm = self.geom.colour_material; + let pick = |bit: u32, base: usize| -> [f32; 4] { + if cm & bit != 0 { colour } else { std::array::from_fn(|k| f(self.geom.material[base + k])) } + }; + let (ma, md, ms, me) = ( + pick(colour::MATERIAL_AMBIENT, 0), + pick(colour::MATERIAL_DIFFUSE, 4), + pick(colour::MATERIAL_SPECULAR, 8), + pick(colour::MATERIAL_EMISSION, 12), + ); + let m = &mut lt.mat[0]; + m.ambient = [ma[0], ma[1], ma[2]]; + m.diffuse = md; + m.specular = [ms[0], ms[1], ms[2]]; + m.emission = [me[0], me[1], me[2]]; + let shininess = f(self.geom.shininess_sqrt); + m.shininess = shininess * shininess; + let ambient = gl_cmd::LIGHT_MODEL_AMBIENT as usize * 4; + lt.amb_sum = std::array::from_fn(|k| f(self.geom.light_colour[ambient + k])); + + let mut lit = 0u8; + for i in 0..ODSY_GL_LIGHTS as usize { + let blk = &self.geom.light_vec[i * gl_cmd::LIGHT_VEC_FLOATS as usize..]; + let col = &self.geom.light_colour[i * 3 * 4..]; + let rgb = |base: usize| -> [f32; 3] { std::array::from_fn(|k| f(col[base + k])) }; + let (la, ld, ls) = (rgb(0), rgb(4), rgb(8)); + if la.iter().chain(&ld).chain(&ls).all(|&v| v == 0.0) { + continue; + } + lit |= 1 << i; + let l = &mut lt.lights[i]; + l.ambient = la; + l.diffuse = ld; + l.specular = ls; + let pos: [f32; 3] = std::array::from_fn(|k| f(blk[light::POS + k])); + let directional = (0..3).any(|k| f(blk[light::HALF + k]) != 0.0); + if directional { + l.pos = [pos[0], pos[1], pos[2], 0.0]; + } else { + l.pos = [pos[0], pos[1], pos[2], 1.0]; + // The board's order is constant, linear, quadratic. + l.atten = [f(blk[light::ATTEN + 1]), f(blk[light::ATTEN + 2]), f(blk[light::ATTEN])]; + // The board keeps minus the cutoff cosine, minus the spot + // direction and minus the exponent. + let cut = -f(blk[light::SPOT_COS]); + if cut > -1.0 { + l.spot_on = 1; + l.spot_cos_cutoff = cut; + l.spot_dir = std::array::from_fn(|k| -f(blk[light::SPOT_DIR + k])); + l.spot_exp = -f(blk[light::SPOT_EXP]); + } + } + } + // The core walks the enabled lights as a chain from head, -1 ending it. + let mut next = -1i32; + for i in (0..ODSY_GL_LIGHTS as usize).rev().filter(|&i| lit & (1 << i) != 0) { + lt.lights[i].next = next; + next = i as i32; + } + lt.head = next; + lt.count = lit.count_ones(); + let normal = self.geom.normal.map(f); + LitVertex { eye, normal, colour, alpha: md[3], lit } + } + + /// The colour a GL vertex takes, handed to the shared context just before + /// the vertex: in index mode the current index (glIndex, not glColor); under + /// a lit program the lit colour; otherwise the current glColor, restored if + /// an index or lit vertex replaced it. xyz is the vertex's object position, + /// already converted from whatever form its word carried, z 0 for a 2D one; + /// lighting needs it for positional lights and the local viewer. + pub fn gl_vertex_colour(&mut self, xyz: &[f32; 3]) { + if self.gl_index_mode() { + if self.geom.lit && !self.diag.gl_index_lit_reported { + self.diag.gl_index_lit_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: colour-index lighting is not modeled; drawn with the current index"); + } + let words = gl_index_rgba(self.gl_color_index).map(f32::to_bits); + self.gl_feed(op::COLOR4F as u16, &words); + self.geom.colour_overridden = true; + } else if self.geom.lit { + let words = self.gl_light(&[xyz[0], xyz[1], xyz[2], 1.0]).map(f32::to_bits); + self.gl_feed(op::COLOR4F as u16, &words); + self.geom.colour_overridden = true; + } else if self.geom.colour_overridden { + let words = self.geom.colour; + self.gl_feed(op::COLOR4F as u16, &words); + self.geom.colour_overridden = false; + } + } + + /// The combined matrix (slot 0x00): the client's model-view and projection + /// composed with its window map, so it takes object coordinates straight to + /// window pixels with row 0 at the window's top. That it carries the + /// model-view is ESTABLISHED from a capture: its fourth row is minus the + /// third row of the model-view it loads into slot 0x24, as a frustum times + /// that model-view gives. So the model-view (slot 0x24) is kept only for + /// lighting's eye position (Board::gl_light()), and the shared context's own + /// model-view stays the identity. For a 128x128 window under an identity + /// model-view and projection it is x' = 64x + 64, y' = -64y + 63.996, and + /// glOrtho(-2, 2, -2, 2, -1, 1) halves the x and y scales. The two cannot be + /// pulled apart, so the model applies the product whole, as the board must. + /// + /// The shared context's pipeline is projection, divide, then viewport map. It + /// is given a fixed viewport of +-gl_cmd::WINDOW_REACH about the window + /// origin, xw = R x_ndc and yw = R y_ndc, and as its projection the combined + /// matrix with the x and y rows divided by R, so its output is the combined + /// matrix's window position unchanged, the library's 1/256 row bias included. + /// The rows run top-down like the window origin, rectangles and id plane, so + /// no flip is needed. Clipping is then the window's own rectangles; where the + /// board clips geometry that leaves the window is unknown. + /// + /// Depth: the library builds the depth row and the four block-16 floats from + /// the same rows of its composite transform (ESTABLISHED from the library's + /// combined-matrix validation). Block 16 is the eye-space w row times d, the + /// determinant of the projection's lower-right 2x2; the depth row is the + /// eye-space z and w rows, mapped so that the near and far eye depths give 0 + /// and 2S, times the same d (S = gl_cmd::DEPTH_S). Their ratio + /// (row2 . v) / (block16 . v) is then the eye-linear depth in board units for + /// any projection, and under an affine model-view block 16 is (0, 0, 0, d). + /// That the board divides by block 16 and not by w is INFERRED: it is the one + /// reading under which the identity, glOrtho and glFrustum captures, the + /// orthographic controls and a perspective scene's rows all span the depth + /// range. That it divides by the whole row, and not by its fourth float + /// alone, is INFERRED the same way: under an affine model-view the two + /// agree, and under a model-view that carries the perspective (gluPerspective + /// in model-view mode) block 16 equals the combined matrix's fourth row. + /// Dividing by the fourth float alone puts such a mesh's rows behind the + /// front one, all clipped, where the whole row puts every row just inside + /// the far plane. Malformed zero or non-finite words keep the sign-only + /// fallback, which divides by a constant. + /// + /// The depth reaches the shared rasterizer as its rules' depth row, + /// row2 / (2S block16[3]), and depth divisor, block16 / block16[3], each + /// applied to the object coordinates and interpolated with perspective + /// correction (Board::gl_operation_rules()). Under perspective the shared + /// projection's z row is 0, so no depth is carried as a difference from w, + /// which a float row could not resolve; a fragment outside depth [0, 1] by + /// more than the rounding slack (gl_cmd::DEPTH_SLACK) is dropped, which is + /// the near and far clip. Under an orthographic matrix the z row stays + /// row2 / (block16[3] S) - row3, whose window depth is the same. The plane + /// holds the board's 24-bit float depth word of 2S times that depth + /// (depth_word.rs). The word's layout is + /// ESTABLISHED as the library's reading; that the plane stores exactly it is + /// INFERRED, and the rounding of a rasterised depth into it (truncation here) + /// is UNKNOWN. Whether the board interpolates depth with perspective + /// correction is INFERRED from SGI's raster patents; the model does. + /// + /// Polygon offset (block 0x058) is a signed count of steps of the encoded + /// word, added to it after encoding (RasterRules::split_offset()). The + /// field is ESTABLISHED; adding it to the word, not to eye-linear depth, is + /// INFERRED, so the offset shrinks toward the near plane in proportion to + /// d - n. The far-plane kill is by depth value in the raster stage + /// (INFERRED), and this model cuts at the slack gl_cmd::DEPTH_SLACK + /// (APPROXIMATION: the board's cut point is UNKNOWN). + /// + /// APPROXIMATION, raster position: under perspective the shared projection's + /// z row is 0, so glRasterPos takes window z 0.5 whatever its eye distance, + /// and a bitmap or pixel rectangle placed there is depth-tested at 0.5. + /// Where the board puts a perspective raster position's depth is UNKNOWN. + /// + /// A non-default glDepthRange changes the row in a way this rule does not + /// fit: it is drawn with the default [0, 1] range, and the near and far clip + /// planes the library adds for it are reported as a program stage + /// (Board::gl_program()). + /// + /// A matrix whose x or y row is zero -- the library's load before its window + /// exists, or the kernel's zero restore at a context switch -- leaves an empty + /// viewport, so nothing is drawn. + pub fn gl_depth_scale(&mut self) -> f64 { + let sign_word = f32::from_bits(self.gl_block16[gl_cmd::BLOCK16_SIGN as usize]); + + if sign_word.is_finite() && sign_word != 0.0 { + return sign_word as f64; + } + if sign_word < 0.0 { -1.0 } else { 1.0 } + } + + pub fn gl_load_combined(&mut self, m: &[f32; 16]) { + let perspective = m[3] != 0.0 || m[7] != 0.0 || m[11] != 0.0; + let depth_scale = self.gl_depth_scale(); + let mut p = [0f32; 16]; + + self.gl().cur.viewport = [0; 4]; + if (m[0] == 0.0 && m[4] == 0.0 && m[8] == 0.0) || (m[1] == 0.0 && m[5] == 0.0 && m[9] == 0.0) { + return; + } + for col in 0..4usize { + p[col * 4] = m[col * 4] / gl_cmd::WINDOW_REACH as f32; + p[col * 4 + 1] = m[col * 4 + 1] / gl_cmd::WINDOW_REACH as f32; + p[col * 4 + 2] = if perspective { + 0.0 + } else { + (m[col * 4 + 2] as f64 / (depth_scale * gl_cmd::DEPTH_S) - m[col * 4 + 3] as f64) as f32 + }; + p[col * 4 + 3] = m[col * 4 + 3]; + } + self.gl().set_matrix(GL_PROJECTION, &p); + let vp = &mut self.gl().cur.viewport; + vp[0] = -gl_cmd::WINDOW_REACH; + vp[1] = -gl_cmd::WINDOW_REACH; + vp[2] = 2 * gl_cmd::WINDOW_REACH; + vp[3] = 2 * gl_cmd::WINDOW_REACH; + } +} + +/// A board-side matrix load: 16 IEEE-754 floats, ROW-major on the wire. +/// GlContext::set_matrix() takes column-major, so transpose before the call. +/// gl_matrix_slot is the slot the last matrix-select chose; the normal slot +/// (gl_cmd::SLOT_NORMAL) is one the context refuses, and its false return is +/// expected. The texture matrix is kept as sent and applied to the current +/// ST by the shared context. +pub(super) fn gl_transpose(rows: &[u32]) -> [f32; 16] { + std::array::from_fn(|i| f32::from_bits(rows[(i % 4) * 4 + i / 4])) +} + +impl Board { + /// Apply the kept slot-0x00 matrix under the kept block-16 sign. + pub fn gl_reload_combined(&mut self) { + let m = gl_transpose(&self.gl_combined); + self.gl_load_combined(&m); + } + + pub fn gl_load_matrix(&mut self) { + if self.gl_matrix_slot == gl_cmd::SLOT_TEXTURE { + self.gl_texture_matrix = self.gl_matrix; + return; + } + if self.gl_matrix_slot == gl_cmd::SLOT_NORMAL_MATRIX { + self.geom.normal_matrix = self.gl_matrix; + return; + } + if self.gl_matrix_slot == gl_cmd::SLOT_COMBINED { + self.gl_combined = self.gl_matrix; + self.gl_reload_combined(); + return; + } + if self.gl_matrix_slot == GL_MODELVIEW { + // The combined matrix already carries the model-view + // (Board::gl_load_combined()), so the shared context keeps its + // identity model-view; this copy serves lighting's eye position. + self.geom.modelview = self.gl_matrix; + for i in 0..GL_CLIP_PLANES { + self.gl_apply_clip_plane(i); + } + return; + } + let m = gl_transpose(&self.gl_matrix); + let slot = self.gl_matrix_slot; + self.gl().set_matrix(slot, &m); + } + + /// The four floats behind index 16, gathered in cmd_operand[]. Their sign + /// word orients the combined matrix's depth row, which arrived just before + /// them, so the projection is rebuilt. A four-float load after any other + /// select has no known meaning and is reported. + pub fn gl_load_block16(&mut self) { + if self.gl_matrix_slot != gl_cmd::SLOT_BLOCK16 { + self.cmd_unimplemented(gl_cmd::LOAD_4F); + return; + } + let n = self.gl_block16.len(); + self.gl_block16.copy_from_slice(&self.cmd.operand[..n]); + self.gl_reload_combined(); + } + + /// The screen-aligned rectangle (gl_cmd::RECT): its corners are window pixels, + /// so add the window origin, then fill the pixels whose centres fall inside the + /// half-open box between the two corners -- sx0 <= p + 0.5 < sx1 -- each + /// through the pixel test. It goes through the shared rasterizer's clear so + /// the float colour is quantized the same way a triangle's is; the clear's + /// rectangle is the box within the plane and its clip list the kept 0x204b + /// rectangles. + /// + /// glClear of the depth buffer is the same rectangle: the library sets the + /// 0x0452 depth write bit and puts the clear depth, d x 2S, in both corners' + /// z, and for a depth-only clear also zeroes the colour write mask, leaving + /// the colour operands uninitialised. So the rectangle writes depth, from the + /// first corner's z scaled to the 24-bit plane, when the depth write bit is + /// set, and writes colour only when the colour mask is not zero (ESTABLISHED + /// for the words). The library forces GL_ALWAYS around every rectangle it + /// sends; how the board would test a rectangle under another comparison is not + /// known, so that case is reported and the rectangle written unconditionally. + /// In index mode the first float is the clear index, not red (glClearIndex(3) + /// sends 3.0 there, ESTABLISHED from a colour-index capture), and the + /// rectangle stores it like any index fragment. glClear of the stencil + /// planes is the rectangle with the stencil test on under GL_ALWAYS and + /// GL_REPLACE (mode::STENCIL_TEST), so it writes the reference under the + /// stencil write mask; another stencil comparison or operation around a + /// rectangle is reported and leaves the stencil unchanged. + pub fn gl_rect(&mut self) { + let mut f = [0f32; gl_cmd::RECT_FLOATS as usize]; + let mut anchor_x: u32 = 0; + let mut anchor_y: u32 = 0; + let patterned = (self.stream.pattern_mode & gl_cmd::RECT_PATTERN_ENABLE) != 0; + let rules = self.gl_operation_rules(); + let mut op = RasterOp { + kind: RASTER_CLEAR, + clear_colour_mask: u32::MAX, + ..Default::default() + }; + + for i in 0..f.len() { + f[i] = f32::from_bits(self.gl_rect[i]); + } + if patterned { + if self.stream.pattern_shape != pattern::SHAPE_32X32 + || !f[4].is_finite() + || !f[5].is_finite() + || !f[7].is_finite() + || !f[8].is_finite() + || f[4].trunc() != f[4] + || f[5].trunc() != f[5] + || f[7].trunc() != f[7] + || f[8].trunc() != f[8] + { + if !self.diag.gl_rect_pattern_logged { + self.diag.gl_rect_pattern_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: patterned rectangle requires shape 4 and finite integral corners"); + } + return; + } + // Reduce before conversion: integral floats can exceed int range. + anchor_x = ((self.win.origin[0] % 32).wrapping_add(((f[4] as f64) % 32.0) as i32) & 31) as u32; + anchor_y = ((self.win.origin[1] % 32).wrapping_add(((f[5] as f64) % 32.0) as i32) & 31) as u32; + } + if ((self.gl_write_mask >> write::COLOUR_SHIFT) & write::COLOUR_MASK) != 0 { + op.clear_buffers |= GL_COLOR_BUFFER_BIT; + } + if (self.gl_write_mask & write::DEPTH) != 0 { + let mut d = f[6] as f64 / (2.0 * gl_cmd::DEPTH_S); + d = cmin(cmax(d, 0.0), 1.0); + op.clear_buffers |= GL_DEPTH_BUFFER_BIT; + op.clear_depth24 = rules.depth_code(d); + } + if (self.stream.pattern_mode & mode::STENCIL_TEST) != 0 && self.gl_ref().cur.stencil_write_mask != 0 { + let st = &self.gl_ref().cur; + + if st.stencil_func == GL_ALWAYS && st.stencil_op[2] == GL_REPLACE { + // glClear's form (mode::STENCIL_TEST): write the reference. + op.clear_buffers |= GL_STENCIL_BUFFER_BIT; + op.clear_stencil = st.stencil_ref as u32; + op.stencil_mask = st.stencil_write_mask; + } else if !self.diag.gl_rect_stencil_logged { + let (func, sop) = (st.stencil_func, st.stencil_op[2]); + self.diag.gl_rect_stencil_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: stencil-tested rectangle under comparison {} and operation {:#x} is not modeled; stencil left unchanged", func, sop); + } + } + if (self.stream.pattern_mode & mode::DEPTH_TEST) != 0 && (self.gl_depth_func & ODSY_DEPTH_FUNC_MASK) != GL_ALWAYS && !self.diag.gl_rect_depth_logged { + self.diag.gl_rect_depth_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: depth-tested rectangle under comparison {} is not modeled; written unconditionally", self.gl_depth_func & ODSY_DEPTH_FUNC_MASK); + } + // No kept rectangle means every 0x204b rectangle is empty: the window is + // fully covered and nothing may be drawn. The shared clear reads a clip + // list with n = 0 as "no clip", so the rectangle is skipped here. (Reset + // is not this case: rectangle 0 then spans the whole range.) + if self.win.clip_kept == 0 || op.clear_buffers == 0 { + return; + } + let mut x0 = self.win.origin[0] as f64 + f[4] as f64; + let mut y0 = self.win.origin[1] as f64 + f[5] as f64; + let mut x1 = self.win.origin[0] as f64 + f[7] as f64; + let mut y1 = self.win.origin[1] as f64 + f[8] as f64; + if x0 > x1 { + std::mem::swap(&mut x0, &mut x1); + } + if y0 > y1 { + std::mem::swap(&mut y0, &mut y1); + } + // Pixel p is covered when x0 <= p + 0.5 < x1, clamped to the plane. + x0 = cmax((x0 - 0.5).ceil(), 0.0); + y0 = cmax((y0 - 0.5).ceil(), 0.0); + x1 = cmin((x1 - 0.5).ceil(), ODSY_WIDTH as f64) - 1.0; + y1 = cmin((y1 - 0.5).ceil(), ODSY_HEIGHT as f64) - 1.0; + if x0 > x1 || y0 > y1 { + return; + } + // The buffers the drawing mask selects; none leaves only depth. + op.ntargets = 0; + let buffers = self.draw_buffers(); + for b in 0..ODSY_BUFFERS { + if buffers & (1u32 << b) != 0 { + op.targets[op.ntargets as usize] = b; + op.ntargets += 1; + } + } + op.x0 = x0 as i32; + op.y0 = y0 as i32; + op.x1 = x1 as i32; + op.y1 = y1 as i32; + for i in 0..self.win.clip_kept as usize { + op.clip.r[i] = self.win.clip_rect[i]; + } + op.clip.n = self.win.clip_kept; + if !self.gl_index_mode() { + op.clear_colour_mask = (self.gl_write_mask >> write::COLOUR_SHIFT) & write::COLOUR_MASK; + } + if (op.clear_buffers & GL_COLOR_BUFFER_BIT) != 0 { + if self.gl_index_mode() { + // glClearIndex travels as the first float (the probe's 3.0). + op.clear_rgba = gl_index_rgba(float_index(self.gl_rect[0])); + } else { + op.clear_rgba.copy_from_slice(&f[..4]); + } + } + if !patterned { + self.gl_raster(&op); + return; + } + // Proposed shape-4 policy: the first corner anchors a repeating bitmap, + // independent of corner order. Admit runs through the existing clear so + // clear bits preserve both colour and depth, with clipping unchanged. + let (xs, xe) = (x0 as i32, x1 as i32); + let mut y = y0 as i32; + let mut runs = Vec::new(); + while y <= y1 as i32 { + let row = self.stream.pattern_buffer[((y as u32).wrapping_sub(anchor_y) & 31) as usize]; + let mut x = xs; + + op.y0 = y; + op.y1 = y; + while x <= xe { + while x <= xe && ((row >> (31 - ((x as u32).wrapping_sub(anchor_x) & 31))) & 1) == 0 { + x += 1; + } + if x > xe { + break; + } + op.x0 = x; + loop { + x += 1; + if !(x <= xe && ((row >> (31 - ((x as u32).wrapping_sub(anchor_x) & 31))) & 1) != 0) { + break; + } + } + op.x1 = x - 1; + runs.push(op); + } + y += 1; + } + if runs.is_empty() { + self.gl_report_refusals(op.kind != RASTER_CLEAR || (op.clear_buffers & GL_COLOR_BUFFER_BIT) != 0); + } + self.gl_raster_ops(runs); + } + + /// One QUADS rectangle: the bounding box of the four vertices from first, + /// painted through the id-plane, pattern or solid path. + pub fn gl_quad(&mut self, first: u32) { + let f = first as usize; + let mut minx = self.stream.vertex_x[f] as i32; + let mut maxx = minx; + let mut miny = self.stream.vertex_y[f] as i32; + let mut maxy = miny; + + for i in f + 1..f + 4 { + let vx = self.stream.vertex_x[i] as i32; + let vy = self.stream.vertex_y[i] as i32; + + minx = minx.min(vx); + maxx = maxx.max(vx); + miny = miny.min(vy); + maxy = maxy.max(vy); + } + + let x0 = (minx as i64).max(0); + let x1 = (maxx as i64).min(ODSY_WIDTH as i64); + let y0 = (miny as i64).max(0); + let y1 = (maxy as i64).min(ODSY_HEIGHT as i64); + if x0 >= x1 || y0 >= y1 { + return; + } + + // The X server's drawable-id plane write, marked by the 0x1017 + // destination word: it updates the did plane under the rectangle and does + // not touch the colour framebuffer. The id value is the raw COLOR_INDEX + // operand (id << 3), the mask the 0x1023 operand shifted down from its << + // 12 position. A pattern quad there is overlay text and goes through + // the pattern path, which Board::paint_integer() sends to the same + // bytes. + if self.dest_is_id_planes() && !self.pattern_enabled() { + let value = self.gl_color_index as u8; + let mask = ((self.plane_write_mask >> ODSY_PLANE_MASK_SHIFT) & 0xff) as u8; + self.clear_planes(x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32, value, mask); + return; + } + + let value = if self.gl_color_is_index { self.gl_color_index } else { self.stream.color }; + if self.pattern_enabled() { + let is_index = self.gl_color_is_index; + self.pattern_fill(x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32, (x0 - minx as i64) as u32, (y0 - miny as i64) as u32, is_index, value); + } else { + let is_index = self.gl_color_is_index; + self.fill_rect(x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32, is_index, value); + } + } + + /// One line or polygon pixel in the current colour, clipped to the plane. + pub fn gl_plot(&mut self, x: i64, y: i64) { + let is_index = self.gl_color_is_index; + let value = if is_index { self.gl_color_index } else { self.stream.color }; + self.paint_integer(x, y, is_index, value); + } + + /// One line segment. The DDX pushes an X span (x, y, w) as the vertices (x, y), + /// (x + w, y) for a solid span and in the reverse order for a stippled one, and + /// both are right only if a line paints the pixels whose centres lie strictly + /// between its endpoints along the major axis -- [min, max) -- whichever way it + /// runs (INFERRED from the two DDX routines; the same is what lets the X + /// server's line routine add a one-pixel stub to draw a polyline's last point). + /// Along the major axis pixel i is painted at the minor-axis position of its + /// centre, rounded down. With the stipple enabled (0x0404 bits 3 and 4) the k-th + /// covered pixel from the first vertex, k = 1 next to it, is painted only where + /// bit (k & 31) of the 0x0c48 pattern is set, and where it is clear, with 0x0404 + /// bit 17 set, in the 0x0c21 constant colour (Board::pattern_background()), + /// when 0x0c21's factors are ONE, ZERO. The X server's stippled-span and + /// dashed-line routines write no 0x043f, so its shape is whatever an earlier + /// fill left; they set bit 17 and send 0x0c21 = ONE, ZERO, background exactly + /// for FillOpaqueStippled spans and LineDoubleDash lines, the two forms whose + /// clear bits X paints in the background (ESTABLISHED as software behaviour + /// from the X driver module). Requiring ONE, ZERO keeps a GL client's blended + /// integer stippled line, bit 17 with its own factors, from painting clear + /// bits. That the board then paints the clear bits in the constant colour is + /// INFERRED as for the opaque quads. + pub fn gl_segment_pixel(&mut self, x: i64, y: i64, clear: bool) { + if !clear { + self.gl_plot(x, y); + } else if (self.stream.pattern_mode & mode::BLEND) != 0 && self.blend_word == blend::X_OPAQUE { + let is_index = self.gl_color_is_index; + let value = self.pattern_background(is_index); + self.paint_integer(x, y, is_index, value); + } + } + + pub fn gl_segment(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, stippled: bool) { + let pattern = self.stream.line_stipple[line::STIPPLE_PATTERN]; + let dx = x2 as f64 - x1 as f64; + let dy = y2 as f64 - y1 as f64; + + if stippled && self.stream.line_stipple[line::STIPPLE_CONTROL] != line::STIPPLE_CONTROL_32 && !self.diag.line_stipple_reported { + self.diag.line_stipple_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: integer line stipple control {:#x} is not modeled; drawn as a 32-pixel stipple", self.stream.line_stipple[line::STIPPLE_CONTROL]); + } + + // These captured Xlib modes distinguish inclusion of the final vertex. + // The bit's physical meaning is unknown. Limit this model policy to + // solid axial independent lines; other integer paths retain their rules. + if !stippled + && self.stream.primitive == GL_LINES + && ((self.stream.pattern_mode & !(1u32 << 25)) == 0x00080000 || (self.stream.pattern_mode & !(1u32 << 25)) == 0x00100000) + && (x1 == x2 || y1 == y2) + { + let omit_last = (self.stream.pattern_mode & (1u32 << 25)) != 0; + + if y1 == y2 { + let lo = (x1.min(x2) as i64).max(0); + let hi = (x1.max(x2) as i64).min(ODSY_WIDTH as i64 - 1); + let mut i = lo; + while i <= hi { + if !omit_last || i != x2 as i64 { + self.gl_plot(i, y1 as i64); + } + i += 1; + } + } else { + let lo = (y1.min(y2) as i64).max(0); + let hi = (y1.max(y2) as i64).min(ODSY_HEIGHT as i64 - 1); + let mut i = lo; + while i <= hi { + if !omit_last || i != y2 as i64 { + self.gl_plot(x1 as i64, i); + } + i += 1; + } + } + return; + } + if dx == 0.0 && dy == 0.0 { + return; + } + if dx.abs() >= dy.abs() { + let lo = (x1.min(x2) as i64).max(0); + let hi = (x1.max(x2) as i64).min(ODSY_WIDTH as i64); + let mut i = lo; + while i < hi { + let c = i as f64 + 0.5; + let k = ((c - x1 as f64).abs() + 0.5) as u32; + + self.gl_segment_pixel(i, (y1 as f64 + (c - x1 as f64) * dy / dx).floor() as i64, stippled && ((pattern >> (k & 31)) & 1) == 0); + i += 1; + } + } else { + let lo = (y1.min(y2) as i64).max(0); + let hi = (y1.max(y2) as i64).min(ODSY_HEIGHT as i64); + let mut i = lo; + while i < hi { + let c = i as f64 + 0.5; + let k = ((c - y1 as f64).abs() + 0.5) as u32; + + self.gl_segment_pixel((x1 as f64 + (c - y1 as f64) * dx / dy).floor() as i64, i, stippled && ((pattern >> (k & 31)) & 1) == 0); + i += 1; + } + } + } + + /// A POLYGON is filled by the even-odd rule, the pixels whose centres are + /// inside. It streams like every other integer primitive, so instead of a + /// vertex list it keeps crossing marks (poly_marks): each edge toggles, on + /// every row whose centre lies in [its lower y, its upper y) -- so a shared + /// vertex is crossed once -- the mark at the first pixel whose centre is at or + /// right of the crossing. At END a pixel is inside when an odd number of marks + /// lie at or left of it, which is the same pixel set as pairing the sorted + /// crossings of each row. INFERRED: the two traced polygons are four-vertex + /// convex trapezoids, for which every inside rule agrees. + pub fn gl_polygon_edge(&mut self, x1: i32, y1: i32, x2: i32, y2: i32) { + let lo = (y1.min(y2) as i64).max(0); + let hi = (y1.max(y2) as i64).min(ODSY_HEIGHT as i64); + let mut row = lo; + + while row < hi { + let c = row as f64 + 0.5; + let px = (x1 as f64 + (c - y1 as f64) * (x2 as f64 - x1 as f64) / (y2 as f64 - y1 as f64) - 0.5).ceil(); + + // A crossing right of the plane leaves every plane pixel outside. + if px >= ODSY_WIDTH as f64 { + row += 1; + continue; + } + let col: u32 = if px > 0.0 { px as u32 } else { 0 }; + self.stream.poly_marks[(row as usize) * ODSY_WIDTH as usize + col as usize] ^= 1; + self.stream.poly_row_lo = (self.stream.poly_row_lo as i64).min(row) as u32; + self.stream.poly_row_hi = (self.stream.poly_row_hi as i64).max(row + 1) as u32; + self.stream.poly_col_lo = self.stream.poly_col_lo.min(col); + row += 1; + } + } + + /// Clear the polygon's marks; with paint, first fill what they enclose. + pub fn gl_polygon_close(&mut self, paint: bool) { + let mut row = self.stream.poly_row_lo; + + while row < self.stream.poly_row_hi { + let base = row as usize * ODSY_WIDTH as usize; + let mut inside: u8 = 0; + + let mut col = self.stream.poly_col_lo; + while col < ODSY_WIDTH { + inside ^= self.stream.poly_marks[base + col as usize]; + self.stream.poly_marks[base + col as usize] = 0; + if paint && inside != 0 { + self.gl_plot(col as i64, row as i64); + } + col += 1; + } + row += 1; + } + self.stream.poly_row_lo = ODSY_HEIGHT; + self.stream.poly_row_hi = 0; + self.stream.poly_col_lo = ODSY_WIDTH; + } + + pub fn gl_hold(&mut self, slot: u32, x: u32, y: u32) { + self.stream.vertex_x[slot as usize] = x; + self.stream.vertex_y[slot as usize] = y; + } + + /// One integer vertex, window origin already added. The command FIFO is a + /// stream with no batch size, so each piece is drawn as its last vertex + /// arrives, the way the shared GL context assembles the float path, holding + /// only what a later piece needs: a QUADS rectangle's corners, a LINES span's + /// first vertex, a strip's first and previous vertex (slots 0 and 1). A + /// trailing partial piece -- a QUADS remainder, an odd LINES vertex -- is + /// never drawn, as glEnd drops it. A primitive this path does not draw is + /// reported at END. + /// + /// A POINTS vertex paints pixel (x, y). INFERRED: a line's pixels lie at the + /// minor-axis position rounded down (Board::gl_segment()), so an integer + /// vertex names the top-left corner of its pixel, and a size-one point lands + /// on the pixel GL's rule (floor of each coordinate) picks. The point size is + /// not modeled. + pub fn gl_vertex(&mut self, x: u32, y: u32) { + let n = self.stream.vertex_count; + self.stream.vertex_count = n.wrapping_add(1); + let stippled = (self.stream.pattern_mode & pattern::MODE_ENABLE) == pattern::MODE_ENABLE; + + // The (0, 0) vertex a BEGIN-0xa image carries is not geometry. + if self.image.active { + return; + } + match self.stream.primitive { + gl_cmd::POINTS => { + self.gl_plot(x as i32 as i64, y as i32 as i64); + } + gl_cmd::QUADS => { + // One rectangle per four vertices (the greeter's frame fills). + self.gl_hold(n % 4, x, y); + if n % 4 == 3 { + self.gl_quad(0); + } + } + gl_cmd::LINES => { + if n & 1 != 0 { + let (hx, hy) = (self.stream.vertex_x[0], self.stream.vertex_y[0]); + self.gl_segment(hx as i32, hy as i32, x as i32, y as i32, stippled); + } else { + self.gl_hold(0, x, y); + } + } + gl_cmd::LINE_STRIP | gl_cmd::LINE_LOOP => { + if n == 0 { + self.gl_hold(0, x, y); + } else { + let (hx, hy) = (self.stream.vertex_x[1], self.stream.vertex_y[1]); + self.gl_segment(hx as i32, hy as i32, x as i32, y as i32, stippled); + } + self.gl_hold(1, x, y); + } + gl_cmd::POLYGON => { + if n == 0 { + self.gl_hold(0, x, y); + } else { + let (hx, hy) = (self.stream.vertex_x[1], self.stream.vertex_y[1]); + self.gl_polygon_edge(hx as i32, hy as i32, x as i32, y as i32); + } + self.gl_hold(1, x, y); + } + _ => {} + } + } + + pub fn gl_end(&mut self) { + let count = self.stream.vertex_count; + let stippled = (self.stream.pattern_mode & pattern::MODE_ENABLE) == pattern::MODE_ENABLE; + + // END always closes the current primitive, the same as a real + // immediate-mode glEnd(): the next BEGIN starts a fresh vertex count, + // and a stray END with nothing newly accumulated since -- exactly + // what the console text path issues to close out its per-row pixel runs, + // which never call BEGIN again -- paints nothing. + self.stream.vertex_count = 0; + + // END also closes a BEGIN-0xa image. The box and bursts already painted + // as they streamed, so there is nothing more to do but leave image mode. + if self.image.active { + self.image.active = false; + self.image.bytes = 0; + self.image.nbytes = 0; + return; + } + + // Every piece but a loop's closing edge was drawn as it streamed. + match self.stream.primitive { + gl_cmd::POINTS | gl_cmd::QUADS | gl_cmd::LINES | gl_cmd::LINE_STRIP => {} + gl_cmd::LINE_LOOP => { + if count >= 2 { + let (ax, ay, bx, by) = (self.stream.vertex_x[1], self.stream.vertex_y[1], self.stream.vertex_x[0], self.stream.vertex_y[0]); + self.gl_segment(ax as i32, ay as i32, bx as i32, by as i32, stippled); + } + } + gl_cmd::POLYGON => { + // Fewer than three vertices enclose nothing. + if count >= 3 { + let (ax, ay, bx, by) = (self.stream.vertex_x[1], self.stream.vertex_y[1], self.stream.vertex_x[0], self.stream.vertex_y[0]); + self.gl_polygon_edge(ax as i32, ay as i32, bx as i32, by as i32); + } + self.gl_polygon_close(count >= 3); + } + _ => { + // A glBegin/glEnd pair with no vertex between draws nothing in GL + // (2.6.1), whatever the primitive. Performer's town and pointblank + // send such pairs, a texture coordinate and no vertex. + if count == 0 { + return; + } + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: command FIFO OpenGL primitive {:#x} is not modeled", self.stream.primitive); + return; + } + } + } +} diff --git a/src/dev/odyssey/gl.rs b/src/dev/odyssey/gl.rs new file mode 100644 index 000000000..4370b901a --- /dev/null +++ b/src/dev/odyssey/gl.rs @@ -0,0 +1,2201 @@ +//! The colour, depth, stencil, fog and texture hooks the board gives the shared +//! rasterizer, and the state its GL draws are made under. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use super::bits::sextract32; +use super::board::{ + Board, ODSY_CLIP_RECTS, ODSY_HEIGHT, ODSY_OBJECTS, ODSY_OBJ_POOL_WORDS, + ODSY_TEX_COLOUR_TABLE_ENTRIES, ODSY_TEX_DETAIL_FUNCTION_ENTRIES, ODSY_TEX_LEVELS, + ODSY_VOLUME_SLICES, ODSY_WIDTH, +}; +use super::consts::{ + clip, colour, gl_cmd, mode, pattern, prog, stencil, tex, write, ODSY_AUX_OVERLAY_BUFFER, + ODSY_BUFFERS, ODSY_DEPTH24_MAX, ODSY_DEPTH_FUNC_MASK, ODSY_DRAW_WORD_STATE, + ODSY_ID_COMPARE_ENABLE, ODSY_ID_COMPARE_MASK_SHIFT, ODSY_INDEX_MASK, + ODSY_VOLUME_SLICE_SHIFT_MAX, +}; +use super::draw::{gl_rules, id_compare_passes}; +use super::geometry::{f32_bits, gl_transpose, odyssey_f32}; +use super::gl_context::{ + op, vtx, ColourTarget, GL_ADD, GL_ALPHA, GL_ALPHA_TEST, GL_BACK, GL_BLEND, GL_CCW, GL_CLAMP, + GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE, GL_CLIP_PLANE0, GL_CLIP_PLANES, GL_COLOR_BUFFER_BIT, + GL_COLOR_LOGIC_OP, GL_CONSTANT_ALPHA, GL_CONSTANT_COLOR, GL_CULL_FACE, GL_CW, GL_DECAL, + GL_DECR, GL_DEPTH_TEST, GL_DRAW_BACK, GL_DRAW_FRONT, GL_DRAW_OK, GL_DST_ALPHA, GL_DST_COLOR, + GL_FILL, GL_FLAT, GL_FOG, GL_FRONT, GL_FRONT_AND_BACK, GL_FUNC_ADD, GL_FUNC_REVERSE_SUBTRACT, + GL_FUNC_SUBTRACT, GL_INCR, GL_INDEX_LOGIC_OP, GL_INTENSITY, GL_INVERT, GL_KEEP, GL_LINE, + GL_LINEAR, GL_LINEAR_DETAIL_ALPHA_SGIS, GL_LINEAR_DETAIL_COLOR_SGIS, GL_LINEAR_DETAIL_SGIS, + GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINE_SMOOTH, GL_LINE_STIPPLE, + GL_LOGIC_OP, GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_MAX, GL_MAX_TARGETS, GL_MIN, GL_MODULATE, + GL_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_NEVER_ENUM, + GL_ONE_MINUS_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_COLOR, GL_ONE_MINUS_DST_ALPHA, + GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_SRC_COLOR, GL_POINT, + GL_POINT_SMOOTH, GL_POLYGON_OFFSET_FILL, GL_POLYGON_OFFSET_LINE, GL_POLYGON_OFFSET_POINT, + GL_POLYGON_STIPPLE, GL_REPEAT, GL_REPLACE, GL_RGB, GL_RGBA, GL_SMOOTH, GL_SRC_ALPHA, + GL_SRC_ALPHA_SATURATE, GL_SRC_COLOR, GL_STENCIL_TEST, GL_TEXTURE, GlCmd, GlContext, GlDraw, + GlDrawResult, GlDrawState, GlDrawable, GlOp, GlSink, RASTER_CLEAR, RasterOp, +}; +use super::memory::{plane_bpp, word_load, word_store, words_fill, ClipRect, Plane, Rows, RowsMut}; +use super::raster::{ + cmax, cmin, GL_TEX_LEVELS_MAX, GlTexImage, GlTexLevel, GlTexPyramid, NoTexels, Planes, + RasterColour, RasterRules, RasterTarget, Texels, +}; +use super::sampler::{texture_level_format, Refusal, TexelFormat, Wrap}; +use super::raster_unit::{ + primitive_cost, rect_cost, touch_row, LINE_PHASE_ON_RASTER, PlaneSpans, RasterCtx, Span, + TOUCHED_OVERLAY, TouchedRows, +}; +/// One level's texels in the object store: `width` by `height` words from +/// word `offset`, rows `stride` words apart. +#[derive(Clone, Copy, Default)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub(super) struct Texture { + pub offset: u32, + pub stride: u32, + pub width: u32, + pub height: u32, + pub base_format: u32, + pub words: u32, + /// A 2D level's border rows are stored, so a one-row level is not 1D. + pub border_rows: bool, +} + +/// The detail function table's weights, unsigned 12-bit fractions, entry i +/// at LOD -i/32 (TextureDetailWeight::at()). +#[derive(Clone, Copy)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub(super) struct TextureDetailWeight { + pub table: [u16; ODSY_TEX_DETAIL_FUNCTION_ENTRIES as usize], +} + +impl Default for TextureDetailWeight { + fn default() -> Self { + TextureDetailWeight { table: [0; ODSY_TEX_DETAIL_FUNCTION_ENTRIES as usize] } + } +} + +#[derive(Clone, Copy, Default)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub(super) struct TextureVolume { + pub base: u32, + pub stride_units: u32, + pub level_word2: u32, + /// Its slices in tex_slice_offset/tex_slice_width (texture setup). + pub first_slice: u32, + pub slices: u32, + pub width: u32, + pub height: u32, + pub depth: u32, + pub base_format: u32, + pub words: u32, + pub colour_table_alpha: bool, + pub border: bool, + pub colour_table: bool, + pub scale_bias: bool, +} + +/// A GlTexPyramid's shape: its levels are the first `count` of +/// TextureChain::levels. +#[derive(Clone, Copy, Default)] +pub(super) struct PyramidShape { + pub count: u32, + pub base_level: u32, + pub max_level: u32, + pub min_lod: f64, + pub max_lod: f64, +} + +/// Where a draw's texture lives (Board::texture_setup()). +#[derive(Clone, Default)] +pub(super) struct TextureChain { + /// The level-zero volume, and the post-filter and detail weight state. + pub volume: TextureVolume, + pub volumes: [TextureVolume; GL_TEX_LEVELS_MAX as usize], + pub storage: [Texture; GL_TEX_LEVELS_MAX as usize], + pub levels: [GlTexLevel; GL_TEX_LEVELS_MAX as usize], + pub pyramid: Option, + pub detail_storage: Texture, + pub detail_image: GlTexLevel, + pub detail_weight: TextureDetailWeight, + /// The leading entries of `volumes`, `storage` and `levels` written since + /// the chain was last cleared; the rest are still default. + filled: usize, +} + +impl TextureChain { + /// Make the chain default again, writing only what a setup wrote. A chain + /// is about 7 KB, and a textured draw cleared all of it, though a typical + /// texture fills a few levels. + pub fn clear(&mut self) { + let n = self.filled.min(self.levels.len()); + + // Every setup in the tests proves `touch` covers what it writes. + #[cfg(test)] + { + assert!(self.volumes[n..].iter().all(|v| *v == TextureVolume::default()), "a written volume was not touched"); + assert!(self.storage[n..].iter().all(|t| *t == Texture::default()), "a written level store was not touched"); + assert!(self.levels[n..].iter().all(|l| *l == GlTexLevel::default()), "a written level was not touched"); + } + self.volumes[..n].fill(TextureVolume::default()); + self.storage[..n].fill(Texture::default()); + self.levels[..n].fill(GlTexLevel::default()); + self.volume = TextureVolume::default(); + self.pyramid = None; + self.detail_storage = Texture::default(); + self.detail_image = GlTexLevel::default(); + self.detail_weight = TextureDetailWeight::default(); + self.filled = 0; + } + + /// Note that level `i`'s entries are about to be written. + pub fn touch(&mut self, i: u32) { + self.filled = self.filled.max(i as usize + 1); + } + + /// image with this chain's pyramid. + pub fn image<'a>(&'a self, image: &GlTexImage<'static>) -> GlTexImage<'a> { + let mut image: GlTexImage<'a> = *image; + + image.pyramid = self.pyramid.map(|p| GlTexPyramid { + levels: &self.levels[..p.count as usize], + base_level: p.base_level, + max_level: p.max_level, + min_lod: p.min_lod, + max_lod: p.max_lod, + }); + image + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct TextureDetailSampler { + pub descriptor: u32, + pub width: u32, + pub height: u32, + pub level: i32, + pub mode: u32, +} + +impl Board { + /// Whether a GL fragment's colour is an index (prog::COLOUR_MODE). + pub fn gl_index_mode(&self) -> bool { + self.colour_mode[0] & colour::MODE_INDEX != 0 + } +} + +/// A colour index carried through the shared rasterizer, which knows only +/// RGBA: bits 7:0 of the index ride in blue and bits 11:8 in green, so at +/// the rules' eight colour bits the stored colour's low twelve bits are the +/// index again (gl_store_colour()). Smooth shading then +/// interpolates the index as GL does, within one run of 256. +pub(super) fn gl_index_rgba(index: u32) -> [f32; 4] { + [0.0, ((index >> 8) & 0xf) as f32 / 255.0f32, (index & 0xff) as f32 / 255.0f32, 1.0] +} + +/// A float index (glIndexf, glClearIndex): its integer part, wrapped to the +/// twelve index bits the way a write keeps only the buffer's bits. +pub(super) fn float_index(bits: u32) -> u32 { + let mut f = f32::from_bits(bits); + + if !f.is_finite() { + return 0; + } + // The integer part, wrapped to the buffer's twelve bits without an + // out-of-range conversion: fmodf is exact. + f = f.floor() % 4096.0f32; + if f < 0.0 { + f += 4096.0f32; + } + (f as u32) & ODSY_INDEX_MASK +} + +impl Board { + /// Whether GL draws go to the id-plane destination: it is selected and the + /// kernel has bound it as the draw drawable (0x608, bit 31 aside), as it + /// does for flight's overlay context (0x80142000; OBSERVED). The X server + /// selects the same destination for its own id writes, which a GL + /// context's draws do not follow. + /// + /// APPROXIMATION: this rule reconciles those two observations; it is not a + /// known board mechanism. An equally consistent alternative is that GL + /// draws simply follow the drawable 0x608 binds, whatever 0x1017 last + /// selected. + pub fn gl_to_aux(&mut self) -> bool { + self.dest_is_id_planes() && (self.draw_word & !ODSY_DRAW_WORD_STATE) == self.dest_base + } + + /// A GL draw aimed at the id-plane destination that has no modelled home. + pub fn gl_aux_refused(&mut self, what: &str) { + if !self.diag.gl_aux_reported { + self.diag.gl_aux_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: {} to the id-plane destination is not modeled; dropped", what); + } + } + + /// What GlPlanes declines, reported once, from the draw rather than from + /// a pixel: the conditions are fixed for the whole draw, and GlPlanes + /// then only stores. colour says whether the draw writes colour at + /// all. + pub fn gl_report_refusals(&mut self, colour: bool) { + let index_mask = self.gl_write_mask & write::INDEX_MASK; + let mask = (self.gl_write_mask >> write::COLOUR_SHIFT) & write::COLOUR_MASK; + let buffers = self.draw_buffers(); + + if !colour { + return; + } + if !self.gl_index_mode() { + if buffers != 0 && self.gl_to_aux() { + self.gl_aux_refused("an RGBA draw"); + } + return; + } + if index_mask != write::INDEX_MASK && !self.diag.gl_index_mask_logged { + self.diag.gl_index_mask_logged = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: index write mask {:#05x} is decoded but not applied", index_mask); + } + if mask != 0 && (buffers & !(1u32 << ODSY_AUX_OVERLAY_BUFFER)) != 0 && self.gl_to_aux() { + self.gl_aux_refused("a GL id-byte write"); + } + } +} + +/// The board state a GL draw tests and stores its pixels under, fixed for +/// the whole draw. +#[derive(Clone, Copy)] +pub(super) struct GlPixelState<'a> { + pub did: &'a [u8], + pub id_compare: u32, + pub clip_rect: [ClipRect; ODSY_CLIP_RECTS as usize], + pub clip_kept: u32, + pub write_mask: u32, + /// Board::gl_to_aux() for the draw. + pub to_aux: bool, + pub fog: [u32; 6], + pub colour: [Plane; ODSY_BUFFERS as usize], + pub depth: Plane, + pub stencil: Plane, +} + +impl GlPixelState<'_> { + /// Board::pixel_allowed(), from the draw's copy of its state. + pub fn pixel_allowed(&self, x: i64, y: i64) -> bool { + x >= 0 + && y >= 0 + && x < ODSY_WIDTH as i64 + && y < ODSY_HEIGHT as i64 + && id_compare_passes(self.did, self.id_compare, x, y) + && self.clip_rect[..self.clip_kept as usize] + .iter() + .any(|r| x >= r.x0 as i64 && x <= r.x1 as i64 && y >= r.y0 as i64 && y <= r.y1 as i64) + } +} + +/// The board's planes as a GL draw borrows them: the two colour buffers, +/// the model depth and stencil planes and the overlay byte. +pub(super) struct GlPixels<'a> { + pub state: GlPixelState<'a>, + pub colour: [RowsMut<'a, u8>; ODSY_BUFFERS as usize], + pub depth: RowsMut<'a, u8>, + pub stencil: RowsMut<'a, u8>, + pub overlay: RowsMut<'a, u8>, + /// The rows the draw stored colour to (see `GlPlanes`), for the raster + /// unit to hand the display. + pub touched: TouchedRows, + /// The colour buffers a lent row's stores may reach, bit b for buffer + /// b: what the draw's targets name. A lent row marks them all. + pub row_targets: u32, + /// GlPlanes::row() lends rows; tests clear it to compare the row loops + /// with the per-pixel path. + pub lend_rows: bool, +} + +impl GlPixels<'_> { + /// Hand the rows the draw wrote to the raster unit's record, for the + /// display to resolve (RasterCtx::drawn). + pub fn record(&self, ctx: &mut RasterCtx) { + ctx.drawn.add(&self.touched, self.state.id_compare); + } + + /// The buffers a list of colour targets names, as `row_targets` has them. + pub fn target_bits(targets: &[ColourTarget]) -> u32 { + targets.iter().filter(|&&t| t < ODSY_BUFFERS).fold(0, |bits, &t| bits | 1 << t) + } +} + +impl GlPixels<'_> { + /// The planes, for drawing on this thread. + pub fn planes(&mut self) -> GlPlanes<'_> { + let [c0, c1] = &mut self.colour; + + GlPlanes { + state: self.state, + colour: [c0.view(), c1.view()], + depth: self.depth.view(), + stencil: self.stencil.view(), + overlay: self.overlay.view(), + touched: &self.touched, + row_targets: self.row_targets, + lend_rows: self.lend_rows, + } + } +} + +/// GlPixels as one thread of a draw stores to them. Colour-index draws use +/// store_colour; RGBA draws store_rgba and load_rgba (Board::gl_target()). +/// Their once-only reports are made before the draw +/// (Board::gl_report_refusals()). +pub(super) struct GlPlanes<'a> { + state: GlPixelState<'a>, + colour: [Rows<'a, u8>; ODSY_BUFFERS as usize], + depth: Rows<'a, u8>, + stencil: Rows<'a, u8>, + overlay: Rows<'a, u8>, + /// Each row a colour store (or a lent row, for the draw's + /// `row_targets`) reached, by buffer: what the display resolves again. + touched: &'a TouchedRows, + row_targets: u32, + lend_rows: bool, +} + +/// One row of GlPlanes as a row loop reaches it (GlPlanes::row()): the depth +/// plane's and both colour buffers' words and the stencil plane's bytes for +/// the row's pixels, and of GlPixelState::pixel_allowed() only the id test, +/// which is per pixel; the plane bound and clip rectangles were tested for +/// the whole row. Its +/// methods take plane coordinates on that row and answer as GlPlanes would. +pub(super) struct GlPlaneRow<'a> { + x0: i32, + depth: &'a [std::cell::Cell], + depth_le: bool, + colour: [&'a [std::cell::Cell]; ODSY_BUFFERS as usize], + colour_le: [bool; ODSY_BUFFERS as usize], + stencil: &'a [std::cell::Cell], + /// Stores reach the planes: not a draw to the id-plane destination. + store: bool, + /// The id bytes of the row's pixels, or empty with the id test off. + did: &'a [u8], + id_value: u8, + id_mask: u8, +} + +impl GlPlaneRow<'_> { + #[inline(always)] + fn allowed(&self, i: usize) -> bool { + self.did.is_empty() || ((self.did[i] ^ self.id_value) & self.id_mask) == 0 + } + + /// Whether the id test passes the pixels x0 to x1 of the row. + fn all_allowed(&self, x0: i32, x1: i32) -> bool { + (x0..=x1).all(|x| self.allowed((x - self.x0) as usize)) + } + + /// store_depth() of one value at the row's pixels x0 to x1: a slice fill + /// where the id test passes them all, else a pixel at a time. + pub fn fill_depth(&self, x0: i32, x1: i32, depth: u32) { + if !self.store { + return; + } + if self.all_allowed(x0, x1) { + let (i0, i1) = ((x0 - self.x0) as usize, (x1 - self.x0) as usize); + + words_fill(&self.depth[4 * i0..4 * (i1 + 1)], self.depth_le, depth & ODSY_DEPTH24_MAX); + } else { + for x in x0..=x1 { + self.store_depth(x, 0, depth); + } + } + } + + /// store_rgba() of one colour at the row's pixels x0 to x1, as + /// fill_depth() does. + pub fn fill_rgba(&self, target: ColourTarget, x0: i32, x1: i32, colour: &RasterColour) { + if target >= ODSY_BUFFERS || !self.store { + return; + } + if self.all_allowed(x0, x1) { + let (i0, i1) = ((x0 - self.x0) as usize, (x1 - self.x0) as usize); + let c = &colour.component; + + words_fill( + &self.colour[target as usize][4 * i0..4 * (i1 + 1)], + self.colour_le[target as usize], + (c[0] << 24) | (c[1] << 16) | (c[2] << 8) | c[3], + ); + } else { + for x in x0..=x1 { + self.store_rgba(target, x, 0, colour); + } + } + } +} + +#[cfg(test)] +mod plane_row_tests { + use super::*; + use std::cell::Cell; + + const N: usize = 24; + + fn noise(seed: u8) -> Vec> { + (0..4 * N).map(|i| Cell::new((i as u8).wrapping_mul(37).wrapping_add(seed))).collect() + } + + fn bytes(v: &[Cell]) -> Vec { + v.iter().map(Cell::get).collect() + } + + // A run fill stores what the pixel stores do, in either byte order, with + // the id test passing all, some or none of the row's pixels, a draw to + // the id plane, a target past the buffers, and runs that start and end inside the row. + #[test] + fn run_fills_match_the_pixel_stores() { + let pass = [0u8; N]; + let mut some = [0u8; N]; + let none = [0xffu8; N]; + let mut one_in_range = [0u8; N]; + + for i in (0..N).step_by(5) { + some[i] = 0xff; + } + one_in_range[9] = 0xff; + let colour = RasterColour { component: [0x12, 0x34, 0x56, 0x78] }; + + for le in [false, true] { + for did in [&[][..], &pass[..], &some[..], &none[..], &one_in_range[..]] { + for store in [true, false] { + for (x0, x1) in [(0, N as i32 - 1), (0, 0), (3, 3), (4, 20), (N as i32 - 1, N as i32 - 1), (5, 8)] { + let (fd, sd) = (noise(1), noise(1)); + let (fc, sc) = ([noise(2), noise(4)], [noise(2), noise(4)]); + let stencil = noise(3); + let row = |depth, colour0, colour1| GlPlaneRow { + x0: 100, + depth, + depth_le: le, + colour: [colour0, colour1], + colour_le: [le, !le], + stencil: &stencil, + store, + did, + id_value: 0, + id_mask: 0xff, + }; + let fast = row(&fd, &fc[0], &fc[1]); + let slow = row(&sd, &sc[0], &sc[1]); + + fast.fill_depth(100 + x0, 100 + x1, 0xabcdef); + for target in 0..3 { + fast.fill_rgba(target, 100 + x0, 100 + x1, &colour); + } + for x in x0..=x1 { + slow.store_depth(100 + x, 0, 0xabcdef); + for target in 0..3 { + slow.store_rgba(target, 100 + x, 0, &colour); + } + } + assert_eq!(bytes(&fd), bytes(&sd), "depth, le {le}, did {did:?}, store {store}, {x0}..={x1}"); + assert_eq!(bytes(&fc[0]), bytes(&sc[0]), "colour 0, le {le}, did {did:?}, store {store}, {x0}..={x1}"); + assert_eq!(bytes(&fc[1]), bytes(&sc[1]), "colour 1, le {le}, did {did:?}, store {store}, {x0}..={x1}"); + } + } + } + } + } +} + +impl Planes for GlPlaneRow<'_> { + #[inline(always)] + fn store_rgba(&self, target: ColourTarget, x: i32, _y: i32, colour: &RasterColour) { + let i = (x - self.x0) as usize; + let c = &colour.component; + + if target < ODSY_BUFFERS && self.store && self.allowed(i) { + let word = (c[0] << 24) | (c[1] << 16) | (c[2] << 8) | c[3]; + + word_store(self.colour[target as usize], i, self.colour_le[target as usize], word); + } + } + + #[inline(always)] + fn load_rgba(&self, target: ColourTarget, x: i32, _y: i32) -> Option { + if target >= ODSY_BUFFERS { + return None; + } + let word = word_load(self.colour[target as usize], (x - self.x0) as usize, self.colour_le[target as usize]); + let mut colour = RasterColour::default(); + for i in 0..4u32 { + colour.component[i as usize] = (word >> (24 - 8 * i)) & 0xff; + } + Some(colour) + } + + #[inline(always)] + fn load_depth(&self, x: i32, _y: i32) -> u32 { + word_load(self.depth, (x - self.x0) as usize, self.depth_le) + } + + #[inline(always)] + fn store_depth(&self, x: i32, _y: i32, depth: u32) { + let i = (x - self.x0) as usize; + + if self.store && self.allowed(i) { + word_store(self.depth, i, self.depth_le, depth & ODSY_DEPTH24_MAX); + } + } + + #[inline(always)] + fn load_stencil(&self, x: i32, _y: i32) -> u32 { + self.stencil[(x - self.x0) as usize].get() as u32 + } + + /// As GlPlanes::store_stencil(): the pixel test alone, to_aux or not. + #[inline(always)] + fn store_stencil(&self, x: i32, _y: i32, stencil: u32) { + let i = (x - self.x0) as usize; + + if self.allowed(i) { + self.stencil[i].set((stencil & stencil::MAX) as u8); + } + } +} + +impl GlPlanes<'_> { + /// The stencil plane's bytes for pixels x0 to x0 + n - 1 of row y, as + /// Rows::plane_load() reaches each: None unless all are in the plane. + fn stencil_bytes(&self, y: i32, x0: i32, n: usize) -> Option<&[std::cell::Cell]> { + let p = &self.state.stencil; + + if plane_bpp(p.format) != 1 || !p.contains(x0, y) || !p.contains(x0 + n as i32 - 1, y) { + return None; + } + self.stencil.get(y as usize, x0 as usize, n) + } +} + +impl GlPlanes<'_> { + /// Plane pixels x0 to x1 of row y as a row loop reaches them, with the + /// plane bounds and clip rectangles tested once for the row: when the + /// whole row is on the screen, inside one kept clip rectangle, and in + /// the depth plane and both colour planes. Otherwise None, and the loop + /// reaches each pixel through GlPlanes itself, with the same results. + pub fn row(&self, y: i32, x0: i32, x1: i32) -> Option> { + let s = &self.state; + let n = (x1 as i64 - x0 as i64 + 1) as usize; + + if !self.lend_rows || x0 < 0 || y < 0 || x1 >= ODSY_WIDTH as i32 || y >= ODSY_HEIGHT as i32 || x1 < x0 { + return None; + } + if !s.clip_rect[..s.clip_kept as usize].iter().any(|r| y >= r.y0 && y <= r.y1 && x0 >= r.x0 && x1 <= r.x1) { + return None; + } + // The row's stores reach the buffers the draw targets. + if !s.to_aux { + for b in (0..ODSY_BUFFERS as usize).filter(|b| self.row_targets >> b & 1 != 0) { + touch_row(self.touched, b, y); + } + } + let did = if s.id_compare & ODSY_ID_COMPARE_ENABLE != 0 { + let at = y as usize * ODSY_WIDTH as usize + x0 as usize; + s.did.get(at..at + n)? + } else { + &[] + }; + Some(GlPlaneRow { + x0, + depth: self.depth.plane_words(&s.depth, y, x0, n)?, + depth_le: s.depth.is_le(), + colour: [ + self.colour[0].plane_words(&s.colour[0], y, x0, n)?, + self.colour[1].plane_words(&s.colour[1], y, x0, n)?, + ], + colour_le: [s.colour[0].is_le(), s.colour[1].is_le()], + stencil: self.stencil_bytes(y, x0, n)?, + store: !s.to_aux, + did, + id_value: s.id_compare as u8, + id_mask: (s.id_compare >> ODSY_ID_COMPARE_MASK_SHIFT) as u8, + }) + } + + /// A colour-index fragment's packed index word. + pub fn store_colour(&self, target: ColourTarget, x: i32, y: i32, colour: u32) { + let s = &self.state; + let mask = (s.write_mask >> write::COLOUR_SHIFT) & write::COLOUR_MASK; + + if mask == 0 || target >= ODSY_BUFFERS || !s.pixel_allowed(x as i64, y as i64) { + return; + } + if s.to_aux { + // A colour-index GL draw to the id-plane destination: an overlay + // context's drawing (overlay::MODE_BASE). Buffer 1 is the + // overlay pixel byte and takes the index's low eight bits; the id + // byte is the X server's alone, so a GL write to buffer 0 is + // reported and dropped. + if target == ODSY_AUX_OVERLAY_BUFFER { + if let Some(px) = self.overlay.get(y as usize, x as usize, 1) { + px[0].set((colour & 0xff) as u8); + touch_row(self.touched, TOUCHED_OVERLAY, y); + } + } + return; + } + touch_row(self.touched, target as usize, y); + self.colour[target as usize].plane_store(&s.colour[target as usize], x, y, colour & ODSY_INDEX_MASK); + } + + /// Experimental policy; the board's scale sign and clamps are unmeasured. + pub fn fog_factor(&self, window_z: f32) -> f32 { + let w = &self.state.fog; + let scale = if (w[2] & 0xffffff) != 0 { + f64::from(odyssey_f32(((w[2] & 0xffffff) << 5).wrapping_add(96u32 << 23)).abs()) + } else { + 0.0 + }; + let mut t = (scale * window_z as f64 + sextract32(w[3], 0, 12) as f64 / 256.0) + * 2.0f64.powi(((w[3] >> 12) & 7) as i32); + let p = [ + sextract32(w[4] >> 12, 0, 12) as f64 / 256.0, + sextract32(w[4], 0, 12) as f64 / 256.0, + sextract32(w[5] >> 12, 0, 12) as f64 / 256.0, + sextract32(w[5], 0, 12) as f64 / 256.0, + ]; + + t = t.clamp(0.0, 1.0); + let u = 1.0 - t; + let f = p[0] * u * u * u + 3.0 * p[1] * u * u * t + 3.0 * p[2] * u * t * t + p[3] * t * t * t; + (if f >= 1.0 { 1.0 } else if f > 0.0 { f } else { 0.0 }) as f32 + } +} + +impl Planes for GlPlanes<'_> { + /// The shared renderer supplies all four already quantized channel bytes. + fn store_rgba(&self, target: ColourTarget, x: i32, y: i32, colour: &RasterColour) { + let s = &self.state; + let c = &colour.component; + + if target < ODSY_BUFFERS && s.pixel_allowed(x as i64, y as i64) && !s.to_aux { + let word = (c[0] << 24) | (c[1] << 16) | (c[2] << 8) | c[3]; + + touch_row(self.touched, target as usize, y); + self.colour[target as usize].plane_store(&s.colour[target as usize], x, y, word); + } + } + + /// Storage the plane does not reach reads as nothing, not as zero. + fn load_rgba(&self, target: ColourTarget, x: i32, y: i32) -> Option { + if target >= ODSY_BUFFERS { + return None; + } + let p = &self.state.colour[target as usize]; + let rows = &self.colour[target as usize]; + if !p.contains(x, y) || rows.get(y as usize, x as usize * 4, 4).is_none() { + return None; + } + let word = rows.plane_load(p, x, y); + let mut colour = RasterColour::default(); + for i in 0..4u32 { + colour.component[i as usize] = (word >> (24 - 8 * i)) & 0xff; + } + Some(colour) + } + + /// The depth plane (set up by Board::allocate()): the board's 24-bit depth word + /// (depth_word.rs), 0 nearest, + /// through the same pixel test as a colour write. It is a model plane + /// because the board memory the real depth buffer occupies is not known. + fn load_depth(&self, x: i32, y: i32) -> u32 { + self.depth.plane_load(&self.state.depth, x, y) + } + + fn store_depth(&self, x: i32, y: i32, depth: u32) { + // The overlay has no depth; an overlay clear leaves the main depth. + if !self.state.to_aux && self.state.pixel_allowed(x as i64, y as i64) { + self.depth.plane_store(&self.state.depth, x, y, depth & ODSY_DEPTH24_MAX); + } + } + + /// The stencil plane (set up by Board::allocate()): 8 bits, through the + /// same pixel test as a depth write. A model plane: where the board keeps + /// its stencil planes is not known. + fn load_stencil(&self, x: i32, y: i32) -> u32 { + self.stencil.plane_load(&self.state.stencil, x, y) + } + + fn store_stencil(&self, x: i32, y: i32, stencil: u32) { + if self.state.pixel_allowed(x as i64, y as i64) { + self.stencil.plane_store(&self.state.stencil, x, y, stencil & stencil::MAX); + } + } +} + +/// The board's planes and pixel state as a raster job takes them: the +/// pixel state of the moment it was set up, and spans of the planes the +/// raster unit owns (raster_unit.rs). +#[derive(Clone, Copy)] +pub(super) struct GlPixelJob { + pub state: GlPixelState<'static>, + pub spans: PlaneSpans, +} + +impl GlPixelJob { + /// The planes, as GlPixels borrows them. + /// + /// # Safety + /// As `Span::bytes`: only the raster unit, or a caller the raster unit + /// has finished for, uses the planes meanwhile. + pub unsafe fn pixels<'a>(&self) -> GlPixels<'a> { + let s = &self.spans; + let st = &self.state; + // SAFETY: the caller's contract (this function's # Safety) is + // `Span::bytes`'s: the planes belong to the raster unit or to a + // caller it has finished for, so the spans' bytes are not aliased. + unsafe { + GlPixels { + state: GlPixelState { did: s.did.bytes(), ..*st }, + colour: [ + RowsMut::new(s.colour[0].bytes(), st.colour[0].stride as usize), + RowsMut::new(s.colour[1].bytes(), st.colour[1].stride as usize), + ], + depth: RowsMut::new(s.depth.bytes(), st.depth.stride as usize), + stencil: RowsMut::new(s.stencil.bytes(), st.stencil.stride as usize), + overlay: RowsMut::new(s.overlay.bytes(), ODSY_WIDTH as usize), + touched: Default::default(), + row_targets: 3, + lend_rows: true, + } + } + } +} + +/// The texture tables a textured draw reads, copied when it is set up: a +/// later command may rewrite them before the raster unit draws. Only what +/// the chain uses is copied. +#[derive(Default)] +pub(super) struct TexelTables { + slice_offset: Vec, + slice_width: Vec, + colour_table: Option>, + scale_bias: [u32; 8], +} + +static NO_COLOUR_TABLE: [[u32; ODSY_TEX_COLOUR_TABLE_ENTRIES as usize]; 2] = + [[0; ODSY_TEX_COLOUR_TABLE_ENTRIES as usize]; 2]; + +impl TexelTables { + fn of(s: &Board, chain: Option<&TextureChain>) -> Self { + let Some(chain) = chain else { + return TexelTables::default(); + }; + // Only a volume reads the slice tables (BoardTexels::read_3d()). + let volume = chain.volume.slices != 0 || chain.volumes.iter().any(|v| v.slices != 0); + TexelTables { + slice_offset: if volume { s.tex.slice_offset.to_vec() } else { Vec::new() }, + slice_width: if volume { s.tex.slice_width.to_vec() } else { Vec::new() }, + colour_table: chain.volume.colour_table.then(|| Box::new(s.tex.colour_table)), + scale_bias: s.tex.scale_bias, + } + } + + /// The board's texels, as a raster job reads them. + /// + /// # Safety + /// As `Span::bytes` for the object store. + unsafe fn texels<'a>(&'a self, chain: &'a TextureChain, obj: Span) -> BoardTexels<'a> { + let obj: &'a [u8] = unsafe { obj.bytes() }; + BoardTexels { + chain, + obj, + obj_words: obj.len() as u64 / 4, + slice_offset: &self.slice_offset, + slice_width: &self.slice_width, + colour_table: self.colour_table.as_deref().unwrap_or(&NO_COLOUR_TABLE), + scale_bias: &self.scale_bias, + } + } +} + +/// A GL draw's fields other than its borrows, for a raster job to own. +#[derive(Clone, Copy)] +struct GlDrawFields { + context: u32, + mode: u32, + count: u32, + back_colors: bool, + point_size: f32, + line_width: f32, + point_smooth: bool, + line_smooth: bool, + polygon_smooth: bool, + polygon_stipple_enabled: bool, + polygon_modes: Option<[u32; 2]>, + fill_by_triangle: bool, + line_stipple_enabled: bool, + line_stipple_repeat: u32, + line_stipple_pattern: u32, + line_phase: Option, + line_phase_reset: bool, +} + +impl GlDrawFields { + fn of(d: &GlDraw) -> Self { + GlDrawFields { + context: d.context, + mode: d.mode, + count: d.count, + back_colors: d.back_colors, + point_size: d.point_size, + line_width: d.line_width, + point_smooth: d.point_smooth, + line_smooth: d.line_smooth, + polygon_smooth: d.polygon_smooth, + polygon_stipple_enabled: d.polygon_stipple_enabled, + polygon_modes: d.polygon_modes, + fill_by_triangle: d.fill_by_triangle, + line_stipple_enabled: d.line_stipple_enabled, + line_stipple_repeat: d.line_stipple_repeat, + line_stipple_pattern: d.line_stipple_pattern, + line_phase: d.line_phase, + line_phase_reset: d.line_phase_reset, + } + } + + fn draw<'a>(&self, vertices: &'a [f32], state: &'a GlDrawState, polygon_stipple: &'a [u32; 32]) -> GlDraw<'a> { + let f = *self; + GlDraw { + context: f.context, + mode: f.mode, + count: f.count, + vertices, + back_colors: f.back_colors, + state, + point_size: f.point_size, + line_width: f.line_width, + point_smooth: f.point_smooth, + line_smooth: f.line_smooth, + polygon_smooth: f.polygon_smooth, + polygon_stipple, + polygon_stipple_enabled: f.polygon_stipple_enabled, + polygon_modes: f.polygon_modes, + fill_by_triangle: f.fill_by_triangle, + line_stipple_enabled: f.line_stipple_enabled, + line_stipple_repeat: f.line_stipple_repeat, + line_stipple_pattern: f.line_stipple_pattern, + line_phase: f.line_phase, + line_phase_reset: f.line_phase_reset, + } + } +} + +/// One GL primitive set up for the raster unit: everything Board::gl_draw() +/// read of the board, owned. +/// +/// The raster unit hands a finished job back (RasterQueue::give_back) and +/// the front end fills it again for a later draw, keeping its vertex +/// buffer and texture chain: allocating them per draw on the front end and +/// freeing them on the raster thread had the two threads contending for +/// the allocator. +pub(super) struct GlDrawJob { + rules: RasterRules, + target: RasterTarget<'static>, + image: GlTexImage<'static>, + /// Kept across reuse; the draw reads it only when `textured`. + chain: Option>, + textured: bool, + tables: TexelTables, + state: GlDrawState, + vertices: Vec, + polygon_stipple: [u32; 32], + fields: GlDrawFields, + pixels: GlPixelJob, +} + +impl GlDrawJob { + /// Draw it; the result, and the line-stipple phase the context keeps. + pub fn run(&self, ctx: &mut RasterCtx) -> (GlDrawResult, Option) { + let mut t = self.target.clone(); + let chain = self.chain.as_deref().filter(|_| self.textured); + t.texture = chain.map(|chain| chain.image(&self.image)); + let mut d = self.fields.draw(&self.vertices, &self.state, &self.polygon_stipple); + if d.line_phase == Some(LINE_PHASE_ON_RASTER) { + d.line_phase = Some(ctx.line_phase); + } + // SAFETY: the raster unit owns the planes while the job runs + // (PlaneSpans). + let mut pixels = unsafe { self.pixels.pixels() }; + let mut targets = [0; GL_MAX_TARGETS as usize]; + let n = self.state.draw_targets(&mut targets) as usize; + pixels.row_targets = GlPixels::target_bits(&targets[..n]); + // SAFETY: the raster unit owns the board memory the spans name while + // the job runs (PlaneSpans), and the chain is the one the job holds. + let texels = chain.map(|chain| unsafe { self.tables.texels(chain, self.pixels.spans.obj) }); + let texels: &dyn Texels = match &texels { + Some(texels) => texels, + None => &NoTexels, + }; + let result = self.rules.draw(&t, &mut pixels, texels, &mut d); + pixels.record(ctx); + if let Some(phase) = d.line_phase { + ctx.line_phase = phase; + } + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: GL draw path: backend scalar result {} called=1", result); + (result, d.line_phase) + } +} + +impl Board { + /// The board state a GL draw's pixels pass under, without the id bytes. + pub fn gl_pixel_state(&mut self) -> GlPixelState<'static> { + GlPixelState { + did: &[], + id_compare: self.win.id_compare, + clip_rect: self.win.clip_rect, + clip_kept: self.win.clip_kept, + write_mask: self.gl_write_mask, + to_aux: self.gl_to_aux(), + fog: self.gl_fog, + colour: [ + self.planes.colour.plane(self.planes.colour_plane).expect("the colour plane"), + self.planes.buffer1.plane(self.planes.buffer1_plane).expect("colour buffer 1's plane"), + ], + depth: self.planes.depth.plane(self.planes.depth_plane).expect("the depth plane"), + stencil: self.planes.stencil.plane(self.planes.stencil_plane).expect("the stencil plane"), + } + } + + /// The board's planes as a GL draw borrows them, and, for a textured + /// draw, its texels in chain, for a draw on the caller's thread. It + /// waits for the raster unit's queued jobs first. + pub fn gl_parts<'a>(&'a mut self, chain: Option<&'a TextureChain>) -> (GlPixels<'a>, Option>) { + self.raster_wait(); + let state = self.gl_pixel_state(); + let texels = chain.map(|chain| BoardTexels { + chain, + obj: &self.objects.mem.bytes, + obj_words: self.objects.mem.size / 4, + slice_offset: &self.tex.slice_offset, + slice_width: &self.tex.slice_width, + colour_table: &self.tex.colour_table, + scale_bias: &self.tex.scale_bias, + }); + let pixels = GlPixels { + state: GlPixelState { did: &self.planes.did, ..state }, + colour: [self.planes.colour.rows(&state.colour[0]), self.planes.buffer1.rows(&state.colour[1])], + depth: self.planes.depth.rows(&state.depth), + stencil: self.planes.stencil.rows(&state.stencil), + overlay: RowsMut::new(&mut self.planes.overlay, ODSY_WIDTH as usize), + touched: Default::default(), + row_targets: 3, + lend_rows: true, + }; + (pixels, texels) + } +} + +impl Board { + /// How far past [0, 1] a fragment's depth may round and still be drawn, + /// clamped: 16 units in the last place of a single near 1 (gl_cmd::DEPTH_SLACK + /// in consts.rs). APPROXIMATION: the depth row and block 16 arrive as + /// singles, and a library that maps the far plane to 0.99999999 of the range + /// leaves less margin than their rounding takes. A board does its + /// arithmetic in its own precision and the clip it applies at the far plane + /// is UNKNOWN. + /// + /// The depth row over the block-16 row, in units of the client's [0, 1] + /// depth (Board::gl_load_combined()). The shared context's model-view is + /// the identity, so its eye coordinates are the object coordinates the + /// combined matrix takes. Block 16 is the divisor only when all four of its + /// words are finite and the fourth is not zero; otherwise the sign-only + /// fallback divides by a constant. + pub fn gl_operation_rules(&mut self) -> RasterRules { + let mut rules = gl_rules().clone(); + let scale = 2.0 * gl_cmd::DEPTH_S * self.gl_depth_scale(); + let mut block16 = [0.0f64; 4]; + let mut divisor = true; + + rules.alpha_bits = if self.gl_index_mode() { 0 } else { 8 }; + rules.depth_row_given = true; + for k in 0..4usize { + rules.depth_row[k] = f64::from(odyssey_f32(self.gl_combined[8 + k])) / scale; + block16[k] = f64::from(odyssey_f32(self.gl_block16[k])); + divisor &= block16[k].is_finite(); + } + if divisor && block16[3] != 0.0 { + rules.depth_divisor_given = true; + for k in 0..4usize { + rules.depth_divisor[k] = block16[k] / block16[3]; + } + } + rules.depth_slack = gl_cmd::DEPTH_SLACK; + rules + } +} + +impl Board { + /// An opaque polygon stipple (0x043f bit 4 with shape 0, the GL stipple): + /// clear bits take the 0x0c21 constant colour, as the 2D path's opaque + /// patterns do (Board::pattern_background()). That the board paints the + /// clear bits here as it does for the 2D quads is INFERRED: it is the only + /// reading under which the X server's FillOpaqueStippled sequence draws an + /// opaque stipple. + pub fn gl_stipple_background(&mut self, t: &mut RasterTarget) { + if (self.stream.pattern_shape & pattern::SHAPE_OPAQUE) == 0 { + return; + } + t.stipple_opaque = true; + if self.gl_index_mode() { + t.stipple_background = gl_index_rgba(self.pattern_background(true)); + } else { + let pixel = self.pattern_background(false); + + for k in 0..3u32 { + t.stipple_background[k as usize] = ((pixel >> (24 - 8 * k)) & 0xff) as f32 / 255.0f32; + } + t.stipple_background[3] = 1.0; + } + } + + /// What the board's planes offer a GL draw (GlPlanes). + pub fn gl_target(&mut self) -> RasterTarget<'static> { + let mut t = RasterTarget { index: self.gl_index_mode(), ..Default::default() }; + + self.gl_stipple_background(&mut t); + t + } +} + +/// The board's texture storage as a textured GL draw reads it: the levels +/// Board::texture_setup() found in the object store, and the post-filter +/// state. +pub(super) struct BoardTexels<'a> { + pub chain: &'a TextureChain, + pub obj: &'a [u8], + /// The object store's size in words. + pub obj_words: u64, + pub slice_offset: &'a [u32], + pub slice_width: &'a [u32], + pub colour_table: &'a [[u32; ODSY_TEX_COLOUR_TABLE_ENTRIES as usize]; 2], + pub scale_bias: &'a [u32; 8], +} + +impl BoardTexels<'_> { + /// Texel (x, y) of texture, packed RRGGBBAA. + pub fn read(&self, texture: &Texture, x: u32, y: u32) -> Option { + if x >= texture.width || y >= texture.height { + return None; + } + // y * stride cannot overflow 64 bits. + let word = y as u64 * texture.stride as u64 + texture.offset as u64 + x as u64; + let end = word + 1; + if end > texture.words as u64 || end > self.obj_words { + return None; + } + let at = (word * 4) as usize; + let rgba = u32::from_be_bytes(self.obj.get(at..at + 4)?.try_into().unwrap()); + Some(if texture.base_format == GL_ALPHA { (rgba >> 24) & 0xff } else { rgba }) + } + + /// A border texel of level `level` of the 2D pyramid. + pub fn read_border_level(&self, level: u32, x: i32, y: i32) -> Option { + let mut texture = *self.chain.storage.get(level as usize)?; + + if texture.height == 1 && !texture.border_rows { + if y != 0 + || texture.offset == 0 + || (x != -1 && x != texture.width as i32) + { + return None; + } + texture.offset -= 1; + texture.width += 2; + return self.read(&texture, (x + 1) as u32, 0); + } + if x < -1 + || x > texture.width as i32 + || y < -1 + || y > texture.height as i32 + || (x >= 0 && x < texture.width as i32 && y >= 0 && y < texture.height as i32) + || (texture.offset as u64) < texture.stride as u64 + 1 + { + return None; + } + // Setup takes a bordered level only when its whole border was written. + texture.offset -= texture.stride + 1; + texture.width += 2; + texture.height += 2; + self.read(&texture, (x + 1) as u32, (y + 1) as u32) + } + + /// Texel (x, y, z) of volume, its border included when border is set. + pub fn read_3d(&self, volume: &TextureVolume, x: i32, y: i32, z: i32, border: bool) -> Option { + if (border + && (x < -1 || y < -1 || z < -1 || x > volume.width as i32 || y > volume.height as i32 || z > volume.depth as i32)) + || (!border + && (x < 0 || y < 0 || z < 0 || x >= volume.width as i32 || y >= volume.height as i32 || z >= volume.depth as i32)) + { + return None; + } + // Setup found and checked the object of every slice a read in these + // bounds can reach (Board::texture_setup()), so the slice's place in + // the object store is read from its table, not looked up per texel. + let slice = if border { (z + 1) as u32 } else { z as u32 }; + if slice >= volume.slices { + return None; + } + let at = (volume.first_slice + slice) as usize; + let stride = self.slice_width[at]; + let width = if border { volume.width + 2 } else { volume.width }; + let height = if border { volume.height + 2 } else { volume.height }; + let px = if border { (x + 1) as u32 } else { x as u32 }; + let py = if border { (y + 1) as u32 } else { y as u32 }; + let offset = if border { + (stride as u64 * 15 + 15).checked_add(self.slice_offset[at] as u64)? as u32 + } else { + self.slice_offset[at] + }; + let texture = Texture { + offset, + stride, + width, + height, + base_format: volume.base_format, + words: volume.words, + border_rows: false, + }; + self.read(&texture, px, py) + } + + /// The texture colour table: each normalized component indexes it after + /// filtering. + pub fn colour_table(&self, alpha: bool, rgba: &mut [f64; 4]) { + for (channel, c) in rgba.iter_mut().enumerate().take(3) { + let index = (cmax(0.0, cmin(*c, 1.0)) * 255.0 + 0.5).floor() as u32; + let plane = (channel == 2) as usize; + let word = self.colour_table[plane][index as usize]; + let component = if channel == 1 { word & 0xfff } else { (word >> 12) & 0xfff }; + + *c = component as f64 / 4095.0; + } + if alpha { + let index = (cmax(0.0, cmin(rgba[3], 1.0)) * 255.0 + 0.5).floor() as u32; + let word = self.colour_table[1][index as usize]; + + rgba[3] = (word & 0xfff) as f64 / 4095.0; + } + } + + /// GL_SGIX_texture_scale_bias applies scale, then bias, then a clamp to + /// [0, 1] to the filtered texel, before the texture colour table and the + /// environment. That the board follows the extension's order and clamp is + /// INFERRED: no source gives the board's arithmetic. + pub fn scale_bias(&self, rgba: &mut [f64; 4]) { + for c in 0..4usize { + let scale = sextract32(self.scale_bias[c * 2], 0, tex::SCALE_BIAS_BITS) as f64; + let bias = sextract32(self.scale_bias[c * 2 + 1], 0, tex::SCALE_BIAS_BITS) as f64; + + rgba[c] = (rgba[c] * scale + bias) / tex::SCALE_BIAS_ONE as f64; + rgba[c] = cmin(cmax(rgba[c], 0.0), 1.0); + } + } +} + +impl Texels for BoardTexels<'_> { + fn texel(&self, level: Option<&GlTexLevel>, x: u32, y: u32) -> Option { + match level { + Some(l) if l.detail => (l.level == 0).then(|| self.read(&self.chain.detail_storage, x, y))?, + Some(l) => self.read(self.chain.storage.get(l.level as usize)?, x, y), + None => None, + } + } + + fn border_texel(&self, level: Option<&GlTexLevel>, x: i32, y: i32) -> Option { + match level { + Some(l) if !l.detail => self.read_border_level(l.level, x, y), + _ => None, + } + } + + fn texel_3d(&self, level: Option<&GlTexLevel>, x: u32, y: u32, z: u32) -> Option { + let volume = match level { + Some(l) => self.chain.volumes.get(l.level as usize)?, + None => &self.chain.volume, + }; + self.read_3d(volume, x as i32, y as i32, z as i32, volume.border) + } + + fn border_texel_3d(&self, level: Option<&GlTexLevel>, x: i32, y: i32, z: i32) -> Option { + match level { + None => self.read_3d(&self.chain.volume, x, y, z, true), + Some(_) => None, + } + } + + fn post_filter(&self, rgba: &mut [f64; 4]) { + let volume = &self.chain.volume; + + if volume.scale_bias { + self.scale_bias(rgba); + } + if volume.colour_table { + self.colour_table(volume.colour_table_alpha, rgba); + } + } + + fn filter_weight(&self, lambda: f64) -> Option { + self.chain.detail_weight.at(lambda) + } +} + +/// A volume level's slices sit 2^(word 2) tiles of 256 texels apart, each +/// texel the bytes the library books for the level's format, in 16-byte +/// address units. The library places each slice's upload so and sends the +/// tile count's base-2 ceiling in word 2 (ESTABLISHED). That the board +/// derives its slice stride the same way is INFERRED: it is the only reading +/// under which an upload and a later sample agree, as are the 16-byte unit +/// and the 256-texel tile. For a 4-byte format this gives the spacing seen +/// three ways on the model: 0x80 apart for word 2 1, 0x400 for 4 and 0x1000 +/// for 6. +pub(super) fn volume_slice_stride(level_word2: u32, format: TexelFormat) -> u64 { + (256u64 * format.booked() as u64 / 16).wrapping_shl(level_word2) +} + +/// The detail function table (0x3e40-0x3e4f): the GL library fills its 256 +/// entries with the application's function at LOD -i/32, interpolated +/// linearly between the points and held beyond the end ones, as unsigned +/// 12-bit fractions in bits 23:12 (ESTABLISHED as the library's table +/// builder). Beyond +/// the table's ends the weight is the end entry's, as SGIS_detail_texture +/// holds the function past its end points. Between two entries the model +/// interpolates linearly, which gives back the application's function +/// wherever its points sit on the 1/32 grid. How the board itself indexes +/// and rounds the table between entries is UNKNOWN: APPROXIMATION. +impl TextureDetailWeight { + /// The detail and sharpen weight at lambda, where the table gives one. + pub fn at(&self, lambda: f64) -> Option { + let last = (ODSY_TEX_DETAIL_FUNCTION_ENTRIES - 1) as usize; + let weight = |i: usize| self.table[i] as f64 / 4096.0; + + if lambda.is_nan() { + return None; + } + if lambda >= 0.0 { + return Some(weight(0)); + } + if lambda <= tex::DETAIL_FUNCTION_LAST_LOD { + return Some(weight(last)); + } + let x = -lambda * 32.0; + let i = (x as usize).min(last - 1); + let f = x - i as f64; + Some(weight(i) + (weight(i + 1) - weight(i)) * f) + } +} + +/// Post-filter scale and bias (0x203b, where texture validation sends a +/// texture's words): eight words, a scale and a bias for each of R, G, B +/// and A in turn, each signed 3.12 in bits 15:0. The GL library clamps both +/// to -8 through 7.999755859375 and defaults them to 4096 (1.0) and 0 +/// (ESTABLISHED as the library's conversion). Pairing scale before bias +/// within a channel is INFERRED from the order the library names them. +/// The library never sets bits 31:16, so a word that does is unknown state +/// and drops the draw. The reset value is the library's default, a model +/// choice. +impl Board { + pub fn texture_scale_bias_known(&self) -> bool { + self.tex.scale_bias[..tex::SCALE_BIAS_WORDS as usize] + .iter() + .all(|&w| (w >> tex::SCALE_BIAS_BITS) == 0) + } + + pub fn texture_scale_bias_identity(&self) -> bool { + for c in 0..4usize { + if self.tex.scale_bias[c * 2] != tex::SCALE_BIAS_ONE || self.tex.scale_bias[c * 2 + 1] != 0 { + return false; + } + } + true + } + +} + +/// The detail texture a sampler word 0 names under a detail magnification +/// filter. Bit 31 is the combine (1 modulate, 0 add) and bits 29:25 the +/// detail level as a positive count, at most 15 (ESTABLISHED as the library's +/// setter); bits 14:0 are the base level's sizes. Bits 24:20 and 19:15 are +/// the detail image's log2 width and height, and the image is level-table +/// entry 64 minus the level (ESTABLISHED from the library's detail branch, +/// and consistent with the captured 2x2 and 4x2 controls at levels -1 to -3, +/// entries 63 to 61, and SGI Quake's 128x128 detail image at the default +/// level -4, whose entry 60 holds a 128-texel row). The library writes bits +/// 24:15 only for a detail magnification filter and leaves them stale after, +/// so the caller reads them only while word 4 carries magnification code 2, 3 +/// or 4. Bit 30 has no known meaning and refuses the word. +pub(super) fn texture_detail_sampler(sampler: u32) -> Option { + let level = (sampler >> 25) & 0x1f; + + if (sampler & 0x40000000) != 0 || level == 0 || level > 15 { + return None; + } + Some(TextureDetailSampler { + descriptor: ODSY_TEX_LEVELS - level, + width: 1 << ((sampler >> 20) & 0x1f), + height: 1 << ((sampler >> 15) & 0x1f), + level: -(level as i32), + mode: if (sampler & 0x80000000) != 0 { GL_MODULATE } else { GL_ADD }, + }) +} + +/// A detail function entry's weight, bits 23:12 as an unsigned 12-bit +/// fraction. The library never sets the other bits, so an entry that does +/// is unknown state and drops the draw. +pub(super) fn texture_detail_entry_weight(value: u32) -> Option { + if (value & !0x00fff000) != 0 { + return None; + } + Some((value >> 12) as u16) +} + +impl Board { + pub fn texture_detail_function_weight(&self) -> Option { + let table = &self.tex.detail_function; + + if self.tex.detail_function_valid != 0xffff { + return None; + } + let mut weight = TextureDetailWeight::default(); + for (w, &e) in weight.table.iter_mut().zip(table.iter()) { + *w = texture_detail_entry_weight(e)?; + } + Some(weight) + } +} + +impl Board { + /// The detail image at level-table entry `descriptor`, held as a plain 2D + /// level is: its row is its width in 16-texel units rounded up to even, + /// and its entry's word 1 carries no bits outside the format and that + /// stride (Board::texture_setup()). + /// + /// The entry's word 1 bits 31:30 and 25:20 give the image's own index and + /// size code, which must be a table row; its components are the + /// primary's, `primary`, since the board is sent only the primary's class + /// (texture_level_format()). An index the table does not list refuses + /// the draw. + pub(super) fn texture_detail_backing_setup( + &mut self, + chain: &mut TextureChain, + primary: u32, + descriptor: u32, + width: u32, + height: u32, + ) -> Result<(), Refusal> { + let e = (descriptor * 3) as usize; + let entry = [self.tex.levels[e], self.tex.levels[e + 1], self.tex.levels[e + 2]]; + let Some(base_format) = texture_level_format(entry[1], primary) else { + return Err(Refusal("a detail level's format index or size code is not one the library emits")); + }; + let base = entry[0] & 0x1fffffff; + let stride_units = (width.div_ceil(16) + 1) & !1u32; + + if base == 0 + || (entry[1] & 0xfff) != stride_units + || (entry[1] & 0x3c0ff000) != 0 + || entry[2] != 0 + || self.objects.count > ODSY_OBJECTS + { + return Err(Refusal("a detail level that is not a plain level")); + } + let Some(o) = self.object_find(base) else { + return Err(Refusal("a detail level with no object")); + }; + let end = (height.wrapping_sub(1) as u64) + .wrapping_mul(self.objects.width[o] as u64) + .wrapping_add(self.objects.offset[o] as u64) + .wrapping_add(width as u64); + let region_written = self.object_region_written(o, 0, 0, width, height); + if self.objects.width[o] != stride_units * 16 + || self.objects.rows[o] < height + || end > self.objects.used as u64 + || end > self.objects.mem.size / 4 + || !region_written + { + return Err(Refusal("a detail level whose object does not hold it written")); + } + chain.detail_storage = Texture { + offset: self.objects.offset[o], + stride: self.objects.width[o], + width, + height, + base_format, + words: self.objects.used, + border_rows: false, + }; + chain.detail_image = GlTexLevel { + level: 0, + width, + height, + depth: 1, + base_format, + internal_format: base_format, + detail: true, + read_texel: true, + ..Default::default() + }; + Ok(()) + } +} + +/// The four 0x1013 words are 14-bit two's complement with 8 fraction bits: +/// the GL library writes trunc(x * 256) & 0x3fff (ESTABLISHED as the +/// library's conversion). It never sets bits 31:14, so a word that does is +/// unknown state and drops the draw. +pub(super) fn texture_lod_word(word: u32) -> i32 { + sextract32(word, 0, tex::LOD_BITS) +} + +/// Minimum and maximum LOD (words 2 and 3). The library clamps both to +/// +/-15 before writing them (ESTABLISHED); the field holds +/-32. What the +/// board does with a limit past 15 is UNKNOWN, as no stock client sends +/// one: the model clamps it the way the library does, an approximation. +pub(super) fn texture_lod_limit(word: u32) -> i32 { + cmin(cmax(texture_lod_word(word), -(tex::LOD_LIMIT as i32)), tex::LOD_LIMIT as i32) +} + +impl Board { + /// LOD bias S and T (words 0 and 1). The library clamps each to -32 through + /// 31.99609375, the field's whole range, so every value is one it can send + /// (ESTABLISHED). The R bias never reaches a register. The model applies a + /// bias as GL_SGIX_texture_lod_bias defines it, scaling that axis's + /// derivatives by 2^bias before the LOD is computed: INFERRED, since the + /// board's LOD arithmetic is not documented. A zero bias leaves the scale + /// unset, so an unbiased draw keeps the exact unscaled path. + pub fn texture_lod_bias(&self, image: &mut GlTexImage) { + let bias_s = texture_lod_word(self.tex.lod[0]); + let bias_t = texture_lod_word(self.tex.lod[1]); + + image.lod_scale_s = if bias_s != 0 { (bias_s as f64 / tex::LOD_ONE as f64).exp2() } else { 0.0 }; + image.lod_scale_t = if bias_t != 0 { (bias_t as f64 / tex::LOD_ONE as f64).exp2() } else { 0.0 }; + } +} + +impl GlTexImage<'_> { + /// Sampler words 1 and 2, bits 16:0, hold each axis's maximum clamp in + /// whole texels of the base level: the library writes trunc(2^log2size * + /// GL_TEXTURE_MAX_CLAMP_*_SGIX), so the default 1.0 gives the size itself + /// (ESTABLISHED as the library's setter). A smaller value under GL_CLAMP + /// (wrap 2) clamps that axis's coordinate to value / size, as + /// GL_SGIX_texture_coordinate_clamp defines it, and the size comes from + /// word 0's log2 field. That the board applies it only under GL_CLAMP, and + /// at every level as the same fraction of the image, is INFERRED from the + /// extension. Under other wraps a smaller value is ignored. A value above + /// the size cannot come from the library and refuses the draw + /// (Board::texture_sampler()). Limitation: the R axis's clamp (word 3) is + /// not applied; a volume whose word 3 is below its depth stays + /// unsupported (Board::texture_support()). + pub(super) fn texture_max_clamp(&mut self, size: [u32; 3], max_clamp: [u32; 3], wrap: [Wrap; 3]) { + self.max_clamp_s = 1.0; + self.max_clamp_t = 1.0; + self.max_clamp_r = 1.0; + if max_clamp[0] < size[0] && wrap[0] == Wrap::Clamp { + self.max_clamp = true; + self.max_clamp_s = max_clamp[0] as f64 / size[0] as f64; + } + if max_clamp[1] < size[1] && wrap[1] == Wrap::Clamp { + self.max_clamp = true; + self.max_clamp_t = max_clamp[1] as f64 / size[1] as f64; + } + } +} + +impl Board { + /// Rebuild shared state from board words; no borrowed image survives a draw. + pub fn texture_vertex(&mut self) { + const ENVIRONMENTS: [u32; 5] = [GL_REPLACE, GL_REPLACE, GL_MODULATE, GL_DECAL, GL_BLEND]; + let mut strq: [u32; 4] = [self.tex.coord[0], self.tex.coord[1], self.tex.coord_r, self.tex.coord_q]; + let one_d = (self.tex.mode & (tex::MODE_1D | tex::MODE_2D)) == tex::MODE_1D; + self.gl().cur.texture_2d = (self.tex.mode & (tex::MODE_1D | tex::MODE_2D)) != 0; + self.gl().cur.texture_3d = (self.tex.mode & tex::MODE_3D) != 0; + // Excluded environments are rejected by the board before rasterization. + self.gl().cur.texture_env_mode = GL_REPLACE; + if (self.tex.env[0] as usize) < ENVIRONMENTS.len() { + self.gl().cur.texture_env_mode = ENVIRONMENTS[self.tex.env[0] as usize]; + } + // Captured linear planes already include the eye-space transform. + if (self.cmd.tag == gl_cmd::VERTEX_2I + || self.cmd.tag == gl_cmd::VERTEX_2F + || self.cmd.tag == gl_cmd::VERTEX_3F + || self.cmd.tag == gl_cmd::VERTEX_3I + || self.cmd.tag == gl_cmd::VERTEX_4F) + && self.geom.linear_texgen != 0 + { + let mut vertex = [ + f64::from(odyssey_f32(self.cmd.operand[0])), + f64::from(odyssey_f32(self.cmd.operand[1])), + if self.cmd.tag != gl_cmd::VERTEX_2I && self.cmd.tag != gl_cmd::VERTEX_2F { f64::from(odyssey_f32(self.cmd.operand[2])) } else { 0.0 }, + if self.cmd.tag == gl_cmd::VERTEX_4F { f64::from(odyssey_f32(self.cmd.operand[3])) } else { 1.0 }, + ]; + + if self.cmd.tag == gl_cmd::VERTEX_2I || self.cmd.tag == gl_cmd::VERTEX_3I { + vertex[0] = (self.cmd.operand[0] as i32) as f64; + vertex[1] = (self.cmd.operand[1] as i32) as f64; + } + if self.cmd.tag == gl_cmd::VERTEX_3I { + vertex[2] = (self.cmd.operand[2] as i32) as f64; + } + for axis in 0..4usize { + let plane = if axis == 3 { + self.tex.linear_plane_q + } else if axis == 2 { + self.tex.linear_plane_r + } else { + self.tex.linear_plane[axis] + }; + let mut value = 0.0f32; + + if (self.geom.linear_texgen & self.tex.linear_valid & (1u32 << axis)) == 0 { + continue; + } + for k in 0..4usize { + value = (value as f64 + f64::from(odyssey_f32(plane[k])) * vertex[k]) as f32; + } + strq[axis] = value.to_bits(); + } + } + let rows = self.gl_texture_matrix; + let mut matrix = gl_transpose(&rows); + // Input T may transform S, but transformed T cannot affect 1D sampling. + if one_d { + for i in 0..4usize { + matrix[i * 4 + 1] = 0.0; + } + } + self.gl().set_matrix(GL_TEXTURE, &matrix); + self.gl_feed(op::TEXCOORD4F as u16, &strq); + } +} + +/// A draw's estimated raster work in pixels, from its vertices' window +/// positions (`primitive_cost`). +fn gl_draw_cost(d: &GlDraw) -> u32 { + let points = d.vertices.chunks_exact(vtx::FLOATS).take(d.count as usize).map(|v| { + let clip = [v[vtx::POSITION], v[vtx::POSITION + 1], v[vtx::POSITION + 2], v[vtx::POSITION + 3]]; + if !(clip[3] > 0.0) { + return None; + } + let win = d.state.clip_to_window(&clip, (1i32 << gl_rules().subpixel_bits) as f32)?; + Some((win[0] as f64, win[1] as f64)) + }); + primitive_cost(d.mode, points, d.point_size, d.line_width) +} + +impl Board { + /// The context emits one assembled primitive here; rasterize it into the + /// planes. The context requires the checked result once per-face polygon + /// modes are activated (Board::reset_registers): it relays + /// RasterRules::draw()'s out-of-memory signal from GL_POLYGON's dynamic + /// vertex assembly back to the context synchronously instead of dropping + /// it. The native renderer does not exist here, so this is always the + /// scalar draw. + pub fn gl_draw(&mut self, d: &mut GlDraw) -> GlDrawResult { + let mut t = self.gl_target(); + let rules = self.gl_operation_rules(); + let mut state: GlDrawState = *d.state; + let mut image = GlTexImage::default(); + // A job the raster unit has finished with, if one is back. + let mut spare = self.raster.take_draw(); + // Some 7 KB that only a textured draw uses, and Board::texture_setup() + // clears before it fills any of it, so it is not built for every + // primitive, and a reused job's is reused. + let mut texture: Option> = spare.as_mut().and_then(|j| j.chain.take()); + let textured = d.state.texture_2d || d.state.texture_3d; + + if textured { + let chain = texture.get_or_insert_with(Box::default); + + if !self.texture_setup(&mut image, chain) { + if let Some(mut j) = spare { + j.chain = texture; + self.raster.give_back(j); + } + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: GL draw path: backend scalar-skip result {} called=0", GL_DRAW_OK); + return GL_DRAW_OK; + } + } + if (state.texture_2d || state.texture_3d) && self.tex.env[0] == 4 { + // The board words pack two normalized 12-bit components each. + state.texture_env_color[0] = ((self.tex.env[1] >> 12) & 0xfff) as f32 / 4095.0f32; + state.texture_env_color[1] = (self.tex.env[1] & 0xfff) as f32 / 4095.0f32; + state.texture_env_color[2] = ((self.tex.env[2] >> 12) & 0xfff) as f32 / 4095.0f32; + state.texture_env_color[3] = (self.tex.env[2] & 0xfff) as f32 / 4095.0f32; + } + if !self.gl_index_mode() { + state.colour_mask = (self.gl_write_mask >> write::COLOUR_SHIFT) & write::COLOUR_MASK; + } + state.fog_color[0] = ((self.gl_fog[0] >> 12) & 0xfff) as f32 / 4096.0f32; + state.fog_color[1] = (self.gl_fog[0] & 0xfff) as f32 / 4096.0f32; + state.fog_color[2] = ((self.gl_fog[1] >> 12) & 0xfff) as f32 / 4096.0f32; + state.fog_color[3] = (self.gl_fog[1] & 0xfff) as f32 / 4096.0f32; + self.gl_report_refusals(true); + t.texture = None; + let tables = TexelTables::of(self, texture.as_deref().filter(|_| textured)); + let pixels = self.gl_pixel_job(); + let job = match spare { + Some(mut j) => { + j.rules = rules; + j.target = t; + j.image = image; + j.chain = texture; + j.textured = textured; + j.tables = tables; + j.state = state; + j.vertices.clear(); + j.vertices.extend_from_slice(d.vertices); + j.polygon_stipple = *d.polygon_stipple; + j.fields = GlDrawFields::of(d); + j.pixels = pixels; + j + } + None => Box::new(GlDrawJob { + rules, + target: t, + image, + chain: texture, + textured, + tables, + state, + vertices: d.vertices.to_vec(), + polygon_stipple: *d.polygon_stipple, + fields: GlDrawFields::of(d), + pixels, + }), + }; + let cost = gl_draw_cost(d); + if !self.raster.threaded() { + let (result, phase) = self.raster.run_now(|ctx| job.run(ctx)); + d.line_phase = phase; + self.raster.give_back(job); + return result; + } + // The raster unit draws it later, so the context hears no result: + // every result but out-of-memory is only reported, and that one + // needs a polygon of more vertices than the host can address. The + // raster unit keeps the line-stipple phase the draw advances. + self.raster.submit_draw(job, cost); + if d.line_phase.is_some() { + d.line_phase = Some(LINE_PHASE_ON_RASTER); + } + GL_DRAW_OK + } + + /// The board's planes and pixel state as a raster job takes them. + pub fn gl_pixel_job(&mut self) -> GlPixelJob { + GlPixelJob { state: self.gl_pixel_state(), spans: self.plane_spans() } + } + + /// glClear's raster op, and the pattern rectangle's runs: the raster + /// unit runs ops in order with the draw state of now. + pub fn gl_raster_ops(&mut self, ops: Vec) { + let t = self.gl_target(); + let rules = self.gl_operation_rules(); + let pixels = self.gl_pixel_job(); + + if let Some(op) = ops.first() { + self.gl_report_refusals(op.kind != RASTER_CLEAR || (op.clear_buffers & GL_COLOR_BUFFER_BIT) != 0); + } + let cost = ops.iter().fold(0u32, |c, op| c.saturating_add(rect_cost(op.x0, op.y0, op.x1, op.y1, op.kind == RASTER_CLEAR))); + self.raster.submit_work(cost, move |ctx| { + // SAFETY: the job runs before the front end next touches the + // planes (PlaneSpans). + let mut planes = unsafe { pixels.pixels() }; + planes.row_targets = ops.iter().fold(0, |bits, op| bits | GlPixels::target_bits(&op.targets[..op.ntargets.min(GL_MAX_TARGETS) as usize])); + for op in &ops { + rules.raster(&t, &mut planes, op); + } + planes.record(ctx); + }); + } + + /// glClear's raster op. + pub fn gl_raster(&mut self, op: &RasterOp) { + self.gl_raster_ops(vec![*op]); + } + + /// Run one command in the GL context, with the board as the sink for the + /// draws it emits. The context runs outside the board for the call, so + /// that a draw can borrow the rest of the board; a stand-in fills its + /// field meanwhile. The draw path does not touch the context. + pub fn gl_command(&mut self, cmd: &GlCmd) { + let stand_in = self.gl_spare.take().expect("GL commands do not nest"); + let mut ctx = std::mem::replace(&mut self.gl_ctx, stand_in); + + ctx.command(self, cmd); + self.gl_spare = Some(std::mem::replace(&mut self.gl_ctx, ctx)); + } + + /// The board's GL context. Not for the draw path: while a command runs + /// the field holds a stand-in. + pub fn gl(&mut self) -> &mut GlContext { + assert!(self.gl_spare.is_some(), "the GL context is running a command"); + &mut self.gl_ctx + } + + pub fn gl_ref(&self) -> &GlContext { + assert!(self.gl_spare.is_some(), "the GL context is running a command"); + &self.gl_ctx + } +} + +impl GlSink for Board { + fn draw(&mut self, d: &mut GlDraw) -> GlDrawResult { + self.gl_draw(d) + } +} + +impl ClipRect { + fn gl_clip_rect_eq(&self, b: &ClipRect) -> bool { + self.x0 == b.x0 && self.y0 == b.y0 && self.x1 == b.x1 && self.y1 == b.y1 + } +} + +impl GlDrawable { + // Compares the two drawables field by field. + pub fn drawable_eq(&self, b: &GlDrawable) -> bool { + self.bound == b.bound + && self.origin == b.origin + && self.width == b.width + && self.height == b.height + && self.clip.n == b.clip.n + && self.clip.empty == b.clip.empty + // Only the rectangles the list counts: this runs before every + // command, and a window rarely owns more than one or two of the + // sixteen. Those past the count are not drawn against. + && self.clip.r.iter().zip(b.clip.r.iter()).take(self.clip.n as usize).all(|(x, y)| x.gl_clip_rect_eq(y)) + && self.front == b.front + && self.back == b.back + && self.extra == b.extra + && self.overlay == b.overlay + } +} + +impl Board { + /// Point the shared context at the current window: its origin from 0x841 + /// and the kept 0x204b rectangles as the pixels it owns, in plane + /// coordinates. Called before every command, so a change to either + /// register takes effect at the next one. With no rectangle kept, one + /// inverted rectangle keeps the shared rasterizer drawing nothing rather + /// than logging an unbound window. The shared context's front and back + /// targets carry buffers 0 and 1, the raster target callbacks' buffer + /// numbers, and its draw buffers follow the 0x0424 drawing mask. The kernel + /// has already turned the client's front and back into buffer numbers + /// (reg::DRAW_MASK), so the shared names are slots here, not the + /// client's roles: a client drawing its front after one swap draws the + /// context's "back", buffer 1. + /// + /// The shared rasterizer adds window rows to the origin as they come; the + /// client's combined matrix already counts them down from the window's top + /// row, the one 0x841 names (Board::gl_load_combined()), so the origin, + /// rectangles and id plane all stay in the registers' top-down plane + /// coordinates. + pub fn gl_bind(&mut self) { + let buffers = self.draw_buffers(); + let draw: u32 = (if buffers & 1 != 0 { GL_DRAW_FRONT } else { 0 }) + | (if buffers & 2 != 0 { GL_DRAW_BACK } else { 0 }); + let n = cmax(self.win.clip_kept, 1); + let origin = self.win.origin; + let clip_kept = self.win.clip_kept; + let clip_rect = self.win.clip_rect; + + // This runs before every command, but the drawable changes only with + // the window registers, so it is built in place and handed over only + // when it differs from the shared context's. gl_bind_drawable's other + // fields keep their initial values, and its rectangles past the + // count stay zero, so it is the drawable a fresh build would make. + { + let d = &mut self.gl_bind_drawable; + + d.origin[0] = origin[0]; + d.origin[1] = origin[1]; + if clip_kept != 0 { + d.clip.r[..clip_kept as usize].copy_from_slice(&clip_rect[..clip_kept as usize]); + } else { + // An all-empty clip; ClipList.empty replaces this at rebase. + d.clip.r[0] = ClipRect { x0: 1, y0: 1, x1: 0, y1: 0 }; + } + if d.clip.n > n { + for i in n as usize..d.clip.n as usize { + d.clip.r[i] = ClipRect::default(); + } + } + d.clip.n = n; + } + if !self.gl_bind_drawable.drawable_eq(&self.gl_ref().cur.drawable) { + let d = self.gl_bind_drawable; + self.gl().set_drawable(&d); + } + if self.gl_ref().cur.draw_buffers != draw { + self.gl_command(&GlCmd::new(op::DRAW_BUFFERS as u16, std::slice::from_ref(&draw))); + } + } + + /// Decode one command into the shared context. + pub fn gl_feed(&mut self, op: u16, args: &[u32]) { + self.gl_bind(); + self.gl_command(&GlCmd::new(op, args)); + } + + // Board::gl_feed with the op as the shared layer's u32 enum. + fn gl_feed32(&mut self, op: GlOp, args: &[u32]) { + self.gl_feed(op as u16, args); + } +} + +/// 0x404 bits 10:9 to the shared layer's cull-face enum: GL_BACK 01, +/// GL_FRONT 10, GL_FRONT_AND_BACK 11. Returns UINT32_MAX for 00, which no +/// probe word has sent; the shared layer's own glCullFace validation then +/// rejects it, leaving the previous mode in place. +pub(super) fn cull_face_mode(code: u32) -> u32 { + match code { + 1 => GL_BACK, + 2 => GL_FRONT, + 3 => GL_FRONT_AND_BACK, + _ => u32::MAX, + } +} + +/// 0x404 bit 23 to the shared layer's glFrontFace sense, inverted -- see +/// the mode::FRONT_FACE_CW comment for why. +pub(super) fn front_face(pattern_mode: u32) -> u32 { + if (pattern_mode & mode::FRONT_FACE_CW) != 0 { GL_CCW } else { GL_CW } +} + +impl Board { + /// Feed one raster-state register (polygon mode, line width and point + /// size, line stipple and its enable). A context swap drops the outgoing + /// primitive and only then re-pushes the owner's polygon mode, sizes and + /// stipple; on the board no primitive is open by then. The model is + /// different: it keeps a GL_POLYGON open across the switch, because it + /// assembles a polygon whole and the board's way of continuing one after a + /// switch is unknown (Board::cmd_dispatch()'s 0x0444 and 0x0850 cases). + /// The shared layer would refuse the restored values as GL calls inside + /// glBegin and keep what the other context left, so while a GL_POLYGON is + /// held open its begin flag is set aside for the one call. APPROXIMATION: + /// this follows the model's hold, not any board behaviour; for every other + /// primitive the shared layer's rule stands. The hold is right only because + /// the model saves no GL_POLYGON vertices to the shadow, so the kernel's + /// restart replays nothing: if the model ever saves them, it must stop + /// holding at the same time, or the polygon draws twice. + pub fn gl_feed_raster_state(&mut self, op: u16, args: &[u32]) { + let in_begin = self.gl_ref().in_begin; + let held_polygon = self.prim.begin_open && self.stream.primitive == 9; + + if held_polygon { + self.gl().in_begin = false; + } + self.gl_feed(op, args); + self.gl().in_begin = in_begin; + } + + /// Hand the GL state register 0x0404 carries to the shared context: bit 12, + /// the shade model, bit 16, the depth test, bit 14, the alpha test, bit 15, + /// the stencil test, bit 17, blending, bit 19, index logic op, bit 20, + /// colour logic op, bit 8, GL_CULL_FACE, bits 10:9, the face it culls, and + /// bit 23, front face (fed inverted; see the mode::FRONT_FACE_CW + /// comment), and bit 3, float GL line stipple. The shared rasterizer fills + /// a flat triangle from its provoking vertex, the last one for triangles + /// and strips as GL specifies. Nothing in the stream says which vertex the + /// board takes; the library never reorders or repeats colours to steer it, + /// so GL's own rule is INFERRED. Called on every 0x0404, including the X + /// server's pattern-mode writes, which also update the separate integer 2D + /// interpreter. + /// + /// Bit 15, the stencil test, gates fragments through the model's stencil + /// plane (mode::STENCIL_TEST). + pub fn gl_apply_polygon_stipple(&mut self) { + let cap = GL_POLYGON_STIPPLE; + let enable = (self.stream.pattern_mode & gl_cmd::RECT_PATTERN_ENABLE) != 0 && (self.stream.pattern_shape & !pattern::SHAPE_OPAQUE) == 0; + + if self.prim.in_prim { + return; + } + if enable { + let mut rows = [0u32; 32]; + + for i in 0..rows.len() { + rows[i] = self.stream.pattern_buffer[31 - i]; + } + if !self.gl().polygon_stipple(&rows) { + return; + } + } + self.gl_feed32(if enable { op::ENABLE } else { op::DISABLE }, &[cap]); + } + + /// 0x0404 bit 17 (mode::BLEND) blends only into an RGBA drawable: + /// OpenGL blends RGBA fragments alone (OpenGL 1.2.1, 4.1.6), and the X + /// server sets the bit with factors ONE, ZERO for its opaque stipples, + /// whose background is the 0x0c21 constant colour and needs no destination + /// (pattern::SHAPE_OPAQUE). Fed again when the colour mode changes. + pub fn gl_apply_blend(&mut self) { + let blend = GL_BLEND; + let on = (self.stream.pattern_mode & mode::BLEND) != 0 && !self.gl_index_mode(); + + self.gl_feed32(if on { op::ENABLE } else { op::DISABLE }, &[blend]); + } + + pub fn gl_apply_mode(&mut self) { + let shade = if (self.stream.pattern_mode & mode::FLAT_SHADE) != 0 { GL_FLAT } else { GL_SMOOTH }; + let cull_mode = cull_face_mode((self.stream.pattern_mode >> mode::CULL_MODE_SHIFT) & mode::CULL_MODE_MASK); + let front_face = front_face(self.stream.pattern_mode); + let pm = self.stream.pattern_mode; + let en = |bit: u32| if (pm & bit) != 0 { op::ENABLE } else { op::DISABLE }; + + self.gl_feed32(op::SHADE_MODEL, &[shade]); + // Bit assignments follow the library's fragment-state encoding. + self.gl_feed32(en(mode::POINT_SMOOTH), &[GL_POINT_SMOOTH]); + self.gl_feed32(en(mode::LINE_SMOOTH), &[GL_LINE_SMOOTH]); + // The shared layer has no dither rule yet, so the first mode word that + // enables it is reported and the board draws undithered. Once the + // shared rule lands this feeds op::ENABLE/DISABLE of GL_DITHER. + if (pm & mode::DITHER) != 0 && !self.diag.gl_dither_reported { + self.diag.gl_dither_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: dither not modelled; drawing undithered"); + } + self.gl_feed32(en(mode::DEPTH_TEST), &[GL_DEPTH_TEST]); + self.gl_feed32(en(mode::ALPHA_TEST), &[GL_ALPHA_TEST]); + self.gl_feed32(en(mode::STENCIL_TEST), &[GL_STENCIL_TEST]); + self.gl_apply_blend(); + self.gl_feed32(en(mode::FOG), &[GL_FOG]); + self.gl_feed32(en(mode::INDEX_LOGIC_OP), &[GL_INDEX_LOGIC_OP]); + self.gl_feed32(en(mode::COLOR_LOGIC_OP), &[GL_COLOR_LOGIC_OP]); + self.gl_feed32(en(mode::OFFSET_POINT), &[GL_POLYGON_OFFSET_POINT]); + self.gl_feed32(en(mode::OFFSET_LINE), &[GL_POLYGON_OFFSET_LINE]); + self.gl_feed32(en(mode::OFFSET_FILL), &[GL_POLYGON_OFFSET_FILL]); + self.gl_feed32(en(mode::CULL_FACE), &[GL_CULL_FACE]); + self.gl_feed_raster_state(en(mode::GL_LINE_STIPPLE) as u16, &[GL_LINE_STIPPLE]); + self.gl_feed32(op::CULL_FACE, &[cull_mode]); + self.gl_feed32(op::FRONT_FACE, &[front_face]); + self.gl_apply_polygon_stipple(); + } + + pub fn gl_apply_line_stipple(&mut self) { + // Word 1 remains the integer 2D path's 32-bit rotated pattern. + let args = [(self.stream.line_stipple[2] & 0xff) + 1, self.stream.line_stipple[0] & 0xffff]; + + self.gl_feed_raster_state(op::LINE_STIPPLE as u16, &args); + } + + /// Push the board-shadowed clip box (reg::CLIP_BOX) to the shared + /// context's scissor rectangle (see the macro's own comment for the + /// encoding and the always-on rationale). min > max -- including the + /// library's own 0x1000, 0x1000 empty encoding -- clips every fragment, + /// matched here with a zero-area box rather than a negative width or + /// height, which op::SCISSOR rejects. + pub fn gl_apply_clip_box(&mut self) { + let x_min = (self.gl_clip_box[0] >> clip::BOX_MIN_SHIFT) & clip::BOX_FIELD_MASK; + let x_max = self.gl_clip_box[0] & clip::BOX_FIELD_MASK; + let y_min = (self.gl_clip_box[1] >> clip::BOX_MIN_SHIFT) & clip::BOX_FIELD_MASK; + let y_max = self.gl_clip_box[1] & clip::BOX_FIELD_MASK; + let mut box_ = [x_min, y_min, 0, 0]; + + if x_min <= x_max && y_min <= y_max { + box_[2] = x_max - x_min + 1; + box_[3] = y_max - y_min + 1; + } + self.gl_feed32(op::SCISSOR, &box_); + } + + /// Feed eye-space clip plane i to the shared context as the object-space + /// plane the current model-view gives it: p_obj[k] = sum_r p_eye[r] + /// M[r][k], M row-major as gl_modelview holds it. + pub fn gl_apply_clip_plane(&mut self, i: u32) { + let mut args = [0u32; 5]; + let mut eye = [0.0f64; 4]; + let mut obj = [0.0f64; 4]; + + args[0] = i; + for r in 0..4usize { + eye[r] = f64::from(odyssey_f32(self.geom.clip_eye[i as usize * 4 + r])); + } + for r in 0..4usize { + for k in 0..4usize { + obj[k] += eye[r] * f64::from(odyssey_f32(self.geom.modelview[r * 4 + k])); + } + } + for k in 0..4usize { + args[1 + k] = f32_bits(obj[k] as f32); + } + self.gl_feed32(op::CLIP_PLANE, &args); + } + + /// Recompute which of the shared context's six clip-plane slots are + /// tested, from 0x0405 bits 2:0's thermometer count n (see + /// tex::MODE_CLIP_COUNT_MASK's comment): slots 0..n-1 enabled, the rest + /// disabled. Called on every 0x0405 write; does not + /// touch the loaded coefficients themselves, which a separate load + /// (gl_cmd::CLIP_PLANE_LOAD) feeds. + pub fn gl_apply_clip_planes(&mut self) { + let bits = self.tex.mode & tex::MODE_CLIP_COUNT_MASK; + let n = bits.count_ones(); + + for i in 0..GL_CLIP_PLANES { + let plane = GL_CLIP_PLANE0 + i; + + self.gl_feed32(if i < n { op::ENABLE } else { op::DISABLE }, &[plane]); + } + } +} + +/// 0x0c21's 6-bit blend factor code to the shared layer's enum +/// (established): 0 GL_ZERO, 0x08 GL_ONE, 0x10 "the other colour" +/// (GL_DST_COLOR as a source factor, GL_SRC_COLOR as a destination factor +/// -- the board never needs both names at the same position, so one code +/// covers both), 0x18 GL_SRC_ALPHA, 0x20 GL_DST_ALPHA, 0x28 +/// GL_CONSTANT_COLOR, 0x30 GL_CONSTANT_ALPHA, 0x38 the "one minus" of the +/// 0x10 pairing, 0x39 through 0x3c the "one minus" of 0x18/0x20/0x28/0x30 +/// in turn, and 0x40, a source-only code, GL_SRC_ALPHA_SATURATE. Returns +/// UINT32_MAX for a code with no factor at the requested position. +pub(super) fn blend_factor(code: u32, dst: bool) -> u32 { + match code { + 0x00 => 0, // GL_ZERO + 0x08 => 1, // GL_ONE + 0x10 => { + if dst { GL_SRC_COLOR } else { GL_DST_COLOR } + } + 0x18 => GL_SRC_ALPHA, + 0x20 => GL_DST_ALPHA, + 0x28 => GL_CONSTANT_COLOR, + 0x30 => GL_CONSTANT_ALPHA, + 0x38 => { + if dst { GL_ONE_MINUS_SRC_COLOR } else { GL_ONE_MINUS_DST_COLOR } + } + 0x39 => GL_ONE_MINUS_SRC_ALPHA, + 0x3a => GL_ONE_MINUS_DST_ALPHA, + 0x3b => GL_ONE_MINUS_CONSTANT_COLOR, + 0x3c => GL_ONE_MINUS_CONSTANT_ALPHA, + 0x40 => { + if dst { u32::MAX } else { GL_SRC_ALPHA_SATURATE } + } + _ => u32::MAX, + } +} + +/// 0x041f's three 3-bit stencil-op fields (fail, depth-fail, depth-pass) +/// to the shared layer's enum: KEEP 0, ZERO 1, REPLACE 2, INCR 3, DECR 4, +/// INVERT 5. Returns UINT32_MAX for the two codes the board leaves unused. +pub(super) fn stencil_op(code: u32) -> u32 { + match code { + 0 => GL_KEEP, + 1 => 0, // GL_ZERO + 2 => GL_REPLACE, + 3 => GL_INCR, + 4 => GL_DECR, + 5 => GL_INVERT, + _ => u32::MAX, + } +} + +/// 0x0415's 2-bit face code to the shared layer's polygon-mode enum, +/// GL_POINT 0 to GL_FILL 2. Returns UINT32_MAX for the one code the board +/// leaves undefined, which the shared layer's own glPolygonMode validation +/// then rejects, leaving the previous mode in place. +pub(super) fn polygon_mode(code: u32) -> u32 { + match code { + 0 => GL_POINT, + 1 => GL_LINE, + 2 => GL_FILL, + _ => u32::MAX, + } +} + +/// 0x0c21 word 0 bits 15:13 to the shared layer's blend equation. Codes 3 +/// and 4, the SGIX alpha min/max extension, have no shared equivalent +/// (gl_blend_equation_valid() accepts only the other six), so the +/// dispatch case reports and skips them rather than feeding a code the +/// shared layer would reject. +pub(super) static BLEND_EQUATION: [u32; 8] = [ + GL_FUNC_ADD, + GL_MIN, + GL_MAX, + 0, + 0, + GL_LOGIC_OP, + GL_FUNC_SUBTRACT, + GL_FUNC_REVERSE_SUBTRACT, +]; + +impl Board { + /// 0x0420 and 0x0452 to the shared context's comparison and depth mask. + pub fn gl_apply_depth(&mut self) { + let func = GL_NEVER_ENUM + (self.gl_depth_func & ODSY_DEPTH_FUNC_MASK); + let mask = ((self.gl_write_mask & write::DEPTH) != 0) as u32; + + self.gl_feed32(op::DEPTH_FUNC, &[func]); + self.gl_feed32(op::DEPTH_MASK, &[mask]); + } +} + +/// The lighting words matched by value (gl_cmd::NORMAL_3F and the rest). +pub(super) static GL_LIGHT_WORDS: [u32; 16] = [ + gl_cmd::NORMAL_3F, + gl_cmd::MATERIAL_AMBIENT, + gl_cmd::MATERIAL_DIFFUSE, + gl_cmd::MATERIAL_SPECULAR, + gl_cmd::MATERIAL_EMISSION, + gl_cmd::BACK_AMBIENT, + gl_cmd::BACK_DIFFUSE, + gl_cmd::BACK_SPECULAR, + gl_cmd::BACK_EMISSION, + gl_cmd::SHININESS_SQRT, + gl_cmd::BACK_SHININESS_SQRT, + gl_cmd::SHININESS_DERIVED, + gl_cmd::LIGHT_COLOUR_LOAD, + gl_cmd::LIGHT_VEC_LOAD, + gl_cmd::CLIP_PLANE_LOAD, + prog::RUN_ONCE, +]; + +/// The texture-coordinate and generation fragments. +pub(super) static UCODE_TEXGEN: [u32; 14] = [ + 0x033, 0x04d, 0x052, 0x057, 0x05c, 0x061, 0x066, 0x06b, 0x06c, 0x077, 0x07b, 0x07f, 0x091, 0x09f, +]; + +pub(super) fn prog_value(word: u32) -> u32 { + (word & prog::LOW_MASK) | (((word >> prog::HIGH_SHIFT) & 1) << 10) +} + +pub(super) fn is_program_word(word: u32) -> bool { + let cls = word >> prog::CLASS_SHIFT; + + if (word & prog::SHAPE) != prog::TAG { + return false; + } + if (prog::PROGRAM_HALFWORD..=prog::PROGRAM_HALFWORD_LAST).contains(&cls) { + return true; + } + if (prog::REG_FIRST..=prog::REG_LAST).contains(&cls) + && (word & gl_cmd::MATRIX_SELECT_SHAPE) != gl_cmd::MATRIX_SELECT_TAG + { + return true; + } + matches!( + cls, + prog::COLOUR_MODE + | prog::COLOUR_MODE_B + | prog::SET_UCODE + | prog::PROGRAM_ADDR + | prog::CONST_SELECT + | prog::ENTRY_29 + | prog::ENTRY_2A + | prog::ENTRY_2C + | prog::LIGHT_VEC_SELECT + | prog::LIGHT_COLOUR_SELECT + | prog::LIST_INDEX + | prog::COLOUR_MATERIAL + | prog::CONST_STORE + ) +} + +pub(super) fn ucode_is_texgen(a: u32) -> bool { + UCODE_TEXGEN.contains(&a) +} diff --git a/src/dev/odyssey/gl_context.rs b/src/dev/odyssey/gl_context.rs new file mode 100644 index 000000000..fcfa1d756 --- /dev/null +++ b/src/dev/odyssey/gl_context.rs @@ -0,0 +1,1915 @@ +//! One OpenGL context's state. The board decodes its own command words +//! (geometry.rs, gl.rs) and feeds them to a context as GL operations. A +//! context holds the GL state a draw is made under (GlDrawState), the +//! matrices, the current vertex attributes, the clear values and the +//! primitive being assembled. Colours, matrices and vertex components are IEEE single +//! floats holding exactly the bits the client sent; arithmetic is double. + +//! +//! Ported from include/hw/display/sgi/gl-context.h in jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use super::bits::deposit32; +use super::memory::ClipList; +use crate::dev::gl::math::{invert, xform, IDENT}; +use crate::dev::gl::vertex::{Geometry, NearW, Viewport, Wv}; + +/// A raster operation: pixel work on a head's planes that is not a GL +/// primitive, queued beside draws in the order the head received it. +pub(super) type RasterKind = u32; + +/// Where colour is stored: a value the board's drawable gives and only +/// the board's store hook reads. Shared code copies it, never interprets it. +pub(super) type ColourTarget = u32; + +pub(super) type GlDrawResult = u32; +pub(super) type GlOp = u32; + +// Constants +pub(super) const GL_MAX_TARGETS: u32 = 4; + +// The channels glColorMask lets a draw or clear write (4.2.2). +pub(super) const GL_COLOUR_MASK_RED: u32 = 0x1; +pub(super) const GL_COLOUR_MASK_ALPHA: u32 = 0x8; +pub(super) const GL_COLOUR_MASK_ALL: u32 = 0xf; + +// The buffers a clear names, glClear's mask bits. +pub(super) const GL_DEPTH_BUFFER_BIT: u32 = 0x0100; +pub(super) const GL_STENCIL_BUFFER_BIT: u32 = 0x0400; +pub(super) const GL_COLOR_BUFFER_BIT: u32 = 0x4000; + +/// Float slots of a captured vertex, `vtx::FLOATS` of them. +pub(super) mod vtx { + pub const FLOATS: usize = 22; + pub const POSITION: usize = 0; + pub const COLOR: usize = 4; + pub const NORMAL: usize = 8; + pub const MATERIAL: usize = 11; + pub const TEXCOORD: usize = 12; + pub const EDGE: usize = 17; + pub const BACK_COLOR: usize = 18; +} + +// OpenGL 1.2 enumerants the state holds. +pub(super) const GL_POINTS: u32 = 0; +pub(super) const GL_LINES: u32 = 1; +pub(super) const GL_LINE_LOOP: u32 = 2; +pub(super) const GL_LINE_STRIP: u32 = 3; +pub(super) const GL_TRIANGLES: u32 = 4; +pub(super) const GL_TRIANGLE_STRIP: u32 = 5; +pub(super) const GL_TRIANGLE_FAN: u32 = 6; +pub(super) const GL_QUADS: u32 = 7; +pub(super) const GL_QUAD_STRIP: u32 = 8; +pub(super) const GL_POLYGON: u32 = 9; +pub(super) const GL_MODELVIEW: u32 = 0x1700; +pub(super) const GL_PROJECTION: u32 = 0x1701; +pub(super) const GL_TEXTURE: u32 = 0x1702; +pub(super) const GL_EYE_LINEAR: u32 = 0x2400; +pub(super) const GL_MODULATE: u32 = 0x2100; +pub(super) const GL_DECAL: u32 = 0x2101; +pub(super) const GL_NEAREST: u32 = 0x2600; +pub(super) const GL_NEAREST_MIPMAP_NEAREST: u32 = 0x2700; +pub(super) const GL_LINEAR_MIPMAP_NEAREST: u32 = 0x2701; +pub(super) const GL_NEAREST_MIPMAP_LINEAR: u32 = 0x2702; +pub(super) const GL_LINEAR_MIPMAP_LINEAR: u32 = 0x2703; +pub(super) const GL_LINEAR_SHARPEN_SGIS: u32 = 0x80ad; +pub(super) const GL_LINEAR_SHARPEN_ALPHA_SGIS: u32 = 0x80ae; +pub(super) const GL_LINEAR_SHARPEN_COLOR_SGIS: u32 = 0x80af; +pub(super) const GL_LINEAR_DETAIL_SGIS: u32 = 0x8097; +pub(super) const GL_LINEAR_DETAIL_ALPHA_SGIS: u32 = 0x8098; +pub(super) const GL_LINEAR_DETAIL_COLOR_SGIS: u32 = 0x8099; +pub(super) const GL_ADD: u32 = 0x0104; +pub(super) const GL_REPEAT: u32 = 0x2901; +pub(super) const GL_CLAMP: u32 = 0x2900; +pub(super) const GL_CLAMP_TO_EDGE: u32 = 0x812f; +pub(super) const GL_CLAMP_TO_BORDER: u32 = 0x812d; +pub(super) const GL_ALPHA4: u32 = 0x803b; +pub(super) const GL_ALPHA8: u32 = 0x803c; +pub(super) const GL_ALPHA12: u32 = 0x803d; +pub(super) const GL_ALPHA16: u32 = 0x803e; +pub(super) const GL_LUMINANCE4: u32 = 0x803f; +pub(super) const GL_LUMINANCE8: u32 = 0x8040; +pub(super) const GL_LUMINANCE12: u32 = 0x8041; +pub(super) const GL_LUMINANCE16: u32 = 0x8042; +pub(super) const GL_LUMINANCE4_ALPHA4: u32 = 0x8043; +pub(super) const GL_LUMINANCE6_ALPHA2: u32 = 0x8044; +pub(super) const GL_LUMINANCE8_ALPHA8: u32 = 0x8045; +pub(super) const GL_LUMINANCE12_ALPHA4: u32 = 0x8046; +pub(super) const GL_LUMINANCE12_ALPHA12: u32 = 0x8047; +pub(super) const GL_LUMINANCE16_ALPHA16: u32 = 0x8048; +pub(super) const GL_INTENSITY4: u32 = 0x804a; +pub(super) const GL_INTENSITY8: u32 = 0x804b; +pub(super) const GL_INTENSITY12: u32 = 0x804c; +pub(super) const GL_INTENSITY16: u32 = 0x804d; +pub(super) const GL_R3_G3_B2: u32 = 0x2a10; +pub(super) const GL_RGB4: u32 = 0x804f; +pub(super) const GL_RGB5: u32 = 0x8050; +pub(super) const GL_RGB8: u32 = 0x8051; +pub(super) const GL_RGB10: u32 = 0x8052; +pub(super) const GL_RGB12: u32 = 0x8053; +pub(super) const GL_RGB16: u32 = 0x8054; +pub(super) const GL_RGBA2: u32 = 0x8055; +pub(super) const GL_RGBA4: u32 = 0x8056; +pub(super) const GL_RGB5_A1: u32 = 0x8057; +pub(super) const GL_RGBA8: u32 = 0x8058; +pub(super) const GL_RGB10_A2: u32 = 0x8059; +pub(super) const GL_RGBA12: u32 = 0x805a; +pub(super) const GL_RGBA16: u32 = 0x805b; +pub(super) const GL_ALPHA: u32 = 0x1906; +pub(super) const GL_RGB: u32 = 0x1907; +pub(super) const GL_RGBA: u32 = 0x1908; +pub(super) const GL_DEPTH_COMPONENT: u32 = 0x1902; +pub(super) const GL_DEPTH_COMPONENT16_SGIX: u32 = 0x81a5; +pub(super) const GL_DEPTH_COMPONENT24_SGIX: u32 = 0x81a6; +pub(super) const GL_DEPTH_COMPONENT32_SGIX: u32 = 0x81a7; +pub(super) const GL_TEXTURE_LEQUAL_R_SGIX: u32 = 0x819c; +pub(super) const GL_TEXTURE_GEQUAL_R_SGIX: u32 = 0x819d; +pub(super) const GL_LUMINANCE: u32 = 0x1909; +pub(super) const GL_LUMINANCE_ALPHA: u32 = 0x190a; +pub(super) const GL_INTENSITY: u32 = 0x8049; +pub(super) const GL_FLAT: u32 = 0x1d00; +pub(super) const GL_SMOOTH: u32 = 0x1d01; +// The comparisons in GL_NEVER's order, as depth_func holds them. +pub(super) const GL_NEVER: u32 = 0; +pub(super) const GL_LESS: u32 = 1; +pub(super) const GL_EQUAL: u32 = 2; +pub(super) const GL_LEQUAL: u32 = 3; +pub(super) const GL_GREATER: u32 = 4; +pub(super) const GL_NOTEQUAL: u32 = 5; +pub(super) const GL_GEQUAL: u32 = 6; +pub(super) const GL_ALWAYS: u32 = 7; +pub(super) const GL_DRAW_FRONT: u32 = 0x1; +pub(super) const GL_DRAW_BACK: u32 = 0x2; +// +// The board's overlay planes, a layer of their own: set, the draw goes +// to the drawable's overlay target alone and the other bits name nothing. +// +pub(super) const GL_DRAW_OVERLAY: u32 = 0x40; +pub(super) const GL_NEVER_ENUM: u32 = 0x0200; +pub(super) const GL_CURRENT_COLOR: u32 = 0x0b00; +pub(super) const GL_POINT_SMOOTH: u32 = 0x0b10; +pub(super) const GL_LINE_SMOOTH: u32 = 0x0b20; +pub(super) const GL_LINE_STIPPLE: u32 = 0x0b24; +pub(super) const GL_LINE_STIPPLE_PATTERN: u32 = 0x0b25; +pub(super) const GL_LINE_STIPPLE_REPEAT: u32 = 0x0b26; +pub(super) const GL_POLYGON_SMOOTH: u32 = 0x0b41; +pub(super) const GL_POLYGON_STIPPLE: u32 = 0x0b42; +pub(super) const GL_CULL_FACE: u32 = 0x0b44; +pub(super) const GL_FOG: u32 = 0x0b60; +pub(super) const GL_DEPTH_TEST: u32 = 0x0b71; +pub(super) const GL_STENCIL_TEST: u32 = 0x0b90; +pub(super) const GL_DONT_CARE: u32 = 0x1100; +pub(super) const GL_HINTS: u32 = 5; +pub(super) const GL_SPRITE_AXIAL_SGIX: u32 = 0x814c; +pub(super) const GL_ALPHA_TEST: u32 = 0x0bc0; +pub(super) const GL_BLEND: u32 = 0x0be2; +pub(super) const GL_INDEX_LOGIC_OP: u32 = 0x0bf1; +pub(super) const GL_COLOR_LOGIC_OP: u32 = 0x0bf2; +pub(super) const GL_SCISSOR_TEST: u32 = 0x0c11; +pub(super) const GL_CLIP_PLANE0: u32 = 0x3000; +pub(super) const GL_POLYGON_OFFSET_POINT: u32 = 0x2a01; +pub(super) const GL_POLYGON_OFFSET_LINE: u32 = 0x2a02; +pub(super) const GL_POLYGON_OFFSET_FILL: u32 = 0x8037; +// The polygon_offset bits: the enable of each polygon mode's offset. +pub(super) const GL_OFFSET_FILL: u32 = 0x1; +pub(super) const GL_OFFSET_LINE: u32 = 0x2; +pub(super) const GL_OFFSET_POINT: u32 = 0x4; + +// +// glBlendFunc's factors (4.1.6, and the imaging subset's constant +// factors) and glBlendEquation's equations. GL_ZERO is 0 and GL_ONE 1; +// GL_LOGIC_OP is EXT_blend_logic_op's equation. +// +pub(super) const GL_SRC_COLOR: u32 = 0x0300; +pub(super) const GL_ONE_MINUS_SRC_COLOR: u32 = 0x0301; +pub(super) const GL_SRC_ALPHA: u32 = 0x0302; +pub(super) const GL_ONE_MINUS_SRC_ALPHA: u32 = 0x0303; +pub(super) const GL_DST_ALPHA: u32 = 0x0304; +pub(super) const GL_ONE_MINUS_DST_ALPHA: u32 = 0x0305; +pub(super) const GL_DST_COLOR: u32 = 0x0306; +pub(super) const GL_ONE_MINUS_DST_COLOR: u32 = 0x0307; +pub(super) const GL_SRC_ALPHA_SATURATE: u32 = 0x0308; +pub(super) const GL_CONSTANT_COLOR: u32 = 0x8001; +pub(super) const GL_ONE_MINUS_CONSTANT_COLOR: u32 = 0x8002; +pub(super) const GL_CONSTANT_ALPHA: u32 = 0x8003; +pub(super) const GL_ONE_MINUS_CONSTANT_ALPHA: u32 = 0x8004; +pub(super) const GL_FUNC_ADD: u32 = 0x8006; +pub(super) const GL_MIN: u32 = 0x8007; +pub(super) const GL_MAX: u32 = 0x8008; +pub(super) const GL_FUNC_SUBTRACT: u32 = 0x800a; +pub(super) const GL_FUNC_REVERSE_SUBTRACT: u32 = 0x800b; +pub(super) const GL_LOGIC_OP: u32 = 0x0bf1; +// glLogicOp's operations run from GL_CLEAR to GL_SET (4.1.8). +pub(super) const GL_CLEAR: u32 = 0x1500; +pub(super) const GL_COPY: u32 = 0x1503; +pub(super) const GL_SET: u32 = 0x150f; + +// Faces, and the light and material parameters (OpenGL 1.2.1, 2.13.2). +pub(super) const GL_FRONT: u32 = 0x0404; +pub(super) const GL_BACK: u32 = 0x0405; +pub(super) const GL_FRONT_AND_BACK: u32 = 0x0408; +// glPolygonMode's modes (3.5.4). +pub(super) const GL_POINT: u32 = 0x1b00; +pub(super) const GL_LINE: u32 = 0x1b01; +pub(super) const GL_FILL: u32 = 0x1b02; +// +// The rasterization enables a context keeps but does not draw with, as +// bits of raster_enables. +// +pub(super) const GL_RASTER_POINT_SMOOTH: u32 = 0x01; +pub(super) const GL_RASTER_LINE_SMOOTH: u32 = 0x02; +pub(super) const GL_RASTER_LINE_STIPPLE: u32 = 0x04; +pub(super) const GL_RASTER_POLYGON_SMOOTH: u32 = 0x08; +pub(super) const GL_RASTER_POLYGON_STIPPLE: u32 = 0x10; +// glFog's modes (3.10). +pub(super) const GL_EXP: u32 = 0x0800; +pub(super) const GL_EXP2: u32 = 0x0801; +pub(super) const GL_LINEAR: u32 = 0x2601; +// glStencilOp's operations (4.1.5); GL_ZERO is 0. +pub(super) const GL_INVERT: u32 = 0x150a; +pub(super) const GL_KEEP: u32 = 0x1e00; +pub(super) const GL_REPLACE: u32 = 0x1e01; +pub(super) const GL_INCR: u32 = 0x1e02; +pub(super) const GL_DECR: u32 = 0x1e03; +// glFrontFace's directions (2.13.1). +pub(super) const GL_CW: u32 = 0x0900; +pub(super) const GL_CCW: u32 = 0x0901; + +// The client clip planes a context keeps, GL_CLIP_PLANE0 to 5 (2.11). +pub(super) const GL_CLIP_PLANES: u32 = 6; + +// +// The most vertices a context holds between pieces of a primitive: a quad +// is drawn once its fourth arrives, so three wait. +// +pub(super) const GL_HELD_VERTICES: u32 = 3; + +/// Half the extent, in pixels, of the guard band the window-matrix path clips +/// x and y to, about the drawable's origin: h0 and h1 are divided by it so +/// that the shared clip planes (|x| <= w) fall on it. It is iris's choice, +/// a limit of the integer edge arithmetic and not of the board, and is +/// separate from `WINDOW_REACH` (8192, the combined-matrix path's band about +/// the window) because a window position here is measured from the +/// drawable's origin and a window can sit anywhere in it. Both are far +/// inside the hard limit, the +-65536 check in `Board::project()`. +pub(super) const GL_WINDOW_GUARD: i32 = 16384; + +/// Reports, once per process (a machine has one VPro board), that the w +/// plane clipped a primitive: preclip is off by default, the library stores +/// positions untested, and the board's behaviour at w <= 0 is unknown. iris +/// clips there so nothing reaches the integer edge functions. Call it from +/// a clip's slow path only. +pub(super) fn report_w_plane_clip() { + static REPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + + if !REPORTED.swap(true, std::sync::atomic::Ordering::Relaxed) { + crate::dlog!(LogModule::Odyssey, "sgi-gl: a primitive reached w <= 0 and was clipped; the board's behaviour there is unknown"); + } +} + +pub(super) const GL_OUT_OF_MEMORY: u32 = 0x0505; + +pub(super) const RASTER_CLEAR: RasterKind = 1; + +pub(super) const GL_DRAW_OK: GlDrawResult = 0; +pub(super) const GL_DRAW_OUT_OF_MEMORY: GlDrawResult = 1; +pub(super) const GL_DRAW_UNSUPPORTED: GlDrawResult = 2; +pub(super) const GL_DRAW_RESOURCE_ERROR: GlDrawResult = 3; +pub(super) const GL_DRAW_HOST_ERROR: GlDrawResult = 4; + +/// The operations the board feeds the shared GL context (`Board::gl_feed32()`). +pub(super) mod op { + use super::GlOp; + + pub const VIEWPORT: GlOp = 0; + pub const CLEAR_COLOR: GlOp = 1; + pub const CLEAR_DEPTH: GlOp = 2; + pub const SHADE_MODEL: GlOp = 3; + pub const MATRIX_MODE: GlOp = 4; + pub const LOAD_MATRIX: GlOp = 5; + pub const LOAD_IDENTITY: GlOp = 6; + pub const MULT_MATRIX: GlOp = 7; + pub const FRUSTUM: GlOp = 8; + pub const ORTHO: GlOp = 9; + pub const PUSH_MATRIX: GlOp = 10; + pub const POP_MATRIX: GlOp = 11; + pub const ROTATE: GlOp = 12; + pub const SCALE: GlOp = 13; + pub const TRANSLATE: GlOp = 14; + pub const ENABLE: GlOp = 15; + pub const DISABLE: GlOp = 16; + pub const DEPTH_FUNC: GlOp = 17; + pub const DEPTH_MASK: GlOp = 18; + pub const DRAW_BUFFERS: GlOp = 19; + pub const COLOR3F: GlOp = 20; + pub const COLOR4F: GlOp = 21; + pub const COLOR3UB: GlOp = 22; + pub const COLOR4UB: GlOp = 23; + pub const NORMAL3F: GlOp = 24; + pub const EDGE_FLAG: GlOp = 25; + pub const BEGIN: GlOp = 26; + pub const END: GlOp = 27; + pub const VERTEX2F: GlOp = 28; + pub const VERTEX3F: GlOp = 29; + pub const VERTEX4F: GlOp = 30; + pub const VERTEX2I: GlOp = 31; + pub const FLUSH: GlOp = 32; + pub const CLEAR: GlOp = 33; + pub const FINISH: GlOp = 34; + pub const RASTER_POS2F: GlOp = 35; + pub const RASTER_POS3F: GlOp = 36; + pub const RASTER_POS4F: GlOp = 37; + pub const INDEXF: GlOp = 38; + pub const INDEX_MASK: GlOp = 39; + pub const CLEAR_INDEX: GlOp = 40; + pub const DEPTH_RANGE: GlOp = 41; + + // + // glLightfv: the light's number, 0 for super::GL_LIGHT0, the parameter and + // four floats, of which the parameter uses as many as it takes. + // + pub const LIGHT: GlOp = 42; + + // glMaterialfv: the face, the parameter and four floats, likewise. + pub const MATERIAL: GlOp = 43; + + // + // glLightModelfv: the parameter and four floats; a boolean parameter + // is true for a nonzero first float. + // + pub const LIGHT_MODEL: GlOp = 44; + pub const COLOR_MATERIAL: GlOp = 45; + pub const CULL_FACE: GlOp = 46; + pub const FRONT_FACE: GlOp = 47; + pub const SCISSOR: GlOp = 48; + pub const ALPHA_FUNC: GlOp = 49; + + // super::GL_NEVER to super::GL_ALWAYS, the reference as int32, the value mask + pub const STENCIL_FUNC: GlOp = 50; + pub const STENCIL_OP: GlOp = 51; + pub const STENCIL_MASK: GlOp = 52; + pub const CLEAR_STENCIL: GlOp = 53; + pub const BLEND_FUNC: GlOp = 54; + pub const BLEND_EQUATION: GlOp = 55; + pub const BLEND_COLOR: GlOp = 56; + pub const LOGIC_OP: GlOp = 57; + pub const COLOR_MASK: GlOp = 58; + + // + // glFogfv: the parameter and four floats, of which it uses as many as + // it takes; super::GL_FOG_MODE's is the mode's enumerant as a float. + // + pub const FOG: GlOp = 59; + pub const POLYGON_OFFSET: GlOp = 60; + pub const POLYGON_OFFSET_EXT: GlOp = 61; + pub const CLIP_PLANE: GlOp = 62; + pub const POINT_SIZE: GlOp = 63; + pub const LINE_WIDTH: GlOp = 64; + pub const LINE_STIPPLE: GlOp = 65; + pub const POLYGON_MODE: GlOp = 66; + pub const TEXCOORD2F: GlOp = 67; + pub const TEXCOORD3F: GlOp = 68; + pub const TEXCOORD4F: GlOp = 69; + + // Parameter and four floats; mode's enumerant as a float. + pub const TEX_ENV: GlOp = 70; + + // + // IRIS GL swaptmesh as the OpenGL client passes it through an open + // triangle strip: the next vertex makes its triangle with the strip's + // oldest retained vertex instead of its middle one. + // + pub const SWAP_TMESH: GlOp = 71; + pub const HINT: GlOp = 72; + + // + // glTexGenfv: the coordinate (super::GL_S to super::GL_Q), the parameter and four + // floats; super::GL_TEXTURE_GEN_MODE's mode is its enumerant as a float. + // + pub const TEX_GEN: GlOp = 73; +} + +/// Only the fields a kind the rasterizer executes needs are here. +/// Coordinates are plane pixels, the rectangle inclusive. +#[derive(Clone, Copy, Default)] +pub(super) struct RasterOp { + /// RasterKind. + pub kind: u32, + /// The colour targets written, as the drawable names them. + pub targets: [ColourTarget; GL_MAX_TARGETS as usize], + pub ntargets: u32, + pub x0: i32, + pub y0: i32, + pub x1: i32, + pub y1: i32, + /// Only pixels inside these are written; at most the rectangle's. An + /// empty list is how the board says its window has no visible pixel. + pub clip: ClipList, + /// CLEAR: GL_COLOR_BUFFER_BIT and GL_DEPTH_BUFFER_BIT. + pub clear_buffers: u32, + /// The GL clear colour; the rasterizer quantizes it to the plane. + pub clear_rgba: [f32; 4], + /// glColorMask's channels written: GL_COLOUR_MASK_RED and so on. + pub clear_colour_mask: u32, + /// In the depth plane's units; the plane keeps its low bits. + pub clear_depth24: u32, + /// GL_STENCIL_BUFFER_BIT: the clear value, written under the + /// stencil write mask; the plane keeps its low bits. + pub clear_stencil: u32, + pub stencil_mask: u32, +} + +/// Where a context draws, as the board's context switch, window clip and +/// buffer selection give it. Coordinates are plane pixels. +#[derive(Clone, Copy, Default)] +pub(super) struct GlDrawable { + /// False while the board has no window for the context: nothing draws, + /// and each draw, clear or bitmap is logged as dropped. + pub bound: bool, + /// The window's lower-left pixel, where window coordinates start; its + /// top-left under a window matrix whose rows run down the display. + pub origin: [i32; 2], + /// 0/0 until the board supplies a validated drawable extent. + pub width: u32, + pub height: u32, + /// The pixels the window owns. A bound window with no rectangle is one + /// other windows wholly cover: nothing draws and nothing is logged. The + /// list's empty flag plays no part here. + pub clip: ClipList, + /// The colour targets glDrawBuffer's front and back buffers name. + pub front: ColourTarget, + pub back: ColourTarget, + /// The targets draw-buffer bits 2 and 3 name; zero where the board has + /// none, and the bit is then ignored. + pub extra: [ColourTarget; 2], + /// The target GL_DRAW_OVERLAY names; zero where none. + pub overlay: ColourTarget, +} + +/// The GL state a draw is made under. +#[derive(Clone, Copy, Default)] +pub(super) struct GlDrawState { + /// Column-major, as glLoadMatrixf lays a matrix out. + pub modelview: [f32; 16], + pub projection: [f32; 16], + pub viewport: [i32; 4], + /// GL_FLAT or GL_SMOOTH. + pub shade_model: u32, + pub depth_test: bool, + /// The comparison minus GL_NEVER, 0 to 7. + pub depth_func: u32, + pub depth_mask: bool, + /// glDepthRange's near and far, each clamped to [0, 1] (2.10.1). + pub depth_range: [f32; 2], + /// GL_DRAW_* bits; those the drawable has no target for are ignored. + pub draw_buffers: u32, + /// glColorMask's channels, GL_COLOUR_MASK_RED and so on (4.2.2). + pub colour_mask: u32, + /// Polygon culling (3.5.1): GL_CULL_FACE's enable, + /// glCullFace's mode, and glFrontFace's GL_CW or GL_CCW. Any + /// front_face but GL_CW is GL_CCW. + pub cull_face: bool, + pub cull_face_mode: u32, + pub front_face: u32, + /// The scissor test (4.1.2): its enable and glScissor's box, + /// x, y, width and height in window coordinates. + pub scissor_test: bool, + pub scissor: [i32; 4], + /// The alpha test (4.1.4): its enable, glAlphaFunc's + /// comparison minus GL_NEVER, and its reference clamped to [0, 1]. + pub alpha_test: bool, + pub alpha_func: u32, + pub alpha_ref: f32, + /// The stencil test (4.1.5): its enable, glStencilFunc's + /// comparison minus GL_NEVER, reference (kept as sent) and value mask, + /// glStencilMask's write mask, and glStencilOp's three operations as + /// enumerants. + pub stencil_test: bool, + pub stencil_func: u32, + pub stencil_ref: i32, + pub stencil_value_mask: u32, + pub stencil_write_mask: u32, + pub stencil_op: [u32; 3], + /// Blending (4.1.6): its enable, glBlendFunc's factors, + /// glBlendEquation's equation, and glBlendColor clamped to [0, 1]. + pub blend: bool, + pub blend_src: u32, + pub blend_dst: u32, + pub blend_equation: u32, + pub blend_color: [f32; 4], + /// The logical operation (4.1.8): the GL_COLOR_LOGIC_OP enable and + /// glLogicOp's operation. + pub color_logic_op: bool, + pub logic_op_mode: u32, + /// Fog (3.10): its enable, mode, density, start and end, the + /// fog colour clamped to [0, 1]. The board applies its own curve + /// (GlPlanes::fog_factor()) and reads only the enable and the colour. + pub fog: bool, + pub fog_mode: u32, + pub fog_density: f32, + pub fog_start: f32, + pub fog_end: f32, + pub fog_color: [f32; 4], + /// Polygon offset (3.5.5): the enables as GL_OFFSET_FILL and so + /// on, the factor, and the constant term, either glPolygonOffset's + /// units or EXT_polygon_offset's bias; the command last given sets its + /// own and zeroes the other. + pub polygon_offset: u32, + pub offset_factor: f32, + pub offset_units: f32, + pub offset_bias: f32, + /// Client clip planes (2.11): bit i enables GL_CLIP_PLANEi; each plane + /// is kept in eye coordinates. + pub clip_planes: u32, + pub clip_plane: [[f32; 4]; GL_CLIP_PLANES as usize], + pub drawable: GlDrawable, + /// A window matrix, for a board that takes one eye-to-window + /// transform in place of a projection and a viewport; none of it is GL + /// state (GlContext::set_window_matrix()). While window_matrix_given is set, + /// an eye position e goes to h = W e by window_matrix W, column-major, + /// and h replaces the projection, the divide and the viewport map: + /// window x and y are h0 / h3 and h1 / h3, unrounded, in pixels from + /// the drawable's origin, and window depth is + /// window_depth_scale * h2 / h3. With window_rows_down set, window y + /// counts rows down the display from the drawable's origin (then the + /// window's top-left pixel). Clipping bounds x and y by GL_WINDOW_GUARD and + /// w by the near-w plane; near and far are no clip, the raster drops a + /// fragment by its window depth (0 and 1). This + /// departs from OpenGL in raster position validity, bitmap rows, the tie + /// rule and the scissor, which is measured from the top. + pub window_matrix_given: bool, + pub window_matrix: [f32; 16], + pub window_depth_scale: f32, + pub window_rows_down: bool, + /// The texture environment belongs to the context, not its objects. + pub texture_1d: bool, + pub texture_2d: bool, + pub texture_3d: bool, + pub texture_env_mode: u32, + pub texture_env_color: [f32; 4], +} + +/// Host-owned deferred boundary. `vertices` was a malloc'd float array of +/// `capacity` floats with `count` vertices used; here a Vec sized by +/// `capacity`. Never shallow-copy a live context. +#[derive(Clone, Default)] +pub(super) struct GlPolygonAssembly { + pub vertices: Vec, + pub count: u32, + pub capacity: u32, + pub failed: bool, + pub error: u32, + /// Host activation marker; not part of the context's GL state. + pub enabled: bool, + pub loaded: bool, +} + +#[derive(Clone, Default)] +pub(super) struct GlContext { + /// The hardware identity the board keys this context by. + pub hw_id: u32, + pub cur: GlDrawState, + /// The texture matrix (2.10.2), applied to each vertex before clipping. + pub texture: [f32; 16], + pub cur_color: [f32; 4], + pub cur_normal: [f32; 3], + pub cur_edge: u32, + /// The current raster position (2.12): window x, y and z and clip w, + /// whether it is valid, its eye distance and its colour. + pub raster_pos: [f32; 4], + pub raster_valid: bool, + pub raster_color: [f32; 4], + pub raster_index: f32, + /// In the depth plane's units, as the board delivered it; a clear + /// keeps the low bits the plane holds, so all ones is 1.0 at any depth. + pub clear_depth: u32, + /// Aliased sizes and retained polygon modes (3.3 to 3.5). + pub point_size: f32, + pub line_width: f32, + pub line_stipple_repeat: u32, + pub line_stipple_pattern: u32, + pub polygon_mode: [u32; 2], + pub raster_enables: u32, + /// Canonical window rows: bit x of row y (OpenGL 1.2.1, 3.5.2). + pub polygon_stipple: [u32; 32], + pub polygon: GlPolygonAssembly, + /// The primitive open since glBegin, GL_POINTS to GL_POLYGON. + pub begin_mode: u32, + pub in_begin: bool, + /// Vertices waiting for the rest of their piece, nheld of them. + pub held: [[f32; vtx::FLOATS]; GL_HELD_VERTICES as usize], + pub nheld: u32, + /// Set while the next triangle of an open strip is an odd one, whose + /// first two vertices are sent swapped (2.6.1). + pub strip_odd: bool, + /// Set while held[2] keeps the open triangle strip's oldest retained + /// vertex, which a swap makes the next triangle's first vertex. + pub strip_prev: bool, + /// Set from a swap until the next vertex of the strip. + pub strip_swap: bool, + pub cur_texcoord: [f32; 4], + pub raster_texcoord: [f32; 4], + /// Host policy and presence markers; not part of the context's GL state. + pub standalone_stipple: bool, + pub line_phase_loaded: bool, + pub draw_error_loaded: bool, + pub line_phase: u32, + /// Advisory modes are retained even though rendering ignores them. + pub hints: [u32; GL_HINTS as usize], + pub sprite_mode: u32, + pub sprite_axis: [f32; 3], + pub tex_gen_mode: [u32; 4], + pub tex_gen_object_plane: [[f32; 4]; 4], + pub tex_gen_eye_plane: [[f32; 4]; 4], + /// Draw-result failures already reported, one bit per DrawFailure. + draw_failures_reported: u32, +} + +/// The kinds of checked-draw failure that have no consumer, each reported +/// once per context. +#[derive(Clone, Copy)] +enum DrawFailure { + Unsupported, + Resource, + Host, + Invalid, +} + +/// A decoded command: its entry point and its scalar arguments as 32-bit +/// words in the specification's order, a float argument as the IEEE single +/// bits the client sent. +#[derive(Clone, Copy)] +pub(super) struct GlCmd<'a> { + pub op: u16, + pub args: &'a [u32], +} + +impl<'a> GlCmd<'a> { + pub fn new(op: u16, args: &'a [u32]) -> GlCmd<'a> { + GlCmd { op, args } + } +} + +/// A batch of vertices drawn as one primitive mode under one draw state. +/// The context lights each vertex before it is drawn, so no draw carries +/// materials: a vertex's material index is 0, the draw state's. Everything +/// it points at is the context's, borrowed for the call that carries it. +#[derive(Clone, Copy)] +pub(super) struct GlDraw<'a> { + /// The context's hw_id. + pub context: u32, + /// GL_POINTS to GL_POLYGON. + pub mode: u32, + pub count: u32, + /// count * vtx::FLOATS floats, texture-matrix-transformed strq + /// included, ready for clipping. + pub vertices: &'a [f32], + /// True when the vertices carry back-face colour or index values for a + /// back-facing polygon: two-sided lighting (2.13.1). + pub back_colors: bool, + pub state: &'a GlDrawState, + /// Borrowed dimensions; zero in direct submissions means one. + pub point_size: f32, + pub line_width: f32, + pub point_smooth: bool, + pub line_smooth: bool, + pub polygon_smooth: bool, + /// 32 canonical rows, used while polygon_stipple_enabled is set. + pub polygon_stipple: &'a [u32; 32], + pub polygon_stipple_enabled: bool, + /// Front and back modes; None keeps legacy FILL. + pub polygon_modes: Option<[u32; 2]>, + pub fill_by_triangle: bool, + pub line_stipple_enabled: bool, + pub line_stipple_repeat: u32, + pub line_stipple_pattern: u32, + /// The context's standalone stipple phase, which the draw advances in + /// place; None counts a phase local to the draw. + pub line_phase: Option, + /// With a borrowed phase, reset a local candidate before counting. + pub line_phase_reset: bool, +} + +impl<'a> GlDraw<'a> { + /// A draw of count vertices in mode, the rest of it off. + pub fn new(mode: u32, count: u32, vertices: &'a [f32], state: &'a GlDrawState, polygon_stipple: &'a [u32; 32]) -> Self { + GlDraw { + context: 0, + mode, + count, + vertices, + back_colors: false, + state, + point_size: 0.0, + line_width: 0.0, + point_smooth: false, + line_smooth: false, + polygon_smooth: false, + polygon_stipple, + polygon_stipple_enabled: false, + polygon_modes: None, + fill_by_triangle: false, + line_stipple_enabled: false, + line_stipple_repeat: 0, + line_stipple_pattern: 0, + line_phase: None, + line_phase_reset: false, + } + } +} + +/// Where a context's draws go. Every draw is executed before the call +/// returns, so what it points at need only live that long. +pub(super) trait GlSink { + /// Rasterize one draw; a line stipple's phase comes back in + /// d.line_phase. + fn draw(&mut self, d: &mut GlDraw) -> GlDrawResult; +} + +// Functions. + +// The last client clip plane and light enumerants. +pub(super) const GL_CLIP_PLANE_LAST: u32 = GL_CLIP_PLANE0 + GL_CLIP_PLANES - 1; + +/// The word `i` of a command's arguments (a short list reads as 0 rather +/// than past the end). +pub(super) fn gl_arg(p: &[u32], i: usize) -> u32 { + p.get(i).copied().unwrap_or(0) +} + +pub(super) fn gl_float(bits: u32) -> f32 { + f32::from_bits(bits) +} + +pub(super) fn gl_bits(value: f32) -> u32 { + value.to_bits() +} + +impl GlContext { + + /// OpenGL 1.2.1 initial state (2.7, 2.10, 2.13.7, 4.1.5, 4.2.1-4.2.3). + pub fn init(&mut self, hw_id: u32) { + *self = GlContext::default(); + self.hw_id = hw_id; + self.hints[..GL_HINTS as usize].fill(GL_DONT_CARE); + self.cur_color = [1.0; 4]; + self.cur.shade_model = GL_SMOOTH; + self.sprite_mode = GL_SPRITE_AXIAL_SGIX; + self.sprite_axis[2] = 1.0; + self.cur.modelview = IDENT; + self.cur.projection = IDENT; + self.texture = IDENT; + self.cur.depth_func = GL_LESS; + self.cur.alpha_func = GL_ALWAYS; + self.cur.stencil_func = GL_ALWAYS; + self.cur.stencil_value_mask = u32::MAX; + self.cur.stencil_write_mask = u32::MAX; + self.cur.stencil_op[..3].fill(GL_KEEP); + self.cur.blend_src = 1; + self.cur.blend_equation = GL_FUNC_ADD; + self.cur.logic_op_mode = GL_COPY; + self.cur.texture_env_mode = GL_MODULATE; + self.cur_texcoord[3] = 1.0; + self.raster_texcoord[3] = 1.0; + self.tex_gen_mode[..4].fill(GL_EYE_LINEAR); + self.tex_gen_object_plane[0][0] = 1.0; + self.tex_gen_eye_plane[0][0] = 1.0; + self.tex_gen_object_plane[1][1] = 1.0; + self.tex_gen_eye_plane[1][1] = 1.0; + self.cur.fog_mode = GL_EXP; + self.cur.fog_density = 1.0; + self.cur.fog_end = 1.0; + self.point_size = 1.0; + self.line_width = 1.0; + self.line_stipple_repeat = 1; + self.line_stipple_pattern = 0xffff; + self.polygon_mode[0] = GL_FILL; + self.polygon_mode[1] = GL_FILL; + self.polygon_stipple = [0xffff_ffff; 32]; + self.cur.depth_mask = true; + self.cur.depth_range[1] = 1.0; + self.clear_depth = u32::MAX; + self.cur.draw_buffers = GL_DRAW_FRONT; + self.cur.colour_mask = GL_COLOUR_MASK_ALL; + self.cur.cull_face_mode = GL_BACK; + self.cur.front_face = GL_CCW; + self.cur_normal[2] = 1.0; + self.cur_edge = 1; + self.raster_pos[3] = 1.0; + self.raster_valid = true; + self.raster_color[..4].fill(1.0); + self.raster_index = 1.0; + } + +} + +impl GlPolygonAssembly { + pub fn polygon_release(&mut self) { + self.vertices = Vec::new(); + self.count = 0; + self.capacity = 0; + self.failed = false; + self.loaded = false; + } +} + +impl GlContext { + /// Audited host consumers opt in; reset preserves that host policy. + pub fn enable_polygon_modes(&mut self) -> bool { + if self.in_begin { + return false; + } + self.polygon.enabled = true; + true + } + + pub fn enable_line_stipple(&mut self) -> bool { + if self.in_begin { + return false; + } + self.standalone_stipple = true; + true + } + + pub fn cleanup(&mut self) { + self.polygon.polygon_release(); + self.polygon.enabled = false; + self.polygon.error = 0; + self.standalone_stipple = false; + self.line_phase_loaded = false; + self.draw_error_loaded = false; + self.line_phase = 0; + } + + pub fn reset(&mut self, hw_id: u32) { + let enabled = self.polygon.enabled; + let standalone = self.standalone_stipple; + + self.cleanup(); + self.init(hw_id); + self.polygon.enabled = enabled; + self.standalone_stipple = standalone; + } +} + +impl GlPolygonAssembly { + /// Count and bytes are representation bounds, not hardware size limits. + /// `vertices` is kept `capacity * vtx::FLOATS` floats long. + pub fn polygon_reserve(&mut self, count: usize) -> bool { + let mut capacity: usize = std::cmp::max(self.capacity as usize, 8); + + if count > u32::MAX as usize || count > usize::MAX / vtx::FLOATS / 4 { + return false; + } + if count <= self.capacity as usize { + return true; + } + while capacity < count { + if capacity > (u32::MAX / 2) as usize + || capacity > usize::MAX / vtx::FLOATS / 4 / 2 + { + capacity = count; + break; + } + capacity *= 2; + } + let floats = capacity * vtx::FLOATS; + if self.vertices.try_reserve_exact(floats.saturating_sub(self.vertices.len())).is_err() { + return false; + } + self.vertices.resize(floats, 0.0); + self.capacity = capacity as u32; + true + } +} + +impl GlContext { + + /// Whether the context consumes a draw's result: with per-face polygon + /// modes or the standalone line stipple active. + pub fn draws_checked(&self) -> bool { + self.polygon.enabled || self.standalone_stipple + } + + /// Account for a draw's result, for a draw made while draws_checked() was + /// `checked`. + pub fn draw_result(&mut self, checked: bool, result: GlDrawResult) { + if !checked { + return; + } + if result == GL_DRAW_OUT_OF_MEMORY && self.polygon.error == 0 { + self.polygon.error = GL_OUT_OF_MEMORY; + } + if result == GL_DRAW_OK { + return; + } + match result { + GL_DRAW_OUT_OF_MEMORY => {} + GL_DRAW_UNSUPPORTED => { + self.report_draw_failure(DrawFailure::Unsupported, "unsupported checked draw"); + } + GL_DRAW_RESOURCE_ERROR => { + self.report_draw_failure(DrawFailure::Resource, "checked draw resource failure"); + } + GL_DRAW_HOST_ERROR => { + self.report_draw_failure(DrawFailure::Host, "checked draw host failure"); + } + _ => { + self.report_draw_failure(DrawFailure::Invalid, &format!("invalid checked draw result {}", result)); + } + } + } + + /// Say that a draw failed with nobody to tell, the first time for each + /// kind of failure. + fn report_draw_failure(&mut self, kind: DrawFailure, what: &str) { + let bit = 1u32 << kind as u32; + + if self.draw_failures_reported & bit == 0 { + self.draw_failures_reported |= bit; + eprintln!("sgi-gl: {what} has no result consumer"); + } + } + + /// Canonical 32x32 pattern, copied on upload. Bit x in row y names window + /// (x, y). Rejects use inside Begin/End. + pub fn polygon_stipple(&mut self, rows: &[u32; 32]) -> bool { + if self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: polygon stipple inside glBegin: invalid operation"); + return false; + } + self.polygon_stipple = *rows; + true + } + + /// Point the context at a drawable. A bound half-known extent is refused + /// without changing the current drawable. Unbinding clears both dimensions. + pub fn set_drawable(&mut self, d: &GlDrawable) -> bool { + if d.bound && (d.width != 0) != (d.height != 0) { + return false; + } + self.cur.drawable = *d; + if !d.bound { + self.cur.drawable.width = 0; + self.cur.drawable.height = 0; + } + true + } + +} + +impl GlContext { + /// The current matrix of the stack a matrix mode names, or None. + pub fn slot_matrix(&mut self, slot: u32) -> Option<&mut [f32; 16]> { + match slot { + GL_MODELVIEW => Some(&mut self.cur.modelview), + GL_PROJECTION => Some(&mut self.cur.projection), + GL_TEXTURE => Some(&mut self.texture), + _ => { + crate::dlog!( + LogModule::Odyssey, + "sgi-gl: matrix mode {:#x} is not modelled; matrix command ignored", + slot + ); + None + } + } + } + + /// Load m, column-major, as the current matrix of the stack slot names: + /// GL_MODELVIEW, GL_PROJECTION or GL_TEXTURE, whatever the + /// context's matrix mode. False, and logged, for a slot the context does + /// not keep. + pub fn set_matrix(&mut self, slot: u32, m: &[f32; 16]) -> bool { + match self.slot_matrix(slot) { + None => false, + Some(dst) => { + *dst = *m; + true + } + } + } +} + +/// The arguments each command takes. +pub(super) const GL_NARGS_LEN: usize = 74; +pub(super) static GL_NARGS: [u8; GL_NARGS_LEN] = { + let mut t = [0u8; GL_NARGS_LEN]; + t[op::HINT as usize] = 2; + t[op::VIEWPORT as usize] = 4; + t[op::CLEAR_COLOR as usize] = 4; + t[op::CLEAR_DEPTH as usize] = 1; + t[op::SHADE_MODEL as usize] = 1; + t[op::MATRIX_MODE as usize] = 1; + t[op::LOAD_MATRIX as usize] = 16; + t[op::LOAD_IDENTITY as usize] = 0; + t[op::MULT_MATRIX as usize] = 16; + t[op::FRUSTUM as usize] = 6; + t[op::ORTHO as usize] = 6; + t[op::PUSH_MATRIX as usize] = 0; + t[op::POP_MATRIX as usize] = 0; + t[op::ROTATE as usize] = 4; + t[op::SCALE as usize] = 3; + t[op::TRANSLATE as usize] = 3; + t[op::ENABLE as usize] = 1; + t[op::DISABLE as usize] = 1; + t[op::DEPTH_FUNC as usize] = 1; + t[op::DEPTH_MASK as usize] = 1; + t[op::DRAW_BUFFERS as usize] = 1; + t[op::COLOR3F as usize] = 3; + t[op::COLOR4F as usize] = 4; + t[op::COLOR3UB as usize] = 3; + t[op::COLOR4UB as usize] = 4; + t[op::NORMAL3F as usize] = 3; + t[op::EDGE_FLAG as usize] = 1; + t[op::BEGIN as usize] = 1; + t[op::END as usize] = 0; + t[op::VERTEX2F as usize] = 2; + t[op::VERTEX3F as usize] = 3; + t[op::VERTEX4F as usize] = 4; + t[op::VERTEX2I as usize] = 2; + t[op::FLUSH as usize] = 0; + t[op::CLEAR as usize] = 1; + t[op::FINISH as usize] = 0; + t[op::RASTER_POS2F as usize] = 2; + t[op::RASTER_POS3F as usize] = 3; + t[op::RASTER_POS4F as usize] = 4; + t[op::INDEXF as usize] = 1; + t[op::INDEX_MASK as usize] = 1; + t[op::CLEAR_INDEX as usize] = 1; + t[op::DEPTH_RANGE as usize] = 2; + t[op::LIGHT as usize] = 6; + t[op::MATERIAL as usize] = 6; + t[op::LIGHT_MODEL as usize] = 5; + t[op::COLOR_MATERIAL as usize] = 2; + t[op::CULL_FACE as usize] = 1; + t[op::FRONT_FACE as usize] = 1; + t[op::SCISSOR as usize] = 4; + t[op::ALPHA_FUNC as usize] = 2; + t[op::STENCIL_FUNC as usize] = 3; + t[op::STENCIL_OP as usize] = 3; + t[op::STENCIL_MASK as usize] = 1; + t[op::CLEAR_STENCIL as usize] = 1; + t[op::BLEND_FUNC as usize] = 2; + t[op::BLEND_EQUATION as usize] = 1; + t[op::BLEND_COLOR as usize] = 4; + t[op::LOGIC_OP as usize] = 1; + t[op::COLOR_MASK as usize] = 4; + t[op::FOG as usize] = 5; + t[op::POLYGON_OFFSET as usize] = 2; + t[op::POLYGON_OFFSET_EXT as usize] = 2; + t[op::CLIP_PLANE as usize] = 5; + t[op::POINT_SIZE as usize] = 1; + t[op::LINE_WIDTH as usize] = 1; + t[op::LINE_STIPPLE as usize] = 2; + t[op::POLYGON_MODE as usize] = 2; + t[op::TEXCOORD2F as usize] = 2; + t[op::TEXCOORD3F as usize] = 3; + t[op::TEXCOORD4F as usize] = 4; + t[op::TEX_ENV as usize] = 5; + t[op::TEX_GEN as usize] = 6; + t[op::SWAP_TMESH as usize] = 0; + t +}; + +impl GlDrawState { + /// The colour targets the draw state's draw buffers select, in bit order + /// and each once, into out; returns how many. + pub fn draw_targets(&self, out: &mut [ColourTarget; GL_MAX_TARGETS as usize]) -> u32 { + let pick: [ColourTarget; GL_MAX_TARGETS as usize] = [ + self.drawable.front, + self.drawable.back, + self.drawable.extra[0], + self.drawable.extra[1], + ]; + let mut n: u32 = 0; + + if (self.draw_buffers & GL_DRAW_OVERLAY) != 0 && self.drawable.overlay != 0 { + out[0] = self.drawable.overlay; + return 1; + } + for (bit, &target) in pick.iter().enumerate().take(GL_MAX_TARGETS as usize) { + if self.draw_buffers & (1u32 << bit) == 0 { + continue; + } + if bit >= 2 && target == 0 { + crate::dlog!( + LogModule::Odyssey, + "sgi-gl: draw buffer bit {} of {:#x} has no buffer; ignored", + bit, + self.draw_buffers + ); + continue; + } + // A drawable whose buffers are one target, as a single-buffered one + // may be, is stored to once: a read-modify-write stage in front of + // the store must not apply twice. + if !out[..n as usize].contains(&target) { + out[n as usize] = target; + n += 1; + } + } + if self.draw_buffers & !0xfu32 != 0 { + crate::dlog!(LogModule::Odyssey, "sgi-gl: draw buffers {:#x} beyond bit 3 ignored", self.draw_buffers); + } + n + } +} + +impl GlContext { + + /// Send one piece of the open primitive: count of v0 to v3. + #[allow(clippy::too_many_arguments, reason = "the primitive and up to four vertices of it")] + pub fn submit(&mut self, sink: &mut dyn GlSink, mode: u32, count: u32, v0: Option<&[f32; vtx::FLOATS]>, v1: Option<&[f32; vtx::FLOATS]>, v2: Option<&[f32; vtx::FLOATS]>, v3: Option<&[f32; vtx::FLOATS]>) { + let stipple = if mode == GL_LINES { self.standalone_stipple } else { self.polygon.enabled }; + let v = [v0, v1, v2, v3]; + let mut verts = [0.0f32; 4 * vtx::FLOATS]; + + for i in 0..count as usize { + if let Some(src) = v[i] { + verts[i * vtx::FLOATS..][..vtx::FLOATS].copy_from_slice(src); + } + } + let checked = self.draws_checked(); + let mut d = GlDraw::new(mode, count, &verts, &self.cur, &self.polygon_stipple); + + d.context = self.hw_id; + d.point_size = self.point_size; + d.line_width = self.line_width; + d.point_smooth = self.raster_enables & GL_RASTER_POINT_SMOOTH != 0; + d.line_smooth = self.raster_enables & GL_RASTER_LINE_SMOOTH != 0; + d.polygon_smooth = self.raster_enables & GL_RASTER_POLYGON_SMOOTH != 0; + d.polygon_modes = (self.polygon.enabled).then_some(self.polygon_mode); + d.line_stipple_enabled = stipple && (self.raster_enables & GL_RASTER_LINE_STIPPLE != 0); + d.line_stipple_repeat = if stipple { self.line_stipple_repeat } else { 0 }; + d.line_stipple_pattern = if stipple { self.line_stipple_pattern } else { 0 }; + d.line_phase = (mode == GL_LINES && self.standalone_stipple).then_some(self.line_phase); + d.line_phase_reset = self.begin_mode == GL_LINES; + d.polygon_stipple_enabled = self.raster_enables & GL_RASTER_POLYGON_STIPPLE != 0; + + let result = sink.draw(&mut d); + let phase = d.line_phase; + if let Some(phase) = phase { + self.line_phase = phase; + } + self.draw_result(checked, result); + } + + /// A vertex's texture coordinates: the current ones through the + /// texture matrix, before clipping (OpenGL 1.2.1, 2.10.2). The board + /// generates none of its own. + pub fn texture_coord(&self) -> [f32; 4] { + xform(&self.texture, self.cur_texcoord) + } + + /// Hold a vertex in slot n of the three a context keeps. + pub fn hold(&mut self, n: u32, v: &[f32; vtx::FLOATS]) { + self.held[n as usize] = *v; + } +} + +impl GlContext { + + /// Capture one vertex with the current attributes, without + /// drawing. `bits` holds `components` words. + pub fn capture_vertex(&self, mode: u32, bits: &[u32], components: u32) -> [f32; vtx::FLOATS] { + let mut out = [0.0; vtx::FLOATS]; + out[vtx::POSITION + 3] = 1.0; + for i in 0..std::cmp::min(components as usize, bits.len()) { + out[vtx::POSITION + i] = gl_float(bits[i]); + } + out[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&self.cur_color); + out[vtx::NORMAL..vtx::NORMAL + 3].copy_from_slice(&self.cur_normal); + out[vtx::TEXCOORD..vtx::TEXCOORD + 4].copy_from_slice(&self.texture_coord()); + out[vtx::EDGE] = f32::from_bits(self.cur_edge); + // Material and lighting belong to the instant this vertex arrived. + out[vtx::BACK_COLOR..vtx::BACK_COLOR + 4].copy_from_slice(&self.cur_color); + + if self.polygon.enabled + && (mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN || mode == GL_QUAD_STRIP) + { + out[vtx::EDGE] = f32::from_bits(1); + } + out + } + + /// One vertex of the primitive open since glBegin (OpenGL 1.2.1, 2.6.1), + /// carrying the current attributes. The context holds at most three + /// earlier vertices, in held[] with nheld of them in use, and every piece + /// is sent as its own draw as soon as its last vertex arrives, so a + /// context switch between two vertices loses nothing. A line loop's + /// closing segment is sent at glEnd. Vertices that complete no piece + /// before glEnd draw nothing. + pub fn vertex(&mut self, sink: &mut dyn GlSink, pos: &[f32; 4], components: u32) { + let h0 = self.held[0]; + let h1 = self.held[1]; + let h2 = self.held[2]; + let n = self.nheld; + + if !self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: vertex outside glBegin ignored"); + return; + } + let bits = [pos[0].to_bits(), pos[1].to_bits(), pos[2].to_bits(), pos[3].to_bits()]; + let mode = self.begin_mode; + let cur = self.capture_vertex(mode, &bits, components); + if self.polygon.enabled && self.begin_mode == GL_POLYGON { + let p = &mut self.polygon; + + if p.failed { + return; + } + if p.count == u32::MAX || !p.polygon_reserve(p.count as usize + 1) { + p.polygon_release(); + p.failed = true; + if p.error == 0 { + p.error = GL_OUT_OF_MEMORY; + } + return; + } + let at = p.count as usize * vtx::FLOATS; + p.vertices[at..at + vtx::FLOATS].copy_from_slice(&cur); + p.count += 1; + return; + } + + match self.begin_mode { + GL_POINTS => { + self.submit(sink, GL_POINTS, 1, Some(&cur), None, None, None); + return; + } + GL_LINES => { + if n >= 1 { + self.submit(sink, GL_LINES, 2, Some(&h0), Some(&cur), None, None); + self.nheld = 0; + return; + } + } + GL_LINE_STRIP => { + if n >= 1 { + self.submit(sink, GL_LINES, 2, Some(&h0), Some(&cur), None, None); + self.hold(0, &cur); + return; + } + } + GL_LINE_LOOP => { + // Slot 0 keeps the first vertex for the closing segment. + if n >= 1 { + let prev = self.held[n as usize - 1]; + self.submit(sink, GL_LINES, 2, Some(&prev), Some(&cur), None, None); + self.hold(1, &cur); + self.nheld = 2; + return; + } + } + GL_TRIANGLES => { + if n >= 2 { + self.submit(sink, GL_TRIANGLES, 3, Some(&h0), Some(&h1), Some(&cur), None); + self.nheld = 0; + return; + } + } + GL_TRIANGLE_STRIP => { + if n >= 2 { + if self.strip_swap && self.strip_prev { + // After a swap the vertex replaces the middle one of the + // three the strip keeps, so its triangle shares the + // strip's oldest and newest vertices, and keeps the + // facing of the triangle before it, as a fan's does. The + // oldest stays the oldest for the next swap. + if self.strip_odd { + self.submit(sink, GL_TRIANGLES, 3, Some(&h2), Some(&h1), Some(&cur), None); + } else { + self.submit(sink, GL_TRIANGLES, 3, Some(&h1), Some(&h2), Some(&cur), None); + } + self.strip_swap = false; + self.hold(0, &h1); + self.hold(1, &cur); + return; + } + self.strip_swap = false; + if self.strip_odd { + self.submit(sink, GL_TRIANGLES, 3, Some(&h1), Some(&h0), Some(&cur), None); + } else { + self.submit(sink, GL_TRIANGLES, 3, Some(&h0), Some(&h1), Some(&cur), None); + } + self.strip_odd = !self.strip_odd; + self.hold(2, &h0); + self.strip_prev = true; + self.hold(0, &h1); + self.hold(1, &cur); + return; + } + } + GL_TRIANGLE_FAN => { + if n >= 2 { + self.submit(sink, GL_TRIANGLES, 3, Some(&h0), Some(&h1), Some(&cur), None); + self.hold(1, &cur); + return; + } + } + GL_POLYGON => { + if n >= 2 { + self.submit(sink, GL_POLYGON, 3, Some(&h0), Some(&h1), Some(&cur), None); + self.hold(1, &cur); + return; + } + } + GL_QUADS => { + if n >= 3 { + self.submit(sink, GL_QUADS, 4, Some(&h0), Some(&h1), Some(&h2), Some(&cur)); + self.nheld = 0; + return; + } + } + GL_QUAD_STRIP => { + if n >= 3 { + self.submit(sink, GL_QUAD_STRIP, 4, Some(&h0), Some(&h1), Some(&h2), Some(&cur)); + self.hold(0, &h2); + self.hold(1, &cur); + self.nheld = 2; + return; + } + } + _ => unreachable!("sgi-gl: begin mode past GL_POLYGON"), + } + self.hold(n, &cur); + self.nheld = n + 1; + } + +} + +impl GlDrawState { + /// An eye position's clip coordinates (2.10) under the draw state, in + /// single precision through the shared transform (dev::gl::math): by the + /// projection, or under a window matrix the window matrix's h with x and + /// y scaled by 1 / GL_WINDOW_GUARD and z by 2 window_depth_scale, less w. + pub fn eye_to_clip(&self, eye: [f32; 4]) -> [f32; 4] { + if !self.window_matrix_given { + return xform(&self.projection, eye); + } + let h = xform(&self.window_matrix, eye); + let guard = GL_WINDOW_GUARD as f32; + [h[0] / guard, h[1] / guard, 2.0 * self.window_depth_scale * h[2] - h[3], h[3]] + } + + /// The viewport as the shared viewport mapping takes it, depth [0, 1] + /// left normalized: the depth range maps it per fragment. + pub fn gl_viewport(&self) -> Viewport { + let [x, y, w, h] = self.viewport.map(|v| v as f32); + let mut vp = Viewport::default(); + + vp.set(x, y, w, h, 0.0, 1.0, 1.0); + vp + } + + /// Clip coordinates as GlDrawState::eye_to_clip() gives them to window x + /// and y on the grid of 1 / subpixel of a pixel, and normalized depth, + /// (z / w + 1) / 2, in single precision through the shared VPro geometry + /// (dev::gl::vertex::NearW): the viewport mapping + /// (dev::gl::vertex::Viewport::project_by()), or under a window matrix the + /// window position the matrix gives. The divide is a nearest-rounded + /// reciprocal and a multiply, and the position truncates onto the grid. + /// None for a w of zero or less. + pub fn clip_to_window(&self, clip: &[f32; 4], subpixel: f32) -> Option<[f32; 3]> { + if !self.window_matrix_given { + let v = self.gl_viewport().project_by::(Wv::<0, 0> { h: *clip, ..Default::default() }, [0.0, 0.0], subpixel); + return (v.ok != 0).then_some([v.x, v.y, v.z]); + } + let [x, y, z] = NearW::divide(clip)?; + // The guard is a power of two, so scaling by it is exact. + let guard = GL_WINDOW_GUARD as f32; + Some([NearW::snap(x * guard, subpixel), NearW::snap(y * guard, subpixel), (z + 1.0) * 0.5]) + } +} + +/// Whether op is one of glStencilOp's operations (4.1.5). +pub(super) fn gl_stencil_op_valid(op: u32) -> bool { + matches!(op, 0 | GL_KEEP | GL_REPLACE | GL_INCR | GL_DECR | GL_INVERT) +} + +/// Whether glBlendFunc accepts factor as a source factor (OpenGL 1.2.1, +/// table 4.1, with the imaging subset's constant factors) or, with dst, +/// as a destination one (table 4.2). +pub(super) fn gl_blend_factor_valid(factor: u32, dst: bool) -> bool { + match factor { + 0 + | 1 + | GL_SRC_ALPHA + | GL_ONE_MINUS_SRC_ALPHA + | GL_DST_ALPHA + | GL_ONE_MINUS_DST_ALPHA + | GL_CONSTANT_COLOR + | GL_ONE_MINUS_CONSTANT_COLOR + | GL_CONSTANT_ALPHA + | GL_ONE_MINUS_CONSTANT_ALPHA => true, + GL_SRC_COLOR | GL_ONE_MINUS_SRC_COLOR => dst, + GL_DST_COLOR | GL_ONE_MINUS_DST_COLOR | GL_SRC_ALPHA_SATURATE => !dst, + _ => false, + } +} + +/// Whether glBlendEquation accepts equation. +pub(super) fn gl_blend_equation_valid(equation: u32) -> bool { + matches!( + equation, + GL_FUNC_ADD + | GL_MIN + | GL_MAX + | GL_FUNC_SUBTRACT + | GL_FUNC_REVERSE_SUBTRACT + | GL_LOGIC_OP + ) +} + +impl GlContext { + /// glClipPlane (2.11): the plane p in object coordinates becomes p M^-1 in + /// eye coordinates, M the modelview current at the call. A plane number + /// the context does not keep, or a modelview with no inverse, which the + /// specification leaves undefined, changes nothing. + pub fn clip_plane(&mut self, plane: u32, p: &[f32; 4]) { + let inverse = invert(&self.cur.modelview).filter(|m| m.iter().all(|v| v.is_finite())); + let Some(inv) = inverse.filter(|_| plane < GL_CLIP_PLANES) else { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glClipPlane of plane {} ignored", plane); + return; + }; + // (p M^-1)_j = sum_i p_i M^-1[i][j], M^-1[i][j] at inv[j * 4 + i]. + self.cur.clip_plane[plane as usize] = std::array::from_fn(|j| (0..4).map(|i| p[i] * inv[j * 4 + i]).sum()); + } +} + +/// The rasterization enables kept as raster_enables bits; 0 for any other +/// capability. +pub(super) fn gl_raster_enable(cap: u32) -> u32 { + match cap { + GL_POINT_SMOOTH => GL_RASTER_POINT_SMOOTH, + GL_LINE_SMOOTH => GL_RASTER_LINE_SMOOTH, + GL_LINE_STIPPLE => GL_RASTER_LINE_STIPPLE, + GL_POLYGON_SMOOTH => GL_RASTER_POLYGON_SMOOTH, + GL_POLYGON_STIPPLE => GL_RASTER_POLYGON_STIPPLE, + _ => 0, + } +} + +impl GlContext { + /// glPointSize, glLineWidth, glLineStipple and glPolygonMode (3.3 to 3.5): + /// a size or width not above 0 or a face or mode the specification lacks + /// is an error and changes nothing, and the repeat clamps to [1, 256]. + /// The state is kept and answered, and a value the rasterizer does not + /// draw is logged. + pub fn raster_state(&mut self, op: u16, a: &[u32]) { + let f = gl_float(gl_arg(a, 0)); + + match op as u32 { + op::POINT_SIZE | op::LINE_WIDTH => { + if self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: size setter inside glBegin: invalid operation"); + return; + } + if !f.is_finite() || !(f > 0.0) { + crate::dlog!( + LogModule::Odyssey, + "sgi-gl: {}({}) ignored", + if op as u32 == op::POINT_SIZE { "glPointSize" } else { "glLineWidth" }, + f + ); + return; + } + if op as u32 == op::POINT_SIZE { + self.point_size = f; + } else { + self.line_width = f; + } + } + op::LINE_STIPPLE => { + if self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: line stipple inside glBegin: invalid operation"); + return; + } + let repeat = gl_arg(a, 0) as i32; + self.line_stipple_repeat = repeat.clamp(1, 256) as u32; + self.line_stipple_pattern = gl_arg(a, 1) & 0xffff; + } + _ => { + if self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: polygon mode inside glBegin: invalid operation"); + return; + } + let a0 = gl_arg(a, 0); + let a1 = gl_arg(a, 1); + let faces: u32 = if a0 == GL_FRONT_AND_BACK { + 3 + } else if a0 == GL_FRONT { + 1 + } else if a0 == GL_BACK { + 2 + } else { + 0 + }; + if faces == 0 || a1.wrapping_sub(GL_POINT) > GL_FILL - GL_POINT { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glPolygonMode({:#x}, {:#x}) ignored", a0, a1); + return; + } + for (i, mode) in self.polygon_mode.iter_mut().enumerate() { + if faces & (1u32 << i) != 0 { + *mode = a1; + } + } + if a1 != GL_FILL && !self.polygon.enabled { + crate::dlog!(LogModule::Odyssey, "sgi-gl: polygon mode {:#x} is kept; polygons are drawn filled", a1); + } + } + } + } + + pub fn capability(&mut self, cap: u32, on: bool) { + let raster = gl_raster_enable(cap); + let onv = on as u32; + + if raster != 0 { + if (cap == GL_POLYGON_STIPPLE || cap == GL_LINE_STIPPLE) && self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: stipple enable inside glBegin: invalid operation"); + return; + } + self.raster_enables = if on { self.raster_enables | raster } else { self.raster_enables & !raster }; + if on && cap == GL_LINE_STIPPLE && !self.standalone_stipple { + crate::dlog!(LogModule::Odyssey, "sgi-gl: standalone line stipple is kept; standalone lines are drawn without it"); + } + return; + } + match cap { + GL_DEPTH_TEST => self.cur.depth_test = on, + GL_CULL_FACE => self.cur.cull_face = on, + GL_SCISSOR_TEST => self.cur.scissor_test = on, + GL_ALPHA_TEST => self.cur.alpha_test = on, + GL_STENCIL_TEST => self.cur.stencil_test = on, + GL_BLEND => self.cur.blend = on, + // Colour-index drawing stores the index unchanged; no logical + // operation is modelled for it. + GL_INDEX_LOGIC_OP => {} + GL_COLOR_LOGIC_OP => self.cur.color_logic_op = on, + GL_FOG => self.cur.fog = on, + GL_POLYGON_OFFSET_FILL => self.cur.polygon_offset = deposit32(self.cur.polygon_offset, 0, 1, onv), + GL_POLYGON_OFFSET_LINE => self.cur.polygon_offset = deposit32(self.cur.polygon_offset, 1, 1, onv), + GL_POLYGON_OFFSET_POINT => self.cur.polygon_offset = deposit32(self.cur.polygon_offset, 2, 1, onv), + GL_CLIP_PLANE0..=GL_CLIP_PLANE_LAST => { + self.cur.clip_planes = deposit32(self.cur.clip_planes, cap - GL_CLIP_PLANE0, 1, onv); + } + _ => { + crate::dlog!( + LogModule::Odyssey, + "sgi-gl: capability {:#x} is not modelled; {} ignored", + cap, + if on { "glEnable" } else { "glDisable" } + ); + } + } + } + +} + +/// Four floats from command words `a[from..from + 4]`. +pub(super) fn gl_words4(a: &[u32], from: usize) -> [f32; 4] { + [ + gl_float(gl_arg(a, from)), + gl_float(gl_arg(a, from + 1)), + gl_float(gl_arg(a, from + 2)), + gl_float(gl_arg(a, from + 3)), + ] +} + +impl GlContext { + /// Execute one command against the context, sending any work to sink. + pub fn command(&mut self, sink: &mut dyn GlSink, cmd: &GlCmd) { + let a = cmd.args; + let mut pos = [0.0f32; 4]; + + if cmd.op as usize >= GL_NARGS.len() || a.len() != GL_NARGS[cmd.op as usize] as usize { + crate::dlog!(LogModule::Odyssey, "sgi-gl: command {} with {} arguments ignored", cmd.op, a.len()); + return; + } + match cmd.op as u32 { + op::TEXCOORD2F | op::TEXCOORD3F | op::TEXCOORD4F => { + self.cur_texcoord[2] = 0.0; + self.cur_texcoord[3] = 1.0; + for i in 0..a.len() { + self.cur_texcoord[i] = gl_float(a[i]); + } + } + op::SHADE_MODEL => self.cur.shade_model = a[0], + op::ENABLE | op::DISABLE => { + self.capability(a[0], cmd.op as u32 == op::ENABLE); + } + op::DEPTH_FUNC => { + if a[0].wrapping_sub(GL_NEVER_ENUM) > GL_ALWAYS { + crate::dlog!(LogModule::Odyssey, "sgi-gl: depth function {:#x} ignored", a[0]); + return; + } + self.cur.depth_func = a[0] - GL_NEVER_ENUM; + } + op::DEPTH_MASK => self.cur.depth_mask = a[0] != 0, + op::CULL_FACE => { + if gl_faces(a[0]) == 0 { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glCullFace({:#x}) ignored", a[0]); + return; + } + self.cur.cull_face_mode = a[0]; + } + op::FRONT_FACE => { + if a[0] != GL_CW && a[0] != GL_CCW { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glFrontFace({:#x}) ignored", a[0]); + return; + } + self.cur.front_face = a[0]; + } + op::DRAW_BUFFERS => self.cur.draw_buffers = a[0], + op::SCISSOR => { + // A negative width or height is an error and changes nothing. + if (a[2] as i32) < 0 || (a[3] as i32) < 0 { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glScissor of {} by {} ignored", a[2] as i32, a[3] as i32); + return; + } + for i in 0..4 { + self.cur.scissor[i] = a[i] as i32; + } + } + op::ALPHA_FUNC => { + let r = gl_float(a[1]); + + if a[0].wrapping_sub(GL_NEVER_ENUM) > GL_ALWAYS { + crate::dlog!(LogModule::Odyssey, "sgi-gl: alpha function {:#x} ignored", a[0]); + return; + } + self.cur.alpha_func = a[0] - GL_NEVER_ENUM; + // Clamped to [0, 1] (4.1.4); not a number clamps to 0. + self.cur.alpha_ref = if r >= 1.0 { + 1.0 + } else if r > 0.0 { + r + } else { + 0.0 + }; + } + op::STENCIL_FUNC => { + if a[0].wrapping_sub(GL_NEVER_ENUM) > GL_ALWAYS { + crate::dlog!(LogModule::Odyssey, "sgi-gl: stencil function {:#x} ignored", a[0]); + return; + } + self.cur.stencil_func = a[0] - GL_NEVER_ENUM; + self.cur.stencil_ref = a[1] as i32; + self.cur.stencil_value_mask = a[2]; + } + op::STENCIL_OP => { + for i in 0..3 { + if !gl_stencil_op_valid(a[i]) { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glStencilOp({:#x}, {:#x}, {:#x}) ignored", a[0], a[1], a[2]); + return; + } + } + self.cur.stencil_op.copy_from_slice(&a[..3]); + } + op::STENCIL_MASK => self.cur.stencil_write_mask = a[0], + op::BLEND_FUNC => { + if !gl_blend_factor_valid(a[0], false) || !gl_blend_factor_valid(a[1], true) { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glBlendFunc({:#x}, {:#x}) ignored", a[0], a[1]); + return; + } + self.cur.blend_src = a[0]; + self.cur.blend_dst = a[1]; + } + op::BLEND_EQUATION => { + if !gl_blend_equation_valid(a[0]) { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glBlendEquation({:#x}) ignored", a[0]); + return; + } + self.cur.blend_equation = a[0]; + } + op::BLEND_COLOR => { + for i in 0..4 { + let v = gl_float(a[i]); + + self.cur.blend_color[i] = if v >= 1.0 { + 1.0 + } else if v > 0.0 { + v + } else { + 0.0 + }; + } + } + op::LOGIC_OP => { + if a[0].wrapping_sub(GL_CLEAR) > GL_SET - GL_CLEAR { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glLogicOp({:#x}) ignored", a[0]); + return; + } + self.cur.logic_op_mode = a[0]; + } + op::POLYGON_OFFSET | op::POLYGON_OFFSET_EXT => { + self.cur.offset_factor = gl_float(a[0]); + self.cur.offset_units = + if cmd.op as u32 == op::POLYGON_OFFSET { gl_float(a[1]) } else { 0.0 }; + self.cur.offset_bias = + if cmd.op as u32 == op::POLYGON_OFFSET_EXT { gl_float(a[1]) } else { 0.0 }; + } + op::CLIP_PLANE => { + pos = gl_words4(a, 1); + self.clip_plane(a[0], &pos); + } + op::POINT_SIZE | op::LINE_WIDTH | op::LINE_STIPPLE | op::POLYGON_MODE => { + self.raster_state(cmd.op, a); + } + op::COLOR3F => { + for i in 0..3 { + self.cur_color[i] = gl_float(a[i]); + } + self.cur_color[3] = 1.0; + } + op::COLOR4F => { + for i in 0..4 { + self.cur_color[i] = gl_float(a[i]); + } + } + op::COLOR3UB | op::COLOR4UB => { + // A byte c is c / 255 (OpenGL 1.2.1, table 2.6); 3ub's alpha is 1. + for i in 0..4usize { + self.cur_color[i] = + if i == a.len() { 1.0 } else { (a[i] & 0xff) as f32 / 255.0 }; + } + } + op::BEGIN => { + if self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: nested glBegin ignored"); + return; + } + if a[0] > GL_POLYGON { + crate::dlog!(LogModule::Odyssey, "sgi-gl: glBegin of mode {} ignored", a[0]); + return; + } + self.line_phase = 0; + self.in_begin = true; + self.begin_mode = a[0]; + self.nheld = 0; + self.strip_odd = false; + self.strip_prev = false; + self.strip_swap = false; + } + op::END => { + if !self.in_begin && self.polygon.enabled { + return; + } + if self.polygon.enabled && self.begin_mode == GL_POLYGON { + if !self.polygon.failed && self.polygon.count >= 3 { + let checked = self.draws_checked(); + let mut d = GlDraw::new( + GL_POLYGON, + self.polygon.count, + &self.polygon.vertices, + &self.cur, + &self.polygon_stipple, + ); + + d.context = self.hw_id; + d.point_size = self.point_size; + d.line_width = self.line_width; + d.point_smooth = self.raster_enables & GL_RASTER_POINT_SMOOTH != 0; + d.line_smooth = self.raster_enables & GL_RASTER_LINE_SMOOTH != 0; + d.polygon_smooth = self.raster_enables & GL_RASTER_POLYGON_SMOOTH != 0; + d.polygon_modes = Some(self.polygon_mode); + d.fill_by_triangle = true; + d.polygon_stipple_enabled = self.raster_enables & GL_RASTER_POLYGON_STIPPLE != 0; + d.line_stipple_enabled = self.raster_enables & GL_RASTER_LINE_STIPPLE != 0; + d.line_stipple_repeat = self.line_stipple_repeat; + d.line_stipple_pattern = self.line_stipple_pattern; + + let result = sink.draw(&mut d); + self.draw_result(checked, result); + } + self.polygon.polygon_release(); + } + // A line loop closes from its last vertex to its first, in the + // first vertex's colour when flat (2.13.7). + if self.in_begin && self.begin_mode == GL_LINE_LOOP && self.nheld == 2 { + let h1 = self.held[1]; + let h0 = self.held[0]; + self.submit(sink, GL_LINES, 2, Some(&h1), Some(&h0), None, None); + } + self.line_phase = 0; + self.in_begin = false; + self.begin_mode = 0; + self.nheld = 0; + self.strip_odd = false; + self.strip_prev = false; + self.strip_swap = false; + } + op::VERTEX2F | op::VERTEX3F | op::VERTEX4F => { + for i in 0..a.len() { + pos[i] = gl_float(a[i]); + } + self.vertex(sink, &pos, a.len() as u32); + } + op::VERTEX2I => { + // Integer components convert to float unscaled (2.7). + pos[0] = (a[0] as i32) as f32; + pos[1] = (a[1] as i32) as f32; + self.vertex(sink, &pos, 2); + } + _ => { + // Unreachable: every op the table lists is handled above. + } + } + } + +} + +impl GlContext { + /// glGet of GL_CURRENT_COLOR: the current colour as float bits (6.1.2). + pub fn current_color(&self) -> [u32; 4] { + self.cur_color.map(gl_bits) + } +} + +/// Whether a primitive of this mode can hold k vertices after count vertices +/// since glBegin. Only count mod 2, +/// 3 and 4 and k itself decide it, never count's absolute size, so a +/// count that is right only modulo 12 is enough: once a mode has all it +/// will ever hold, k alone says so. +pub(super) fn gl_resume_held_ok(mode: u32, k: u32, count: u32) -> bool { + match mode { + GL_POINTS => k == 0, + GL_LINES => k == count % 2, + GL_TRIANGLES => k == count % 3, + GL_QUADS => k == count % 4, + GL_QUAD_STRIP => { + if k >= 2 { + k == 2 + count % 2 + } else { + k == count + } + } + GL_LINE_STRIP => k <= 1 && (k == 1 || count == 0), + // Strips of triangles, fans, loops, polygons: first and last. + _ => k <= 2 && (k == 2 || k == count), + } +} + +impl GlContext { + /// Resume a primitive a context switch interrupted inside glBegin/glEnd: + /// begin mode and take k of the vertices the primitive held, oldest first, + /// without drawing. count is how many vertices had been sent since glBegin. + /// Refused, with a log, inside glBegin, for a (k, count, mode) that cannot + /// occur, and for glPolygon while polygon mode assembly is on. `v` holds k + /// vertices; None when k is 0. + pub fn resume_primitive(&mut self, mode: u32, v: Option<&[[f32; vtx::FLOATS]]>, k: u32, count: u32) -> bool { + if self.in_begin { + crate::dlog!(LogModule::Odyssey, "sgi-gl: resume inside glBegin ignored"); + return false; + } + if mode > GL_POLYGON + || k > GL_HELD_VERTICES + || (k != 0 && v.is_none_or(|s| s.len() < k as usize)) + || !gl_resume_held_ok(mode, k, count) + || (self.polygon.enabled && mode == GL_POLYGON) + { + crate::dlog!(LogModule::Odyssey, "sgi-gl: resume of mode {} with {} held of {} sent refused", mode, k, count); + return false; + } + // line_phase is deliberately left alone, unlike glBegin. + self.in_begin = true; + self.begin_mode = mode; + self.nheld = k; + for i in 0..k { + self.hold(i, &v.unwrap()[i as usize]); + } + // The strip's next triangle faces as its count's parity says. + self.strip_odd = mode == GL_TRIANGLE_STRIP && k == 2 && count & 1 != 0; + // The oldest retained vertex of a resumed strip is not sent. + self.strip_prev = false; + self.strip_swap = false; + true + } + + /// Close an interrupted primitive without drawing a line-loop edge. + pub fn discard_primitive(&mut self) { + if self.polygon.enabled && self.begin_mode == GL_POLYGON { + self.polygon.polygon_release(); + } + self.in_begin = false; + self.begin_mode = 0; + self.nheld = 0; + self.strip_odd = false; + self.strip_prev = false; + self.strip_swap = false; + } + +} + +/// The faces a glMaterial, glColorMaterial or glCullFace face names, as a +/// mask of material[] entries, or 0 for a value that names none. +pub(super) fn gl_faces(face: u32) -> u32 { + match face { + GL_FRONT => 1, + GL_BACK => 2, + GL_FRONT_AND_BACK => 3, + _ => 0, + } +} diff --git a/src/dev/odyssey/golden/mod.rs b/src/dev/odyssey/golden/mod.rs new file mode 100644 index 000000000..5357181e1 --- /dev/null +++ b/src/dev/odyssey/golden/mod.rs @@ -0,0 +1,484 @@ +//! Draw-level golden frames for the VPro rasteriser and its vertex side. +//! +//! Each scene drives `Board::gl_draw()` the way the GL context does (and the +//! fill-rate benchmark's scenes do): vertices and a `GlDrawState` into a v12 +//! board's own planes, the board's texture, fog and depth registers set the +//! way the command stream sets them. No guest runs. A scene's colour, depth +//! and stencil planes over its 128 x 128 window are hashed, and the hash is +//! kept in `testdata/golden/scenes.tsv`; the planes themselves are kept as +//! PNGs in `testdata/golden/frames/`, so a deliberate change can be judged +//! by how far the frame moved, not only that it did. +//! +//! The default check is bit-exact. `VPRO_GOLDEN=tolerance` lets a scene whose +//! hash changed pass while it stays inside the limits `scenes.tsv` keeps for +//! it, measured against the reference images: for each plane, the number of +//! pixels that differ by more than the scene's per-channel delta. That is the +//! check for a change that moves numerics on purpose (the vertex side moving +//! to 32-bit floats); a change that must not move a pixel uses the default. +//! +//! `scripts/vpro-goldens.sh` wraps the commands, and +//! `src/dev/odyssey/testdata/README.md` says how to re-take the goldens. + +use super::raster_bench; +use super::board::Board; +use super::draw::gl_rules; +pub mod replay; +pub mod scenes; +pub mod vertices; + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + + +/// A scene's window: the frame is the square from (0, 0) to SIDE - 1. +pub const SIDE: i32 = 128; + +pub fn data_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("src/dev/odyssey/testdata/golden") +} + +/// The planes of a finished scene. +#[derive(Clone, PartialEq, Eq)] +pub struct Frame { + pub colour: Vec, + pub depth: Vec, + pub stencil: Vec, +} + +impl Frame { + /// The planes of `b` over the scene window. + pub fn of(b: &Board) -> Frame { + let depth = b.planes.depth.plane(b.planes.depth_plane).expect("the depth plane"); + let stencil = b.planes.stencil.plane(b.planes.stencil_plane).expect("the stencil plane"); + let mut f = Frame { colour: vec![], depth: vec![], stencil: vec![] }; + for y in 0..SIDE { + for x in 0..SIDE { + f.colour.push(b.buffer_load(0, x as i64, y as i64)); + f.depth.push(b.planes.depth.plane_load(&depth, x, y)); + f.stencil.push(b.planes.stencil.plane_load(&stencil, x, y)); + } + } + f + } + + /// blake3 over the three planes, big-endian words, in a fixed order. + pub fn hash(&self) -> String { + let mut h = blake3::Hasher::new(); + for plane in [&self.colour, &self.depth, &self.stencil] { + for w in plane.iter() { + h.update(&w.to_be_bytes()); + } + } + h.finalize().to_hex().to_string() + } + + /// Pixels that differ from `other` in any plane. + pub fn differing(&self, other: &Frame) -> usize { + (0..self.colour.len()) + .filter(|&i| self.colour[i] != other.colour[i] || self.depth[i] != other.depth[i] || self.stencil[i] != other.stencil[i]) + .count() + } +} + +/// How far a frame is from a reference, plane by plane. A pixel is "bad" in +/// a plane when it differs by more than the scene's delta there. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Distance { + pub colour_max: u32, + pub colour_differ: usize, + pub colour_bad: usize, + pub depth_max: u32, + pub depth_differ: usize, + pub depth_bad: usize, + pub stencil_differ: usize, +} + +/// A scene's limits in tolerance mode. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Tolerance { + /// Colour: a channel difference above this makes a pixel bad. + pub colour_delta: u32, + pub colour_bad: usize, + /// Depth: a 24-bit word difference above this makes a pixel bad. + pub depth_delta: u32, + pub depth_bad: usize, + /// Stencil: pixels that differ at all. + pub stencil_bad: usize, +} + +impl Default for Tolerance { + /// What blessing gives a new scene: a few least-significant steps of + /// colour and a sliver of edge pixels may move. + fn default() -> Self { + Tolerance { colour_delta: 2, colour_bad: 64, depth_delta: 0x40, depth_bad: 64, stencil_bad: 64 } + } +} + +impl Distance { + pub fn within(&self, t: &Tolerance) -> bool { + self.colour_bad <= t.colour_bad && self.depth_bad <= t.depth_bad && self.stencil_differ <= t.stencil_bad + } +} + +fn channel_max(a: u32, b: u32) -> u32 { + (0..4).map(|k| ((a >> (8 * k)) & 0xff).abs_diff((b >> (8 * k)) & 0xff)).max().unwrap() +} + +pub fn distance(f: &Frame, r: &Frame, t: &Tolerance) -> Distance { + let mut d = Distance::default(); + for i in 0..f.colour.len() { + let c = channel_max(f.colour[i], r.colour[i]); + d.colour_max = d.colour_max.max(c); + d.colour_differ += (c != 0) as usize; + d.colour_bad += (c > t.colour_delta) as usize; + let z = (f.depth[i] & 0xff_ffff).abs_diff(r.depth[i] & 0xff_ffff); + d.depth_max = d.depth_max.max(z); + d.depth_differ += (z != 0) as usize; + d.depth_bad += (z > t.depth_delta) as usize; + d.stencil_differ += (f.stencil[i] != r.stencil[i]) as usize; + } + d +} + +// The reference images: colour as RGBA, depth as RGB (the 24-bit word's +// bytes), stencil as grey. + +fn write_png(path: &Path, w: u32, h: u32, colour: png::ColorType, data: &[u8]) { + let file = std::fs::File::create(path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w, h); + enc.set_color(colour); + enc.set_depth(png::BitDepth::Eight); + enc.set_compression(png::Compression::High); + enc.write_header().unwrap().write_image_data(data).unwrap(); +} + +fn read_png(path: &Path) -> Option> { + let file = std::fs::File::open(path).ok()?; + let mut r = png::Decoder::new(std::io::BufReader::new(file)).read_info().ok()?; + let mut buf = vec![0; r.output_buffer_size()?]; + let info = r.next_frame(&mut buf).ok()?; + buf.truncate(info.buffer_size()); + Some(buf) +} + +pub fn save_reference(name: &str, f: &Frame) { + let dir = data_dir().join("frames"); + std::fs::create_dir_all(&dir).unwrap(); + let n = SIDE as u32; + let rgba: Vec = f.colour.iter().flat_map(|w| w.to_be_bytes()).collect(); + write_png(&dir.join(format!("{name}.colour.png")), n, n, png::ColorType::Rgba, &rgba); + let depth: Vec = f.depth.iter().flat_map(|w| [(w >> 16) as u8, (w >> 8) as u8, *w as u8]).collect(); + write_png(&dir.join(format!("{name}.depth.png")), n, n, png::ColorType::Rgb, &depth); + let stencil: Vec = f.stencil.iter().map(|&w| w as u8).collect(); + write_png(&dir.join(format!("{name}.stencil.png")), n, n, png::ColorType::Grayscale, &stencil); +} + +pub fn load_reference(name: &str) -> Option { + let dir = data_dir().join("frames"); + let rgba = read_png(&dir.join(format!("{name}.colour.png")))?; + let depth = read_png(&dir.join(format!("{name}.depth.png")))?; + let stencil = read_png(&dir.join(format!("{name}.stencil.png")))?; + Some(Frame { + colour: rgba.chunks_exact(4).map(|c| u32::from_be_bytes(c.try_into().unwrap())).collect(), + depth: depth.chunks_exact(3).map(|c| (c[0] as u32) << 16 | (c[1] as u32) << 8 | c[2] as u32).collect(), + stencil: stencil.iter().map(|&b| b as u32).collect(), + }) +} + +// scenes.tsv: name, hash, then the tolerance. `#` starts a comment. + +pub struct Entry { + pub hash: String, + pub tol: Tolerance, +} + +pub fn load_entries() -> BTreeMap { + let text = std::fs::read_to_string(data_dir().join("scenes.tsv")).unwrap_or_default(); + let mut out = BTreeMap::new(); + for line in text.lines() { + let line = line.split('#').next().unwrap().trim(); + if line.is_empty() { + continue; + } + let f: Vec<&str> = line.split_whitespace().collect(); + assert_eq!(f.len(), 7, "scenes.tsv: {line:?}"); + let n = |i: usize| f[i].parse::().unwrap_or_else(|_| panic!("scenes.tsv: {line:?}")); + out.insert( + f[0].to_string(), + Entry { + hash: f[1].to_string(), + tol: Tolerance { colour_delta: n(2) as u32, colour_bad: n(3) as usize, depth_delta: n(4) as u32, depth_bad: n(5) as usize, stencil_bad: n(6) as usize }, + }, + ); + } + out +} + +pub fn save_entries(e: &BTreeMap) { + let mut s = String::from( + "# VPro draw-level goldens (src/dev/odyssey/golden). One scene a line:\n\ + # name hash(blake3 of colour, depth, stencil) colour_delta colour_bad depth_delta depth_bad stencil_bad\n\ + # The hash is the default, bit-exact check. The five numbers are the limits for\n\ + # VPRO_GOLDEN=tolerance: pixels past colour_delta (per channel) or depth_delta (24-bit\n\ + # word) may number at most colour_bad / depth_bad, and at most stencil_bad stencil pixels\n\ + # may differ. Edit them by hand per scene; blessing keeps what is there.\n", + ); + for (k, v) in e { + let t = v.tol; + s += &format!("{k}\t{}\t{}\t{}\t{}\t{}\t{}\n", v.hash, t.colour_delta, t.colour_bad, t.depth_delta, t.depth_bad, t.stencil_bad); + } + std::fs::write(data_dir().join("scenes.tsv"), s).unwrap(); +} + +/// Run a scene on a fresh board and return its frame and how many pixels +/// it changed from the cleared window. +pub fn render_counted(name: &str) -> (Frame, usize) { + let mut c = scenes::Ctx::new(); + assert!(scenes::run_scene(name, &mut c), "no scene {name}"); + let f = Frame::of(&c.board); + let n = f.differing(&c.blank()); + (f, n) +} + +/// A scene that draws nothing would pin nothing. +const MIN_DRAWN: usize = 200; + +pub fn render(name: &str) -> Frame { + let (f, n) = render_counted(name); + assert!(n >= MIN_DRAWN, "scene {name} drew almost nothing ({n} pixels)"); + f +} + +/// The check one scene's test makes. +pub fn check(name: &str) { + let f = render(name); + let entries = load_entries(); + let Some(e) = entries.get(name) else { + panic!("scene {name} has no golden: run scripts/vpro-goldens.sh bless"); + }; + let hash = f.hash(); + if hash == e.hash { + return; + } + let msg = match load_reference(name) { + Some(r) => { + let d = distance(&f, &r, &e.tol); + let within = d.within(&e.tol); + if within && std::env::var("VPRO_GOLDEN").is_ok_and(|v| v == "tolerance") { + eprintln!("{name}: hash moved, within tolerance: {d:?}"); + return; + } + format!("{d:?}; {} of the limits {:?}", if within { "inside" } else { "OUTSIDE" }, e.tol) + } + None => "no reference image".to_string(), + }; + if let Ok(dir) = std::env::var("VPRO_GOLDEN_OUT") { + // The failing frame's colour, for looking at; the references stay put. + std::fs::create_dir_all(&dir).unwrap(); + let p = Path::new(&dir).join(format!("{name}.colour.png")); + let rgba: Vec = f.colour.iter().flat_map(|w| w.to_be_bytes()).collect(); + write_png(&p, SIDE as u32, SIDE as u32, png::ColorType::Rgba, &rgba); + } + panic!("scene {name}: hash {hash} is not the golden {} ({msg})", e.hash); +} + +/// Re-take every scene's hash and reference images, keeping tolerances. +/// `VPRO_GOLDEN_BLESS=1 cargo test --release --lib odyssey::golden::bless -- --ignored` +#[test] +#[ignore] +fn bless() { + if std::env::var("VPRO_GOLDEN_BLESS").is_err() { + panic!("set VPRO_GOLDEN_BLESS=1: blessing rewrites the goldens"); + } + let mut entries = load_entries(); + let only = std::env::var("VPRO_GOLDEN_SCENE").ok(); + let mut blank = vec![]; + for name in scenes::NAMES { + if only.as_deref().is_some_and(|o| o != *name) { + continue; + } + let (f, n) = render_counted(name); + eprintln!("{name}: {n} pixels changed"); + if n < MIN_DRAWN { + blank.push(*name); + continue; + } + let tol = entries.get(*name).map(|e| e.tol).unwrap_or_default(); + entries.insert(name.to_string(), Entry { hash: f.hash(), tol }); + save_reference(name, &f); + } + assert!(blank.is_empty(), "scenes that drew almost nothing: {blank:?}"); + // Scenes dropped from the list leave no golden behind. + entries.retain(|k, _| scenes::NAMES.contains(&k.as_str())); + save_entries(&entries); + vertices::bless(); + eprintln!("blessed {} scenes", entries.len()); +} + +/// Every scene in the list has a hash and reference images, and no golden is +/// left for a scene that is gone. +#[test] +fn every_scene_has_a_golden_and_no_golden_is_orphaned() { + let entries = load_entries(); + for name in scenes::NAMES { + assert!(entries.contains_key(*name), "scene {name} has no line in scenes.tsv"); + assert!(load_reference(name).is_some(), "scene {name} has no reference images"); + } + for k in entries.keys() { + assert!(scenes::NAMES.contains(&k.as_str()), "scenes.tsv has {k}, which is no scene"); + } +} + +/// The comparison itself: a changed pixel is seen, and tolerance counts it. +#[test] +fn the_distance_counts_what_moved() { + let f = Frame { colour: vec![0x1020_3040; 4], depth: vec![100; 4], stencil: vec![0; 4] }; + let mut g = f.clone(); + g.colour[1] = 0x1020_3045; + g.depth[2] = 100 + 0x8000; + g.stencil[3] = 1; + let t = Tolerance { colour_delta: 2, colour_bad: 0, depth_delta: 0x4000, depth_bad: 0, stencil_bad: 0 }; + let d = distance(&g, &f, &t); + assert_eq!((d.colour_max, d.colour_differ, d.colour_bad), (5, 1, 1)); + assert_eq!((d.depth_max, d.depth_differ, d.depth_bad), (0x8000, 1, 1)); + assert_eq!(d.stencil_differ, 1); + assert!(!d.within(&t)); + assert!(distance(&f, &f, &t).within(&t)); + assert_ne!(f.hash(), g.hash()); +} + +fn render_without(name: &str, off: &'static str) -> Frame { + let mut c = scenes::Ctx::new(); + c.off = Some(off); + assert!(scenes::run_scene(name, &mut c)); + Frame::of(&c.board) +} + +/// The goldens are only worth keeping if a scene's frame depends on the +/// feature it is about: leave the feature out and the frame changes, by more +/// than the odd pixel. +#[test] +fn the_features_the_scenes_cover_change_their_frames() { + let pairs = [ + ("fog_ramp_triangles", "fog"), + ("fog_lines_and_points", "fog"), + ("fog_over_blend", "fog"), + ("tex_with_fog_blend_depth", "fog"), + ("tex_with_fog_blend_depth", "texture"), + ("blend_triangles_over", "blend"), + ("blend_factor_grid", "blend"), + ("polygon_smooth", "blend"), + ("lines_smooth", "blend"), + ("depth_less", "depth"), + ("depth_range_wide", "depth"), + ("tex_format_rgba", "texture"), + ("tex_env_blend_rgba", "texture"), + ("tex_mip_linear_linear", "texture"), + ("perspective_correct_colour", "texture"), + ("light_lit_blended_textured", "texture"), + ]; + for (scene, feature) in pairs { + let on = render(scene); + let off = render_without(scene, feature); + assert!(on.differing(&off) >= 50, "{scene} looks the same with {feature} left out ({} pixels differ)", on.differing(&off)); + } +} + +/// Scenes that vary one thing at a time produce different frames: a texture +/// format, environment, filter or light that drew like another would be a +/// scene that does not test what it names. +#[test] +fn scenes_of_one_family_are_told_apart() { + let families: [&[&str]; 5] = [ + &["tex_format_rgba", "tex_format_rgb", "tex_format_luminance", "tex_format_luminance_alpha", "tex_format_alpha", "tex_format_intensity"], + &["tex_env_replace_rgba", "tex_env_modulate_rgba", "tex_env_decal_rgba", "tex_env_decal_rgb", "tex_env_blend_rgba", "tex_env_blend_luminance", "tex_env_modulate_luminance_alpha", "tex_env_replace_alpha"], + &["tex_mip_nearest_nearest", "tex_mip_linear_nearest", "tex_mip_nearest_linear", "tex_mip_linear_linear", "tex_nearest_repeat", "tex_clamp_to_edge_linear"], + &["light_directional", "light_positional_attenuated", "light_spot", "light_local_viewer_specular", "light_three_lights_colour_material", "light_unlit_emission_only"], + &["depth_less", "depth_lequal_greater", "depth_always_masked", "depth_equal_notequal", "blend_equations", "blend_constant_colour"], + ]; + for family in families { + let hashes: Vec = family.iter().map(|n| render(n).hash()).collect(); + for i in 0..hashes.len() { + for j in 0..i { + assert_ne!(hashes[i], hashes[j], "{} and {} draw the same frame", family[i], family[j]); + } + } + } +} + +/// A clip vector scaled by a tiny positive w is the same point: the +/// mapping keeps VPro's w > 0 rule rather than the shared core's cut at +/// w <= 1e-6 (the board's rule for a small w is unknown). +#[test] +fn a_small_positive_w_maps_as_the_same_point() { + let st = scenes::Ctx::new().st; + let sub = (1i32 << gl_rules().subpixel_bits) as f32; + let at = |w: f32| st.clip_to_window(&[0.5 * w, -0.25 * w, 0.125 * w, w], sub); + let want = at(1.0).expect("a window position"); + + for w in [1e-6, 5e-7, 1e-7, 1e-12] { + assert_eq!(at(w), Some(want), "w {w}"); + } + assert_eq!(at(0.0), None); + assert_eq!(at(-1e-7), None); +} + +/// A triangle drawn at w = 1e-7 covers the pixels the same triangle does at +/// w = 1. +#[test] +fn a_triangle_at_a_small_w_draws_as_at_one() { + let draw = |w: f32| { + let mut c = scenes::Ctx::new(); + let g = [0.0, 1.0, 0.0, 1.0]; + // The same point, its whole clip vector scaled by w. + let at = |x: f32, y: f32| { + let mut v = scenes::v(x, y, 0.25, g); + + v.p = v.p.map(|c| c * w); + v + }; + + c.tri(at(10.0, 10.0), at(110.0, 20.0), at(40.0, 110.0)); + Frame::of(&c.board) + }; + let (one, small) = (draw(1.0), draw(1e-7)); + let blank = scenes::Ctx::new().blank(); + + assert!(one.colour.iter().zip(&blank.colour).filter(|(a, b)| a != b).count() > 1000, "the triangle draws"); + assert_eq!(small.colour, one.colour); +} + +/// amesh's block 16 is the clip w row itself and its depth row the same row +/// times 0.99999999. The depth row and divisor are now +/// taken on eye and w rounded to single precision, which moves the ratio by +/// about 6e-8: it must stay inside the 2^-20 slack, so a wall from the floor +/// to a peak still draws, and the depth it writes is the far plane's give or +/// take the steps that error is worth. +#[test] +fn the_depth_row_on_single_precision_eye_stays_in_the_slack() { + let block16 = [-0.0f32, -6.84, -2.31, 65.93]; + let draw = |rows: bool| { + let mut c = scenes::Ctx::new(); + let g = [0.0, 1.0, 0.0, 1.0]; + + if rows { + for k in 0..4 { + c.board.gl_block16[k] = block16[k].to_bits(); + } + // The board's divisor is block 16 over its own w. + c.depth_row(block16.map(|b| b as f64 * 0.99999999 / block16[3] as f64)); + } else { + c.depth_row([0.0, 0.0, 0.0, 1.0]); + } + c.clear([0.0; 4], 0, 0); + c.tri(scenes::v(10.0, 10.0, -0.9, g), scenes::v(110.0, 20.0, 0.9, g), scenes::v(40.0, 110.0, 0.3, g)); + Frame::of(&c.board) + }; + let (rows, far) = (draw(true), draw(false)); + + assert!(far.colour.iter().filter(|&&p| p != 0).count() > 1000, "the reference draws"); + assert_eq!(rows.colour, far.colour, "every pixel the far plane draws is drawn"); + let worst = rows.depth.iter().zip(&far.depth).map(|(a, b)| (a & 0xff_ffff).abs_diff(b & 0xff_ffff)).max().unwrap(); + eprintln!("worst depth steps from the far plane: {worst}"); + assert!(worst <= 2, "{worst} steps from the far plane"); +} diff --git a/src/dev/odyssey/golden/replay.rs b/src/dev/odyssey/golden/replay.rs new file mode 100644 index 000000000..822acc26a --- /dev/null +++ b/src/dev/odyssey/golden/replay.rs @@ -0,0 +1,424 @@ +//! Replay of recorded guest sessions (`record.rs`), headless. +//! +//! The recording's stores go through a fresh board's register interface in +//! order, its DMA reads are answered from the recording, and the board's +//! planes are hashed at checkpoints and compared with the hashes kept +//! beside the recording in `testdata/replay/.hashes`. The recordings +//! themselves stay out of git (see `testdata/README.md`); they are found in +//! `VPRO_REPLAY_DIR`, default `ignore/vpro-golden/` under the checkout. +//! +//! `VPRO_REPLAY=` replays one file, `VPRO_REPLAY_OUT=` +//! saves its final colour buffer, `VPRO_REPLAY_BLESS=1` with +//! `cargo test ... replay_bless -- --ignored` re-takes every recording's +//! checkpoints. + +use crate::dev::odyssey::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use crate::dev::odyssey::consts::{cmd, gl_cmd, image, reg, ODSY_BUZZ_1}; +use crate::dev::odyssey::memory::ClipRect; +use crate::dev::odyssey::pixel_dma::{read_guest, GuestMemory}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use parking_lot::Mutex; + +use super::super::record::{self, Rec}; +use crate::traits::{BusDevice, BusRead8, BUS_ERR, BUS_OK}; + +/// Records between checkpoints. +pub const EVERY: usize = 500_000; + +/// The planes a checkpoint hashes: both colour buffers, depth and stencil, +/// and the object memory in use. +pub fn plane_hash(b: &Board) -> String { + let mut h = blake3::Hasher::new(); + let (w, ht) = (ODSY_WIDTH as i32, ODSY_HEIGHT as i32); + let depth = b.planes.depth.plane(b.planes.depth_plane).expect("the depth plane"); + let stencil = b.planes.stencil.plane(b.planes.stencil_plane).expect("the stencil plane"); + let mut row = Vec::with_capacity(w as usize * 16); + for y in 0..ht { + row.clear(); + for x in 0..w { + row.extend_from_slice(&b.buffer_load(0, x as i64, y as i64).to_be_bytes()); + row.extend_from_slice(&b.buffer_load(1, x as i64, y as i64).to_be_bytes()); + row.extend_from_slice(&b.planes.depth.plane_load(&depth, x, y).to_be_bytes()); + row.extend_from_slice(&b.planes.stencil.plane_load(&stencil, x, y).to_be_bytes()); + } + h.update(&row); + } + let used = (b.objects.used as usize * 4).min(b.objects.mem.bytes.len()); + h.update(&b.objects.mem.bytes[..used]); + h.finalize().to_hex().to_string() +} + +/// Guest memory for replay: answers the board's DMA reads with the recorded +/// values in order, and notes any that do not match the recorded address. +pub struct ReplayMem { + reads: Vec, + next: Mutex, + pub mismatches: Mutex, +} + +impl ReplayMem { + pub fn new(recs: &[Rec]) -> Self { + let reads = recs.iter().filter(|r| matches!(r, Rec::DmaBlock { .. } | Rec::DmaByte { .. })).cloned().collect(); + ReplayMem { reads, next: Mutex::new(0), mismatches: Mutex::new(0) } + } + pub fn consumed(&self) -> (usize, usize) { + (*self.next.lock(), self.reads.len()) + } + fn take(&self) -> Option<&Rec> { + let mut n = self.next.lock(); + let r = self.reads.get(*n); + *n += r.is_some() as usize; + r + } +} + +impl BusDevice for ReplayMem { + fn read8(&self, addr: u64) -> BusRead8 { + match self.take() { + Some(Rec::DmaByte { addr: a, val }) if *a == addr => BusRead8::ok(*val), + _ => { + *self.mismatches.lock() += 1; + BusRead8::ok(0) + } + } + } + fn write8(&self, _addr: u64, _val: u8) -> u32 { + BUS_OK + } + fn read_block(&self, addr: u64, buf: &mut [u64]) -> u32 { + match self.take() { + Some(Rec::DmaBlock { addr: a, words }) if *a == addr && words.len() == buf.len() => { + buf.copy_from_slice(words); + BUS_OK + } + _ => { + *self.mismatches.lock() += 1; + buf.fill(0); + BUS_ERR + } + } + } + fn write_block(&self, _addr: u64, _buf: &[u64]) -> u32 { + BUS_OK + } +} + +/// What a replay saw. +#[derive(Debug, Default)] +pub struct Report { + pub records: usize, + /// The checkpoints reached: record index and plane hash. + pub hashes: Vec<(usize, String)>, + pub dma_used: usize, + pub dma_total: usize, + pub dma_mismatched: u64, + pub reads_differ: u64, +} + +/// Feed `recs` through a fresh board's registers, `setup` run on it first. +/// `board_out` receives the board when it is done. +pub fn run(recs: &[Rec], setup: impl FnOnce(&mut Board), every: usize) -> (Report, Box) { + let mut b = Board::new("v12", 11).expect("board"); + let mem = Arc::new(ReplayMem::new(recs)); + b.guest = Some(mem.clone()); + setup(&mut b); + let mut rep = Report { records: recs.len(), ..Default::default() }; + for (i, r) in recs.iter().enumerate() { + match r { + Rec::Write { size, off, val } => b.mmio_write(*off as u64, *val, *size as u32), + Rec::Read { size, off, val } => { + let mask = if *size >= 8 { u64::MAX } else { (1u64 << (8 * *size as u32)) - 1 }; + if b.mmio_read(*off as u64, *size as u32) & mask != val & mask { + rep.reads_differ += 1; + } + } + Rec::DmaBlock { .. } | Rec::DmaByte { .. } => {} + } + if every != 0 && (i + 1) % every == 0 { + rep.hashes.push((i + 1, plane_hash(&b))); + } + } + b.raster_wait(); + if every != 0 && recs.len() % every != 0 { + rep.hashes.push((recs.len(), plane_hash(&b))); + } + (rep.dma_used, rep.dma_total) = mem.consumed(); + rep.dma_mismatched = *mem.mismatches.lock(); + (rep, b) +} + +pub fn recordings_dir() -> PathBuf { + match std::env::var("VPRO_REPLAY_DIR") { + Ok(d) => PathBuf::from(d), + Err(_) => Path::new(env!("CARGO_MANIFEST_DIR")).join("ignore/vpro-golden"), + } +} + +fn hashes_path(rec: &Path) -> PathBuf { + let stem = rec.file_stem().unwrap().to_string_lossy().to_string(); + Path::new(env!("CARGO_MANIFEST_DIR")).join("src/dev/odyssey/testdata/replay").join(format!("{stem}.hashes")) +} + +fn load_hashes(rec: &Path) -> Result, String> { + let p = hashes_path(rec); + let text = std::fs::read_to_string(&p).map_err(|e| format!("{}: {e} (run scripts/vpro-goldens.sh replay-bless)", p.display()))?; + Ok(text + .lines() + .filter(|l| !l.starts_with('#') && !l.trim().is_empty()) + .map(|l| { + let (i, h) = l.split_once(' ').expect("index hash"); + (i.parse().expect("record index"), h.to_string()) + }) + .collect()) +} + +fn save_hashes(rec: &Path, rep: &Report) { + let p = hashes_path(rec); + std::fs::create_dir_all(p.parent().unwrap()).unwrap(); + let mut s = format!( + "# Checkpoints of {} (record index, blake3 of the planes), every {EVERY} records.\n# {} records, {} DMA reads.\n", + rec.file_name().unwrap().to_string_lossy(), + rep.records, + rep.dma_total + ); + for (i, h) in &rep.hashes { + s += &format!("{i} {h}\n"); + } + std::fs::write(p, s).unwrap(); +} + +/// Replay one recording and check its checkpoints; `png` saves the final +/// colour buffer. +pub fn replay_file(path: &Path, png: Option<&str>) -> Result { + let recs = record::load(&path.to_string_lossy()).map_err(|e| format!("{}: {e}", path.display()))?; + let want = load_hashes(path)?; + let t0 = std::time::Instant::now(); + let (rep, board) = run(&recs, |_| {}, EVERY); + eprintln!("{}: {} records in {:.1} s, DMA reads {}/{} ({} mismatched), {} bus loads differ", path.display(), rep.records, t0.elapsed().as_secs_f64(), rep.dma_used, rep.dma_total, rep.dma_mismatched, rep.reads_differ); + if let Some(p) = png { + save_screen(&board, p); + } + if rep.dma_mismatched != 0 || rep.dma_used != rep.dma_total { + return Err(format!("{}: the replay's DMA reads do not follow the recording ({}/{} used, {} mismatched)", path.display(), rep.dma_used, rep.dma_total, rep.dma_mismatched)); + } + let failed: Vec = want.iter().filter(|(i, h)| rep.hashes.iter().find(|(j, _)| j == i).is_none_or(|(_, g)| g != h)).map(|(i, _)| *i).collect(); + if want.is_empty() || !failed.is_empty() || want.len() != rep.hashes.len() { + return Err(format!("{}: {} of {} checkpoints failed, at records {:?}", path.display(), failed.len(), want.len(), &failed[..failed.len().min(8)])); + } + Ok(rep) +} + +fn save_screen(b: &Board, path: &str) { + let (w, h) = (ODSY_WIDTH as u32, ODSY_HEIGHT as u32); + let mut rgb = Vec::with_capacity(w as usize * h as usize * 3); + for y in 0..h { + for x in 0..w { + let p = b.buffer_load(0, x as i64, y as i64); + rgb.extend_from_slice(&[(p >> 24) as u8, (p >> 16) as u8, (p >> 8) as u8]); + } + } + let file = std::fs::File::create(path).unwrap(); + let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w, h); + enc.set_color(png::ColorType::Rgb); + enc.set_depth(png::BitDepth::Eight); + enc.write_header().unwrap().write_image_data(&rgb).unwrap(); +} + +fn recordings() -> Vec { + let mut v: Vec = std::fs::read_dir(recordings_dir()) + .map(|d| d.filter_map(|e| e.ok()).map(|e| e.path()).filter(|p| p.extension().is_some_and(|e| e == "rec")).collect()) + .unwrap_or_default(); + v.sort(); + v +} + +/// Every recording in the recordings directory replays to its checkpoints. +/// With no recordings the test says so and passes, since they are not in the +/// tree; `VPRO_REPLAY_REQUIRE=1` (the script's `replay`) makes that a failure. +#[test] +fn recorded_sessions_replay_to_their_checkpoints() { + let files = match std::env::var("VPRO_REPLAY") { + Ok(f) => vec![PathBuf::from(f)], + Err(_) => recordings(), + }; + if files.is_empty() { + let msg = format!("no recordings in {}: see testdata/README.md", recordings_dir().display()); + assert!(std::env::var("VPRO_REPLAY_REQUIRE").is_err(), "{msg}"); + eprintln!("SKIPPED: {msg}"); + return; + } + let png = std::env::var("VPRO_REPLAY_OUT").ok(); + let mut errs = vec![]; + for f in &files { + if let Err(e) = replay_file(f, png.as_deref()) { + errs.push(e); + } + } + assert!(errs.is_empty(), "{}", errs.join("\n")); +} + +/// Re-take every recording's checkpoints after a deliberate change. +#[test] +#[ignore] +fn replay_bless() { + assert!(std::env::var("VPRO_REPLAY_BLESS").is_ok(), "set VPRO_REPLAY_BLESS=1: blessing rewrites the checkpoints"); + let files = recordings(); + assert!(!files.is_empty(), "no recordings in {}", recordings_dir().display()); + for f in files { + let recs = record::load(&f.to_string_lossy()).unwrap(); + let (rep, _) = run(&recs, |_| {}, EVERY); + assert!(rep.dma_mismatched == 0 && rep.dma_used == rep.dma_total, "{}: the DMA reads do not follow the recording; the session cannot be replayed", f.display()); + save_hashes(&f, &rep); + eprintln!("{}: {} checkpoints", f.display(), rep.hashes.len()); + } +} + +// The replay machinery on a short session built in memory. + +fn window(b: &mut Board) { + b.win.clip_kept = 1; + b.win.clip_rect[0] = ClipRect { x0: 0, y0: 0, x1: ODSY_WIDTH as i32 - 1, y1: ODSY_HEIGHT as i32 - 1 }; +} + +/// A glDrawPixels of two pixels by pixel burst, as a store sequence: the same +/// words the library sends for a small image. +fn image_session(pixels: &[u32]) -> Vec { + let w = |off: u64, val: u32| Rec::Write { size: 4, off: off as u32, val: val as u64 }; + let mut v = vec![]; + let mut cmd = |word: u32| v.push(w(cmd::FIFO, word)); + cmd(cmd::REGISTER_TAG | reg::GL_BEGIN); + cmd(gl_cmd::PRIMITIVE_IMAGE); + cmd(cmd::IMAGE_BOX); + for i in 0..10 { + let val = match i { + image::BOX_X1_INDEX => 40, + image::BOX_Y1_INDEX => 10, + image::BOX_X2_INDEX => 40 + pixels.len() as u32, + image::BOX_Y2_INDEX => 11, + _ => 0, + }; + cmd((val as f32).to_bits()); + } + cmd(cmd::IMAGE_HEADER); + cmd(0x0021_104b); + assert_eq!(pixels.len(), 2, "the header is a 2 x 1 image's"); + cmd(cmd::REGISTER_TAG | (image::BURST_BASE | (pixels.len() as u32) << cmd::ARITY_SHIFT | image::FMT_RGB)); + for &p in pixels { + cmd(p); + } + cmd(cmd::REGISTER_TAG | reg::GL_END); + // A load between the stores, as a polling driver makes. + v.push(Rec::Read { size: 4, off: ODSY_BUZZ_1 as u32, val: 0 }); + v +} + +/// A session replayed gives the planes the same stores gave a board +/// directly, and a changed store gives other planes. +#[test] +fn a_replayed_session_reaches_the_planes_the_stores_do() { + let session = image_session(&[0x116c_c722, 0x2233_4400]); + let (rep, b) = run(&session, window, 10); + assert!(rep.dma_total == 0 && rep.dma_mismatched == 0); + assert_eq!(b.buffer_load(0, 40, 10), 0x116c_c722); + assert_eq!(b.buffer_load(0, 41, 10), 0x2233_4400); + + // The same stores, applied to a board directly. + let mut direct = Board::new("v12", 11).unwrap(); + window(&mut direct); + for r in &session { + if let Rec::Write { size, off, val } = r { + direct.mmio_write(*off as u64, *val, *size as u32); + } + } + assert_eq!(plane_hash(&direct), rep.hashes.last().unwrap().1); + + let mut changed = session.clone(); + for r in changed.iter_mut() { + if let Rec::Write { val, .. } = r { + if *val == 0x2233_4400 { + *val = 0x2233_4401; + } + } + } + let (rep2, _) = run(&changed, window, 10); + assert_ne!(rep2.hashes.last().unwrap().1, rep.hashes.last().unwrap().1); +} + +/// A recording written and read back is the same records, and the DMA the +/// replay serves comes back in order. +#[test] +fn a_recording_reads_back_as_written() { + let dir = std::env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| std::env::temp_dir()); + let path = dir.join(format!("vpro_rec_test_{}.rec", std::process::id())); + let p = path.to_string_lossy().to_string(); + let want = vec![ + Rec::Write { size: 4, off: 0x1000, val: 0xdead_beef }, + Rec::Write { size: 8, off: 0x2000, val: 0x0123_4567_89ab_cdef }, + Rec::Read { size: 4, off: 0x106c, val: 0xa100_0000 }, + Rec::DmaBlock { addr: 0x2000_0000, words: vec![1, 2, 3] }, + Rec::DmaByte { addr: 0x2000_0003, val: 0x7f }, + Rec::Write { size: 4, off: 0x1004, val: 9 }, + ]; + { + let h = record::Recorder::create(&p).unwrap(); + let mut r = h.lock(); + for x in &want { + match x { + Rec::Write { size, off, val } => r.write(*size, *off, *val), + Rec::Read { size, off, val } => { + r.read(*size, *off, *val); + // An identical load straight after is not kept. + r.read(*size, *off, *val); + } + _ => {} + } + } + r.flush(); + } + let got = record::load(&p).unwrap(); + let _ = std::fs::remove_file(&path); + let kept: Vec = want.iter().filter(|r| matches!(r, Rec::Write { .. } | Rec::Read { .. })).cloned().collect(); + assert_eq!(got, kept); +} + +/// The board's DMA reads, recorded through `RecordingMem` and served back by +/// `ReplayMem`, give the same bytes in the same order; a read at another +/// address is counted as a mismatch. +#[test] +fn recorded_dma_reads_are_served_back_in_order() { + use super::super::tests::Ram; + + let ram = Ram::new(1 << 16); + let bytes: Vec = (0..64u8).collect(); + ram.put(0x100, &bytes); + let dir = std::env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| std::env::temp_dir()); + let path = dir.join(format!("vpro_dma_test_{}.rec", std::process::id())); + let p = path.to_string_lossy().to_string(); + let mut live = [vec![0u8; 21], vec![0u8; 8], vec![0u8; 16]]; + { + let h = record::Recorder::create(&p).unwrap(); + let mem: GuestMemory = Some(Arc::new(record::RecordingMem { inner: ram.clone(), rec: h.clone() })); + // Ragged at both ends, then a whole qword, then two. + assert!(read_guest(&mem, 0x103, &mut live[0])); + assert!(read_guest(&mem, 0x120, &mut live[1])); + assert!(read_guest(&mem, 0x128, &mut live[2])); + h.lock().flush(); + } + let recs = record::load(&p).unwrap(); + let _ = std::fs::remove_file(&path); + let served: GuestMemory = Some(Arc::new(ReplayMem::new(&recs))); + let mut again = [vec![0u8; 21], vec![0u8; 8], vec![0u8; 16]]; + assert!(read_guest(&served, 0x103, &mut again[0])); + assert!(read_guest(&served, 0x120, &mut again[1])); + assert!(read_guest(&served, 0x128, &mut again[2])); + assert_eq!(live, again); + assert_eq!(&live[0][..4], &[3, 4, 5, 6]); + + let m = Arc::new(ReplayMem::new(&recs)); + let wrong: GuestMemory = Some(m.clone()); + let mut buf = [0u8; 21]; + read_guest(&wrong, 0x104, &mut buf); + assert_ne!(*m.mismatches.lock(), 0); +} diff --git a/src/dev/odyssey/golden/scenes.rs b/src/dev/odyssey/golden/scenes.rs new file mode 100644 index 000000000..2459abcca --- /dev/null +++ b/src/dev/odyssey/golden/scenes.rs @@ -0,0 +1,1306 @@ +//! The scenes, and the context that draws them. + +use crate::dev::odyssey::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use crate::dev::odyssey::consts::{colour, gl_cmd, light, tex, write}; +use crate::dev::odyssey::draw::gl_rules; +use crate::dev::odyssey::gl_context::{ + vtx, GL_ALWAYS, GL_BACK, GL_BLEND, GL_CCW, GL_CLEAR, GL_COLOR_BUFFER_BIT, GL_COLOUR_MASK_ALL, + GL_COLOUR_MASK_ALPHA, GL_COLOUR_MASK_RED, GL_CONSTANT_ALPHA, GL_CONSTANT_COLOR, GL_CW, + GL_DECAL, GL_DECR, GL_DEPTH_BUFFER_BIT, GL_DRAW_OK, GL_DST_ALPHA, GL_DST_COLOR, GL_EQUAL, + GL_FILL, GL_FLAT, GL_FRONT, GL_FUNC_ADD, GL_FUNC_REVERSE_SUBTRACT, GL_FUNC_SUBTRACT, GL_GEQUAL, + GL_GREATER, GL_INCR, GL_INVERT, GL_KEEP, GL_LEQUAL, GL_LESS, GL_LINE, GL_LINES, GL_MAX, GL_MIN, + GL_MODULATE, GL_NOTEQUAL, GL_OFFSET_FILL, GL_ONE_MINUS_CONSTANT_ALPHA, + GL_ONE_MINUS_CONSTANT_COLOR, GL_ONE_MINUS_DST_ALPHA, GL_ONE_MINUS_DST_COLOR, + GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_SRC_COLOR, GL_POINT, GL_POINTS, GL_POLYGON, GL_QUADS, + GL_REPLACE, GL_SMOOTH, GL_SRC_ALPHA, GL_SRC_ALPHA_SATURATE, GL_SRC_COLOR, + GL_STENCIL_BUFFER_BIT, GL_TRIANGLES, GlDraw, GlDrawState, RASTER_CLEAR, RasterOp, +}; +use crate::dev::odyssey::golden::{Frame, SIDE}; +use crate::dev::odyssey::memory::ClipRect; +use crate::dev::odyssey::raster_bench::Bench; +use super::raster_bench::Lcg; + +const W: f32 = SIDE as f32; + +pub const RED: [f32; 4] = [1.0, 0.1, 0.1, 1.0]; +pub const GREEN: [f32; 4] = [0.1, 0.9, 0.2, 1.0]; +pub const BLUE: [f32; 4] = [0.1, 0.2, 1.0, 1.0]; +pub const YELLOW: [f32; 4] = [1.0, 0.9, 0.1, 1.0]; +pub const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + +/// What every scene starts from. +const CLEAR: [f32; 4] = [0.15, 0.2, 0.3, 1.0]; + +/// A vertex: window position as a normalized device position, colour, +/// texture coordinates and normal. +#[derive(Clone, Copy)] +pub struct V { + pub p: [f32; 4], + pub c: [f32; 4], + pub t: [f32; 4], + pub n: [f32; 3], + pub back: Option<[f32; 4]>, +} + +fn nd(px: f32) -> f32 { + px / W * 2.0 - 1.0 +} + +/// A vertex at window pixel (x, y) and depth z in [-1, 1]. +pub fn v(x: f32, y: f32, z: f32, c: [f32; 4]) -> V { + V { p: [nd(x), nd(y), z, 1.0], c, t: [0.0, 0.0, 0.0, 1.0], n: [0.0, 0.0, 1.0], back: None } +} + +impl V { + pub fn tex(mut self, s: f32, t: f32) -> V { + self.t = [s, t, 0.0, 1.0]; + self + } + /// The vertex as a perspective division by w would leave it: the same + /// window position, and window depth d. + pub fn persp(mut self, w: f32, d: f32) -> V { + self.p = [self.p[0] * w, self.p[1] * w, 2.0 * d - w, w]; + self.t = [self.t[0] * w, self.t[1] * w, 0.0, w]; + self + } + fn floats(&self) -> [f32; vtx::FLOATS] { + let mut o = [0.0f32; vtx::FLOATS]; + o[vtx::POSITION..][..4].copy_from_slice(&self.p); + o[vtx::COLOR..][..4].copy_from_slice(&self.c); + o[vtx::NORMAL..][..3].copy_from_slice(&self.n); + o[vtx::TEXCOORD..][..4].copy_from_slice(&self.t); + o[vtx::EDGE] = f32::from_bits(1); + o[vtx::BACK_COLOR..][..4].copy_from_slice(&self.back.unwrap_or(self.c)); + o + } +} + +/// A board set up to draw into the scene window, the draw state, and the +/// per-primitive flags the GL context would add to a draw. +pub struct Ctx { + pub board: Box, + pub st: GlDrawState, + pub stipple: [u32; 32], + pub polygon_stipple_enabled: bool, + pub polygon_modes: Option<[u32; 2]>, + pub point_size: f32, + pub line_width: f32, + pub point_smooth: bool, + pub line_smooth: bool, + pub polygon_smooth: bool, + pub line_stipple: Option<(u32, u32)>, + pub back_colors: bool, + phase: Option, + /// A feature to leave out of the scene ("fog", "blend", "depth", "texture"), + /// so a test can tell the feature changes the frame. + pub off: Option<&'static str>, +} + +impl Ctx { + /// A v12 board with the scene window cleared: colour, depth to the far + /// plane, stencil to 0. + pub fn new() -> Ctx { + let b = Bench::empty(); + let (mut board, mut st) = (b.board, b.st); + let rect = ClipRect { x0: 0, y0: 0, x1: SIDE - 1, y1: SIDE - 1 }; + + board.win.clip_rect[0] = rect; + board.gl_write_mask |= write::COLOUR_MASK << write::COLOUR_SHIFT; + st.drawable.clip.r[0] = rect; + st.viewport = [0, 0, SIDE, SIDE]; + st.shade_model = GL_SMOOTH; + let mut c = Ctx { + board, + st, + stipple: [0; 32], + polygon_stipple_enabled: false, + polygon_modes: None, + point_size: 1.0, + line_width: 1.0, + point_smooth: false, + line_smooth: false, + polygon_smooth: false, + line_stipple: None, + back_colors: false, + phase: Some(0), + off: None, + }; + c.depth_row([0.0, 0.0, 0.5, 0.5]); + c.clear(CLEAR, 0xff_ffff, 0); + c + } + + /// The window as `new` leaves it, for telling what a scene drew. + pub fn blank(&self) -> Frame { + Frame::of(&Ctx::new().board) + } + + /// The depth the board derives from its combined matrix: row . eye over + /// clip w, scaled the way Board::gl_operation_rules() unscales it. The + /// default row is the identity projection's, (z + w) / 2. + pub fn depth_row(&mut self, row: [f64; 4]) { + let scale = 2.0 * gl_cmd::DEPTH_S * self.board.gl_depth_scale(); + for k in 0..4 { + self.board.gl_combined[8 + k] = ((row[k] * scale) as f32).to_bits(); + } + } + + pub fn clear(&mut self, rgba: [f32; 4], depth: u32, stencil: u32) { + let t = self.board.gl_target(); + let mut op = RasterOp { + kind: RASTER_CLEAR, + ntargets: 1, + x1: BENCH_W_MAX, + y1: BENCH_H_MAX, + clear_buffers: GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, + clear_rgba: rgba, + clear_colour_mask: GL_COLOUR_MASK_ALL, + clear_depth24: depth, + clear_stencil: stencil, + stencil_mask: 0xff, + ..Default::default() + }; + op.clip.n = 1; + op.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: BENCH_W_MAX, y1: BENCH_H_MAX }; + gl_rules().raster(&t, &mut self.board.gl_parts(None).0, &op); + } + + /// Draw one primitive through the board's own GL draw. + pub fn draw(&mut self, mode: u32, vs: &[V]) { + let floats: Vec = vs.iter().flat_map(|v| v.floats()).collect(); + let mut d = GlDraw::new(mode, vs.len() as u32, &floats, &self.st, &self.stipple); + + d.back_colors = self.back_colors; + d.point_size = self.point_size; + d.line_width = self.line_width; + d.point_smooth = self.point_smooth; + d.line_smooth = self.line_smooth; + d.polygon_smooth = self.polygon_smooth; + d.polygon_stipple_enabled = self.polygon_stipple_enabled; + d.polygon_modes = self.polygon_modes; + if let Some((repeat, pattern)) = self.line_stipple { + d.line_stipple_enabled = true; + d.line_stipple_repeat = repeat; + d.line_stipple_pattern = pattern; + d.line_phase = self.phase; + } + assert_eq!(self.board.gl_draw(&mut d), GL_DRAW_OK); + if self.line_stipple.is_some() { + self.phase = d.line_phase; + } + } + + pub fn tri(&mut self, a: V, b: V, c: V) { + self.draw(GL_TRIANGLES, &[a, b, c]); + } + + /// A rectangle of two triangles' worth, as one GL_QUADS primitive. + pub fn rect(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, z: f32, c: [f32; 4]) { + self.draw(GL_QUADS, &[v(x0, y0, z, c), v(x1, y0, z, c), v(x1, y1, z, c), v(x0, y1, z, c)]); + } + + pub fn depth_test(&mut self, func: u32) { + if self.off == Some("depth") { + return; + } + self.st.depth_test = true; + self.st.depth_func = func; + } + + pub fn blend(&mut self, src: u32, dst: u32) { + if self.off == Some("blend") { + return; + } + self.st.blend = true; + self.st.blend_src = src; + self.st.blend_dst = dst; + } + + /// The board's fog curve (word 2 the scale, 3 the offset and shift, 4 and + /// 5 the curve's four control values), a ramp over the window depth, and + /// the fog colour. + pub fn fog(&mut self, colour: [u32; 3], steep: u32) { + if self.off == Some("fog") { + return; + } + let c12 = |f: f32| (f * 4095.0) as u32; + self.st.fog = true; + self.board.gl_fog = [ + c12(colour[0] as f32 / 255.0) << 12 | c12(colour[1] as f32 / 255.0), + c12(colour[2] as f32 / 255.0) << 12 | 0xfff, + 0x7c0000, + steep << 12, + 85, + 171 << 12 | 256, + ]; + } +} + +const BENCH_W_MAX: i32 = ODSY_WIDTH as i32 - 1; +const BENCH_H_MAX: i32 = ODSY_HEIGHT as i32 - 1; + +// Textures. Each level lives in an object of the board's memory, found from +// the sampler words the way the library's uploads leave them. + +#[derive(Clone, Copy)] +pub struct Tex { + /// The sampler word's component code: 0x01000000 luminance, 0x01800000 + /// luminance-alpha, 0x00800000 alpha, 0x02000000 intensity, 0x02800000 + /// RGB, 0x03000000 RGBA. + pub components: u32, + pub size: u32, + /// 0 nearest, 1 linear, 2 to 5 the mipmapped filters in GL's order. + pub min: u32, + pub mag_linear: bool, + /// 0 repeat, 1 clamp to edge. + pub wrap: u32, + /// 1 replace, 2 modulate, 3 decal, 4 blend. + pub env: u32, +} + +pub const LUMINANCE: u32 = 0x0100_0000; +pub const LUMINANCE_ALPHA: u32 = 0x0180_0000; +pub const ALPHA: u32 = 0x0080_0000; +pub const INTENSITY: u32 = 0x0200_0000; +pub const RGB: u32 = 0x0280_0000; +pub const RGBA: u32 = 0x0300_0000; + +impl Tex { + pub fn new(components: u32) -> Tex { + Tex { components, size: 16, min: 0, mag_linear: false, wrap: 0, env: 2 } + } +} + +/// Texel (x, y) of a level: a gradient and a check, tinted by level so a +/// wrong level shows. +fn texel(level: u32, x: u32, y: u32, w: u32, h: u32) -> u32 { + let r = x * 255 / w.max(2).saturating_sub(1).max(1); + let g = y * 255 / h.max(2).saturating_sub(1).max(1); + let b = if (x / 2 + y / 2) & 1 == 0 { 230 } else { 40 }; + let a = 255 - ((x + y) * 200 / (w + h)).min(200); + let tint = [0, 60, 120, 30, 90, 150, 20, 70][level as usize & 7]; + ((r.min(255) ^ tint) << 24) | (g.min(255) << 16) | (b << 8) | a +} + +/// Level descriptor word 1's size code and format index for a sampler +/// component code: alpha 8 (index 0), luminance 8 (4), luminance-alpha 8/8 +/// (11), intensity 8 (2), and 8/8/8/8 (17) for RGB and RGBA, which the class +/// alone tells apart. +fn texel_format_word(components: u32) -> u32 { + match components { + ALPHA => 0x0000_0000, + LUMINANCE => 0x0040_0000, + LUMINANCE_ALPHA => 0x00b0_0000, + INTENSITY => 0x0020_0000, + _ => 0x4110_0000, + } +} + +impl Ctx { + pub fn texture(&mut self, t: &Tex) { + if self.off == Some("texture") { + return; + } + let b = &mut self.board; + let levels = if t.min >= 2 { t.size.trailing_zeros() + 1 } else { 1 }; + let (ws, wt) = (t.wrap, t.wrap); + + for level in 0..levels { + let w = (t.size >> level).max(1); + let units = (w.div_ceil(16) + 1) & !1; + let base = 0x10_0000 + level * 0x1000; + let obj = b.object_select(base, units * 16); + assert!(b.object_rows(obj, w)); + for y in 0..w { + for x in 0..w { + b.object_store(obj, x, y, texel(level, x, y, w, w)); + } + } + b.tex.levels[level as usize * 3..][..3].copy_from_slice(&[base, texel_format_word(t.components) | units, 0]); + } + let axis = |w: u32, wrap: u32| w | if wrap == 1 { 0x4_0000 } else { 0 }; + let filter = (t.min << 5) | if t.mag_linear { 2 } else { 0 }; + b.tex.mode = tex::MODE_2D; + b.tex.level0 = 0; + b.tex.sampler = [ + 0x0800_0000 | (t.size.trailing_zeros() << 10) | (t.size.trailing_zeros() << 5), + axis(t.size, ws), + axis(t.size, wt), + 1, + 0x1000_0000 | t.components | (ws << 12) | (wt << 10) | filter, + 0, + 0, + ]; + b.tex.env = [t.env, 0, 0, 0, 0]; + if t.env == 4 { + // The environment colour: two normalized 12-bit components a word. + b.tex.env[1] = 0xc00 << 12 | 0x400; + b.tex.env[2] = 0x200 << 12 | 0xfff; + } + self.st.texture_2d = true; + self.st.texture_env_mode = [GL_REPLACE, GL_REPLACE, GL_MODULATE, GL_DECAL, GL_BLEND][t.env as usize]; + } +} + +// Scenes. Each draws into a fresh `Ctx`. + +fn tri_flat(c: &mut Ctx) { + c.st.shade_model = GL_FLAT; + c.tri(v(10.0, 10.0, 0.0, RED), v(70.0, 20.0, 0.0, GREEN), v(30.0, 80.0, 0.0, BLUE)); + c.tri(v(60.0, 60.0, 0.0, YELLOW), v(120.0, 70.0, 0.0, RED), v(90.0, 120.0, 0.0, GREEN)); + c.draw(GL_QUADS, &[v(5.0, 90.0, 0.0, BLUE), v(40.0, 95.0, 0.0, GREEN), v(45.0, 125.0, 0.0, RED), v(8.0, 120.0, 0.0, YELLOW)]); +} + +fn tri_smooth(c: &mut Ctx) { + c.tri(v(10.0, 10.0, 0.0, RED), v(110.0, 25.0, 0.0, GREEN), v(40.0, 115.0, 0.0, BLUE)); + c.tri(v(70.0, 70.0, 0.0, [1.0, 1.0, 1.0, 0.25]), v(125.0, 80.0, 0.0, [0.0, 0.0, 0.0, 1.0]), v(95.0, 125.0, 0.0, YELLOW)); +} + +fn tri_tiny_and_thin(c: &mut Ctx) { + // Sub-pixel and sliver triangles: coverage, tie rule and seam pixels. + let mut rng = Lcg(7); + for _ in 0..60 { + let (x, y) = (rng.range(4.0, 120.0), rng.range(4.0, 120.0)); + let s = rng.range(0.3, 3.0); + c.tri(v(x, y, 0.0, RED), v(x + s, y + 0.2, 0.0, GREEN), v(x + 0.3, y + s, 0.0, BLUE)); + } + for i in 0..8 { + let y = 10.0 + 14.0 * i as f32; + c.tri(v(5.0, y, 0.0, WHITE), v(120.0, y + 0.4 + 0.1 * i as f32, 0.0, YELLOW), v(5.0, y + 0.6, 0.0, WHITE)); + } +} + +fn quad_shared_edges(c: &mut Ctx) { + // A grid of quads that share edges: no pixel drawn twice, none missed. + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + let n = 6; + let step = 100.0 / n as f32; + for i in 0..n { + for j in 0..n { + let (x, y) = (12.3 + step * i as f32, 9.7 + step * j as f32); + let a = [0.5, 0.5, 0.5, 0.5]; + c.draw(GL_QUADS, &[v(x, y, 0.0, a), v(x + step, y, 0.0, a), v(x + step, y + step, 0.0, a), v(x, y + step, 0.0, a)]); + } + } +} + +fn polygon_convex(c: &mut Ctx) { + let pts = [(64.0, 8.0), (118.0, 40.0), (100.0, 110.0), (30.0, 118.0), (8.0, 50.0)]; + let cols = [RED, GREEN, BLUE, YELLOW, WHITE]; + let vs: Vec = pts.iter().zip(cols).map(|(&(x, y), col)| v(x, y, 0.0, col)).collect(); + c.draw(GL_POLYGON, &vs); +} + +fn polygon_modes_line_point(c: &mut Ctx) { + c.polygon_modes = Some([GL_LINE, GL_POINT]); + c.point_size = 3.0; + c.tri(v(10.0, 10.0, 0.0, RED), v(60.0, 15.0, 0.0, GREEN), v(30.0, 60.0, 0.0, BLUE)); + c.polygon_modes = Some([GL_FILL, GL_LINE]); + c.tri(v(70.0, 70.0, 0.0, RED), v(120.0, 120.0, 0.0, GREEN), v(70.0, 120.0, 0.0, BLUE)); + c.polygon_modes = Some([GL_POINT, GL_FILL]); + c.tri(v(70.0, 10.0, 0.0, YELLOW), v(120.0, 20.0, 0.0, YELLOW), v(90.0, 60.0, 0.0, YELLOW)); +} + +fn polygon_stipple(c: &mut Ctx) { + for (i, row) in c.stipple.iter_mut().enumerate() { + *row = if i & 1 == 0 { 0xaaaa_aaaa } else { 0x5555_5555 }; + } + c.stipple[8] = 0xffff_0000; + c.polygon_stipple_enabled = true; + c.tri(v(5.0, 5.0, 0.0, RED), v(123.0, 20.0, 0.0, GREEN), v(40.0, 120.0, 0.0, BLUE)); +} + +fn polygon_smooth(c: &mut Ctx) { + c.polygon_smooth = true; + c.depth_test(GL_LESS); + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + c.tri(v(10.0, 15.0, 0.0, RED), v(115.0, 35.0, 0.0, GREEN), v(50.0, 118.0, 0.0, BLUE)); +} + +fn cull_and_front_face(c: &mut Ctx) { + c.st.cull_face = true; + c.st.cull_face_mode = GL_BACK; + c.st.front_face = GL_CCW; + c.tri(v(5.0, 5.0, 0.0, RED), v(55.0, 8.0, 0.0, RED), v(20.0, 50.0, 0.0, RED)); + c.tri(v(5.0, 70.0, 0.0, GREEN), v(20.0, 120.0, 0.0, GREEN), v(55.0, 75.0, 0.0, GREEN)); + c.st.cull_face_mode = GL_FRONT; + c.tri(v(70.0, 5.0, 0.0, BLUE), v(120.0, 8.0, 0.0, BLUE), v(85.0, 50.0, 0.0, BLUE)); + c.tri(v(70.0, 70.0, 0.0, YELLOW), v(85.0, 120.0, 0.0, YELLOW), v(120.0, 75.0, 0.0, YELLOW)); + c.st.front_face = GL_CW; + c.st.cull_face_mode = GL_BACK; + c.tri(v(60.0, 60.0, 0.0, WHITE), v(75.0, 100.0, 0.0, WHITE), v(100.0, 62.0, 0.0, WHITE)); +} + +fn two_sided_colours(c: &mut Ctx) { + c.back_colors = true; + let back = [0.9, 0.1, 0.9, 1.0]; + let mk = |x: f32, y: f32, col| V { back: Some(back), ..v(x, y, 0.0, col) }; + c.tri(mk(10.0, 10.0, RED), mk(60.0, 15.0, GREEN), mk(30.0, 70.0, BLUE)); + c.tri(mk(70.0, 10.0, RED), mk(90.0, 70.0, GREEN), mk(120.0, 15.0, BLUE)); +} + +fn polygon_offset(c: &mut Ctx) { + c.depth_test(GL_LEQUAL); + c.rect(10.0, 10.0, 118.0, 118.0, 0.2, BLUE); + c.st.polygon_offset = GL_OFFSET_FILL; + c.st.offset_factor = 1.0; + c.st.offset_units = -20000.0; + c.tri(v(10.0, 10.0, 0.2, RED), v(118.0, 20.0, 0.2, RED), v(40.0, 118.0, 0.2, RED)); + c.st.offset_units = 20000.0; + c.tri(v(118.0, 118.0, 0.2, GREEN), v(10.0, 100.0, 0.2, GREEN), v(100.0, 10.0, 0.2, GREEN)); +} + +fn lines_flat(c: &mut Ctx) { + c.st.shade_model = GL_FLAT; + let mut rng = Lcg(11); + for _ in 0..24 { + let col = [rng.range(0.2, 1.0), rng.range(0.2, 1.0), rng.range(0.2, 1.0), 1.0]; + let (x0, y0, x1, y1) = (rng.range(2.0, 126.0), rng.range(2.0, 126.0), rng.range(2.0, 126.0), rng.range(2.0, 126.0)); + c.draw(GL_LINES, &[v(x0, y0, 0.0, col), v(x1, y1, 0.0, col)]); + } + // Axis-aligned and diagonal runs, where the end rule shows. + for (i, (a, b)) in [((5.0, 5.0), (60.0, 5.0)), ((5.0, 8.0), (5.0, 60.0)), ((10.0, 10.0), (50.0, 50.0)), ((60.0, 60.0), (100.0, 20.0))].iter().enumerate() { + c.draw(GL_LINES, &[v(a.0 + 60.0, a.1 + 60.0 + i as f32, 0.0, WHITE), v(b.0 + 60.0, b.1 + 60.0 + i as f32, 0.0, WHITE)]); + } +} + +fn lines_smooth_colour(c: &mut Ctx) { + c.draw(GL_LINES, &[v(8.5, 10.5, 0.0, RED), v(120.5, 40.5, 0.0, BLUE)]); + c.draw(GL_LINES, &[v(8.5, 120.5, 0.0, GREEN), v(120.5, 60.5, 0.0, YELLOW)]); + c.draw(GL_LINES, &[v(64.5, 5.5, 0.0, WHITE), v(64.5, 125.5, 0.0, RED)]); +} + +fn lines_wide(c: &mut Ctx) { + for (i, w) in [2.0, 3.0, 5.0].iter().enumerate() { + c.line_width = *w; + let y = 20.0 + 35.0 * i as f32; + c.draw(GL_LINES, &[v(10.0, y, 0.0, RED), v(110.0, y + 15.0, 0.0, GREEN)]); + c.draw(GL_LINES, &[v(20.0 + 40.0 * i as f32, 5.0, 0.0, BLUE), v(30.0 + 40.0 * i as f32, 120.0, 0.0, YELLOW)]); + } +} + +fn lines_smooth(c: &mut Ctx) { + c.line_smooth = true; + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + for (i, w) in [1.0, 2.0, 4.0].iter().enumerate() { + c.line_width = *w; + let y = 15.0 + 35.0 * i as f32; + c.draw(GL_LINES, &[v(8.0, y, 0.0, WHITE), v(120.0, y + 20.0, 0.0, YELLOW)]); + } +} + +fn lines_stippled(c: &mut Ctx) { + c.line_stipple = Some((1, 0x0f0f)); + c.draw(GL_LINES, &[v(5.0, 10.5, 0.0, WHITE), v(60.0, 10.5, 0.0, WHITE)]); + // The second piece continues the first's pattern. + c.draw(GL_LINES, &[v(60.0, 10.5, 0.0, WHITE), v(123.0, 10.5, 0.0, WHITE)]); + c.line_stipple = Some((3, 0x00ff)); + c.draw(GL_LINES, &[v(5.0, 30.5, 0.0, YELLOW), v(120.0, 100.5, 0.0, YELLOW)]); + c.line_width = 3.0; + c.line_stipple = Some((2, 0xaaaa)); + c.draw(GL_LINES, &[v(5.0, 120.5, 0.0, RED), v(120.0, 60.5, 0.0, RED)]); +} + +fn points(c: &mut Ctx) { + let mut rng = Lcg(5); + for size in [1.0, 2.0, 3.0, 6.0] { + c.point_size = size; + for _ in 0..12 { + let col = [rng.range(0.2, 1.0), rng.range(0.2, 1.0), rng.range(0.2, 1.0), 1.0]; + c.draw(GL_POINTS, &[v(rng.range(6.0, 122.0), rng.range(6.0, 122.0), 0.0, col)]); + } + } +} + +fn points_smooth(c: &mut Ctx) { + c.point_smooth = true; + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + for (i, size) in [1.0, 2.5, 4.0, 7.0, 12.0].iter().enumerate() { + c.point_size = *size; + c.draw(GL_POINTS, &[v(15.0 + 22.0 * i as f32, 30.3 + 12.0 * i as f32, 0.0, YELLOW)]); + c.draw(GL_POINTS, &[v(15.5 + 22.0 * i as f32, 100.5, 0.0, RED)]); + } +} + +/// Three overlapping triangles that cross in depth. +fn crossing(c: &mut Ctx) { + c.tri(v(5.0, 10.0, -0.6, RED), v(120.0, 20.0, 0.6, RED), v(30.0, 120.0, 0.0, RED)); + c.tri(v(5.0, 110.0, 0.6, GREEN), v(120.0, 100.0, -0.6, GREEN), v(80.0, 5.0, 0.0, GREEN)); + c.tri(v(60.0, 5.0, 0.1, BLUE), v(70.0, 125.0, -0.1, BLUE), v(125.0, 60.0, 0.1, BLUE)); +} + +fn depth_less(c: &mut Ctx) { + c.depth_test(GL_LESS); + crossing(c); +} + +fn depth_lequal_greater(c: &mut Ctx) { + c.depth_test(GL_GREATER); + c.clear(CLEAR, 0, 0); + crossing(c); +} + +fn depth_always_masked(c: &mut Ctx) { + c.depth_test(GL_ALWAYS); + c.st.depth_mask = false; + c.rect(10.0, 10.0, 100.0, 100.0, 0.3, RED); + c.st.depth_func = GL_LESS; + c.st.depth_mask = true; + c.rect(40.0, 40.0, 120.0, 120.0, 0.4, GREEN); +} + +fn depth_equal_notequal(c: &mut Ctx) { + c.depth_test(GL_LESS); + c.rect(10.0, 10.0, 80.0, 80.0, 0.25, BLUE); + c.st.depth_func = GL_EQUAL; + c.rect(30.0, 30.0, 110.0, 110.0, 0.25, YELLOW); + c.st.depth_func = GL_NOTEQUAL; + c.rect(60.0, 5.0, 125.0, 60.0, 0.25, RED); +} + +fn depth_range_wide(c: &mut Ctx) { + // Window depth from the far plane to the near: the depth word's float + // encoding across its exponents. + c.depth_test(GL_LESS); + for i in 0..8 { + let z = -0.95 + 0.27 * i as f32; + let x = 5.0 + 14.0 * i as f32; + c.rect(x, 5.0 + 6.0 * i as f32, x + 40.0, 80.0 + 6.0 * i as f32, z, [0.1 + 0.12 * i as f32, 0.3, 1.0 - 0.1 * i as f32, 1.0]); + } +} + +fn stencil_mark_then_masked(c: &mut Ctx) { + c.depth_test(GL_LESS); + c.st.stencil_test = true; + c.st.stencil_func = GL_ALWAYS; + c.st.stencil_value_mask = u32::MAX; + c.st.stencil_write_mask = u32::MAX; + c.st.stencil_op = [GL_KEEP, GL_KEEP, GL_INCR]; + c.st.colour_mask = 0; + c.board.gl_write_mask &= !(write::COLOUR_MASK << write::COLOUR_SHIFT); + c.tri(v(10.0, 10.0, 0.0, RED), v(110.0, 30.0, 0.0, RED), v(40.0, 110.0, 0.0, RED)); + c.tri(v(30.0, 30.0, 0.0, RED), v(120.0, 90.0, 0.0, RED), v(20.0, 100.0, 0.0, RED)); + c.board.gl_write_mask |= write::COLOUR_MASK << write::COLOUR_SHIFT; + c.st.stencil_func = GL_EQUAL; + c.st.stencil_ref = 2; + c.st.stencil_op = [GL_KEEP; 3]; + c.rect(0.0, 0.0, 128.0, 128.0, -0.5, GREEN); + c.st.stencil_ref = 1; + c.rect(0.0, 0.0, 128.0, 128.0, -0.5, YELLOW); +} + +fn stencil_ops(c: &mut Ctx) { + c.st.stencil_test = true; + c.st.stencil_value_mask = u32::MAX; + c.st.stencil_write_mask = 0xff; + c.st.stencil_func = GL_ALWAYS; + c.st.stencil_ref = 0x35; + for (i, op) in [GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT, GL_KEEP].iter().enumerate() { + c.st.stencil_op = [GL_KEEP, GL_KEEP, *op]; + let x = 4.0 + 24.0 * i as f32; + c.rect(x, 10.0, x + 22.0, 60.0, 0.0, RED); + c.rect(x, 10.0, x + 22.0, 40.0, 0.0, RED); + } + c.st.stencil_func = GL_NOTEQUAL; + c.st.stencil_ref = 0x35; + c.st.stencil_value_mask = 0x0f; + c.st.stencil_op = [GL_KEEP; 3]; + c.rect(0.0, 70.0, 128.0, 120.0, 0.0, BLUE); +} + +fn stencil_depth_fail(c: &mut Ctx) { + c.depth_test(GL_LESS); + c.rect(20.0, 20.0, 100.0, 100.0, -0.5, BLUE); + c.st.stencil_test = true; + c.st.stencil_func = GL_ALWAYS; + c.st.stencil_value_mask = u32::MAX; + c.st.stencil_write_mask = 0xf0; + c.st.stencil_ref = 0xff; + c.st.stencil_op = [GL_KEEP, GL_REPLACE, GL_INCR]; + c.rect(5.0, 5.0, 80.0, 80.0, 0.5, RED); +} + +fn alpha_test(c: &mut Ctx) { + c.st.alpha_test = true; + c.st.alpha_func = GL_GEQUAL; + c.st.alpha_ref = 0.5; + c.tri(v(5.0, 5.0, 0.0, [1.0, 0.0, 0.0, 1.0]), v(120.0, 10.0, 0.0, [0.0, 1.0, 0.0, 0.0]), v(30.0, 120.0, 0.0, [0.0, 0.0, 1.0, 0.5])); + c.st.alpha_func = GL_LESS; + c.st.alpha_ref = 0.3; + c.tri(v(120.0, 120.0, 0.0, YELLOW), v(60.0, 60.0, 0.0, [1.0, 1.0, 1.0, 0.0]), v(120.0, 40.0, 0.0, WHITE)); +} + +/// A background to blend over: horizontal ramps. +fn ramp_background(c: &mut Ctx) { + c.draw(GL_QUADS, &[v(0.0, 0.0, 0.9, [0.9, 0.2, 0.1, 0.8]), v(128.0, 0.0, 0.9, [0.1, 0.9, 0.3, 0.6]), v(128.0, 128.0, 0.9, [0.2, 0.3, 1.0, 0.4]), v(0.0, 128.0, 0.9, [1.0, 1.0, 0.1, 0.9])]); +} + +/// Every source factor against a few destination factors, in a grid. +fn blend_factor_grid(c: &mut Ctx) { + ramp_background(c); + let srcs = [GL_ZERO_F, GL_ONE_F, GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_SRC_ALPHA_SATURATE]; + let dsts = [GL_ZERO_F, GL_ONE_F, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA]; + c.board.gl_write_mask |= write::COLOUR_MASK << write::COLOUR_SHIFT; + for (i, &s) in srcs.iter().enumerate() { + for (j, &d) in dsts.iter().enumerate() { + c.blend(s, d); + let (x, y) = (2.0 + 14.0 * i as f32, 2.0 + 20.0 * j as f32); + c.draw(GL_QUADS, &[v(x, y, 0.0, [0.8, 0.5, 0.2, 0.6]), v(x + 12.0, y, 0.0, [0.3, 0.9, 0.6, 0.3]), v(x + 12.0, y + 18.0, 0.0, [0.9, 0.2, 0.9, 0.8]), v(x, y + 18.0, 0.0, [0.5, 0.5, 0.5, 0.5])]); + } + } +} + +const GL_ZERO_F: u32 = 0; +const GL_ONE_F: u32 = 1; + +fn blend_equations(c: &mut Ctx) { + ramp_background(c); + c.blend(GL_ONE_F, GL_ONE_F); + for (i, eq) in [GL_FUNC_ADD, GL_FUNC_SUBTRACT, GL_FUNC_REVERSE_SUBTRACT, GL_MIN, GL_MAX].iter().enumerate() { + c.st.blend_equation = *eq; + let x = 4.0 + 24.0 * i as f32; + c.draw(GL_QUADS, &[v(x, 10.0, 0.0, [0.6, 0.4, 0.2, 0.7]), v(x + 22.0, 10.0, 0.0, [0.2, 0.7, 0.9, 0.7]), v(x + 22.0, 118.0, 0.0, [0.8, 0.8, 0.1, 0.3]), v(x, 118.0, 0.0, [0.1, 0.1, 0.6, 0.3])]); + } +} + +fn blend_constant_colour(c: &mut Ctx) { + ramp_background(c); + c.st.blend_color = [0.25, 0.5, 0.75, 0.4]; + for (i, (s, d)) in [(GL_CONSTANT_COLOR, GL_ONE_MINUS_CONSTANT_COLOR), (GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA), (GL_ONE_MINUS_CONSTANT_COLOR, GL_CONSTANT_COLOR), (GL_ONE_MINUS_CONSTANT_ALPHA, GL_CONSTANT_ALPHA)].iter().enumerate() { + c.blend(*s, *d); + let x = 4.0 + 31.0 * i as f32; + c.rect(x, 10.0, x + 28.0, 118.0, 0.0, [0.9, 0.3, 0.2, 0.5]); + } +} + +fn blend_triangles_over(c: &mut Ctx) { + ramp_background(c); + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + c.tri(v(10.0, 10.0, 0.0, [1.0, 0.0, 0.0, 0.8]), v(120.0, 20.0, 0.0, [0.0, 1.0, 0.0, 0.4]), v(40.0, 120.0, 0.0, [0.0, 0.0, 1.0, 0.1])); + c.tri(v(120.0, 120.0, 0.0, [1.0, 1.0, 1.0, 0.7]), v(10.0, 100.0, 0.0, [1.0, 1.0, 1.0, 0.7]), v(100.0, 10.0, 0.0, [0.0, 0.0, 0.0, 0.7])); +} + +fn logic_op_and_colour_mask(c: &mut Ctx) { + ramp_background(c); + c.st.color_logic_op = true; + c.st.logic_op_mode = GL_CLEAR + 6; // XOR + c.rect(10.0, 10.0, 60.0, 118.0, 0.0, [0.5, 0.7, 0.2, 1.0]); + c.st.logic_op_mode = GL_CLEAR + 1; // AND + c.rect(40.0, 40.0, 100.0, 100.0, 0.0, [0.9, 0.5, 0.8, 1.0]); + c.st.color_logic_op = false; + c.board.gl_write_mask = (c.board.gl_write_mask & !(write::COLOUR_MASK << write::COLOUR_SHIFT)) | ((GL_COLOUR_MASK_RED | GL_COLOUR_MASK_ALPHA) << write::COLOUR_SHIFT); + c.rect(70.0, 20.0, 120.0, 110.0, 0.0, [1.0, 1.0, 1.0, 0.5]); +} + +fn scissor(c: &mut Ctx) { + c.st.scissor_test = true; + c.st.scissor = [20, 30, 70, 50]; + c.tri(v(0.0, 0.0, 0.0, RED), v(128.0, 10.0, 0.0, GREEN), v(10.0, 128.0, 0.0, BLUE)); + c.draw(GL_LINES, &[v(0.0, 40.5, 0.0, WHITE), v(128.0, 60.5, 0.0, WHITE)]); +} + +// Clipping. + +/// Triangles that leave the window on each side, and one that covers it. +fn clip_frustum_xy(c: &mut Ctx) { + c.tri(v(-40.0, 20.0, 0.0, RED), v(60.0, -30.0, 0.0, GREEN), v(70.0, 70.0, 0.0, BLUE)); + c.tri(v(100.0, -20.0, 0.0, YELLOW), v(180.0, 60.0, 0.0, YELLOW), v(90.0, 50.0, 0.0, RED)); + c.tri(v(-20.0, 90.0, 0.0, WHITE), v(60.0, 90.0, 0.0, GREEN), v(20.0, 190.0, 0.0, BLUE)); + c.draw(GL_QUADS, &[v(80.0, 80.0, 0.0, RED), v(200.0, 90.0, 0.0, GREEN), v(190.0, 200.0, 0.0, BLUE), v(70.0, 150.0, 0.0, YELLOW)]); +} + +/// Depth outside [-w, w]: the near and far planes cut, with w varying. +fn clip_frustum_z(c: &mut Ctx) { + c.depth_test(GL_LESS); + // z beyond -w (near) at one corner, beyond w (far) at another. + c.tri( + V { p: [-0.9, -0.9, -2.0, 1.0], ..v(0.0, 0.0, 0.0, RED) }, + V { p: [0.9, -0.5, 0.5, 1.0], ..v(0.0, 0.0, 0.0, GREEN) }, + V { p: [0.0, 0.9, 1.9, 1.0], ..v(0.0, 0.0, 0.0, BLUE) }, + ); + // Window depth carried by w: a quad through the near plane in perspective. + c.draw( + GL_QUADS, + &[ + v(70.0, 10.0, 0.0, YELLOW).persp(0.5, 0.2), + v(120.0, 10.0, 0.0, YELLOW).persp(3.0, 0.9), + v(120.0, 110.0, 0.0, RED).persp(3.0, 0.9), + v(70.0, 110.0, 0.0, RED).persp(0.5, 0.2), + ], + ); +} + +/// Vertices at and behind the eye: w zero and negative. +fn clip_w_behind_eye(c: &mut Ctx) { + c.tri(v(10.0, 10.0, 0.0, RED).persp(1.0, 0.5), v(120.0, 20.0, 0.0, GREEN).persp(-1.0, 0.5), v(40.0, 110.0, 0.0, BLUE).persp(2.0, 0.5)); + c.tri(v(10.0, 120.0, 0.0, YELLOW).persp(1.0, 0.5), v(100.0, 100.0, 0.0, WHITE).persp(0.0, 0.5), v(120.0, 50.0, 0.0, RED).persp(1.5, 0.5)); + c.draw(GL_LINES, &[v(10.0, 64.0, 0.0, WHITE).persp(2.0, 0.5), v(120.0, 64.0, 0.0, WHITE).persp(-0.5, 0.5)]); +} + +fn clip_user_plane(c: &mut Ctx) { + // x + y/4 >= -0.25 in eye space, as the bench's mirror scene cuts. + c.st.clip_planes = 1; + c.st.clip_plane[0] = [1.0, 0.25, 0.0, 0.25]; + c.tri(v(5.0, 5.0, 0.0, RED), v(120.0, 20.0, 0.0, GREEN), v(30.0, 120.0, 0.0, BLUE)); + c.draw(GL_LINES, &[v(0.0, 100.5, 0.0, WHITE), v(128.0, 30.5, 0.0, WHITE)]); + c.point_size = 4.0; + for i in 0..6 { + c.draw(GL_POINTS, &[v(8.0 + 20.0 * i as f32, 8.0 + 20.0 * i as f32, 0.0, YELLOW)]); + } +} + +fn clip_user_planes_rotated(c: &mut Ctx) { + // Two planes under a modelview that rotates 30 degrees and moves: the + // plane is in eye space, so its edge in the window turns with it. + let (s, k) = (0.5f32, 0.866_025_4f32); + c.st.modelview = [k, s, 0.0, 0.0, -s, k, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.1, -0.05, 0.0, 1.0]; + c.st.clip_planes = 0b101; + c.st.clip_plane[0] = [0.0, 1.0, 0.0, 0.3]; + c.st.clip_plane[2] = [-1.0, 0.2, 0.3, 0.5]; + c.tri(v(5.0, 5.0, 0.0, RED), v(125.0, 15.0, 0.0, GREEN), v(20.0, 125.0, 0.0, BLUE)); + c.tri(v(125.0, 125.0, 0.0, YELLOW), v(10.0, 110.0, 0.0, WHITE), v(110.0, 10.0, 0.0, RED)); +} + +fn clip_user_plane_textured_smooth(c: &mut Ctx) { + c.texture(&Tex { min: 1, mag_linear: true, ..Tex::new(RGBA) }); + c.st.clip_planes = 2; + c.st.clip_plane[1] = [-0.5, 1.0, 0.0, 0.1]; + c.depth_test(GL_LESS); + c.tri(v(5.0, 5.0, 0.0, WHITE).tex(0.0, 0.0), v(125.0, 10.0, 0.0, WHITE).tex(1.0, 0.0), v(60.0, 125.0, 0.0, WHITE).tex(0.5, 1.0)); +} + +// The window-matrix mode: one eye-to-window transform in place of the +// projection and the viewport map. + +fn window_matrix(c: &mut Ctx, rows_down: bool) { + c.st.window_matrix_given = true; + c.st.window_depth_scale = 1.0; + c.st.window_rows_down = rows_down; + // Eye coordinates are window pixels and depth in [0, 1]. + let mut m = [0.0f32; 16]; + m[0] = 1.0; + m[5] = 1.0; + m[10] = 1.0; + m[15] = 1.0; + m[12] = 0.0; + c.st.window_matrix = m; + c.depth_row([0.0, 0.0, 1.0, 0.0]); + // The window-matrix y is in the draw's own rows. + if rows_down { + c.st.drawable.height = SIDE as u32; + } +} + +fn wv(x: f32, y: f32, z: f32, col: [f32; 4]) -> V { + V { p: [x, y, z, 1.0], ..v(0.0, 0.0, 0.0, col) } +} + +fn window_matrix_triangles(c: &mut Ctx) { + window_matrix(c, false); + c.depth_test(GL_LESS); + c.tri(wv(10.0, 10.0, 0.2, RED), wv(110.0, 25.0, 0.4, GREEN), wv(40.0, 115.0, 0.6, BLUE)); + c.tri(wv(100.0, 100.0, 0.1, YELLOW), wv(20.0, 90.0, 0.7, WHITE), wv(110.0, 20.0, 0.5, RED)); +} + +fn window_matrix_rows_down(c: &mut Ctx) { + window_matrix(c, true); + c.st.cull_face = true; + c.st.cull_face_mode = GL_BACK; + c.st.front_face = GL_CCW; + c.tri(wv(10.0, 10.0, 0.5, RED), wv(60.0, 15.0, 0.5, GREEN), wv(20.0, 60.0, 0.5, BLUE)); + c.tri(wv(70.0, 10.0, 0.5, YELLOW), wv(80.0, 60.0, 0.5, YELLOW), wv(120.0, 15.0, 0.5, YELLOW)); +} + +fn window_matrix_clip_and_lines(c: &mut Ctx) { + window_matrix(c, false); + c.depth_test(GL_LESS); + // Outside the depth range and outside the window. + c.tri(wv(-30.0, 10.0, 0.5, RED), wv(60.0, 5.0, 1.4, GREEN), wv(30.0, 90.0, -0.4, BLUE)); + c.tri(wv(90.0, 60.0, 0.3, YELLOW), wv(200.0, 70.0, 0.3, YELLOW), wv(120.0, 190.0, 0.3, WHITE)); + c.draw(GL_LINES, &[wv(5.0, 100.5, 0.2, WHITE), wv(125.0, 120.5, 0.8, RED)]); + c.point_size = 3.0; + c.draw(GL_POINTS, &[wv(64.0, 64.0, 0.3, GREEN)]); +} + +fn window_matrix_scaled(c: &mut Ctx) { + // A real transform: scale, rotate and offset, with a depth scale below 1. + window_matrix(c, false); + c.st.window_depth_scale = 0.5; + let (s, k) = (0.6f32, 0.8f32); + c.st.window_matrix = [60.0 * k, 60.0 * s, 0.0, 0.0, -60.0 * s, 60.0 * k, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 64.0, 64.0, 0.0, 1.0]; + c.depth_test(GL_LESS); + c.tri(V { p: [-0.8, -0.8, 0.3, 1.0], ..wv(0.0, 0.0, 0.0, RED) }, V { p: [0.9, -0.7, 0.5, 1.0], ..wv(0.0, 0.0, 0.0, GREEN) }, V { p: [0.0, 0.9, 0.2, 1.0], ..wv(0.0, 0.0, 0.0, BLUE) }); +} + +// Perspective and the general transform. + +fn projection_frustum(c: &mut Ctx) { + // glFrustum(-0.5, 0.5, -0.5, 0.5, 1, 20) looking at a floor and a wall, + // under a modelview that moves the scene 4 units away. + let (l, r, b, t, n, f) = (-0.5f32, 0.5f32, -0.5f32, 0.5f32, 1.0f32, 20.0f32); + let p = [2.0 * n / (r - l), 0.0, 0.0, 0.0, 0.0, 2.0 * n / (t - b), 0.0, 0.0, (r + l) / (r - l), (t + b) / (t - b), -(f + n) / (f - n), -1.0, 0.0, 0.0, -2.0 * f * n / (f - n), 0.0]; + c.st.projection = p; + c.st.modelview = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -4.0, 1.0]; + // The board's window depth is linear in eye distance, 0 at the near plane + // and 1 at the far: the depth row applied to the eye position. + c.depth_row([0.0, 0.0, -1.0 / (f - n) as f64, -n as f64 / (f - n) as f64]); + c.depth_test(GL_LESS); + let o = |x: f32, y: f32, z: f32, col| V { p: [x, y, z, 1.0], ..v(0.0, 0.0, 0.0, col) }; + // A floor receding, a wall across it, a tilted slab through both. + c.draw(GL_QUADS, &[o(-2.0, -1.0, 2.0, RED), o(2.0, -1.0, 2.0, GREEN), o(2.0, -1.0, -10.0, BLUE), o(-2.0, -1.0, -10.0, YELLOW)]); + c.draw(GL_QUADS, &[o(-1.0, -1.0, -2.0, WHITE), o(1.0, -1.0, -2.0, WHITE), o(1.0, 1.0, -2.0, RED), o(-1.0, 1.0, -2.0, RED)]); + c.tri(o(-1.5, -0.5, 1.0, GREEN), o(1.5, 0.5, -3.0, BLUE), o(0.0, 1.5, -1.0, YELLOW)); +} + +fn projection_ortho_rotated(c: &mut Ctx) { + // glOrtho(-2, 2, -2, 2, -1, 1) under a modelview that rotates about z + // and moves: depth row (a z + b + 1) / 2 for z' = a z + b. + c.st.projection = [0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0]; + let (s, k) = (0.7071f32, 0.7071f32); + c.st.modelview = [k, s, 0.0, 0.0, -s, k, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.3, -0.2, 0.0, 1.0]; + c.depth_row([0.0, 0.0, -0.5, 0.5]); + c.depth_test(GL_LESS); + let o = |x: f32, y: f32, z: f32, col| V { p: [x, y, z, 1.0], ..v(0.0, 0.0, 0.0, col) }; + c.tri(o(-1.5, -1.0, -0.5, RED), o(1.5, -1.2, 0.5, GREEN), o(0.0, 1.7, 0.0, BLUE)); + c.draw(GL_QUADS, &[o(-1.0, -1.0, 0.2, YELLOW), o(1.0, -1.0, 0.2, YELLOW), o(1.0, 1.0, -0.2, WHITE), o(-1.0, 1.0, -0.2, WHITE)]); +} + +fn perspective_correct_colour(c: &mut Ctx) { + // A quad that recedes: colour and texture are perspective-corrected. + c.texture(&Tex { min: 1, mag_linear: true, ..Tex::new(RGB) }); + c.depth_test(GL_LESS); + c.draw( + GL_QUADS, + &[ + v(5.0, 5.0, 0.0, WHITE).tex(0.0, 0.0).persp(1.0, 0.1), + v(123.0, 5.0, 0.0, WHITE).tex(4.0, 0.0).persp(4.0, 0.9), + v(123.0, 123.0, 0.0, WHITE).tex(4.0, 4.0).persp(4.0, 0.9), + v(5.0, 123.0, 0.0, WHITE).tex(0.0, 4.0).persp(1.0, 0.1), + ], + ); +} + +// Textures: formats, environments, filters. + +fn textured(c: &mut Ctx, t: Tex, vertex: [f32; 4]) { + c.texture(&t); + let col = vertex; + c.draw(GL_QUADS, &[v(8.0, 8.0, 0.0, col).tex(0.0, 0.0), v(120.0, 8.0, 0.0, col).tex(1.0, 0.0), v(120.0, 120.0, 0.0, col).tex(1.0, 1.0), v(8.0, 120.0, 0.0, col).tex(0.0, 1.0)]); +} + +macro_rules! format_scenes { + ($($name:ident: $fmt:expr,)*) => {$( + fn $name(c: &mut Ctx) { + textured(c, Tex { mag_linear: true, min: 1, ..Tex::new($fmt) }, [0.9, 0.8, 0.6, 0.8]); + } + )*}; +} + +format_scenes! { + tex_format_rgba: RGBA, + tex_format_rgb: RGB, + tex_format_luminance: LUMINANCE, + tex_format_luminance_alpha: LUMINANCE_ALPHA, + tex_format_alpha: ALPHA, + tex_format_intensity: INTENSITY, +} + +macro_rules! env_scenes { + ($($name:ident: $env:expr, $fmt:expr,)*) => {$( + fn $name(c: &mut Ctx) { + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + ramp_background(c); + textured(c, Tex { env: $env, mag_linear: true, min: 1, ..Tex::new($fmt) }, [0.9, 0.5, 0.7, 0.7]); + } + )*}; +} + +env_scenes! { + tex_env_replace_rgba: 1, RGBA, + tex_env_modulate_rgba: 2, RGBA, + tex_env_decal_rgba: 3, RGBA, + tex_env_decal_rgb: 3, RGB, + tex_env_blend_rgba: 4, RGBA, + tex_env_blend_luminance: 4, LUMINANCE, + tex_env_modulate_luminance_alpha: 2, LUMINANCE_ALPHA, + tex_env_replace_alpha: 1, ALPHA, +} + +fn tex_nearest_repeat(c: &mut Ctx) { + c.texture(&Tex { min: 0, mag_linear: false, ..Tex::new(RGBA) }); + c.draw(GL_QUADS, &[v(5.0, 5.0, 0.0, WHITE).tex(-1.0, -1.0), v(123.0, 5.0, 0.0, WHITE).tex(2.5, -1.0), v(123.0, 123.0, 0.0, WHITE).tex(2.5, 2.5), v(5.0, 123.0, 0.0, WHITE).tex(-1.0, 2.5)]); +} + +fn tex_clamp_to_edge_linear(c: &mut Ctx) { + c.texture(&Tex { min: 1, mag_linear: true, wrap: 1, ..Tex::new(RGBA) }); + c.draw(GL_QUADS, &[v(5.0, 5.0, 0.0, WHITE).tex(-0.5, -0.5), v(123.0, 5.0, 0.0, WHITE).tex(1.5, -0.5), v(123.0, 123.0, 0.0, WHITE).tex(1.5, 1.5), v(5.0, 123.0, 0.0, WHITE).tex(-0.5, 1.5)]); +} + +macro_rules! mip_scenes { + ($($name:ident: $min:expr,)*) => {$( + fn $name(c: &mut Ctx) { + // A texture of 32 texels minified to about a quarter, and a + // recession through several levels. + c.texture(&Tex { size: 32, min: $min, mag_linear: true, ..Tex::new(RGB) }); + c.draw(GL_QUADS, &[v(4.0, 4.0, 0.0, WHITE).tex(0.0, 0.0), v(60.0, 4.0, 0.0, WHITE).tex(4.0, 0.0), v(60.0, 60.0, 0.0, WHITE).tex(4.0, 4.0), v(4.0, 60.0, 0.0, WHITE).tex(0.0, 4.0)]); + c.depth_test(GL_LESS); + c.draw(GL_QUADS, &[ + v(66.0, 4.0, 0.0, WHITE).tex(0.0, 0.0).persp(1.0, 0.2), + v(124.0, 4.0, 0.0, WHITE).tex(6.0, 0.0).persp(6.0, 0.8), + v(124.0, 124.0, 0.0, WHITE).tex(6.0, 6.0).persp(6.0, 0.8), + v(66.0, 124.0, 0.0, WHITE).tex(0.0, 6.0).persp(1.0, 0.2), + ]); + } + )*}; +} + +mip_scenes! { + tex_mip_nearest_nearest: 2, + tex_mip_linear_nearest: 3, + tex_mip_nearest_linear: 4, + tex_mip_linear_linear: 5, +} + +/// SGIS_detail_texture under GL_LINEAR_DETAIL_COLOR_SGIS, as SGI Quake draws +/// its world: a mipmapped RGB base with a 16x16 detail image at the default +/// level -4 (level-table entry 60), and the library's ramp from weight 0 at +/// LOD 0 to 1 at LOD -4. Both quads magnify, so the weight comes from +/// between the table's entries. +fn tex_detail_colour(c: &mut Ctx) { + tex_detail(c, 0); +} + +/// The same scene under the modulate combine, word 0 bit 31. +fn tex_detail_colour_modulate(c: &mut Ctx) { + tex_detail(c, 0x8000_0000); +} + +fn tex_detail(c: &mut Ctx, combine: u32) { + c.texture(&Tex { size: 32, min: 5, ..Tex::new(RGB) }); + let b = &mut c.board; + let (size, base) = (16u32, 0x18_0000u32); + let units = (size.div_ceil(16) + 1) & !1; + let obj = b.object_select(base, units * 16); + assert!(b.object_rows(obj, size)); + for y in 0..size { + for x in 0..size { + let g = if (x / 2 + y) & 1 == 0 { 0xc0 } else { 0x40 } + (x * 3) as u32; + b.object_store(obj, x, y, (g << 24) | (g << 16) | (g << 8) | 0xff); + } + } + b.tex.levels[60 * 3..][..3].copy_from_slice(&[base, 0x00d0_0000 | units, 0]); + // Detail level 4 in bits 29:25, log2 16 in 24:20 and 19:15. + b.tex.sampler[0] = combine | 0x0800_0000 | (4 << 20) | (4 << 15) | (5 << 10) | (5 << 5); + // Minification code 5 and magnification code 4, detail colour. + b.tex.sampler[4] = 0x1000_0000 | RGB | (5 << 5) | (4 << 1); + for (i, e) in b.tex.detail_function.iter_mut().enumerate() { + *e = ((i as u32 * 32).min(4095)) << 12; + } + b.tex.detail_function_valid = 0xffff; + c.draw(GL_QUADS, &[v(4.0, 4.0, 0.0, WHITE).tex(0.0, 0.0), v(62.0, 4.0, 0.0, WHITE).tex(1.0, 0.0), v(62.0, 124.0, 0.0, WHITE).tex(1.0, 2.0), v(4.0, 124.0, 0.0, WHITE).tex(0.0, 2.0)]); + c.draw(GL_QUADS, &[v(66.0, 4.0, 0.0, WHITE).tex(0.0, 0.0), v(124.0, 4.0, 0.0, WHITE).tex(0.125, 0.0), v(124.0, 124.0, 0.0, WHITE).tex(0.125, 0.25), v(66.0, 124.0, 0.0, WHITE).tex(0.0, 0.25)]); +} + +fn tex_with_fog_blend_depth(c: &mut Ctx) { + c.depth_test(GL_LESS); + c.fog([200, 210, 230], 1); + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + c.texture(&Tex { min: 5, size: 32, mag_linear: true, ..Tex::new(RGBA) }); + for i in 0..4 { + let z = -0.6 + 0.4 * i as f32; + let x = 6.0 + 20.0 * i as f32; + c.draw(GL_QUADS, &[v(x, 6.0, z, WHITE).tex(0.0, 0.0), v(x + 70.0, 6.0, z, WHITE).tex(2.0, 0.0), v(x + 70.0, 116.0, z, WHITE).tex(2.0, 2.0), v(x, 116.0, z, WHITE).tex(0.0, 2.0)]); + } +} + +// Fog. + +fn fog_ramp_triangles(c: &mut Ctx) { + c.depth_test(GL_LESS); + c.fog([180, 190, 210], 1); + c.draw(GL_QUADS, &[v(5.0, 5.0, -0.9, RED), v(123.0, 5.0, 0.9, GREEN), v(123.0, 60.0, 0.9, GREEN), v(5.0, 60.0, -0.9, RED)]); + c.tri(v(5.0, 70.0, -0.8, BLUE), v(120.0, 70.0, 0.2, YELLOW), v(60.0, 125.0, 0.8, WHITE)); +} + +fn fog_lines_and_points(c: &mut Ctx) { + c.fog([255, 128, 0], 2); + c.draw(GL_LINES, &[v(5.0, 10.5, -0.9, WHITE), v(120.0, 40.5, 0.9, WHITE)]); + c.point_size = 5.0; + for i in 0..6 { + c.draw(GL_POINTS, &[v(10.0 + 20.0 * i as f32, 80.0, -0.9 + 0.3 * i as f32, GREEN)]); + } +} + +fn fog_over_blend(c: &mut Ctx) { + ramp_background(c); + c.fog([40, 40, 40], 1); + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + c.tri(v(5.0, 5.0, -0.9, [1.0, 1.0, 1.0, 0.8]), v(120.0, 20.0, 0.9, [1.0, 1.0, 1.0, 0.8]), v(40.0, 120.0, 0.0, [1.0, 1.0, 1.0, 0.8])); +} + +// Lighting. The board lights a vertex from the words the command stream +// loaded (Board::gl_light()); the scenes load them and light a sphere's +// vertices the way the front end would before it hands the draw on. + +#[derive(Clone, Copy, Default)] +pub struct Light { + pub pos: [f32; 3], + pub directional: bool, + pub colour: [f32; 3], + /// Constant, linear, quadratic. + pub atten: [f32; 3], + /// Spot direction, cutoff cosine and exponent; a cosine of -1 is no spot. + pub spot: Option<([f32; 3], f32, f32)>, +} + +pub fn load_lights(b: &mut Board, lights: &[(usize, Light)], ambient: [f32; 3], local_viewer: bool, shininess: f32, material: ([f32; 3], [f32; 3], [f32; 3], [f32; 3]), colour_material: u32) { + let identity: [f32; 16] = [1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]; + b.geom.modelview = identity.map(f32::to_bits); + b.geom.normal_matrix = identity.map(f32::to_bits); + b.geom.local_viewer = local_viewer; + b.geom.normalize = true; + b.geom.colour_material = colour_material; + b.geom.light_colour.fill(0); + b.geom.light_vec.fill(0); + let a = gl_cmd::LIGHT_MODEL_AMBIENT as usize * 4; + for k in 0..3 { + b.geom.light_colour[a + k] = ambient[k].to_bits(); + } + let (ma, md, ms, me) = material; + for k in 0..3 { + b.geom.material[k] = ma[k].to_bits(); + b.geom.material[4 + k] = md[k].to_bits(); + b.geom.material[8 + k] = ms[k].to_bits(); + b.geom.material[12 + k] = me[k].to_bits(); + } + b.geom.material[3] = 1.0f32.to_bits(); + b.geom.material[7] = 0.9f32.to_bits(); + b.geom.material[11] = 1.0f32.to_bits(); + b.geom.material[15] = 1.0f32.to_bits(); + b.geom.shininess_sqrt = shininess.sqrt().to_bits(); + for &(i, l) in lights { + for k in 0..3 { + b.geom.light_colour[i * 12 + k] = (l.colour[k] * 0.2).to_bits(); + b.geom.light_colour[i * 12 + 4 + k] = l.colour[k].to_bits(); + b.geom.light_colour[i * 12 + 8 + k] = l.colour[k].to_bits(); + } + let blk = &mut b.geom.light_vec[i * gl_cmd::LIGHT_VEC_FLOATS as usize..]; + for k in 0..3 { + blk[light::POS + k] = l.pos[k].to_bits(); + } + if l.directional { + // The board is given the half vector; any non-zero one marks the + // light directional. + let n = (l.pos[0] * l.pos[0] + l.pos[1] * l.pos[1] + (l.pos[2] + 1.0).powi(2)).sqrt().max(1e-6); + blk[light::HALF] = (l.pos[0] / n).to_bits(); + blk[light::HALF + 1] = (l.pos[1] / n).to_bits(); + blk[light::HALF + 2] = ((l.pos[2] + 1.0) / n).to_bits(); + } + blk[light::ATTEN] = l.atten[2].to_bits(); + blk[light::ATTEN + 1] = l.atten[0].to_bits(); + blk[light::ATTEN + 2] = l.atten[1].to_bits(); + blk[light::SPOT_COS] = 1.0f32.to_bits(); + if let Some((dir, cos, exp)) = l.spot { + blk[light::SPOT_COS] = (-cos).to_bits(); + blk[light::SPOT_EXP] = (-exp).to_bits(); + for k in 0..3 { + blk[light::SPOT_DIR + k] = (-dir[k]).to_bits(); + } + } + } +} + +/// A sphere of radius 0.8 about the window's middle (and a flat disc behind +/// it), lit vertex by vertex, drawn smooth and depth tested. +fn lit_sphere(c: &mut Ctx, current_colour: [f32; 4]) { + c.depth_test(GL_LESS); + c.board.geom.colour = current_colour.map(f32::to_bits); + let (nlat, nlon) = (10usize, 16usize); + let point = |i: usize, j: usize| -> [f32; 3] { + let th = -std::f32::consts::FRAC_PI_2 + std::f32::consts::PI * i as f32 / nlat as f32; + let ph = 2.0 * std::f32::consts::PI * j as f32 / nlon as f32; + [th.cos() * ph.sin(), th.sin(), -th.cos() * ph.cos()] + }; + let lit = |c: &mut Ctx, p: [f32; 3]| -> V { + let n = p; + c.board.geom.normal = n.map(f32::to_bits); + let obj = [p[0] * 0.8, p[1] * 0.8, p[2] * 0.8, 1.0]; + let rgba = c.board.gl_light(&obj); + V { p: [p[0] * 0.8, p[1] * 0.8, p[2] * 0.8, 1.0], c: rgba, t: [0.0, 0.0, 0.0, 1.0], n, back: None } + }; + for i in 0..nlat { + for j in 0..nlon { + let q = [point(i, j), point(i + 1, j), point(i + 1, j + 1), point(i, j + 1)]; + let vs: Vec = q.iter().map(|&p| lit(c, p)).collect(); + c.draw(GL_TRIANGLES, &[vs[0], vs[1], vs[2]]); + c.draw(GL_TRIANGLES, &[vs[0], vs[2], vs[3]]); + } + } +} + +pub const MATERIAL: ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) = ([0.2, 0.2, 0.25], [0.8, 0.5, 0.4], [0.9, 0.9, 0.9], [0.02, 0.0, 0.05]); + +fn light_directional(c: &mut Ctx) { + let l = Light { pos: [0.5, 0.6, -0.62], directional: true, colour: [1.0, 0.95, 0.85], atten: [1.0, 0.0, 0.0], spot: None }; + load_lights(&mut c.board, &[(0, l)], [0.2, 0.2, 0.2], false, 20.0, MATERIAL, 0); + lit_sphere(c, WHITE); +} + +fn light_positional_attenuated(c: &mut Ctx) { + let l = Light { pos: [1.5, 1.0, -2.0], directional: false, colour: [1.0, 1.0, 1.0], atten: [1.0, 0.3, 0.15], spot: None }; + load_lights(&mut c.board, &[(0, l)], [0.1, 0.1, 0.1], false, 8.0, MATERIAL, 0); + lit_sphere(c, WHITE); +} + +fn light_spot(c: &mut Ctx) { + let l = Light { pos: [0.0, 1.5, -2.5], directional: false, colour: [1.0, 1.0, 0.8], atten: [1.0, 0.0, 0.0], spot: Some(([0.0, -0.5, 0.85], 0.8, 4.0)) }; + load_lights(&mut c.board, &[(0, l)], [0.05, 0.05, 0.1], false, 30.0, MATERIAL, 0); + lit_sphere(c, WHITE); +} + +fn light_local_viewer_specular(c: &mut Ctx) { + let l = Light { pos: [-1.0, 1.0, -1.5], directional: false, colour: [0.9, 0.9, 1.0], atten: [1.0, 0.0, 0.0], spot: None }; + load_lights(&mut c.board, &[(2, l)], [0.15, 0.15, 0.15], true, 64.0, MATERIAL, 0); + lit_sphere(c, WHITE); +} + +fn light_three_lights_colour_material(c: &mut Ctx) { + let a = Light { pos: [1.0, 0.0, -1.0], directional: true, colour: [1.0, 0.1, 0.1], atten: [1.0, 0.0, 0.0], spot: None }; + let b = Light { pos: [-1.0, 0.5, -1.0], directional: true, colour: [0.1, 0.1, 1.0], atten: [1.0, 0.0, 0.0], spot: None }; + let s = Light { pos: [0.0, -2.0, -1.5], directional: false, colour: [0.2, 1.0, 0.2], atten: [1.0, 0.1, 0.0], spot: Some(([0.0, 1.0, 0.5], 0.7, 2.0)) }; + load_lights(&mut c.board, &[(0, a), (3, b), (7, s)], [0.1, 0.1, 0.1], false, 12.0, MATERIAL, colour::MATERIAL_AMBIENT | colour::MATERIAL_DIFFUSE); + lit_sphere(c, [0.9, 0.7, 0.3, 0.75]); +} + +fn light_unlit_emission_only(c: &mut Ctx) { + load_lights(&mut c.board, &[], [0.3, 0.3, 0.3], false, 1.0, MATERIAL, colour::MATERIAL_EMISSION); + lit_sphere(c, [0.2, 0.8, 0.4, 1.0]); +} + +fn light_lit_blended_textured(c: &mut Ctx) { + let l = Light { pos: [0.5, 0.6, -0.62], directional: true, colour: [1.0, 1.0, 1.0], atten: [1.0, 0.0, 0.0], spot: None }; + load_lights(&mut c.board, &[(0, l)], [0.2, 0.2, 0.2], false, 20.0, MATERIAL, 0); + c.texture(&Tex { min: 1, mag_linear: true, ..Tex::new(RGB) }); + c.blend(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + ramp_background(c); + // Texture coordinates by latitude and longitude. + c.depth_test(GL_LESS); + let (nlat, nlon) = (8usize, 12usize); + let pt = |c: &mut Ctx, i: usize, j: usize| -> V { + let th = -std::f32::consts::FRAC_PI_2 + std::f32::consts::PI * i as f32 / nlat as f32; + let ph = 2.0 * std::f32::consts::PI * j as f32 / nlon as f32; + let p = [th.cos() * ph.sin(), th.sin(), -th.cos() * ph.cos()]; + c.board.geom.normal = p.map(f32::to_bits); + c.board.geom.colour = WHITE.map(f32::to_bits); + let rgba = c.board.gl_light(&[p[0] * 0.8, p[1] * 0.8, p[2] * 0.8, 1.0]); + V { p: [p[0] * 0.8, p[1] * 0.8, p[2] * 0.8, 1.0], c: rgba, t: [j as f32 / nlon as f32 * 2.0, i as f32 / nlat as f32 * 2.0, 0.0, 1.0], n: p, back: None } + }; + for i in 0..nlat { + for j in 0..nlon { + let q = [pt(c, i, j), pt(c, i + 1, j), pt(c, i + 1, j + 1), pt(c, i, j + 1)]; + c.draw(GL_QUADS, &q); + } + } +} + +macro_rules! scenes { + ($($name:ident,)*) => { + /// Every scene, in the order the goldens list them. + pub const NAMES: &[&str] = &[$(stringify!($name)),*]; + + pub fn run_scene(name: &str, c: &mut Ctx) -> bool { + match name { + $(stringify!($name) => $name(c),)* + _ => return false, + } + true + } + + #[cfg(test)] + mod scene_tests { + $(#[test] fn $name() { super::super::check(stringify!($name)); })* + } + }; +} + +scenes! { + tri_flat, + tri_smooth, + tri_tiny_and_thin, + quad_shared_edges, + polygon_convex, + polygon_modes_line_point, + polygon_stipple, + polygon_smooth, + cull_and_front_face, + two_sided_colours, + polygon_offset, + lines_flat, + lines_smooth_colour, + lines_wide, + lines_smooth, + lines_stippled, + points, + points_smooth, + depth_less, + depth_lequal_greater, + depth_always_masked, + depth_equal_notequal, + depth_range_wide, + stencil_mark_then_masked, + stencil_ops, + stencil_depth_fail, + alpha_test, + blend_factor_grid, + blend_equations, + blend_constant_colour, + blend_triangles_over, + logic_op_and_colour_mask, + scissor, + clip_frustum_xy, + clip_frustum_z, + clip_w_behind_eye, + clip_user_plane, + clip_user_planes_rotated, + clip_user_plane_textured_smooth, + window_matrix_triangles, + window_matrix_rows_down, + window_matrix_clip_and_lines, + window_matrix_scaled, + projection_frustum, + projection_ortho_rotated, + perspective_correct_colour, + tex_format_rgba, + tex_format_rgb, + tex_format_luminance, + tex_format_luminance_alpha, + tex_format_alpha, + tex_format_intensity, + tex_env_replace_rgba, + tex_env_modulate_rgba, + tex_env_decal_rgba, + tex_env_decal_rgb, + tex_env_blend_rgba, + tex_env_blend_luminance, + tex_env_modulate_luminance_alpha, + tex_env_replace_alpha, + tex_nearest_repeat, + tex_clamp_to_edge_linear, + tex_mip_nearest_nearest, + tex_mip_linear_nearest, + tex_mip_nearest_linear, + tex_mip_linear_linear, + tex_detail_colour, + tex_detail_colour_modulate, + tex_with_fog_blend_depth, + fog_ramp_triangles, + fog_lines_and_points, + fog_over_blend, + light_directional, + light_positional_attenuated, + light_spot, + light_local_viewer_specular, + light_three_lights_colour_material, + light_unlit_emission_only, + light_lit_blended_textured, +} diff --git a/src/dev/odyssey/golden/vertices.rs b/src/dev/odyssey/golden/vertices.rs new file mode 100644 index 000000000..801d7f21f --- /dev/null +++ b/src/dev/odyssey/golden/vertices.rs @@ -0,0 +1,191 @@ +//! The vertex side on its own: where object-space points land. +//! +//! The scene frames pin what the vertex side does only as far as it shows in +//! pixels. This pins the numbers: a corpus of points through several +//! model-view, projection, viewport and window-matrix setups (eye position, +//! clip coordinates, window position and depth), and through the board's +//! lighting under several light setups (the vertex's front colour). The file +//! `testdata/golden/vertices.txt` holds them as the shortest decimal that +//! reads back exactly. The default check is exact; `VPRO_GOLDEN=tolerance` +//! compares numerically against the limits in the file's header, in window +//! pixels for x and y, in window depth, and in colour units, which is the +//! check for a move to 32-bit floats. + +use crate::dev::odyssey::consts::colour; +use crate::dev::odyssey::draw::gl_rules; +use crate::dev::odyssey::gl_context::{vtx, GlDrawState}; +use crate::dev::odyssey::raster_bench::Bench; +use super::scenes::{load_lights, Light, MATERIAL}; +use super::*; + +use super::raster_bench::Lcg; + +pub struct Row { + pub name: String, + pub kind: &'static str, + pub vals: Vec, +} + +fn rows() -> Vec { + let mut out = Vec::new(); + let ident = [1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]; + let (l, r, b, t, n, f) = (-0.6f32, 0.4f32, -0.5f32, 0.5f32, 1.0f32, 30.0f32); + let frustum = [2.0 * n / (r - l), 0.0, 0.0, 0.0, 0.0, 2.0 * n / (t - b), 0.0, 0.0, (r + l) / (r - l), (t + b) / (t - b), -(f + n) / (f - n), -1.0, 0.0, 0.0, -2.0 * f * n / (f - n), 0.0]; + let ortho = [0.5f32, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, -0.1, 0.0, -0.1, 0.2, -0.9, 1.0]; + let (s, k) = (0.6f32, 0.8f32); + let rot = [k, s, 0.0, 0.0, -s, k, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.7, -0.4, -6.0, 1.0]; + let tilt = [k, 0.0, -s, 0.0, 0.0, 1.0, 0.0, 0.0, s, 0.0, k, 0.0, 0.0, 0.3, -9.0, 1.0]; + let window = [60.0 * k, 60.0 * s, 0.0, 0.0, -60.0 * s, 60.0 * k, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 400.5, 300.25, 0.0, 1.0]; + // (name, modelview, projection, viewport, window matrix) + let setups: [(&str, [f32; 16], [f32; 16], [i32; 4], Option<[f32; 16]>); 6] = [ + ("identity", ident, ident, [0, 0, 128, 128], None), + ("ortho_rot", rot, ortho, [10, 20, 600, 480], None), + ("frustum_rot", rot, frustum, [0, 0, 1280, 1024], None), + ("frustum_tilt_vp", tilt, frustum, [-30, 15, 333, 257], None), + ("window_matrix", ident, ident, [0, 0, 1280, 1024], Some(window)), + ("window_matrix_rot", rot, ident, [0, 0, 1280, 1024], Some(window)), + ]; + let mut rng = Lcg(2024); + let points: Vec<[f32; 4]> = (0..40) + .map(|i| match i { + 0 => [0.0, 0.0, 0.0, 1.0], + 1 => [1.0, 1.0, 1.0, 1.0], + 2 => [-1.0, 0.5, -2.0, 1.0], + 3 => [0.3, -0.3, 5.0, 1.0], + _ => [rng.range(-3.0, 3.0), rng.range(-3.0, 3.0), rng.range(-6.0, 4.0), if i % 5 == 0 { rng.range(0.5, 2.0) } else { 1.0 }], + }) + .collect(); + for (name, mv, proj, vp, wm) in setups { + let mut st = GlDrawState { modelview: mv, projection: proj, viewport: vp, ..Default::default() }; + if let Some(w) = wm { + st.window_matrix_given = true; + st.window_matrix = w; + st.window_depth_scale = 0.75; + st.window_rows_down = name == "window_matrix_rot"; + } + for (i, p) in points.iter().enumerate() { + let mut vertex = [0.0f32; vtx::FLOATS]; + vertex[..4].copy_from_slice(p); + let vv = st.volume_from_draw_vertex(&vertex); + out.push(Row { name: format!("{name}/{i}"), kind: "eye", vals: vv.eye.iter().map(|&x| f64::from(x)).collect() }); + out.push(Row { name: format!("{name}/{i}"), kind: "clip", vals: vv.clip.iter().map(|&x| f64::from(x)).collect() }); + // A vertex at or behind the eye has no window position. + if vv.clip[3] > 0.0 { + let win = st.clip_to_window(&vv.clip, (1i32 << gl_rules().subpixel_bits) as f32).expect("a window position"); + out.push(Row { name: format!("{name}/{i}"), kind: "win", vals: win.iter().map(|&c| f64::from(c)).collect() }); + } + } + } + + // Lighting: the board's front colour at a spread of positions and normals. + let dir = |pos: [f32; 3], colour: [f32; 3]| Light { pos, directional: true, colour, atten: [1.0, 0.0, 0.0], spot: None }; + let setups: [(&str, Vec<(usize, Light)>, [f32; 3], bool, f32, u32); 6] = [ + ("directional", vec![(0, dir([0.5, 0.6, -0.62], [1.0, 0.9, 0.8]))], [0.2; 3], false, 20.0, 0), + ("positional", vec![(1, Light { pos: [1.5, 1.0, -2.0], colour: [1.0; 3], atten: [1.0, 0.3, 0.15], ..Default::default() })], [0.1; 3], false, 8.0, 0), + ("spot", vec![(0, Light { pos: [0.0, 1.5, -2.5], colour: [1.0, 1.0, 0.8], atten: [1.0, 0.0, 0.0], spot: Some(([0.0, -0.5, 0.85], 0.8, 4.0)), ..Default::default() })], [0.05; 3], false, 30.0, 0), + ("local_viewer", vec![(2, Light { pos: [-1.0, 1.0, -1.5], colour: [0.9, 0.9, 1.0], atten: [1.0, 0.0, 0.0], ..Default::default() })], [0.15; 3], true, 64.0, 0), + ("three", vec![(0, dir([1.0, 0.0, -1.0], [1.0, 0.1, 0.1])), (3, dir([-1.0, 0.5, -1.0], [0.1, 0.1, 1.0])), (7, Light { pos: [0.0, -2.0, -1.5], colour: [0.2, 1.0, 0.2], atten: [1.0, 0.1, 0.0], spot: Some(([0.0, 1.0, 0.5], 0.7, 2.0)), ..Default::default() })], [0.1; 3], false, 12.0, colour::MATERIAL_AMBIENT | colour::MATERIAL_DIFFUSE), + ("none", vec![], [0.3; 3], false, 1.0, colour::MATERIAL_EMISSION), + ]; + let mut board = Bench::empty().board; + for (name, lights, ambient, local, shininess, cm) in setups { + load_lights(&mut board, &lights, ambient, local, shininess, MATERIAL, cm); + board.geom.colour = [0.9f32, 0.7, 0.3, 0.75].map(f32::to_bits); + let mut rng = Lcg(99); + for i in 0..24 { + let mut nrm = [rng.range(-1.0, 1.0), rng.range(-1.0, 1.0), rng.range(-1.0, 1.0)]; + let len = (nrm[0] * nrm[0] + nrm[1] * nrm[1] + nrm[2] * nrm[2]).sqrt().max(1e-3); + nrm = nrm.map(|x| x / len); + board.geom.normal = nrm.map(f32::to_bits); + let obj = [rng.range(-1.0, 1.0), rng.range(-1.0, 1.0), rng.range(-1.0, 1.0), 1.0]; + let rgba = board.gl_light(&obj); + out.push(Row { name: format!("light_{name}/{i}"), kind: "rgba", vals: rgba.iter().map(|&c| c as f64).collect() }); + } + } + out +} + +const HEADER: &str = "# VPro vertex-side goldens (src/dev/odyssey/golden/vertices.rs).\n\ +# name kind values, as the shortest decimal that reads back exactly: kind eye is the eye\n\ +# position, clip the clip coordinates, win the window x, y and depth in [0, 1], rgba the\n\ +# lit front colour. Limits for VPRO_GOLDEN=tolerance:\n"; +const TOLERANCE_LINE: &str = "# tolerance eye 1e-5 clip 1e-5 win_xy 0.01 win_z 1e-5 rgba 0.004\n"; + +fn render() -> String { + let mut s = String::from(HEADER); + s += TOLERANCE_LINE; + for r in rows() { + s += &format!("{}\t{}", r.name, r.kind); + for v in &r.vals { + s += &format!("\t{v:e}"); + } + s.push('\n'); + } + s +} + +fn path() -> std::path::PathBuf { + data_dir().join("vertices.txt") +} + +pub fn bless() { + std::fs::write(path(), render()).unwrap(); +} + +struct Limits { + eye: f64, + clip: f64, + win_xy: f64, + win_z: f64, + rgba: f64, +} + +fn limits(text: &str) -> Limits { + let line = text.lines().find(|l| l.starts_with("# tolerance")).expect("a tolerance line"); + let f: Vec<&str> = line.split_whitespace().collect(); + let get = |k: &str| -> f64 { f.iter().position(|x| *x == k).and_then(|i| f.get(i + 1)).and_then(|v| v.parse().ok()).unwrap_or_else(|| panic!("tolerance {k}")) }; + Limits { eye: get("eye"), clip: get("clip"), win_xy: get("win_xy"), win_z: get("win_z"), rgba: get("rgba") } +} + +#[test] +fn the_vertex_side_gives_the_golden_numbers() { + let want = std::fs::read_to_string(path()).expect("vertices.txt: run scripts/vpro-goldens.sh bless"); + let got = render(); + if got == want { + return; + } + let lim = limits(&want); + let (mut worst, mut bad, mut moved) = ([0.0f64; 5], 0usize, 0usize); + let (wl, gl): (Vec<&str>, Vec<&str>) = (want.lines().filter(|l| !l.starts_with('#')).collect(), got.lines().filter(|l| !l.starts_with('#')).collect()); + assert_eq!(wl.len(), gl.len(), "the corpus changed size: {} rows against the golden {}", gl.len(), wl.len()); + for (w, g) in wl.iter().zip(&gl) { + if w == g { + continue; + } + moved += 1; + let (wf, gf): (Vec<&str>, Vec<&str>) = (w.split('\t').collect(), g.split('\t').collect()); + assert_eq!((wf[0], wf[1], wf.len()), (gf[0], gf[1], gf.len()), "row shape changed: {w:?} against {g:?}"); + for i in 2..wf.len() { + let (a, b): (f64, f64) = (wf[i].parse().unwrap(), gf[i].parse().unwrap()); + let err = (a - b).abs(); + let (slot, limit) = match (wf[1], i) { + ("eye", _) => (0, lim.eye), + ("clip", _) => (1, lim.clip), + ("win", 2 | 3) => (2, lim.win_xy), + ("win", _) => (3, lim.win_z), + _ => (4, lim.rgba), + }; + worst[slot] = worst[slot].max(err); + if err > limit { + bad += 1; + eprintln!("{}: {} value {} is {b:e}, the golden {a:e}", wf[0], wf[1], i - 2); + } + } + } + let msg = format!("{moved} rows moved; worst error eye {:e} clip {:e} win_xy {:e} win_z {:e} rgba {:e}; {bad} values outside the limits", worst[0], worst[1], worst[2], worst[3], worst[4]); + if bad == 0 && std::env::var("VPRO_GOLDEN").is_ok_and(|v| v == "tolerance") { + eprintln!("vertex goldens: {msg}"); + return; + } + panic!("vertex side differs from vertices.txt: {msg}"); +} diff --git a/src/dev/odyssey/interface.rs b/src/dev/odyssey/interface.rs new file mode 100644 index 000000000..139129035 --- /dev/null +++ b/src/dev/odyssey/interface.rs @@ -0,0 +1,499 @@ +//! The board's host interface: the registers a processor reaches without waiting. +//! +//! The board takes command-FIFO stores into a FIFO and draws behind them: +//! the processor goes on while a primitive is drawn. Meanwhile the driver +//! stores more commands and polls the registers that do not wait for the +//! drawing: identity, control, the interrupt bank, the routing, the swap +//! bank and the FIFO status (`access_waits`). Every other access +//! waits until the drawing behind the stores before it is done. +//! +//! So the board is two states. The host interface (`HostInterface`) holds +//! those registers and the interrupt state; its lock only serialises the +//! register file, and is held for a few instructions. The rest of the board +//! (`Board`) is the board's thread's (worker.rs), with no lock: a processor's +//! front access and its command stores never wait for a draw, and every +//! other access is answered busy until the stores before it and the raster +//! work they handed on are done (`BoardLocks::bus_read`), so the processor +//! touches the state only while the board's thread is idle. +//! +//! Ported from hw/misc/sgi-odyssey.c in jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use super::consts::{ + cfifo, flctl, intr, route, status0, swap, ODSY_CONTROL, ODSY_DISPLAYED_BUFFER, + ODSY_ERROR_ADDR_LOWER, ODSY_ERROR_ADDR_UPPER, ODSY_ERROR_COMMAND, ODSY_IDENTITY, + ODSY_INT_DEST_LOWER, ODSY_INT_DEST_UPPER, ODSY_INT_DEST_WIDGET_BITS, ODSY_STATUS, ODSY_STATUS0, + ODSY_SW_TOKEN, ODSY_XT_REQ_TIMEOUT, +}; +use super::fifo::CommandFifo; +use super::mmio::{access_waits, command_store, fifo_port}; +use super::registers::reg; +use parking_lot::Mutex; +use std::sync::Arc; + +pub(super) struct HostInterface { + /// Where interrupt packets go (the iris glue's HEART). + pub irq: Option>, + /// The widget number of HEART, the packets' destination. + pub heart_widget: u32, + /// ODSY_IDENTITY, composed from the revision at reset. + pub identity: u32, + + pub widget_control: u32, + pub xt_req_timeout: u32, + pub int_dest_upper: u32, + pub int_dest_lower: u32, + pub error_command: u32, + pub intr_flags: u32, + pub intr_enable: u32, + /// The flag source's transition state, for the packet edge. + pub interrupt_asserted: bool, + /// The flag level rose with no destination programmed and its set + /// packet waits for one (`HostInterface::deliver_held_flag`). + pub flag_held: bool, + pub sw_token: u32, + /// The flow-control target word (+0x1074), as last written. Stored, not + /// routed: nothing consumes it, so it is a front register that neither + /// waits for the command FIFO nor touches the board's state. + pub flctl_target: u32, + pub flctl_target_reported: bool, + pub route_dest_upper: u32, + pub route_dest_lower: u32, + pub route_vectors: u32, + pub swap_arm: u32, + pub swap_pending: u32, + pub swap_control: u32, + pub swap_control_companion: u32, + pub swap_set: u32, + pub swap_clear: u32, + pub displayed_buffer: u32, + /// A processor wrote ODSY_DISPLAYED_BUFFER: the scanout repaints at its + /// next update. + pub displayed_written: bool, + + /// The command FIFO (fifo.rs), whose level STATUS0 reports. + pub fifo: Arc, + /// The front accesses' own trace numbering; the board's accesses keep + /// the state's. + pub trace_sequence: u64, +} + +impl Default for HostInterface { + fn default() -> Self { + HostInterface { + irq: None, + heart_widget: 8, + identity: 0, + widget_control: 0, + xt_req_timeout: 0, + int_dest_upper: 0, + int_dest_lower: 0, + error_command: 0, + intr_flags: 0, + intr_enable: 0, + interrupt_asserted: false, + flag_held: false, + sw_token: 0, + flctl_target: 0, + flctl_target_reported: false, + route_dest_upper: 0, + route_dest_lower: 0, + route_vectors: 0, + swap_arm: 0, + swap_pending: 0, + swap_control: 0, + swap_control_companion: 0, + swap_set: 0, + swap_clear: 0, + displayed_buffer: 0, + displayed_written: false, + fifo: Default::default(), + trace_sequence: 0, + } + } +} + +// Interrupts. Every source is delivered from the front, which holds the +// routing and the flag bank. + +impl HostInterface { + /// A vertical retrace, which the scanout's clock runs at the mode's rate + /// (ScanoutReader::poll()). The driver issues a swap by writing + /// swap::ARM, a toggle mask with one bit per window whose displayed + /// buffer must flip at the next retrace, then waits for the retrace + /// interrupt. At each retrace the board first latches the pending flips + /// into the displayed-buffer bits and then raises the retrace source, + /// whose vector is byte 23:16 of route::VECTORS. The interrupt fires + /// at every retrace once the routing is programmed, not only when a swap + /// is armed; before the driver programs the routing nothing is + /// delivered. Whether the board can mask this source is unknown and it is + /// left unmasked. + /// + /// Approximation: the board's encoding of ODSY_DISPLAYED_BUFFER is + /// inferred from the driver's compare, and the latch's timing within the + /// retrace is unknown. The model latches at its retrace callback and + /// repaints the rows the new selection changes (ScanoutReader::flip_rows()) + /// in the same frame's update; a frame is never presented half from each + /// buffer. + /// + /// Whether a swap latched. The latch needs this lock alone. + pub fn retrace(&mut self) -> bool { + let flips = self.swap_arm; + + if self.swap_set != 0 || self.swap_clear != 0 { + self.displayed_buffer = (self.displayed_buffer | self.swap_set) & !self.swap_clear; + self.swap_set = 0; + self.swap_clear = 0; + } + if flips != 0 { + self.swap_arm = 0; + self.displayed_buffer ^= flips; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: swap latch: arm {:#x} displayed {:#x}", flips, self.displayed_buffer); + } + crate::pf_instant!(crate::perfetto::events::ODSY_RETRACE, self.displayed_buffer); + if self.routed() { + self.deliver_routed(route::VECTOR_RETRACE_SHIFT, "retrace", true); + } + flips != 0 + } +} + +impl HostInterface { + /// An interrupt packet into the widget-addressed fabric. + pub fn send_packet(&mut self, where_: u64, vector: u8, set: bool) { + self.send_interrupt(where_, vector, set); + } +} + +/// The fabric address of a widget/address pair: the widget in bits 51:48. +pub(super) fn odyssey_xio_compose(widget: u32, address: u64, fabric: &mut u64) -> bool { + sgi_xio_compose("sgi-odyssey", widget, address, fabric) +} + +impl HostInterface { + /// One routed source's interrupt. Every routed source shares one + /// destination (route::DEST_UPPER/_LOWER), distinct from + /// int_dest_upper/int_dest_lower, which the kernel driver gives its + /// crosstalk error handler; the sources differ only in their byte of + /// route::VECTORS. The vectors are read from the register, never + /// assumed. The flag source is a level the guest clears through + /// intr::CLEAR, so it sends a clearing packet as it falls + /// (HostInterface::update_irq()); the softsync, retrace and DMA-done handlers ack + /// nothing, and those sources send a single set pulse. + /// + /// MODEL CHOICE: bits 18:16 of + /// ROUTE_DEST_UPPER are the per-source enables of the DMA-done, retrace + /// and softsync sources, in the order of ODSY_ROUTE_ENABLE_*. A source + /// raised while its bit is clear sends nothing and is dropped. Which + /// bit enables which source is unknown, and what the board does with a + /// source raised while disabled is approximated; + /// IRIX sets and clears all three together, so it cannot tell. The flag + /// source has no bit here: intr::ENABLE_SET gates it. + pub fn deliver_routed(&mut self, shift: u32, what: &str, set: bool) { + let widget = (self.route_dest_upper >> route::DEST_WIDGET_SHIFT) & ((1u32 << ODSY_INT_DEST_WIDGET_BITS) - 1); + let addr = (((self.route_dest_upper & 0xffff) as u64) << 32) | self.route_dest_lower as u64; + let vector = self.route_vectors.checked_shr(shift).unwrap_or(0) as u8; + let mut where_ = 0u64; + + if !self.routed() { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: {} interrupt {} with no destination programmed; no packet sent", + what, + if set { "raised" } else { "lowered" } + ); + return; + } + let enable = match shift { + route::VECTOR_DMA_SHIFT => route::ENABLE_DMA, + route::VECTOR_RETRACE_SHIFT => route::ENABLE_RETRACE, + route::VECTOR_SOFTSYNC_SHIFT => route::ENABLE_SOFTSYNC, + _ => 0, + }; + if self.route_dest_upper & enable != enable { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: {} interrupt with its enable bit clear; dropped", what); + return; + } + if !odyssey_xio_compose(widget, addr, &mut where_) { + return; + } + crate::pf_instant!(crate::perfetto::events::ODSY_IRQ, (shift as u64) << 9 | (vector as u64) << 1 | set as u64); + self.send_packet(where_, vector, set); + } + + /// Whether the routing is programmed: both halves of the destination and + /// the vector table written. The kernel's interrupt set-up writes all + /// three, the vectors first and the upper half, which carries the target + /// widget, last, so a routing with one of them still zero is not yet the + /// one it means, and no source sends to it. Limitation: a routing with a + /// zero lower half or vector table never takes an interrupt. + pub fn routed(&self) -> bool { + self.route_dest_upper != 0 && self.route_dest_lower != 0 && self.route_vectors != 0 + } + + /// The softsync source reg::HOST_WRITE (0x0a5a) raises. + pub fn deliver_softsync(&mut self) { + self.deliver_routed(route::VECTOR_SOFTSYNC_SHIFT, "softsync", true); + } + + pub fn update_irq(&mut self) { + let level = (self.intr_flags & self.intr_enable) != 0; + + if level != self.interrupt_asserted { + self.interrupt_asserted = level; + self.flag_held = level && !self.routed(); + if self.flag_held { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: flag interrupt raised with no destination programmed; held until one is"); + } else { + // A level that falls while held was never sent, and with no + // routing programmed there is no packet for its clear. + self.deliver_routed(route::VECTOR_FLAG_SHIFT, "flag", level); + } + } + } + + /// A routing write: send the flag level that rose before the destination + /// was programmed, if it is still raised. MODEL CHOICE: the kernel enables + /// flow control, and so can raise low water, before its interrupt set-up + /// programs 0x1044, 0x104c and 0x1054, so the model holds the level + /// pending and sends it once the destination is written. What the board + /// does with such a flag (drop it, hold it, or send it to address zero) + /// is unknown. + fn deliver_held_flag(&mut self) { + if self.flag_held && self.routed() { + self.flag_held = false; + self.deliver_routed(route::VECTOR_FLAG_SHIFT, "held flag", true); + } + } + + /// The command FIFO's level: the words waiting for the board's work. + pub fn fifo_level(&self) -> u32 { + self.fifo.level() + } + + /// The FIFO's level went from 'before' to its current value. Falling below + /// the model's low-water threshold (cfifo::LOW_WATER, a model choice) + /// latches the low-water flag whatever the enable mask + /// says; it reaches the driver only while the source is enabled. + pub fn fifo_level_changed(&mut self, before: u32) { + if before >= cfifo::LOW_WATER && self.fifo_level() < cfifo::LOW_WATER { + self.intr_flags |= intr::FIFO_LOW_WATER; + self.update_irq(); + } + } +} + +// The front registers. + +/// Whether `addr` is a front register for this access: one of the +/// registers that do not wait for the board's work. The other accesses +/// that do not wait (the back end's read window, unmodeled offsets) read +/// the board's state directly (`BoardCell`). +pub(super) fn front_register(addr: u64, size: u32, write: bool) -> bool { + if fifo_port(addr) || reg(addr, size).is_none() || access_waits(addr, size, write) { + return false; + } + if !write { + // Reads of these two fall to the unmodeled-access report. + return !matches!(reg(addr, size), Some(intr::CLEAR | intr::ENABLE_CLEAR)); + } + true +} + +impl HostInterface { + pub fn read(&mut self, addr: u64, size: u32) -> u64 { + let mut value: u64 = 0; + let sequence = self.trace_sequence; + self.trace_sequence = self.trace_sequence.wrapping_add(1); + + match reg(addr, size) { + Some(ODSY_IDENTITY) => value = self.identity as u64, + Some(ODSY_CONTROL) => value = self.widget_control as u64, + Some(ODSY_XT_REQ_TIMEOUT) => value = self.xt_req_timeout as u64, + Some(ODSY_INT_DEST_UPPER) => value = self.int_dest_upper as u64, + Some(ODSY_INT_DEST_LOWER) => value = self.int_dest_lower as u64, + Some(ODSY_ERROR_COMMAND) => value = self.error_command as u64, + Some(ODSY_STATUS | ODSY_ERROR_ADDR_UPPER | ODSY_ERROR_ADDR_LOWER) => { + // No crosstalk error is ever posted, so the error class is clear. + } + Some(intr::FLAGS) => value = self.intr_flags as u64, + Some(intr::ENABLE_SET) => value = self.intr_enable as u64, + Some(ODSY_STATUS0) => { + // The waiting stores are its level (HostInterface::fifo_level()). + // Bit 15 and bit 17 use model-chosen thresholds, see cfifo::LOW_WATER + // and reg::FIFO_SETUP_FIRST. + let level = self.fifo_level(); + crate::pf_value!(self.fifo.pf_level, crate::perfetto::events::ODSY_CFIFO_LEVEL, level); + value = (status0::FIFO_ENABLED + | if self.fifo.high_water() { status0::FIFO_HIGH_WATER } else { 0 } + | if level != 0 { 0 } else { status0::FIFO_EMPTY } + | if level < cfifo::LOW_WATER { status0::FIFO_LOW_WATER } else { 0 }) as u64; + } + Some(ODSY_SW_TOKEN) => value = self.sw_token as u64, + Some(flctl::TARGET) => value = self.flctl_target as u64, + Some(swap::ARM) => value = self.swap_arm as u64, + Some(swap::PENDING) => value = self.swap_pending as u64, + Some(swap::CONTROL) => value = self.swap_control as u64, + Some(swap::CONTROL_COMPANION) => value = self.swap_control_companion as u64, + // The buffer the last latched swap put on screen (retrace()). + Some(ODSY_DISPLAYED_BUFFER) => value = self.displayed_buffer as u64, + Some(route::DEST_UPPER | route::DEST_LOWER | route::VECTORS) => { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: read of the write-only register at {:#x}", addr); + } + _ => unreachable!("odyssey_front_register() admits only these"), + } + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: front MMIO read seq {} offset {:#x} size {} value {:#x}", sequence, addr, size, value); + value + } + + pub fn write(&mut self, addr: u64, value: u64, size: u32) { + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: front MMIO write seq {} offset {:#x} size {} value {:#x}", self.trace_sequence, addr, size, value); + self.trace_sequence = self.trace_sequence.wrapping_add(1); + + match reg(addr, size) { + Some(ODSY_CONTROL) => self.widget_control = value as u32, + Some(ODSY_XT_REQ_TIMEOUT) => self.xt_req_timeout = value as u32, + Some(ODSY_INT_DEST_UPPER) => self.int_dest_upper = value as u32, + Some(ODSY_INT_DEST_LOWER) => self.int_dest_lower = value as u32, + // Written to acknowledge; the data itself is discarded. + Some(ODSY_ERROR_COMMAND) => self.error_command = 0, + Some(intr::CLEAR) => { + self.intr_flags &= !(value as u32); + self.update_irq(); + } + Some(intr::ENABLE_SET) => { + self.intr_enable |= value as u32; + self.update_irq(); + } + Some(intr::ENABLE_CLEAR) => { + self.intr_enable &= !(value as u32); + self.update_irq(); + } + Some(intr::FLAGS) => { + // The flags take the value written: an inferred model choice. + // A driver's flow control writes the low-water bit here before + // enabling that source, and its DMA diagnostic clears one flag + // by writing back the flags it read without it; only a + // replacing write serves the second. The board's paired + // registers (set enables, clear enables, clear flags) make a + // set-only write the competing reading. + self.intr_flags = value as u32; + self.update_irq(); + } + Some(ODSY_SW_TOKEN) => self.sw_token = value as u32, + Some(flctl::TARGET) => { + self.flctl_target = value as u32; + if !self.flctl_target_reported { + self.flctl_target_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: flow-control interrupt target {} and credit target {} stored", + (value as u32) >> flctl::INTR_TARGET_SHIFT, + ((value >> flctl::TARGET_SHIFT) as u32) & flctl::TARGET_MASK + ); + } + } + Some(swap::ARM) => { + // Queue flips for the next retrace. Writes accumulate: the + // driver can issue several windows or swap groups between two + // retraces, and its retrace handler re-writes one window's bit + // when a flip has not shown after two retraces. Whether the + // board ORs or replaces is unknown; ORing loses no flip. + self.swap_arm |= value as u32; + crate::pf_instant!(crate::perfetto::events::swap::ARM, value); + } + Some(swap::PENDING) => self.swap_pending = value as u32, + Some(swap::CONTROL) => { + // The windows that must show buffer 1 from the next retrace on. + // A later write to either register for the same window replaces + // this one before the retrace latches it. + self.swap_control = value as u32; + self.swap_set |= value as u32; + self.swap_clear &= !(value as u32); + } + Some(swap::CONTROL_COMPANION) => { + self.swap_control_companion = value as u32; + self.swap_clear |= value as u32; + self.swap_set &= !(value as u32); + } + Some(ODSY_DISPLAYED_BUFFER) => { + // Board-produced status; a guest write is not seen in the + // capture, but store it rather than drop it. + self.displayed_buffer = value as u32; + self.displayed_written = true; + } + Some(route::DEST_UPPER) => { + self.route_dest_upper = value as u32; + self.deliver_held_flag(); + } + Some(route::DEST_LOWER) => { + self.route_dest_lower = value as u32; + self.deliver_held_flag(); + } + Some(route::VECTORS) => { + self.route_vectors = value as u32; + self.deliver_held_flag(); + } + Some(ODSY_IDENTITY | ODSY_STATUS | ODSY_ERROR_ADDR_UPPER | ODSY_ERROR_ADDR_LOWER) => { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: write of {:#x} to the read-only register at {:#x}", value, addr); + } + _ => unreachable!("odyssey_front_register() admits only these"), + } + } +} + +/// A processor's read through the front alone: Some when it is a front +/// register, None when the access needs the board's state. +pub(super) fn front_mmio_read(f: &Mutex, addr: u64, size: u32) -> Option { + if !front_register(addr, size, false) { + return None; + } + Some(f.lock().read(addr, size)) +} + +/// A processor's store through the front alone: true when it went to a +/// front register or joined the command FIFO for the board's thread, false +/// when it needs the board's state. +pub(super) fn front_mmio_write(f: &Mutex, fifo: &CommandFifo, addr: u64, value: u64, size: u32) -> bool { + if front_register(addr, size, true) { + f.lock().write(addr, value, size); + return true; + } + // A queued store needs the board's thread to run it, and the queue to + // have room; otherwise the caller runs it. + command_store(addr, size) && fifo.running.load(std::sync::atomic::Ordering::SeqCst) && fifo.push(f, addr, value, size) +} + +/// The fabric packet's address bits; the widget sits above them. +pub(super) const XIO_PACKET_ADDRESS_BITS: u32 = 48; + +pub(super) const XIO_WIDGETS: u32 = 16; + +/// The fabric address of a widget/address pair: the widget in bits 51:48. +pub(super) fn sgi_xio_compose(who: &str, widget: u32, address: u64, packet: &mut u64) -> bool { + if widget >= XIO_WIDGETS || address >> XIO_PACKET_ADDRESS_BITS != 0 { + crate::dlog!(LogModule::Odyssey, "{who}: packet fabric cannot encode widget {widget} address {address:#x}"); + return false; + } + *packet = (widget as u64) << XIO_PACKET_ADDRESS_BITS | address; + true +} + +/// HEART's interrupt-packet register in its widget space. +const HEART_PACKET: u64 = 0x80; + +impl HostInterface { + /// The interrupt-modify write to the interrupt target. HEART takes it at + /// 0x80 of widget 8. + pub fn send_interrupt(&mut self, packet: u64, number: u8, set: bool) { + let widget = (packet >> XIO_PACKET_ADDRESS_BITS) as u32; + let address = packet & ((1u64 << XIO_PACKET_ADDRESS_BITS) - 1); + if widget != self.heart_widget || address != HEART_PACKET { + crate::dlog!(LogModule::Odyssey, "odyssey: interrupt packet to widget {widget} address {address:#x} has no receiver"); + return; + } + if let Some(irq) = &self.irq { + irq.set_line(number as u32, set); + } + } +} diff --git a/src/dev/odyssey/memory.rs b/src/dev/odyssey/memory.rs new file mode 100644 index 000000000..d82a3ed0c --- /dev/null +++ b/src/dev/odyssey/memory.rs @@ -0,0 +1,456 @@ +//! SGI graphics board memory and the planes over it. A plane is a typed +//! window (format, size, stride, packing, row origin) over one head's +//! memory; pixel (x, y) is at byte `base + y * stride + x * bpp`. Planes +//! may overlap. Out-of-plane loads read zero and out-of-plane stores are +//! dropped. +//! +//! Ported from include/hw/display/sgi/board-memory.h in jamesbraid/qemu-sgi-origami. + +use super::bits::{be16, be32, le16, le32}; +use crate::devlog::LogModule; +use std::cell::Cell; + +pub(super) const PLANES_MAX: usize = 32; + +/// How a plane's pixels are stored. +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] +pub(super) enum PlaneFormat { + /// 1 byte: colour index + #[default] + Ci8, + /// 2 bytes: 12-bit colour in a 16-bit word + Rgb12, + /// 4 bytes: 24-bit colour in a 32-bit word + Rgb24, + /// 4 bytes + Rgba8, + /// 4 bytes: 24-bit depth in a 32-bit word + Z24, + /// 1 byte: stencil + S8, + /// 1 byte: per-pixel tag + Tag, + /// 1 byte + Overlay8, + /// 4 bytes + Texture, + /// 4 bytes: a word the board interprets + Raw, +} + +/// The byte order and channel order the model writes a plane's words in. +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] +pub(super) enum Packing { + #[default] + LeRedLow, + LeRedHigh, + BeRedHigh, +} + +#[derive(Clone, Copy, Default, Debug)] +pub(super) struct Plane { + pub id: u32, // index in the board's plane table + pub base: u64, + pub len: u64, // bytes within the board memory + pub width: u32, + pub height: u32, + pub stride: u32, + pub format: PlaneFormat, + pub packing: Packing, // what the model writes, labelled + pub bottom_origin: bool, // plane row 0 is the bottom display line +} + +/// A head's board memory: a model-owned byte array, and the planes over it. +#[derive(Clone, Default)] +pub(super) struct BoardMemory { + pub size: u64, + pub bytes: Vec, + /// A fixed table, so a plane index stays valid for the memory's life. + pub planes: [Plane; PLANES_MAX], + pub nplanes: u32, +} + +/// Up to 16 inclusive rectangles, in plane coordinates. With n 0 the list +/// is no clip, unless empty is set: then it keeps no pixel. +pub(super) const CLIP_MAX: usize = 16; + +#[derive(Clone, Copy, Default, Debug)] +pub(super) struct ClipRect { + pub x0: i32, + pub y0: i32, + pub x1: i32, + pub y1: i32, +} + +#[derive(Clone, Copy, Default, Debug)] +pub(super) struct ClipList { + pub n: u32, + /// With n 0: true keeps nothing, false keeps everything. + pub empty: bool, + pub r: [ClipRect; CLIP_MAX], +} + +// The sixteen logic ops, in OpenGL's order (GL_CLEAR + op). +pub(super) const LOGIC_CLEAR: u32 = 0; +pub(super) const LOGIC_AND: u32 = 1; +pub(super) const LOGIC_AND_REVERSE: u32 = 2; +pub(super) const LOGIC_COPY: u32 = 3; +pub(super) const LOGIC_AND_INVERTED: u32 = 4; +pub(super) const LOGIC_NOOP: u32 = 5; +pub(super) const LOGIC_XOR: u32 = 6; +pub(super) const LOGIC_OR: u32 = 7; +pub(super) const LOGIC_NOR: u32 = 8; +pub(super) const LOGIC_EQUIV: u32 = 9; +pub(super) const LOGIC_INVERT: u32 = 10; +pub(super) const LOGIC_OR_REVERSE: u32 = 11; +pub(super) const LOGIC_COPY_INVERTED: u32 = 12; +pub(super) const LOGIC_OR_INVERTED: u32 = 13; +pub(super) const LOGIC_NAND: u32 = 14; + +impl BoardMemory { + /// A zeroed memory of size bytes, at most 4 GiB - 1. + pub fn init(&mut self, size: u64) { + // A board constant; the limit is the 32-bit offsets planes use. + assert!(size != 0 && size <= u32::MAX as u64); + *self = BoardMemory::default(); + self.size = size; + self.bytes = vec![0u8; size as usize]; + } +} + +pub(super) fn plane_bytes_per_pixel(format: PlaneFormat) -> u32 { + plane_bpp(format) +} + +impl BoardMemory { + /// Declare a plane. Its id is assigned here and returned; the rest of + /// *plane is copied. Fails when the plane does not fit the memory, its + /// rows do not fit its stride and length, or the table already holds + /// PLANES_MAX planes. + pub fn add_plane(&mut self, plane: &Plane) -> Result { + let bpp = plane_bytes_per_pixel(plane.format); + let row = plane.width as u64 * bpp as u64; + + if plane.width == 0 || plane.height == 0 { + return Err("plane has no pixels".to_string()); + } + if (plane.stride as u64) < row { + return Err(format!( + "plane stride {} is shorter than its {}-byte rows", + plane.stride, row + )); + } + let need = (plane.height as u64 - 1) * plane.stride as u64 + row; + if need > plane.len { + return Err(format!("plane needs {} bytes but has {}", need, plane.len)); + } + if plane.base > self.size || plane.len > self.size - plane.base { + return Err(format!( + "plane {:#x}+{:#x} is outside {:#x} bytes of board memory", + plane.base, plane.len, self.size + )); + } + + if self.nplanes as usize == PLANES_MAX { + return Err(format!("board memory already has {} planes", PLANES_MAX)); + } + let id = self.nplanes; + self.nplanes += 1; + let p = &mut self.planes[id as usize]; + *p = *plane; + p.id = id; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: plane added: plane {} base {:#x} len {:#x} {}x{} stride {} format {:?}", p.id, p.base, p.len, p.width, p.height, p.stride, p.format); + Ok(p.id) + } + /// The plane with this id, if there is one. + pub fn plane(&self, id: u32) -> Option { + (id < self.nplanes).then(|| self.planes[id as usize]) + } +} + +impl Plane { + #[inline] + pub fn contains(&self, x: i32, y: i32) -> bool { + x >= 0 && y >= 0 && (x as u32) < self.width && (y as u32) < self.height + } +} + +impl ClipList { + pub fn contains(&self, x: i32, y: i32) -> bool { + if self.n == 0 { + return !self.empty; + } + for i in 0..self.n.min(CLIP_MAX as u32) { + let r = &self.r[i as usize]; + + if x >= r.x0 && x <= r.x1 && y >= r.y0 && y <= r.y1 { + return true; + } + } + false + } +} +pub(super) fn logic_op(op: u8, src: u32, dst: u32) -> u32 { + let s = src; + let d = dst; + match (op & 0xf) as u32 { + LOGIC_CLEAR => 0, + LOGIC_AND => s & d, + LOGIC_AND_REVERSE => s & !d, + LOGIC_COPY => s, + LOGIC_AND_INVERTED => !s & d, + LOGIC_NOOP => d, + LOGIC_XOR => s ^ d, + LOGIC_OR => s | d, + LOGIC_NOR => !(s | d), + LOGIC_EQUIV => !(s ^ d), + LOGIC_INVERT => !d, + LOGIC_OR_REVERSE => s | !d, + LOGIC_COPY_INVERTED => !s, + LOGIC_OR_INVERTED => !s | d, + LOGIC_NAND => !(s & d), + _ => !0u32, // LOGIC_SET + } +} + +impl BoardMemory { + pub fn plane_load(&self, p: &Plane, x: i32, y: i32) -> u32 { + if !p.contains(x, y) { + return 0; + } + let bpp = plane_bpp(p.format); + self.pixel(p.offset(x, y, bpp), bpp).map_or(0, |b| p.get(bpp, b)) + } + pub fn plane_store(&mut self, p: &Plane, x: i32, y: i32, word: u32) { + if !p.contains(x, y) { + return; + } + let bpp = plane_bpp(p.format); + if let Some(b) = self.pixel_mut(p.offset(x, y, bpp), bpp) { + p.put(bpp, word, b); + } + } + + /// The `bpp` bytes at `at`, when they are inside the memory. + #[inline] + pub fn pixel(&self, at: u64, bpp: u32) -> Option<&[u8]> { + self.bytes.get(usize::try_from(at).ok()?..)?.get(..bpp as usize) + } + #[inline] + pub fn pixel_mut(&mut self, at: u64, bpp: u32) -> Option<&mut [u8]> { + self.bytes.get_mut(usize::try_from(at).ok()?..)?.get_mut(..bpp as usize) + } +} + +/// One plane word of `bpp` bytes, little-endian when `le` is set. +#[inline] +pub(super) fn plane_word_load(bytes: &[u8], bpp: u32, le: bool) -> u32 { + match bpp { + 1 => bytes[0] as u32, + 2 => { + if le { + le16(&bytes[..2]) as u32 + } else { + be16(&bytes[..2]) as u32 + } + } + _ => { + if le { + le32(&bytes[..4]) + } else { + be32(&bytes[..4]) + } + } + } +} + +#[inline] +pub(super) fn plane_word_store(bytes: &mut [u8], bpp: u32, le: bool, word: u32) { + match bpp { + 1 => bytes[0] = word as u8, + 2 => { + if le { + bytes[..2].copy_from_slice(&(word as u16).to_le_bytes()); + } else { + bytes[..2].copy_from_slice(&(word as u16).to_be_bytes()); + } + } + _ => { + if le { + bytes[..4].copy_from_slice(&word.to_le_bytes()); + } else { + bytes[..4].copy_from_slice(&word.to_be_bytes()); + } + } + } +} + +impl Plane { + /// A plane word to and from its stored bytes, in the plane's packing. + pub fn decode(&self, bytes: &[u8]) -> u32 { + self.get(plane_bpp(self.format), bytes) + } + pub fn encode(&self, word: u32, bytes: &mut [u8]) { + self.put(plane_bpp(self.format), word, bytes) + } +} + +// The per-pixel paths take a plane's pixel size once and reuse it. +#[inline] +pub(super) fn plane_bpp(format: PlaneFormat) -> u32 { + match format { + PlaneFormat::Ci8 | PlaneFormat::S8 | PlaneFormat::Tag | PlaneFormat::Overlay8 => 1, + PlaneFormat::Rgb12 => 2, + PlaneFormat::Rgb24 | PlaneFormat::Rgba8 | PlaneFormat::Z24 | PlaneFormat::Texture | PlaneFormat::Raw => 4, + } +} + +impl Plane { + pub fn is_le(&self) -> bool { + matches!(self.packing, Packing::LeRedLow | Packing::LeRedHigh) + } + + #[inline] + pub fn get(&self, bpp: u32, bytes: &[u8]) -> u32 { + plane_word_load(bytes, bpp, self.is_le()) + } + + #[inline] + pub fn put(&self, bpp: u32, word: u32, bytes: &mut [u8]) { + plane_word_store(bytes, bpp, self.is_le(), word) + } +} + +impl Plane { + pub fn offset(&self, x: i32, y: i32, bpp: u32) -> u64 { + self.base + .wrapping_add((y as u64).wrapping_mul(self.stride as u64)) + .wrapping_add((x as u64).wrapping_mul(bpp as u64)) + } +} + +/// A pixel array's rows as a draw borrows them. +pub(super) struct RowsMut<'a, T> { + /// Elements from the start of one row to the next. + stride: usize, + data: &'a mut [T], +} + +impl<'a, T> RowsMut<'a, T> { + pub fn new(data: &'a mut [T], stride: usize) -> Self { + RowsMut { stride, data } + } + + /// The rows as the draw writes them. + pub fn view(&mut self) -> Rows<'_, T> { + Rows { stride: self.stride, data: Cell::from_mut(&mut *self.data).as_slice_of_cells() } + } +} + +/// RowsMut as the draw writes it, through shared references. +pub(super) struct Rows<'a, T> { + stride: usize, + data: &'a [Cell], +} + +impl Rows<'_, T> { + /// The len elements at element `offset` of row y, when the array is + /// long enough. + #[inline] + pub fn get(&self, y: usize, offset: usize, len: usize) -> Option<&[Cell]> { + self.data.get(y * self.stride + offset..)?.get(..len) + } +} + +impl Rows<'_, u8> { + /// Plane p's word at (x, y), or 0 outside the plane or these rows. + #[inline] + pub fn plane_load(&self, p: &Plane, x: i32, y: i32) -> u32 { + if !p.contains(x, y) { + return 0; + } + let bpp = plane_bpp(p.format); + let Some(cells) = self.get(y as usize, x as usize * bpp as usize, bpp as usize) else { + return 0; + }; + let mut bytes = [0u8; 4]; + for (b, c) in bytes.iter_mut().zip(cells) { + *b = c.get(); + } + p.get(bpp, &bytes) + } + + /// Store plane p's word at (x, y), dropped outside the plane or these rows. + #[inline] + pub fn plane_store(&self, p: &Plane, x: i32, y: i32, word: u32) { + if !p.contains(x, y) { + return; + } + let bpp = plane_bpp(p.format); + let Some(cells) = self.get(y as usize, x as usize * bpp as usize, bpp as usize) else { + return; + }; + let mut bytes = [0u8; 4]; + p.put(bpp, word, &mut bytes); + for (c, b) in cells.iter().zip(bytes) { + c.set(b); + } + } + + /// The bytes of plane p's pixels x0 to x0 + n - 1 of row y, a 4-byte + /// plane, when every one of them is in the plane and these rows; None + /// otherwise. A row loop reaches the words through these with the tests + /// of plane_load() and plane_store() made once (word_load(), + /// word_store()). + #[inline] + pub fn plane_words(&self, p: &Plane, y: i32, x0: i32, n: usize) -> Option<&[Cell]> { + let x1 = (x0 as i64 + n as i64 - 1) as i32; + + if n == 0 || plane_bpp(p.format) != 4 || !p.contains(x0, y) || !p.contains(x1, y) { + return None; + } + self.get(y as usize, x0 as usize * 4, n * 4) + } +} + +/// Word i of a 4-byte plane's row of bytes (Rows::plane_words()), little-endian +/// when le is set, as Rows::plane_load() reads it. +#[inline(always)] +pub(super) fn word_load(bytes: &[Cell], i: usize, le: bool) -> u32 { + let c = &bytes[4 * i..4 * i + 4]; + let b = [c[0].get(), c[1].get(), c[2].get(), c[3].get()]; + + if le { u32::from_le_bytes(b) } else { u32::from_be_bytes(b) } +} + +/// Store word i of a 4-byte plane's row of bytes, as Rows::plane_store() does. +#[inline(always)] +pub(super) fn word_store(bytes: &[Cell], i: usize, le: bool, word: u32) { + let c = &bytes[4 * i..4 * i + 4]; + let b = if le { word.to_le_bytes() } else { word.to_be_bytes() }; + + for k in 0..4 { + c[k].set(b[k]); + } +} + +/// Store word into every word of a 4-byte plane's bytes (a whole number of +/// words), as word_store() does each. +#[inline] +pub(super) fn words_fill(bytes: &[Cell], le: bool, word: u32) { + let b = if le { word.to_le_bytes() } else { word.to_be_bytes() }; + + for c in bytes.chunks_exact(4) { + for k in 0..4 { + c[k].set(b[k]); + } + } +} + +impl BoardMemory { + /// Plane p's rows, which start at its base. + pub fn rows(&mut self, p: &Plane) -> RowsMut<'_, u8> { + let end = (p.base + p.len).min(self.size) as usize; + RowsMut::new(&mut self.bytes[p.base as usize..end], p.stride as usize) + } +} diff --git a/src/dev/odyssey/mmio.rs b/src/dev/odyssey/mmio.rs new file mode 100644 index 000000000..59e3c7708 --- /dev/null +++ b/src/dev/odyssey/mmio.rs @@ -0,0 +1,583 @@ +//! The MMIO aperture: write dispatch, which accesses wait for drawing, and the +//! locks the board is reached through. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use parking_lot::Mutex; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use super::board::Board; +use super::consts::{ + backend, cmd, ctx, cursor, flctl, host, i2c, intr, mem, nic, route, stats, swap, ODSY_BUZZ_1, + ODSY_BUZZ_2, ODSY_BUZZ_3, ODSY_BUZZ_4, ODSY_BUZZ_5, ODSY_BUZZ_6, ODSY_CONTROL, + ODSY_DISPLAYED_BUFFER, ODSY_ERROR_ADDR_LOWER, ODSY_ERROR_ADDR_UPPER, ODSY_ERROR_COMMAND, + ODSY_IDENTITY, ODSY_INT_DEST_LOWER, ODSY_INT_DEST_UPPER, ODSY_RESET_CONTROL, ODSY_STATUS, + ODSY_STATUS0, ODSY_SW_TOKEN, ODSY_USER_CMD_FIFO, ODSY_XT_REQ_TIMEOUT, +}; +use super::fifo::CommandFifo; +use super::interface::{front_mmio_read, front_mmio_write, front_register, HostInterface}; +use super::raster_unit::RasterQueue; +use super::reader::DisplayShared; +use super::registers::reg; +impl Board { + pub fn write_dispatch(&mut self, addr: u64, value: u64, size: u32) { + let sequence = self.trace_sequence.fetch_add(1, Ordering::Relaxed); + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: MMIO write seq {} offset {:#x} size {} value {:#x}", sequence, addr, size, value); + + if addr == backend::SELECT && (size == 8 || size == 4) { + self.backend_write(value, size); + return; + } + if addr == ctx::SWITCH_PORT && size == 8 { + self.ctx_switch(value); + return; + } + if addr == cmd::FIFO { + self.selftest_track(cmd::FIFO, size, value); + self.cmd.from_kernel = true; + self.cmd_fifo_write(value, size); + return; + } + if addr == ODSY_USER_CMD_FIFO { + // The user-mapped command FIFO has the kernel port's grammar but + // never carries the self-test preamble, so it skips the tracker. + self.cmd.from_kernel = false; + self.cmd_fifo_write(value, size); + return; + } + + // Registers that do not wait for the board's work live in the front + // (interface.rs). + if front_register(addr, size, true) { + self.front.lock().write(addr, value, size); + return; + } + + match reg(addr, size) { + Some(ODSY_RESET_CONTROL) => { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: write of {:#x} to the reset control register accepted; its effect is unknown", + value + ); + } + Some(mem::CONTROL) => self.memory_control(value as u32), + Some(flctl::CONFIG) => self.widget.flctl_config = value as u32, + Some(ODSY_BUZZ_1) => self.widget.buzz[0] = value as u32, + Some(ODSY_BUZZ_2) => self.widget.buzz[1] = value as u32, + Some(ODSY_BUZZ_3) => self.widget.buzz[2] = value as u32, + Some(ODSY_BUZZ_4) => self.widget.buzz[3] = value as u32, + Some(ODSY_BUZZ_5) => self.widget.buzz[4] = value as u32, + Some(ODSY_BUZZ_6) => self.widget.buzz[5] = value as u32, + Some(nic::PROM | nic::KERNEL) => self.nic_slot(value as u32), + Some(host::WRITE_ADDR_UPPER) => { + self.query_cancel(); + self.widget.host_write_addr_upper = value as u32; + } + Some(host::WRITE_ADDR_LOWER) => { + self.query_cancel(); + self.widget.host_write_addr_lower = value as u32; + } + Some(stats::ADDR_UPPER | stats::ADDR_LOWER | stats::CONTROL) => { + if addr == stats::ADDR_UPPER { + self.widget.stats_addr_upper = value as u32; + } else if addr == stats::ADDR_LOWER { + self.widget.stats_addr_lower = value as u32; + } else { + self.widget.stats_control = value as u32; + } + if value != 0 && !self.diag.stats_reported { + self.diag.stats_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: statistics region word {:#x} at {:#x} stored; the board's writes into the region are not modeled", + value as u32, + addr + ); + } + } + Some(ctx::SHADOW_ADDR_UPPER) => { + self.query_cancel(); + self.widget.ctx_shadow_addr_upper = value as u32; + } + Some(ctx::SHADOW_ADDR_LOWER) => { + self.query_cancel(); + self.widget.ctx_shadow_addr_lower = value as u32; + } + Some(mem::CONFIG + | i2c::MAIN_CMD_READ + | i2c::MAIN_STATUS_READ + | i2c::OPT_CMD_READ + | i2c::OPT_STATUS_READ + | backend::VERSION) => { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: write of {:#x} to the read-only register at {:#x}", value, addr); + } + _ => self.unimplemented(addr, size, true), + } + } + + // Command work after the store. + // + // The board draws a primitive after the store that completes it has + // finished: the processor goes on, and the driver learns of completion from + // a host-write fence, the context-switch word in guest memory, or an + // interrupt. A model that draws inside the store keeps the processor from + // its scheduler tick for as long as the draw takes, and the guest does not + // make up lost ticks. + // + // So every store that joins the command stream waits in a queue, in + // order, and the board's thread (worker.rs) runs it. Accesses that must + // see the drawing are answered busy until the queue is empty + // (BoardLocks::bus_read) or, with no thread running (unit tests), run what + // is queued first (Board::work_drain). + +} + +pub(super) fn fifo_port(addr: u64) -> bool { + addr == ODSY_USER_CMD_FIFO || addr == cmd::FIFO +} + +/// The stores that join the command stream: the two command-FIFO ports, and +/// the kernel's context switch, the shadow address it names and the switch +/// store itself. +pub(super) fn command_store(addr: u64, size: u32) -> bool { + fifo_port(addr) + || (addr == ctx::SWITCH_PORT && size == 8) + || matches!(reg(addr, size), Some(ctx::SHADOW_ADDR_UPPER | ctx::SHADOW_ADDR_LOWER)) +} + +/// A read of the level word `0x106c`, which never waits: it is a plain load +/// of live levels. The make-current and detach readers discard it, and the +/// drain waits poll bits 15:7 until they read 0, which the live level +/// does once the board has run the queue. +pub(super) fn level_word(addr: u64, size: u32) -> bool { + reg(addr, size) == Some(mem::CONFIG) +} + +/// Accesses that need not wait for the queued stores, and act on the board +/// as it stands. Inferred from what a driver does while the board works, +/// not measured on a board; every other access waits. +pub(super) fn access_waits(addr: u64, size: u32, write: bool) -> bool { + let cursor = backend::READ_BASE + ((cursor::BASE as u64) << 3); + + if fifo_port(addr) { + return !write; + } + let Some(r) = reg(addr, size) else { + // Outside the modeled registers only the back-end and + // context-switch ports and the cursor do anything; the rest is + // logged and answers zero, so it need not wait. + if write { + return addr == backend::SELECT || addr == ctx::SWITCH_PORT; + } + return false; + }; + if !write && addr >= cursor && addr < cursor + ((cursor::ENTRIES as u64) << 3) { + return false; + } + !matches!( + r, + ODSY_IDENTITY + | ODSY_STATUS + | ODSY_ERROR_ADDR_UPPER + | ODSY_ERROR_ADDR_LOWER + | ODSY_CONTROL + | ODSY_XT_REQ_TIMEOUT + | ODSY_INT_DEST_UPPER + | ODSY_INT_DEST_LOWER + | ODSY_ERROR_COMMAND + | ODSY_STATUS0 + | intr::FLAGS + | intr::CLEAR + | intr::ENABLE_SET + | intr::ENABLE_CLEAR + | route::DEST_UPPER + | route::DEST_LOWER + | route::VECTORS + | ODSY_SW_TOKEN + // The word is stored, not routed, with no wait; making the processor wait on it modelled nothing. + | flctl::TARGET + | swap::ARM + | swap::PENDING + | swap::CONTROL + | swap::CONTROL_COMPANION + | ODSY_DISPLAYED_BUFFER + ) +} + +impl Board { + /// A read of the board's state. + pub fn mmio_read(&mut self, addr: u64, size: u32) -> u64 { + if access_waits(addr, size, false) && !level_word(addr, size) { + // The stores before it and the raster work they handed on. + if self.work_pending() { + self.work_drain(addr); + } + self.raster_wait(); + } + self.read(addr, size) + } +} + +impl Board { + /// A store to the board's state. + pub fn mmio_write(&mut self, addr: u64, value: u64, size: u32) { + let fifo = self.fifo.clone(); + let queued = command_store(addr, size) && fifo.queues() && fifo.push(&self.front, addr, value, size); + if queued { + // With no board thread running the storing caller runs it. + if !fifo.running.load(std::sync::atomic::Ordering::SeqCst) && !fifo.held() { + self.work_drain(addr); + } + } else if command_store(addr, size) { + // The queue is full and nobody waits for room (a caller other + // than the bus): run what waits and then this store, in order. + self.work_drain(addr); + self.write_dispatch(addr, value, size); + } else { + if access_waits(addr, size, true) && self.work_pending() { + self.work_drain(addr); + } + self.write_dispatch(addr, value, size); + } + } +} + +/// The board's state, which the front end's thread owns. There is no lock +/// between the processor and the front end: the board's units reach their +/// memory and registers at the same time, and software orders them. Here +/// that order is the command FIFO (stores run in arrival order on the +/// board's thread), the waiting accesses (a processor's read or store that +/// must see the drawing is answered busy until the FIFO and the raster queue +/// are empty, so the board's thread is idle when the processor touches the +/// state), and the host interface's registers, which the processor reaches +/// without waiting (`HostInterface`). +/// +/// Processors are serialised against each other by `BoardLocks::submit`, as +/// MGRAS's producers are by its `submit` lock: a processor holds it across +/// its busy check and its access, and across a command push, so a second +/// processor's push cannot wake the board's thread under a first +/// processor's access. The board's thread and the raster thread never take +/// it, so it adds no wait for drawing. +/// +/// A processor access that does not wait and reaches the board (the level +/// word, the back-end read window, an unmodelled offset) still borrows the +/// whole board while the board's thread may run: it reads words the thread +/// writes only in whole stores, or atomics, and shows the old or the new +/// value. That is the other boards' tolerated racy read, not a lock. +pub(super) struct BoardCell(std::cell::UnsafeCell>); + +// SAFETY: see the type's comment: the board's thread owns the state while it +// runs stores; a processor touches it holding `submit` after the waiting +// accesses have drained the FIFO and the raster queue, with the machine +// stopped, or for the whole-word reads above. This is the other boards' +// contract between a CPU thread and an engine's FIFO. +unsafe impl Sync for BoardCell {} +unsafe impl Send for BoardCell {} + +impl BoardCell { + pub fn new(b: Box) -> Self { + BoardCell(std::cell::UnsafeCell::new(b)) + } + + /// The board, for the thread that may touch it now: the board's thread + /// running stores, a processor through `BoardLocks::board()`, or the + /// only thread using the board (machine stopped, tests). Dereferencing + /// it is the caller's claim to be that thread. + pub(super) fn get(&self) -> *mut Board { + // The box never moves, so the pointer is the board's for its life. + // SAFETY: a place, not a reference: nothing is borrowed here. + unsafe { std::ptr::addr_of_mut!(**self.0.get()) } + } +} + +/// The board for a processor, with the other processors held off +/// (`BoardLocks::submit`), as MGRAS's `FrontGuard` holds its front lock. +pub(super) struct BoardGuard<'a> { + _cpu: crate::locks::SpinGuard<'a, ()>, + board: &'a mut Board, +} + +impl std::ops::Deref for BoardGuard<'_> { + type Target = Board; + fn deref(&self) -> &Board { self.board } +} + +impl std::ops::DerefMut for BoardGuard<'_> { + fn deref_mut(&mut self) -> &mut Board { self.board } +} + +/// What a processor's access goes through: the host interface's lock, the +/// board's state, and the queues behind them. +#[derive(Clone)] +pub(super) struct BoardLocks { + pub(super) board: Arc, + /// Serialises the processors' accesses to the board against each other + /// (see `BoardCell`). Never held by the board's thread or the raster + /// thread. + submit: Arc>, + pub front: Arc>, + pub fifo: Arc, + pub raster: Arc, + pub refusals: Arc, + /// What the display thread reads of the board. + pub shared: Arc, +} + +/// Why the bus answered busy. +#[derive(Clone, Copy)] +pub(super) enum Refusal { + /// A read behind command stores still in the FIFO. + ReadFifo, + /// A read behind raster work still queued. + ReadRaster, + /// A command store with the host ring full. + StoreFull, + /// A store that waits for the FIFO to drain. + StoreWaits, +} + +impl Refusal { + const NAMES: [&'static str; 4] = ["read-fifo-pending", "read-raster-pending", "store-ring-full", "store-waits-fifo"]; +} + +const REFUSAL_SLOTS: usize = 512; + +/// Standing counters of the accesses the bus answered busy, by reason and +/// by the aperture offset and width: lock-free, so the processor's threads +/// count without waiting. Read by the monitor's `vpro refusals`. The table +/// is open addressed; a key that finds it full is counted in `lost`. +pub(super) struct RefusalStats { + keys: [AtomicU64; REFUSAL_SLOTS], + counts: [AtomicU64; REFUSAL_SLOTS], + lost: AtomicU64, +} + +impl RefusalStats { + pub fn new() -> Self { + Self { keys: std::array::from_fn(|_| AtomicU64::new(0)), counts: std::array::from_fn(|_| AtomicU64::new(0)), lost: AtomicU64::new(0) } + } + + /// Count one refusal. The key is the reason, width and offset, plus one + /// so that zero means an empty slot. + pub fn note(&self, why: Refusal, addr: u64, size: u32) { + let key = (((why as u64) << 56) | ((size as u64) << 48) | addr) + 1; + let mut i = (key.wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 40) as usize % REFUSAL_SLOTS; + for _ in 0..REFUSAL_SLOTS { + let k = self.keys[i].load(Ordering::Relaxed); + if k == key || (k == 0 && self.keys[i].compare_exchange(0, key, Ordering::Relaxed, Ordering::Relaxed).map_or_else(|c| c == key, |_| true)) { + self.counts[i].fetch_add(1, Ordering::Relaxed); + return; + } + i = (i + 1) % REFUSAL_SLOTS; + } + self.lost.fetch_add(1, Ordering::Relaxed); + } + + pub fn reset(&self) { + for c in &self.counts { + c.store(0, Ordering::Relaxed); + } + self.lost.store(0, Ordering::Relaxed); + } + + /// Totals by reason, then each offset by count, largest first. + pub fn report(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> { + let mut rows: Vec<(u64, u64)> = (0..REFUSAL_SLOTS) + .filter_map(|i| { + let (k, c) = (self.keys[i].load(Ordering::Relaxed), self.counts[i].load(Ordering::Relaxed)); + (k != 0 && c != 0).then(|| (k - 1, c)) + }) + .collect(); + rows.sort_by(|a, b| b.1.cmp(&a.1)); + let mut totals = [0u64; Refusal::NAMES.len()]; + for (k, c) in &rows { + totals[(k >> 56) as usize] += c; + } + for (name, t) in Refusal::NAMES.iter().zip(totals) { + writeln!(w, "reason {name} {t}")?; + } + writeln!(w, "lost {}", self.lost.load(Ordering::Relaxed))?; + for (k, c) in rows { + writeln!(w, "{} offset {:#x} size {} count {}", Refusal::NAMES[(k >> 56) as usize], k & 0xffff_ffff_ffff, (k >> 48) & 0xff, c)?; + } + Ok(()) + } +} + +impl Default for RefusalStats { + fn default() -> Self { + Self::new() + } +} + +impl BoardLocks { + /// The board for a processor holding `submit`, which `cpu` proves. + /// + /// # Safety + /// The FIFO and the raster queue are empty (the waiting accesses), or + /// the board's thread is stopped, or the access is a whole-word read + /// `BoardCell` describes. + unsafe fn board<'a>(&'a self, cpu: crate::locks::SpinGuard<'a, ()>) -> BoardGuard<'a> { + // SAFETY: the caller's contract; `cpu` keeps the other processors out. + BoardGuard { _cpu: cpu, board: unsafe { &mut *self.board.get() } } + } + + pub fn new(s: Box) -> Self { + let (front, fifo, raster, shared) = (s.front.clone(), s.fifo.clone(), s.raster.clone(), s.scanout.shared.clone()); + BoardLocks { + board: Arc::new(BoardCell::new(s)), + submit: Arc::new(crate::locks::SpinLock::new(())), + front, + fifo, + raster, + refusals: Arc::new(RefusalStats::new()), + shared, + } + } + + /// A read for a caller other than the bus (tests, the monitor): through the + /// front alone when it can, else it runs what waits and reads the state. + /// With the board's thread running, call `bus_read` and retry instead. + pub fn read(&self, addr: u64, size: u32) -> u64 { + match front_mmio_read(&self.front, addr, size) { + Some(v) => v, + None => { + // SAFETY: the caller owns the board with its thread stopped, + // or accepts the race of a whole-word read. + unsafe { self.board(self.submit.lock()) }.mmio_read(addr, size) + } + } + } + + /// A processor's read as the bus takes it. None, for the bus to answer + /// busy, while the work the read must see is still queued on the + /// board's threads: the command stores before it and the raster work + /// they handed on. The level word never waits. The processor retries + /// the load, taking its interrupts meanwhile, while the board's threads + /// run that work, as IMPACT's waiting reads do; the read then finds + /// nothing to drain. Without the board's thread the read drains the + /// queue itself. + pub fn bus_read(&self, addr: u64, size: u32) -> Option { + if let Some(v) = front_mmio_read(&self.front, addr, size) { + return Some(v); + } + // From the busy check to the end of the access no other processor + // pushes a store that would wake the board's thread under it. + let cpu = self.submit.lock(); + if access_waits(addr, size, false) && !level_word(addr, size) && self.fifo.running.load(std::sync::atomic::Ordering::SeqCst) { + if self.fifo.pending() { + self.fifo.hurry(); + self.refusals.note(Refusal::ReadFifo, addr, size); + return None; + } + if self.raster.pending() != 0 { + self.refusals.note(Refusal::ReadRaster, addr, size); + return None; + } + } + // SAFETY: the FIFO and the raster queue are empty, or the access is + // a whole-word read. + Some(unsafe { self.board(cpu) }.mmio_read(addr, size)) + } + + /// A processor's store as the bus takes it. False, for the bus to + /// answer busy, when it waits for queued stores the board's thread will + /// run, or when it is a command store and the host ring is full while + /// the board's thread runs: the processor retries the store, taking its + /// interrupts meanwhile, while the board's thread makes room. + /// + /// A store that waits is held back for the command FIFO only, not the + /// raster queue: the raster unit never shared a lock with the board's + /// state, and a store that touches the planes waits for the raster queue + /// itself (`Board::raster_wait`). A read, which reads the planes' results, + /// checks both. + pub fn bus_write(&self, addr: u64, value: u64, size: u32) -> bool { + if front_register(addr, size, true) { + // The host interface's registers: no board state, no wait. + self.write(addr, value, size); + return true; + } + let cpu = self.submit.lock(); + // Classified once: a command store goes straight to the FIFO. + if command_store(addr, size) { + if self.fifo.full_for(size) { + self.fifo.hurry(); + self.refusals.note(Refusal::StoreFull, addr, size); + return false; + } + self.write_command(cpu, addr, value, size); + return true; + } + // A store that waits for the work, as a waiting read does. + if access_waits(addr, size, true) && self.fifo.running.load(std::sync::atomic::Ordering::SeqCst) && self.fifo.pending() { + self.fifo.hurry(); + self.refusals.note(Refusal::StoreWaits, addr, size); + return false; + } + self.write_board(cpu, addr, value, size); + true + } + + /// A command store the caller has classified, holding `submit`: it joins + /// the FIFO for the board's thread, else, with no thread, runs here. + fn write_command(&self, cpu: crate::locks::SpinGuard<'_, ()>, addr: u64, value: u64, size: u32) { + while self.fifo.running.load(std::sync::atomic::Ordering::SeqCst) { + // The ring refuses a push that finds the last place taken between + // the room check and the push: the board's thread owns the state, + // so the store goes in behind it rather than around it. + if self.fifo.push(&self.front, addr, value, size) { + return; + } + std::thread::yield_now(); + } + // SAFETY: no board thread is running. + unsafe { self.board(cpu) }.mmio_write(addr, value, size); + } + + /// A store to the board's state that is not a command store, holding + /// `submit`. + fn write_board(&self, cpu: crate::locks::SpinGuard<'_, ()>, addr: u64, value: u64, size: u32) { + // SAFETY: the waiting accesses have drained the FIFO, or the board's + // thread is stopped. + unsafe { self.board(cpu) }.mmio_write(addr, value, size); + } + + /// A store for a caller other than the bus: through the front alone when it + /// can, a command store into the FIFO, else it runs what waits and then + /// the store. With the board's thread running, call `bus_write` and retry + /// for a store that waits. + pub fn write(&self, addr: u64, value: u64, size: u32) { + if front_register(addr, size, true) { + front_mmio_write(&self.front, &self.fifo, addr, value, size); + return; + } + let cpu = self.submit.lock(); + if command_store(addr, size) { + return self.write_command(cpu, addr, value, size); + } + self.write_board(cpu, addr, value, size); + } +} + +#[cfg(test)] +impl BoardLocks { + /// The board, for a test that is the only thread using it, or whose + /// board thread is idle. + #[allow(clippy::mut_from_ref)] + pub(super) fn test_board(&self) -> &mut Board { + // SAFETY: the test's contract. + unsafe { &mut *self.board.get() } + } +} + +#[cfg(test)] +impl BoardLocks { + /// Hold the processors' lock, as a processor in the middle of an access + /// does. + pub(super) fn test_submit(&self) -> crate::locks::SpinGuard<'_, ()> { + self.submit.lock() + } +} diff --git a/src/dev/odyssey/mod.rs b/src/dev/odyssey/mod.rs new file mode 100644 index 000000000..c7edb4bf6 --- /dev/null +++ b/src/dev/odyssey/mod.rs @@ -0,0 +1,393 @@ +//! The SGI VPro (Odyssey) graphics board in the Octane's graphics port. +//! +//! The board is a crosstalk widget. The processor stores commands into a +//! command FIFO; the board decodes them, draws integer and GL primitives +//! into its own memory, moves pixels to and from guest memory by DMA, and +//! scans the colour planes out to the display. Four threads reach the +//! board: the processor's widget accesses; the board's own thread +//! (worker.rs), BUZZ's front end, which runs the queued command-FIFO stores; +//! BUZZ's raster unit (raster_unit.rs), which draws what the front end sets +//! up; and the display thread, the back end, which counts the retrace and +//! copies the scanout's frame to the window 60 times a second. +//! interface.rs describes how the first two share the board's state. +//! Interrupt packets go to HEART, the only interrupt target on the Octane: +//! a packet to widget 8 sets or clears the HEART pending bit of its vector. +//! +//! - `board`: the board's state, construction and reset. +//! - `interface`: the host interface: the registers that do not wait for +//! drawing, interrupts and the queue of command stores. +//! - `mmio`: register accesses, and which of them wait for the drawing. +//! - `fifo`: the command FIFO between the processor and the board's thread. +//! - `worker`: the board's thread, which runs the queued stores. +//! - `raster_unit`: the raster unit's thread and the queue that feeds it. +//! - `registers`: identity, the board NIC, the cursor and the back end's +//! register file. +//! - `command`: the command-FIFO interpreter. +//! - `context`: graphics context save and switch. +//! - `draw`: buffers, integer drawing and the image path. +//! - `objects`: off-screen objects in board memory. +//! - `dma`: pixel DMA, queries and fences. +//! - `pixel_write`: the library's image-write codes and their pixels. +//! - `gl`, `geometry`: GL fragment state and texturing; the geometry +//! program, lighting and the GL primitives. +//! - `gl_context`: the GL state machine the command stream drives. +//! - `raster`: the scalar rasteriser's rules, fragment path and draw entry; +//! `raster_triangle`, `raster_line`, `raster_pixels`: triangles and +//! polygons, lines and points, pixel rectangles, bitmaps, clears and +//! raster ops; `raster_texture`: texture sampling; `raster_fixed_texture`: +//! the fixed-point fast path for textured triangles; `raster_fill`: the row +//! loops filled triangles take, specialised by draw state and planes; +//! `raster_bench`: a fill-rate benchmark (tests only). +//! - `memory`, `scanout`, `reader`, `display`: board memory and planes; the +//! marks writers make and the display thread's own reader, with the scanned +//! frame and retrace; what reaches the window. +//! - `pixel_dma`: DMA between guest memory and planes. +//! - `depth_word`: the board's 24-bit float depth word and its offset. +//! - `consts`: register, command and field constants. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code +//! (hw/display/sgi/) in jamesbraid/qemu-sgi-origami. + +// The rasteriser writes its range tests as !(x > 0.0) and the like so that +// a NaN fails them. +#![allow(clippy::neg_cmp_op_on_partial_ord, reason = "a NaN must fail these tests")] + +mod bits; +mod board; +mod command; +mod consts; +mod context; +mod depth_word; +mod display; +#[cfg(test)] +mod display_bench; +mod dma; +mod draw; +mod fifo; +#[cfg(test)] +mod golden; +mod geometry; +mod gl; +mod gl_context; +mod interface; +mod memory; +mod mmio; +mod objects; +mod pixel_dma; +mod pixel_write; +mod raster; +#[cfg(test)] +mod raster_bench; +mod raster_fill; +mod raster_fixed_texture; +mod raster_line; +mod raster_pixels; +mod raster_texture; +mod raster_triangle; +mod raster_unit; +mod record; +mod reader; +mod registers; +mod sampler; +mod scanout; +mod worker; +#[cfg(test)] +mod tests; + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; + +use parking_lot::Mutex; + +use crate::cpu::mips_core::CyclesPtr; +use crate::ip30::{widget_large_base, widget_small_base}; +use crate::irq::InterruptController; +use crate::traits::{ + BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Device, Resettable, BUS_BUSY, BUS_ERR, + BUS_OK, +}; + +use board::Board; +use consts::ODSY_APERTURE; +use display::{Display, ScanoutJob}; +use fifo::set_display_fifo_pending; +use mmio::BoardLocks; +use reader::ScanoutReader; +use scanout::{monotonic_ns, DirtyStats}; + +/// The VPro board on widget `slot`: a `BusDevice` for the widget's +/// windows. +pub struct Odyssey { + small: u64, + large: u64, + state: BoardLocks, + display: Arc, + running: Arc, + thread: Mutex>>, + /// `IRIS_ODSY_REC`: the recording of the bus accesses (record.rs). + rec: Option, +} + +impl Odyssey { + /// A board of `product` (v12, v10, v8, v6) at `widget`: its interrupt + /// packets reach `irq` (HEART), its DMA reaches `mem`. + pub fn new( + widget: u8, + product: &str, + irq: Arc, + mem: Arc, + status: Arc, + ) -> Result, String> { + let mut s = Board::new(product, widget as u32)?; + let rec = match std::env::var("IRIS_ODSY_REC") { + Ok(path) => Some(record::Recorder::create(&path).map_err(|e| format!("IRIS_ODSY_REC {path}: {e}"))?), + Err(_) => None, + }; + let mem: Arc = match &rec { + Some(r) => Arc::new(record::RecordingMem { inner: mem, rec: r.clone() }), + None => mem, + }; + s.guest = Some(mem); + s.front.lock().irq = Some(irq); + Ok(Arc::new(Self { + small: widget_small_base(widget), + large: widget_large_base(widget), + state: BoardLocks::new(s), + display: Arc::new(Display::new(status)), + running: Arc::new(AtomicBool::new(false)), + thread: Mutex::new(None), + rec, + })) + } + + pub fn display(&self) -> Arc { + // The host's cursor query reads the registers the display reads. + self.display.attach_registers(self.state.shared.clone()); + self.display.clone() + } + + /// The aperture offset of `addr`, in either window. + fn offset(&self, addr: u64) -> Option { + let off = if addr >= self.large { addr - self.large } else { addr.wrapping_sub(self.small) }; + (off < ODSY_APERTURE).then_some(off) + } + + fn read(&self, addr: u64, size: u32) -> (u32, u64) { + match self.offset(addr) { + Some(off) => match self.state.bus_read(off, size) { + Some(v) => { + if let Some(r) = &self.rec { + r.lock().read(size as u8, off as u32, v); + } + (BUS_OK, v) + } + None => (BUS_BUSY, 0), + }, + None => (BUS_ERR, 0), + } + } + + fn write(&self, addr: u64, size: u32, val: u64) -> u32 { + match self.offset(addr) { + Some(off) => { + // The recording lock is held across the store and its record, + // so records come out in the order the stores were taken. + let rec = self.rec.as_ref().map(|r| r.lock()); + if self.state.bus_write(off, val, size) { + if let Some(mut r) = rec { + r.write(size as u8, off as u32, val); + } + BUS_OK + } else { + BUS_BUSY + } + } + None => BUS_ERR, + } + } + + /// One display frame: the retrace if it is due, then the dirty lines + /// resolved and, if any changed (or a refresh is due), presented. The + /// retrace fires either way. + fn frame(state: &BoardLocks, display: &Display) { + crate::pf_begin!(crate::perfetto::events::ODSY_CAPTURE); + let mut reader = display.reader.lock(); + reader.attach(&state.shared); + reader.recycle_last(); + // The display takes no board lock and waits for no drawing. It + // reads the planes while the raster unit and the front end write + // them, as the other graphics boards' displays do: a row copied + // half-written is wrong for one frame, then marked again by its + // writer and copied whole (reader.rs). + reader.poll(monotonic_ns()); + let copy = std::time::Instant::now(); + crate::pf_begin!(crate::perfetto::events::ODSY_FRAME_PLANES); + reader.stage(); + crate::pf_end!(crate::perfetto::events::ODSY_FRAME_PLANES); + reader.stats.copied(copy.elapsed()); + let mut job = reader.take(); + if let Some(job) = &mut job { + job.resolve(); + } + let span = job.as_mut().and_then(|job| reader.commit(job)); + Self::present_frame(state, display, reader, job, span); + } + + /// The end of a frame: hand the committed lines to the window. + fn present_frame(state: &BoardLocks, display: &Display, mut reader: parking_lot::MutexGuard<'_, ScanoutReader>, job: Option, span: Option<(usize, usize)>) { + let (w, h) = (reader.frame_width as usize, reader.frame_height as usize); + if w == 0 || h == 0 || reader.frame.len() < w * h { + reader.keep(job); + crate::pf_end!(crate::perfetto::events::ODSY_CAPTURE); + return; + } + // A new size has nothing valid on the screen yet: take every line. + let new_size = display.shown() != (w, h); + if !display.due(new_size || span.is_some()) { + reader.keep(job); + crate::pf_end!(crate::perfetto::events::ODSY_CAPTURE); + return; + } + // Only a new size copies the frame whole; otherwise the committed + // lines are converted from the update's own rows. + let whole = new_size.then(|| reader.frame[..w * h].to_vec()); + set_display_fifo_pending(state.fifo.level() as usize + state.raster.pending() as usize); + crate::pf_end!(crate::perfetto::events::ODSY_CAPTURE); + crate::pf_begin!(crate::perfetto::events::ODSY_PRESENT); + match (&whole, &job) { + (Some(rows), _) => display.present(rows, w, h, 0), + (None, Some(job)) => display.present_lines(job.lines(), w, h), + (None, None) => display.present(&[], w, h, 0), + } + crate::pf_end!(crate::perfetto::events::ODSY_PRESENT); + reader.keep(job); + } +} + +impl BusDevice for Odyssey { + fn read8(&self, addr: u64) -> BusRead8 { + let (status, data) = self.read(addr, 1); + BusRead8 { status, data: data as u8 } + } + fn read16(&self, addr: u64) -> BusRead16 { + let (status, data) = self.read(addr, 2); + BusRead16 { status, data: data as u16 } + } + fn read32(&self, addr: u64) -> BusRead32 { + let (status, data) = self.read(addr, 4); + BusRead32 { status, data: data as u32 } + } + fn read64(&self, addr: u64) -> BusRead64 { + let (status, data) = self.read(addr, 8); + BusRead64 { status, data } + } + fn write8(&self, addr: u64, val: u8) -> u32 { + self.write(addr, 1, val as u64) + } + fn write16(&self, addr: u64, val: u16) -> u32 { + self.write(addr, 2, val as u64) + } + fn write32(&self, addr: u64, val: u32) -> u32 { + self.write(addr, 4, val as u64) + } + fn write64(&self, addr: u64, val: u64) -> u32 { + self.write(addr, 8, val) + } +} + +/// The display thread's frame period: the modelled mode's 60 Hz. +const FRAME_PERIOD: std::time::Duration = std::time::Duration::from_micros(16_667); + +impl Device for Odyssey { + fn step(&self, _cycles: u64) {} + + fn register_commands(&self) -> Vec<(String, String)> { + vec![("vpro".to_string(), "VPro counters: vpro refusals [reset] (bus refusals by reason and register), vpro dirty [reset] (display)".to_string())] + } + + fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { + if cmd != "vpro" { + return Err(format!("unknown command: {cmd}")); + } + match args { + ["refusals"] => self.state.refusals.report(&mut *w).map_err(|e| e.to_string()), + ["refusals", "reset"] => { + self.state.refusals.reset(); + Ok(()) + } + ["dirty"] => self.display.reader.lock().stats.report(&self.state.shared.marks, &mut *w).map_err(|e| e.to_string()), + ["dirty", "reset"] => { + self.display.reader.lock().stats = DirtyStats::default(); + self.state.shared.marks.reset_sites(); + Ok(()) + } + _ => Err("Usage: vpro refusals [reset] | vpro dirty [reset]".to_string()), + } + } + + fn start(&self) { + let mut t = self.thread.lock(); + if t.is_some() { + return; + } + self.state.work_start(); + self.running.store(true, Ordering::Release); + let (state, display, running) = (self.state.clone(), self.display.clone(), self.running.clone()); + *t = Some( + std::thread::Builder::new() + .name("odyssey-display".into()) + .spawn(move || { + let mut next = std::time::Instant::now(); + while running.load(Ordering::Acquire) { + Odyssey::frame(&state, &display); + next += FRAME_PERIOD; + let now = std::time::Instant::now(); + if next > now { + std::thread::sleep(next - now); + } else { + next = now; + } + } + // The renderer is released on this thread, which owns its + // context; a stop from the main thread leaves it alone + // (`DisplayStop`). + display.stop(); + }) + .expect("odyssey display thread"), + ); + } + + fn stop(&self) { + self.display.display_stop.request(); + self.running.store(false, Ordering::Release); + if let Some(t) = self.thread.lock().take() { + let _ = t.join(); + } + self.state.work_stop(); + } + + fn is_running(&self) -> bool { + self.running.load(Ordering::Acquire) + } + + fn get_clock(&self) -> u64 { + 0 + } +} + +impl Drop for Odyssey { + fn drop(&mut self) { + // The work thread holds a clone of the state's handles; stop it + // so the state can go. + Device::stop(self); + } +} + +impl Resettable for Odyssey { + fn power_on(&self) { + // SAFETY: power-on comes before the machine starts the board's + // threads and the processors. + unsafe { &mut *self.state.board.get() }.reset(); + } +} diff --git a/src/dev/odyssey/objects.rs b/src/dev/odyssey/objects.rs new file mode 100644 index 000000000..24b22f085 --- /dev/null +++ b/src/dev/odyssey/objects.rs @@ -0,0 +1,573 @@ + +use crate::devlog::LogModule; +use super::bits::{be32, sextract32}; +use super::board::{Board, ODSY_HEIGHT, ODSY_OBJECTS, ODSY_OBJ_POOL_BYTES, ODSY_OBJ_POOL_WORDS, ODSY_WIDTH}; +use super::consts::{dma, tex}; +use super::raster_unit::RASTER_JOB_OVERHEAD; +impl Board { + /// The index of the object at board base base. A base names at most one + /// live object (Board::object_select()), so the index's entry is the one a + /// search of obj_base would find. + pub fn object_find(&mut self, base: u32) -> Option { + if base == 0 { + return None; + } + match self.objects.index.get(&base) { + Some(slot) if *slot != 0 => Some((*slot - 1) as usize), + _ => None, + } + } + + /// Give object i the base base, keeping obj_index in step. + pub fn object_set_base(&mut self, i: usize, base: u32) { + if self.objects.base[i] != 0 { + self.objects.index.remove(&self.objects.base[i]); + } + self.objects.base[i] = base; + if base != 0 { + self.objects.index.insert(base, i as u32 + 1); + } + } + + pub fn object_word_valid(&self, word: u64) -> bool { + word < ODSY_OBJ_POOL_WORDS as u64 && (self.objects.valid[(word >> 3) as usize] & (1u8 << (word & 7))) != 0 + } + + pub fn object_word_set_valid(&mut self, word: u64, valid: bool) { + let mask = 1u8 << (word & 7); + + assert!(word < ODSY_OBJ_POOL_WORDS as u64); + if valid { + self.objects.valid[(word >> 3) as usize] |= mask; + } else { + self.objects.valid[(word >> 3) as usize] &= !mask; + } + } + + pub fn object_valid_range(&mut self, first: u64, count: u64, valid: bool) { + for i in 0..count { + self.object_word_set_valid(first + i, valid); + } + } + + pub fn object_valid_move(&mut self, source: u64, dest: u64, count: u64) { + if dest > source { + for i in (1..=count).rev() { + let v = self.object_word_valid(source + i - 1); + + self.object_word_set_valid(dest + i - 1, v); + } + } else { + for i in 0..count { + let v = self.object_word_valid(source + i); + + self.object_word_set_valid(dest + i, v); + } + } + } + + pub fn object_region_written(&self, o: usize, x: u32, y: u32, width: u32, height: u32) -> bool { + + if x > self.objects.width[o] || width > self.objects.width[o] - x || y > self.objects.rows[o] || height > self.objects.rows[o] - y { + return false; + } + for dy in 0..height { + let row = self.objects.offset[o] as u64 + (y + dy) as u64 * self.objects.width[o] as u64 + x as u64; + + for dx in 0..width { + if !self.object_word_valid(row + dx as u64) { + return false; + } + } + } + true + } + + /// Mark object i as the most recently used. Use is an upload or a grow + /// that targets the object, never a draw that samples it: a texture drawn + /// every frame but uploaded once is the first to go when the store or + /// the table fills. Model policy, not board behaviour. + pub fn object_touch(&mut self, i: usize) { + self.objects.clock += 1; + self.objects.stamp[i] = self.objects.clock; + } + + /// Forget object i: its slot stays, with base 0, for a later object. + pub fn object_forget(&mut self, i: usize) { + self.object_set_base(i, 0); + self.objects.width[i] = 0; + self.objects.rows[i] = 0; + self.objects.offset[i] = 0; + self.objects.stamp[i] = 0; + } + + /// The least recently used live object other than keep. + pub fn object_oldest(&mut self, keep: Option) -> Option { + let mut oldest: Option = None; + + for i in 0..self.objects.count as usize { + if Some(i) != keep && self.objects.base[i] != 0 && oldest.is_none_or(|o| self.objects.stamp[i] < self.objects.stamp[o]) { + oldest = Some(i); + } + } + oldest + } + + /// Pack the objects that hold rows to the front of the store, in store order, + /// so the space between them is reclaimed, then object last (when given) + /// after them all, so that it can grow in place. Indices do not change. + pub fn object_compact(&mut self, last: Option) { + // A queued textured draw may still read the store. + self.raster_wait(); + let last_size: u64 = last.map_or(0, |l| self.objects.width[l] as u64 * self.objects.rows[l] as u64); + let mut held: Vec = Vec::new(); + let mut held_valid: Vec = Vec::new(); + let mut next: u64 = 0; + + // Packing the others may overwrite last's words: keep them aside. + if last_size != 0 { + let off = last.map_or(0, |l| self.objects.offset[l]) as u64; + + held = self.objects.mem.bytes[(off * 4) as usize..((off + last_size) * 4) as usize].to_vec(); + for k in 0..last_size { + held_valid.push(self.object_word_valid(off + k)); + } + } + // The others in store order, once: each is then the lowest-placed + // one not yet packed. One whose words a packed object now covers is + // left where it is, as a search from `next` would leave it. + let mut order: Vec = (0..self.objects.count as usize) + .filter(|&i| Some(i) != last && self.objects.base[i] != 0 && self.objects.rows[i] != 0) + .collect(); + order.sort_unstable_by_key(|&i| (self.objects.offset[i], i)); + for p in order { + if (self.objects.offset[p] as u64) < next { + continue; + } + let size = self.objects.width[p] as u64 * self.objects.rows[p] as u64; + if self.objects.offset[p] as u64 != next { + let from = self.objects.offset[p] as usize * 4; + self.objects.mem.bytes.copy_within(from..from + size as usize * 4, next as usize * 4); + self.object_valid_move(self.objects.offset[p] as u64, next, size); + self.objects.offset[p] = next as u32; + } + next += size; + } + if let (Some(last), true) = (last, last_size != 0) { + let at = next as usize * 4; + self.objects.mem.bytes[at..at + held.len()].copy_from_slice(&held); + for k in 0..last_size { + self.object_word_set_valid(next + k, held_valid[k as usize]); + } + self.objects.offset[last] = next as u32; + next += last_size; + } + // Growth in place assumes the words past the store's end are zero. + let tail = (self.objects.used as u64).saturating_sub(next); + let at = next as usize * 4; + self.objects.mem.bytes[at..at + tail as usize * 4].fill(0); + self.object_valid_range(next, tail, false); + self.objects.used = next as u32; + } + + /// The object at base, created with the given row length in pixels when it + /// does not exist; a new row length for an existing object discards its + /// storage (its layout changed). When the table is full, the least recently + /// used object is forgotten, reported once. + pub fn object_select(&mut self, base: u32, width: u32) -> usize { + if let Some(i) = self.object_find(base) { + if width != 0 && self.objects.width[i] != width { + self.objects.width[i] = width; + self.objects.rows[i] = 0; + } + self.object_touch(i); + return i; + } + let free = self.objects.base[..self.objects.count as usize].iter().position(|&b| b == 0); + let i = match free { + Some(j) => j, + None if self.objects.count == ODSY_OBJECTS => { + // A full table has no free slot, so every entry is live. + let i = self.object_oldest(None).expect("a full table has a live object"); + if !self.diag.obj_full_reported { + self.diag.obj_full_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: more than {} board-memory objects; the least recently used, {:#x}, is forgotten", + ODSY_OBJECTS, + self.objects.base[i] + ); + } + self.object_forget(i); + i + } + None => { + self.objects.count += 1; + self.objects.count as usize - 1 + } + }; + self.object_set_base(i, base); + self.objects.width[i] = width; + self.objects.offset[i] = 0; + self.objects.rows[i] = 0; + self.object_touch(i); + i + } + + /// Make room for rows rows in object i: grow it in place when it is the last + /// in the store, else move it to the end. When there is no room, forget the + /// least recently used other objects until the live objects and i's new size + /// fit, reporting the first object forgotten, then pack the store once with i + /// last. False, reported once, when i alone does not fit. + pub fn object_rows(&mut self, i: usize, rows: u32) -> bool { + let iu = i; + let pool = ODSY_OBJ_POOL_WORDS as u64; + let need = self.objects.width[iu] as u64 * rows as u64; + let have = self.objects.width[iu] as u64 * self.objects.rows[iu] as u64; + + if rows <= self.objects.rows[iu] { + return true; + } + self.raster_wait(); + self.object_touch(i); + if need <= pool + && !(have != 0 && self.objects.offset[iu] as u64 + have == self.objects.used as u64 && self.objects.offset[iu] as u64 + need <= pool) + && self.objects.used as u64 + need > pool + { + let mut live: u64 = 0; + + for j in 0..self.objects.count as usize { + if j != iu && self.objects.base[j] != 0 { + live += self.objects.width[j] as u64 * self.objects.rows[j] as u64; + } + } + while live + need > pool { + let Some(v) = self.object_oldest(Some(i)) else { + break; + }; + live -= self.objects.width[v] as u64 * self.objects.rows[v] as u64; + if !self.diag.obj_forget_reported { + self.diag.obj_forget_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: the model's {} MiB board-memory store is full; the least recently used object, {:#x}, is forgotten", + ODSY_OBJ_POOL_BYTES / (1024 * 1024), + self.objects.base[v] + ); + } + self.object_forget(v); + } + self.object_compact((have != 0).then_some(i)); + } + if have != 0 && self.objects.offset[iu] as u64 + have == self.objects.used as u64 && self.objects.offset[iu] as u64 + need <= pool { + self.objects.used = (self.objects.offset[iu] as u64 + need) as u32; + self.object_valid_range(self.objects.offset[iu] as u64 + have, need - have, false); + } else if self.objects.used as u64 + need <= pool { + let new_offset = self.objects.used; + let from = self.objects.offset[iu] as usize * 4; + + self.objects.mem.bytes.copy_within(from..from + have as usize * 4, new_offset as usize * 4); + let z = (new_offset as usize + have as usize) * 4; + self.objects.mem.bytes[z..z + (need - have) as usize * 4].fill(0); + self.object_valid_move(self.objects.offset[iu] as u64, new_offset as u64, have); + self.object_valid_range(new_offset as u64 + have, need - have, false); + self.objects.offset[iu] = new_offset; + self.objects.used += need as u32; + } else { + if !self.diag.obj_full_reported { + self.diag.obj_full_reported = true; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: board-memory object {:#x} ({} x {}) exceeds the model's {} MiB store", + self.objects.base[iu], + self.objects.width[iu], + rows, + ODSY_OBJ_POOL_BYTES / (1024 * 1024) + ); + } + return false; + } + self.objects.rows[iu] = rows; + true + } + + /// Object i's texel (x, y) as 0xRRGGBBAA; outside its rows or width, dropped. + pub fn object_store(&mut self, i: usize, x: u32, y: u32, rgba: u32) { + if x >= self.objects.width[i] || !self.object_rows(i, y.wrapping_add(1)) { + return; + } + let word = self.objects.offset[i] as u64 + y as u64 * self.objects.width[i] as u64 + x as u64; + self.raster_wait(); + self.objects.mem.bytes[(word * 4) as usize..][..4].copy_from_slice(&rgba.to_be_bytes()); + self.object_word_set_valid(word, true); + } + + /// object_store() for a transfer the raster unit lands: the bookkeeping + /// now (rows, validity), the word into `staged` for object_flush() to + /// hand on. A queued textured draw may still read the texels this + /// replaces, and the raster unit runs its jobs in order, so the write + /// queues behind that draw instead of waiting for it. + pub fn object_stage(&mut self, staged: &mut Vec<(u32, u32)>, i: usize, x: u32, y: u32, rgba: u32) { + let iu = i; + + if x >= self.objects.width[iu] { + return; + } + if y.wrapping_add(1) > self.objects.rows[iu] { + // Growing the object may move it in the store, and the words + // staged so far name their places as they are now. + self.object_flush(staged); + if !self.object_rows(i, y.wrapping_add(1)) { + return; + } + } + let word = self.objects.offset[iu] as u64 + y as u64 * self.objects.width[iu] as u64 + x as u64; + self.object_word_set_valid(word, true); + staged.push((word as u32, rgba)); + } + + /// Hand the staged object words to the raster unit, behind the jobs + /// queued before them. + pub fn object_flush(&mut self, staged: &mut Vec<(u32, u32)>) { + if staged.is_empty() { + return; + } + let words = std::mem::take(staged); + let spans = self.plane_spans(); + self.raster.submit_work(RASTER_JOB_OVERHEAD.saturating_add(words.len() as u32), move |_| { + // SAFETY: the store outlives the job, and the front end waits + // for the raster unit before it touches the store (PlaneSpans). + // The whole PlaneSpans moves in, which is Send; its pointer alone + // is not. + let spans = spans; + let bytes = unsafe { spans.obj.bytes() }; + for (word, rgba) in words { + bytes[word as usize * 4..][..4].copy_from_slice(&rgba.to_be_bytes()); + } + }); + } + + pub fn object_load(&mut self, i: usize, x: u32, y: u32) -> u32 { + let iu = i; + + if x >= self.objects.width[iu] || y >= self.objects.rows[iu] { + return 0; + } + // A staged transfer may still be queued for the raster unit. + self.raster_wait(); + be32(&self.objects.mem.bytes[((self.objects.offset[iu] as u64 + y as u64 * self.objects.width[iu] as u64 + x as u64) * 4) as usize..][..4]) + } + + /// A GL pixel copy (block 0x2031), decoded as its eight + /// operands (CopyOperands): from the read drawable or an object, + /// to the draw drawable or an object. Any other block is the X server's + /// rectangle copy (Board::copy_rect()). False when an operand names an + /// object that is not there or does not fit, so the block is reported. + /// + /// A copy from the window into an object lists the rectangle's rows + /// bottom first: the library reads such an object top row first, with no + /// bottom-up bit, and expects the lowest row of the rectangle first (the + /// way a window read asks for it). OBSERVED in guest traces of + /// glReadPixels of 16-bit types and glCopyTexImage, where the host got + /// the rectangle's top row. INFERRED to be the board's copy rule: no + /// known header bit selects it. The other directions + /// keep the rows in the order they are held. + /// + /// The opposite copy, from an object to the window, lists the rows + /// bottom first as well: the burst that filled the object put the + /// image's first row (GL's lowest) first, and a window row grows + /// downwards, so the object's first row lands on the rectangle's last. + /// OBSERVED in a glDrawPixels of the signed types, whose raw burst goes + /// to an object and then to the window: the host read back the rows in + /// reverse (row 4's bytes where row 0's belonged, which differs from + /// row 0's by 4 * 29 = 0x74 in the test pattern). INFERRED to be the + /// board's copy rule, as above. + /// + /// The programmed post-filter (0x203b) is applied to the copied words + /// (see copy_post_filter()); the identity leaves them alone. + /// + /// The source is staged first, so an overlapping copy needs no direction. + /// Words move as the planes hold them: a colour pixel keeps its alpha and + /// an index copies as the raw index. The read header's class, code and + /// fields and the write header's format and overlap bits say how the board + /// converts and in which order it walks; the model's planes need neither. + /// A window source takes the buffer the header's front bit names (see + /// dma_read_pixel()), and a window destination the buffers the drawing + /// mask selects, through the window's clip rectangles and id test. + pub fn copy_rect_gl(&mut self, c: &CopyOperands) -> bool { + let from_window = c.src_base == dma::SENTINEL_READ; + let to_window = c.dest_base == dma::SENTINEL_DRAW; + let (w, h) = c.size; + let src = if from_window { None } else { self.object_find(c.src_base) }; + let dest = if to_window { None } else { self.object_find(c.dest_base) }; + + if (!from_window && src.is_none()) || (!to_window && dest.is_none()) { + return false; + } + if let Some(d) = dest { + if c.dest.0 as u64 + w as u64 > self.objects.width[d] as u64 || !self.object_rows(d, c.dest.1.wrapping_add(h)) { + return false; + } + } + if w == 0 || h == 0 { + return true; + } + let mut words = Vec::with_capacity(w as usize * h as usize); + + for y in 0..h { + for x in 0..w { + words.push(if let Some(src) = src { + self.object_load(src, c.src.0 + x, c.src.1 + y) + } else { + // Into an object the window's rows go bottom first. + let row = if dest.is_some() { h - 1 - y } else { y }; + let (px, py) = ((c.src.0 + x) as i64 + self.win.read_origin[0] as i64, (c.src.1 + row) as i64 + self.win.read_origin[1] as i64); + + if px < 0 || py < 0 || px >= ODSY_WIDTH as i64 || py >= ODSY_HEIGHT as i64 { + 0 + } else { + let buffer = self.read_buffer(c.read_front(), px, py); + + self.buffer_load(buffer, px, py) + } + }); + } + } + self.copy_post_filter(&mut words, Self::copy_reads_signed(from_window)); + // From an object to the window the rows go bottom first. + let flip = !from_window && to_window; + + for y in 0..h { + for x in 0..w { + let word = words[(y * w + x) as usize]; + let y = if flip { h - 1 - y } else { y }; + + if let Some(dest) = dest { + self.object_store(dest, c.dest.0 + x, c.dest.1 + y, word); + } else { + self.paint_pixel( + (c.dest.0 + x) as i64 + self.win.origin[0] as i64, + (c.dest.1 + y) as i64 + self.win.origin[1] as i64, + word, + ); + } + } + } + true + } +} + +impl Board { + /// Whether the board reads the bytes of a copy's source as signed values + /// when it filters them. INFERRED, and nothing board-visible establishes + /// it: the library sends a signed type's burst and write code as the + /// unsigned type's, and programs the final pass as ordinary scale and + /// bias words. The pass the library builds for a signed type is 2x + 1/4096 + /// from an object the burst filled, which gives GL's result only if the + /// bits are read as signed. So the model reads an object's bytes as signed + /// and a window's as unsigned. This is the one place the choice is made. + fn copy_reads_signed(from_window: bool) -> bool { + !from_window + } + + /// Apply the post-filter words (block 0x203b) to a copy's words, lane by + /// lane in R, G, B, A order: the byte b of a lane becomes + /// clamp(x * scale + bias, 0, 1) as a byte, with x = b / 255 (or, for a + /// signed source, the byte as a signed value over 255), scale and bias + /// signed 3.12 fixed point (ESTABLISHED as the layout of the words). The + /// bias is used as programmed: the library halves the application's bias + /// before it programs the step (ESTABLISHED), so + /// the board does not halve again. That the filter governs a copy at all + /// is INFERRED from the library's use of the block; the identity (scale + /// 1.0, bias 0) leaves every word unchanged, as does a lane that is + /// the identity. + /// + /// An alpha lane whose bias is full scale (0x1000) is the library's + /// opaque overwrite for a source without alpha (ESTABLISHED): it stores + /// 1.0, since the object's stored alpha is the model's 255, not source + /// bits. + pub fn copy_post_filter(&self, words: &mut [u32], signed: bool) { + let sb = &self.tex.scale_bias; + let lane = |c: usize| { + (sextract32(sb[c * 2], 0, tex::SCALE_BIAS_BITS) as f64 / tex::SCALE_BIAS_ONE as f64, + sextract32(sb[c * 2 + 1], 0, tex::SCALE_BIAS_BITS) as f64 / tex::SCALE_BIAS_ONE as f64) + }; + + if self.texture_scale_bias_identity() { + return; + } + for word in words.iter_mut() { + let mut out = *word; + + for c in 0..4usize { + let (scale, bias) = lane(c); + let shift = 24 - 8 * c as u32; + let byte = (*word >> shift) as u8; + let x = if signed { byte as i8 as f64 } else { byte as f64 } / 255.0; + let v = if c == 3 && bias >= 1.0 { 1.0 } else { (x * scale + bias).clamp(0.0, 1.0) }; + + out = (out & !(0xff << shift)) | (((v * 255.0 + 0.5).floor() as u32) << shift); + } + *word = out; + } + } +} + +/// The eight operands of the GL pixel copy, block 0x2031. +/// +/// Established as what the library sends; what the board does with the +/// overlap bits is inferred. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct CopyOperands { + /// Word 0: 0x30000019 for the read drawable, else an object's address. + pub src_base: u32, + /// Word 1: source x and y. + pub src: (u32, u32), + /// Word 2 bit 31: the destination is the front buffer. + pub dest_front: bool, + /// Word 2 bits 27:16 and 11:0: the destination's and the source's + /// 12-bit drawable words. + pub dest_drawable: u32, + pub src_drawable: u32, + /// Word 2 bits 15:12: the destination's second field code. + pub dest_field: u32, + /// Word 3: width and height. + pub size: (u32, u32), + /// Word 4: the read header's word 4 (class, code, fields, front). + pub read_header: u32, + /// Word 5: 0x30000010 for the draw drawable, else an object's address. + pub dest_base: u32, + /// Word 6: destination x and y. + pub dest: (u32, u32), + /// Word 7: the write header's word 7: format and type, expand, size. + pub write_header: u32, +} + +impl CopyOperands { + pub fn decode(op: &[u32]) -> Self { + CopyOperands { + src_base: op[0], + src: (op[1] & 0xffff, op[1] >> 16), + dest_front: op[2] & (1 << 31) != 0, + dest_drawable: (op[2] >> 16) & 0xfff, + src_drawable: op[2] & 0xfff, + dest_field: (op[2] >> 12) & 0xf, + size: (op[3] & 0xffff, op[3] >> 16), + read_header: op[4], + dest_base: op[5], + dest: (op[6] & 0xffff, op[6] >> 16), + write_header: op[7], + } + } + + /// The read header's front bit: logical front of the read buffer. + pub fn read_front(&self) -> bool { + self.read_header & dma::FMT_FRONT != 0 + } + + /// Write header bit 31: the destination x lies inside the source's x + /// range; bit 30 the same for y. INFERRED to pick the copy direction. + pub fn overlap(&self) -> (bool, bool) { + (self.write_header & (1 << 31) != 0, self.write_header & (1 << 30) != 0) + } +} diff --git a/src/dev/odyssey/pixel_dma.rs b/src/dev/odyssey/pixel_dma.rs new file mode 100644 index 000000000..faaa7e2ca --- /dev/null +++ b/src/dev/odyssey/pixel_dma.rs @@ -0,0 +1,306 @@ +//! SGI graphics DMA: guest pages resolved to chunks, and pixel transfers +//! between those chunks and a plane. +//! +//! A transfer reads or writes guest memory in chunks of at most a page, as +//! a head's DMA engine bursts. Each chunk is a buffer of its own: a source +//! is read whole when the transfer is mapped, and of a target only the run +//! of bytes the head wrote goes back when it is unmapped, so a target the +//! head did not fully write keeps its other bytes. +//! +//! Ported from include/hw/display/sgi/pixel-dma.h in jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use std::sync::Arc; + +use crate::traits::{BusDevice, BUS_OK}; + +/// Guest memory as a graphics head's DMA reaches it: the machine's +/// bus-master view, or none before the machine attaches one. +pub(super) type GuestMemory = Option>; + +/// Read `buf.len()` guest bytes at `addr`; false when no memory is attached +/// or the bus reports an error. +pub(super) fn read_guest(mem: &GuestMemory, addr: u64, buf: &mut [u8]) -> bool { + mem.as_deref().is_some_and(|m| crate::physical::read_bytes(m, addr, buf) == BUS_OK) +} + +/// Write `buf` to guest memory at `addr`; false as for [`read_guest`]. +pub(super) fn write_guest(mem: &GuestMemory, addr: u64, buf: &[u8]) -> bool { + mem.as_deref().is_some_and(|m| crate::physical::write_bytes(m, addr, buf) == BUS_OK) +} + +/// Which way a mapped transfer moves bytes. +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] +pub(super) enum DmaDirection { + /// Guest memory to the head: chunks are read when mapped. + #[default] + ToDevice, + /// The head to guest memory: written runs go back when unmapped. + FromDevice, +} + +pub(super) const DMA_CHUNK_MAX: u32 = 4096; +/// The smallest page a page list may name. +pub(super) const DMA_MIN_PAGE_SIZE: u32 = DMA_CHUNK_MAX; + +/// Why a DMA transfer to or from guest memory failed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) enum DmaError { + /// A guest-memory transaction failed. + Bus, + /// Outside the page list, plane or pool. + Bounds, +} + +/// A head's view of guest memory. The inbound path is direct-mapped: a +/// device address is the bus address. +#[derive(Clone, Default)] +pub(super) struct DmaBus { + pub mem: GuestMemory, + /// The MMIO write that started the transfer, which a traced chunk + /// names (Board::trace_sequence). + pub trace_sequence: u64, +} + +/// A guest buffer as the driver described it: page i at addr[i]. +#[derive(Clone, Default)] +pub(super) struct DmaPageList { + pub addr: Vec, // device addresses + pub npages: u32, + pub page_size: u32, +} + +#[derive(Clone, Default)] +pub(super) struct DmaChunk { + pub host: Vec, // the chunk's bytes + pub len: u32, + pub pos: u64, // offset within the page-listed buffer + pub addr: u64, // device address, as traced + pub bus_addr: u64, // bus address the transfer used + /// The bytes the head wrote, as offsets into the chunk: one run from + /// the first to the last, empty while written_end is 0. + pub written_start: u32, + pub written_end: u32, +} + +#[derive(Clone, Default)] +pub(super) struct DmaMap { + pub chunks: Vec, + pub cursor: u32, // the chunk the last copy ended in + pub mem: GuestMemory, + pub dir: DmaDirection, +} + +impl DmaPageList { + /// The bytes a page list maps, or 0 (logged) when its page size is not a + /// power of two of at least DMA_MIN_PAGE_SIZE. + pub fn bytes(&self) -> u64 { + if self.page_size < DMA_MIN_PAGE_SIZE || !(self.page_size as u64).is_power_of_two() { + crate::dlog!( + LogModule::Odyssey, + "sgi-dma: page size {} is not a power of two of at least {}", + self.page_size, + DMA_MIN_PAGE_SIZE + ); + return 0; + } + self.npages as u64 * self.page_size as u64 + } +} + +impl DmaBus { + /// Read buf.len() bytes at device address addr, tracing them in chunks + /// when `trace` is set. Returns + /// false, having logged a guest error, when the read fails. + pub fn read(&self, addr: u64, buf: &mut [u8], trace: bool, trace_seq: u64) -> bool { + let len = buf.len() as u32; + let mut done: u32 = 0; + + // No page list here, so chunks break at 4 KiB boundaries of the device + // address: a traced chunk is then never longer than the contract allows + // and never crosses a page of any size the families use. + while done < len { + let at = addr.wrapping_add(done as u64); + let n = (len - done).min(DMA_CHUNK_MAX - (at & (DMA_CHUNK_MAX as u64 - 1)) as u32); + let bus_addr = at; + let dst = &mut buf[done as usize..(done + n) as usize]; + if !read_guest(&self.mem, bus_addr, dst) { + crate::dlog!(LogModule::Odyssey, "sgi-dma: read of {} bytes at {:#x} failed", n, bus_addr); + return false; + } + self.trace(trace, trace_seq, at, dst); + done += n; + } + true + } + + /// Resolve [offset, offset + len) of a page-listed buffer and append its + /// chunks to map. Fails, after logging a guest error, with the reason. + #[allow(clippy::too_many_arguments, reason = "the range, its direction and its trace label")] + pub fn map(&self, map: &mut DmaMap, pl: &DmaPageList, offset: u64, len: u64, dir: DmaDirection, trace: bool, trace_seq: u64) -> Result<(), DmaError> { + let total = pl.bytes(); + let mut pos = offset; + + assert!(map.chunks.is_empty() || map.dir == dir); + map.mem = self.mem.clone(); + map.dir = dir; + let first = map.chunks.len(); + + if offset > total || len > total - offset { + crate::dlog!( + LogModule::Odyssey, + "sgi-dma: bytes {:#x}+{:#x} are outside a {}-page buffer", + offset, + len, + pl.npages + ); + return Err(DmaError::Bounds); + } + + let ps = pl.page_size as u64; + while pos < offset + len { + let page = pl.addr[(pos / ps) as usize]; + let in_page = pos & (ps - 1); + let addr = page + in_page; + // The cut at the page's end is what keeps a chunk off guest memory + // the driver did not hand over: the next page of the buffer is + // wherever the list says, not the physically next one. + let n = (offset + len - pos).min(ps - in_page).min(DMA_CHUNK_MAX as u64); + let bus_addr = addr; + + if page & (ps - 1) != 0 { + crate::dlog!( + LogModule::Odyssey, + "sgi-dma: page {:#x} is not aligned to its {}-byte size", + page, + pl.page_size + ); + return Err(DmaError::Bounds); + } + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: DMA chunk mapped: addr {:#x} len {}", addr, n); + let mut c = DmaChunk { + host: vec![0u8; n as usize], + len: n as u32, + pos, + addr, + bus_addr, + written_start: 0, + written_end: 0, + }; + // Only a source is read. A buffer the head writes starts empty and + // only the bytes it wrote go back, so a target that is not RAM sees + // the writes the head made and no read of its own. + if dir == DmaDirection::ToDevice && !read_guest(&self.mem, bus_addr, &mut c.host[..]) { + crate::dlog!(LogModule::Odyssey, "sgi-dma: read of {} bytes at {:#x} failed", n, bus_addr); + return Err(DmaError::Bus); + } + if dir == DmaDirection::ToDevice { + self.trace(trace, trace_seq, addr, &c.host[..c.len as usize]); + } + pos += c.len as u64; + map.chunks.push(c); + } + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: DMA map: seq {} dir {:?} offset {:#x} len {:#x} chunks {}", trace_seq, dir, offset, len, map.chunks.len() - first); + Ok(()) + } +} + +impl DmaMap { + /// Copy buf.len() bytes at buffer offset pos between map and buf. Returns + /// false when map does not cover the range. `to_guest`: buf is copied into + /// map; otherwise map into buf. (buf is `&mut` for both directions.) + pub fn copy(&mut self, pos: u64, buf: &mut [u8], to_guest: bool) -> bool { + let len = buf.len() as u64; + let nchunks = self.chunks.len() as u32; + let start = if self.cursor < nchunks && self.chunks[self.cursor as usize].pos <= pos { + self.cursor + } else { + 0 + }; + + // Rows are copied in the order their chunks were mapped, so starting at + // the chunk the previous copy ended in walks the list about once per + // transfer instead of once per row. A range the walk from there does + // not cover is looked for again from the start. + if self.copy_from(start, pos, buf, to_guest) == len { + return true; + } + start != 0 && self.copy_from(0, pos, buf, to_guest) == len + } + + /// Release map, writing the head's written runs back to guest memory. + pub fn unmap(&mut self) { + for c in self.chunks.iter() { + let start = c.written_start; + let len = c.written_end.wrapping_sub(start); + + // Whatever the head wrote reaches the guest, even when the transfer + // failed later on: the guest's memory then shows how far the + // transfer got. + if len != 0 { + let at = c.bus_addr + start as u64; + if !write_guest(&self.mem, at, &c.host[start as usize..(start + len) as usize]) { + crate::dlog!(LogModule::Odyssey, "sgi-dma: write of {} bytes at {:#x} failed", len, at); + } + } + } + *self = DmaMap::default(); + } +} + +impl DmaBus { + /// A run of guest bytes the DMA engine read, for the device log. + pub fn trace(&self, trace: bool, seq: u64, addr: u64, bytes: &[u8]) { + let _ = seq; + if trace { + crate::dlog_dev!( + LogModule::Odyssey, + "sgi-odyssey: DMA chunk read: seq {} addr {:#x} len {} hex {}", + self.trace_sequence, + addr, + bytes.len(), + bytes.iter().map(|b| format!("{:02x}", b)).collect::() + ); + } + } +} + +impl DmaMap { + pub fn copy_from(&mut self, start: u32, pos: u64, p: &mut [u8], to_guest: bool) -> u64 { + let len = p.len() as u64; + let mut done: u64 = 0; + + let mut i = start as usize; + while i < self.chunks.len() && done < len { + let c = &mut self.chunks[i]; + let at = pos + done; + + if at < c.pos || at >= c.pos + c.len as u64 { + i += 1; + continue; + } + let n = (len - done).min(c.pos + c.len as u64 - at); + let (d0, n0) = (done as usize, n as usize); + let off = (at - c.pos) as usize; + if to_guest { + let mut from = off as u32; + let mut to = from + n as u32; + + c.host[off..off + n0].copy_from_slice(&p[d0..d0 + n0]); + if c.written_end != 0 { + from = from.min(c.written_start); + to = to.max(c.written_end); + } + c.written_start = from; + c.written_end = to; + } else { + p[d0..d0 + n0].copy_from_slice(&c.host[off..off + n0]); + } + done += n; + self.cursor = i as u32; + i += 1; + } + done + } +} + diff --git a/src/dev/odyssey/pixel_write.rs b/src/dev/odyssey/pixel_write.rs new file mode 100644 index 000000000..0680d5d89 --- /dev/null +++ b/src/dev/odyssey/pixel_write.rs @@ -0,0 +1,217 @@ +//! The library's image-write codes: word 7's format (bits 4:0) and type +//! (bits 8:5), and what bytes a pixel of each takes and means. +//! +//! ESTABLISHED as what the library sends. A packed type is +//! always sent under the format whose component order it has in memory, so +//! the format lists the components most significant first; the `_REV` +//! variants are other formats, not a bit. +//! +//! What the board does with the bytes is INFERRED from the names: single +//! channels widen to eight bits by the usual GL scaling, a luminance or +//! intensity source spreads over red, green and blue, and a component the +//! source lacks is 0, or full scale for alpha. + +use super::bits::be32; +use super::consts::ODSY_INDEX_MASK; +/// A source component. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(super) enum Comp { + Red, + Green, + Blue, + Alpha, + /// Luminance: red, green and blue together. + Lum, + /// Intensity: red, green, blue and alpha together. + Intensity, + Index, +} + +/// One field of a packed word: component, width in bits, shift. +#[derive(Clone, Copy, Debug)] +pub(super) struct Field { + pub comp: Comp, + pub width: u8, + pub shift: u8, +} + +#[derive(Clone, Debug)] +pub(super) enum WriteKind { + /// Whole components in order, `comp_bytes` bytes each, big-endian. + Comps { comps: [Comp; 4], n: u8, comp_bytes: u8 }, + /// One big-endian word of fields. + Packed { fields: [Field; 4], n: u8 }, +} + +/// What a write code names: the bytes a pixel takes and how to read them. +#[derive(Clone, Debug)] +pub(super) struct WriteForm { + pub bytes: u32, + pub kind: WriteKind, +} + +const fn f(comp: Comp, width: u8, shift: u8) -> Field { + Field { comp, width, shift } +} + +const NONE: Field = f(Comp::Red, 0, 0); + +/// The form of write code `code` (word 7 & 0x1ff), or None for a code this +/// model does not take: depth and stencil (format 0, which goes to its own +/// planes), the 4:2:2 formats 0xa and 0xe, and anything not in the table. +pub(super) fn write_form(code: u32) -> Option { + use Comp::*; + let format = code & 0x1f; + let ty = (code >> 5) & 0xf; + let comps: &[Comp] = match format { + 1 => &[Index], + 2 => &[Lum], + 3 => &[Intensity], + 4 => &[Red], + 5 => &[Green], + 6 => &[Blue], + 7 => &[Alpha], + 8 => &[Lum, Alpha], + 9 => &[Red, Green, Blue], + 0xb => &[Red, Green, Blue, Alpha], + 0xc => &[Alpha, Blue, Green, Red], + 0xd => &[Blue, Green, Red], + 0x11 => &[Blue, Green, Red, Alpha], + _ => &[], + }; + // Whole components: type 2 bytes, 3 shorts, 4 words; 0xb and 0xc are + // bytes written into a 12-bit or an 8-bit RGBA drawable. + let comp_bytes = match ty { + 2 | 0xb | 0xc => 1, + 3 => 2, + 4 => 4, + _ => 0, + }; + if comp_bytes != 0 && !comps.is_empty() { + let mut c = [Red; 4]; + c[..comps.len()].copy_from_slice(comps); + let n = comps.len() as u8; + return Some(WriteForm { bytes: n as u32 * comp_bytes as u32, kind: WriteKind::Comps { comps: c, n, comp_bytes } }); + } + // Packed types. The format orders the fields from the top bit. + let order: &[Comp] = match format { + 9 | 0xd => match format { + 9 => &[Red, Green, Blue], + _ => &[Blue, Green, Red], + }, + 0xb => &[Red, Green, Blue, Alpha], + 0xc => &[Alpha, Blue, Green, Red], + 0x11 => &[Blue, Green, Red, Alpha], + 0x10 => &[Alpha, Red, Green, Blue], + _ => return None, + }; + let widths: &[u8] = match (ty, order.len()) { + (5, 3) => { + if order[0] == Blue { &[2, 3, 3] } else { &[3, 3, 2] } + } + (0xa, 3) => &[5, 6, 5], + (6, 4) => &[4, 4, 4, 4], + (7, 4) => { + if order[0] == Alpha { &[1, 5, 5, 5] } else { &[5, 5, 5, 1] } + } + (8, 4) => { + if order[0] == Alpha { &[2, 10, 10, 10] } else { &[10, 10, 10, 2] } + } + _ => return None, + }; + let total: u8 = widths.iter().sum(); + let mut fields = [NONE; 4]; + let mut shift = total; + + for (i, (&comp, &width)) in order.iter().zip(widths).enumerate() { + shift -= width; + fields[i] = f(comp, width, shift); + } + Some(WriteForm { bytes: total.div_ceil(8) as u32, kind: WriteKind::Packed { fields, n: order.len() as u8 } }) +} + +/// A component value `v` of `bits` bits as an 8-bit channel, by the GL +/// scaling and rounding. +fn scale8(v: u64, bits: u32) -> u32 { + let max = (1u64 << bits) - 1; + + ((v * 255 + max / 2) / max) as u32 +} + +impl WriteForm { + /// Whether a pixel of this form carries an alpha (or intensity) value the + /// board stores, rather than the model's 255. + pub fn has_alpha(&self) -> bool { + let any = |c: &[Comp]| c.iter().any(|&c| c == Comp::Alpha || c == Comp::Intensity); + + match &self.kind { + WriteKind::Comps { comps, n, .. } => any(&comps[..*n as usize]), + WriteKind::Packed { fields, n } => fields[..*n as usize].iter().any(|f| f.comp == Comp::Alpha), + } + } + + pub fn is_index(&self) -> bool { + matches!(&self.kind, WriteKind::Comps { comps, .. } if comps[0] == Comp::Index) + } + + /// The pixel in `b` (at least `bytes` long) as the plane word 0xRRGGBBAA, + /// or for an index form the raw index. + pub fn decode(&self, b: &[u8]) -> u32 { + let (mut r, mut g, mut bl, mut a) = (0u32, 0u32, 0u32, 0xffu32); + let mut put = |comp: Comp, v: u32| match comp { + Comp::Red => r = v, + Comp::Green => g = v, + Comp::Blue => bl = v, + Comp::Alpha => a = v, + Comp::Lum => { + r = v; + g = v; + bl = v; + } + Comp::Intensity => { + r = v; + g = v; + bl = v; + a = v; + } + Comp::Index => r = v, + }; + + match &self.kind { + WriteKind::Comps { comps, n, comp_bytes } => { + for (i, &comp) in comps[..*n as usize].iter().enumerate() { + let at = i * *comp_bytes as usize; + let v = match comp_bytes { + 1 => b[at] as u32, + 2 => scale8(u16::from_be_bytes([b[at], b[at + 1]]) as u64, 16), + _ => scale8(be32(&b[at..at + 4]) as u64, 32), + }; + + if comp == Comp::Index { + // The raw value: 12 index bits. + let raw = match comp_bytes { + 1 => b[at] as u32, + 2 => u16::from_be_bytes([b[at], b[at + 1]]) as u32, + _ => be32(&b[at..at + 4]), + }; + return raw & ODSY_INDEX_MASK; + } + put(comp, v); + } + } + WriteKind::Packed { fields, n } => { + let mut word = 0u64; + + for &byte in &b[..self.bytes as usize] { + word = word << 8 | byte as u64; + } + for fl in &fields[..*n as usize] { + let v = (word >> fl.shift) & ((1u64 << fl.width) - 1); + + put(fl.comp, scale8(v, fl.width as u32)); + } + } + } + (r << 24) | (g << 16) | (bl << 8) | a + } +} diff --git a/src/dev/odyssey/raster.rs b/src/dev/odyssey/raster.rs new file mode 100644 index 000000000..f5eb1a578 --- /dev/null +++ b/src/dev/odyssey/raster.rs @@ -0,0 +1,1756 @@ +//! The scalar rasteriser's core: the rules a draw follows where OpenGL +//! leaves a choice, the draw and fragment types, the fragment path (window +//! mapping, depth and stencil, blending, logic ops, fog, the colour mask) +//! and the draw entry point. Triangles, lines, pixel work and texturing +//! are in the other raster_*.rs files; raster_fixed_texture.rs is the +//! fixed-point fast path for textured triangles. +//! +//! Ported from include/hw/display/sgi/rasterizer.h in jamesbraid/qemu-sgi-origami. + + +use crate::dev::gl::vertex::ClipVertex; +use crate::devlog::LogModule; +use super::gl::{GlPixels, GlPlanes}; +use super::gl_context::{ + vtx, ColourTarget, GL_CLEAR, GL_CLIP_PLANES, GL_COLOUR_MASK_ALL, GL_COLOUR_MASK_ALPHA, + GL_COLOUR_MASK_RED, GL_CONSTANT_ALPHA, GL_CONSTANT_COLOR, GL_DECR, GL_DRAW_OK, GL_DST_ALPHA, + GL_DST_COLOR, GL_EQUAL, GL_FILL, GL_FUNC_REVERSE_SUBTRACT, GL_FUNC_SUBTRACT, GL_GEQUAL, + GL_GREATER, GL_INCR, GL_INVERT, GL_KEEP, GL_LEQUAL, GL_LESS, GL_LINES, GL_LINE_LOOP, + GL_LINE_STRIP, GL_LOGIC_OP, GL_MAX, GL_MAX_TARGETS, GL_MIN, GL_NEVER, GL_NOTEQUAL, + GL_OFFSET_FILL, GL_ONE_MINUS_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_COLOR, + GL_ONE_MINUS_DST_ALPHA, GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_SRC_COLOR, + GL_POINTS, GL_POLYGON, GL_QUADS, GL_QUAD_STRIP, GL_REPLACE, GL_SRC_ALPHA, + GL_SRC_ALPHA_SATURATE, GL_SRC_COLOR, GL_TRIANGLES, GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP, GlDraw, + GlDrawResult, GlDrawState, +}; +use super::memory::{CLIP_MAX, ClipList}; +use super::raster_fill::{Fill, FillDyn, FillRow}; +use super::raster_fixed_texture::{fixed_return, FIXED_PREFILL_MIN_PIXELS, SoftFixedTex}; +use super::raster_line::{soft_dimension, SoftLineStipple}; +use super::raster_pixels::x_logic_op; +use super::raster_texture::{SoftTexSelection, SoftTexSelectionMemo, SoftTexelCache}; +/// Which pixels a width-1 segment covers. +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] +pub(super) enum LineRule { + /// OpenGL's diamond-exit rule (1.2.1, 3.4.1) on the snapped grid. + #[default] + Diamond, + /// One pixel per major-axis column (or row) whose centre lies between the + /// ends, the start's side included: the minor-axis pixel holding the + /// segment at that centre, a crossing exactly on a pixel edge taking the + /// lower pixel. Width and smoothing as for `Diamond`. + CentreSampled, +} + +// Window coordinates truncate toward minus infinity onto the subpixel grid +// (inferred), through the shared VPro geometry (dev::gl::vertex::NearW); +// triangle edges are then exact integer functions of the snapped vertices (a +// model choice the vendor frames pass). Depths round half away from zero. A +// counter-clockwise triangle edge owns the pixel centres on it when it is a +// left or a top edge, rows counted up from the bottom of the plane +// (SoftVertex::owns()). Smoothed points and lines take the exact box filter's +// coverage. The board does not dither (Board::gl_apply_mode() reports +// GL_DITHER). + +#[derive(Clone, Default)] +pub(super) struct RasterRules { + /// window coordinates snap to 2^-n pixel + pub subpixel_bits: u8, + /// per channel of a stored colour + pub color_bits: u8, + /// of a stored depth + pub depth_bits: u8, + /// of a stored stencil value, 0 for planes with no stencil buffer + pub stencil_bits: u8, + pub line_rule: LineRule, + /// stored A width, zero means no alpha + pub alpha_bits: u8, + /// Zero derives precision from color/alpha bits. + pub alpha_test_bits: u8, + /// Smooth points, lines and polygons as their enables ask; without it + /// a smoothed primitive is drawn aliased and reported. + pub smooth_primitives: bool, + /// Depth linear in eye distance (a w-buffer): a vertex's window depth + /// is depth_row . e for its eye coordinates e, perspective-corrected + /// per fragment; a depth outside [0, 1] is dropped. + pub depth_row_given: bool, + pub depth_row: [f64; 4], + /// With depth_row, a second row whose product with the eye coordinates + /// divides the first's. Not given, the divisor is one. + pub depth_divisor_given: bool, + pub depth_divisor: [f64; 4], + /// With depth_row, how far outside [0, 1] a depth may lie and be + /// clamped rather than dropped. + pub depth_slack: f64, + /// Stored depth is the board's 24-bit float word of 2S times the window + /// depth (depth_word.rs), which orders as an integer; needs depth_bits + /// 24. A polygon offset then counts steps of that word. + pub depth_float: bool, +} + +/// A vertex in clip coordinates, as the shared GL core's clipper +/// (`dev::gl::vertex::Clip`, under `NearW`) takes and makes it. Clipping is +/// in single precision: a model choice, since the board's clip precision is +/// unknown. The window position and 1/w come from this clip result. +#[derive(Clone, Copy)] +pub(super) struct VolumeVertex { + pub clip: [f32; 4], + pub eye: [f32; 4], + pub attr: [f32; vtx::FLOATS], +} + +impl Default for VolumeVertex { + fn default() -> Self { + VolumeVertex { clip: [0.0; 4], eye: [0.0; 4], attr: [0.0; vtx::FLOATS] } + } +} + +impl VolumeVertex { + /// The point at t along from self to out, in clip and eye coordinates + /// and every float attribute (2.13.8); the material index and edge flag + /// are bit patterns and stay self's. + pub fn lerp(&self, out: &VolumeVertex, t: f32) -> VolumeVertex { + let mut r = VolumeVertex::default(); + // The shared core's crossing (`dev::gl::vertex::clip_cross`). + let l = |x: f32, y: f32| x + (y - x) * t; + + for k in 0..4usize { + r.clip[k] = l(self.clip[k], out.clip[k]); + r.eye[k] = l(self.eye[k], out.eye[k]); + } + for k in 0..vtx::FLOATS { + r.attr[k] = if k == vtx::MATERIAL || k == vtx::EDGE { self.attr[k] } else { l(self.attr[k], out.attr[k]) }; + } + r + } +} + +impl ClipVertex for VolumeVertex { + fn h(&self) -> &[f32; 4] { + &self.clip + } + fn e(&self) -> &[f32; 4] { + &self.eye + } + /// Found from the inside end of the edge, so that two primitives + /// sharing the edge make the same vertex. + fn cross(a: &Self, b: &Self, da: f32, db: f32) -> Self { + if da >= 0.0 { a.lerp(b, da / (da - db)) } else { b.lerp(a, db / (db - da)) } + } + const EDGE_FLAGS: bool = true; + fn edge(&self) -> bool { + self.attr[vtx::EDGE].to_bits() != 0 + } + fn set_edge(&mut self, edge: bool) { + self.attr[vtx::EDGE] = f32::from_bits(edge as u32); + } +} + +/// The most vertices clipping a triangle leaves: one more per view-volume +/// plane that clips (`NearW` has five). usize so it can size arrays. +pub(super) const VOLUME_TRIANGLE_MAX: usize = 3 + 6; + +pub(super) const GL_TEX_LEVELS_MAX: u32 = 31; + +/// One level of a texture image, as the board describes it. The `read_*` +/// flags say which reads the draw's Texels answer for it. +#[derive(Clone, Copy, Default)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub(super) struct GlTexLevel { + pub level: u32, + pub width: u32, + pub height: u32, + pub depth: u32, + pub base_format: u32, + pub internal_format: u32, + pub border: u32, + /// The image's detail texture rather than a level of its pyramid. + pub detail: bool, + pub read_texel: bool, + /// Border indices range from -1 through width/height, inclusive. + pub read_border_texel: bool, + pub read_texel_3d: bool, + /// Each signed axis ranges from -1 through its interior dimension. + pub read_border_texel_3d: bool, +} + +#[derive(Clone, Copy, Default)] +pub(super) struct GlTexPyramid<'a> { + pub levels: &'a [GlTexLevel], + pub base_level: u32, + pub max_level: u32, + pub min_lod: f64, + pub max_lod: f64, +} + +/// One board-owned level-zero image and its texture object's sampler. +/// volume selects 3D sampling, one_dimensional S-only sampling; they are +/// mutually exclusive. Dimensions count interior texels. Texels return +/// canonical RRGGBBAA. Immutable through each synchronous draw. +#[derive(Clone, Copy, Default)] +pub(super) struct GlTexImage<'a> { + pub width: u32, + pub height: u32, + pub depth: u32, + pub base_format: u32, + pub border: u32, + pub min_filter: u32, + pub mag_filter: u32, + pub wrap_s: u32, + pub wrap_t: u32, + pub wrap_r: u32, + pub border_color: [f32; 4], + pub read_texel: bool, + pub read_texel_3d: bool, + pub volume: bool, + pub one_dimensional: bool, + /// None selects the level-zero image fields. + pub pyramid: Option>, + /// Required for a 2D image with border == 1; outside the interior only. + pub read_border_texel: bool, + /// Positive per-axis derivative factors; zero preserves identity. + pub lod_scale_s: f64, + pub lod_scale_t: f64, + /// Texels::post_filter() applies to the expanded RGBA after filtering, + /// before the texture environment. + pub post_filter: bool, + /// Required for a volume with border == 1, including depth one. + pub read_border_texel_3d: bool, + /// Positive volume R derivative factor; zero preserves identity. + pub lod_scale_r: f64, + /// Texels::filter_weight() replaces the public default curve; lambda is + /// already clamped. + pub read_filter_weight: bool, + /// Level-zero detail image; L <= 0, mode ADD or MODULATE. + pub detail_image: Option, + pub detail_level: i32, + pub detail_mode: u32, + /// Zero disables comparison. SGIX operators compare depth to projected R. + pub shadow_compare: u32, + pub shadow_ambient: f64, + /// GL_SGIX_texture_coordinate_clamp: with max_clamp set, a CLAMP axis + /// clamps to [0, max_clamp_*] instead of [0, 1]. + pub max_clamp: bool, + pub max_clamp_s: f64, + pub max_clamp_t: f64, + pub max_clamp_r: f64, +} + +/// Where a draw's texels come from: the board's texture storage +/// (BoardTexels), or a test's. `level` is one of the image's pyramid levels +/// or its detail image, or None for the level-zero image itself. A read +/// outside the storage is None. +pub(super) trait Texels { + fn texel(&self, level: Option<&GlTexLevel>, x: u32, y: u32) -> Option; + fn border_texel(&self, level: Option<&GlTexLevel>, x: i32, y: i32) -> Option; + fn texel_3d(&self, level: Option<&GlTexLevel>, x: u32, y: u32, z: u32) -> Option; + fn border_texel_3d(&self, level: Option<&GlTexLevel>, x: i32, y: i32, z: i32) -> Option; + /// The image's post-filter (GlTexImage::post_filter). + fn post_filter(&self, rgba: &mut [f64; 4]) { + let _ = rgba; + } + /// The detail and sharpen weight at lambda (GlTexImage::read_filter_weight). + fn filter_weight(&self, lambda: f64) -> Option { + let _ = lambda; + None + } +} + +/// A draw without a texture reads no texels. +pub(super) struct NoTexels; + +impl Texels for NoTexels { + fn texel(&self, _: Option<&GlTexLevel>, _: u32, _: u32) -> Option { + None + } + fn border_texel(&self, _: Option<&GlTexLevel>, _: i32, _: i32) -> Option { + None + } + fn texel_3d(&self, _: Option<&GlTexLevel>, _: u32, _: u32, _: u32) -> Option { + None + } + fn border_texel_3d(&self, _: Option<&GlTexLevel>, _: i32, _: i32, _: i32) -> Option { + None + } +} + +/// Quantized scalar channels, each normalized by its own declared maximum. +#[derive(Clone, Copy, Default)] +pub(super) struct RasterColour { + /// R, G, B, A; absent alpha is canonical zero + pub component: [u32; 4], +} + +/// A pixel's colour, depth and stencil as a draw reaches them, in plane +/// pixels: the board's planes whole (GlPlanes) or one row a row loop +/// borrows (GlPlaneRow), so that the loops that run on both are one source. +/// target is one of the drawable's colour buffers. The planes are shared, so +/// a store goes through interior mutability. +pub(super) trait Planes { + fn store_rgba(&self, target: ColourTarget, x: i32, y: i32, colour: &RasterColour); + /// The colour at (x, y); None where it cannot be read. + fn load_rgba(&self, target: ColourTarget, x: i32, y: i32) -> Option; + fn load_depth(&self, x: i32, y: i32) -> u32; + fn store_depth(&self, x: i32, y: i32, depth: u32); + fn load_stencil(&self, x: i32, y: i32) -> u32; + fn store_stencil(&self, x: i32, y: i32, stencil: u32); +} + +/// How a draw stores colour, and the board state it takes at the start. +#[derive(Clone, Default)] +pub(super) struct RasterTarget<'a> { + /// Colour-index mode: the board stores the index packed in the colour + /// word (GlPlanes::store_colour()) and cannot read it back, so blending, + /// logical operations and partial colour masks are refused. Otherwise + /// RGBA, stored and read through Planes. + pub index: bool, + /// Current binding; resolved once by the board before drawing. + pub texture: Option>, + /// An opaque polygon stipple: a fragment whose stipple bit is clear + /// takes stipple_background. + pub stipple_opaque: bool, + pub stipple_background: [f32; 4], +} + +/// How filled triangles sample a texture, for tests (raster_set_texture_path()): +/// the row loops, the fixed-point sampler alone, or the double-precision path. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) enum TexturePath { + Spans, + Fixed, + Double, +} + +/// C's max and min: the second operand wins a tie and a NaN on either side, +/// where f64::max and f64::min give the number. The rasteriser's arithmetic +/// is written against this order. +#[inline(always)] +pub(super) fn cmax(a: T, b: T) -> T { + if a > b { a } else { b } +} +#[inline(always)] +pub(super) fn cmin(a: T, b: T) -> T { + if a < b { a } else { b } +} + +// --------------------------------------------------------------------- +// The draw itself. The fixed-point texture path is in raster_fixed_texture.rs. +// --------------------------------------------------------------------- + +/// The drawable's window: its origin and the bounds of the pixels it owns, +/// in plane pixels, and the rectangles themselves. +#[derive(Clone, Copy, Default)] +pub(super) struct SoftWindow { + pub ox: i32, + pub oy: i32, + pub x0: i32, + pub y0: i32, + pub x1: i32, + pub y1: i32, + pub clip: ClipList, +} + +/// Once-a-draw reports. +#[derive(Default)] +pub(super) struct SoftReports { + pub texture: std::sync::atomic::AtomicU32, + pub rgba: std::sync::atomic::AtomicBool, +} + +/// One draw as the rasteriser runs it: its rules, state and planes, and +/// what its setup decided. +#[derive(Clone, Copy)] +pub(super) struct SoftDraw<'a> { + pub rules: &'a RasterRules, + pub t: &'a RasterTarget<'a>, + pub st: &'a GlDrawState, + pub texels: &'a dyn Texels, + pub reports: &'a SoftReports, + /// The board's planes. + pub px: &'a GlPlanes<'a>, + /// This thread's texel cache, when texels read in this draw are kept. + pub texels_cache: Option<&'a std::cell::RefCell>, + pub win: SoftWindow, + pub targets: [ColourTarget; GL_MAX_TARGETS as usize], + pub ntargets: u32, + /// Back-facing polygons take the vertices' back colours. + pub back_colors: bool, + /// Fragments blend with the colour they replace. + pub blend: bool, + /// Fragments combine with it by the logical operation instead. + pub logic_op: bool, + /// In RGBA mode, the packed colour's bits glColorMask lets through. + pub colour_mask: u32, + pub texture: Option<&'a GlTexImage<'a>>, + pub texture_base: Option<&'a GlTexLevel>, + pub texture_last: u32, + pub point_size: u32, + pub line_width: u32, + pub point_smooth: bool, + pub line_smooth: bool, + pub smooth_point_size: f64, + pub smooth_line_width: f64, + pub polygon_smooth: bool, + pub polygon_stipple: Option<&'a [u32; 32]>, + pub rgba_mask: u32, + pub rgba_read: bool, + /// Filled triangles sample in fixed point. + pub fixed: Option<&'a SoftFixedTex>, + /// The row loop untextured filled triangles take, or None for the + /// general per-fragment path (SoftDraw::fill_select()). + pub fill: Option, +} + +impl<'a> SoftDraw<'a> { + /// A draw under rules into target, in state st, with nothing else + /// decided yet. + pub fn new(rules: &'a RasterRules, t: &'a RasterTarget<'a>, st: &'a GlDrawState, reports: &'a SoftReports, px: &'a GlPlanes<'a>) -> Self { + SoftDraw { + rules, + t, + st, + texels: &NoTexels, + reports, + px, + texels_cache: None, + win: SoftWindow::default(), + targets: [0; GL_MAX_TARGETS as usize], + ntargets: 0, + back_colors: false, + blend: false, + logic_op: false, + colour_mask: 0, + texture: None, + texture_base: None, + texture_last: 0, + point_size: 0, + line_width: 0, + point_smooth: false, + line_smooth: false, + smooth_point_size: 0.0, + smooth_line_width: 0.0, + polygon_smooth: false, + polygon_stipple: None, + rgba_mask: 0, + rgba_read: false, + fixed: None, + fill: None, + } + } + + /// The draw's texture, which a caller has checked is there. + pub fn image(&self) -> &'a GlTexImage<'a> { + self.texture.expect("a textured draw") + } + + /// The base level of the draw's pyramid, which a caller has checked is + /// there. + pub fn base_level(&self) -> &'a GlTexLevel { + self.texture_base.expect("a texture pyramid") + } +} + +/// A vertex after transformation, ready to rasterize. +#[derive(Clone, Copy)] +pub(super) struct SoftVertex { + /// window position in subpixels + pub x: i64, + pub y: i64, + /// normalized depth; [0, 1] inside the view volume + pub z: f64, + /// 1 / clip w, for perspective-correct colour + pub inv_w: f64, + /// the rules' depth divisor over clip w + pub depth_w: f64, + pub rgba: [f64; 4], + pub texcoord: [f64; 4], + /// Each client clip plane's value at the vertex, kept where >= 0. + pub clip: [f64; GL_CLIP_PLANES as usize], +} + +impl Default for SoftVertex { + fn default() -> Self { + SoftVertex { + x: 0, + y: 0, + z: 0.0, + inv_w: 0.0, + depth_w: 0.0, + rgba: [0.0; 4], + texcoord: [0.0; 4], + clip: [0.0; GL_CLIP_PLANES as usize], + } + } +} + +/// A fragment's values as rasterization leaves them (OpenGL 1.2.1, 3.1), +/// before the per-fragment operations of chapter 4: its colour, each +/// component in [0, 1], and its texture coordinates. +#[derive(Clone, Copy, Default)] +pub(super) struct SoftFrag { + pub rgba: [f64; 4], + pub texcoord: [f64; 4], + pub texture_rho: f64, + /// With fixed set, the texel the fixed-point sampler filtered for the + /// fragment, which the texture environment applies in place of + /// sampling texcoord. + pub fixed: bool, + pub texel: [f64; 4], +} + +impl GlDrawState { + pub fn window(&self) -> Option { + let d = &self.drawable; + let mut win = SoftWindow::default(); + + if !d.bound { + crate::dlog!(LogModule::Odyssey, "sgi-gl: draw with no window bound dropped"); + return None; + } + if d.clip.n == 0 { + // Other windows cover all of this one: nothing to draw. + return None; + } + win.ox = d.origin[0]; + win.oy = d.origin[1]; + win.x0 = d.clip.r[0].x0; + win.y0 = d.clip.r[0].y0; + win.x1 = d.clip.r[0].x1; + win.y1 = d.clip.r[0].y1; + for r in &d.clip.r[1..std::cmp::min(d.clip.n as usize, CLIP_MAX)] { + win.x0 = win.x0.min(r.x0); + win.y0 = win.y0.min(r.y0); + win.x1 = win.x1.max(r.x1); + win.y1 = win.y1.max(r.y1); + } + win.clip = d.clip; + Some(win) + } +} + +impl RasterRules { + pub fn rules_supported(&self) -> bool { + self.subpixel_bits >= 1 && self.subpixel_bits <= 12 && + self.color_bits >= 1 && self.color_bits <= 10 && + self.depth_bits >= 1 && self.depth_bits <= 32 && + (!self.depth_float || self.depth_bits == 24) && + (!self.depth_row_given || + (self.depth_row[0].is_finite() && self.depth_row[1].is_finite() && + self.depth_row[2].is_finite() && self.depth_row[3].is_finite())) && + (!self.depth_divisor_given || + (self.depth_row_given && + self.depth_divisor[0].is_finite() && self.depth_divisor[1].is_finite() && + self.depth_divisor[2].is_finite() && self.depth_divisor[3].is_finite())) && + self.depth_slack >= 0.0 && self.depth_slack < 1.0 && + self.stencil_bits <= 16 && self.alpha_bits <= 10 && + self.alpha_test_bits <= 16 + } + + pub fn check_rules(&self) -> bool { + if !self.rules_supported() { + crate::dlog!(LogModule::Odyssey, "sgi-gl: raster rules are not implemented; nothing drawn"); + return false; + } + true + } +} + +impl RasterRules { + /// x rounded to an integer, half away from zero, without a libm call: + /// x less its truncation is exact, so a fraction of a half or more + /// rounds away from zero. + #[inline] + pub fn round(&self, x: f64) -> i64 { + let t = x as i64; + let frac = x - t as f64; + t + (frac >= 0.5) as i64 - (frac <= -0.5) as i64 + } + + /// The largest stored depth. + #[inline] + pub fn depth_max(&self) -> u32 { + u32::MAX.wrapping_shr(32u32.wrapping_sub(self.depth_bits as u32)) + } + + /// The stored code of window depth z in [0, 1]: an integer fraction of + /// depth_max, or with depth_float the board's depth word for 2S z + /// (depth_word::word_for_fraction()). + #[inline] + pub fn depth_code(&self, z: f64) -> u32 { + if !self.depth_float { + return self.round(z * self.depth_max() as f64) as u32; + } + super::depth_word::word_for_fraction(z) + } + + /// A polygon offset (RasterRules::offset_amount(), in window depth) split + /// into what adds to the window depth before the code and the steps that + /// add to the code after it. With depth_float the amount is the 0x058 + /// field over 2^24 (polygon::OFFSET_SCALE), a step count the board + /// adds to the encoded word (depth_word::word_with_offset(); the + /// placement is INFERRED), so none + /// of it adds to window depth. + pub fn split_offset(&self, offset: f64) -> (f64, i64) { + if !self.depth_float { + return (offset, 0); + } + (0.0, (offset * (1u64 << 24) as f64).round() as i64) + } + + /// [`depth_code`](Self::depth_code) of z, then `steps` of polygon offset. + pub fn depth_code_offset(&self, z: f64, steps: i64) -> u32 { + let code = self.depth_code(z); + + if self.depth_float { + super::depth_word::word_with_offset(code, steps) + } else { + code + } + } + + /// A component clamped to [0, 1] and scaled to the rules' colour bits as + /// floor(c * 2^bits) limited to the largest value: 256c limited to 255 + /// for eight bits. ESTABLISHED (vendor simulation): the frames allow + /// floor(256c + b) for b in 0 to 0.2 and test no value below 0.1. + #[inline] + pub fn channel(&self, c: f64) -> u32 { + let max = (1u32.wrapping_shl(self.color_bits as u32)).wrapping_sub(1); + + if !(c > 0.0) { + return 0; + } + if c >= 1.0 { + return max; + } + cmin((c * (max as f64 + 1.0)) as u32, max) + } + + /// Incoming alpha precision is independent of absent or narrower stored A. + #[inline] + pub fn alpha_max(&self) -> u32 { + (1u32.wrapping_shl(std::cmp::max(2, std::cmp::max(self.color_bits, self.alpha_bits)) as u32)).wrapping_sub(1) + } + + #[inline] + pub fn alpha(&self, value: f64) -> u32 { + let max = self.alpha_max(); + + if !(value > 0.0) { + 0 + } else if value >= 1.0 { + max + } else { + cmin((value * (max as f64 + 1.0)) as u32, max) + } + } + + /// The alpha-test comparison value: min(4095, floor(4096 a)) at the 12-bit + /// reference. INFERRED (graded) from the vendor frames. + pub fn soft_alpha_test(&self, value: f64) -> u32 { + let max = if self.alpha_test_bits != 0 { + (1u32.wrapping_shl(self.alpha_test_bits as u32)).wrapping_sub(1) + } else { + self.alpha_max() + }; + + if !(value > 0.0) { + 0 + } else if value >= 1.0 { + max + } else { + cmin((value * (max as f64 + 1.0)) as u32, max) + } + } + + /// The internal floor conversion precedes storage truncation, even on clears. + pub fn stored_alpha(&self, value: f64) -> u32 { + let m = self.alpha_max(); + if m == 0 { + return 0; + } + let a = self.alpha(value); + let stored = (1u32.wrapping_shl(self.alpha_bits as u32)).wrapping_sub(1); + + // Scaling by the same number twice over leaves a, unless the product + // wraps. + if stored == m && m <= 0xffff { + return a; + } + a.wrapping_mul(stored) / m + } + + #[inline] + pub fn rgba_mask(&self, mask: u32) -> u32 { + mask & if self.alpha_bits != 0 { GL_COLOUR_MASK_ALL } else { GL_COLOUR_MASK_ALPHA - 1 } + } + + /// Read every selected destination before any colour/depth/stencil write. + #[allow(clippy::too_many_arguments, reason = "the fragment path takes a fragment as its parts")] + #[inline] + pub fn rgba_load(&self, px: &P, targets: &[ColourTarget], count: u32, x: i32, y: i32, dest: &mut [RasterColour; GL_MAX_TARGETS as usize], reported: &std::sync::atomic::AtomicBool) -> bool { + for i in 0..count as usize { + let loaded = px.load_rgba(targets[i], x, y); + let mut valid = loaded.is_some(); + if let Some(c) = loaded { + dest[i] = c; + } + + let mut k = 0; + while valid && k < 4 { + let bits = if k == 3 { self.alpha_bits } else { self.color_bits }; + + // An absent A is ignored, including its returned representation. + valid = bits == 0 || dest[i].component[k] < (1u32 << bits); + k += 1; + } + if !valid { + if !reported.swap(true, std::sync::atomic::Ordering::Relaxed) { + crate::dlog!(LogModule::Odyssey, "sgi-gl: RGBA destination read failed; pixel writes dropped"); + } + return false; + } + if self.alpha_bits == 0 { + dest[i].component[3] = 0; + } + } + true + } + + /// A colour, each component clamped and rounded to the rules' bits. + #[inline] + pub fn rgb(&self, c: &[f64]) -> u32 { + let bits = self.color_bits as u32; + let max = (1u32.wrapping_shl(bits)).wrapping_sub(1); + let maxf = max as f64; + // channel() with its largest value taken once. + let channel = |c: f64| -> u32 { + if !(c > 0.0) { + 0 + } else if c >= 1.0 { + max + } else { + cmin((c * (maxf + 1.0)) as u32, max) + } + }; + + (channel(c[0]).wrapping_shl(2 * bits)) | (channel(c[1]).wrapping_shl(bits)) | channel(c[2]) + } +} + +/// c / 255 for a component c of eight bits, exactly as the division gives it. +pub(super) static SOFT_UNIT8: [f64; 256] = { + let mut t = [0.0f64; 256]; + let mut i = 0; + + while i < 256 { + t[i] = i as f64 / 255.0; + i += 1; + } + t +}; + +/// c as a fraction of max, the largest value of its bits: c / max, from +/// SOFT_UNIT8 for eight bits, whose table is that division done once. +#[inline(always)] +pub(super) fn soft_unit(c: u32, max: u32) -> f64 { + match SOFT_UNIT8.get(c as usize) { + Some(&u) if max == 255 => u, + _ => c as f64 / max as f64, + } +} + +impl SoftVertex { + pub fn edge(&self, b: &SoftVertex, px: i64, py: i64) -> i64 { + (b.x.wrapping_sub(self.x)).wrapping_mul(py.wrapping_sub(self.y)) + .wrapping_sub((b.y.wrapping_sub(self.y)).wrapping_mul(px.wrapping_sub(self.x))) + } + + /// The tie rule: a counter-clockwise edge owns the pixel + /// centres on it when it is a left edge (running down) or a top edge + /// (horizontal, running left), with rows counted up from the bottom of the + /// plane, so two triangles sharing an edge never both draw its centres, as + /// OpenGL 1.2.1, 3.5.1 requires. + pub fn owns(&self, b: &SoftVertex, e: i64) -> bool { + if e != 0 { + return e > 0; + } + b.y < self.y || (b.y == self.y && b.x < self.x) + } + + /// Narrow [*lo, *hi], pixel steps along a row from a start whose edge value + /// is e, to the steps where SoftVertex::owns() holds for the edge from a to b, + /// whose value moves by step a pixel. The edge function is linear along the + /// row, so the owned steps are one run: e + k * step >= need, where need is + /// 0 for an edge that owns the centres on it and 1 for one that does not. + pub fn owned_run(&self, b: &SoftVertex, e: i64, step: i64, (mut lo, mut hi): (i64, i64)) -> (i64, i64) { + let need: i64 = if self.owns(b, 0) { 0 } else { 1 }; + let d = need.wrapping_sub(e); + + if step > 0 { + // The first k with k * step >= d, rounding the quotient up. + let v = if d > 0 { (d + step - 1) / step } else { -(-d / step) }; + lo = std::cmp::max(lo, v); + } else if step < 0 { + // The last k with k * -step <= -d, rounding the quotient down. + let v = if -d >= 0 { -d / -step } else { -((d - step - 1) / -step) }; + hi = std::cmp::min(hi, v); + } else if d > 0 { + hi = lo - 1; + } + (lo, hi) + } +} + +/// Whether value passes a test comparing it with ref by func, the +/// comparison minus GL_NEVER: the depth test's incoming depth against the +/// stored one (4.1.5), the alpha test's alpha against its reference +/// (4.1.4), the stencil test's masked reference against the masked stored +/// value (4.1.5). +#[inline] +pub(super) fn soft_compare(func: u32, value: u32, ref_: u32) -> bool { + match func { + GL_NEVER => false, + GL_LESS => value < ref_, + GL_EQUAL => value == ref_, + GL_LEQUAL => value <= ref_, + GL_GREATER => value > ref_, + GL_NOTEQUAL => value != ref_, + GL_GEQUAL => value >= ref_, + _ => true, + } +} + +impl RasterRules { + pub fn alpha_test(&self, func: u32, alpha: f64, ref_: f64) -> bool { + soft_compare(func, self.soft_alpha_test(alpha), self.soft_alpha_test(ref_)) + } + + /// The stencil buffer's largest value, if the planes have one. + pub fn stencil_max(&self) -> Option { + (self.stencil_bits != 0).then(|| (1u32.wrapping_shl(self.stencil_bits as u32)).wrapping_sub(1)) + } +} + +impl GlDrawState { + /// One of glStencilOp's operations on the stencil value at (x, y), the + /// result written under the stencil write mask (4.1.5, 4.2.2). GL_INCR + /// and GL_DECR clamp to the buffer's range. + #[inline(always)] + pub fn stencil_op(&self, px: &P, x: i32, y: i32, op: u32, max: u32, ref_: u32) { + let old = px.load_stencil(x, y) & max; + let mask = self.stencil_write_mask & max; + + let value: u32 = match op { + GL_KEEP => return, + GL_REPLACE => ref_, + GL_INCR => if old < max { old + 1 } else { max }, + GL_DECR => if old != 0 { old - 1 } else { 0 }, + GL_INVERT => !old & max, + _ => 0, + }; + px.store_stencil(x, y, (old & !mask) | (value & mask)); + } +} + +impl RasterRules { + #[allow(clippy::too_many_arguments, reason = "the fragment path takes a fragment as its parts")] + pub fn depth_stencil(&self, st: &GlDrawState, px: &GlPlanes, x: i32, y: i32, z: f64, steps: i64) -> bool { + let mut sref: u32 = 0; + + if !(0.0..=1.0).contains(&z) { + return false; + } + // The reference clamps to the buffer's range; with no stencil buffer + // the test passes and nothing is written. + let smax = if st.stencil_test { self.stencil_max() } else { None }; + let stencil = smax.is_some(); + let smax = smax.unwrap_or(0); + if stencil { + let vmask = st.stencil_value_mask; + + sref = if st.stencil_ref < 0 { 0 } else { std::cmp::min(st.stencil_ref as u32, smax) }; + if !soft_compare( + st.stencil_func, + sref & vmask, + px.load_stencil(x, y) & smax & vmask, + ) { + st.stencil_op(px, x, y, st.stencil_op[0], smax, sref); + return false; + } + } + let zq = self.depth_code_offset(z, steps); + if st.depth_test { + if !soft_compare(st.depth_func, zq, px.load_depth(x, y)) { + if stencil { + st.stencil_op(px, x, y, st.stencil_op[1], smax, sref); + } + return false; + } + if st.depth_mask { + px.store_depth(x, y, zq); + } + } + // A disabled depth test passes (4.1.5). + if stencil { + st.stencil_op(px, x, y, st.stencil_op[2], smax, sref); + } + true + } +} + +/// One blend factor (OpenGL 1.2.1, tables 4.1 and 4.2) for component k, +/// red to alpha, from the source s, destination d and constant colour c. +#[inline] +pub(super) fn soft_blend_factor(factor: u32, k: usize, s: &[f64; 4], d: &[f64; 4], c: &[f32; 4]) -> f64 { + match factor { + 1 => 1.0, + GL_SRC_COLOR => s[k], + GL_ONE_MINUS_SRC_COLOR => 1.0 - s[k], + GL_DST_COLOR => d[k], + GL_ONE_MINUS_DST_COLOR => 1.0 - d[k], + GL_SRC_ALPHA => s[3], + GL_ONE_MINUS_SRC_ALPHA => 1.0 - s[3], + GL_DST_ALPHA => d[3], + GL_ONE_MINUS_DST_ALPHA => 1.0 - d[3], + GL_SRC_ALPHA_SATURATE => if k == 3 { 1.0 } else { cmin(s[3], 1.0 - d[3]) }, + GL_CONSTANT_COLOR => c[k] as f64, + GL_ONE_MINUS_CONSTANT_COLOR => 1.0 - c[k] as f64, + GL_CONSTANT_ALPHA => c[3] as f64, + GL_ONE_MINUS_CONSTANT_ALPHA => 1.0 - c[3] as f64, + _ => 0.0, + } +} + +/// glLogicOp's operation (4.1.8, table 4.3) on the source s and the +/// destination d, bit by bit. +pub(super) fn soft_logic_op(op: u32, s: u32, d: u32) -> u32 { + x_logic_op(op.wrapping_sub(GL_CLEAR), s, d) +} + +impl RasterRules { + /// RGB retains its existing arithmetic; spatial alpha dithering is omitted. + #[inline] + pub fn rgba_value(&self, rgb: u32, alpha: f64) -> RasterColour { + let mut c = RasterColour::default(); + let max = (1u32 << self.color_bits) - 1; + + for (k, comp) in c.component.iter_mut().enumerate().take(3) { + *comp = (rgb >> ((2 - k as u32) * self.color_bits as u32)) & max; + } + c.component[3] = self.stored_alpha(alpha); + c + } +} + +impl SoftDraw<'_> { + /// The colour a fragment stores over `dest`, as the draw's blend, logical + /// operation and colour mask make it. M names the state a specialised + /// row loop has fixed (raster_fill.rs); `FillDyn` reads all of it. + #[inline(always)] + pub fn rgba_result(&self, frag: &SoftFrag, dest: &RasterColour) -> RasterColour { + let mut out = RasterColour::default(); + let rules = self.rules; + let max = (1u32 << rules.color_bits) - 1; + let alpha_max = rules.alpha_max(); + let alpha_q = rules.alpha(frag.rgba[3]); + let alpha = soft_unit(alpha_q, alpha_max); + let blend = M::blend(self); + let rgb = rules.rgb(&frag.rgba); + + if !blend && !self.logic_op && self.rgba_mask == rules.rgba_mask(GL_COLOUR_MASK_ALL) { + // RasterRules::rgba_value()'s stored alpha, RasterRules::alpha() of + // alpha_q / alpha_max scaled to the stored bits, without the round + // trip: alpha_q / alpha_max * alpha_max + 0.5 truncates to alpha_q + // for every alpha_q up to 1023. + let stored = (1u32.wrapping_shl(rules.alpha_bits as u32)).wrapping_sub(1); + + for (k, comp) in out.component.iter_mut().enumerate().take(3) { + *comp = (rgb >> ((2 - k as u32) * rules.color_bits as u32)) & max; + } + out.component[3] = if stored == alpha_max { alpha_q } else { alpha_q.wrapping_mul(stored) / alpha_max }; + return out; + } + out = rules.rgba_value(rgb, alpha); + if blend { + let mut src = [0.0f64; 4]; + let mut dst = [0.0f64; 4]; + let mut blended = [0.0f64; 4]; + + for k in 0..3usize { + src[k] = soft_unit(out.component[k], max); + dst[k] = soft_unit(dest.component[k], max); + } + src[3] = alpha; + dst[3] = if rules.alpha_bits != 0 { + soft_unit(dest.component[3], (1u32 << rules.alpha_bits) - 1) + } else { + 1.0 + }; + for (k, b) in blended.iter_mut().enumerate() { + *b = self.fill_blend_component::(k, &src, &dst); + } + out = rules.rgba_value(rules.rgb(&blended), blended[3]); + } + for k in 0..4usize { + let bits = if k == 3 { rules.alpha_bits } else { rules.color_bits } as u32; + + if self.logic_op { + out.component[k] = soft_logic_op(self.st.logic_op_mode, out.component[k], dest.component[k]) + & (1u32.wrapping_shl(bits)).wrapping_sub(1); + } + if self.rgba_mask & (1u32 << k) == 0 { + out.component[k] = dest.component[k]; + } + } + out + } +} + +impl RasterRules { + /// The bits of a packed colour of the rules' colour bits that a + /// glColorMask channel mask lets through. The planes hold no alpha, so + /// its bit has no effect. + pub fn colour_mask(&self, mask: u32) -> u32 { + let channel = (1u32 << self.color_bits) - 1; + let mut out = 0u32; + + for k in 0..3u32 { + if mask & (GL_COLOUR_MASK_RED << k) != 0 { + out |= channel << ((2 - k) * self.color_bits as u32); + } + } + out + } + + /// The packed colour's bits, all of which a full mask writes. + pub fn colour_bits(&self) -> u32 { + self.colour_mask(GL_COLOUR_MASK_ALL) + } +} + +impl SoftDraw<'_> { + /// Which of the operations that read the stored colour a draw state + /// enables. The logical operation applies to an RGBA colour while + /// GL_COLOR_LOGIC_OP is enabled, or while blending is enabled with + /// EXT_blend_logic_op's GL_LOGIC_OP equation; it replaces blending + /// (4.1.8). A colour mask with some channels clear keeps the stored + /// bits of those (4.2.2). Colour-index drawing cannot read the planes + /// back (RasterTarget::index), so it draws as if the operations were + /// disabled and the mask were all ones, logged once for the draw. + pub fn setup_ops(&mut self) { + let st = self.st; + + self.logic_op = st.color_logic_op || (st.blend && st.blend_equation == GL_LOGIC_OP); + self.blend = st.blend && !self.logic_op; + self.colour_mask = self.rules.colour_mask(st.colour_mask); + self.rgba_mask = self.rules.rgba_mask(st.colour_mask); + self.rgba_read = self.ntargets != 0 && self.rgba_mask != 0 && + (self.blend || self.logic_op || + self.rgba_mask != self.rules.rgba_mask(GL_COLOUR_MASK_ALL)); + if self.t.index && + (self.blend || self.logic_op || (self.colour_mask != 0 && self.colour_mask != self.rules.colour_bits())) + { + crate::dlog!(LogModule::Odyssey, "sgi-gl: blending, logical operations and partial colour masks need planes that can be read back; drawn without them"); + self.blend = false; + self.logic_op = false; + self.colour_mask = self.rules.colour_bits(); + } + } +} + +pub(super) const FIXED_LEVELS: usize = 14; +pub(super) const FIXED_FAILED: u32 = 0x80000000; + +/// One texel of the fixed-point sampler's store: the packed RGBA8 texel, +/// components expanded, in lanes (soft_fixed_lanes()), and the tag of the +/// draw that read it (SoftDraw::fixed_texel()). Side by side, since a tap +/// reads the tag and then the lanes, and a bilinear tap's two texels in a +/// row share a cache line. +#[derive(Clone, Copy, Default)] +pub(super) struct SoftFixedCell { + pub lanes: u64, + pub tag: u32, +} + +/// The fixed-point sampler's texels of one level for one draw, by interior +/// index. +#[derive(Clone, Default)] +pub(super) struct SoftFixedStore { + pub cells: Vec, + pub size: usize, + pub plain: Vec, +} + +thread_local! { + pub(super) static SOFT_TEXEL_CACHE: std::cell::RefCell = std::cell::RefCell::new(SoftTexelCache::new()); +} + +/// Run f with this thread's texel cache, begun for a new draw. +pub(super) fn with_texel_cache(f: impl FnOnce(&std::cell::RefCell) -> R) -> R { + SOFT_TEXEL_CACHE.with(|cache| { + cache.borrow_mut().begin(); + f(cache) + }) +} + +thread_local! { + pub(super) static SOFT_TEXTURE_SELECTED: std::cell::Cell = + const { std::cell::Cell::new(SoftTexSelectionMemo { + serial: 0, + rho: 0, + selected: SoftTexSelection { first: 0, second: 0, filter: 0, alpha: 0.0, lambda: 0.0 }, + }) }; +} + +impl SoftDraw<'_> { + /// Fog (3.10): the board's fog factor f from the window depth z + /// (GlPlanes::fog_factor()), clamped to [0, 1], blends the fragment's red, + /// green and blue toward the fog colour, f C + (1 - f) Cf. + pub fn fog(&self, frag: &mut SoftFrag, z: f64) { + let st: &GlDrawState = self.st; + let f = self.px.fog_factor(z as f32) as f64; + let f = if f >= 1.0 { 1.0 } else if f > 0.0 { f } else { 0.0 }; + + for k in 0..3usize { + frag.rgba[k] = f * frag.rgba[k] + (1.0 - f) * st.fog_color[k] as f64; + } + } +} + +impl GlDrawState { + /// Whether a point whose client clip plane values are d lies outside an + /// enabled plane (2.11). + pub fn clipped(&self, d: &[f64]) -> bool { + for (i, &di) in d.iter().enumerate().take(GL_CLIP_PLANES as usize) { + if (self.clip_planes & (1u32 << i)) != 0 && !(di >= 0.0) { + return true; + } + } + false + } +} + +impl SoftDraw<'_> { + /// A fragment's colour value from an interpolated or flat index. + pub fn owned_at(&self, px: i32, py: i32) -> bool { + let x = self.win.ox.wrapping_add(px); + let y = self.win.oy.wrapping_add(py); + let st: &GlDrawState = self.st; + + (self.win.clip.n <= 1 || self.win.clip.contains(x, y)) && + (!st.scissor_test || + (px >= st.scissor[0] && + (px as i64) < st.scissor[0] as i64 + st.scissor[2] as i64 && + py >= st.scissor[1] && + (py as i64) < st.scissor[1] as i64 + st.scissor[3] as i64)) + } + + /// The colour of a fragment that has passed every test written to the + /// draw's colour targets at plane pixel (x, y): blended with or combined + /// by the logical operation with what is there and masked, from + /// dest in RGBA mode, which the caller has read where the draw reads it. + #[inline(always)] + pub fn fragment_store(&self, x: i32, y: i32, f: &SoftFrag, dest: &[RasterColour; GL_MAX_TARGETS as usize]) { + if !self.t.index { + let mut i = 0; + while self.rgba_mask != 0 && i < self.ntargets as usize { + let out = self.rgba_result::(f, &dest[i]); + + self.px.store_rgba(self.targets[i], x, y, &out); + i += 1; + } + return; + } + // Colour-index mode has no blend, logical operation or partial mask + // (SoftDraw::setup_ops()). + if self.colour_mask == 0 { + return; + } + let colour = self.rules.rgb(&f.rgba); + for &target in &self.targets[..self.ntargets as usize] { + self.px.store_colour(target, x, y, colour); + } + } + + pub fn fragment_at(&self, px: i32, py: i32, z: f64, frag: &SoftFrag, coverage: f64, antialias: bool) { + self.fragment_at_offset(px, py, z, 0, frag, coverage, antialias); + } + + /// [`fragment_at`](Self::fragment_at) with `steps` of the board's polygon + /// offset added to the depth code (RasterRules::split_offset()). + #[allow(clippy::too_many_arguments, reason = "the fragment path takes a fragment as its parts")] + pub fn fragment_at_offset(&self, px: i32, py: i32, z: f64, steps: i64, frag: &SoftFrag, coverage: f64, antialias: bool) { + let st: &GlDrawState = self.st; + let mut fogged = *frag; + let x = self.win.ox.wrapping_add(px); + let y = self.win.oy.wrapping_add(py); + let mut dest = [RasterColour::default(); GL_MAX_TARGETS as usize]; + + if !(0.0..=1.0).contains(&z) { + return; + } + if let Some(im) = self.texture { + if frag.fixed { + let format = match self.texture_base { + Some(b) => b.base_format, + None => im.base_format, + }; + self.texture_apply(format, st.texture_env_mode, &mut fogged.texel, &mut fogged.rgba); + } else if !self.texture(&mut fogged) { + return; + } + } + if st.fog { + self.fog(&mut fogged, z); + } + fogged.rgba[3] *= coverage; + if !self.owned_at(px, py) { + return; + } + // Incoming alpha and reference use at least two bits and at least the + // stored alpha width, independently of the colour mask (4.1.4, 2.13.9). + let f = &fogged; + if st.alpha_test && + !self.rules.alpha_test(st.alpha_func, f.rgba[3], st.alpha_ref as f64) + { + return; + } + if !self.t.index && self.rgba_read && + !self.rules.rgba_load(self.px, &self.targets, self.ntargets, x, y, &mut dest, &self.reports.rgba) + { + return; + } + if !self.rules.depth_stencil(st, self.px, x, y, z, steps) { + return; + } + self.fragment_store(x, y, f, &dest); + } +} + +impl GlDrawState { + /// The depth range [n, f] normalized depth is mapped through (2.10.1): the + /// draw state's, or [0, 1] under a window matrix, whose depth is already + /// window depth. + #[inline] + pub fn depth_range(&self) -> (f64, f64) { + if self.window_matrix_given { + (0.0, 1.0) + } else { + (self.depth_range[0] as f64, self.depth_range[1] as f64) + } + } + + /// Normalized depth z, which must be in [0, 1], and an offset, mapped + /// through the depth range and clamped to [0, 1]. + #[inline] + pub fn window_depth(&self, z: f64, offset: f64) -> Option { + if !(0.0..=1.0).contains(&z) { + return None; + } + let (n, f) = self.depth_range(); + let zw = n + (f - n) * z + offset; + Some(if zw >= 1.0 { 1.0 } else if zw > 0.0 { zw } else { 0.0 }) + } +} + +impl SoftDraw<'_> { + /// The window pixels a primitive's fragments may fall on, inclusive: the + /// bounds of those the window owns, within the viewport unless a window + /// matrix replaces it, as (x0, y0, x1, y1). None when there are none. + pub fn bounds(&self) -> Option<(i32, i32, i32, i32)> { + let st: &GlDrawState = self.st; + let (x0, y0, x1, y1) = self.owned_bounds(); + let vx = st.viewport[0]; + let vy = st.viewport[1]; + let vw = st.viewport[2]; + let vh = st.viewport[3]; + + if st.window_matrix_given { + return Some((x0, y0, x1, y1)); + } + if vw <= 0 || vh <= 0 { + return None; + } + Some(( + std::cmp::max(x0, vx), + std::cmp::max(y0, vy), + std::cmp::min(x1, vx.wrapping_add(vw).wrapping_sub(1)), + std::cmp::min(y1, vy.wrapping_add(vh).wrapping_sub(1)), + )) + } + + /// The window's owned rectangle in draw coordinates, before the viewport + /// narrows it, as (x0, y0, x1, y1). + pub fn owned_bounds(&self) -> (i32, i32, i32, i32) { + let win = &self.win; + + ( + win.x0.wrapping_sub(win.ox), + win.y0.wrapping_sub(win.oy), + win.x1.wrapping_sub(win.ox), + win.y1.wrapping_sub(win.oy), + ) + } + + /// Whether a polygon fragment at (px, py) is drawn under the polygon + /// stipple: None when it is not, else whether it takes the target's + /// opaque stipple background (RasterTarget::stipple_opaque). + pub fn stipple_pass(&self, px: i32, py: i32) -> Option { + if self.polygon_stipple.is_none_or(|rows| rows[((py as u32) & 31) as usize] & (1u32 << ((px as u32) & 31)) != 0) { + return Some(false); + } + self.t.stipple_opaque.then_some(true) + } + + pub fn stipple_background(&self, frag: &mut SoftFrag) { + for k in 0..4usize { + frag.rgba[k] = self.t.stipple_background[k] as f64; + } + } + + /// A primitive's fragment at normalized depth z, (zd + 1) / 2 for the + /// normalized device z zd: dropped outside [0, 1], the view volume's near + /// and far planes, which are now the only near and far kill (the clipper + /// keeps them), and otherwise mapped to window depth n + (f - n) z through the depth range + /// [n, f] (OpenGL 1.2.1, 2.10.1). A polygon's offset is then added to the + /// window depth, and the sum clamped to [0, 1] (3.5.5). + #[allow(clippy::too_many_arguments, reason = "the fragment path takes a fragment as its parts")] + pub fn fragment_coverage(&self, px: i32, py: i32, z: f64, offset: f64, depth_w: f64, frag: &SoftFrag, coverage: f64, antialias: bool) { + let mut z = z; + let (mut offset, steps) = self.rules.split_offset(offset); + + if self.rules.depth_row_given { + let slack = self.rules.depth_slack; + + // Depth and its divisor, each over w, interpolate linearly in the + // window; their ratio is the row's depth at the fragment. + z = (z + offset) / depth_w; + offset = 0.0; + if z < 0.0 && z >= -slack { + z = 0.0; + } else if z > 1.0 && z <= 1.0 + slack { + z = 1.0; + } + } + let Some(zw) = self.st.window_depth(z, offset).filter(|_| coverage > 0.0) else { + return; + }; + self.fragment_at_offset(px, py, zw, steps, frag, coverage, antialias); + } + + pub fn fragment(&self, px: i32, py: i32, z: f64, offset: f64, depth_w: f64, frag: &SoftFrag) { + self.fragment_coverage(px, py, z, offset, depth_w, frag, 1.0, false); + } +} + +impl GlDrawState { + /// A triangle's polygon offset (3.5.5), o = m factor + r units, where m is + /// the larger magnitude of its window depth's slopes in x and y, which the + /// specification allows in place of their length, and r the rules' one + /// depth step; EXT_polygon_offset's bias adds in window depth. Boundary modes + /// share this slope calculation and select their own offset enable. + pub fn depth_slope(&self, window: &[[f64; 3]; 3]) -> f64 { + let (n, f) = self.depth_range(); + let x1 = window[1][0] - window[0][0]; + let y1 = window[1][1] - window[0][1]; + let x2 = window[2][0] - window[0][0]; + let y2 = window[2][1] - window[0][1]; + let z1 = window[1][2] - window[0][2]; + let z2 = window[2][2] - window[0][2]; + let det = x1 * y2 - x2 * y1; + // Reviewed model choice for an entirely degenerate boundary: m = 0. + if det == 0.0 { + return 0.0; + } + cmax((z1 * y2 - z2 * y1).abs(), (x1 * z2 - x2 * z1).abs()) / det.abs() * (f - n).abs() + } +} + +impl SoftDraw<'_> { + pub fn window_vertices(&self, v: &[SoftVertex; 3]) -> [[f64; 3]; 3] { + let sub = (1i32 << self.rules.subpixel_bits) as f64; + + v.map(|v| [v.x as f64 / sub, v.y as f64 / sub, v.z]) + } + + pub fn slope(&self, v: &[SoftVertex; 3]) -> f64 { + self.st.depth_slope(&self.window_vertices(v)) + } +} + +impl RasterRules { + pub fn polygon_offset(&self, st: &GlDrawState, window: &[[f64; 3]; 3]) -> f64 { + if st.polygon_offset & GL_OFFSET_FILL == 0 { + return 0.0; + } + self.offset_amount(st, st.depth_slope(window)) + } + + /// The offset of an enabled polygon mode for a primitive of depth slope m: + /// m factor + r units + bias in window depth (OpenGL 1.2.1, 3.5.5). With + /// depth_float the guest library has already folded factor (through + /// tan L) and 2 units into one per-projection step count and sends it as + /// the 0x058 field, which the context carries as the bias; the board has + /// no per-polygon slope term. So only the + /// bias counts there, and the slope and the context's factor and units, + /// which the library never sets, add nothing. + pub fn offset_amount(&self, st: &GlDrawState, slope: f64) -> f64 { + if self.depth_float { + return st.offset_bias as f64; + } + slope * st.offset_factor as f64 + st.offset_units as f64 / self.depth_max() as f64 + st.offset_bias as f64 + } +} + +impl SoftDraw<'_> { + pub fn offset(&self, v: &[SoftVertex; 3]) -> f64 { + self.rules.polygon_offset(self.st, &self.window_vertices(v)) + } +} + +impl SoftDraw<'_> { + /// Map one clipped vertex (2.10): the divide by w and the viewport map, + /// in single precision through the shared mapping + /// (GlDrawState::clip_to_window()), with depth left normalized to + /// [0, 1]; the depth range maps it per fragment. Under a window matrix + /// the position is the matrix's and the depth its window depth. The + /// depth is not clamped: no near or far plane clips geometry (inferred), + /// so a fragment outside [0, 1] is dropped per pixel + /// (GlDrawState::window_depth()). Under the rules' depth row the depth is + /// the row's over w instead. The colour is the front colour; a back-facing + /// triangle takes its back colour later. + /// A vertex with w <= 0 is in the volume only at the clip-space origin, + /// where the divide has no answer, and returns None, as does one mapped + /// beyond the rasterizer's range. + pub fn project(&self, cv: &VolumeVertex) -> Option { + let mut out = SoftVertex::default(); + let st: &GlDrawState = self.st; + let clip = cv.clip.map(f64::from); + let eye = cv.eye.map(f64::from); + if !self.texture_vertex_valid(cv) { + return None; + } + let sub = (1i32 << self.rules.subpixel_bits) as f64; + let Some(win) = st.clip_to_window(&cv.clip, sub as f32).map(|w| w.map(f64::from)) else { + crate::dlog!(LogModule::Odyssey, "sgi-gl: clipped vertex at w {} has no window position, primitive dropped", clip[3]); + return None; + }; + let xw = win[0]; + let yw = win[1]; + if !(xw.abs() < 65536.0 && yw.abs() < 65536.0) { + crate::dlog!(LogModule::Odyssey, "sgi-gl: vertex outside the rasterizer's range, primitive dropped"); + return None; + } + for k in 0..4usize { + out.texcoord[k] = cv.attr[vtx::TEXCOORD + k] as f64; + } + // Already on the grid: the truncation only makes it an integer. + out.x = (xw * sub).floor() as i64; + out.y = (yw * sub).floor() as i64; + if !self.rules.depth_row_given { + out.z = win[2]; + } else { + let d = &self.rules.depth_row; + + // The head's depth over w, which a window line interpolates. + out.z = (d[0] * eye[0] + d[1] * eye[1] + d[2] * eye[2] + d[3] * eye[3]) / clip[3]; + } + out.inv_w = 1.0 / clip[3]; + out.depth_w = out.inv_w; + if self.rules.depth_divisor_given { + let d = &self.rules.depth_divisor; + + out.depth_w = (d[0] * eye[0] + d[1] * eye[1] + d[2] * eye[2] + d[3] * eye[3]) / clip[3]; + } + for i in 0..GL_CLIP_PLANES as usize { + let p = &st.clip_plane[i]; + + out.clip[i] = p[0] as f64 * eye[0] + p[1] as f64 * eye[1] + p[2] as f64 * eye[2] + p[3] as f64 * eye[3]; + } + for k in 0..4usize { + let a = cv.attr[vtx::COLOR + k]; + let m = if a > 0.0 { a } else { 0.0 }; + let n = if m < 1.0 { m } else { 1.0 }; + out.rgba[k] = n as f64; + } + Some(out) + } +} + +impl RasterRules { + /// Draw a draw's primitives into the planes, its texels from texels. + /// A standalone line stipple's phase goes back in d.line_phase. + pub fn draw(&self, t: &RasterTarget, pixels: &mut GlPixels, texels: &dyn Texels, d: &mut GlDraw) -> GlDrawResult { + let planes = pixels.planes(); + with_texel_cache(|cache| self.draw_cached(t, &planes, texels, d, cache)) + } + + fn draw_cached( + &self, + t: &RasterTarget, + planes: &GlPlanes, + texels: &dyn Texels, + d: &mut GlDraw, + cache: &std::cell::RefCell, + ) -> GlDrawResult { + let reports = SoftReports::default(); + let mut r = SoftDraw::new(self, t, d.state, &reports, planes); + let mut line = SoftLineStipple { + counter: match d.line_phase { + Some(phase) if !d.line_phase_reset => phase, + _ => 0, + }, + enabled: d.line_stipple_enabled, + repeat: cmax(1, cmin(d.line_stipple_repeat, 256)), + pattern: d.line_stipple_pattern, + consumed: false, + }; + let use_stipple = d.line_phase.is_some() || d.line_stipple_enabled; + let mut fixed = SoftFixedTex::default(); + + r.texels = texels; + r.back_colors = d.back_colors; + r.point_smooth = self.smooth_primitives && d.point_smooth; + r.line_smooth = self.smooth_primitives && d.line_smooth; + r.smooth_point_size = d.point_size as f64; + r.smooth_line_width = d.line_width as f64; + r.polygon_smooth = self.smooth_primitives && d.polygon_smooth; + r.polygon_stipple = if d.polygon_stipple_enabled { Some(d.polygon_stipple) } else { None }; + line.counter %= 16u32.wrapping_mul(line.repeat); + r.ntargets = d.state.draw_targets(&mut r.targets); + if !self.check_rules() { + return GL_DRAW_OK; + } + if let Some(win) = d.state.window() { + r.win = win; + } else { + if !use_stipple + || d.mode < GL_LINES + || d.mode > GL_LINE_STRIP + || !d.state.drawable.bound + || d.state.drawable.clip.n != 0 + { + return GL_DRAW_OK; + } + // Empty ownership still counts standalone geometric coverage. + r.win.x1 = -1; + r.win.y1 = -1; + } + if !r.texture_setup(true) { + return GL_DRAW_OK; + } + if r.texture.is_some() { + r.texels_cache = Some(cache); + } + r.setup_ops(); + if d.mode >= GL_TRIANGLES && d.mode <= GL_POLYGON { + r.fill = r.fill_select(); + } + let mut fixed_on = false; + if d.mode >= GL_TRIANGLES && d.mode <= GL_POLYGON && r.fixed_setup(&mut fixed) { + let mut texels = 0u64; + fixed.span = r.fixed_span_select(&fixed); + let (first, last) = fixed.fixed_reach(); + for i in first..=last { + texels += fixed.level[i as usize].width as u64 * fixed.level[i as usize].height as u64; + } + texels = cmax(texels, FIXED_PREFILL_MIN_PIXELS); + fixed_on = (r.draw_pixels(d, texels) >= texels + && r.fixed_coordinates_valid(d) + && r.fixed_prefill(&mut fixed)) + || r.fixed_begin(&fixed); + } + if fixed_on { + r.fixed = Some(&fixed); + } + let result = r.draw_primitives(d, &mut line, use_stipple); + if d.line_phase.is_some() && line.consumed { + d.line_phase = Some(line.counter); + } + if fixed.prefilled { + // The prefilled arrays go back to this thread's store for the next draw. + fixed_return(cache, &mut fixed); + } + result + } +} + +impl<'a> SoftDraw<'a> { + /// The primitives of a draw that has passed its setup. + fn draw_primitives(&self, d: &GlDraw<'a>, line: &mut SoftLineStipple, use_stipple: bool) -> GlDrawResult { + let n = d.count as usize; + let v = |i: usize| -> &[f32] { d.soft_polygon_vertex(None, i) }; + + if !self.rules.smooth_primitives { + let mut smooth: Option<&str> = None; + + if d.mode == GL_POINTS && d.point_smooth { + smooth = Some("point"); + } else if d.mode >= GL_LINES && d.mode <= GL_LINE_STRIP && d.line_smooth { + smooth = Some("line"); + } else if d.mode >= GL_TRIANGLES && d.mode <= GL_POLYGON && d.polygon_smooth { + smooth = Some("polygon"); + } + if let Some(s) = smooth { + crate::dlog!(LogModule::Odyssey, "sgi-gl: {} smoothing requested but unsupported by raster rules; drawn aliased", s); + } + } + let filled = d.mode >= GL_TRIANGLES && d.mode <= GL_POLYGON; + if filled + && (self.polygon_smooth || d.polygon_modes.is_some_and(|m| m != [GL_FILL, GL_FILL])) + { + return self.polygon_modes(d); + } + let mut r = *self; + match d.mode { + GL_POINTS => { + let Some(size) = soft_dimension(d.point_size) else { + return GL_DRAW_OK; + }; + r.point_size = size; + for i in 0..n { + r.point(v(i)); + } + GL_DRAW_OK + } + GL_LINES => { + let Some(width) = soft_dimension(d.line_width) else { + return GL_DRAW_OK; + }; + r.line_width = width; + let mut i = 0usize; + while i + 1 < n { + if d.line_phase.is_none() { + line.counter = 0; + } + r.line(v(i), v(i + 1), v(i + 1), if use_stipple { Some(&mut *line) } else { None }); + i += 2; + } + GL_DRAW_OK + } + GL_LINE_STRIP | GL_LINE_LOOP => { + let Some(width) = soft_dimension(d.line_width) else { + return GL_DRAW_OK; + }; + r.line_width = width; + let mut i = 0usize; + while i + 1 < n { + r.line(v(i), v(i + 1), v(i + 1), if use_stipple { Some(&mut *line) } else { None }); + i += 1; + } + if d.mode == GL_LINE_LOOP && n >= 2 { + r.line(v(n - 1), v(0), v(0), if use_stipple { Some(&mut *line) } else { None }); + } + GL_DRAW_OK + } + GL_TRIANGLES | GL_TRIANGLE_STRIP | GL_TRIANGLE_FAN | GL_POLYGON | GL_QUADS + | GL_QUAD_STRIP => { + r.draw_triangles(d); + GL_DRAW_OK + } + _ => { + crate::dlog!(LogModule::Odyssey, "sgi-gl: draw of mode {} ignored", d.mode); + GL_DRAW_OK + } + } + } +} + +#[cfg(test)] +mod colour_tests { + use super::*; + + /// The rasteriser's min and max keep C's operand order: a tie or a NaN + /// on either side gives the second operand, where f64::max and f64::min + /// would give the number. The fill arithmetic depends on it. + #[test] + fn min_and_max_give_ties_and_nans_to_the_second_operand() { + let nan = f64::NAN; + assert_eq!(cmax(nan, 1.0), 1.0); + assert!(cmax(1.0, nan).is_nan()); + assert_eq!(cmin(nan, 1.0), 1.0); + assert!(cmin(1.0, nan).is_nan()); + assert!(cmax(0.0f64, -0.0).is_sign_negative()); + assert!(cmin(-0.0f64, 0.0).is_sign_positive()); + assert_eq!((cmax(2.0, 1.0), cmin(2.0, 1.0)), (2.0, 1.0)); + assert_eq!((cmax(3i32, 7), cmin(3i32, 7)), (7, 3)); + assert_eq!((cmax(-1i64, -5), cmin(-1i64, -5)), (-1, -5)); + } + + fn rules(color_bits: u8, alpha_bits: u8) -> RasterRules { + RasterRules { color_bits, alpha_bits, ..Default::default() } + } + + /// Components on and around every step of an n-bit channel, and the + /// values that clamp. + fn components(bits: u32) -> Vec { + let max = ((1u32 << bits) - 1) as f64; + let mut v = vec![-1.0, -0.0, 0.0, f64::NAN, 1.0, 1.5, f64::INFINITY, f64::MIN_POSITIVE]; + for i in 0..=(1u32 << bits) * 4 { + let c = (i as f64 / 4.0) / max; + + v.push(c); + v.push(f64::from_bits(c.to_bits().wrapping_add(1))); + v.push(f64::from_bits(c.to_bits().wrapping_sub(1))); + } + v + } + + // Colour storage is floor(256c) limited to 255: the vendor-frame points. + // 0.1 also rules out round(255c), which gives 0x1a. + #[test] + fn colour_storage_is_floor_256c() { + let r = rules(8, 8); + + assert_eq!(r.channel(0.1), 0x19); + assert_eq!(r.channel(0.5), 0x80); + assert_eq!(r.channel(1.0), 0xff); + } + + // rgb() takes each channel as channel() does. + #[test] + fn rgb_is_its_channels() { + for bits in [1u8, 5, 8, 10, 12] { + let r = rules(bits, 8); + let cs = components(bits as u32); + + for (i, &c) in cs.iter().enumerate() { + let (d, e) = (cs[(i * 7 + 3) % cs.len()], cs[(i * 13 + 5) % cs.len()]); + let want = (r.channel(c).wrapping_shl(2 * bits as u32)) | (r.channel(d).wrapping_shl(bits as u32)) | r.channel(e); + + assert_eq!(r.rgb(&[c, d, e]), want, "{bits} bits, {c} {d} {e}"); + } + } + } + + // c / max, from the table at eight bits. + #[test] + fn unit_components_are_the_division() { + for max in [1u32, 3, 15, 31, 255, 1023, 4095] { + for c in 0..=max.min(1023) + 3 { + assert_eq!(soft_unit(c, max).to_bits(), (c as f64 / max as f64).to_bits(), "{c} / {max}"); + } + } + // An eight bit divisor with a component beyond the table. + assert_eq!(soft_unit(300, 255).to_bits(), (300.0f64 / 255.0).to_bits()); + } + + // A stored alpha scales the incoming alpha's step to the stored bits. + #[test] + fn stored_alpha_scales_by_the_bits() { + for (cb, ab) in [(8u8, 8u8), (8, 4), (8, 0), (4, 8), (10, 10), (12, 12), (8, 1), (1, 1), (12, 16)] { + let r = rules(cb, ab); + let m = r.alpha_max(); + + for &v in &components(8) { + let want = if m == 0 { + 0 + } else { + r.alpha(v).wrapping_mul((1u32.wrapping_shl(ab as u32)).wrapping_sub(1)) / m + }; + + assert_eq!(r.stored_alpha(v), want, "{cb}/{ab} bits, {v}"); + } + } + } +} diff --git a/src/dev/odyssey/raster_bench.rs b/src/dev/odyssey/raster_bench.rs new file mode 100644 index 000000000..425980e50 --- /dev/null +++ b/src/dev/odyssey/raster_bench.rs @@ -0,0 +1,659 @@ +//! A fill-rate benchmark for the rasteriser, and the scenes it draws. +//! +//! Each scene drives RasterRules::draw() and RasterRules::raster() the way +//! Board::gl_draw() does: one primitive per draw, into a v12 board's own +//! planes (Board::gl_parts()), with the board's raster rules (depth row, +//! float depth, RGBA stores). No guest is involved, so the numbers are the +//! rasteriser's and the planes', not the command decoder's or the geometry +//! program's. +//! +//! Run it from a release build, one test thread: +//! +//! cargo test --release --lib odyssey::raster_bench -- --ignored --nocapture --test-threads=1 +//! +//! BENCH_SCENE picks one scene, BENCH_SECS the time each takes (default +//! one second), BENCH_FILL the row loops (table, dynamic or off; see +//! raster_set_fill_path()). The tests in raster_fill.rs draw with the same +//! board and state. + + +use super::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use super::consts::gl_cmd; +use super::draw::gl_rules; +use super::gl_context::{ + vtx, GL_ALWAYS, GL_CLAMP_TO_EDGE, GL_COLOR_BUFFER_BIT, GL_COLOUR_MASK_ALL, GL_DEPTH_BUFFER_BIT, + GL_EQUAL, GL_FLAT, GL_INCR, GL_KEEP, GL_LEQUAL, GL_LESS, GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, + GL_LINES, GL_MODULATE, GL_ONE_MINUS_SRC_ALPHA, GL_QUADS, GL_REPEAT, GL_RGB, GL_RGBA, GL_SMOOTH, + GL_SRC_ALPHA, GL_TRIANGLES, GlDraw, GlDrawResult, GlDrawState, RASTER_CLEAR, RasterOp, +}; +use super::memory::ClipRect; +use super::raster::{GlTexImage, GlTexLevel, GlTexPyramid, RasterRules, RasterTarget, Texels}; +use super::raster_fill::{raster_set_fill_path, FillPath}; +pub(super) const BENCH_W: i32 = ODSY_WIDTH as i32; +pub(super) const BENCH_H: i32 = ODSY_HEIGHT as i32; + +/// A deterministic generator, so every run and every path draws the same. +pub(super) struct Lcg(pub u64); + +impl Lcg { + pub fn next_u32(&mut self) -> u32 { + self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + (self.0 >> 33) as u32 + } + /// In [lo, hi). + pub fn range(&mut self, lo: f32, hi: f32) -> f32 { + lo + (hi - lo) * (self.next_u32() as f32 / (1u64 << 31) as f32) + } +} + +/// Texels that are a hash of their level and position, so a wrong filter or +/// level shows. +pub(super) struct HashTexels; + +impl Texels for HashTexels { + fn texel(&self, level: Option<&GlTexLevel>, x: u32, y: u32) -> Option { + let l = level.map_or(0, |l| l.level); + let h = x.wrapping_mul(0x9e3779b1) ^ y.wrapping_mul(0x85ebca77) ^ l.wrapping_mul(0xc2b2ae35) ^ 0x5bd1e995; + Some(h ^ (h >> 13)) + } + fn border_texel(&self, _: Option<&GlTexLevel>, _: i32, _: i32) -> Option { + None + } + fn texel_3d(&self, _: Option<&GlTexLevel>, _: u32, _: u32, _: u32) -> Option { + None + } + fn border_texel_3d(&self, _: Option<&GlTexLevel>, _: i32, _: i32, _: i32) -> Option { + None + } +} + +/// A square mipmapped texture of side size: its levels and sampler. +pub(super) struct BenchTexture { + pub levels: Vec, + pub min: u32, + pub mag: u32, + pub format: u32, + pub wrap_s: u32, + pub wrap_t: u32, +} + +impl BenchTexture { + pub fn new(size: u32, min: u32, mag: u32, format: u32) -> BenchTexture { + let mut levels = Vec::new(); + let mut s = size; + let mut level = 0; + loop { + levels.push(GlTexLevel { + level, + width: s, + height: s, + depth: 1, + base_format: format, + internal_format: format, + read_texel: true, + ..Default::default() + }); + if s == 1 { + break; + } + s /= 2; + level += 1; + } + BenchTexture { levels, min, mag, format, wrap_s: GL_REPEAT, wrap_t: GL_REPEAT } + } + + pub fn image(&self) -> GlTexImage<'_> { + GlTexImage { + width: self.levels[0].width, + height: self.levels[0].height, + base_format: self.format, + min_filter: self.min, + mag_filter: self.mag, + wrap_s: self.wrap_s, + wrap_t: self.wrap_t, + read_texel: true, + pyramid: Some(GlTexPyramid { + levels: &self.levels, + base_level: 0, + max_level: 1000, + min_lod: -1000.0, + max_lod: 1000.0, + }), + ..Default::default() + } + } +} + +/// What a scene's primitives are and how they are drawn. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(super) enum Scene { + /// Flat-shaded triangles, no depth test: 2D-style GL fills. + FlatFill, + /// Smooth-shaded, depth-tested triangles. + SmoothDepth, + /// Smooth, depth-tested, alpha-blended triangles. + SmoothBlend, + /// Textured, depth-tested, modulated, bilinear. + TexBilinearDepth, + /// Textured, depth-tested, modulated, trilinear. + TexTrilinearDepth, + /// Textured with fog and blending: the textured fallback loop. + TexFogBlend, + /// Textured, depth-tested, trilinear, in perspective: each quad takes + /// its own level of detail. + TexPerspective, + /// Smooth, depth-tested triangles of some thousands of pixels. + MidSmooth, + /// Screen-filling quads, smooth and depth tested. + BigQuads, + /// Screen-filling trilinear, depth-tested quads: GLperf's fill test. + BigTexTrilinear, + /// One-pixel lines. + Lines, + /// Colour and depth clears of the whole screen. + Clear, + /// Textured, bilinear RGB, depth tested, inside a stencil mask: the + /// stencil EQUAL test, every operation KEEP (Smoke & Mirrors' scene). + TexStencil, + /// Untextured, writing only the stencil: ALWAYS, incremented where the + /// depth test passes, colour and depth masked (a mirror's mask). + StencilMark, + /// Trilinear RGB, flat, depth LEQUAL, s clamped to the edge, in + /// perspective (perfly's ground and buildings). + TexClampTrilinear, + /// Trilinear RGBA blended over what is there, depth ALWAYS and not + /// written (perfly's sky and overlays). + TexBlendOver, + /// TexStencil's state under a client clip plane that cuts the screen + /// (Smoke & Mirrors' reflected scene). + TexStencilClip, + /// Small bilinear RGB quads, depth tested: each quad's two triangles + /// validated as a pair before either draws. + TexQuads, + /// Bilinear RGB blended over what is there, depth tested LESS and + /// written (Smoke & Mirrors' glass). + TexGlass, + /// Antialiased triangles, smooth shaded, blended and depth tested. + PolySmooth, +} + +pub(super) const ALL_SCENES: [Scene; 20] = [ + Scene::FlatFill, + Scene::SmoothDepth, + Scene::SmoothBlend, + Scene::TexBilinearDepth, + Scene::TexTrilinearDepth, + Scene::TexFogBlend, + Scene::TexPerspective, + Scene::MidSmooth, + Scene::BigQuads, + Scene::BigTexTrilinear, + Scene::Lines, + Scene::Clear, + Scene::TexStencil, + Scene::StencilMark, + Scene::TexClampTrilinear, + Scene::TexBlendOver, + Scene::TexStencilClip, + Scene::TexQuads, + Scene::TexGlass, + Scene::PolySmooth, +]; + +/// One scene, built: the board, the draw state, the vertex lists of its +/// primitives and the pixels they stand for. +pub(super) struct Bench { + pub board: Box, + pub st: GlDrawState, + pub rules: RasterRules, + pub tex: Option, + /// Each primitive's vertices, vtx::FLOATS apiece. + pub prims: Vec>, + pub mode: u32, + /// The pixels the scene covers, by area, for the rate. + pub pixels: f64, + pub clears: usize, + /// Polygons antialiased (the draw's own flag, not the state's). + pub polygon_smooth: bool, +} + +fn identity() -> [f32; 16] { + let mut m = [0.0f32; 16]; + for k in 0..4 { + m[k * 5] = 1.0; + } + m +} + +fn ndc(x: f32, size: i32) -> f32 { + x / size as f32 * 2.0 - 1.0 +} + +/// A triangle of about side 2 * size pixels around a random centre, with +/// random colours and depths, its texture coordinates texscale a pixel; and +/// its area in pixels. +fn triangle(rng: &mut Lcg, size: f32, texscale: f32) -> (Vec, f64) { + let cx = rng.range(0.0, BENCH_W as f32); + let cy = rng.range(0.0, BENCH_H as f32); + let mut v = vec![0.0f32; 3 * vtx::FLOATS]; + let mut xy = [(0.0f32, 0.0f32); 3]; + let s0 = rng.range(0.0, 4.0); + let t0 = rng.range(0.0, 4.0); + for i in 0..3 { + let x = cx + rng.range(-size, size); + let y = cy + rng.range(-size, size); + let z = rng.range(-0.9, 0.9); + let o = &mut v[i * vtx::FLOATS..(i + 1) * vtx::FLOATS]; + xy[i] = (x.clamp(0.0, BENCH_W as f32), y.clamp(0.0, BENCH_H as f32)); + o[vtx::POSITION..vtx::POSITION + 4].copy_from_slice(&[ndc(x, BENCH_W), ndc(y, BENCH_H), z, 1.0]); + o[vtx::COLOR..vtx::COLOR + 4] + .copy_from_slice(&[rng.range(0.0, 1.0), rng.range(0.0, 1.0), rng.range(0.0, 1.0), rng.range(0.2, 1.0)]); + o[vtx::TEXCOORD..vtx::TEXCOORD + 4] + .copy_from_slice(&[s0 + (x - cx) * texscale, t0 + (y - cy) * texscale, 0.0, 1.0]); + } + let area = ((xy[1].0 - xy[0].0) * (xy[2].1 - xy[0].1) - (xy[2].0 - xy[0].0) * (xy[1].1 - xy[0].1)).abs() / 2.0; + (v, area as f64) +} + +impl Bench { + /// The board and draw state every scene starts from, with no primitives. + pub fn empty() -> Bench { + let mut board = Board::new("v12", 11).expect("board"); + + // Every pixel of the screen is the window's. + board.win.clip_kept = 1; + board.win.clip_rect[0] = ClipRect { x0: 0, y0: 0, x1: BENCH_W - 1, y1: BENCH_H - 1 }; + let mut rules = gl_rules(); + rules.alpha_bits = 8; + rules.depth_row_given = true; + // Depth (z + w) / 2 over w: the identity projection's [-1, 1] mapped + // to [0, 1], as Board::gl_operation_rules() derives it from a + // combined matrix. + rules.depth_row = [0.0, 0.0, 0.5, 0.5]; + rules.depth_slack = gl_cmd::DEPTH_SLACK; + + let mut st = GlDrawState { + modelview: identity(), + projection: identity(), + viewport: [0, 0, BENCH_W, BENCH_H], + shade_model: GL_SMOOTH, + depth_func: GL_LESS, + depth_mask: true, + depth_range: [0.0, 1.0], + draw_buffers: 1, + colour_mask: GL_COLOUR_MASK_ALL, + texture_env_mode: GL_MODULATE, + ..Default::default() + }; + st.drawable.bound = true; + st.drawable.front = 0; + st.drawable.back = 0; + st.drawable.clip.n = 1; + st.drawable.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: BENCH_W - 1, y1: BENCH_H - 1 }; + Bench { + board, + st, + rules, + tex: None, + prims: Vec::new(), + mode: GL_TRIANGLES, + pixels: 0.0, + clears: 0, + polygon_smooth: false, + } + } + + pub fn new(scene: Scene, seed: u64, count: usize) -> Bench { + let mut b = Bench::empty(); + let mut rng = Lcg(seed); + let st = &mut b.st; + let mut tris = |b: &mut Vec>, pixels: &mut f64, size: f32, texscale: f32| { + for _ in 0..count { + let (v, a) = triangle(&mut rng, size, texscale); + b.push(v); + *pixels += a; + } + }; + + match scene { + Scene::FlatFill => { + st.shade_model = GL_FLAT; + tris(&mut b.prims, &mut b.pixels, 40.0, 0.0); + } + Scene::SmoothDepth | Scene::SmoothBlend | Scene::MidSmooth => { + st.depth_test = true; + if scene == Scene::SmoothBlend { + st.blend = true; + st.blend_src = GL_SRC_ALPHA; + st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + } + tris(&mut b.prims, &mut b.pixels, if scene == Scene::MidSmooth { 110.0 } else { 40.0 }, 0.0); + } + Scene::TexBilinearDepth | Scene::TexTrilinearDepth | Scene::TexFogBlend => { + let (min, mag) = if scene == Scene::TexBilinearDepth { + (GL_LINEAR, GL_LINEAR) + } else { + (GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) + }; + st.depth_test = true; + st.texture_2d = true; + if scene == Scene::TexFogBlend { + st.fog = true; + st.fog_mode = GL_LINEAR; + st.fog_start = 0.0; + st.fog_end = 2.0; + st.fog_color = [0.5, 0.5, 0.6, 1.0]; + st.blend = true; + st.blend_src = GL_SRC_ALPHA; + st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + } + b.tex = Some(BenchTexture::new(256, min, mag, GL_RGBA)); + // Minified by about 1.5: a trilinear draw mixes two levels. + tris(&mut b.prims, &mut b.pixels, 40.0, 1.5 / 256.0); + } + Scene::TexPerspective | Scene::TexClampTrilinear => { + st.depth_test = true; + st.texture_2d = true; + if scene == Scene::TexPerspective { + b.tex = Some(BenchTexture::new(256, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_RGBA)); + } else { + let mut tex = BenchTexture::new(256, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_RGB); + + tex.wrap_s = GL_CLAMP_TO_EDGE; + b.tex = Some(tex); + st.depth_func = GL_LEQUAL; + st.shade_model = GL_FLAT; + } + for _ in 0..count { + let (mut v, a) = triangle(&mut rng, 40.0, 1.5 / 256.0); + + // Each vertex its own w, the same window position, and + // depth (z + w) / 2 kept within [0, 1]. + for i in 0..3 { + let w = rng.range(1.0, 4.0); + let d = rng.range(0.05, 0.95); + let o = &mut v[i * vtx::FLOATS + vtx::POSITION..]; + + o[0] *= w; + o[1] *= w; + o[2] = 2.0 * d - w; + o[3] = w; + } + b.prims.push(v); + b.pixels += a; + } + } + Scene::TexStencil | Scene::TexStencilClip => { + if scene == Scene::TexStencilClip { + // x + y / 4 >= -0.25: about three fifths of the screen. + st.clip_planes = 1; + st.clip_plane[0] = [1.0, 0.25, 0.0, 0.25]; + } + st.depth_test = true; + st.texture_2d = true; + st.stencil_test = true; + st.stencil_func = GL_EQUAL; + st.stencil_value_mask = u32::MAX; + st.stencil_write_mask = u32::MAX; + st.stencil_op = [GL_KEEP; 3]; + b.tex = Some(BenchTexture::new(256, GL_LINEAR, GL_LINEAR, GL_RGB)); + tris(&mut b.prims, &mut b.pixels, 40.0, 1.5 / 256.0); + } + Scene::StencilMark => { + st.depth_test = true; + st.depth_mask = false; + st.colour_mask = 0; + st.stencil_test = true; + st.stencil_func = GL_ALWAYS; + st.stencil_value_mask = u32::MAX; + st.stencil_write_mask = u32::MAX; + st.stencil_op = [GL_KEEP, GL_KEEP, GL_INCR]; + tris(&mut b.prims, &mut b.pixels, 40.0, 0.0); + } + Scene::TexBlendOver => { + st.depth_test = true; + st.depth_func = GL_ALWAYS; + st.depth_mask = false; + st.texture_2d = true; + st.blend = true; + st.blend_src = GL_SRC_ALPHA; + st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + b.tex = Some(BenchTexture::new(256, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_RGBA)); + tris(&mut b.prims, &mut b.pixels, 40.0, 1.5 / 256.0); + } + Scene::PolySmooth => { + st.depth_test = true; + b.polygon_smooth = true; + st.blend = true; + st.blend_src = GL_SRC_ALPHA; + st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + tris(&mut b.prims, &mut b.pixels, 40.0, 0.0); + } + Scene::TexGlass => { + st.depth_test = true; + st.texture_2d = true; + st.blend = true; + st.blend_src = GL_SRC_ALPHA; + st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + // The bilinear texture has no alpha, so the blend weighs the + // vertices' own. + b.tex = Some(BenchTexture::new(256, GL_LINEAR, GL_LINEAR, GL_RGB)); + tris(&mut b.prims, &mut b.pixels, 40.0, 1.5 / 256.0); + } + Scene::TexQuads => { + st.depth_test = true; + st.texture_2d = true; + b.mode = GL_QUADS; + b.tex = Some(BenchTexture::new(256, GL_LINEAR, GL_LINEAR, GL_RGB)); + for _ in 0..count { + // A triangle's three corners and the fourth that makes + // a parallelogram of them. + let (mut v, a) = triangle(&mut rng, 20.0, 1.5 / 256.0); + let f = vtx::FLOATS; + let d: Vec = (0..f).map(|k| v[k] + v[2 * f + k] - v[f + k]).collect(); + v.extend_from_slice(&d); + b.prims.push(v); + b.pixels += 2.0 * a; + } + } + Scene::BigQuads | Scene::BigTexTrilinear => { + st.depth_test = true; + b.mode = GL_QUADS; + if scene == Scene::BigTexTrilinear { + st.texture_2d = true; + b.tex = Some(BenchTexture::new(256, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_RGBA)); + } + for i in 0..count { + let mut v = vec![0.0f32; 4 * vtx::FLOATS]; + // Each nearer than the last, so every pixel passes. + let z = 0.8 - 1.6 * (i as f32 / count as f32); + let corners = [(-1.0f32, -1.0f32), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)]; + for (k, (x, y)) in corners.iter().enumerate() { + let o = &mut v[k * vtx::FLOATS..(k + 1) * vtx::FLOATS]; + o[vtx::POSITION..vtx::POSITION + 4].copy_from_slice(&[*x, *y, z, 1.0]); + o[vtx::COLOR..vtx::COLOR + 4] + .copy_from_slice(&[rng.range(0.0, 1.0), rng.range(0.0, 1.0), rng.range(0.0, 1.0), 1.0]); + // The 256-texel image some 2.5 times across: minified + // by about 2, two levels mixed. + o[vtx::TEXCOORD..vtx::TEXCOORD + 4].copy_from_slice(&[(x + 1.0) * 2.5, (y + 1.0) * 2.0, 0.0, 1.0]); + } + b.prims.push(v); + b.pixels += (BENCH_W * BENCH_H) as f64; + } + } + Scene::Lines => { + b.mode = GL_LINES; + st.shade_model = GL_FLAT; + for _ in 0..count { + let mut v = vec![0.0f32; 2 * vtx::FLOATS]; + let x0 = rng.range(0.0, BENCH_W as f32); + let y0 = rng.range(0.0, BENCH_H as f32); + let x1 = (x0 + rng.range(-100.0, 100.0)).clamp(0.0, BENCH_W as f32 - 1.0); + let y1 = (y0 + rng.range(-100.0, 100.0)).clamp(0.0, BENCH_H as f32 - 1.0); + let c = [rng.range(0.0, 1.0), rng.range(0.0, 1.0), rng.range(0.0, 1.0), 1.0]; + for (k, (x, y)) in [(x0, y0), (x1, y1)].iter().enumerate() { + let o = &mut v[k * vtx::FLOATS..(k + 1) * vtx::FLOATS]; + o[vtx::POSITION..vtx::POSITION + 4].copy_from_slice(&[ndc(*x, BENCH_W), ndc(*y, BENCH_H), 0.0, 1.0]); + o[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&c); + } + b.prims.push(v); + b.pixels += ((x1 - x0).abs().max((y1 - y0).abs()) + 1.0) as f64; + } + } + Scene::Clear => { + b.clears = count; + b.pixels = (BENCH_W * BENCH_H) as f64 * count as f64; + } + } + b + } + + fn clear_op(buffers: u32) -> RasterOp { + let mut op = RasterOp { + kind: RASTER_CLEAR, + ntargets: 1, + x0: 0, + y0: 0, + x1: BENCH_W - 1, + y1: BENCH_H - 1, + clear_buffers: buffers, + clear_rgba: [0.2, 0.3, 0.4, 1.0], + clear_colour_mask: GL_COLOUR_MASK_ALL, + clear_depth24: 0xffffff, + ..Default::default() + }; + op.clip.n = 1; + op.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: BENCH_W - 1, y1: BENCH_H - 1 }; + op + } + + /// Reset the depth plane to its farthest, so that each pass of a + /// depth-tested scene draws the same fragments; outside the timing. + pub fn prepare(&mut self) { + let t = self.board.gl_target(); + + self.rules.raster(&t, &mut self.board.gl_parts(None).0, &Bench::clear_op(GL_DEPTH_BUFFER_BIT)); + } + + /// Draw the scene once. + pub fn run(&mut self) { + let mut t = self.board.gl_target(); + let stipple = [0u32; 32]; + + t.texture = self.tex.as_ref().map(|tex| tex.image()); + for _ in 0..self.clears { + let op = Bench::clear_op(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + self.rules.raster(&t, &mut self.board.gl_parts(None).0, &op); + } + let floats = vtx::FLOATS as u32; + for v in self.prims.iter() { + let mut d = GlDraw::new(self.mode, v.len() as u32 / floats, v, &self.st, &stipple); + + d.polygon_smooth = self.polygon_smooth; + self.rules.draw(&t, &mut self.board.gl_parts(None).0, &HashTexels, &mut d); + } + } + + /// Draw d on the board under the bench's rules, as Board::gl_draw() + /// would, reading texels from texels. + pub fn draw(&mut self, t: &RasterTarget, texels: &dyn Texels, d: &mut GlDraw) -> GlDrawResult { + self.rules.draw(t, &mut self.board.gl_parts(None).0, texels, d) + } + + /// The depth plane's words in the square from (0, 0) to side - 1, row + /// by row. + pub fn depth(&self, side: i32) -> Vec { + self.snapshot(side).1 + } + + /// Set the depth plane's words in the square from (0, 0) to side - 1 + /// from words, row by row. + pub fn set_depth(&mut self, side: i32, words: &[u32]) { + let b = &mut self.board; + let depth = b.planes.depth.plane(b.planes.depth_plane).expect("the depth plane"); + + for y in 0..side { + for x in 0..side { + b.planes.depth.plane_store(&depth, x, y, words[(y * side + x) as usize]); + } + } + } + + /// The colour, depth and stencil planes in the square from (0, 0) to + /// side - 1. + pub fn snapshot(&self, side: i32) -> (Vec, Vec, Vec) { + let b = &self.board; + let depth = b.planes.depth.plane(b.planes.depth_plane).expect("the depth plane"); + let stencil = b.planes.stencil.plane(b.planes.stencil_plane).expect("the stencil plane"); + let mut c = Vec::new(); + let mut z = Vec::new(); + let mut s = Vec::new(); + + for y in 0..side { + for x in 0..side { + c.push(b.buffer_load(0, x as i64, y as i64)); + z.push(b.planes.depth.plane_load(&depth, x, y)); + s.push(b.planes.stencil.plane_load(&stencil, x, y)); + } + } + (c, z, s) + } +} + +/// Primitives per scene: enough that a pass takes some milliseconds. +pub(super) fn bench_count(scene: Scene) -> usize { + match scene { + Scene::BigQuads | Scene::BigTexTrilinear | Scene::Clear => 8, + Scene::MidSmooth => 1000, + Scene::PolySmooth => 1000, + Scene::Lines => 20000, + _ => 4000, + } +} + +#[test] +#[ignore] +fn raster_bench() { + let only = std::env::var("BENCH_SCENE").ok(); + let secs: f64 = std::env::var("BENCH_SECS").ok().and_then(|s| s.parse().ok()).unwrap_or(1.0); + let fill = match std::env::var("BENCH_FILL").as_deref() { + Ok("dynamic") => FillPath::Dynamic, + Ok("off") => FillPath::Off, + _ => FillPath::Table, + }; + + raster_set_fill_path(fill); + for scene in ALL_SCENES { + if let Some(o) = &only { + if !format!("{scene:?}").eq_ignore_ascii_case(o) { + continue; + } + } + let mut bench = Bench::new(scene, 1, bench_count(scene)); + let mut best = f64::MAX; + let mut total = 0.0; + let mut runs = 0; + + bench.prepare(); + bench.run(); + while total < secs || runs < 3 { + bench.prepare(); + let t0 = std::time::Instant::now(); + bench.run(); + let dt = t0.elapsed().as_secs_f64(); + best = best.min(dt); + total += dt; + runs += 1; + } + println!( + "bench {:<18} {:>8.2} Mpix/s best, {:>8.2} mean, {:>8.2} ms/pass, {} passes", + format!("{scene:?}"), + bench.pixels / best / 1e6, + bench.pixels * runs as f64 / total / 1e6, + best * 1e3, + runs + ); + } + raster_set_fill_path(FillPath::Table); +} diff --git a/src/dev/odyssey/raster_fill.rs b/src/dev/odyssey/raster_fill.rs new file mode 100644 index 000000000..723bca3b2 --- /dev/null +++ b/src/dev/odyssey/raster_fill.rs @@ -0,0 +1,1677 @@ +//! Quad loops for filled triangles, specialised by draw state and by the +//! planes they reach. +//! +//! A filled triangle is drawn two rows at a time, in 2x2 pixel quads: the +//! texture unit takes one level of detail per quad (soft_quad_rho()), and +//! a quad's four pixels interpolate together. One loop, +//! SoftDraw::fill_quads(), is written against the Fill trait, +//! whose implementor FillMode carries in const parameters the draw state +//! each pixel would otherwise test: texturing and the fixed-point sampler's +//! kernel, wrap, environment and format; the depth test, its function and +//! write; flat or smooth shading; blending; fog; the stencil test; client +//! clip planes. A parameter of FILL_DYN +//! reads its state at run time instead. So the same source is both a loop +//! with every test folded away and the fallback that takes any state the +//! loops admit, and the two cannot disagree: the arithmetic is the same +//! expressions in the same order, only the branches around it differ. +//! +//! The loop is also generic over the Planes implementation, the board's +//! planes whole or one row of them a loop borrows (GlPlaneRow), so a plane +//! access in the loop is a direct call the compiler can inline. +//! +//! FillRow names the specialisations, the states measured as common; +//! SoftDraw::fill_select() and SoftDraw::fixed_span_select() choose one per +//! draw, or the fallback. What a loop does per fragment is what +//! SoftDraw::fragment_at() does for the same state, through the same +//! functions and plane calls; only the order of plane reads differs (the +//! depth read before the texture's). The tests at the end compare every +//! loop pixel for pixel against the general path, which visits the same +//! quads and takes the same level of detail for each, a fragment at a time +//! (SoftDraw::clipped_triangle()). + + +use super::gl::{GlPlaneRow, GlPlanes}; +use super::gl_context::{ + GL_ALWAYS, GL_CLIP_PLANES, GL_COLOUR_MASK_ALL, GL_FLAT, GL_FUNC_ADD, GL_FUNC_REVERSE_SUBTRACT, + GL_FUNC_SUBTRACT, GL_INTENSITY, GL_KEEP, GL_LEQUAL, GL_LESS, GL_LINEAR, + GL_LINEAR_MIPMAP_LINEAR, GL_LUMINANCE_ALPHA, GL_MAX, GL_MAX_TARGETS, GL_MIN, GL_MODULATE, + GL_NEAREST, GL_ONE_MINUS_SRC_ALPHA, GL_REPEAT, GL_REPLACE, GL_RGB, GL_RGBA, GL_SRC_ALPHA, + GlDrawState, +}; +use super::memory::CLIP_MAX; +use super::raster::{ + cmax, cmin, soft_blend_factor, soft_compare, soft_unit, Planes, RasterColour, RasterRules, + RasterTarget, SoftDraw, SoftFrag, SoftVertex, TexturePath, +}; +use super::raster_fixed_texture::{ + FIXED_ANY, FIXED_LINEAR, FIXED_NEAREST, FIXED_TRILINEAR, SOFT_TEXTURE_PATH, + SoftFixedTex, SoftTexSpan, +}; +use super::raster_texture::{soft_quad_coords, soft_quad_rho_of, SoftQuadCoords, SoftTexPlane}; +/// A FillMode parameter that reads its state at run time. +pub(super) const FILL_DYN: u32 = u32::MAX; + +/// BLEND: off, the alpha-over blend (SRC_ALPHA, ONE_MINUS_SRC_ALPHA, added), +/// or FILL_DYN for any blend the draw state names. +pub(super) const FILL_BLEND_OFF: u32 = 0; +pub(super) const FILL_BLEND_OVER: u32 = 1; + +/// Which row loops filled triangles take, for tests and the benchmark: +/// TABLE the specialisations where the state has one, DYNAMIC the +/// fallback loops for every state they admit, OFF no loop for untextured +/// fills (the general per-fragment path; textured fills keep their own +/// switch, raster_set_texture_path()). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) enum FillPath { + Table, + Dynamic, + Off, +} + +thread_local! { + pub(super) static SOFT_FILL_PATH: std::cell::Cell = const { std::cell::Cell::new(FillPath::Table) }; +} + +/// Select the row loops for draws made on this thread. Per thread, so that +/// tests running at once cannot change each other's path. +pub(super) fn raster_set_fill_path(path: FillPath) { + SOFT_FILL_PATH.with(|p| p.set(path)); +} + +/// The draw state a row loop branches on. Under FillMode each accessor is a +/// constant where its parameter is, and a read of the state where it is +/// FILL_DYN. +pub(super) trait Fill { + /// The loop that tests at run time all the state this one names: the + /// fallback with the same texturing. + type Dyn: Fill; + /// Fragments are textured through the fixed-point sampler. + const TEX: bool; + /// The texture environment and base format, or 0 to read them. + const ENV: u32; + const FORMAT: u32; + /// The fixed-point sampler under the loop's kernel and wrap. + fn sample(r: &SoftDraw, s: f64, t: f64, lambda: i32, texel: &mut [f64; 4]) -> bool; + fn depth_test(st: &GlDrawState) -> bool; + fn depth_func(st: &GlDrawState) -> u32; + fn depth_write(st: &GlDrawState) -> bool; + fn smooth(sp: &SoftTexSpan) -> bool; + fn blend(r: &SoftDraw) -> bool; + fn blend_src(st: &GlDrawState) -> u32; + fn blend_dst(st: &GlDrawState) -> u32; + fn blend_equation(st: &GlDrawState) -> u32; + fn fog(st: &GlDrawState) -> bool; + /// The stencil test and its operations, on planes with a stencil buffer. + fn stencil(r: &SoftDraw) -> bool; + /// The client clip planes, any enabled. + fn clip(st: &GlDrawState) -> bool; +} + +/// The row loop that reads all of its state at run time. +pub(super) type FillDyn = + FillMode; + +/// The const draw state of one row loop (see the Fill trait). DEPTH, WRITE, +/// SMOOTH and FOG are 0, 1 or FILL_DYN; FUNC a depth function or FILL_DYN; +/// BLEND one of the FILL_BLEND_ values or FILL_DYN; STENCIL and CLIP 0 or +/// FILL_DYN. +pub(super) struct FillMode< + const TEX: bool, + const KERNEL: u32, + const REPEAT: bool, + const ENV: u32, + const FORMAT: u32, + const DEPTH: u32, + const FUNC: u32, + const WRITE: u32, + const SMOOTH: u32, + const BLEND: u32, + const FOG: u32, + const STENCIL: u32, + const CLIP: u32, +>; + +impl< + const TEX: bool, + const KERNEL: u32, + const REPEAT: bool, + const ENV: u32, + const FORMAT: u32, + const DEPTH: u32, + const FUNC: u32, + const WRITE: u32, + const SMOOTH: u32, + const BLEND: u32, + const FOG: u32, + const STENCIL: u32, + const CLIP: u32, + > Fill for FillMode +{ + type Dyn = + FillMode; + const TEX: bool = TEX; + const ENV: u32 = ENV; + const FORMAT: u32 = FORMAT; + + #[inline(always)] + fn sample(r: &SoftDraw, s: f64, t: f64, lambda: i32, texel: &mut [f64; 4]) -> bool { + SoftDraw::fixed_sample::(r, s, t, lambda, texel) + } + #[inline(always)] + fn depth_test(st: &GlDrawState) -> bool { + if DEPTH == FILL_DYN { st.depth_test } else { DEPTH != 0 } + } + #[inline(always)] + fn depth_func(st: &GlDrawState) -> u32 { + if FUNC == FILL_DYN { st.depth_func } else { FUNC } + } + #[inline(always)] + fn depth_write(st: &GlDrawState) -> bool { + if WRITE == FILL_DYN { st.depth_mask } else { WRITE != 0 } + } + #[inline(always)] + fn smooth(sp: &SoftTexSpan) -> bool { + if SMOOTH == FILL_DYN { sp.smooth } else { SMOOTH != 0 } + } + #[inline(always)] + fn blend(r: &SoftDraw) -> bool { + if BLEND == FILL_DYN { r.blend } else { BLEND != FILL_BLEND_OFF } + } + #[inline(always)] + fn blend_src(st: &GlDrawState) -> u32 { + if BLEND == FILL_BLEND_OVER { GL_SRC_ALPHA } else { st.blend_src } + } + #[inline(always)] + fn blend_dst(st: &GlDrawState) -> u32 { + if BLEND == FILL_BLEND_OVER { GL_ONE_MINUS_SRC_ALPHA } else { st.blend_dst } + } + #[inline(always)] + fn blend_equation(st: &GlDrawState) -> u32 { + if BLEND == FILL_BLEND_OVER { GL_FUNC_ADD } else { st.blend_equation } + } + #[inline(always)] + fn fog(st: &GlDrawState) -> bool { + if FOG == FILL_DYN { st.fog } else { FOG != 0 } + } + #[inline(always)] + fn stencil(r: &SoftDraw) -> bool { + STENCIL == FILL_DYN && r.st.stencil_test && r.rules.stencil_max().is_some() + } + #[inline(always)] + fn clip(st: &GlDrawState) -> bool { + CLIP == FILL_DYN && st.clip_planes != 0 + } +} + +/// A row's pixels as the window's clip rectangles and the scissor leave +/// them (SoftDraw::fill_run()): px0 to px1, the edge values at px0, and +/// whether a pixel must still test the window's rectangles. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) struct FillRun { + pub px0: i32, + pub px1: i32, + pub e1: i64, + pub e2: i64, + pub clipped: bool, +} + +impl SoftDraw<'_> { + /// The blend of one component with the factors and equation M names. + #[inline(always)] + pub fn fill_blend_component(&self, k: usize, src: &[f64; 4], dst: &[f64; 4]) -> f64 { + let st: &GlDrawState = self.st; + let sf = soft_blend_factor(M::blend_src(st), k, src, dst, &st.blend_color); + let df = soft_blend_factor(M::blend_dst(st), k, src, dst, &st.blend_color); + + match M::blend_equation(st) { + GL_MIN => cmin(src[k], dst[k]), + GL_MAX => cmax(src[k], dst[k]), + GL_FUNC_SUBTRACT => src[k] * sf - dst[k] * df, + GL_FUNC_REVERSE_SUBTRACT => dst[k] * df - src[k] * sf, + _ => src[k] * sf + dst[k] * df, + } + } + + /// Pixels px0 to px1 of row py of a filled triangle, whose edge values + /// for v[2]-v[0] and v[0]-v[1] at px0 are e1 and e2, as the window's clip + /// rectangles and the scissor leave them for the whole row; None where + /// they leave none. + #[allow(clippy::too_many_arguments, reason = "a row loop takes the row as its parts")] + #[inline(always)] + pub fn fill_run(&self, sp: &SoftTexSpan, py: i32, mut px0: i32, mut px1: i32, mut e1: i64, mut e2: i64) -> Option { + let st: &GlDrawState = self.st; + let y = self.win.oy.wrapping_add(py); + let mut clipped = self.win.clip.n > 1; + + let mut i = 0u32; + while clipped && i < cmin(self.win.clip.n, CLIP_MAX as u32) { + // One of the window's rectangles holds the whole row. + let c = &self.win.clip.r[i as usize]; + + clipped = !(y >= c.y0 + && y <= c.y1 + && (self.win.ox as i64) + px0 as i64 >= c.x0 as i64 + && (self.win.ox as i64) + px1 as i64 <= c.x1 as i64); + i += 1; + } + if st.scissor_test && !clipped { + // SoftDraw::owned_at()'s scissor test, decided for the whole row. + let sx0 = st.scissor[0] as i64; + let sx1 = st.scissor[0] as i64 + st.scissor[2] as i64 - 1; + + if !(py >= st.scissor[1] && (py as i64) < st.scissor[1] as i64 + st.scissor[3] as i64) { + return None; + } + if sx0 > px0 as i64 { + if sx0 > px1 as i64 { + return None; + } + e1 = e1.wrapping_add((sx0 - px0 as i64).wrapping_mul(sp.step1)); + e2 = e2.wrapping_add((sx0 - px0 as i64).wrapping_mul(sp.step2)); + px0 = sx0 as i32; + } + px1 = cmin(px1 as i64, sx1) as i32; + } + (px0 <= px1).then_some(FillRun { px0, px1, e1, e2, clipped }) + } + + /// The pixels of runs, rows qy and qy + 1 of a filled triangle (qy + /// even), a 2x2 quad at a time, under state SoftDraw::fill_select() or + /// SoftDraw::fixed_span_select() admits, and whose varying parts M names, + /// into planes px, one for each row: self.px, or the rows of it + /// GlPlanes::row() lends. Each quad interpolates its four pixels together, + /// drawn or not, and a textured quad takes one level of detail for all + /// four (soft_quad_rho()); then each pixel the runs hold is tested and + /// stored on its own. A fragment the fixed sampler cannot take goes to + /// the general path, general. + #[inline(always)] + pub fn fill_quads( + &self, + px: [&P; 2], + sp: &SoftTexSpan, + qy: i32, + runs: &[Option; 2], + general: &QuadGeneral, + ) { + let rules: &RasterRules = self.rules; + let st: &GlDrawState = self.st; + let t: &RasterTarget = self.t; + let v: &[SoftVertex; 3] = sp.v; + let g: &SoftTexPlane = sp.g; + let depth_test = M::depth_test(st); + let depth_func = M::depth_func(st); + let depth_write = depth_test && M::depth_write(st); + let fog = M::fog(st); + let clip = M::clip(st); + let smooth = M::smooth(sp); + // The stencil test as RasterRules::depth_stencil() makes it: the + // reference clamped to the buffer's range, compared under the value + // mask, and the operation each outcome names. + let stencil = M::stencil(self); + let smax = if stencil { self.rules.stencil_max().unwrap_or(0) } else { 0 }; + let sref = if st.stencil_ref < 0 { 0 } else { cmin(st.stencil_ref as u32, smax) }; + let svalue = sref & st.stencil_value_mask; + let rgba = !t.index; + let rgba_read = rgba && self.rgba_read; + let store = self.rgba_mask != 0 && self.ntargets != 0; + let env = if !M::TEX { 0 } else if M::ENV != 0 { M::ENV } else { st.texture_env_mode }; + let format = if !M::TEX { 0 } else if M::FORMAT != 0 { M::FORMAT } else { self.fixed_tex().format }; + let lod = M::TEX && self.fixed_tex().lod; + // REPLACE leaves none of the colour of these formats' fragments. + let keeps = !M::TEX + || env != GL_REPLACE + || (format != GL_RGBA && format != GL_INTENSITY && format != GL_LUMINANCE_ALPHA); + let mut frag: SoftFrag = *sp.flat; + let mut dest = [RasterColour::default(); GL_MAX_TARGETS as usize]; + let (x_lo, x_hi) = match runs { + [Some(a), Some(b)] => (cmin(a.px0, b.px0), cmax(a.px1, b.px1)), + [Some(a), None] | [None, Some(a)] => (a.px0, a.px1), + [None, None] => return, + }; + + // Nothing in an untextured flat colour varies over the triangle + // unless it is fogged or blended. + let uniform = rgba && store && !M::TEX && !smooth && !fog && !rgba_read; + let flat_out = if uniform { + self.rgba_result::(&frag, &dest[0]) + } else { + RasterColour::default() + }; + // The polygon offset, split between window depth and depth-word steps + // (RasterRules::split_offset()). + let (offset, steps) = rules.split_offset(sp.offset); + // The texture planes' row terms, for each row. + let mut tex_row = [[0.0f64; 4]; 2]; + if M::TEX { + for (r, row) in tex_row.iter_mut().enumerate() { + for k in 0..4 { + row[k] = ((qy + r as i32) as f64 + 0.5 - g.y) * g.dy[k]; + } + } + } + let mut qx = x_lo - (x_lo & 1); + while qx <= x_hi { + // Pixel i of the quad is column qx + (i & 1) of row qy + (i >> 1). + let mut drawn = [false; 4]; + let mut e1 = [0i64; 4]; + let mut e2 = [0i64; 4]; + for i in 0..4 { + if let Some(run) = &runs[i >> 1] { + let k = (qx + (i & 1) as i32 - run.px0) as i64; + + drawn[i] = k >= 0 && qx + (i & 1) as i32 <= run.px1; + e1[i] = run.e1.wrapping_add(k.wrapping_mul(sp.step1)); + e2[i] = run.e2.wrapping_add(k.wrapping_mul(sp.step2)); + } + } + let quad_x = qx; + qx += 2; + if drawn == [false; 4] { + continue; + } + // The quad's four pixels interpolated together: for each, the + // same arithmetic, in the same order, as one at a time. + let mut b1 = [0.0f64; 4]; + let mut b2 = [0.0f64; 4]; + let mut zi = [0.0f64; 4]; + let mut w0 = [0.0f64; 4]; + let mut w1 = [0.0f64; 4]; + let mut w2 = [0.0f64; 4]; + let mut sum = [0.0f64; 4]; + let mut depth_w = [0.0f64; 4]; + for i in 0..4 { + b1[i] = e1[i] as f64 / sp.area as f64; + b2[i] = e2[i] as f64 / sp.area as f64; + zi[i] = v[0].z + b1[i] * (v[1].z - v[0].z) + b2[i] * (v[2].z - v[0].z); + w0[i] = (1.0 - b1[i] - b2[i]) * v[0].inv_w; + w1[i] = b1[i] * v[1].inv_w; + w2[i] = b2[i] * v[2].inv_w; + sum[i] = w0[i] + w1[i] + w2[i]; + depth_w[i] = (1.0 - b1[i] - b2[i]) * v[0].depth_w + b1[i] * v[1].depth_w + b2[i] * v[2].depth_w; + } + // The quad's level of detail, where it has one. + // Its pixels' coordinates come with the footprint, where that is + // taken from them. + let mut coords: Option = None; + let lambda = if !lod { + None + } else if sp.flat_lambda.is_some() { + sp.flat_lambda + } else if let Some((ns, nt)) = general.scales { + let c = coords.insert(soft_quad_coords(g, quad_x, qy)); + + soft_quad_rho_of(c, ns, nt).and_then(|rho| self.fixed_tex().quad_lambda(rho)) + } else { + None + }; + for i in 0..4 { + if !drawn[i] { + continue; + } + let r = i >> 1; + let col = quad_x + (i & 1) as i32; + let py = qy + r as i32; + let planes = px[r]; + let x = self.win.ox.wrapping_add(col); + let y = self.win.oy.wrapping_add(py); + let mut z = zi[i]; + let mut off = offset; + let mut zq = 0u32; + + if rules.depth_row_given { + let slack = rules.depth_slack; + + z = (z + off) / depth_w[i]; + off = 0.0; + if z < 0.0 && z >= -slack { + z = 0.0; + } else if z > 1.0 && z <= 1.0 + slack { + z = 1.0; + } + } + let Some(zw) = st.window_depth(z, off) else { + continue; + }; + if runs[r].is_some_and(|run| run.clipped) && !self.owned_at(col, py) { + continue; + } + if clip { + let mut d = [0.0f64; GL_CLIP_PLANES as usize]; + + for (k, dk) in d.iter_mut().enumerate() { + if st.clip_planes & (1u32 << k) != 0 { + *dk = (w0[i] * v[0].clip[k] + w1[i] * v[1].clip[k] + w2[i] * v[2].clip[k]) / sum[i]; + } + } + if st.clipped(&d) { + continue; + } + } + // Which of the stencil operations the fragment takes: 0 where + // the stencil test fails, 1 the depth test, 2 where it passes. + let mut outcome = 2usize; + if stencil && !soft_compare(st.stencil_func, svalue, planes.load_stencil(x, y) & smax & st.stencil_value_mask) { + outcome = 0; + } + if depth_test && outcome == 2 { + zq = rules.depth_code_offset(zw, steps); + if !soft_compare(depth_func, zq, planes.load_depth(x, y)) { + outcome = 1; + } + } + // A failed fragment that writes nothing is done with. + if outcome != 2 && !(stencil && st.stencil_op[outcome] != GL_KEEP) { + continue; + } + if M::TEX { + let at = match (&coords, lambda) { + (Some(c), Some(l)) => SoftFixedTex::quad_coords(c, i, l), + _ => self.fixed_tex().fixed_coords(g, &tex_row[r], col, lambda), + }; + match at { + Some((s, tc, l)) if M::sample(self, s, tc, l, &mut frag.texel) => {} + _ => { + (general.fragment)(col, py, e1[i], e2[i]); + continue; + } + } + } + if outcome != 2 { + // The general path reads the destination before the + // stencil test, and one it cannot read drops the fragment. + if rgba_read && !rules.rgba_load(planes, &self.targets, self.ntargets, x, y, &mut dest, &self.reports.rgba) { + continue; + } + st.stencil_op(planes, x, y, st.stencil_op[outcome], smax, sref); + continue; + } + if uniform { + if depth_write { + planes.store_depth(x, y, zq); + } + if stencil { + st.stencil_op(planes, x, y, st.stencil_op[2], smax, sref); + } + for &target in &self.targets[..self.ntargets as usize] { + planes.store_rgba(target, x, y, &flat_out); + } + continue; + } + if smooth && keeps { + for k in 0..4 { + frag.rgba[k] = if sp.constant & (1u8 << k) != 0 { + v[0].rgba[k] + } else { + (w0[i] * v[0].rgba[k] + w1[i] * v[1].rgba[k] + w2[i] * v[2].rgba[k]) / sum[i] + }; + } + } else { + frag.rgba = sp.flat.rgba; + } + if M::TEX { + self.texture_apply(format, env, &mut frag.texel, &mut frag.rgba); + } + if fog { + self.fog(&mut frag, zw); + } + if rgba_read && !rules.rgba_load(planes, &self.targets, self.ntargets, x, y, &mut dest, &self.reports.rgba) { + continue; + } + if depth_write { + planes.store_depth(x, y, zq); + } + if stencil { + st.stencil_op(planes, x, y, st.stencil_op[2], smax, sref); + } + if !rgba { + // Packed colour planes: the general store. + self.fragment_store(x, y, &frag, &dest); + } else if store { + for k in 0..self.ntargets as usize { + let out = self.rgba_result::(&frag, &dest[k]); + + planes.store_rgba(self.targets[k], x, y, &out); + } + } + } + } + } +} + +/// What a quad loop leaves to the triangle's general path: scales, the +/// per-axis scales (ns, nt) of the footprint of a quad under the diagonal +/// rule (soft_quad_rho_of()), or None where its fragments take their own; +/// and fragment, which draws the fragment at window pixel (px, py) whose edge +/// values for v[2]-v[0] and v[0]-v[1] are e1 and e2 through the general +/// path, its quad's footprint included. +pub(super) struct QuadGeneral<'g> { + pub scales: Option<(f64, f64)>, + pub fragment: &'g dyn Fn(i32, i32, i64, i64), +} + +/// The owned run of one row of a triangle: pixels px0 to px1, whose edge +/// values for v[2]-v[0] and v[0]-v[1] at px0 are e1 and e2. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(super) struct QuadRow { + pub px0: i32, + pub px1: i32, + pub e1: i64, + pub e2: i64, +} + +/// Generates FillRow, one variant per specialisation, and the dispatch +/// from a variant to its instance of SoftDraw::fill_row(). Each entry is the +/// texture's kernel, wrap, environment and format, then the depth test, +/// function and write, shading, blend, fog, stencil and clip planes. +macro_rules! fill_rows { + ($($id:ident = ($tex:expr, $kernel:expr, $repeat:expr, $env:expr, $format:expr, + $depth:expr, $func:expr, $write:expr, $smooth:expr, $blend:expr, $fog:expr, + $stencil:expr, $clip:expr);)*) => { + /// A row loop for filled triangles (raster_fill.rs). + #[allow(non_camel_case_types)] + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub enum FillRow { + $($id,)* + } + + /// The quads of rows qy and qy + 1 of a filled triangle, whose owned + /// runs are rows, through the quad loop row names, on planes px. + #[inline] + pub fn fill_quads_dispatch( + px: &GlPlanes, + r: &SoftDraw, + row: FillRow, + sp: &SoftTexSpan, + qy: i32, + rows: &[Option; 2], + general: &QuadGeneral, + ) { + match row { + $(FillRow::$id => fill_quads_instance::>(px, r, sp, qy, rows, general),)* + } + } + }; +} + +/// One specialisation as a function of its own. It writes through the rows +/// of planes GlPlanes::row() lends, which test the plane bounds and clip +/// rectangles once for a row; where the planes cannot lend a row, the +/// fallback with the same texturing draws the quads a pixel at a time +/// through the planes' own methods, which give the same pixels. +#[inline(never)] +fn fill_quads_instance( + px: &GlPlanes, + r: &SoftDraw, + sp: &SoftTexSpan, + qy: i32, + rows: &[Option; 2], + general: &QuadGeneral, +) { + let mut runs: [Option; 2] = [None, None]; + let mut lent: [Option; 2] = [None, None]; + let mut lends = true; + + for i in 0..2 { + let py = qy + i as i32; + + runs[i] = rows[i].and_then(|q| r.fill_run(sp, py, q.px0, q.px1, q.e1, q.e2)); + if let Some(run) = &runs[i] { + lent[i] = px.row(r.win.oy.wrapping_add(py), r.win.ox.wrapping_add(run.px0), r.win.ox.wrapping_add(run.px1)); + lends &= lent[i].is_some(); + } + } + if runs == [None, None] { + return; + } + if lends { + // A row with no run takes the other's planes, and draws nothing. + let other = lent[0].as_ref().or(lent[1].as_ref()).expect("a lent row"); + + r.fill_quads::([lent[0].as_ref().unwrap_or(other), lent[1].as_ref().unwrap_or(other)], sp, qy, &runs, general); + } else { + fill_quads_pixels::(px, r, sp, qy, &runs, general); + } +} + +/// Quads a pixel at a time through planes px (fill_quads_instance()). +#[inline(never)] +fn fill_quads_pixels( + px: &GlPlanes, + r: &SoftDraw, + sp: &SoftTexSpan, + qy: i32, + runs: &[Option; 2], + general: &QuadGeneral, +) { + r.fill_quads::([px, px], sp, qy, runs, general) +} + +const D: u32 = FILL_DYN; +const OFF: u32 = FILL_BLEND_OFF; +const OVER: u32 = FILL_BLEND_OVER; +const NEAREST: u32 = FIXED_NEAREST; +const LINEAR: u32 = FIXED_LINEAR; +const TRILINEAR: u32 = FIXED_TRILINEAR; +const REPLACE: u32 = GL_REPLACE; +const MODULATE: u32 = GL_MODULATE; +const RGBA: u32 = GL_RGBA; +const RGB: u32 = GL_RGB; +const LESS: u32 = GL_LESS; +const LEQUAL: u32 = GL_LEQUAL; +const ALWAYS: u32 = GL_ALWAYS; + +fill_rows! { + // The fallbacks: any state the loops take, tested per pixel. + TextureAny = (true, FIXED_ANY, false, 0, 0, D, D, D, D, D, D, D, D); + FillAny = (false, 0, false, 0, 0, D, D, D, D, D, D, D, D); + + // Textured: a repeated RGB or RGBA image, nearest, bilinear or + // trilinear, replacing or modulating the fragment's colour, depth + // tested LESS or LEQUAL and written, without blending or fog. + NearestReplaceRgbaLess = (true, NEAREST, true, REPLACE, RGBA, 1, LESS, 1, D, OFF, 0, D, D); + NearestReplaceRgbLess = (true, NEAREST, true, REPLACE, RGB, 1, LESS, 1, D, OFF, 0, D, D); + NearestModulateRgbaLess = (true, NEAREST, true, MODULATE, RGBA, 1, LESS, 1, D, OFF, 0, D, D); + NearestModulateRgbLess = (true, NEAREST, true, MODULATE, RGB, 1, LESS, 1, D, OFF, 0, D, D); + LinearReplaceRgbaLess = (true, LINEAR, true, REPLACE, RGBA, 1, LESS, 1, D, OFF, 0, D, D); + LinearReplaceRgbLess = (true, LINEAR, true, REPLACE, RGB, 1, LESS, 1, D, OFF, 0, D, D); + LinearModulateRgbaLess = (true, LINEAR, true, MODULATE, RGBA, 1, LESS, 1, D, OFF, 0, D, D); + LinearModulateRgbLess = (true, LINEAR, true, MODULATE, RGB, 1, LESS, 1, D, OFF, 0, D, D); + TrilinearReplaceRgbaLess = (true, TRILINEAR, true, REPLACE, RGBA, 1, LESS, 1, D, OFF, 0, D, D); + TrilinearReplaceRgbLess = (true, TRILINEAR, true, REPLACE, RGB, 1, LESS, 1, D, OFF, 0, D, D); + TrilinearModulateRgbaLess = (true, TRILINEAR, true, MODULATE, RGBA, 1, LESS, 1, D, OFF, 0, D, D); + TrilinearModulateRgbLess = (true, TRILINEAR, true, MODULATE, RGB, 1, LESS, 1, D, OFF, 0, D, D); + NearestReplaceRgbaLequal = (true, NEAREST, true, REPLACE, RGBA, 1, LEQUAL, 1, D, OFF, 0, D, D); + NearestReplaceRgbLequal = (true, NEAREST, true, REPLACE, RGB, 1, LEQUAL, 1, D, OFF, 0, D, D); + NearestModulateRgbaLequal = (true, NEAREST, true, MODULATE, RGBA, 1, LEQUAL, 1, D, OFF, 0, D, D); + NearestModulateRgbLequal = (true, NEAREST, true, MODULATE, RGB, 1, LEQUAL, 1, D, OFF, 0, D, D); + LinearReplaceRgbaLequal = (true, LINEAR, true, REPLACE, RGBA, 1, LEQUAL, 1, D, OFF, 0, D, D); + LinearReplaceRgbLequal = (true, LINEAR, true, REPLACE, RGB, 1, LEQUAL, 1, D, OFF, 0, D, D); + LinearModulateRgbaLequal = (true, LINEAR, true, MODULATE, RGBA, 1, LEQUAL, 1, D, OFF, 0, D, D); + LinearModulateRgbLequal = (true, LINEAR, true, MODULATE, RGB, 1, LEQUAL, 1, D, OFF, 0, D, D); + TrilinearReplaceRgbaLequal = (true, TRILINEAR, true, REPLACE, RGBA, 1, LEQUAL, 1, D, OFF, 0, D, D); + TrilinearReplaceRgbLequal = (true, TRILINEAR, true, REPLACE, RGB, 1, LEQUAL, 1, D, OFF, 0, D, D); + TrilinearModulateRgbaLequal = (true, TRILINEAR, true, MODULATE, RGBA, 1, LEQUAL, 1, D, OFF, 0, D, D); + TrilinearModulateRgbLequal = (true, TRILINEAR, true, MODULATE, RGB, 1, LEQUAL, 1, D, OFF, 0, D, D); + + // Textured, wrapped on one axis and clamped to the edge on the other: + // perfly's trilinear RGB truck, which the repeated entries above do not + // take. The wrap is tested per tap. + TrilinearModulateRgbLequalEdge = (true, TRILINEAR, false, MODULATE, RGB, 1, LEQUAL, 1, D, OFF, 0, D, D); + + // Blended alpha-over and depth tested: Smoke & Mirrors' bilinear RGB + // glass, depth LESS and written, and perfly's trilinear RGBA sky, + // depth ALWAYS and not written. + LinearModulateRgbLessOver = (true, LINEAR, true, MODULATE, RGB, 1, LESS, 1, D, OVER, 0, D, D); + TrilinearModulateRgbaAlwaysOver = (true, TRILINEAR, true, MODULATE, RGBA, 1, ALWAYS, 0, D, OVER, 0, D, D); + + // Untextured: no depth test, or LESS or LEQUAL written; flat or + // smooth; unblended or blended alpha-over; no fog. + NodepthFlat = (false, 0, false, 0, 0, 0, D, 1, 0, OFF, 0, 0, 0); + NodepthSmooth = (false, 0, false, 0, 0, 0, D, 1, 1, OFF, 0, 0, 0); + NodepthFlatOver = (false, 0, false, 0, 0, 0, D, 1, 0, OVER, 0, 0, 0); + NodepthSmoothOver = (false, 0, false, 0, 0, 0, D, 1, 1, OVER, 0, 0, 0); + LessFlat = (false, 0, false, 0, 0, 1, LESS, 1, 0, OFF, 0, 0, 0); + LessSmooth = (false, 0, false, 0, 0, 1, LESS, 1, 1, OFF, 0, 0, 0); + LessFlatOver = (false, 0, false, 0, 0, 1, LESS, 1, 0, OVER, 0, 0, 0); + LessSmoothOver = (false, 0, false, 0, 0, 1, LESS, 1, 1, OVER, 0, 0, 0); + LequalFlat = (false, 0, false, 0, 0, 1, LEQUAL, 1, 0, OFF, 0, 0, 0); + LequalSmooth = (false, 0, false, 0, 0, 1, LEQUAL, 1, 1, OFF, 0, 0, 0); + LequalFlatOver = (false, 0, false, 0, 0, 1, LEQUAL, 1, 0, OVER, 0, 0, 0); + LequalSmoothOver = (false, 0, false, 0, 0, 1, LEQUAL, 1, 1, OVER, 0, 0, 0); +} + +/// Textured specialisations by depth function (LESS, LEQUAL), kernel less +/// one, then replace or modulate, then RGBA or RGB. +pub(super) const SOFT_TEXTURE_SPANS: [[[[FillRow; 2]; 2]; 3]; 2] = { + use FillRow::*; + [ + [ + [[NearestReplaceRgbaLess, NearestReplaceRgbLess], [NearestModulateRgbaLess, NearestModulateRgbLess]], + [[LinearReplaceRgbaLess, LinearReplaceRgbLess], [LinearModulateRgbaLess, LinearModulateRgbLess]], + [[TrilinearReplaceRgbaLess, TrilinearReplaceRgbLess], [TrilinearModulateRgbaLess, TrilinearModulateRgbLess]], + ], + [ + [[NearestReplaceRgbaLequal, NearestReplaceRgbLequal], [NearestModulateRgbaLequal, NearestModulateRgbLequal]], + [[LinearReplaceRgbaLequal, LinearReplaceRgbLequal], [LinearModulateRgbaLequal, LinearModulateRgbLequal]], + [ + [TrilinearReplaceRgbaLequal, TrilinearReplaceRgbLequal], + [TrilinearModulateRgbaLequal, TrilinearModulateRgbLequal], + ], + ], + ] +}; + +/// The specialisations for a wrap other than REPEAT on both axes, indexed as +/// SOFT_TEXTURE_SPANS; None where only the loop that tests every state per +/// pixel (FillRow::TextureAny) takes the draw. +pub(super) const SOFT_TEXTURE_SPANS_EDGE: [[[[Option; 2]; 2]; 3]; 2] = { + use FillRow::*; + let mut t = [[[[None; 2]; 2]; 3]; 2]; + + t[1][2][1][1] = Some(TrilinearModulateRgbLequalEdge); + t +}; + +/// Untextured specialisations by depth (none, LESS, LEQUAL), shading (flat, +/// smooth), blend (off, over). +pub(super) const SOFT_FILL_TABLE: [[[FillRow; 2]; 2]; 3] = { + use FillRow::*; + [ + [[NodepthFlat, NodepthFlatOver], [NodepthSmooth, NodepthSmoothOver]], + [[LessFlat, LessFlatOver], [LessSmooth, LessSmoothOver]], + [[LequalFlat, LequalFlatOver], [LequalSmooth, LequalSmoothOver]], + ] +}; + +impl SoftDraw<'_> { + /// Whether the draw's blend is the alpha-over one the specialisations name. + pub fn fill_blend_over(&self) -> bool { + let st: &GlDrawState = self.st; + + st.blend_src == GL_SRC_ALPHA + && st.blend_dst == GL_ONE_MINUS_SRC_ALPHA + && st.blend_equation != GL_MIN + && st.blend_equation != GL_MAX + && st.blend_equation != GL_FUNC_SUBTRACT + && st.blend_equation != GL_FUNC_REVERSE_SUBTRACT + } + + /// The state every row loop leaves to SoftDraw::fragment_at(): what none + /// of them does per fragment: an alpha test, a logical operation or a + /// polygon stipple. + pub fn fill_admits(&self) -> bool { + let st: &GlDrawState = self.st; + + !st.alpha_test && !self.logic_op && self.polygon_stipple.is_none() + } + + /// The row loop for the draw's untextured filled triangles, or None for + /// the general per-fragment path. + pub fn fill_select(&self) -> Option { + let st: &GlDrawState = self.st; + let path = SOFT_FILL_PATH.with(|p| p.get()); + + if path == FillPath::Off || self.texture.is_some() || !self.fill_admits() { + return None; + } + let depth: i32 = if !st.depth_test { + 0 + } else if !st.depth_mask { + -1 + } else if st.depth_func == GL_LESS { + 1 + } else if st.depth_func == GL_LEQUAL { + 2 + } else { + -1 + }; + let blend: i32 = if !self.blend { + 0 + } else if self.fill_blend_over() { + 1 + } else { + -1 + }; + let stencil = st.stencil_test && self.rules.stencil_max().is_some(); + if path == FillPath::Dynamic || depth < 0 || blend < 0 || st.fog || stencil || st.clip_planes != 0 { + return Some(FillRow::FillAny); + } + let smooth = (st.shade_model != GL_FLAT) as usize; + Some(SOFT_FILL_TABLE[depth as usize][smooth][blend as usize]) + } + + /// The row loop for the draw's textured triangles, sampled through f, or + /// None for the general loop: under TexturePath::Spans, for state + /// SoftDraw::fill_admits() takes (the fixed-point sampler already + /// excludes the rest). A loop of its own for the common states + /// SOFT_TEXTURE_SPANS, SOFT_TEXTURE_SPANS_EDGE and the two blended + /// entries name; the others take + /// the loop that tests the state per pixel. + pub fn fixed_span_select(&self, f: &SoftFixedTex) -> Option { + let st: &GlDrawState = self.st; + + if SOFT_TEXTURE_PATH.with(|p| p.get()) != TexturePath::Spans || !self.fill_admits() { + return None; + } + let kernel: i32 = if !f.lod && f.min == GL_NEAREST { + 0 + } else if !f.lod && f.min == GL_LINEAR { + 1 + } else if f.min == GL_LINEAR_MIPMAP_LINEAR && f.mag == GL_LINEAR { + 2 + } else { + -1 + }; + let env: i32 = if st.texture_env_mode == GL_REPLACE { + 0 + } else if st.texture_env_mode == GL_MODULATE { + 1 + } else { + -1 + }; + let format: i32 = if f.format == GL_RGBA { + 0 + } else if f.format == GL_RGB { + 1 + } else { + -1 + }; + let func: i32 = if st.depth_func == GL_LESS { + 0 + } else if st.depth_func == GL_LEQUAL { + 1 + } else { + -1 + }; + if SOFT_FILL_PATH.with(|p| p.get()) == FillPath::Dynamic + || kernel < 0 + || env < 0 + || !st.depth_test + || st.fog + { + return Some(FillRow::TextureAny); + } + let repeat = f.wrap_s == GL_REPEAT && f.wrap_t == GL_REPEAT; + if self.blend { + // The two blended states named for themselves. + let over = repeat && env == 1 && self.fill_blend_over(); + if over && kernel == 1 && format == 1 && st.depth_func == GL_LESS && st.depth_mask { + return Some(FillRow::LinearModulateRgbLessOver); + } + if over && kernel == 2 && format == 0 && st.depth_func == GL_ALWAYS && !st.depth_mask { + return Some(FillRow::TrilinearModulateRgbaAlwaysOver); + } + return Some(FillRow::TextureAny); + } + if format < 0 || func < 0 || !st.depth_mask { + return Some(FillRow::TextureAny); + } + let (func, kernel, env, format) = (func as usize, kernel as usize, env as usize, format as usize); + if !repeat { + return Some(SOFT_TEXTURE_SPANS_EDGE[func][kernel][env][format].unwrap_or(FillRow::TextureAny)); + } + Some(SOFT_TEXTURE_SPANS[func][kernel][env][format]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::dev::odyssey::consts::{tex, ODSY_ID_COMPARE_ENABLE, ODSY_ID_COMPARE_MASK_SHIFT}; + use crate::dev::odyssey::gl::{texture_lod_limit, texture_lod_word}; + use crate::dev::odyssey::gl_context::{ + vtx, GL_BLEND, GL_CLAMP_TO_EDGE, GL_CONSTANT_ALPHA, GL_DECAL, GL_DECR, GL_DST_ALPHA, + GL_EQUAL, GL_EXP, GL_EXP2, GL_GREATER, GL_INCR, GL_INVERT, GL_LUMINANCE, + GL_NEAREST_MIPMAP_NEAREST, GL_NEVER, GL_NOTEQUAL, GL_OFFSET_FILL, GL_ONE_MINUS_DST_COLOR, + GL_SMOOTH, GL_SRC_ALPHA_SATURATE, GL_TRIANGLES, GlDraw, + }; + + use crate::dev::odyssey::memory::ClipRect; + use crate::dev::odyssey::raster::{GlTexLevel, SoftReports, Texels}; + use crate::dev::odyssey::raster_bench::{BENCH_H, BENCH_W, Bench, BenchTexture, Lcg}; + use crate::dev::odyssey::raster_fixed_texture::raster_set_texture_path; + use crate::dev::odyssey::raster_triangle::raster_set_aa_whole_box; + + + + /// The square of plane pixels the cases draw in. + const SIDE: i32 = 160; + + fn pick(rng: &mut Lcg, from: &[T]) -> T { + from[rng.next_u32() as usize % from.len()] + } + + /// A random draw state, biased toward the specialised ones, and + /// triangles in perspective, some crossing the near and far planes. + fn case(seed: u64, textured: bool) -> Bench { + let mut b = Bench::empty(); + let mut rng = Lcg(seed.wrapping_mul(0x9e3779b97f4a7c15) | 1); + let st = &mut b.st; + + st.depth_test = rng.next_u32() % 4 != 0; + st.depth_func = pick(&mut rng, &[GL_LESS, GL_LEQUAL, GL_LESS, GL_LEQUAL, GL_GREATER, GL_ALWAYS, GL_EQUAL, GL_NOTEQUAL, GL_NEVER]); + st.depth_mask = rng.next_u32() % 5 != 0; + st.shade_model = pick(&mut rng, &[GL_FLAT, GL_SMOOTH]); + // Once the dither enable; still drawn so the seeds keep their cases. + rng.next_u32(); + if rng.next_u32() % 2 == 0 { + st.blend = true; + if rng.next_u32() % 2 == 0 { + st.blend_src = GL_SRC_ALPHA; + st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + } else { + let factors = [0, 1, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA, GL_ONE_MINUS_DST_COLOR, GL_SRC_ALPHA_SATURATE, GL_CONSTANT_ALPHA]; + st.blend_src = pick(&mut rng, &factors); + st.blend_dst = pick(&mut rng, &factors); + st.blend_color = [0.25, 0.5, 0.75, 0.3]; + } + st.blend_equation = pick(&mut rng, &[0, GL_FUNC_ADD, GL_FUNC_ADD, GL_MIN, GL_FUNC_SUBTRACT]); + } + if rng.next_u32() % 4 == 0 { + st.fog = true; + st.fog_mode = pick(&mut rng, &[GL_LINEAR, GL_EXP, GL_EXP2]); + st.fog_start = 0.0; + st.fog_end = 2.5; + st.fog_density = 0.7; + st.fog_color = [0.1, 0.8, 0.3, 1.0]; + } + if rng.next_u32() % 4 == 0 { + st.colour_mask = rng.next_u32() % 16; + } + if rng.next_u32() % 4 == 0 { + st.scissor_test = true; + st.scissor = [rng.next_u32() as i32 % 60, rng.next_u32() as i32 % 60, 40 + rng.next_u32() as i32 % 100, 40 + rng.next_u32() as i32 % 100]; + } + if rng.next_u32() % 4 == 0 { + st.polygon_offset = GL_OFFSET_FILL; + st.offset_factor = 1.5; + st.offset_units = 2.0; + } + if rng.next_u32() % 4 == 0 { + // Two rectangles: rows that neither holds whole test each pixel. + st.drawable.clip.n = 2; + st.drawable.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: 99, y1: SIDE - 1 }; + st.drawable.clip.r[1] = ClipRect { x0: 60, y0: 30, x1: SIDE - 1, y1: 120 }; + } + if rng.next_u32() % 3 == 0 { + random_stencil(&mut rng, st); + } + if rng.next_u32() % 4 == 0 { + random_clip(&mut rng, st); + } + if textured { + let min = pick(&mut rng, &[GL_NEAREST, GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST]); + let mag = pick(&mut rng, &[GL_NEAREST, GL_LINEAR, GL_LINEAR]); + let format = pick(&mut rng, &[GL_RGBA, GL_RGB, GL_RGBA, GL_LUMINANCE]); + let mut tex = BenchTexture::new(32, min, mag, format); + + if rng.next_u32() % 4 == 0 { + tex.wrap_t = GL_CLAMP_TO_EDGE; + } + st.texture_2d = true; + st.texture_env_mode = pick(&mut rng, &[GL_REPLACE, GL_MODULATE, GL_MODULATE, GL_DECAL, GL_BLEND]); + st.texture_env_color = [0.9, 0.2, 0.4, 0.6]; + if seed % 2 == 0 { + // Half the cases in a state a specialisation names. + let kernel = pick(&mut rng, &[(GL_NEAREST, GL_NEAREST), (GL_LINEAR, GL_LINEAR), (GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR)]); + tex = BenchTexture::new(32, kernel.0, kernel.1, pick(&mut rng, &[GL_RGBA, GL_RGB])); + st.texture_env_mode = pick(&mut rng, &[GL_REPLACE, GL_MODULATE]); + st.depth_test = true; + st.depth_mask = true; + st.depth_func = pick(&mut rng, &[GL_LESS, GL_LEQUAL]); + st.blend = false; + st.fog = false; + } + b.tex = Some(tex); + } + for _ in 0..60 { + let mut v = vec![0.0f32; 3 * vtx::FLOATS]; + let size = pick(&mut rng, &[3.0f32, 12.0, 40.0, 90.0]); + let cx = rng.range(0.0, SIDE as f32); + let cy = rng.range(0.0, SIDE as f32); + let grey = rng.next_u32() % 3 == 0; + for i in 0..3 { + let x = cx + rng.range(-size, size); + let y = cy + rng.range(-size, size); + let z = rng.range(-1.2, 1.2); + let w = rng.range(0.5, 2.0); + let o = &mut v[i * vtx::FLOATS..(i + 1) * vtx::FLOATS]; + let nx = x / BENCH_W as f32 * 2.0 - 1.0; + let ny = y / BENCH_H as f32 * 2.0 - 1.0; + o[vtx::POSITION..vtx::POSITION + 4].copy_from_slice(&[nx * w, ny * w, z * w, w]); + // Some triangles keep a component 0 or 1 at every vertex. + let c = if grey { + [1.0, 0.0, rng.range(0.0, 1.0), 1.0] + } else { + [rng.range(-0.1, 1.1), rng.range(0.0, 1.0), rng.range(0.0, 1.0), rng.range(0.0, 1.0)] + }; + o[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&c); + o[vtx::TEXCOORD..vtx::TEXCOORD + 4] + .copy_from_slice(&[rng.range(-2.0, 3.0) * w, rng.range(-2.0, 3.0) * w, 0.0, w]); + } + b.prims.push(v); + } + b + } + + /// A stencil test on: any function, reference and masks, and any + /// operation for each outcome. + fn random_stencil(rng: &mut Lcg, st: &mut GlDrawState) { + let ops = [GL_KEEP, GL_KEEP, 0, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT]; + + st.stencil_test = true; + st.stencil_func = rng.next_u32() % 8; + st.stencil_ref = pick(rng, &[0, 1, 2, 3, -1, 300]); + st.stencil_value_mask = pick(rng, &[u32::MAX, 0xff, 0x3, 0x2]); + st.stencil_write_mask = pick(rng, &[u32::MAX, 0xff, 0x1, 0]); + st.stencil_op = [pick(rng, &ops), pick(rng, &ops), pick(rng, &ops)]; + } + + /// One or two client clip planes, in eye coordinates, which are the + /// cases' object coordinates: each cuts some triangles across. + fn random_clip(rng: &mut Lcg, st: &mut GlDrawState) { + st.clip_planes = pick(rng, &[1, 2, 0x21, 0x9]); + for plane in st.clip_plane.iter_mut() { + *plane = [rng.range(-1.0, 1.0), rng.range(-1.0, 1.0), rng.range(-0.5, 0.5), rng.range(-0.5, 0.5)]; + } + } + + type Planes3 = (Vec, Vec, Vec); + + fn draw(seed: u64, textured: bool, fill: FillPath, texture: TexturePath) -> Planes3 { + draw_bench(case(seed, textured), fill, texture) + } + + fn draw_bench(mut b: Bench, fill: FillPath, texture: TexturePath) -> Planes3 { + b.prepare(); + // Stencil values 0 to 3 in a pattern, for the stencil test to tell apart. + for (i, v) in b.board.planes.stencil.bytes.iter_mut().enumerate() { + *v = ((i * 7 + i / 13) % 4) as u8; + } + raster_set_fill_path(fill); + raster_set_texture_path(texture); + b.run(); + raster_set_fill_path(FillPath::Table); + raster_set_texture_path(TexturePath::Spans); + b.snapshot(SIDE) + } + + fn compare(seed: u64, what: &str, want: &Planes3, got: &Planes3) { + for i in 0..want.0.len() { + assert!( + want.0[i] == got.0[i] && want.1[i] == got.1[i] && want.2[i] == got.2[i], + "seed {seed}, {what}: pixel ({}, {}) colour {:#010x} depth {:#08x} stencil {}, general path {:#010x} {:#08x} {}", + i as i32 % SIDE, + i as i32 / SIDE, + got.0[i], + got.1[i], + got.2[i], + want.0[i], + want.1[i], + want.2[i] + ); + } + } + + // Untextured fills: the specialised loops and the dynamic one draw what + // the general per-fragment path draws, quad by quad, pixel for pixel, + // colour and depth. + #[test] + fn fill_rows_match_the_general_path() { + let mut drawn = 0; + for seed in 1..=40u64 { + let want = draw(seed, false, FillPath::Off, TexturePath::Spans); + let dynamic = draw(seed, false, FillPath::Dynamic, TexturePath::Spans); + let table = draw(seed, false, FillPath::Table, TexturePath::Spans); + + compare(seed, "dynamic loop", &want, &dynamic); + compare(seed, "specialised loop", &want, &table); + drawn += want.0.iter().filter(|&&c| c != 0).count(); + } + assert!(drawn > 10000, "the cases drew too little to compare: {drawn}"); + } + + // Antialiased polygons visit the pixels of each row they can reach, not + // their whole bounding box, and draw the same: random states, and + // triangles with edges on pixel boundaries and corners at pixel corners. + #[test] + fn antialiased_polygons_match_the_whole_box() { + let mut drawn = 0; + for seed in 300..=340u64 { + for textured in [false, true] { + let scene = || { + let mut b = case(seed, textured); + let aligned = [ + [(10.0f32, 10.0f32), (50.0, 10.0), (10.0, 50.0)], + [(20.0, 20.0), (21.0, 20.0), (20.0, 21.0)], + [(30.0, 5.0), (30.5, 90.0), (31.0, 5.0)], + [(5.0, 60.0), (100.0, 60.25), (50.0, 61.0)], + [(60.0, 60.0), (90.0, 60.0), (60.0, 60.0)], + ]; + + b.polygon_smooth = true; + for pts in aligned { + let mut v = b.prims[0].clone(); + + for (i, &(x, y)) in pts.iter().enumerate() { + let o = &mut v[i * vtx::FLOATS + vtx::POSITION..]; + + o[..4].copy_from_slice(&[x / BENCH_W as f32 * 2.0 - 1.0, y / BENCH_H as f32 * 2.0 - 1.0, 0.0, 1.0]); + } + b.prims.push(v); + } + b + }; + + raster_set_aa_whole_box(true); + let want = draw_bench(scene(), FillPath::Table, TexturePath::Spans); + raster_set_aa_whole_box(false); + let got = draw_bench(scene(), FillPath::Table, TexturePath::Spans); + + compare(seed, "antialiased rows", &want, &got); + drawn += want.0.iter().filter(|&&c| c != 0).count(); + } + } + assert!(drawn > 10000, "the cases drew too little to compare: {drawn}"); + } + + // Textured fills, many in perspective, so that each quad takes its own + // level of detail: the loops draw what the general path draws with the + // same fixed-point sampler and the same quads. + #[test] + fn texture_rows_match_the_general_path() { + for seed in 100..=140u64 { + let want = draw(seed, true, FillPath::Table, TexturePath::Fixed); + let dynamic = draw(seed, true, FillPath::Dynamic, TexturePath::Spans); + let table = draw(seed, true, FillPath::Table, TexturePath::Spans); + + compare(seed, "dynamic textured loop", &want, &dynamic); + compare(seed, "specialised textured loop", &want, &table); + } + } + + // Each textured specialisation, in the state it names, smooth shaded, + // draws what the general loop draws; with s clamped to the edge, so + // does the fallback. + #[test] + fn every_texture_row_matches_the_general_path() { + let mut seed = 200u64; + for (wrap, func) in [GL_REPEAT, GL_CLAMP_TO_EDGE].into_iter().flat_map(|w| [(w, GL_LESS), (w, GL_LEQUAL)]) { + for (min, mag) in [(GL_NEAREST, GL_NEAREST), (GL_LINEAR, GL_LINEAR), (GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR)] { + for env in [GL_REPLACE, GL_MODULATE] { + for format in [GL_RGBA, GL_RGB] { + let make = || { + let mut b = case(seed, true); + b.st.depth_test = true; + b.st.depth_mask = true; + b.st.depth_func = func; + b.st.shade_model = GL_SMOOTH; + b.st.blend = false; + b.st.fog = false; + b.st.stencil_test = false; + if seed % 2 == 1 { + random_stencil(&mut Lcg(seed), &mut b.st); + } + b.st.texture_env_mode = env; + let mut tex = BenchTexture::new(32, min, mag, format); + tex.wrap_s = wrap; + b.tex = Some(tex); + b + }; + let want = draw_bench(make(), FillPath::Table, TexturePath::Fixed); + let table = draw_bench(make(), FillPath::Table, TexturePath::Spans); + + compare(seed, "specialised textured loop", &want, &table); + seed += 1; + } + } + } + } + } + + // Under the stencil test, with any function, reference, masks and + // operations, some under client clip planes too: the loops test and + // write the stencil plane as the general path does, textured or not. The row loops take these draws (a + // textured one its specialisation), and the cases change stencil values. + #[test] + fn stencil_fills_match_the_general_path() { + let mut changed = 0; + for seed in 400..=460u64 { + let textured = seed % 2 == 0; + let make = || { + let mut b = case(seed, textured); + random_stencil(&mut Lcg(seed), &mut b.st); + if seed % 3 == 0 { + random_clip(&mut Lcg(seed + 1), &mut b.st); + } + b + }; + let (fill, texture) = if textured { + (FillPath::Table, TexturePath::Fixed) + } else { + (FillPath::Off, TexturePath::Spans) + }; + // The stencil pattern draw_bench() starts from. + let before = draw_bench(Bench::empty(), fill, texture).2; + let want = draw_bench(make(), fill, texture); + let dynamic = draw_bench(make(), FillPath::Dynamic, TexturePath::Spans); + let table = draw_bench(make(), FillPath::Table, TexturePath::Spans); + + compare(seed, "dynamic loop under the stencil test", &want, &dynamic); + compare(seed, "row loop under the stencil test", &want, &table); + changed += want.2.iter().zip(&before).filter(|(a, b)| a != b).count(); + } + assert!(changed > 10000, "the cases changed too few stencil values: {changed}"); + } + + // A stencil-tested draw takes a row loop: a textured one in a + // specialised state its specialisation, an untextured one the fallback. + #[test] + fn stencil_draws_take_the_row_loops() { + let mut b = Bench::empty(); + b.st.depth_test = true; + b.st.stencil_test = true; + b.st.stencil_func = GL_EQUAL; + b.st.stencil_op = [GL_KEEP; 3]; + let t = b.board.gl_target(); + let reports = SoftReports::default(); + let mut parts = b.board.gl_parts(None).0; + let px = parts.planes(); + let mut r = SoftDraw::new(&b.rules, &t, &b.st, &reports, &px); + r.ntargets = 1; + r.setup_ops(); + assert_eq!(r.fill_select(), Some(FillRow::FillAny)); + let f = SoftFixedTex { min: GL_LINEAR, mag: GL_LINEAR, format: GL_RGB, wrap_s: GL_REPEAT, wrap_t: GL_REPEAT, ..Default::default() }; + assert_eq!(r.fixed_span_select(&f), Some(FillRow::LinearModulateRgbLess)); + } + + // So does a draw under client clip planes. + #[test] + fn clipped_draws_take_the_row_loops() { + let mut b = Bench::empty(); + b.st.depth_test = true; + b.st.clip_planes = 1; + let t = b.board.gl_target(); + let reports = SoftReports::default(); + let mut parts = b.board.gl_parts(None).0; + let px = parts.planes(); + let mut r = SoftDraw::new(&b.rules, &t, &b.st, &reports, &px); + r.ntargets = 1; + r.setup_ops(); + assert_eq!(r.fill_select(), Some(FillRow::FillAny)); + let f = SoftFixedTex { min: GL_LINEAR, mag: GL_LINEAR, format: GL_RGB, wrap_s: GL_REPEAT, wrap_t: GL_REPEAT, ..Default::default() }; + assert_eq!(r.fixed_span_select(&f), Some(FillRow::LinearModulateRgbLess)); + } + + // The two blended textured specialisations, each in the state it names, + // some stencil tested or clipped, draw what the general loop draws. + #[test] + fn blended_texture_rows_match_the_general_path() { + for seed in 500..=520u64 { + let sky = seed % 2 == 0; + let make = || { + let mut b = case(seed, true); + b.st.depth_test = true; + b.st.depth_func = if sky { GL_ALWAYS } else { GL_LESS }; + b.st.depth_mask = !sky; + b.st.blend = true; + b.st.blend_src = GL_SRC_ALPHA; + b.st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + b.st.blend_equation = GL_FUNC_ADD; + b.st.fog = false; + b.st.texture_env_mode = GL_MODULATE; + b.tex = Some(if sky { + BenchTexture::new(32, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_RGBA) + } else { + BenchTexture::new(32, GL_LINEAR, GL_LINEAR, GL_RGB) + }); + b + }; + let want = draw_bench(make(), FillPath::Table, TexturePath::Fixed); + let table = draw_bench(make(), FillPath::Table, TexturePath::Spans); + + compare(seed, "blended textured loop", &want, &table); + } + } + + // Each textured specialisation is reachable from the state it names. + #[test] + fn selection_reaches_each_texture_entry() { + let mut seen = Vec::new(); + let mut b = Bench::empty(); + b.st.depth_test = true; + for blend in [false, true] { + for wrap in [GL_REPEAT, GL_CLAMP_TO_EDGE] { + for (func, mask) in [(GL_LESS, true), (GL_LEQUAL, true), (GL_ALWAYS, false)] { + for (min, mag) in [(GL_NEAREST, GL_NEAREST), (GL_LINEAR, GL_LINEAR), (GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR)] { + for env in [GL_REPLACE, GL_MODULATE] { + for format in [GL_RGBA, GL_RGB] { + b.st.depth_func = func; + b.st.depth_mask = mask; + b.st.texture_env_mode = env; + b.st.blend = blend; + b.st.blend_src = GL_SRC_ALPHA; + b.st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + let t = b.board.gl_target(); + let reports = SoftReports::default(); + let mut parts = b.board.gl_parts(None).0; + let px = parts.planes(); + let mut r = SoftDraw::new(&b.rules, &t, &b.st, &reports, &px); + r.ntargets = 1; + r.setup_ops(); + let lod = min != mag; + let f = SoftFixedTex { min, mag, lod, format, wrap_s: wrap, wrap_t: GL_REPEAT, ..Default::default() }; + let row = r.fixed_span_select(&f).expect("a row loop"); + if !seen.contains(&row) { + seen.push(row); + } + } + } + } + } + } + } + // 24 unblended, the clamped one, 2 blended and the fallback. + assert_eq!(seen.len(), 28, "{seen:?}"); + } + + // Perfly's trilinear RGB truck wraps S clamped to the edge and T + // repeated: it takes the loop of its own, any other wrap pairing the + // fixed sampler takes still takes the fallback, and an all-repeat draw + // keeps its repeated loop. + #[test] + fn clamped_texture_selects_its_loop() { + let mut b = Bench::empty(); + b.st.depth_test = true; + b.st.depth_func = GL_LEQUAL; + b.st.depth_mask = true; + b.st.texture_env_mode = GL_MODULATE; + let t = b.board.gl_target(); + let reports = SoftReports::default(); + let mut parts = b.board.gl_parts(None).0; + let px = parts.planes(); + let mut r = SoftDraw::new(&b.rules, &t, &b.st, &reports, &px); + r.ntargets = 1; + r.setup_ops(); + let f = |min, mag, format, wrap_s, wrap_t| SoftFixedTex { min, mag, lod: min != mag, format, wrap_s, wrap_t, ..Default::default() }; + let tri = |format, wrap_s, wrap_t| f(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, format, wrap_s, wrap_t); + let (rep, edge) = (GL_REPEAT, GL_CLAMP_TO_EDGE); + + assert_eq!(r.fixed_span_select(&tri(GL_RGB, edge, rep)), Some(FillRow::TrilinearModulateRgbLequalEdge)); + assert_eq!(r.fixed_span_select(&tri(GL_RGB, rep, edge)), Some(FillRow::TrilinearModulateRgbLequalEdge)); + assert_eq!(r.fixed_span_select(&tri(GL_RGB, edge, edge)), Some(FillRow::TrilinearModulateRgbLequalEdge)); + assert_eq!(r.fixed_span_select(&tri(GL_RGB, rep, rep)), Some(FillRow::TrilinearModulateRgbLequal)); + // Other tuples have no clamped loop yet. + assert_eq!(r.fixed_span_select(&tri(GL_RGBA, edge, rep)), Some(FillRow::TextureAny)); + assert_eq!(r.fixed_span_select(&f(GL_LINEAR, GL_LINEAR, GL_RGB, edge, rep)), Some(FillRow::TextureAny)); + } + + // That loop draws what the fallback and the general path draw, with + // texture coordinates that run off both edges. + #[test] + fn clamped_texture_rows_match_the_general_path() { + let mut changed = 0; + for seed in 600..=620u64 { + let make = |wrap_s| { + let mut b = case(seed, true); + b.st.depth_test = true; + b.st.depth_func = GL_LEQUAL; + b.st.depth_mask = true; + b.st.blend = false; + b.st.fog = false; + b.st.texture_env_mode = GL_MODULATE; + let mut tex = BenchTexture::new(32, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_RGB); + + tex.wrap_s = wrap_s; + b.tex = Some(tex); + b + }; + let want = draw_bench(make(GL_CLAMP_TO_EDGE), FillPath::Table, TexturePath::Fixed); + let fallback = draw_bench(make(GL_CLAMP_TO_EDGE), FillPath::Dynamic, TexturePath::Spans); + let table = draw_bench(make(GL_CLAMP_TO_EDGE), FillPath::Table, TexturePath::Spans); + let repeated = draw_bench(make(GL_REPEAT), FillPath::Table, TexturePath::Spans); + + compare(seed, "fallback loop", &want, &fallback); + compare(seed, "clamped loop", &want, &table); + changed += want.0.iter().zip(&repeated.0).filter(|(a, b)| a != b).count(); + } + // The clamp shows: the same draws repeated differ. + assert!(changed > 1000, "the cases did not reach the clamp: {changed}"); + } + + // The board's own pixel tests: an id test that passes some pixels of a + // row and not others, which the row of planes GlPlanes::row() lends + // applies per pixel, and two clip rectangles that split rows, which it + // cannot lend and the loops then draw a pixel at a time. Either way the + // loops draw what the general path draws. + #[test] + fn fill_rows_match_under_the_board_pixel_tests() { + for seed in 300..=330u64 { + let textured = seed % 2 == 0; + let make = || { + let mut b = case(seed, textured); + let mut rng = Lcg(seed); + + if seed % 3 != 0 { + b.board.win.id_compare = ODSY_ID_COMPARE_ENABLE | (0x0f << ODSY_ID_COMPARE_MASK_SHIFT) | 0x05; + for d in b.board.planes.did.iter_mut() { + *d = if rng.next_u32() % 4 == 0 { 0x15 } else { 0x06 }; + } + } + if seed % 3 != 1 { + b.board.win.clip_kept = 2; + b.board.win.clip_rect[0] = ClipRect { x0: 0, y0: 0, x1: 70, y1: SIDE - 1 }; + b.board.win.clip_rect[1] = ClipRect { x0: 50, y0: 40, x1: SIDE - 1, y1: 110 }; + } + b + }; + let (fill, texture) = if textured { + (FillPath::Table, TexturePath::Fixed) + } else { + (FillPath::Off, TexturePath::Spans) + }; + let want = draw_bench(make(), fill, texture); + let table = draw_bench(make(), FillPath::Table, TexturePath::Spans); + + compare(seed, "row loop under the board's pixel tests", &want, &table); + } + } + + /// Texels that name their level: red 20 + 25 * level. + struct LevelTexels; + + impl Texels for LevelTexels { + fn texel(&self, level: Option<&GlTexLevel>, _: u32, _: u32) -> Option { + Some((20 + 25 * level.map_or(0, |l| l.level)) << 24 | 0x4080ff) + } + fn border_texel(&self, _: Option<&GlTexLevel>, _: i32, _: i32) -> Option { + None + } + fn texel_3d(&self, _: Option<&GlTexLevel>, _: u32, _: u32, _: u32) -> Option { + None + } + fn border_texel_3d(&self, _: Option<&GlTexLevel>, _: i32, _: i32, _: i32) -> Option { + None + } + } + + /// A vertex at window pixel (x, y) of the bench board, with w and s, at + /// depth one half: the bench's depth row takes (z + w) / 2. + fn lod_vertex(x: f32, y: f32, w: f32, s: f32) -> [f32; vtx::FLOATS] { + let mut o = [0.0f32; vtx::FLOATS]; + + o[vtx::POSITION..vtx::POSITION + 4] + .copy_from_slice(&[(x / BENCH_W as f32 * 2.0 - 1.0) * w, (y / BENCH_H as f32 * 2.0 - 1.0) * w, 1.0 - w, w]); + o[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&[1.0, 1.0, 1.0, 1.0]); + o[vtx::TEXCOORD..vtx::TEXCOORD + 4].copy_from_slice(&[s, 0.0, 0.0, 1.0]); + o + } + + /// Draw one triangle replacing colour with a nearest-mipmap-nearest + /// 256-texel texture of LevelTexels, its LOD bias, minimum and maximum + /// the 0x1013 words lod (Board::texture_lod_bias(), texture_lod_limit()), + /// through texture path path; and return each drawn pixel as (x, y, + /// level). + fn draw_levels(vertices: &[[f32; vtx::FLOATS]; 3], lod: [u32; 4], path: TexturePath) -> Vec<(i32, i32, u32)> { + let mut b = Bench::empty(); + let tex = BenchTexture::new(256, GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST, GL_RGBA); + let mut image = tex.image(); + let stipple = [0u32; 32]; + let v: Vec = vertices.iter().flatten().copied().collect(); + + b.st.texture_2d = true; + b.st.texture_env_mode = GL_REPLACE; + b.board.tex.lod = lod; + b.board.texture_lod_bias(&mut image); + if let Some(p) = image.pyramid.as_mut() { + p.min_lod = texture_lod_limit(lod[2]) as f64 / tex::LOD_ONE as f64; + p.max_lod = texture_lod_limit(lod[3]) as f64 / tex::LOD_ONE as f64; + } + let mut t = b.board.gl_target(); + t.texture = Some(image); + raster_set_texture_path(path); + let mut d = GlDraw::new(GL_TRIANGLES, 3, &v, &b.st, &stipple); + b.rules.draw(&t, &mut b.board.gl_parts(None).0, &LevelTexels, &mut d); + raster_set_texture_path(TexturePath::Spans); + + let mut out = Vec::new(); + for y in 0..BENCH_H { + for x in 0..BENCH_W { + let c = b.board.buffer_load(0, x as i64, y as i64); + + if c != 0 { + assert_eq!(c & 0xffffff, 0x4080ff, "pixel ({x}, {y}) is not a texel: {c:#010x}"); + out.push((x, y, ((c >> 24) - 20) / 25)); + } + } + } + out + } + + /// The level nearest-mipmap-nearest takes for lambda: rounded, a half + /// down, at most the last of a 256-texel pyramid. + fn nearest_level(lambda: f64) -> u32 { + if lambda <= 0.5 { + return 0; + } + let l = lambda.floor() + (lambda - lambda.floor() > 0.5) as u32 as f64; + (l as u32).min(8) + } + + /// Whether lambda is within margin of a change of nearest level. + fn near_a_choice(lambda: f64, margin: f64) -> bool { + (lambda - lambda.floor() - 0.5).abs() < margin + } + + // The diagonal level of detail (soft_quad_rho()): a perspective triangle + // whose s depends on x alone, so the expected level is known in closed + // form. Every drawn pixel takes its quad's level, from the s difference + // between the quad's columns; at the triangle's edge that quad includes + // pixels outside it, and there the quad's level differs from the one + // the pixel's own footprint gives. Each texture path draws the same. + #[test] + fn quads_take_the_diagonal_level_of_detail() { + // A and C share w and s, so s / w and 1 / w vary along x alone: + // s = (u / 8) / (1 - 7u / 8), u = (x - XL) / (XR - XL). + const XL: f64 = 320.0; + // The right edge: column 360 is drawn and its quad's 361 is not. + const XR: f64 = 361.0; + let vertices = [ + lod_vertex(XL as f32, 256.0, 1.0, 0.0), + lod_vertex(XR as f32, 256.0, 8.0, 1.0), + lod_vertex(XR as f32, 320.0, 8.0, 1.0), + ]; + let s = |x: f64| { + let u = (x - XL) / (XR - XL); + (u / 8.0) / (1.0 - 7.0 * u / 8.0) + }; + let ds = |x: f64| { + let u = (x - XL) / (XR - XL); + (1.0 / 8.0) / ((1.0 - 7.0 * u / 8.0) * (1.0 - 7.0 * u / 8.0)) / (XR - XL) + }; + for path in [TexturePath::Spans, TexturePath::Fixed, TexturePath::Double] { + let drawn = draw_levels(&vertices, [0, 0, (-15i32 * 256) as u32 & 0x3fff, 15 * 256], path); + let at: std::collections::HashSet<(i32, i32)> = drawn.iter().map(|&(x, y, _)| (x, y)).collect(); + let mut checked = 0; + let mut edge_differs = 0; + + assert!(drawn.len() > 500, "path {path:?}: the triangle drew {} pixels", drawn.len()); + for &(x, y, level) in &drawn { + let qx = (x & !1) as f64; + let quad = (256.0 * (s(qx + 1.5) - s(qx + 0.5)).abs()).log2(); + let own = (256.0 * ds(x as f64 + 0.5)).log2(); + + if near_a_choice(quad, 0.02) { + continue; + } + checked += 1; + assert_eq!(level, nearest_level(quad), "path {path:?}: pixel ({x}, {y}) took level {level}, its quad's lambda is {quad}, its own {own}"); + if !at.contains(&(x ^ 1, y)) && !near_a_choice(own, 0.02) && nearest_level(own) != level { + edge_differs += 1; + } + } + assert!(checked > 400, "path {path:?}: only {checked} pixels away from a level change"); + assert!(edge_differs >= 32, "path {path:?}: {edge_differs} edge pixels whose own footprint picks another level"); + } + } + + // The 0x1013 words as Board::texture_lod_bias() and texture_lod_limit() + // decode them: 14-bit two's complement, eight fraction bits; the bias + // scales its own axis's differences, and minimum and maximum clamp the + // quad's lambda, each limit first clamped to +/-15. An affine triangle + // of two texels a pixel along s, t constant: lambda 1 unbiased. + #[test] + fn quad_lod_takes_the_bias_and_limits() { + let word = |x: f64| (x * 256.0) as i32 as u32 & tex::LOD_MASK; + let vertices = [ + lod_vertex(320.0, 256.0, 1.0, 0.0), + lod_vertex(480.0, 256.0, 1.0, 1.25), + lod_vertex(320.0, 384.0, 1.0, 0.0), + ]; + let open = [word(-15.0), word(15.0)]; + + assert_eq!(texture_lod_word(word(-32.0)), -32 * 256); + assert_eq!(texture_lod_limit(word(20.0)), 15 * 256); + assert_eq!(texture_lod_limit(word(-20.0)), -15 * 256); + for (lod, want) in [ + ([0, 0, open[0], open[1]], 1), + ([word(1.0), 0, open[0], open[1]], 2), + ([word(-1.0), 0, open[0], open[1]], 0), + ([word(0.75), 0, open[0], open[1]], 2), + ([word(-0.25), 0, open[0], open[1]], 1), + // The T bias scales t's differences, which are zero here. + ([0, word(3.0), open[0], open[1]], 1), + ([0, 0, word(3.0), open[1]], 3), + ([word(2.0), 0, open[0], word(1.25)], 1), + // A minimum past 15 is taken as 15: past the last level. + ([0, 0, word(20.0), word(20.0)], 8), + ] { + for path in [TexturePath::Spans, TexturePath::Fixed, TexturePath::Double] { + let drawn = draw_levels(&vertices, lod, path); + + assert!(drawn.len() > 5000, "words {lod:x?}, path {path:?}: the triangle drew {} pixels", drawn.len()); + for &(x, y, level) in &drawn { + assert_eq!(level, want, "words {lod:x?}, path {path:?}: pixel ({x}, {y})"); + } + } + } + } + + // Every untextured specialisation is reachable from the state it names. + #[test] + fn selection_reaches_each_table_entry() { + let mut seen = Vec::new(); + for (depth, table) in SOFT_FILL_TABLE.iter().enumerate() { + for smooth in [GL_FLAT, GL_SMOOTH] { + for over in [false, true] { + let mut b = Bench::empty(); + b.st.depth_test = depth != 0; + b.st.depth_func = if depth == 2 { GL_LEQUAL } else { GL_LESS }; + b.st.shade_model = smooth; + if over { + b.st.blend = true; + b.st.blend_src = GL_SRC_ALPHA; + b.st.blend_dst = GL_ONE_MINUS_SRC_ALPHA; + } + let t = b.board.gl_target(); + let reports = SoftReports::default(); + let mut parts = b.board.gl_parts(None).0; + let px = parts.planes(); + let mut r = SoftDraw::new(&b.rules, &t, &b.st, &reports, &px); + r.ntargets = 1; + r.setup_ops(); + let f = r.fill_select().expect("a row loop"); + assert_eq!(f, table[(smooth == GL_SMOOTH) as usize][over as usize]); + if !seen.contains(&f) { + seen.push(f); + } + } + } + } + assert_eq!(seen.len(), 12); + } +} diff --git a/src/dev/odyssey/raster_fixed_texture.rs b/src/dev/odyssey/raster_fixed_texture.rs new file mode 100644 index 000000000..bc2d75ea7 --- /dev/null +++ b/src/dev/odyssey/raster_fixed_texture.rs @@ -0,0 +1,974 @@ +//! The fixed-point fast path for textured triangles: the sampler's state, its +//! texel reads and filters, and the lambda table. The quad loops that draw a +//! textured triangle's pixels through it are SoftDraw::fill_quads() in +//! raster_fill.rs. Its colours stay within 3/255 of the double-precision +//! path's; it is faster, not more accurate. +//! +//! Ported from hw/display/sgi/rasterizer.c in jamesbraid/qemu-sgi-origami. + + +use super::gl_context::{ + GL_ALPHA, GL_CLAMP, GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE, GL_DEPTH_COMPONENT, GL_INTENSITY, + GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LUMINANCE, GL_LUMINANCE_ALPHA, + GL_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_REPEAT, GL_RGB, +}; +use super::raster::{ + cmax, cmin, FIXED_FAILED, FIXED_LEVELS, GlTexLevel, SoftDraw, SoftFixedCell, SoftFixedStore, + SoftFrag, SoftVertex, TexturePath, +}; +use super::raster_fill::FillRow; +use super::raster_texture::{ + soft_texture_components, soft_texture_lod_scale, soft_texture_mipmap, SoftQuadCoords, + SoftTexPlane, SoftTexelCache, +}; + +// Fixed-point texturing for filled triangles. +// +// Approximated: no source gives the board's sub-texel, filter-weight or +// level-of-detail widths, and the double arithmetic of SoftDraw::texture() +// is no closer to a board's than another choice. Where the state allows, +// a filled triangle samples instead with: +// - each fragment's s and t from the triangle's S/W, T/W and Q/W planes, +// divided in double, so a far, many-times repeated floor keeps its +// texel positions (a 32-bit fixed-point numerator does not); +// - texel positions in 1/256 of a texel: bilinear weights of 8 bits; +// - lambda, the level of detail, in 1/256, from the squared footprint +// through a 10-bit table of log2, with no square root; within a few +// steps of a choice between filters or nearest levels it is computed +// exactly instead, rounded up, so the choice is the double path's; +// - a blend between two levels by an 8-bit fraction; +// - texels as 8-bit components, filtered in integers with no rounding +// until the texture environment takes the result in double. +// Coverage, depth, stencil and the pixels written are those of the +// double path. Colour differs from it within a bound measured on the +// comparison corpus. +// +// The state it takes: a 2D image or pyramid of base format alpha, +// luminance, luminance-alpha, intensity, RGB or RGBA, read through the +// packed reader or through component readers 1, 2, 4 or 8 bits wide, +// which convert to 8 bits exactly; levels of at most 8192 texels a side +// and 2^22 texels; NEAREST, LINEAR or a mipmap minification filter with +// NEAREST or LINEAR magnification; any wrap, a border image only under +// REPEAT or CLAMP_TO_EDGE, which never reach it; no alpha test, whose +// outcome would move with the texel's alpha; and no logical operation, +// which turns a difference in the lowest bit into one in any. Anything +// else, and a fragment the fixed sampler cannot take (a coordinate beyond +// 2^30, Q/W not positive, a failed read), goes to SoftDraw::texture(), so it +// is dropped and reported. +pub(super) const FIXED_MAX_SIDE: u32 = 8192; +pub(super) const FIXED_MAX_TEXELS: u32 = 1 << 22; +/// The fewest pixels a draw covers for its texels to be read ahead +/// (SoftDraw::fixed_prefill()). +pub(super) const FIXED_PREFILL_MIN_PIXELS: u64 = 2048; +/// Lambda's bounds, beyond any level count, in 1/256. +pub(super) const FIXED_LAMBDA_MAX: i32 = 1 << 24; + +thread_local! { + pub(super) static SOFT_TEXTURE_PATH: std::cell::Cell = + const { std::cell::Cell::new(TexturePath::Spans) }; +} + +/// How filled triangles drawn on this thread sample a texture, for tests. +/// Per thread, so that tests running at once cannot change each other's +/// path. +pub(super) fn raster_set_texture_path(path: TexturePath) { + SOFT_TEXTURE_PATH.with(|p| p.set(path)); +} + +#[derive(Clone, Copy, Default)] +pub(super) struct SoftFixedLevel { + /// None: the image's level-zero fields + pub level: Option, + pub width: u32, + pub height: u32, + pub width_shift: u32, + pub height_shift: u32, +} + +#[derive(Clone, Default)] +pub(super) struct SoftFixedTex { + pub level: [SoftFixedLevel; FIXED_LEVELS], + /// The row loop for textured triangles, or None for the general one. + pub span: Option, + /// Every texel read before drawing, by level (SoftDraw::fixed_prefill()). + pub prefilled: bool, + /// Lent by the drawing thread's texel store for the draw + /// (SoftDraw::fixed_return()). + pub plain: [Vec; FIXED_LEVELS], + /// the last level, relative to base + pub range: u32, + pub format: u32, + pub min: u32, + pub mag: u32, + pub wrap_s: u32, + pub wrap_t: u32, + /// the border colour, in lanes + pub border: u64, + /// CLAMP's upper coordinate on each axis (GlTexImage.max_clamp). + pub upper_s: f64, + pub upper_t: f64, + /// Whether the footprint chooses the filter or level at all. + pub lod: bool, + /// Whether it chooses between filters or nearest levels. + pub choice: bool, + /// in 1/256 + pub min_lod: i32, + pub max_lod: i32, + /// lambda at or below it magnifies + pub mag_below: i32, + /// the footprint's per-axis scales + pub ns: f64, + pub nt: f64, +} + +/// A packed RRGGBBAA texel's components each in the low byte of a 16-bit +/// lane, red highest: a component times a weight of at most 256 fits its +/// lane, so one product weights all four. +#[inline(always)] +pub(super) fn soft_fixed_lanes(rgba: u32) -> u64 { + let t: u64 = ((rgba & 0xffff0000) as u64) << 16 | (rgba & 0xffff) as u64; + + (t & 0x0000ff000000ff00) << 8 | (t & 0x000000ff000000ff) +} + +/// A packed RRGGBBAA texel with the components the base format lacks +/// filled in, as soft_texture_components() fills them. +pub(super) fn soft_fixed_expand(format: u32, rgba: u32) -> u32 { + let r = rgba >> 24; + + match format { + GL_ALPHA => 0xffffff00 | (rgba & 0xff), + GL_LUMINANCE => r.wrapping_mul(0x01010100) | 0xff, + GL_LUMINANCE_ALPHA => r.wrapping_mul(0x01010100) | (rgba & 0xff), + GL_INTENSITY => r.wrapping_mul(0x01010101), + GL_RGB => rgba | 0xff, + _ => rgba, + } +} + +pub(super) fn soft_fixed_unit(c: f64) -> u32 { + (cmin(cmax(c, 0.0), 1.0) * 255.0 + 0.5) as u32 +} + +impl SoftDraw<'_> { + /// Whether draw r's filled triangles sample its texture in fixed point, + /// and if so f made ready for them; the tests SoftDraw::texture_setup() + /// has already made are not repeated. + pub fn fixed_setup(&self, f: &mut SoftFixedTex) -> bool { + let base = self.texture_base; + let mut border = [0.0f64; 4]; + + if SOFT_TEXTURE_PATH.with(|p| p.get()) == TexturePath::Double + || self.texture.is_none() + || self.texels_cache.is_none() + || self.image().volume + || self.image().one_dimensional + || self.st.alpha_test + || self.logic_op + { + return false; + } + let im = self.image(); + let p = im.pyramid; + *f = SoftFixedTex::default(); + f.format = match base { + Some(b) => b.base_format, + None => im.base_format, + }; + f.min = im.min_filter; + f.mag = im.mag_filter; + f.wrap_s = im.wrap_s; + f.wrap_t = im.wrap_t; + f.upper_s = if im.max_clamp { im.max_clamp_s } else { 1.0 }; + f.upper_t = if im.max_clamp { im.max_clamp_t } else { 1.0 }; + let mipmap: bool = soft_texture_mipmap(f.min); + if f.format == GL_DEPTH_COMPONENT + || (f.mag != GL_NEAREST && f.mag != GL_LINEAR) + || (f.min != GL_NEAREST && f.min != GL_LINEAR && !mipmap) + { + return false; + } + let edge_only: bool = (f.wrap_s == GL_REPEAT || f.wrap_s == GL_CLAMP_TO_EDGE) + && (f.wrap_t == GL_REPEAT || f.wrap_t == GL_CLAMP_TO_EDGE); + // A base level comes from the image's pyramid, so both are there or neither. + let pyramid = base.and(p); + f.range = pyramid.map_or(0, |p| self.texture_last.wrapping_sub(p.base_level)); + if f.range as usize >= FIXED_LEVELS { + return false; + } + for i in 0..=f.range { + let l: Option<&GlTexLevel> = pyramid.and_then(|p| p.soft_texture_level(p.base_level.wrapping_add(i))); + let fl = &mut f.level[i as usize]; + let format = match l { + Some(l) => l.base_format, + None => im.base_format, + }; + fl.level = l.copied(); + fl.width = l.map_or(im.width, |l| l.width); + fl.height = l.map_or(im.height, |l| l.height); + fl.width_shift = fl.width.trailing_zeros(); + fl.height_shift = fl.height.trailing_zeros(); + if l.is_none() && i != 0 { + return false; + } + let border_texels = match l { + Some(l) => l.border, + None => im.border, + }; + let readable = l.map_or(im.read_texel, |l| l.read_texel); + if fl.width > FIXED_MAX_SIDE + || fl.height > FIXED_MAX_SIDE + || (fl.width as u64) * (fl.height as u64) > FIXED_MAX_TEXELS as u64 + || !(fl.width as u64).is_power_of_two() + || !(fl.height as u64).is_power_of_two() + || format != f.format + || (border_texels != 0 && !edge_only) + || !readable + { + return false; + } + } + for k in 0..4 { + border[k] = cmin(cmax(im.border_color[k], 0.0), 1.0) as f64; + } + soft_texture_components(f.format, &mut border); + f.border = soft_fixed_lanes( + soft_fixed_unit(border[0]) << 24 + | soft_fixed_unit(border[1]) << 16 + | soft_fixed_unit(border[2]) << 8 + | soft_fixed_unit(border[3]), + ); + // An image without a pyramid has one filter and no footprint. + f.lod = base.is_some() && (mipmap || f.min != f.mag); + f.choice = f.lod && !(f.min == GL_LINEAR_MIPMAP_LINEAR && f.mag == GL_LINEAR); + if let (Some(base), Some(p)) = (base, pyramid) { + let lmax = FIXED_LAMBDA_MAX as f64; + + // Rounded up, as an exact lambda is (SoftFixedTex::fixed_decide()). + f.min_lod = cmin(cmax(p.min_lod * 256.0, -lmax), lmax).ceil() as i32; + f.max_lod = cmin(cmax(p.max_lod * 256.0, -lmax), lmax).ceil() as i32; + f.ns = base.width as f64 * soft_texture_lod_scale(im.lod_scale_s); + f.nt = base.height as f64 * soft_texture_lod_scale(im.lod_scale_t); + } + // soft_texture_select()'s c, for these filters. + f.mag_below = if f.mag == GL_LINEAR + && (f.min == GL_NEAREST_MIPMAP_NEAREST || f.min == GL_NEAREST_MIPMAP_LINEAR) + { + 128 + } else { + 0 + }; + true + } +} + +/// Allocate zeroed cells for size texels, or None where that fails. +pub(super) fn soft_fixed_alloc(size: usize) -> Option> { + let mut v: Vec = Vec::new(); + + if v.try_reserve_exact(size).is_err() { + return None; + } + v.resize(size, T::default()); + Some(v) +} + +impl<'a> SoftDraw<'a> { + /// The draw's fixed-point sampler, which a caller has checked is there. + pub fn fixed_tex(&self) -> &'a SoftFixedTex { + self.fixed.expect("a fixed-point draw") + } + + /// This thread's texel cache, which a textured draw has. + pub fn texel_cache(&self) -> std::cell::RefMut<'a, SoftTexelCache> { + self.texels_cache.expect("a texel cache").borrow_mut() + } + + /// Make this thread's texel store ready for f's levels: false, and the + /// draw samples in double, where it cannot be allocated. + pub fn fixed_begin(&self, f: &SoftFixedTex) -> bool { + let mut cache = self.texel_cache(); + + for i in 0..=f.range as usize { + let store = &mut cache.fixed[i]; + let size = (f.level[i].width as usize) * (f.level[i].height as usize); + + if store.size < size { + store.cells = Vec::new(); + store.size = 0; + match soft_fixed_alloc(size) { + Some(cells) => store.cells = cells, + None => return false, + } + store.size = size; + } + } + true + } + + /// Interior texel (x, y) of relative level i of the draw's image as a + /// packed texel, components expanded; None where the read fails. + pub fn fixed_read(&self, f: &SoftFixedTex, i: u32, x: u32, y: u32) -> Option { + let rgba = self.texels.texel(f.level[i as usize].level.as_ref(), x, y)?; + + Some(soft_fixed_expand(f.format, rgba)) + } + + /// Read interior texel (x, y) of relative level i into its cell at + /// index of store, tagged with tag, and return it in lanes; or tag it + /// failed and return None where the read fails. + #[inline(never)] + pub fn fixed_fill(&self, store: &mut SoftFixedStore, index: usize, i: u32, x: u32, y: u32, tag: u32) -> Option { + match self.fixed_read(self.fixed_tex(), i, x, y) { + Some(texel) => { + let lanes = soft_fixed_lanes(texel); + + store.cells[index] = SoftFixedCell { lanes, tag }; + Some(lanes) + } + None => { + store.cells[index].tag = tag | FIXED_FAILED; + None + } + } + } +} + +impl SoftFixedTex { + /// The relative levels a draw's fragments can sample, first to last: the + /// level clamp keeps lambda within [min_lod, max_lod], and a lambda above + /// a half never magnifies, so no level outside reaches the sampler. + pub fn fixed_reach(&self) -> (u32, u32) { + if !self.lod { + return (0, self.range); + } + let first = if self.min_lod > 128 { cmin((self.min_lod >> 8) as u32, self.range) } else { 0 }; + let last = if self.max_lod <= 0 { 0 } else { cmin((self.max_lod.wrapping_add(255)) >> 8, self.range as i32) as u32 }; + (cmin(first, last), last) + } +} + +impl SoftDraw<'_> { + /// Read every texel of the levels draw r can sample into this thread's + /// plain arrays, which f then lends the draw: for a large draw that + /// covers about as many pixels as those levels have texels, which then + /// taps them without a tag. False, and the draw reads texels as it reaches them, + /// where any read fails or the arrays cannot be allocated. + pub fn fixed_prefill(&self, f: &mut SoftFixedTex) -> bool { + let (first, last) = f.fixed_reach(); + for i in first..=last { + let fl = f.level[i as usize]; + let size = (fl.width as usize) * (fl.height as usize); + let mut plain = std::mem::take(&mut self.texel_cache().fixed[i as usize].plain); + + if plain.len() < size { + match soft_fixed_alloc(size) { + Some(v) => plain = v, + None => return false, + } + } + for y in 0..fl.height { + for x in 0..fl.width { + let Some(texel) = self.fixed_read(f, i, x, y) else { + self.texel_cache().fixed[i as usize].plain = plain; + return false; + }; + plain[((y as usize) << fl.width_shift) + x as usize] = soft_fixed_lanes(texel); + } + } + f.plain[i as usize] = plain; + } + f.prefilled = true; + true + } + +} + +/// Give the arrays SoftDraw::fixed_prefill() filled back to this thread's +/// texel store, for the next draw to reuse. +pub(super) fn fixed_return(cache: &std::cell::RefCell, f: &mut SoftFixedTex) { + let mut cache = cache.borrow_mut(); + + for (store, plain) in cache.fixed.iter_mut().zip(f.plain.iter_mut()) { + if !plain.is_empty() { + store.plain = std::mem::take(plain); + } + } +} + +impl<'a> SoftDraw<'a> { + /// Interior texel (x, y) of relative level i in lanes, read once a draw: + /// from the prefilled arrays, or with cache, this thread's texel store, + /// through it. + #[inline(always)] + pub fn fixed_texel(&self, cache: &mut Option>, i: u32, x: u32, y: u32, lanes: &mut u64) -> bool { + let f = self.fixed_tex(); + let index = ((y as usize) << f.level[i as usize].width_shift) + x as usize; + let Some(cache) = cache else { + *lanes = f.plain[i as usize][index]; + return true; + }; + let tag = cache.tag; + let store = &mut cache.fixed[i as usize]; + let cell = store.cells[index]; + + if cell.tag == tag { + *lanes = cell.lanes; + return true; + } + if cell.tag == (tag | FIXED_FAILED) { + return false; + } + match self.fixed_fill(store, index, i, x, y, tag) { + Some(v) => { + *lanes = v; + true + } + None => false, + } + } + + /// The texel at signed index (x, y) of relative level i after wrapping, + /// which leaves an index outside the interior only under CLAMP and + /// CLAMP_TO_BORDER: there the border colour. + #[inline(always)] + pub fn fixed_tap(&self, cache: &mut Option>, i: u32, x: i64, y: i64, lanes: &mut u64) -> bool { + let f = self.fixed_tex(); + let fl = &f.level[i as usize]; + + if !REPEAT && (x < 0 || y < 0 || x >= fl.width as i64 || y >= fl.height as i64) { + *lanes = f.border; + return true; + } + self.fixed_texel(cache, i, x as u32, y as u32, lanes) + } +} + +/// floor(x) for |x| below 2^62. +#[inline(always)] +pub(super) fn soft_fixed_floor(x: f64) -> i64 { + let i = x as i64; + + i - ((x < i as f64) as i64) +} + +/// One axis of a fixed-point position u, in 1/256 of a texel of a level n +/// texels wide, wrapped as soft_texture_axis() wraps the coordinate, +/// with CLAMP's upper bound upper rather than 1: floor() of the clamped +/// coordinate is the clamped floor(), so the bound is exactly the double +/// path's. +/// for LINEAR, the first tap's index in *x0, the second's in *x1 and the +/// second's weight; for NEAREST, the tap's index in *x0. +#[inline(always)] +pub(super) fn soft_fixed_axis(mut u: i64, n: u32, wrap: u32, upper: f64, linear: bool, x0: &mut i64, x1: &mut i64) -> u32 { + let whole: i64 = (n as i64) << 8; + let mask: i64 = n.wrapping_sub(1) as i64; + let mut a: u32 = 0; + + if wrap == GL_CLAMP && upper < 1.0 { + u = cmin(u, soft_fixed_floor(upper * whole as f64)); + } + + if linear { + match wrap { + GL_REPEAT => { + u -= 128; + } + GL_CLAMP_TO_EDGE => { + u = cmin(cmax(u - 128, 0), whole - 256); + } + GL_CLAMP_TO_BORDER => { + u = cmin(cmax(u, -128), whole + 128) - 128; + } + _ => { + u = cmin(cmax(u, 0), whole) - 128; + } + } + a = (u & 255) as u32; + *x0 = u >> 8; + *x1 = *x0 + 1; + if wrap == GL_REPEAT { + *x0 &= mask; + *x1 &= mask; + } else if wrap == GL_CLAMP_TO_EDGE { + *x1 = cmin(*x1, mask); + } + return a; + } + *x0 = u >> 8; + match wrap { + GL_REPEAT => { + *x0 &= mask; + } + GL_CLAMP_TO_BORDER => { + *x0 = cmin(cmax(*x0, -1), n as i64); + } + _ => { + *x0 = cmin(cmax(*x0, 0), n as i64 - 1); + } + } + a +} + +/// Two of a texel's lanes, in the low 16 bits of each 32-bit half. +pub(super) const FIXED_PAIRS: u64 = 0x0000ffff0000ffff; + +impl SoftDraw<'_> { + /// Relative level i sampled at position (u, v), in 1/256 of its texels, + /// with filter, each component in 1/(255 * 2^16): out[0] holds red over + /// blue and out[1] green over alpha, one to each 32-bit half. Across, one + /// product weights all four of a texel's components in their 16-bit + /// lanes; down, the sums need 24 bits, so two products take two each. + #[inline(always)] + #[allow(clippy::too_many_arguments, reason = "a level's sample takes its position as scalars")] + pub fn fixed_level(&self, cache: &mut Option>, i: u32, filter: u32, u: i64, v: i64, out: &mut [u64; 2]) -> bool { + let f = self.fixed_tex(); + let fl = &f.level[i as usize]; + let ws = if REPEAT { GL_REPEAT } else { f.wrap_s }; + let wt = if REPEAT { GL_REPEAT } else { f.wrap_t }; + let (mut x0, mut x1, mut y0, mut y1) = (0i64, 0i64, 0i64, 0i64); + let (mut c00, mut c10, mut c01, mut c11) = (0u64, 0u64, 0u64, 0u64); + + if filter == GL_NEAREST { + soft_fixed_axis(u, fl.width, ws, f.upper_s, false, &mut x0, &mut x1); + soft_fixed_axis(v, fl.height, wt, f.upper_t, false, &mut y0, &mut y1); + if !SoftDraw::fixed_tap::(self, cache, i, x0, y0, &mut c00) { + return false; + } + out[0] = ((c00 >> 16) & FIXED_PAIRS) << 16; + out[1] = (c00 & FIXED_PAIRS) << 16; + return true; + } + let a = soft_fixed_axis(u, fl.width, ws, f.upper_s, true, &mut x0, &mut x1); + let b = soft_fixed_axis(v, fl.height, wt, f.upper_t, true, &mut y0, &mut y1); + if !SoftDraw::fixed_tap::(self, cache, i, x0, y0, &mut c00) + || !SoftDraw::fixed_tap::(self, cache, i, x1, y0, &mut c10) + || !SoftDraw::fixed_tap::(self, cache, i, x0, y1, &mut c01) + || !SoftDraw::fixed_tap::(self, cache, i, x1, y1, &mut c11) + { + return false; + } + let (a, b) = (a as u64, b as u64); + let top = c00.wrapping_mul(256 - a).wrapping_add(c10.wrapping_mul(a)); + let bot = c01.wrapping_mul(256 - a).wrapping_add(c11.wrapping_mul(a)); + out[0] = ((top >> 16) & FIXED_PAIRS) * (256 - b) + ((bot >> 16) & FIXED_PAIRS) * b; + out[1] = (top & FIXED_PAIRS) * (256 - b) + (bot & FIXED_PAIRS) * b; + true + } +} + +/// round(128 log2(1 + (i + 0.5) / 1024)): lambda's table, in 1/256. +pub(super) static SOFT_FIXED_LOG2: std::sync::OnceLock<[i16; 1024]> = std::sync::OnceLock::new(); + +/// The table, built on first use (not at start-up). +#[inline(always)] +pub(super) fn soft_fixed_log2_init() -> &'static [i16; 1024] { + SOFT_FIXED_LOG2.get_or_init(|| { + let mut t = [0i16; 1024]; + + for (i, v) in t.iter_mut().enumerate() { + *v = (128.0 * (1.0 + (i as f64 + 0.5) / 1024.0).log2()).round() as i16; + } + t + }) +} + +/// Lambda in 1/256, log2 of the footprint rho, from rho squared, finite +/// and not negative: half its binary exponent plus the table's value for +/// the top ten bits of its significand. Zero and subnormals give the +/// least lambda, which every level clamp raises. +#[inline(always)] +pub(super) fn soft_fixed_lambda(rho2: f64) -> i32 { + let bits = rho2.to_bits(); + let exponent = ((bits >> 52) & 0x7ff) as i32; + + if exponent == 0 { + return -FIXED_LAMBDA_MAX; + } + (exponent - 1023) * 128 + soft_fixed_log2_init()[((bits >> 42) & 1023) as usize] as i32 +} + +impl SoftFixedTex { + /// Lambda in 1/256 for a quad whose footprint under the diagonal rule + /// (soft_quad_rho()) is rho, as SoftFixedTex::fixed_coords() takes a + /// fragment's own footprint to one; None where rho squared overflows. + #[inline(always)] + pub fn quad_lambda(&self, rho: f64) -> Option { + let rho2 = rho * rho; + + (rho2 <= f64::MAX).then(|| self.fixed_decide(rho2, soft_fixed_lambda(rho2))) + } + + /// Lambda for footprint squared rho2 from its table value lambda, or, + /// within a few steps of a lambda where the filter or nearest level + /// changes, computed exactly and rounded up. A lambda of ceil(256 l) is at + /// or below 256 c exactly when l is at or below c, and rounds to the same + /// nearest level, so those choices are soft_texture_select()'s; only + /// the blend between levels takes the table's precision. + #[inline(always)] + pub fn fixed_decide(&self, rho2: f64, lambda: i32) -> i32 { + let clamped = cmin(cmax(lambda, self.min_lod), self.max_lod); + let at_edge = (clamped - self.mag_below).abs() <= 3 + || ((self.min == GL_NEAREST_MIPMAP_NEAREST || self.min == GL_LINEAR_MIPMAP_NEAREST) + && (((clamped - 128) & 255) - 128).abs() >= 125); + let lmax = FIXED_LAMBDA_MAX as f64; + + if !self.choice || !at_edge { + return lambda; + } + if rho2 > 0.0 { + cmin(cmax((128.0 * rho2.log2()).ceil(), -lmax), lmax) as i32 + } else { + -FIXED_LAMBDA_MAX + } + } + + /// A fragment's s, t and lambda from its quad's coordinates c + /// (soft_quad_coords()) at pixel i of the quad, for the quad's lambda: + /// the same as SoftFixedTex::fixed_coords() with that lambda, which + /// computes them from the planes again. + #[inline(always)] + pub fn quad_coords(c: &SoftQuadCoords, i: usize, lambda: i32) -> Option<(f64, f64, i32)> { + let (s, t) = (c.s[i], c.t[i]); + + (c.pos[i] && s.abs() < 1073741824.0 && t.abs() < 1073741824.0).then_some((s, t, lambda)) + } + + /// A fragment's s, t and lambda from the triangle's planes g at pixel + /// column px, whose row terms are row; None where the fixed sampler + /// cannot take it. lambda, where given, is the fragment's: its quad's + /// (SoftFixedTex::quad_lambda()). Without it the fragment takes its own + /// footprint's. + #[inline(always)] + pub fn fixed_coords(&self, g: &SoftTexPlane, row: &[f64; 4], px: i32, lambda: Option) -> Option<(f64, f64, i32)> { + let x = px as f64 + 0.5 - g.x; + let q = g.value[2] + x * g.dx[2] + row[2]; + + if !(q > 0.0) { + return None; + } + let iq = 1.0 / q; + let s = (g.value[0] + x * g.dx[0] + row[0]) * iq; + let t = (g.value[1] + x * g.dx[1] + row[1]) * iq; + if !(s.abs() < 1073741824.0 && t.abs() < 1073741824.0) { + return None; + } + if !self.lod || lambda.is_some() { + return Some((s, t, lambda.unwrap_or(0))); + } + // d(S/Q)/dx is (dS/dx - s dQ/dx) / Q, and so on. + let a = self.ns * (g.dx[0] - s * g.dx[2]) * iq; + let b = self.nt * (g.dx[1] - t * g.dx[2]) * iq; + let c = self.ns * (g.dy[0] - s * g.dy[2]) * iq; + let e = self.nt * (g.dy[1] - t * g.dy[2]) * iq; + let rho2 = cmax(a * a + b * b, c * c + e * e); + if !(rho2 <= f64::MAX) { + return None; + } + Some((s, t, self.fixed_decide(rho2, soft_fixed_lambda(rho2)))) + } +} + +/// The fixed-point sampler's choice of kernel, a constant in the +/// specialised row loops: NEAREST or LINEAR with no footprint, trilinear +/// (LINEAR_MIPMAP_LINEAR minification with LINEAR magnification), or any +/// filter the image has, chosen per fragment. +pub(super) type SoftFixedKernel = u32; +pub(super) const FIXED_ANY: SoftFixedKernel = 0; +pub(super) const FIXED_NEAREST: SoftFixedKernel = 1; +pub(super) const FIXED_LINEAR: SoftFixedKernel = 2; +pub(super) const FIXED_TRILINEAR: SoftFixedKernel = 3; + +impl SoftDraw<'_> { + /// The filtered texel at (s, t) for lambda, each component in [0, 1], as + /// soft_texture_select() chooses filter, levels and their blend, with + /// lambda and the blend fraction in 1/256. KERNEL, and REPEAT (both axes + /// GL_REPEAT), are constants where the caller can name them. + #[inline(always)] + pub fn fixed_sample(&self, s: f64, t: f64, mut lambda: i32, texel: &mut [f64; 4]) -> bool { + let f = self.fixed_tex(); + let unit: f64 = 1.0 / (255.0 * (1u32 << 24) as f64); + let mut first: u32 = 0; + let mut second: u32 = 0; + let mut alpha: u32 = 0; + let mut filter = if KERNEL == FIXED_NEAREST { + GL_NEAREST + } else if KERNEL == FIXED_ANY { + f.min + } else { + GL_LINEAR + }; + let mut out = [0u64; 2]; + let mut other = [0u64; 2]; + + if KERNEL == FIXED_TRILINEAR || (KERNEL == FIXED_ANY && f.lod) { + lambda = cmin(cmax(lambda, f.min_lod), f.max_lod); + } + if KERNEL == FIXED_TRILINEAR { + // At or below 0, magnification is the same LINEAR on level 0. + let rel = cmin(cmax(lambda, 0), (f.range << 8) as i32) as u32; + + first = rel >> 8; + alpha = rel & 255; + second = cmin(first + 1, f.range); + } else if KERNEL == FIXED_ANY && f.lod { + if lambda <= f.mag_below { + filter = f.mag; + } else if soft_texture_mipmap(f.min) { + filter = if f.min == GL_NEAREST_MIPMAP_NEAREST || f.min == GL_NEAREST_MIPMAP_LINEAR { + GL_NEAREST + } else { + GL_LINEAR + }; + if f.min == GL_NEAREST_MIPMAP_NEAREST || f.min == GL_LINEAR_MIPMAP_NEAREST { + // The nearest level: lambda rounded, half down. + first = if lambda <= 128 { + 0 + } else if lambda > (f.range << 8) as i32 + 128 { + f.range + } else { + (lambda.wrapping_add(127) as u32) >> 8 + }; + } else { + let rel = cmin(cmax(lambda, 0), (f.range << 8) as i32) as u32; + + first = rel >> 8; + alpha = rel & 255; + second = cmin(first + 1, f.range); + } + } + } + let fl = &f.level[first as usize]; + // This thread's texel store, borrowed once for the sample's taps. + let mut cache = if f.prefilled { None } else { Some(self.texel_cache()) }; + let mut u = soft_fixed_floor(s * (fl.width as f64 * 256.0)); + let mut v = soft_fixed_floor(t * (fl.height as f64 * 256.0)); + if !SoftDraw::fixed_level::(self, &mut cache, first, filter, u, v, &mut out) { + return false; + } + if alpha != 0 { + // The next level's position: a level half as wide scales s by an + // exact half, so its floor is this one's halved, rounded down. + u >>= fl.width_shift.wrapping_sub(f.level[second as usize].width_shift) & 63; + v >>= fl.height_shift.wrapping_sub(f.level[second as usize].height_shift) & 63; + if !SoftDraw::fixed_level::(self, &mut cache, second, filter, u, v, &mut other) { + return false; + } + let a = alpha as u64; + out[0] = out[0] * (256 - a) + other[0] * a; + out[1] = out[1] * (256 - a) + other[1] * a; + } else { + out[0] <<= 8; + out[1] <<= 8; + } + texel[0] = (out[0] >> 32) as u32 as f64 * unit; + texel[1] = (out[1] >> 32) as u32 as f64 * unit; + texel[2] = out[0] as u32 as f64 * unit; + texel[3] = out[1] as u32 as f64 * unit; + true + } +} + +/// One triangle's fixed inputs to a quad loop (SoftDraw::fill_quads()). +#[derive(Clone, Copy)] +pub(super) struct SoftTexSpan<'a> { + /// three, wound as the rows step + pub v: &'a [SoftVertex; 3], + pub g: &'a SoftTexPlane, + /// lambda where Q/W is flat, the same for every quad, or None + pub flat_lambda: Option, + pub flat: &'a SoftFrag, + pub offset: f64, + pub area: i64, + pub step1: i64, + pub step2: i64, + pub smooth: bool, + /// Smooth colour components that are 0 at every vertex, or 1 at + /// every vertex: interpolation gives exactly that value, the terms + /// summing to the weights' own sum, so they are not interpolated. + pub constant: u8, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::odyssey::raster_bench; + use crate::dev::odyssey::gl_context::{ + vtx, GL_COLOUR_MASK_ALL, GL_LEQUAL, GL_REPLACE, GL_RGBA, GL_SMOOTH, GL_TRIANGLES, GlDraw, + GlDrawState, + }; + use crate::dev::odyssey::memory::ClipRect; + use crate::dev::odyssey::raster::{GlTexImage, LineRule, RasterRules, RasterTarget, Texels}; + + use crate::dev::odyssey::raster_texture::soft_quad_coords; + + + // Large enough that the draw covers more pixels than the texture path + // prefills for. + const W: usize = 128; + + struct Hashed; + + impl Texels for Hashed { + fn texel(&self, _: Option<&GlTexLevel>, x: u32, y: u32) -> Option { + let h = x.wrapping_mul(0x9e3779b1) ^ y.wrapping_mul(0x85ebca77) ^ 0x5bd1e995; + Some((h ^ (h >> 13)) | 0xff) + } + fn border_texel(&self, _: Option<&GlTexLevel>, _: i32, _: i32) -> Option { + None + } + fn texel_3d(&self, _: Option<&GlTexLevel>, _: u32, _: u32, _: u32) -> Option { + None + } + fn border_texel_3d(&self, _: Option<&GlTexLevel>, _: i32, _: i32, _: i32) -> Option { + None + } + } + + fn draw(path: TexturePath, filter: u32) -> (Vec, Vec) { + let rules = RasterRules { + subpixel_bits: 4, + color_bits: 8, + depth_bits: 24, + line_rule: LineRule::Diamond, + ..Default::default() + }; + let image = GlTexImage { + width: 8, + height: 8, + base_format: GL_RGBA, + min_filter: filter, + mag_filter: filter, + wrap_s: GL_REPEAT, + wrap_t: GL_REPEAT, + read_texel: true, + ..Default::default() + }; + let mut b = raster_bench::Bench::empty(); + let target = RasterTarget { texture: Some(image), ..b.board.gl_target() }; + let mut id = [0.0f32; 16]; + for k in 0..4 { + id[k * 5] = 1.0; + } + let mut st = GlDrawState { + modelview: id, + projection: id, + viewport: [0, 0, W as i32, W as i32], + shade_model: GL_SMOOTH, + depth_test: true, + depth_func: GL_LEQUAL, + depth_mask: true, + depth_range: [0.0, 1.0], + draw_buffers: 1, + colour_mask: GL_COLOUR_MASK_ALL, + texture_2d: true, + texture_env_mode: GL_REPLACE, + ..Default::default() + }; + st.drawable.bound = true; + st.drawable.front = 1; + st.drawable.clip.n = 1; + st.drawable.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: W as i32 - 1, y1: W as i32 - 1 }; + // x, y, z, w, then colour; strq at the texture slot. + let mut verts = vec![0.0f32; 3 * vtx::FLOATS]; + let corners = [(-0.95f32, -0.95f32, 0.0f32, 0.0f32), (0.95, -0.9, 3.0, 0.0), (-0.9, 0.95, 0.5, 4.0)]; + for (i, (x, y, s, t)) in corners.iter().enumerate() { + let v = &mut verts[i * vtx::FLOATS..(i + 1) * vtx::FLOATS]; + v[vtx::POSITION..vtx::POSITION + 4].copy_from_slice(&[*x, *y, 0.0, 1.0]); + v[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&[1.0; 4]); + v[vtx::TEXCOORD..vtx::TEXCOORD + 4].copy_from_slice(&[*s, *t, 0.0, 1.0]); + } + let stipple = [0u32; 32]; + let mut d = GlDraw::new(GL_TRIANGLES, 3, &verts, &st, &stipple); + + b.rules = rules; + b.set_depth(W as i32, &vec![0x00ffffffu32; W * W]); + raster_set_texture_path(path); + b.draw(&target, &Hashed, &mut d); + raster_set_texture_path(TexturePath::Spans); + // Buffer 1, the drawable's front; its red, green and blue as one word. + let colour = (0..W * W).map(|i| b.board.buffer_load(1, (i % W) as i64, (i / W) as i64) >> 8).collect(); + (colour, b.depth(W as i32)) + } + + // A quad's four fragments take the coordinates its footprint was made + // from (soft_quad_coords()), and they are the ones each would compute + // from the planes: over positive, zero, negative and overflowing Q/W. + #[test] + fn a_quads_coordinates_are_its_fragments_own() { + let f = SoftFixedTex::default(); + let mut seed = raster_bench::Lcg(7); + let mut taken = 0; + let mut dropped = 0; + + for n in 0..2000 { + let mut g = SoftTexPlane { x: seed.range(-8.0, 8.0) as f64, y: seed.range(-8.0, 8.0) as f64, ..Default::default() }; + + for k in 0..4 { + // s alone, t alone and both overflow the sampler's range. + let scale = if (k == 0 && n % 7 == 0) || (k == 1 && n % 11 == 0) { 1e9 } else { 1.0 }; + + g.value[k] = seed.range(-2.0, 2.0) as f64 * scale; + g.dx[k] = seed.range(-1.0, 1.0) as f64 * scale; + g.dy[k] = seed.range(-1.0, 1.0) as f64 * scale; + } + if n % 5 == 0 { + g.value[2] = 0.0; + } + let (qx, qy) = (2 * (seed.next_u32() % 40) as i32, 2 * (seed.next_u32() % 40) as i32); + let c = soft_quad_coords(&g, qx, qy); + + for i in 0..4 { + let col = qx + (i & 1) as i32; + let mut row = [0.0f64; 4]; + + for k in 0..4 { + row[k] = ((qy + (i >> 1) as i32) as f64 + 0.5 - g.y) * g.dy[k]; + } + let own = f.fixed_coords(&g, &row, col, Some(77)); + let quad = SoftFixedTex::quad_coords(&c, i, 77); + + match (own, quad) { + (None, None) => dropped += 1, + (Some(a), Some(b)) => { + assert_eq!((a.0.to_bits(), a.1.to_bits(), a.2), (b.0.to_bits(), b.1.to_bits(), b.2), "quad {n} pixel {i}"); + taken += 1; + } + _ => panic!("quad {n} pixel {i}: planes give {own:?}, quad gives {quad:?}"), + } + } + } + assert!(taken > 1000 && dropped > 1000, "{taken} taken, {dropped} dropped"); + } + + // Coverage, depth and the pixels written are the double path's under + // every fixed-point selection; NEAREST texels are exact in both, and + // LINEAR ones differ by less than the 8-bit weights allow, but do differ + // (the weights truncate), which shows the fixed sampler ran. The + // specialised row loop and the general one take the same texels. + #[test] + fn fixed_and_double_paths_agree() { + for filter in [GL_NEAREST, GL_LINEAR] { + let (dc, dz) = draw(TexturePath::Double, filter); + let (sc, sz) = draw(TexturePath::Spans, filter); + let (fc, fz) = draw(TexturePath::Fixed, filter); + let tol = if filter == GL_NEAREST { 0 } else { 2 }; + let mut differ = 0; + + assert!(dc.iter().any(|&c| c != 0), "triangle drew nothing"); + assert_eq!(sc, fc, "span and general loops differ, filter {filter:#x}"); + assert_eq!(sz, fz); + assert_eq!(dz, fz, "depth differs, filter {filter:#x}"); + for i in 0..W * W { + assert_eq!(dc[i] == 0, fc[i] == 0, "coverage differs at {i}"); + differ += (dc[i] != fc[i]) as u32; + for sh in [0, 8, 16] { + let a = ((dc[i] >> sh) & 0xff) as i32; + let b = ((fc[i] >> sh) & 0xff) as i32; + assert!((a - b).abs() <= tol, "pixel {i} differs: {a} vs {b}, filter {filter:#x}"); + } + } + assert_eq!(differ == 0, filter == GL_NEAREST, "differing pixels: {differ}"); + } + } +} diff --git a/src/dev/odyssey/raster_line.rs b/src/dev/odyssey/raster_line.rs new file mode 100644 index 000000000..c274d69fa --- /dev/null +++ b/src/dev/odyssey/raster_line.rs @@ -0,0 +1,741 @@ +//! Lines and points: projection, the diamond-exit and centre-sampled rules +//! for aliased lines, +//! antialiased line and point coverage, wide lines and line stipple. +//! +//! Ported from hw/display/sgi/rasterizer.c in jamesbraid/qemu-sgi-origami. + + +use crate::dev::gl::vertex::NearW; +use crate::devlog::LogModule; +use super::gl_context::{report_w_plane_clip, vtx, GL_CLIP_PLANES, GL_FLAT}; +use super::raster::{cmax, cmin, LineRule, RasterRules, SoftDraw, SoftFrag, SoftVertex}; +use super::raster_texture::{soft_texture_derivatives, soft_texture_lod_scale}; +impl SoftVertex { + /// The texture values at this end of the line to b, their change along + /// it, and its length in pixels; None where one is not finite. + pub fn texture_line_gradient(&self, b: &SoftVertex, sub: u32, volume: bool, one_dimensional: bool) -> Option<([f64; 4], [f64; 4], f64)> { + let mut delta = [0.0f64; 4]; + + let length = ((b.x - self.x) as f64).hypot((b.y - self.y) as f64) / (1i32 << sub) as f64; + if !length.is_finite() { + return None; + } + let values = self.texture_values(volume, one_dimensional); + let end = b.texture_values(volume, one_dimensional); + for k in 0..4 { + delta[k] = end[k] - values[k]; + if !values[k].is_finite() || !delta[k].is_finite() { + return None; + } + } + Some((values, delta, length)) + } +} + +/// Sign of v + e1 * epsilon + e2 * epsilon squared as epsilon -> 0+. +pub(super) fn soft_limit_sign(v: i64, e1: i64, e2: i64) -> i32 { + let first = if v != 0 { v } else if e1 != 0 { e1 } else { e2 }; + + (first > 0) as i32 - (first < 0) as i32 +} + +/// Whether |v + e1*epsilon + e2*epsilon squared| is strictly below bound. +pub(super) fn soft_limit_abs(v: i64, e1: i64, e2: i64, bound: i64) -> bool { + let sign = soft_limit_sign(v, e1, e2) as i64; + + soft_limit_sign( + sign.wrapping_mul(v).wrapping_sub(bound), + sign.wrapping_mul(e1), + sign.wrapping_mul(e2), + ) < 0 +} + +/// The endpoint minus (epsilon, epsilon squared) lies in the open diamond. +pub(super) fn soft_diamond_contains(x: i64, y: i64, half: i64) -> bool { + let sx = soft_limit_sign(x, -1, 0) as i64; + let sy = soft_limit_sign(y, 0, -1) as i64; + + soft_limit_sign( + sx.wrapping_mul(x).wrapping_add(sy.wrapping_mul(y)).wrapping_sub(half), + -sx, + -sy, + ) < 0 +} + +impl SoftVertex { + /// A segment produces a fragment if it enters the open diamond and its + /// final endpoint is outside (OpenGL 1.2.1, 3.4.1, with the limiting + /// perturbation done in lexicographic coefficients on the snapped grid). + pub fn line_covers(&self, b: &SoftVertex, px: i64, py: i64, sub: i32) -> bool { + let half: i64 = 1i64 << (sub - 1); + let cx = px.wrapping_mul(1i64 << sub).wrapping_add(half); + let cy = py.wrapping_mul(1i64 << sub).wrapping_add(half); + let dx = b.x.wrapping_sub(self.x); + let dy = b.y.wrapping_sub(self.y); + let ex = cx.wrapping_sub(self.x); + let ey = cy.wrapping_sub(self.y); + let sdx: i32 = if dx > 0 { 1 } else { -1 }; + let sdy: i32 = if dy > 0 { 1 } else { -1 }; + + if soft_diamond_contains(b.x.wrapping_sub(cx), b.y.wrapping_sub(cy), half) { + return false; + } + if soft_diamond_contains(ex.wrapping_neg(), ey.wrapping_neg(), half) { + return true; + } + // The perturbed column crossing has parameter (ex + epsilon) / dx. + if dx != 0 + && soft_limit_sign(ex, 1, 0) * sdx >= 0 + && soft_limit_sign(dx.wrapping_sub(ex), -1, 0) * sdx >= 0 + && soft_limit_abs( + dy.wrapping_mul(ex).wrapping_sub(ey.wrapping_mul(dx)), + dy, + dx.wrapping_neg(), + half.wrapping_mul(dx.wrapping_abs()), + ) + { + return true; + } + dy != 0 + && soft_limit_sign(ey, 0, 1) * sdy >= 0 + && soft_limit_sign(dy.wrapping_sub(ey), 0, -1) * sdy >= 0 + && soft_limit_abs( + dx.wrapping_mul(ey).wrapping_sub(ex.wrapping_mul(dy)), + dy.wrapping_neg(), + dx, + half.wrapping_mul(dy.wrapping_abs()), + ) + } +} + +impl SoftVertex { + /// LineRule::CentreSampled: pixel (px, py) is on the segment to b if its + /// column (row, for a y-major segment) has its centre between the ends, + /// the start included and the end not, and it is the minor-axis pixel + /// holding the segment at that centre, an exact crossing on a pixel edge + /// going to the lower pixel, ceil(v) - 1. Exact on the snapped grid: + /// with the minor coordinate at the centre v = n + t / den, the pixel is + /// ceil(v / 2^sub) - 1. Coordinate differences are at most 2^29, so t + /// fits, and the minor base shifted by the width stays below 2^43. + pub fn line_centre_covers(&self, b: &SoftVertex, px: i64, py: i64, sub: i32) -> bool { + let one = 1i64 << sub; + let half = one >> 1; + let x_major = b.x.wrapping_sub(self.x).wrapping_abs() >= b.y.wrapping_sub(self.y).wrapping_abs(); + let (la, lb, na, nb) = if x_major { (self.x, b.x, self.y, b.y) } else { (self.y, b.y, self.x, b.x) }; + let c = (if x_major { px } else { py }).wrapping_mul(one).wrapping_add(half); + let mut den = lb.wrapping_sub(la); + + if if den > 0 { c < la || c >= lb } else { c > la || c <= lb } { + return false; + } + let mut t = nb.wrapping_sub(na).wrapping_mul(c.wrapping_sub(la)); + if den < 0 { + t = t.wrapping_neg(); + den = den.wrapping_neg(); + } + // v = na + floor(t / den) plus a fraction that is zero or not. + let v = na.wrapping_add(t.div_euclid(den)); + let minor = if t % den != 0 { v.div_euclid(one) } else { v.wrapping_neg().div_euclid(one).wrapping_neg().wrapping_sub(1) }; + (if x_major { py } else { px }) == minor + } + + /// Whether the segment to b covers pixel (px, py) under the line rule. + pub fn line_covered(&self, rule: LineRule, b: &SoftVertex, px: i64, py: i64, sub: i32) -> bool { + match rule { + LineRule::CentreSampled => self.line_centre_covers(b, px, py, sub), + LineRule::Diamond => self.line_covers(b, px, py, sub), + } + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct SoftPoint2 { + pub x: f64, + pub y: f64, +} + +pub(super) fn soft_clip_edge( + in_: &[SoftPoint2], + n: u32, + out: &mut [SoftPoint2], + axis: u32, + bound: f64, + greater: bool, +) -> u32 { + let mut count = 0usize; + + for i in 0..n as usize { + let a = in_[i]; + let b = in_[(i + 1) % n as usize]; + let av = if axis != 0 { a.y } else { a.x }; + let bv = if axis != 0 { b.y } else { b.x }; + let ai = if greater { av >= bound } else { av <= bound }; + let bi = if greater { bv >= bound } else { bv <= bound }; + + if ai != bi { + let t = (bound - av) / (bv - av); + let mut p = SoftPoint2 { x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) }; + + if axis != 0 { + p.y = bound; + } else { + p.x = bound; + } + out[count] = p; + count += 1; + } + if bi { + out[count] = b; + count += 1; + } + } + count as u32 +} + +pub(super) fn soft_polygon_square_area(polygon: &[SoftPoint2], n: u32, x: i32, y: i32) -> f64 { + let mut a = [SoftPoint2::default(); 12]; + let mut b = [SoftPoint2::default(); 12]; + let mut area = 0.0f64; + let mut n = n; + + a[..n as usize].copy_from_slice(&polygon[..n as usize]); + n = soft_clip_edge(&a, n, &mut b, 0, x as f64, true); + n = soft_clip_edge(&b, n, &mut a, 0, (x + 1) as f64, false); + n = soft_clip_edge(&a, n, &mut b, 1, y as f64, true); + n = soft_clip_edge(&b, n, &mut a, 1, (y + 1) as f64, false); + for i in 0..n as usize { + let p = &a[i]; + let q = &a[(i + 1) % n as usize]; + + area += p.x * q.y - p.y * q.x; + } + cmin(1.0, area.abs() * 0.5) +} + +pub(super) fn soft_line_coverage(ax: f64, ay: f64, bx: f64, by: f64, width: f64, x: i32, y: i32) -> f64 { + let dx = bx - ax; + let dy = by - ay; + let length = dx.hypot(dy); + + if !(length > 0.0) || !(width > 0.0) { + return 0.0; + } + let nx = -dy * width / (2.0 * length); + let ny = dx * width / (2.0 * length); + let rectangle = [ + SoftPoint2 { x: ax + nx, y: ay + ny }, + SoftPoint2 { x: bx + nx, y: by + ny }, + SoftPoint2 { x: bx - nx, y: by - ny }, + SoftPoint2 { x: ax - nx, y: ay - ny }, + ]; + soft_polygon_square_area(&rectangle, 4, x, y) +} + +pub(super) fn soft_circle_integral(x: f64, radius: f64) -> f64 { + let y = cmax(0.0, radius * radius - x * x).sqrt(); + + 0.5 * (x * y + radius * radius * (x / radius).asin()) +} + +pub(super) fn soft_circle_coverage(cx: f64, cy: f64, radius: f64, x: i32, y: i32) -> f64 { + let mut cuts = [0.0f64; 8]; + let mut area = 0.0f64; + let mut n = 0usize; + let lo = cmax(x as f64 - cx, -radius); + let hi = cmin((x + 1) as f64 - cx, radius); + + if !(lo < hi) || !(radius > 0.0) { + return 0.0; + } + cuts[n] = lo; + n += 1; + cuts[n] = hi; + n += 1; + for edge in 0..2 { + let yedge = (if edge != 0 { y + 1 } else { y }) as f64 - cy; + + if yedge.abs() < radius { + let root = (radius * radius - yedge * yedge).sqrt(); + + if -root > lo && -root < hi { + cuts[n] = -root; + n += 1; + } + if root > lo && root < hi { + cuts[n] = root; + n += 1; + } + } + } + for i in 1..n { + let value = cuts[i]; + let mut j = i; + + while j != 0 && cuts[j - 1] > value { + cuts[j] = cuts[j - 1]; + j -= 1; + } + cuts[j] = value; + } + for i in 0..n.saturating_sub(1) { + let a = cuts[i]; + let b = cuts[i + 1]; + let mid = (a + b) * 0.5; + let circle = cmax(0.0, radius * radius - mid * mid).sqrt(); + let ymin = y as f64 - cy; + let ymax = (y + 1) as f64 - cy; + let top = cmin(ymax, circle); + let bottom = cmax(ymin, -circle); + let arc = soft_circle_integral(b, radius) - soft_circle_integral(a, radius); + + if top <= bottom { + continue; + } + area += (if top == circle { arc } else { ymax * (b - a) }) + - (if bottom == -circle { -arc } else { ymin * (b - a) }); + } + cmin(1.0, cmax(0.0, area)) +} + +impl RasterRules { + pub fn point_coverage(&self, cx: f64, cy: f64, size: f64, x: i32, y: i32) -> f64 { + soft_circle_coverage(cx, cy, size * 0.5, x, y) + } + + #[allow(clippy::too_many_arguments, reason = "two end points, a width and a pixel")] + pub fn line_coverage(&self, ax: f64, ay: f64, bx: f64, by: f64, width: f64, x: i32, y: i32) -> f64 { + soft_line_coverage(ax, ay, bx, by, width, x, y) + } + + /// How far from a smoothed point's centre, or a smoothed line, in pixels, + /// a fragment can have coverage. + pub fn smooth_reach(&self, size: f64) -> f64 { + size * 0.5 + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct SoftLineStipple { + pub counter: u32, + pub repeat: u32, + pub pattern: u32, + pub enabled: bool, + pub consumed: bool, +} + +impl SoftVertex { + pub fn line_vertex_at(&self, b: &SoftVertex, t: f64) -> SoftVertex { + let mut out = SoftVertex::default(); + let wa = (1.0 - t) * self.inv_w; + let wb = t * b.inv_w; + let sum = wa + wb; + + out.x = (self.x as f64 + t * (b.x - self.x) as f64).round() as i64; + out.y = (self.y as f64 + t * (b.y - self.y) as f64).round() as i64; + out.z = self.z + t * (b.z - self.z); + out.inv_w = (1.0 - t) * self.inv_w + t * b.inv_w; + out.depth_w = (1.0 - t) * self.depth_w + t * b.depth_w; + for k in 0..4 { + out.rgba[k] = (wa * self.rgba[k] + wb * b.rgba[k]) / sum; + out.texcoord[k] = (wa * self.texcoord[k] + wb * b.texcoord[k]) / sum; + } + for i in 0..GL_CLIP_PLANES as usize { + out.clip[i] = (wa * self.clip[i] + wb * b.clip[i]) / sum; + } + out + } +} + +impl SoftDraw<'_> { + /// Map and rasterize one aliased segment, already projected, with the + /// provoking vertex's colour when flat shaded. Width follows the + /// minor-axis shift and replication of 3.4.3. A smoothed segment takes the + /// box-filter coverage (3.4.2), stippled as one-pixel pieces + /// along its length. + pub fn line_projected(&self, pa: &SoftVertex, pb: &SoftVertex, provoking: &[f32], offset: f64, stipple: Option<&mut SoftLineStipple>, client_clip: bool) { + let mut stipple = stipple; + let st = self.st; + let smooth = st.shade_model != GL_FLAT; + let sub = self.rules.subpixel_bits as i32; + let one_sub = (1i32 << sub) as f64; + let a = *pa; + let b = *pb; + // A stippled segment outside the viewport still counts, against the + // window's own rectangle. + let bounds = self.bounds(); + let visible = bounds.is_some(); + let (bx0, by0, bx1, by1) = bounds.unwrap_or_else(|| self.owned_bounds()); + + if (!visible && stipple.is_none()) || (a.x == b.x && a.y == b.y) { + return; + } + if self.line_smooth && stipple.as_ref().is_some_and(|s| !s.enabled) { + let length = ((b.x - a.x) as f64 / one_sub).hypot((b.y - a.y) as f64 / one_sub); + let s = stipple.as_mut().unwrap(); + let period = 16u32.wrapping_mul(s.repeat); + + // Unstippled, the segment is one rectangle (3.4.2): its fragments + // are not split between one-pixel pieces, each blended on its own. + // The count still moves on by the pieces a stipple would take. + s.counter = s.counter.wrapping_add((length.ceil() % period as f64) as u32) % period; + s.consumed = true; + if !visible { + return; + } + stipple = None; + } + if let Some(s) = stipple.as_mut().filter(|_| self.line_smooth) { + let length = ((b.x - a.x) as f64 / one_sub).hypot((b.y - a.y) as f64 / one_sub); + let segments = length.ceil(); + let mut i = 0.0f64; + + while i < segments { + let bit = (s.counter / s.repeat) % 16; + let keep = !s.enabled || (s.pattern & (1u32 << bit)) != 0; + + s.counter = (s.counter.wrapping_add(1)) % 16u32.wrapping_mul(s.repeat); + s.consumed = true; + if keep { + let t0 = i / length; + let t1 = cmin((i + 1.0) / length, 1.0); + let start = a.line_vertex_at(&b, t0); + let end = a.line_vertex_at(&b, t1); + self.line_projected(&start, &end, provoking, offset, None, client_clip); + } + i += 1.0; + } + return; + } + let gradient = match self.texture_base { + Some(_) => a.texture_line_gradient(&b, sub as u32, self.image().volume, self.image().one_dimensional), + None => Some(([0.0; 4], [0.0; 4], 0.0)), + }; + let Some((texture_value, texture_delta, texture_length)) = gradient else { + self.texture_report(16, "invalid primitive coefficients, primitive dropped"); + return; + }; + let mut flat = [0.0f64; 4]; + for k in 0..4 { + flat[k] = provoking[vtx::COLOR + k] as f64; + } + let len2 = (b.x - a.x) as f64 * (b.x - a.x) as f64 + (b.y - a.y) as f64 * (b.y - a.y) as f64; + let x_major = (b.x - a.x).wrapping_abs() >= (b.y - a.y).wrapping_abs(); + let mut shifted_a = a; + let mut shifted_b = b; + // Width <= INT32_MAX and sub <= 12 give a shift below 2^42. + let shift = (self.line_width.wrapping_sub(1) as i64) * (1i64 << (sub - 1)); + if x_major { + shifted_a.y -= shift; + shifted_b.y -= shift; + } else { + shifted_a.x -= shift; + shifted_b.x -= shift; + } + + // Walk the major axis a pixel at a time, and in each step try the + // minor-axis pixels within one of those the segment spans there. + let mut major0 = ((if x_major { a.x.min(b.x) } else { a.y.min(b.y) }) >> sub) as i32; + let mut major1 = ((if x_major { a.x.max(b.x) } else { a.y.max(b.y) }) >> sub) as i32; + major0 -= 1; + major1 += 1; + if self.line_smooth { + let pad = self.rules.smooth_reach(self.smooth_line_width); + let l0 = (if x_major { a.x } else { a.y }) as f64; + let l1 = (if x_major { b.x } else { b.y }) as f64; + + major0 = (cmin(l0, l1) / one_sub - pad).floor() as i32 - 1; + major1 = (cmax(l0, l1) / one_sub + pad).ceil() as i32 + 1; + } + let mut step = 1i32; + if stipple.is_some() { + // Count coverage before ownership/scissor, in submitted direction. + if if x_major { b.x < a.x } else { b.y < a.y } { + std::mem::swap(&mut major0, &mut major1); + step = -1; + } + } else { + major0 = major0.max(if x_major { bx0 } else { by0 }); + major1 = major1.min(if x_major { bx1 } else { by1 }); + } + let mut mm = major0; + loop { + if !(if step > 0 { mm <= major1 } else { mm >= major1 }) { + break; + } + let m = mm; + mm += step; + let (lo, hi) = if x_major { + if a.x <= b.x { (&shifted_a, &shifted_b) } else { (&shifted_b, &shifted_a) } + } else if a.y <= b.y { + (&shifted_a, &shifted_b) + } else { + (&shifted_b, &shifted_a) + }; + let l0 = (if x_major { lo.x } else { lo.y }) as f64; + let l1 = (if x_major { hi.x } else { hi.y }) as f64; + let n0 = (if x_major { lo.y } else { lo.x }) as f64; + let n1 = (if x_major { hi.y } else { hi.x }) as f64; + let mut e0 = cmax(m as f64 * one_sub, l0); + let mut e1 = cmin((m + 1) as f64 * one_sub, l1); + let f0: f64; + let f1: f64; + + if l1 > l0 { + e0 = cmin(e0, l1); + e1 = cmax(e1, l0); + f0 = n0 + (n1 - n0) * (e0 - l0) / (l1 - l0); + f1 = n0 + (n1 - n0) * (e1 - l0) / (l1 - l0); + } else { + f0 = n0; + f1 = n1; + } + let mut minor0 = (cmin(f0, f1) / one_sub).floor() as i64 - 1; + let mut minor1 = (cmax(f0, f1) / one_sub).floor() as i64 + 1; + if self.line_smooth { + let pad = self.rules.smooth_reach(self.smooth_line_width); + let q0 = (if x_major { a.y } else { a.x }) as f64; + let q1 = (if x_major { b.y } else { b.x }) as f64; + + minor0 = (cmin(q0, q1) / one_sub - pad).floor() as i64 - 1; + minor1 = (cmax(q0, q1) / one_sub + pad).ceil() as i64 + 1; + } + let mut nn = minor0; + while nn <= minor1 { + let n = nn; + nn += 1; + let base_x: i64 = if x_major { m as i64 } else { n }; + let base_y: i64 = if x_major { n } else { m as i64 }; + let replica0: i64 = cmax(n, (if x_major { by0 } else { bx0 }) as i64); + let replica1: i64 = if self.line_smooth { + replica0 + } else { + cmin(n + self.line_width as i64 - 1, (if x_major { by1 } else { bx1 }) as i64) + }; + let mut keep = true; + let coverage = if self.line_smooth { + self.rules.line_coverage( + a.x as f64 / one_sub, + a.y as f64 / one_sub, + b.x as f64 / one_sub, + b.y as f64 / one_sub, + self.smooth_line_width, + base_x as i32, + base_y as i32, + ) + } else if shifted_a.line_covered(self.rules.line_rule, &shifted_b, base_x, base_y, sub) { + 1.0 + } else { + 0.0 + }; + + if !(coverage > 0.0) { + continue; + } + if let Some(s) = stipple.as_mut() { + let bit = (s.counter / s.repeat) % 16; + + keep = !s.enabled || (s.pattern & (1u32 << bit)) != 0; + s.counter = s.counter.wrapping_add(1) % 16u32.wrapping_mul(s.repeat); + s.consumed = true; + } + if !keep + || !visible + || replica0 > replica1 + || m < (if x_major { bx0 } else { by0 }) + || m > (if x_major { bx1 } else { by1 }) + { + continue; + } + // Visit only drawable-visible replicas, never width entries. + let mut replica = replica0; + while replica <= replica1 { + let rep = replica; + replica += 1; + let px: i32 = if x_major { m } else { rep as i32 }; + let py: i32 = if x_major { rep as i32 } else { m }; + let cx = (px as f64 + 0.5) * one_sub; + let cy = (py as f64 + 0.5) * one_sub; + let mut frag = SoftFrag::default(); + + let mut t = ((cx - a.x as f64) * (b.x - a.x) as f64 + (cy - a.y as f64) * (b.y - a.y) as f64) / len2; + // Approximation: retain endpoint values, not extrapolation. + t = cmin(cmax(t, 0.0), 1.0); + let wa = (1.0 - t) * a.inv_w; + let wb = t * b.inv_w; + for k in 0..4 { + frag.texcoord[k] = (wa * a.texcoord[k] + wb * b.texcoord[k]) / (wa + wb); + } + if st.clip_planes != 0 && client_clip { + let mut d = [0.0f64; GL_CLIP_PLANES as usize]; + + for i in 0..GL_CLIP_PLANES as usize { + d[i] = (wa * a.clip[i] + wb * b.clip[i]) / (wa + wb); + } + if st.clipped(&d) { + continue; + } + } + for k in 0..4 { + frag.rgba[k] = if smooth { (wa * a.rgba[k] + wb * b.rgba[k]) / (wa + wb) } else { flat[k] }; + } + if self.texture_base.is_some() { + let mut value = [0.0f64; 4]; + let mut out = [0.0f64; 5]; + let zero = [0.0f64; 4]; + + for k in 0..4 { + value[k] = texture_value[k] + t * texture_delta[k]; + } + frag.texture_rho = if soft_texture_derivatives( + &value, + &texture_delta, + &zero, + self.base_level().width as f64 * soft_texture_lod_scale(self.image().lod_scale_s), + if self.image().one_dimensional { + 0.0 + } else { + self.base_level().height as f64 * soft_texture_lod_scale(self.image().lod_scale_t) + }, + if self.image().volume { + self.base_level().depth as f64 * soft_texture_lod_scale(self.image().lod_scale_r) + } else { + 0.0 + }, + &mut out, + ) { + out[4] / texture_length + } else { + f64::NAN + }; + } + self.fragment_coverage( + px, + py, + a.z + t * (b.z - a.z), + offset, + (1.0 - t) * a.depth_w + t * b.depth_w, + &frag, + coverage, + self.line_smooth, + ); + } + } + } + } + + /// Keep standalone client-plane rejection after geometric counting. + pub fn line(&self, va: &[f32], vb: &[f32], provoking: &[f32], stipple: Option<&mut SoftLineStipple>) { + if stipple.is_none() && self.bounds().is_none() { + return; + } + let ca = self.st.volume_from_draw_vertex(va); + let cb = self.st.volume_from_draw_vertex(vb); + let clip = self.st.volume_clip(false); + let any = clip.outcode_by::(&ca) | clip.outcode_by::(&cb); + if any & NearW::W_PLANE != 0 { + report_w_plane_clip(); + } + let Some((ca, cb)) = clip.clip_line_by::(ca, cb, any) else { + return; + }; + if let Some((a, b)) = self.project(&ca).and_then(|a| self.project(&cb).map(|b| (a, b))) { + self.line_projected(&a, &b, provoking, 0.0, stipple, true); + } + } + + /// Aliased square-point centring follows OpenGL 1.2.1, 3.3, on the shared + /// snapped subpixel grid. A smoothed point takes the + /// box-filter coverage about its snapped centre (3.3.2). MODEL CHOICE: + /// the board's coverage rule for a smoothed point is UNKNOWN; a point of + /// size 1 covers about pi/4 of a pixel at its centre (a sampling artefact, not the board). The graded rule + /// (INFERRED) is a = G(c), c the exact pixel-square area covered by a strip + /// of the line width or a square of the point size, G a table in the + /// datasheet, blended with source alpha = coverage. The model's disc area + /// differs from it (about 0.15 at 0.95 pixel against the vendor frames): + /// a known approximation, to be replaced. + pub fn point_projected(&self, pv: &SoftVertex, provoking: Option<&[f32]>, offset: f64) { + let sub = self.rules.subpixel_bits as i32; + let v = *pv; + let mut frag = SoftFrag::default(); + let psize = self.point_size as i64; + + let Some((bx0, by0, bx1, by1)) = self.bounds() else { + return; + }; + // Even dimensions centre on a pixel corner; odd ones on a centre. + let px: i64 = (v.x + (if self.point_size % 2 != 0 { 0 } else { 1i64 << (sub - 1) })) >> sub; + let py: i64 = (v.y + (if self.point_size % 2 != 0 { 0 } else { 1i64 << (sub - 1) })) >> sub; + let mut x0: i64 = cmax(px - psize / 2, bx0 as i64); + let mut y0: i64 = cmax(py - psize / 2, by0 as i64); + let mut x1: i64 = cmin(px - psize / 2 + psize - 1, bx1 as i64); + let mut y1: i64 = cmin(py - psize / 2 + psize - 1, by1 as i64); + frag.rgba = v.rgba; + frag.fixed = false; + if let Some(p) = provoking { + if self.st.shade_model == GL_FLAT { + for k in 0..4 { + frag.rgba[k] = p[vtx::COLOR + k] as f64; + } + } + } + frag.texture_rho = 1.0; + frag.texcoord = v.texcoord; + if self.point_smooth { + let scale = (1i32 << sub) as f64; + let cx = v.x as f64 / scale; + let cy = v.y as f64 / scale; + let radius = self.rules.smooth_reach(self.smooth_point_size); + + x0 = cmax((cx - radius).floor() as i64, bx0 as i64); + y0 = cmax((cy - radius).floor() as i64, by0 as i64); + x1 = cmin((cx + radius).ceil() as i64 - 1, bx1 as i64); + y1 = cmin((cy + radius).ceil() as i64 - 1, by1 as i64); + let mut y = y0; + while y <= y1 { + let mut x = x0; + while x <= x1 { + let coverage = self.rules.point_coverage(cx, cy, self.smooth_point_size, x as i32, y as i32); + + self.fragment_coverage(x as i32, y as i32, v.z, offset, v.depth_w, &frag, coverage, true); + x += 1; + } + y += 1; + } + return; + } + let mut y = y0; + while y <= y1 { + let mut x = x0; + while x <= x1 { + self.fragment(x as i32, y as i32, v.z, offset, v.depth_w, &frag); + x += 1; + } + y += 1; + } + } + + pub fn point(&self, vp: &[f32]) { + let cv = self.st.volume_from_draw_vertex(vp); + let oc = self.st.volume_clip(false).outcode_by::(&cv); + if oc != 0 { + if oc & NearW::W_PLANE != 0 { + report_w_plane_clip(); + } + return; + } + if let Some(v) = self.project(&cv).filter(|v| !self.st.clipped(&v.clip)) { + self.point_projected(&v, None, 0.0); + } + } +} + +/// Internal representation bound, not an SGI width range. Half ties round up. +pub(super) fn soft_dimension(requested: f32) -> Option { + let n = if requested == 0.0 { 1.0 } else { (requested as f64 + 0.5).floor() }; + + if !requested.is_finite() || requested < 0.0 || n > i32::MAX as f64 { + crate::dlog!(LogModule::Odyssey, "sgi-gl: unsupported aliased dimension {}", requested); + return None; + } + Some(cmax(n, 1.0) as u32) +} diff --git a/src/dev/odyssey/raster_pixels.rs b/src/dev/odyssey/raster_pixels.rs new file mode 100644 index 000000000..cbd09ddf2 --- /dev/null +++ b/src/dev/odyssey/raster_pixels.rs @@ -0,0 +1,437 @@ +//! Pixel work that is not a GL primitive: a draw's pixel estimate, clears +//! and the X server's raster ops. +//! +//! Ported from hw/display/sgi/rasterizer.c in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use super::gl::{GlPixels, GlPlaneRow, GlPlanes}; +use super::gl_context::{ + GL_COLOR_BUFFER_BIT, GL_COLOUR_MASK_ALL, GL_DEPTH_BUFFER_BIT, GL_MAX_TARGETS, GL_QUADS, + GL_QUAD_STRIP, GL_STENCIL_BUFFER_BIT, GL_TRIANGLES, GlDraw, RASTER_CLEAR, RasterOp, +}; +use super::memory::{logic_op, CLIP_MAX, ClipList}; +use super::raster::{cmax, cmin, Planes, RasterColour, RasterRules, RasterTarget, SoftDraw}; +impl SoftDraw<'_> { + /// About how many pixels a draw's filled primitives cover: the area of each + /// one with its vertices moved into the pixels the window owns, summed + /// until it reaches limit. A vertex with no window position counts as + /// reaching it. + pub fn draw_pixels(&self, d: &GlDraw, limit: u64) -> u64 { + let n = d.count as usize; + let mut sum = 0u64; + + let Some((x0, y0, x1, y1)) = self.bounds().filter(|_| n >= 3) else { + return 0; + }; + let mut xy = vec![0.0f64; 2 * n]; + for i in 0..n { + let st = self.st; + let clip = st.eye_to_clip(st.draw_vertex_eye(d.soft_polygon_vertex(None, i))); + if !(clip[3] > 0.0) { + return limit; + } + let Some(win) = st.clip_to_window(&clip, (1i32 << self.rules.subpixel_bits) as f32) else { + return limit; + }; + xy[2 * i] = f64::from(win[0]); + xy[2 * i + 1] = f64::from(win[1]); + } + // Each primitive's own vertices; a fan's first is left out. + let step: usize = if d.mode == GL_TRIANGLES { + 3 + } else if d.mode == GL_QUADS { + 4 + } else if d.mode == GL_QUAD_STRIP { + 2 + } else { + 1 + }; + let size: usize = if d.mode == GL_QUADS || d.mode == GL_QUAD_STRIP { 4 } else { 3 }; + let mut i = 0usize; + while i + size <= n && sum < limit { + let mut px = [0.0f64; 4]; + let mut py = [0.0f64; 4]; + let mut twice = 0.0f64; + + for k in 0..size { + px[k] = cmin(cmax(xy[2 * (i + k)], x0 as f64), (x1 + 1) as f64); + py[k] = cmin(cmax(xy[2 * (i + k) + 1], y0 as f64), (y1 + 1) as f64); + } + for k in 0..size { + let next = (k + 1) % size; + + twice += px[k] * py[next] - px[next] * py[k]; + } + sum = sum.wrapping_add((twice.abs() / 2.0) as u64); + i += step; + } + sum + } +} + +impl ClipList { + /// Whether one rectangle of a non-empty clip list holds all of row y from + /// x0 to x1, so that each pixel's own test would pass. + pub fn soft_clip_covers_row(&self, x0: i32, x1: i32, y: i32) -> bool { + for i in 0..cmin(self.n, CLIP_MAX as u32) { + let r = &self.r[i as usize]; + + if y >= r.y0 && y <= r.y1 && x0 >= r.x0 && x1 <= r.x1 { + return true; + } + } + false + } +} + +/// A clear's decided values, which every row of it stores. +pub(super) struct SoftClear<'a> { + pub rules: &'a RasterRules, + pub t: &'a RasterTarget<'a>, + pub op: &'a RasterOp, + pub colour: bool, + pub depth: bool, + pub stencil: bool, + pub rgba: bool, + pub rgba_read: bool, + pub rgb: u32, + pub smax: u32, + pub mask: u32, + pub rgba_mask: u32, + pub ntargets: u32, + pub depth_value: u32, + pub clear: RasterColour, + pub rgba_reported: std::sync::atomic::AtomicBool, +} + +impl SoftClear<'_> { + /// The clear's rows, stored to px. A row a single clip rectangle of the + /// op holds whole is stored through the row of planes px lends for it + /// (GlPlanes::row()), where it lends one and the clear stores only what a + /// lent row takes: RGBA colour, depth and stencil. + pub fn rows(&self, px: &GlPlanes) { + let op = self.op; + let lend = !self.colour || self.rgba; + // A lent row that reads nothing back stores the same words at every + // pixel: the loop for the planes the clear names. + let stores: Option = match (self.rgba_read, self.depth, self.stencil, self.colour) { + (true, ..) => None, + (false, true, false, true) => Some(clear_lent_row::), + (false, true, false, false) => Some(clear_lent_row::), + (false, false, false, true) => Some(clear_lent_row::), + (false, d, true, c) => Some(if d && c { + clear_lent_row:: + } else if d { + clear_lent_row:: + } else if c { + clear_lent_row:: + } else { + clear_lent_row:: + }), + (false, false, false, false) => None, + }; + + for y in op.y0..=op.y1 { + let whole_row = op.clip.n <= 1 || op.clip.soft_clip_covers_row(op.x0, op.x1, y); + + if lend && whole_row && op.x0 <= op.x1 { + if let Some(row) = px.row(y, op.x0, op.x1) { + match stores { + Some(stores) => stores(self, &row, y), + None => self.row(&row, y, true), + } + continue; + } + } + self.row(px, y, whole_row); + if self.colour && !self.rgba { + self.index_row(px, y, whole_row); + } + } + } + + /// Row y of the clear's depth, stencil and RGBA colour, stored to px; + /// with whole_row unset each pixel tests the op's clip list. + #[inline(always)] + fn row(&self, px: &P, y: i32, whole_row: bool) { + let rules = self.rules; + let op = self.op; + // Only a read fills it, and only a read's targets are read back. + let mut dest = [RasterColour::default(); GL_MAX_TARGETS as usize]; + + for x in op.x0..=op.x1 { + if !whole_row && !op.clip.contains(x, y) { + continue; + } + + if self.rgba && self.rgba_read { + dest = [RasterColour::default(); GL_MAX_TARGETS as usize]; + if !rules.rgba_load(px, &op.targets, self.ntargets, x, y, &mut dest, &self.rgba_reported) { + continue; + } + } + if self.depth { + px.store_depth(x, y, self.depth_value); + } + if self.stencil { + let smask = op.stencil_mask & self.smax; + + px.store_stencil(x, y, (px.load_stencil(x, y) & !smask & self.smax) | (op.clear_stencil & smask)); + } + let mut i = 0u32; + while self.colour && self.rgba && i < self.ntargets { + let mut out = self.clear; + + for k in 0..4 { + if self.rgba_mask & (1u32 << k) == 0 { + out.component[k] = dest[i as usize].component[k]; + } + } + px.store_rgba(op.targets[i as usize], x, y, &out); + i += 1; + } + } + } + + /// Row y of a colour-index clear's colour, the packed clear colour under + /// a full mask (RasterRules::raster()). + fn index_row(&self, px: &GlPlanes, y: i32, whole_row: bool) { + let op = self.op; + + for x in op.x0..=op.x1 { + if whole_row || op.clip.contains(x, y) { + for &target in &op.targets[..self.ntargets as usize] { + px.store_colour(target, x, y, self.rgb); + } + } + } + } +} + +/// Row y of clear c, which reads nothing back, through the row of planes px +/// lends: SoftClear::row()'s stores for the planes DEPTH, STENCIL and COLOUR +/// name. The colour is the clear colour whole: only a partial mask reads +/// the destination. +fn clear_lent_row(c: &SoftClear, px: &GlPlaneRow, y: i32) { + let op = c.op; + let smask = op.stencil_mask & c.smax; + + // The planes are independent, so each is stored across the row in turn: + // depth and colour as runs, which fill a slice where the row's id test + // passes every pixel, and stencil, which reads, a pixel at a time. + if DEPTH { + px.fill_depth(op.x0, op.x1, c.depth_value); + } + if STENCIL { + for x in op.x0..=op.x1 { + px.store_stencil(x, y, (px.load_stencil(x, y) & !smask & c.smax) | (op.clear_stencil & smask)); + } + } + if COLOUR { + for &target in &op.targets[..c.ntargets as usize] { + px.fill_rgba(target, op.x0, op.x1, &c.clear); + } + } +} + +impl RasterRules { + /// CLEAR writes the clear colour under the colour mask, or in colour-index + /// mode the clear index under the index mask, to the op's targets, the + /// clear depth to the depth plane and the clear stencil value under its + /// mask to the stencil buffer, where the planes have one, at every pixel of + /// its rectangle its clip list keeps. The other kinds are not executed + /// here. + pub fn raster(&self, t: &RasterTarget, pixels: &mut GlPixels, op: &RasterOp) { + let mut colour = op.clear_buffers & GL_COLOR_BUFFER_BIT != 0; + let depth = op.clear_buffers & GL_DEPTH_BUFFER_BIT != 0; + let mut stencil = op.clear_buffers & GL_STENCIL_BUFFER_BIT != 0; + let mut rgb = 0u32; + let mut smax = 0u32; + let mut mask = self.colour_bits(); + let rgba_mask = self.rgba_mask(op.clear_colour_mask); + let rgba = !t.index; + let rgba_read = colour + && op.ntargets != 0 + && rgba_mask != 0 + && rgba_mask != self.rgba_mask(GL_COLOUR_MASK_ALL); + let ntargets = cmin(op.ntargets, GL_MAX_TARGETS); + let mut clear = RasterColour::default(); + + if !self.check_rules() { + return; + } + // After the check: the depth maximum shifts by 32 - depth_bits. + let depth_value = op.clear_depth24 & self.depth_max(); + if op.kind != RASTER_CLEAR { + crate::dlog!(LogModule::Odyssey, "sgi-gl: raster op of kind {} is not executed", op.kind); + return; + } + if op.clip.n == 0 && op.clip.empty { + return; + } + if stencil { + match self.stencil_max() { + Some(max) => smax = max, + None => stencil = false, + } + } + if colour { + let c = [op.clear_rgba[0] as f64, op.clear_rgba[1] as f64, op.clear_rgba[2] as f64]; + + mask = self.colour_mask(op.clear_colour_mask); + if !rgba && mask != self.colour_bits() && mask != 0 { + crate::dlog!(LogModule::Odyssey, "sgi-gl: a partial colour mask needs planes that can be read back; cleared without it"); + mask = self.colour_bits(); + } + colour = if rgba { rgba_mask != 0 } else { mask != 0 }; + rgb = self.rgb(&c); + // Clears remain undithered: no dither state is submitted. + clear = self.rgba_value(rgb, op.clear_rgba[3] as f64); + } + let rows = SoftClear { + rules: self, + t, + op, + colour, + depth, + stencil, + rgba, + rgba_read, + rgb, + smax, + mask, + rgba_mask, + ntargets, + depth_value, + clear, + rgba_reported: Default::default(), + }; + rows.rows(&pixels.planes()); + } +} + +/// The X server's logic ops (GX numbers) are GL's logic-op enums less +/// GL_CLEAR; an out-of-range one keeps the source. +pub(super) fn x_logic_op(gx: u32, source: u32, destination: u32) -> u32 { + if gx <= 15 { + logic_op(gx as u8, source, destination) + } else { + source + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::dev::odyssey::consts::{ODSY_ID_COMPARE_ENABLE, ODSY_ID_COMPARE_MASK_SHIFT}; + use crate::dev::odyssey::memory::ClipRect; + use crate::dev::odyssey::raster_bench::{Bench, Lcg}; + + + + /// The square of plane pixels the cases clear in. + const SIDE: i32 = 96; + + /// Both colour buffers, depth and stencil over the square. + fn snapshot(b: &Bench) -> Vec { + let mut out = b.snapshot(SIDE).1; + out.extend(b.snapshot(SIDE).2); + for buffer in 0..2 { + for y in 0..SIDE { + for x in 0..SIDE { + out.push(b.board.buffer_load(buffer, x as i64, y as i64)); + } + } + } + out + } + + /// A clear in random state on a board in random state, its planes + /// first filled with noise; through the row the planes lend, or not. + fn clear(seed: u64, lend: bool) -> Vec { + let mut b = Bench::empty(); + let mut rng = Lcg(seed.wrapping_mul(0x9e3779b97f4a7c15) | 1); + let mut noise = Lcg(seed); + + for buffer in 0..2 { + for y in 0..SIDE { + for x in 0..SIDE { + b.board.buffer_store(buffer, x as i64, y as i64, noise.next_u32()); + } + } + } + for v in b.board.planes.depth.bytes.iter_mut().take(4 * 1280 * SIDE as usize) { + *v = noise.next_u32() as u8; + } + for v in b.board.planes.stencil.bytes.iter_mut().take(1280 * SIDE as usize) { + *v = noise.next_u32() as u8; + } + if rng.next_u32() % 2 == 0 { + b.board.win.id_compare = ODSY_ID_COMPARE_ENABLE | (0x0f << ODSY_ID_COMPARE_MASK_SHIFT) | 0x05; + for d in b.board.planes.did.iter_mut() { + *d = if noise.next_u32() % 4 == 0 { 0x15 } else { 0x06 }; + } + } + if rng.next_u32() % 2 == 0 { + // Board clip rectangles that split rows: those rows are not lent. + b.board.win.clip_kept = 2; + b.board.win.clip_rect[0] = ClipRect { x0: 0, y0: 0, x1: 40, y1: SIDE - 1 }; + b.board.win.clip_rect[1] = ClipRect { x0: 30, y0: 20, x1: SIDE - 1, y1: 70 }; + } + let x0 = (rng.next_u32() % 40) as i32; + let y0 = (rng.next_u32() % 40) as i32; + let mut op = RasterOp { + kind: RASTER_CLEAR, + ntargets: 1 + rng.next_u32() % 2, + targets: [0, 1, 0, 0], + x0, + y0, + x1: x0 + 10 + (rng.next_u32() % 50) as i32, + y1: y0 + 10 + (rng.next_u32() % 50) as i32, + clear_buffers: [ + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, + GL_DEPTH_BUFFER_BIT, + GL_STENCIL_BUFFER_BIT, + GL_COLOR_BUFFER_BIT, + GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, + GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, + ][rng.next_u32() as usize % 7], + clear_rgba: [0.2, 0.7, 0.4, 0.9], + clear_colour_mask: if rng.next_u32() % 3 == 0 { rng.next_u32() % 16 } else { GL_COLOUR_MASK_ALL }, + clear_depth24: 0x123456, + clear_stencil: 0x5a, + stencil_mask: [u32::MAX, 0x0f, 0x81][rng.next_u32() as usize % 3], + ..Default::default() + }; + op.clip.n = 1 + rng.next_u32() % 2; + // An L: rows above y 50 that cross x 30 to 40 lie in neither + // rectangle whole, and below it pixels right of x 40 are outside. + op.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: 40, y1: SIDE - 1 }; + op.clip.r[1] = ClipRect { x0: 30, y0: 0, x1: SIDE - 1, y1: 50 }; + if op.clip.n == 1 { + op.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: SIDE - 1, y1: SIDE - 1 }; + } + let t = b.board.gl_target(); + let mut parts = b.board.gl_parts(None).0; + parts.lend_rows = lend; + b.rules.raster(&t, &mut parts, &op); + drop(parts); + snapshot(&b) + } + + // A clear stores through the rows the planes lend what it stores pixel + // by pixel: colour under any mask, both buffers, depth and stencil + // under its mask, with the board's id test and clip rectangles. + #[test] + fn lent_clear_rows_match_the_pixel_path() { + for seed in 1..=100u64 { + let want = clear(seed, false); + let got = clear(seed, true); + + assert!(want == got, "seed {seed}: {} of {} plane words differ", want.iter().zip(&got).filter(|(a, b)| a != b).count(), want.len()); + } + } +} diff --git a/src/dev/odyssey/raster_texture.rs b/src/dev/odyssey/raster_texture.rs new file mode 100644 index 000000000..4e57d4b95 --- /dev/null +++ b/src/dev/odyssey/raster_texture.rs @@ -0,0 +1,1222 @@ +//! Texture sampling for the scalar rasteriser: the texture pyramid and its +//! checks, texel reads with wrap, borders and the texel cache, level +//! selection, filtering, sharpen and detail textures, and the texture +//! environment that combines a texel with the fragment colour. +//! +//! Ported from hw/display/sgi/rasterizer.c in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use super::gl_context::{ + vtx, GL_ADD, GL_ALPHA, GL_ALPHA12, GL_ALPHA16, GL_ALPHA4, GL_ALPHA8, GL_BLEND, GL_CLAMP, + GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE, GL_DECAL, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT16_SGIX, + GL_DEPTH_COMPONENT24_SGIX, GL_DEPTH_COMPONENT32_SGIX, GL_INTENSITY, GL_INTENSITY12, + GL_INTENSITY16, GL_INTENSITY4, GL_INTENSITY8, GL_LINEAR, GL_LINEAR_DETAIL_ALPHA_SGIS, + GL_LINEAR_DETAIL_COLOR_SGIS, GL_LINEAR_DETAIL_SGIS, GL_LINEAR_MIPMAP_LINEAR, + GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_SHARPEN_ALPHA_SGIS, GL_LINEAR_SHARPEN_COLOR_SGIS, + GL_LINEAR_SHARPEN_SGIS, GL_LUMINANCE, GL_LUMINANCE12, GL_LUMINANCE12_ALPHA12, + GL_LUMINANCE12_ALPHA4, GL_LUMINANCE16, GL_LUMINANCE16_ALPHA16, GL_LUMINANCE4, + GL_LUMINANCE4_ALPHA4, GL_LUMINANCE6_ALPHA2, GL_LUMINANCE8, GL_LUMINANCE8_ALPHA8, + GL_LUMINANCE_ALPHA, GL_MODULATE, GL_NEAREST, GL_NEAREST_MIPMAP_LINEAR, + GL_NEAREST_MIPMAP_NEAREST, GL_R3_G3_B2, GL_REPEAT, GL_REPLACE, GL_RGB, GL_RGB10, GL_RGB10_A2, + GL_RGB12, GL_RGB16, GL_RGB4, GL_RGB5, GL_RGB5_A1, GL_RGB8, GL_RGBA, GL_RGBA12, GL_RGBA16, + GL_RGBA2, GL_RGBA4, GL_RGBA8, GL_TEXTURE_GEQUAL_R_SGIX, GL_TEXTURE_LEQUAL_R_SGIX, GlDrawState, +}; +use super::raster::{ + cmax, cmin, FIXED_FAILED, FIXED_LEVELS, GL_TEX_LEVELS_MAX, GlTexImage, GlTexLevel, + GlTexPyramid, SOFT_TEXTURE_SELECTED, SoftDraw, SoftFixedCell, SoftFixedStore, SoftFrag, + VolumeVertex, +}; +impl SoftDraw<'_> { + /// Runtime errors are observable without logging once for every pixel. + pub fn texture_report(&self, reason: u32, message: &str) { + let old = self.reports.texture.fetch_or(reason, std::sync::atomic::Ordering::Relaxed); + if old & reason == 0 { + crate::dlog!(LogModule::Odyssey, "sgi-gl: texture {}", message); + } + } +} + +pub(super) fn soft_texture_format(format: u32) -> bool { + format == GL_ALPHA || format == GL_LUMINANCE || + format == GL_LUMINANCE_ALPHA || format == GL_INTENSITY || + format == GL_RGB || format == GL_RGBA || + format == GL_DEPTH_COMPONENT +} + +pub(super) fn soft_texture_wrap(wrap: u32) -> bool { + wrap == GL_REPEAT || wrap == GL_CLAMP || + wrap == GL_CLAMP_TO_EDGE || wrap == GL_CLAMP_TO_BORDER +} + +/// OpenGL 1.2.1 table 3.16: exact identities remain distinct in a chain. +pub(super) fn soft_texture_internal_format(format: u32) -> u32 { + match format { + GL_DEPTH_COMPONENT16_SGIX | GL_DEPTH_COMPONENT24_SGIX | GL_DEPTH_COMPONENT32_SGIX => GL_DEPTH_COMPONENT, + GL_ALPHA4 | GL_ALPHA8 | GL_ALPHA12 | GL_ALPHA16 => GL_ALPHA, + GL_LUMINANCE4 | GL_LUMINANCE8 | GL_LUMINANCE12 | GL_LUMINANCE16 => GL_LUMINANCE, + GL_LUMINANCE4_ALPHA4 | GL_LUMINANCE6_ALPHA2 | GL_LUMINANCE8_ALPHA8 | + GL_LUMINANCE12_ALPHA4 | GL_LUMINANCE12_ALPHA12 | GL_LUMINANCE16_ALPHA16 => GL_LUMINANCE_ALPHA, + GL_INTENSITY4 | GL_INTENSITY8 | GL_INTENSITY12 | GL_INTENSITY16 => GL_INTENSITY, + GL_R3_G3_B2 | GL_RGB4 | GL_RGB5 | GL_RGB8 | GL_RGB10 | GL_RGB12 | GL_RGB16 => GL_RGB, + GL_RGBA2 | GL_RGBA4 | GL_RGB5_A1 | GL_RGBA8 | GL_RGB10_A2 | GL_RGBA12 | GL_RGBA16 => GL_RGBA, + _ => if soft_texture_format(format) { format } else { 0 }, + } +} + +pub(super) fn soft_texture_mipmap(filter: u32) -> bool { + (GL_NEAREST_MIPMAP_NEAREST..=GL_LINEAR_MIPMAP_LINEAR).contains(&filter) +} + +pub(super) fn soft_texture_sharpen(filter: u32) -> bool { + filter == GL_LINEAR_SHARPEN_SGIS || filter == GL_LINEAR_SHARPEN_ALPHA_SGIS || + filter == GL_LINEAR_SHARPEN_COLOR_SGIS +} + +pub(super) fn soft_texture_detail(filter: u32) -> bool { + filter == GL_LINEAR_DETAIL_SGIS || filter == GL_LINEAR_DETAIL_ALPHA_SGIS || + filter == GL_LINEAR_DETAIL_COLOR_SGIS +} + +pub(super) fn soft_texture_dimensions(w: u32, h: u32) -> bool { + w != 0 && h != 0 && w <= i32::MAX as u32 && h <= i32::MAX as u32 && + (w as u64).is_power_of_two() && (h as u64).is_power_of_two() +} + +impl<'a> GlTexPyramid<'a> { + pub fn soft_texture_level(&self, level: u32) -> Option<&'a GlTexLevel> { + self.levels.iter().find(|l| l.level == level) + } +} + +impl GlTexImage<'_> { + pub fn soft_texture_border_supported(&self, level: Option<&GlTexLevel>) -> bool { + let border = match level { Some(l) => l.border, None => self.border }; + let callback = if self.volume { + match level { + Some(l) => l.read_border_texel_3d, + None => self.read_border_texel_3d, + } + } else { + match level { + Some(l) => l.read_border_texel, + None => self.read_border_texel, + } + }; + + border <= 1 && (border == 0 || callback) + } + + pub fn soft_texture_shadow_supported(&self, format: u32) -> bool { + if (self.shadow_compare != 0 && + self.shadow_compare != GL_TEXTURE_LEQUAL_R_SGIX && + self.shadow_compare != GL_TEXTURE_GEQUAL_R_SGIX) || + (self.shadow_compare != 0 && + (!self.shadow_ambient.is_finite() || self.shadow_ambient < 0.0 || self.shadow_ambient > 1.0)) + { + return false; + } + // SGIX_depth_texture alone does not define a texture environment. + format != GL_DEPTH_COMPONENT || + (self.shadow_compare != 0 && !soft_texture_sharpen(self.mag_filter) && + !soft_texture_detail(self.mag_filter)) + } +} + +impl<'a> SoftDraw<'a> { + pub fn texture_pyramid(&mut self, im: &'a GlTexImage<'a>, report: bool) -> bool { + let p = im.pyramid.expect("an image with a pyramid"); + let mipmap = soft_texture_mipmap(im.min_filter); + let mut border_finite = true; + + #[derive(PartialEq)] + enum Out { Unsupported, Incomplete, Done } + let res = (|| -> Out { + border_finite &= im.border_color.iter().all(|c| c.is_finite()); + if p.levels.len() > GL_TEX_LEVELS_MAX as usize || + !p.min_lod.is_finite() || !p.max_lod.is_finite() || + p.min_lod > p.max_lod || !border_finite || + !soft_texture_wrap(im.wrap_s) || + (!im.one_dimensional && !soft_texture_wrap(im.wrap_t)) || + (im.volume && !soft_texture_wrap(im.wrap_r)) || + (im.min_filter != GL_NEAREST && im.min_filter != GL_LINEAR && !mipmap) || + (im.mag_filter != GL_NEAREST && im.mag_filter != GL_LINEAR && + !soft_texture_sharpen(im.mag_filter) && !soft_texture_detail(im.mag_filter)) + { + return Out::Unsupported; + } + if p.levels.windows(2).any(|w| w[0].level >= w[1].level) { + return Out::Unsupported; + } + if mipmap && p.max_level < p.base_level { + return Out::Incomplete; + } + let base = match p.soft_texture_level(p.base_level) { + Some(b) => b, + None => return Out::Incomplete, + }; + if !im.soft_texture_shadow_supported(base.base_format) || + !im.soft_texture_border_supported(Some(base)) || + soft_texture_internal_format(base.internal_format) != base.base_format || + !soft_texture_format(base.base_format) || + (self.st.texture_env_mode == GL_DECAL && + base.base_format != GL_RGB && base.base_format != GL_RGBA) + { + return Out::Unsupported; + } + if im.one_dimensional && base.height != 1 { + return Out::Unsupported; + } + if !soft_texture_dimensions(base.width, base.height) || + (im.volume && (base.depth == 0 || base.depth > i32::MAX as u32 || !(base.depth as u64).is_power_of_two())) + { + return Out::Incomplete; + } + let mut last = p.base_level; + if mipmap { + let mut extent = std::cmp::max(base.width, base.height); + + if im.volume { + extent = std::cmp::max(extent, base.depth); + } + let k = 31 - extent.leading_zeros(); + last = std::cmp::min(p.base_level as u64 + k as u64, p.max_level as u64) as u32; + } + // Unsupported relevant forms cannot be concealed by missing images. + for l in p.levels { + if l.level < p.base_level || l.level > last { + continue; + } + if im.one_dimensional && l.height != 1 { + return Out::Unsupported; + } + if !im.soft_texture_shadow_supported(l.base_format) { + return Out::Unsupported; + } + if !im.soft_texture_border_supported(Some(l)) || + !soft_texture_format(l.base_format) || + soft_texture_internal_format(l.internal_format) != l.base_format + { + return Out::Unsupported; + } + } + let mut id = p.base_level as u64; + while id <= last as u64 { + let l = p.soft_texture_level(id as u32); + let shift = (id - p.base_level as u64) as u32; + + let l = match l { Some(l) => l, None => return Out::Incomplete }; + if (if im.volume { !l.read_texel_3d } else { !l.read_texel }) || + (im.volume && l.depth != std::cmp::max(1, base.depth.wrapping_shr(shift))) || + l.width != std::cmp::max(1, base.width.wrapping_shr(shift)) || + l.height != std::cmp::max(1, base.height.wrapping_shr(shift)) || + l.internal_format != base.internal_format || + l.border != base.border + { + return Out::Incomplete; + } + id += 1; + } + self.texture = Some(im); + self.texture_base = Some(base); + self.texture_last = last; + Out::Done + })(); + match res { + Out::Done => true, + Out::Unsupported => { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: unsupported texture state in pyramid, draw dropped"); + } + false + } + Out::Incomplete => { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: incomplete texture image pyramid, texturing disabled for draw"); + } + true + } + } + } + + /// Incomplete images disable texturing; unsupported states drop the draw. + pub fn texture_setup(&mut self, report: bool) -> bool { + let im = self.t.texture.as_ref(); + let st = self.st; + + // With several targets enabled the highest dimension wins: 3D, then + // 2D, then 1D (OpenGL 1.2.1, 3.8.10). The board supplies the winning + // target's image, so the image is checked against that target only. + let target: u32 = if st.texture_3d { 3 } else if st.texture_2d { 2 } else if st.texture_1d { 1 } else { 0 }; + + if target == 0 { + return true; + } + if let Some(im) = im { + if (im.one_dimensional && im.volume) || + (target == 1) != im.one_dimensional || + (im.one_dimensional && im.pyramid.is_none() && im.height != 1) + { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: incompatible 1D texture image, draw dropped"); + } + return false; + } + } + // A volume image under the 2D enable alone stays accepted + // targets were ranked. With 1D enabled as well the 2D binding wins, + // and a volume is not a 2D image. + if target == 2 && im.is_some_and( |i| i.volume) && st.texture_1d { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: volume texture image for a 2D target, draw dropped"); + } + return false; + } + if target == 3 && im.is_some_and( |i| !i.volume) { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: 3D texturing requires a volume image, draw dropped"); + } + return false; + } + if st.texture_env_mode != GL_REPLACE && st.texture_env_mode != GL_MODULATE && + st.texture_env_mode != GL_DECAL && st.texture_env_mode != GL_BLEND + { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: unsupported texture state environment {:#x}, draw dropped", st.texture_env_mode); + } + return false; + } + if let Some(im) = im { + if im.max_clamp && + !(im.max_clamp_s >= 0.0 && im.max_clamp_s <= 1.0 && + im.max_clamp_t >= 0.0 && im.max_clamp_t <= 1.0 && + im.max_clamp_r >= 0.0 && im.max_clamp_r <= 1.0) + { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: invalid coordinate clamp, draw dropped"); + } + return false; + } + if !im.lod_scale_s.is_finite() || im.lod_scale_s < 0.0 || + (!im.one_dimensional && (!im.lod_scale_t.is_finite() || im.lod_scale_t < 0.0)) || + (im.volume && (!im.lod_scale_r.is_finite() || im.lod_scale_r < 0.0)) + { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: invalid derivative scales, draw dropped"); + } + return false; + } + if im.pyramid.is_some() { + return self.texture_pyramid(im, report); + } + let mut border_finite = true; + + border_finite &= im.border_color.iter().all(|c| c.is_finite()); + if !im.soft_texture_shadow_supported(im.base_format) || + !im.soft_texture_border_supported(None) || + !border_finite || + !soft_texture_format(im.base_format) || + !soft_texture_wrap(im.wrap_s) || + (!im.one_dimensional && !soft_texture_wrap(im.wrap_t)) || + (im.volume && !soft_texture_wrap(im.wrap_r)) || + im.min_filter != im.mag_filter || + (im.min_filter != GL_NEAREST && im.min_filter != GL_LINEAR) || + (st.texture_env_mode == GL_DECAL && im.base_format != GL_RGB && im.base_format != GL_RGBA) + { + if report { + crate::dlog!( + LogModule::Odyssey, + "sgi-gl: unsupported texture state format {:#x} border {} filters {:#x}/{:#x} wraps {:#x}/{:#x}/{:#x} environment {:#x}{}, draw dropped", + im.base_format, im.border, im.min_filter, im.mag_filter, im.wrap_s, im.wrap_t, + im.wrap_r, st.texture_env_mode, + if border_finite { "" } else { " nonfinite border colour" } + ); + } + return false; + } + } + let incomplete = match im { + None => true, + Some(im) => { + (if im.volume { !im.read_texel_3d } else { !im.read_texel }) || + im.width == 0 || im.height == 0 || + im.width > i32::MAX as u32 || im.height > i32::MAX as u32 || + !(im.width as u64).is_power_of_two() || !(im.height as u64).is_power_of_two() || + (im.volume && (im.depth == 0 || im.depth > i32::MAX as u32 || !(im.depth as u64).is_power_of_two())) + } + }; + if incomplete { + if report { + crate::dlog!(LogModule::Odyssey, "sgi-gl: incomplete texture image, texturing disabled for draw"); + } + return true; + } + self.texture = im; + true + } +} + +impl GlTexImage<'_> { + /// Whether a clipped vertex has a clip w and the texture coordinates im + /// uses: s and q always, t unless the image is one-dimensional, and r for + /// a shadow comparison against a depth image. + pub fn soft_texcoord_valid(&self, base: Option<&GlTexLevel>, v: &VolumeVertex) -> bool { + let mut valid = v.clip[3].is_finite() && v.clip[3] > 0.0 && v.attr[vtx::TEXCOORD + 3] > 0.0; + + for k in 0..4usize { + if !self.one_dimensional || k == 0 || k == 3 || + (k == 2 && self.shadow_compare != 0 && + base.map_or(self.base_format, |b| b.base_format) == GL_DEPTH_COMPONENT) + { + valid &= v.attr[vtx::TEXCOORD + k].is_finite(); + } + } + valid + } +} + +impl SoftDraw<'_> { + pub fn texture_vertex_valid(&self, v: &VolumeVertex) -> bool { + let Some(im) = self.texture else { + return true; + }; + let valid = im.soft_texcoord_valid(self.texture_base, v); + if !valid { + self.texture_report(4, "invalid clipped coordinates, primitive dropped"); + } + valid + } +} + +/// Meaningful base components, including the border, before filtering. +pub(super) fn soft_texture_components(format: u32, v: &mut [f64; 4]) { + match format { + GL_ALPHA => { + v[0] = 1.0; + v[1] = 1.0; + v[2] = 1.0; + } + GL_LUMINANCE => { + v[3] = 1.0; + v[1] = v[0]; + v[2] = v[0]; + } + GL_LUMINANCE_ALPHA => { + v[1] = v[0]; + v[2] = v[0]; + } + GL_INTENSITY => { + v[1] = v[0]; + v[2] = v[0]; + v[3] = v[0]; + } + GL_RGB => { + v[3] = 1.0; + } + _ => {} + } +} + +impl GlTexImage<'_> { + pub fn soft_texture_shadow(&self, depth: f64, reference: f64, out: &mut [f64; 4]) { + let pass = if self.shadow_compare == GL_TEXTURE_LEQUAL_R_SGIX { depth <= reference } else { depth >= reference }; + + let v = if pass { 1.0 } else { self.shadow_ambient }; + out[0] = v; + out[1] = v; + out[2] = v; + out[3] = 1.0; + } +} + +/// GL_REPEAT's wrap of a texel index into [0, n). Image and level sizes are +/// powers of two wherever setup admits them, and there the mask gives the +/// remainder without two 64-bit divisions a tap. +pub(super) fn soft_texture_repeat(i: i64, n: u32) -> i64 { + if (n as u64).is_power_of_two() { + i & (n as i64 - 1) + } else { + (i % n as i64 + n as i64) % n as i64 + } +} + +/// An 8-bit channel normalized, k / 255.0 for each byte k. +pub(super) fn soft_unorm8(k: u32) -> f64 { + k as f64 / 255.0 +} + +pub(super) const TEXEL_CACHE_SIZE: usize = 4096; + +#[derive(Clone, Copy, Default)] +pub(super) struct SoftTexelEntry { + pub serial: u64, + /// SoftDraw::texel_key() of the level + pub level: usize, + pub x: u32, + pub y: u32, + pub z: u32, + pub out: [f64; 4], +} + +/// The texels one draw has read, by level and interior index, after +/// conversion to components in [0, 1]. The board's image stays unchanged +/// through a synchronous draw (GlTexImage), so a texel read twice in one +/// draw reads the same both times. A draw starts by taking a new serial, +/// which empties the table without clearing it. Border texels, depth +/// texels and failed reads always go to the board. +pub(super) struct SoftTexelCache { + pub serial: u64, + /// Set for a fragment that minifies by more than two texels a pixel + /// without mipmaps. + pub bypass: bool, + pub tag: u32, + pub fixed: Vec, + pub entry: Vec, +} + +impl SoftTexelCache { + pub fn new() -> Self { + SoftTexelCache { + serial: 0, + bypass: false, + tag: 0, + fixed: vec![SoftFixedStore::default(); FIXED_LEVELS], + entry: vec![SoftTexelEntry::default(); TEXEL_CACHE_SIZE], + } + } + + /// Start a draw: a new serial, which empties the table. + pub fn begin(&mut self) { + self.serial += 1; + self.bypass = false; + self.tag = (self.serial & !(FIXED_FAILED as u64)) as u32; + if self.tag == 0 { + // Tags have come round: cells from 2^31 draws ago would match. + for store in self.fixed.iter_mut() { + store.cells.fill(SoftFixedCell::default()); + } + self.serial += 1; + self.tag = (self.serial & !(FIXED_FAILED as u64)) as u32; + } + } +} + +impl Default for SoftTexelCache { + fn default() -> Self { + Self::new() + } +} + +impl SoftTexelCache { + pub fn texel_slot(&self, level: usize, x: u32, y: u32, z: u32) -> usize { + let hash: u32 = x.wrapping_mul(0x9e3779b1) + ^ y.wrapping_mul(0x85ebca77) + ^ z.wrapping_mul(0xc2b2ae3d) + ^ (level as u32).wrapping_mul(0x27d4eb2f); + + ((hash ^ (hash >> 15)) as usize) & (TEXEL_CACHE_SIZE - 1) + } +} + +impl SoftDraw<'_> { + /// None is the read-failure exit. + pub fn texture_tap_inner(&self, level: Option<&GlTexLevel>, mut x: i64, mut y: i64, mut z: i64, reference: f64, out: &mut [f64; 4]) -> Option { + let im = self.image(); + let width = level.map_or(im.width, |l| l.width); + let height = level.map_or(im.height, |l| l.height); + let depth = if im.volume { level.map_or(im.depth, |l| l.depth) } else { 1 }; + let format = level.map_or(im.base_format, |l| l.base_format); + let mut slot: Option = None; + // The cache's name for the level: 0 for the image itself. + let key = level.map_or(0usize, |l| l.level as usize + 1); + + if im.wrap_s == GL_REPEAT { + x = soft_texture_repeat(x, width); + } + if !im.one_dimensional && im.wrap_t == GL_REPEAT { + y = soft_texture_repeat(y, height); + } + if im.volume && im.wrap_r == GL_REPEAT { + z = soft_texture_repeat(z, depth); + } + let rgba = if x < 0 || y < 0 || z < 0 || x >= width as i64 || y >= height as i64 || z >= depth as i64 { + let border = level.map_or(im.border, |l| l.border); + + if border == 0 { + for k in 0..4usize { + out[k] = cmin(cmax(im.border_color[k] as f64, 0.0), 1.0); + } + if format == GL_DEPTH_COMPONENT { + let o0 = out[0]; + im.soft_texture_shadow(o0, reference, out); + } else { + soft_texture_components(format, out); + } + return Some(true); + } + // Signed indices cover the stored faces, edges and corners. + if x < -1 || y < -1 || x > width as i64 || y > height as i64 || + (im.volume && (z < -1 || z > depth as i64)) + { + return None; + } + let readable = if im.volume { + level.map_or(im.read_border_texel_3d, |l| l.read_border_texel_3d) + } else { + level.map_or(im.read_border_texel, |l| l.read_border_texel) + }; + if !readable { + return None; + } + if im.volume { + self.texels.border_texel_3d(level, x as i32, y as i32, z as i32)? + } else { + self.texels.border_texel(level, x as i32, y as i32)? + } + } else { + if let Some(cache) = self.texels_cache { + let c = cache.borrow(); + + if !c.bypass && format != GL_DEPTH_COMPONENT { + let idx = c.texel_slot(key, x as u32, y as u32, z as u32); + let e = &c.entry[idx]; + if e.serial == c.serial && e.level == key && + e.x as i64 == x && e.y as i64 == y && e.z as i64 == z + { + *out = e.out; + return Some(true); + } + slot = Some(idx); + } + } + if im.volume { + self.texels.texel_3d(level, x as u32, y as u32, z as u32)? + } else { + self.texels.texel(level, x as u32, y as u32)? + } + }; + if format == GL_DEPTH_COMPONENT { + im.soft_texture_shadow(rgba as f64 / u32::MAX as f64, reference, out); + return Some(true); + } + for k in 0..4u32 { + out[k as usize] = soft_unorm8((rgba >> (24 - k * 8)) & 255); + } + soft_texture_components(format, out); + if let (Some(idx), Some(cache)) = (slot, self.texels_cache) { + let mut c = cache.borrow_mut(); + let serial = c.serial; + + c.entry[idx] = SoftTexelEntry { + serial, + level: key, + x: x as u32, + y: y as u32, + z: z as u32, + out: *out, + }; + } + Some(true) + } + + pub fn texture_tap(&self, level: Option<&GlTexLevel>, x: i64, y: i64, z: i64, reference: f64, out: &mut [f64; 4]) -> bool { + match self.texture_tap_inner(level, x, y, z, reference, out) { + Some(b) => b, + None => { + self.texture_report(1, "image-read failure, fragment dropped"); + false + } + } + } +} + +/// Reduce finite coordinates before any float-to-integer conversion. +pub(super) fn soft_texture_axis(x: f64, n: u32, wrap: u32, upper: f64) -> f64 { + let half = 0.5 / n as f64; + + match wrap { + GL_REPEAT => x - x.floor(), + GL_CLAMP_TO_EDGE => cmin(cmax(x, half), 1.0 - half), + GL_CLAMP_TO_BORDER => cmin(cmax(x, -half), 1.0 + half), + _ => cmin(cmax(x, 0.0), upper), + } +} + +impl SoftDraw<'_> { + /// Sampling always reduces coordinates before any integer conversion. + pub fn texture_sample(&self, level: Option<&GlTexLevel>, filter: u32, s: f64, t: f64, rr: f64, texel: &mut [f64; 4]) -> bool { + let im = self.image(); + let width = level.map_or(im.width, |l| l.width); + let height = level.map_or(im.height, |l| l.height); + let depth = if im.volume { level.map_or(im.depth, |l| l.depth) } else { 1 }; + + let reference = rr; + let mut rr = rr; + + *texel = [0.0; 4]; + let s = soft_texture_axis(s, width, im.wrap_s, if im.max_clamp { im.max_clamp_s } else { 1.0 }); + let t = if im.one_dimensional { + 0.0 + } else { + soft_texture_axis(t, height, im.wrap_t, if im.max_clamp { im.max_clamp_t } else { 1.0 }) + }; + if im.volume { + rr = soft_texture_axis(rr, depth, im.wrap_r, if im.max_clamp { im.max_clamp_r } else { 1.0 }); + } + if filter == GL_NEAREST { + let mut x = (width as f64 * s).floor() as i64; + let mut y = (height as f64 * t).floor() as i64; + let mut z = if im.volume { (depth as f64 * rr).floor() as i64 } else { 0 }; + + if s == 1.0 && im.wrap_s != GL_CLAMP_TO_BORDER { + x = width as i64 - 1; + } + if t == 1.0 && im.wrap_t != GL_CLAMP_TO_BORDER { + y = height as i64 - 1; + } + if rr == 1.0 && im.volume && im.wrap_r != GL_CLAMP_TO_BORDER { + z = depth as i64 - 1; + } + return self.texture_tap(level, x, y, z, reference, texel); + } + let u = width as f64 * s - 0.5; + let v = if im.one_dimensional { 0.0 } else { height as f64 * t - 0.5 }; + let x = u.floor() as i64; + let y = v.floor() as i64; + let w = depth as f64 * rr - 0.5; + let z = if im.volume { w.floor() as i64 } else { 0 }; + let a = u - x as f64; + let b = v - y as f64; + let c = if im.volume { w - z as f64 } else { 0.0 }; + + for k in 0..(if im.volume { 2 } else { 1 }) { + for j in 0..(if im.one_dimensional { 1 } else { 2 }) { + for i in 0..2 { + let weight = (if i != 0 { a } else { 1.0 - a }) * (if j != 0 { b } else { 1.0 - b }) + * (if im.volume { if k != 0 { c } else { 1.0 - c } } else { 1.0 }); + let mut tap = [0.0f64; 4]; + + if weight == 0.0 { + continue; + } + if !self.texture_tap(level, x + i as i64, y + j as i64, z + k as i64, reference, &mut tap) { + return false; + } + for channel in 0..4usize { + texel[channel] += weight * tap[channel]; + } + } + } + } + true + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct SoftTexSelection { + pub first: u32, + pub second: u32, + pub filter: u32, + pub alpha: f64, + pub lambda: f64, +} + +/// Relative levels avoid rounding a large absolute base in floating point. +pub(super) fn soft_texture_select(min: u32, mag: u32, p: &GlTexPyramid, last: u32, rho: f64, out: &mut SoftTexSelection) -> bool { + let enhanced = soft_texture_sharpen(mag) || soft_texture_detail(mag); + let c: f64 = if enhanced { + if min == GL_NEAREST_MIPMAP_NEAREST || min == GL_LINEAR_MIPMAP_NEAREST { 0.5 } else { 0.0 } + } else if mag == GL_LINEAR && (min == GL_NEAREST_MIPMAP_NEAREST || min == GL_NEAREST_MIPMAP_LINEAR) { + 0.5 + } else { + 0.0 + }; + let range: u32 = last.wrapping_sub(p.base_level); + + if !rho.is_finite() || rho < 0.0 { + return false; + } + let mut lambda = if rho != 0.0 { rho.log2() } else { p.min_lod }; + if !lambda.is_finite() { + return false; + } + lambda = cmin(cmax(lambda, p.min_lod), p.max_lod); + *out = SoftTexSelection { + first: p.base_level, + second: p.base_level, + filter: if lambda <= c { mag } else { min }, + alpha: 0.0, + lambda, + }; + if lambda <= c || !soft_texture_mipmap(min) { + return true; + } + out.filter = if min == GL_NEAREST_MIPMAP_NEAREST || min == GL_NEAREST_MIPMAP_LINEAR { + GL_NEAREST + } else { + GL_LINEAR + }; + if min == GL_NEAREST_MIPMAP_NEAREST || min == GL_LINEAR_MIPMAP_NEAREST { + let mut relative: f64; + + if lambda <= 0.5 { + relative = 0.0; + } else if lambda > range as f64 + 0.5 { + relative = range as f64; + } else { + relative = lambda.floor(); + // Adding 0.5 can round the next value above a half tie onto it. + relative += (lambda - relative > 0.5) as i32 as f64; + } + out.first = out.first.wrapping_add(cmin(cmax(relative, 0.0), range as f64) as u32); + out.second = out.first; + } else { + let relative = cmin(cmax(lambda, 0.0), range as f64); + let n = relative.floor() as u32; + + out.first = out.first.wrapping_add(n); + out.second = if out.first == last { last } else { out.first.wrapping_add(1) }; + out.alpha = relative - n as f64; + } + true +} + +/// Projective gradients precede wrapping; D is Q/W, not geometry W. +pub(super) fn soft_texture_lod_scale(scale: f64) -> f64 { + if scale != 0.0 { scale } else { 1.0 } +} + +pub(super) fn soft_texture_derivatives( + value: &[f64; 4], + dx: &[f64; 4], + dy: &[f64; 4], + ns: f64, + nt: f64, + nr: f64, + out: &mut [f64; 5], +) -> bool { + let d = value[2]; + let d2 = d * d; + + if !(d > 0.0) || !d2.is_finite() || d2 == 0.0 { + return false; + } + for k in 0..2usize { + out[2 * k] = (dx[k] * d - value[k] * dx[2]) / d2; + out[2 * k + 1] = (dy[k] * d - value[k] * dy[2]) / d2; + if !(value[k] / d).is_finite() || !out[2 * k].is_finite() || !out[2 * k + 1].is_finite() { + return false; + } + } + out[4] = cmax((ns * out[0]).hypot(nt * out[2]), (ns * out[1]).hypot(nt * out[3])); + if nr != 0.0 { + let drdx = (dx[3] * d - value[3] * dx[2]) / d2; + let drdy = (dy[3] * d - value[3] * dy[2]) / d2; + + if !(value[3] / d).is_finite() || !drdx.is_finite() || !drdy.is_finite() { + return false; + } + out[4] = cmax( + (ns * out[0]).hypot(nt * out[2]).hypot(nr * drdx), + (ns * out[1]).hypot(nt * out[3]).hypot(nr * drdy), + ); + } + out[4].is_finite() +} + +// The diagonal level of detail of a filled triangle's quads. +// +// INFERRED from US 6,232,981 B1: the texture unit picks one level of +// detail for each 2x2 pixel quad. It takes the s and t differences along +// the quad's two diagonals, pixel 1 to pixel 4 and pixel 2 to pixel 3, +// takes the largest absolute difference and returns its base-2 logarithm; +// all four pixels share it. The rule covers 1D and 2D textures only, so a +// 3D texture keeps the per-fragment footprint (soft_texture_derivatives()). +// The LOD bias, minimum and maximum then apply as their registers give them +// (Board::texture_lod_bias(), texture_lod_limit()): the bias scales each +// axis's differences, here through ns and nt, and the limits clamp the +// logarithm (soft_texture_select(), SoftDraw::fixed_sample()). +// +// Model choices, where the board's behaviour is open: +// - The internal LOD precision is unknown. The model takes the +// differences in double; the general path takes their logarithm in +// double, the fixed-point path in 1/256 of a level, the eight fraction +// bits of the LOD registers, through its table (soft_fixed_lambda()). +// - Quads are the window's even-aligned pixel pairs on even-aligned row +// pairs. Whether the board aligns them to the window or to the screen +// is unknown. +// - A quad's pixels outside the primitive take part in the difference at +// the planes' values there and are not drawn. A pixel whose Q/W is not +// positive, or whose s or t is not finite, has no coordinates: a +// diagonal with one is left out, and a quad with neither diagonal left +// has no level of detail of its own, so each of its fragments takes its +// own footprint. + +/// The texture coordinates of the four pixels of the quad whose first pixel +/// is window pixel (qx, qy), from a triangle's texture planes g: pixels 1 +/// and 2 on the quad's first row, 3 and 4 on its second. s and t are +/// meaningful where pos is set (Q/W positive). A quad loop takes the +/// footprint (soft_quad_rho_of()) and each fragment's coordinates from one +/// of these, not from a division each. +#[derive(Clone, Copy)] +pub(super) struct SoftQuadCoords { + pub s: [f64; 4], + pub t: [f64; 4], + pub pos: [bool; 4], +} + +pub(super) fn soft_quad_coords(g: &SoftTexPlane, qx: i32, qy: i32) -> SoftQuadCoords { + let mut c = SoftQuadCoords { s: [0.0; 4], t: [0.0; 4], pos: [false; 4] }; + + for i in 0..4 { + let x = (qx as f64 + (i & 1) as f64) + 0.5 - g.x; + let y = (qy as f64 + (i >> 1) as f64) + 0.5 - g.y; + let q = g.value[2] + x * g.dx[2] + y * g.dy[2]; + let iq = 1.0 / q; + + c.s[i] = (g.value[0] + x * g.dx[0] + y * g.dy[0]) * iq; + c.t[i] = (g.value[1] + x * g.dx[1] + y * g.dy[1]) * iq; + c.pos[i] = q > 0.0; + } + c +} + +/// The footprint, in texels, of the quad whose coordinates are c under the +/// diagonal rule above: the largest absolute difference along the diagonals +/// of s scaled by ns and of t scaled by nt (the base level's size times the +/// axis's bias, as soft_texture_derivatives() scales them). None where +/// neither diagonal has coordinates at both ends. +pub(super) fn soft_quad_rho_of(c: &SoftQuadCoords, ns: f64, nt: f64) -> Option { + let ok = |i: usize| c.pos[i] && c.s[i].is_finite() && c.t[i].is_finite(); + let mut rho = f64::NAN; + + for (a, b) in [(0, 3), (1, 2)] { + if ok(a) && ok(b) { + let d = cmax((ns * (c.s[b] - c.s[a])).abs(), (nt * (c.t[b] - c.t[a])).abs()); + + rho = if rho.is_nan() { d } else { cmax(rho, d) }; + } + } + rho.is_finite().then_some(rho) +} + +/// The footprint of the quad whose first pixel is window pixel (qx, qy), from +/// a triangle's texture planes g (soft_quad_coords(), soft_quad_rho_of()). +pub(super) fn soft_quad_rho(g: &SoftTexPlane, ns: f64, nt: f64, qx: i32, qy: i32) -> Option { + soft_quad_rho_of(&soft_quad_coords(g, qx, qy), ns, nt) +} + +/// soft_quad_rho() for planes g whose Q/W is the same everywhere, where it +/// is the same for every quad: the planes' slopes along the diagonals over +/// Q/W. None where Q/W is not positive or the footprint is not finite. +pub(super) fn soft_quad_flat_rho(g: &SoftTexPlane, ns: f64, nt: f64) -> Option { + let q = g.value[2]; + + if !(q > 0.0) { + return None; + } + let iq = 1.0 / q; + let rho = cmax( + cmax((ns * (g.dx[0] + g.dy[0]) * iq).abs(), (ns * (g.dy[0] - g.dx[0]) * iq).abs()), + cmax((nt * (g.dx[1] + g.dy[1]) * iq).abs(), (nt * (g.dy[1] - g.dx[1]) * iq).abs()), + ); + rho.is_finite().then_some(rho) +} + +/// The last level selection a thread's draw made, by the footprint it was +/// made for. +#[derive(Clone, Copy, Default)] +pub(super) struct SoftTexSelectionMemo { + pub serial: u64, + pub rho: u64, + pub selected: SoftTexSelection, +} + +impl SoftDraw<'_> { + pub fn texture_select_memo(&self, rho: f64, out: &mut SoftTexSelection) -> bool { + let im = self.image(); + let serial = self.texels_cache.map(|c| c.borrow().serial); + let bits = rho.to_bits(); + + if let Some(serial) = serial { + let m = SOFT_TEXTURE_SELECTED.with(|c| c.get()); + if m.serial == serial && m.rho == bits { + *out = m.selected; + return true; + } + } + if !soft_texture_select(im.min_filter, im.mag_filter, &im.pyramid.expect("a pyramid"), self.texture_last, rho, out) { + return false; + } + if let Some(serial) = serial { + SOFT_TEXTURE_SELECTED.with(|c| c.set(SoftTexSelectionMemo { serial, rho: bits, selected: *out })); + } + true + } +} + +pub(super) fn soft_texture_mix(value: &mut [f64; 4], other: &[f64; 4], alpha: f64) { + for k in 0..4usize { + value[k] = (1.0 - alpha) * value[k] + alpha * other[k]; + } +} + +impl SoftDraw<'_> { + pub fn texture_weight(&self, lambda: f64) -> Option { + let im = self.image(); + + if im.read_filter_weight { + match self.texels.filter_weight(lambda) { + Some(w) if w.is_finite() => return Some(w), + _ => { + self.texture_report(16, "invalid filter weight, fragment dropped"); + return None; + } + } + } + Some(cmin(cmax(-lambda / 4.0, 0.0), 1.0)) + } + + pub fn texture_sharpen_sample(&self, filter: u32, lambda: f64, s: f64, t: f64, rr: f64, texel: &mut [f64; 4]) -> bool { + let im = self.image(); + let p = im.pyramid.expect("a pyramid"); + let base = self.texture_base.expect("a base level"); + let mut extent = std::cmp::max(base.width, base.height); + let mut other = [0.0f64; 4]; + + if !self.texture_sample(Some(base), GL_LINEAR, s, t, rr, texel) { + return false; + } + if im.volume { + extent = std::cmp::max(extent, base.depth); + } + let coarse = if p.base_level < u32::MAX { p.soft_texture_level(p.base_level + 1) } else { None }; + let coarse = match coarse { + Some(c) if !(extent <= 1 || p.max_level <= p.base_level || + c.width != std::cmp::max(1, base.width / 2) || + c.height != std::cmp::max(1, base.height / 2) || + (im.volume && c.depth != std::cmp::max(1, base.depth / 2)) || + c.internal_format != base.internal_format || + c.base_format != base.base_format || + c.border != base.border || + !im.soft_texture_border_supported(Some(c)) || + (if im.volume { !c.read_texel_3d } else { !c.read_texel })) => c, + _ => return true, + }; + let Some(weight) = self.texture_weight(lambda) else { + return false; + }; + if !self.texture_sample(Some(coarse), GL_LINEAR, s, t, rr, &mut other) { + return false; + } + for k in 0..4usize { + if (k == 3 && filter == GL_LINEAR_SHARPEN_COLOR_SGIS) || + (k != 3 && filter == GL_LINEAR_SHARPEN_ALPHA_SGIS) + { + continue; + } + texel[k] = cmin(cmax(texel[k] + weight * (texel[k] - other[k]), 0.0), 1.0); + } + true + } +} + +/// scalbn: x * 2^n, exact. +pub(super) fn scalbn(x: f64, n: i32) -> f64 { + if x == 0.0 || !x.is_finite() { + return x; + } + let mut x = x; + let mut n = n.clamp(-3000, 3000); + while n > 1000 { + x *= 2f64.powi(1000); + n -= 1000; + } + while n < -1000 { + x *= 2f64.powi(-1000); + n += 1000; + } + x * 2f64.powi(n) +} + +impl SoftDraw<'_> { + pub fn texture_detail_sample(&self, filter: u32, lambda: f64, s: f64, t: f64, rr: f64, texel: &mut [f64; 4]) -> bool { + let im = self.image(); + let base = self.texture_base.expect("a base level"); + let mut repeat = *im; + let mut other = [0.0f64; 4]; + let scale: i32 = std::cmp::min(-(im.detail_level as i64), i32::MAX as i64) as i32; + + if !self.texture_sample(Some(base), GL_LINEAR, s, t, rr, texel) { + return false; + } + let detail = match im.detail_image { + Some(d) if !(im.one_dimensional || im.volume || im.pyramid.is_none_or(|p| p.base_level != 0) || + d.level != 0 || d.border != 0 || + !d.read_texel || + !soft_texture_dimensions(d.width, d.height) || + d.internal_format != base.internal_format || + d.base_format != base.base_format || im.detail_level > 0) => d, + _ => return true, + }; + if im.detail_mode != GL_ADD && im.detail_mode != GL_MODULATE { + self.texture_report(32, "unsupported detail mode, fragment dropped"); + return false; + } + // Scaling is in primary texels, independent of the detail dimensions. + let ds = scalbn(s * base.width as f64 / detail.width as f64, scale); + let dt = scalbn(t * base.height as f64 / detail.height as f64, scale); + if !ds.is_finite() || !dt.is_finite() { + self.texture_report(32, "invalid detail coordinates, fragment dropped"); + return false; + } + repeat.wrap_s = GL_REPEAT; + repeat.wrap_t = GL_REPEAT; + // The detail image is not the draw's image: its texels are not cached. + let mut draw = *self; + draw.texture = Some(&repeat); + draw.texels_cache = None; + let Some(weight) = self.texture_weight(lambda) else { + return false; + }; + if !draw.texture_sample(Some(&detail), GL_LINEAR, ds, dt, 0.0, &mut other) { + return false; + } + for k in 0..4usize { + let signed_detail = weight * (2.0 * other[k] - 1.0); + + if (k == 3 && filter == GL_LINEAR_DETAIL_COLOR_SGIS) || + (k != 3 && filter == GL_LINEAR_DETAIL_ALPHA_SGIS) + { + continue; + } + texel[k] = if im.detail_mode == GL_ADD { + texel[k] + signed_detail + } else { + texel[k] * (1.0 + signed_detail) + }; + texel[k] = cmin(cmax(texel[k], 0.0), 1.0); + } + true + } + + /// A filtered texel through the image's post-filter, if it has one, and + /// then by the texture environment env into a fragment's colour rgba, + /// for an image of base format format (OpenGL 1.2.1, 3.8.9). + #[inline(always)] + pub fn texture_apply(&self, format: u32, env: u32, texel: &mut [f64; 4], rgba: &mut [f64; 4]) { + let im = self.image(); + let mut format = format; + + if im.post_filter { + self.texels.post_filter(texel); + } + if format == GL_DEPTH_COMPONENT { + format = GL_LUMINANCE; + } + for k in 0..4usize { + let alpha = k == 3; + + if (!alpha && format == GL_ALPHA) || + (alpha && (format == GL_RGB || format == GL_LUMINANCE)) + { + continue; + } + match env { + GL_REPLACE => rgba[k] = texel[k], + GL_MODULATE => rgba[k] *= texel[k], + GL_DECAL => { + if !alpha { + rgba[k] = if format == GL_RGB { + texel[k] + } else { + rgba[k] * (1.0 - texel[3]) + texel[k] * texel[3] + }; + } + } + GL_BLEND => { + rgba[k] = if alpha && format != GL_INTENSITY { + rgba[k] * texel[k] + } else { + rgba[k] * (1.0 - texel[k]) + + cmin(cmax(self.st.texture_env_color[k] as f64, 0.0), 1.0) * texel[k] + }; + } + _ => {} + } + } + } + + pub fn texture(&self, frag: &mut SoftFrag) -> bool { + let im = self.image(); + let st: &GlDrawState = self.st; + let base = self.texture_base; + let format = base.map_or(im.base_format, |b| b.base_format); + let mut texel = [0.0f64; 4]; + let mut rr = 0.0f64; + let q = frag.texcoord[3]; + + if !(q > 0.0) || !q.is_finite() { + self.texture_report(2, "invalid projected coordinates, fragment dropped"); + return false; + } + let s = frag.texcoord[0] / q; + let t = if im.one_dimensional { 0.0 } else { frag.texcoord[1] / q }; + if im.volume || (format == GL_DEPTH_COMPONENT && im.shadow_compare != 0) { + rr = frag.texcoord[2] / q; + } + if !s.is_finite() || !t.is_finite() || !rr.is_finite() || + (format == GL_DEPTH_COMPONENT && (rr < 0.0 || rr > 1.0)) + { + self.texture_report(2, "invalid projected coordinates, fragment dropped"); + return false; + } + if let Some(base) = base { + let mut selected = SoftTexSelection::default(); + let mut other = [0.0f64; 4]; + + if !self.texture_select_memo(frag.texture_rho, &mut selected) { + self.texture_report(8, "invalid footprint, fragment dropped"); + return false; + } + let pyramid = im.pyramid.expect("a pyramid"); + let mut level = pyramid.soft_texture_level(selected.first); + if let Some(cache) = self.texels_cache { + cache.borrow_mut().bypass = selected.lambda > 1.0 && !soft_texture_mipmap(im.min_filter); + } + if soft_texture_sharpen(selected.filter) { + if !self.texture_sharpen_sample(selected.filter, selected.lambda, s, t, rr, &mut texel) { + return false; + } + } else if soft_texture_detail(selected.filter) { + if !self.texture_detail_sample(selected.filter, selected.lambda, s, t, rr, &mut texel) { + return false; + } + } else if !self.texture_sample(level, selected.filter, s, t, rr, &mut texel) { + return false; + } + if selected.alpha != 0.0 && selected.first != selected.second { + level = pyramid.soft_texture_level(selected.second); + if !self.texture_sample(level, selected.filter, s, t, rr, &mut other) { + return false; + } + soft_texture_mix(&mut texel, &other, selected.alpha); + } + let _ = base; + } else if !self.texture_sample(None, im.min_filter, s, t, rr, &mut texel) { + return false; + } + self.texture_apply(format, st.texture_env_mode, &mut texel, &mut frag.rgba); + true + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct SoftTexPlane { + /// S/W, T/W, Q/W and, for volumes, R/W. + pub value: [f64; 4], + pub dx: [f64; 4], + pub dy: [f64; 4], + pub x: f64, + pub y: f64, +} diff --git a/src/dev/odyssey/raster_triangle.rs b/src/dev/odyssey/raster_triangle.rs new file mode 100644 index 000000000..b36321a0e --- /dev/null +++ b/src/dev/odyssey/raster_triangle.rs @@ -0,0 +1,1392 @@ +//! Triangles and polygons: clipping against the view volume and the user +//! clip planes, the texture-coordinate planes across a triangle, edge +//! walking and spans, antialiased polygon coverage, and polygon modes. +//! +//! Ported from hw/display/sgi/rasterizer.c in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use super::gl_context::{ + report_w_plane_clip, vtx, GL_BACK, GL_CLIP_PLANES, GL_CW, GL_DRAW_OK, GL_DRAW_OUT_OF_MEMORY, GL_FILL, GL_FLAT, + GL_FRONT, GL_FRONT_AND_BACK, GL_LINE, GL_OFFSET_FILL, GL_OFFSET_LINE, GL_OFFSET_POINT, + GL_POINT, GL_POLYGON, GL_QUADS, GL_QUAD_STRIP, GL_TRIANGLES, GL_TRIANGLE_FAN, + GL_TRIANGLE_STRIP, GlDraw, GlDrawResult, GlDrawState, +}; +use super::raster::{cmax, cmin, SoftDraw, SoftFrag, SoftVertex, VOLUME_TRIANGLE_MAX, VolumeVertex}; +use super::raster_fill::{fill_quads_dispatch, FillRow, QuadGeneral, QuadRow}; +use super::raster_fixed_texture::{FIXED_ANY, SoftTexSpan}; +use super::raster_line::{soft_dimension, SoftLineStipple}; +use super::raster_texture::{ + soft_quad_flat_rho, soft_quad_rho, soft_texture_derivatives, soft_texture_lod_scale, + SoftTexPlane, +}; +use crate::dev::gl::math::xform; +use crate::dev::gl::vertex::{Clip, ClipVertex, NearW, CLIP_PLANES}; + +/// Clip triangle in_ by clip under the shared VPro geometry (`NearW`): the +/// polygon left, and its vertex count, 0 when less than a triangle is. +pub(super) fn volume_clip_triangle(clip: &Clip, in_: &[VolumeVertex; 3]) -> ([VolumeVertex; VOLUME_TRIANGLE_MAX], usize) { + let mut out = [VolumeVertex::default(); VOLUME_TRIANGLE_MAX]; + + out[..3].copy_from_slice(in_); + let any = in_.iter().fold(0, |o, v| o | clip.outcode_by::(v)); + if any == 0 { + return (out, 3); + } + // Only a triangle that crosses a plane needs the workspace. + if any & NearW::W_PLANE != 0 { + report_w_plane_clip(); + } + let mut tmp = [VolumeVertex::default(); VOLUME_TRIANGLE_MAX]; + let (n, _) = clip.clip_polygon_by::(&mut out, &mut tmp, 3, any); + (out, if n < 3 { 0 } else { n }) +} + +impl GlDrawState { + /// The planes geometry is clipped by, under the shared VPro geometry + /// (`NearW`): x and y to the guard band about the window, w to the + /// near-w plane, and no near or far plane, since the raster drops a + /// fragment by its depth (inferred). With user, the enabled user clip + /// planes too, which polygon modes clip their outline by; filled + /// triangles, lines and points test them per fragment instead. How the + /// board applies a user plane is unknown, so both are model choices. + pub fn volume_clip(&self, user: bool) -> Clip { + Clip { user: self.clip_plane, user_on: if user { self.clip_planes } else { 0 } } + } + + /// A draw vertex in eye coordinates, in single precision through the + /// shared transform. + pub fn draw_vertex_eye(&self, v: &[f32]) -> [f32; 4] { + xform(&self.modelview, v[vtx::POSITION..vtx::POSITION + 4].try_into().expect("a position")) + } + + /// One vertex to clip coordinates (OpenGL 1.2.1, 2.10), in single + /// precision through the shared transform: object to eye by the + /// modelview and eye to clip by the projection, or by the window matrix + /// as GlDrawState::eye_to_clip() takes it. Clipping then works in single precision + /// on the result. + pub fn volume_from_draw_vertex(&self, v: &[f32]) -> VolumeVertex { + let eye = self.draw_vertex_eye(v); + + VolumeVertex { + clip: self.eye_to_clip(eye), + eye, + attr: v[..vtx::FLOATS].try_into().expect("a whole vertex"), + } + } +} + +impl SoftVertex { + pub fn texture_values(&self, volume: bool, one_dimensional: bool) -> [f64; 4] { + [ + self.texcoord[0] * self.inv_w, + if one_dimensional { 0.0 } else { self.texcoord[1] * self.inv_w }, + self.texcoord[3] * self.inv_w, + if volume { self.texcoord[2] * self.inv_w } else { 0.0 }, + ] + } +} + +/// The texture-coordinate planes across triangle v, or None where they are +/// not finite. A triangle of no area has flat planes at its first vertex. +pub(super) fn soft_texture_plane(v: &[SoftVertex], sub: u32, volume: bool, one_dimensional: bool) -> Option { + let scale = (1i32 << sub) as f64; + let x1 = (v[1].x - v[0].x) as f64 / scale; + let y1 = (v[1].y - v[0].y) as f64 / scale; + let x2 = (v[2].x - v[0].x) as f64 / scale; + let y2 = (v[2].y - v[0].y) as f64 / scale; + let area = x1 * y2 - x2 * y1; + let mut g = SoftTexPlane { x: v[0].x as f64 / scale, y: v[0].y as f64 / scale, ..Default::default() }; + + if area == 0.0 { + return Some(g); + } + if !area.is_finite() { + return None; + } + let values: [[f64; 4]; 3] = std::array::from_fn(|i| v[i].texture_values(volume, one_dimensional)); + for k in 0..4 { + let a = values[1][k] - values[0][k]; + let b = values[2][k] - values[0][k]; + + g.value[k] = values[0][k]; + g.dx[k] = (a * y2 - b * y1) / area; + g.dy[k] = (b * x1 - a * x2) / area; + if !g.value[k].is_finite() || !g.dx[k].is_finite() || !g.dy[k].is_finite() { + return None; + } + } + Some(g) +} + +impl SoftDraw<'_> { + pub fn texture_coefficients(&self, v: &[SoftVertex]) -> bool { + if self.texture_base.is_none() + || soft_texture_plane(v, self.rules.subpixel_bits as u32, self.image().volume, self.image().one_dimensional).is_some() + { + return true; + } + self.texture_report(16, "invalid primitive coefficients, primitive dropped"); + false + } + + /// Map and rasterize one triangle of a clipped polygon, given its three + /// vertices in the view volume and, for flat shading, the provoking vertex + /// of the triangle it was clipped from. + pub fn clipped_triangle(&self, va: &VolumeVertex, vb: &VolumeVertex, vc: &VolumeVertex, provoking: &[f32]) { + let st = self.st; + let verts: [&VolumeVertex; 3] = [va, vb, vc]; + let mut flat = &provoking[vtx::COLOR..vtx::COLOR + 4]; + let mut flat_frag = SoftFrag::default(); + let mut v: [SoftVertex; 3] = Default::default(); + let mut gradient = SoftTexPlane::default(); + // The footprint's per-axis scales, and its value where Q/W is flat. + let mut tex_ns = 0.0f64; + let mut tex_nt = 0.0f64; + let mut tex_nr = 0.0f64; + let mut flat_rho = f64::NAN; + let mut flat_q = false; + let mut flat_ok = false; + // Fragments sample in fixed point, and their lambda where it is flat. + let mut fixed_flat: Option = None; + // The row loop for textured fragments, and its inputs. + let mut span: Option<(FillRow, SoftTexSpan)> = None; + let mut area: i64; + let smooth = st.shade_model != GL_FLAT; + let sub = self.rules.subpixel_bits as i32; + + for i in 0..3 { + let Some(p) = self.project(verts[i]) else { + return; + }; + v[i] = p; + } + + area = v[0].edge(&v[1], v[2].x, v[2].y); + if area == 0 { + return; + } + if self.texture_base.is_some() { + let Some(g) = soft_texture_plane(&v, sub as u32, self.image().volume, self.image().one_dimensional) else { + self.texture_report(16, "invalid primitive coefficients, primitive dropped"); + return; + }; + gradient = g; + } + // An image with no pyramid has no planes to fail: it samples in double. + let fixed: bool = self.fixed.is_some() + && (self.texture_base.is_some() + || soft_texture_plane(&v, sub as u32, false, false).inspect(|&g| gradient = g).is_some()); + if self.texture_base.is_some() { + tex_ns = self.base_level().width as f64 * soft_texture_lod_scale(self.image().lod_scale_s); + tex_nt = if self.image().one_dimensional { + 0.0 + } else { + self.base_level().height as f64 * soft_texture_lod_scale(self.image().lod_scale_t) + }; + tex_nr = if self.image().volume { + self.base_level().depth as f64 * soft_texture_lod_scale(self.image().lod_scale_r) + } else { + 0.0 + }; + // With Q/W the same at every vertex the footprint is one number: + // the same for every quad under the diagonal rule + // (soft_quad_flat_rho()), and for every fragment of a volume. + flat_q = gradient.dx[2] == 0.0 && gradient.dy[2] == 0.0; + if flat_q && !self.image().volume { + (flat_ok, flat_rho) = match soft_quad_flat_rho(&gradient, tex_ns, tex_nt) { + Some(rho) => (true, rho), + None => (false, f64::NAN), + }; + } else if flat_q { + let value = [0.0, 0.0, gradient.value[2], 0.0]; + let mut out = [0.0f64; 5]; + + flat_ok = soft_texture_derivatives(&value, &gradient.dx, &gradient.dy, tex_ns, tex_nt, tex_nr, &mut out); + flat_rho = out[4]; + } + } + if fixed && self.fixed_tex().lod && flat_q && flat_ok { + // Every quad's lambda is this one, which SoftFixedTex::quad_lambda() + // would take from each quad's footprint. + fixed_flat = self.fixed_tex().quad_lambda(flat_rho); + } + let ccw: bool = if st.window_matrix_given && st.window_rows_down { area < 0 } else { area > 0 }; + let front: bool = if st.front_face == GL_CW { !ccw } else { ccw }; + if st.cull_face + && (st.cull_face_mode == GL_FRONT_AND_BACK + || st.cull_face_mode == (if front { GL_FRONT } else { GL_BACK })) + { + return; + } + if !front && self.back_colors { + flat = &provoking[vtx::BACK_COLOR..vtx::BACK_COLOR + 4]; + for i in 0..3 { + let back = &verts[i].attr[vtx::BACK_COLOR..vtx::BACK_COLOR + 4]; + + for k in 0..4 { + // max then min, not clamp: a NaN colour becomes 0. + #[allow(clippy::manual_clamp, reason = "clamp would keep a NaN")] + let c = back[k].max(0.0).min(1.0); + v[i].rgba[k] = c as f64; + } + } + } + for k in 0..4 { + flat_frag.rgba[k] = flat[k] as f64; + } + flat_frag.fixed = false; + let offset: f64 = self.offset(&v); + if area < 0 { + // Rasterize either winding the same way from here. + v.swap(1, 2); + area = -area; + } + + let min_x = v[0].x.min(v[1].x.min(v[2].x)) >> sub; + let max_x = v[0].x.max(v[1].x.max(v[2].x)) >> sub; + let min_y = v[0].y.min(v[1].y.min(v[2].y)) >> sub; + let max_y = v[0].y.max(v[1].y.max(v[2].y)) >> sub; + + // Window pixels, bounded by the viewport and the owned rectangle. + let Some((mut px0, mut py0, mut px1, mut py1)) = self.bounds() else { + return; + }; + px0 = (min_x as i32).max(px0); + px1 = (max_x as i32).min(px1); + py0 = (min_y as i32).max(py0); + py1 = (max_y as i32).min(py1); + + // Each edge function is linear in x, so across a row it steps by an + // exact integer; solving for the ends of each edge's run of owned + // centres visits only the pixels a test of every centre would keep. + let step0: i64 = -(v[2].y - v[1].y) * (1i64 << sub); + let step1: i64 = -(v[0].y - v[2].y) * (1i64 << sub); + let step2: i64 = -(v[1].y - v[0].y) * (1i64 << sub); + // The row loop: the fixed sampler's for a textured triangle, + // SoftDraw::fill_select()'s for an untextured one. + let row_loop = if fixed { + self.fixed.and_then(|f| f.span) + } else if self.texture.is_none() { + self.fill + } else { + None + }; + if let Some(row_loop) = row_loop { + let mut sp = SoftTexSpan { + v: &v, + g: &gradient, + flat_lambda: fixed_flat, + flat: &flat_frag, + offset, + area, + step1, + step2, + smooth, + constant: 0, + }; + for k in 0..4 { + let c = v[0].rgba[k]; + + if (c == 0.0 || c == 1.0) && v[1].rgba[k] == c && v[2].rgba[k] == c { + sp.constant |= 1u8 << k; + } + } + span = Some((row_loop, sp)); + } + // The quad's footprint under the diagonal rule, for a 1D or 2D + // texture whose Q/W varies (soft_quad_rho()); a flat Q/W has one for + // every quad, and a volume's fragments take their own. + let quad_lod = self.texture_base.is_some() && !self.image().volume && !flat_q; + let quad_rho = |qx: i32, qy: i32| -> Option { + if quad_lod { soft_quad_rho(&gradient, tex_ns, tex_nt, qx, qy) } else { None } + }; + // One fragment through the general path, with its quad's footprint + // rho: every fragment a quad loop does not draw, and every fragment + // of a triangle with none. + let general = |px: i32, py: i32, e1: i64, e2: i64, rho: Option| { + let mut tex_row = [0.0f64; 4]; + + // Window phase precedes plane translation and fragment work. + let Some(background) = self.stipple_pass(px, py) else { + return; + }; + if self.texture_base.is_some() || fixed { + for k in 0..4 { + tex_row[k] = (py as f64 + 0.5 - gradient.y) * gradient.dy[k]; + } + } + let mut frag = flat_frag; + let b1 = e1 as f64 / area as f64; + let b2 = e2 as f64 / area as f64; + let z = v[0].z + b1 * (v[1].z - v[0].z) + b2 * (v[2].z - v[0].z); + let w0 = (1.0 - b1 - b2) * v[0].inv_w; + let w1 = b1 * v[1].inv_w; + let w2 = b2 * v[2].inv_w; + let sum = w0 + w1 + w2; + let depth_w = (1.0 - b1 - b2) * v[0].depth_w + b1 * v[1].depth_w + b2 * v[2].depth_w; + if fixed && span.is_none() { + let lambda = if !self.fixed_tex().lod { + None + } else if fixed_flat.is_some() { + fixed_flat + } else { + rho.and_then(|rho| self.fixed_tex().quad_lambda(rho)) + }; + frag.fixed = self + .fixed_tex() + .fixed_coords(&gradient, &tex_row, px, lambda) + .is_some_and(|(s, t, lambda)| { + SoftDraw::fixed_sample::(self, s, t, lambda, &mut frag.texel) + }); + } + // Only texturing reads the coordinates, only fog the distance. + if self.texture.is_some() && !frag.fixed { + for k in 0..4 { + frag.texcoord[k] = (w0 * v[0].texcoord[k] + w1 * v[1].texcoord[k] + w2 * v[2].texcoord[k]) / sum; + } + } + if st.clip_planes != 0 { + let mut d = [0.0f64; GL_CLIP_PLANES as usize]; + + for i in 0..GL_CLIP_PLANES as usize { + d[i] = (w0 * v[0].clip[i] + w1 * v[1].clip[i] + w2 * v[2].clip[i]) / sum; + } + if st.clipped(&d) { + return; + } + } + if smooth { + for k in 0..4 { + frag.rgba[k] = (w0 * v[0].rgba[k] + w1 * v[1].rgba[k] + w2 * v[2].rgba[k]) / sum; + } + } + if self.texture_base.is_some() && !frag.fixed { + let mut value = [0.0f64; 4]; + let mut out = [0.0f64; 5]; + + for k in 0..4 { + value[k] = gradient.value[k] + (px as f64 + 0.5 - gradient.x) * gradient.dx[k] + tex_row[k]; + } + if flat_q { + let d = value[2]; + + frag.texture_rho = if flat_ok + && (value[0] / d).is_finite() + && (value[1] / d).is_finite() + && (tex_nr == 0.0 || (value[3] / d).is_finite()) + { + flat_rho + } else { + f64::NAN + }; + } else if let Some(rho) = rho { + frag.texture_rho = rho; + } else { + frag.texture_rho = if soft_texture_derivatives(&value, &gradient.dx, &gradient.dy, tex_ns, tex_nt, tex_nr, &mut out) { + out[4] + } else { + f64::NAN + }; + } + } + if background { + self.stipple_background(&mut frag); + } + self.fragment(px, py, z, offset, depth_w, &frag); + }; + let quad_general = QuadGeneral { + scales: quad_lod.then_some((tex_ns, tex_nt)), + fragment: &|px, py, e1, e2| general(px, py, e1, e2, quad_rho(px & !1, py & !1)), + }; + // Two rows at a time, from an even one: the rows of a line of quads. + let mut qy = py0 & !1; + while qy <= py1 { + let mut rows: [Option; 2] = [None, None]; + + for (r, row) in rows.iter_mut().enumerate() { + let py = qy + r as i32; + + if py < py0 || py > py1 { + continue; + } + let cy: i64 = (py as i64 * (1i64 << sub)) + (1i64 << (sub - 1)); + let cx: i64 = (px0 as i64 * (1i64 << sub)) + (1i64 << (sub - 1)); + let e0 = v[1].edge(&v[2], cx, cy); + let e1 = v[2].edge(&v[0], cx, cy); + let e2 = v[0].edge(&v[1], cx, cy); + let mut lo: i64 = 0; + let mut hi: i64 = px1 as i64 - px0 as i64; + (lo, hi) = v[1].owned_run(&v[2], e0, step0, (lo, hi)); + (lo, hi) = v[2].owned_run(&v[0], e1, step1, (lo, hi)); + (lo, hi) = v[0].owned_run(&v[1], e2, step2, (lo, hi)); + if lo <= hi { + *row = Some(QuadRow { px0: px0 + lo as i32, px1: px0 + hi as i32, e1: e1 + lo * step1, e2: e2 + lo * step2 }); + } + } + let quad_y = qy; + qy += 2; + if rows == [None, None] { + continue; + } + if let Some((row_loop, sp)) = &span { + fill_quads_dispatch(self.px, self, *row_loop, sp, quad_y, &rows, &quad_general); + continue; + } + let (x_lo, x_hi) = match rows { + [Some(a), Some(b)] => (a.px0.min(b.px0), a.px1.max(b.px1)), + [Some(a), None] | [None, Some(a)] => (a.px0, a.px1), + [None, None] => continue, + }; + let mut qx = x_lo & !1; + while qx <= x_hi { + let rho = quad_rho(qx, quad_y); + + for (r, row) in rows.iter().enumerate() { + let Some(q) = row else { + continue; + }; + for px in qx..qx + 2 { + if px >= q.px0 && px <= q.px1 { + let k = (px - q.px0) as i64; + + general(px, quad_y + r as i32, q.e1 + k * step1, q.e2 + k * step2, rho); + } + } + } + qx += 2; + } + } + } + + /// Transform, clip and rasterize one triangle. Clipped in clip + /// coordinates (GlDrawState::volume_clip()), what is left is drawn as a fan from its first + /// vertex, each flat shaded from the triangle's provoking vertex. + /// The polygon left and its vertex count, or None when a textured + /// piece is invalid. + pub fn clip_triangle(&self, va: &[f32], vb: &[f32], vc: &[f32]) -> Option<([VolumeVertex; VOLUME_TRIANGLE_MAX], usize)> { + let in_ = [va, vb, vc].map(|v| self.st.volume_from_draw_vertex(v)); + let (poly, n) = volume_clip_triangle(&self.st.volume_clip(false), &in_); + + if !poly[..n].iter().all(|v| self.texture_vertex_valid(v)) { + return None; + } + if self.texture_base.is_some() { + let mut i = 1usize; + while i + 1 < n { + let vertices: [&VolumeVertex; 3] = [&poly[0], &poly[i], &poly[i + 1]]; + let mut projected: [SoftVertex; 3] = Default::default(); + + for j in 0..3 { + projected[j] = self.project(vertices[j])?; + } + if !self.texture_coefficients(&projected) { + return None; + } + i += 1; + } + } + Some((poly, n)) + } + + /// A quad or polygon validates all its clipped pieces before any are drawn. + pub fn texture_triangle_valid(&self, a: &[f32], b: &[f32], c: &[f32]) -> bool { + self.clip_triangle(a, b, c).is_some() + } + + pub fn triangle(&self, va: &[f32], vb: &[f32], vc: &[f32], provoking: &[f32]) { + if self.bounds().is_none() { + return; + } + if let Some((poly, n)) = self.clip_triangle(va, vb, vc) { + self.clipped_fan(&poly, n, provoking); + } + } + + /// Rasterize what clipping left of a triangle, n vertices of poly, as a + /// fan from its first vertex, each flat shaded from provoking. + pub fn clipped_fan(&self, poly: &[VolumeVertex; VOLUME_TRIANGLE_MAX], n: usize, provoking: &[f32]) { + let mut i = 1usize; + while i + 1 < n { + self.clipped_triangle(&poly[0], &poly[i], &poly[i + 1], provoking); + i += 1; + } + } + + /// A textured quad's two triangles, a b c and a c d, each flat shaded + /// from provoking: both are validated before either is drawn + /// (SoftDraw::texture_triangle_valid()), and drawn from the clipping + /// that validated them. + pub fn texture_quad(&self, a: &[f32], b: &[f32], c: &[f32], d: &[f32], provoking: [&[f32]; 2]) { + let Some((first, n1)) = self.clip_triangle(a, b, c) else { + return; + }; + let Some((second, n2)) = self.clip_triangle(a, c, d) else { + return; + }; + if self.bounds().is_none() { + return; + } + self.clipped_fan(&first, n1, provoking[0]); + self.clipped_fan(&second, n2, provoking[1]); + } +} + +impl<'a> GlDraw<'a> { + pub fn soft_polygon_vertex(&self, indices: Option<&[u32]>, i: usize) -> &'a [f32] { + let index = match indices { + Some(ix) => ix[i] as usize, + None => i, + }; + + &self.vertices[index * vtx::FLOATS..][..vtx::FLOATS] + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct SoftAaPoint { + pub x: f64, + pub y: f64, +} + +pub(super) type SoftAaEdge = u32; + +pub(super) const AA_LEFT: SoftAaEdge = 0; + +pub(super) const AA_RIGHT: SoftAaEdge = 1; + +pub(super) const AA_BOTTOM: SoftAaEdge = 2; + +pub(super) const AA_TOP: SoftAaEdge = 3; + +impl SoftAaPoint { + pub fn aa_inside(&self, edge: SoftAaEdge, value: f64) -> bool { + match edge { + AA_LEFT => self.x >= value, + AA_RIGHT => self.x <= value, + AA_BOTTOM => self.y >= value, + AA_TOP => self.y <= value, + _ => unreachable!(), + } + } + + pub fn aa_intersection(&self, b: &SoftAaPoint, edge: SoftAaEdge, value: f64) -> SoftAaPoint { + let t = if edge <= AA_RIGHT { (value - self.x) / (b.x - self.x) } else { (value - self.y) / (b.y - self.y) }; + + SoftAaPoint { x: self.x + t * (b.x - self.x), y: self.y + t * (b.y - self.y) } + } +} + +/// Clip a convex window-space polygon to one side of a pixel square. +pub(super) fn soft_aa_clip( + src: &[SoftAaPoint], + n: usize, + dst: &mut [SoftAaPoint], + capacity: usize, + edge: SoftAaEdge, + value: f64, + out_n: &mut usize, +) -> bool { + let mut m = 0usize; + + if n == 0 { + *out_n = 0; + return true; + } + let mut a = &src[n - 1]; + for b in &src[..n] { + let ai = a.aa_inside(edge, value); + let bi = b.aa_inside(edge, value); + + if ai != bi { + if m == capacity { + return false; + } + dst[m] = a.aa_intersection(b, edge, value); + m += 1; + } + if bi { + if m == capacity { + return false; + } + dst[m] = *b; + m += 1; + } + a = b; + } + *out_n = m; + true +} + +impl SoftVertex { + pub fn aa_edge(&self, b: &SoftVertex, x: f64, y: f64) -> f64 { + (b.x.wrapping_sub(self.x)) as f64 * (y - self.y as f64) - (b.y.wrapping_sub(self.y)) as f64 * (x - self.x as f64) + } +} + +/// A convex polygon contains the square when it contains all four corners. +pub(super) fn soft_aa_contains_pixel(v: &[SoftVertex], n: usize, px: i32, py: i32, scale: f64, ccw: bool) -> bool { + for i in 0..n { + let a = &v[i]; + let b = &v[(i + 1) % n]; + let x = px as f64 * scale; + let y = py as f64 * scale; + + if ccw { + if a.aa_edge(b, x, y) < 0.0 + || a.aa_edge(b, x + scale, y) < 0.0 + || a.aa_edge(b, x, y + scale) < 0.0 + || a.aa_edge(b, x + scale, y + scale) < 0.0 + { + return false; + } + } else if a.aa_edge(b, x, y) > 0.0 + || a.aa_edge(b, x + scale, y) > 0.0 + || a.aa_edge(b, x, y + scale) > 0.0 + || a.aa_edge(b, x + scale, y + scale) > 0.0 + { + return false; + } + } + true +} + +thread_local! { + /// Visit every pixel of an antialiased polygon's bounding box, not just + /// the span of each row it can reach (tests and the benchmark). + pub(super) static SOFT_AA_WHOLE_BOX: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// Select the whole bounding box for antialiased polygons drawn on this +/// thread. Per thread, so that tests running at once cannot change each +/// other's path. +pub(super) fn raster_set_aa_whole_box(whole: bool) { + SOFT_AA_WHOLE_BOX.with(|c| c.set(whole)); +} + +/// The pixels of row py that the convex polygon v (n vertices, in subpixels +/// of scale to the pixel) can overlap, a pixel wider on each side: from the +/// polygon's extent along the row's strip, py to py + 1. None where the +/// strip holds none of the polygon. The clipping of a pixel's square +/// against the polygon finds nothing beyond this (soft_aa_clip()). +pub(super) fn soft_aa_row_span(v: &[SoftVertex], n: usize, scale: f64, py: i32) -> Option<(i32, i32)> { + let (y0, y1) = (py as f64, py as f64 + 1.0); + let mut lo = f64::INFINITY; + let mut hi = f64::NEG_INFINITY; + + for i in 0..n { + let (ax, ay) = (v[i].x as f64 / scale, v[i].y as f64 / scale); + let (bx, by) = (v[(i + 1) % n].x as f64 / scale, v[(i + 1) % n].y as f64 / scale); + + if ay >= y0 && ay <= y1 { + lo = lo.min(ax); + hi = hi.max(ax); + } + // Where the edge crosses either side of the strip. + for y in [y0, y1] { + if (ay < y) != (by < y) { + let x = ax + (y - ay) * (bx - ax) / (by - ay); + + lo = lo.min(x); + hi = hi.max(x); + } + } + } + if !(lo <= hi) { + return None; + } + Some(((lo.floor() as i32).saturating_sub(1), (hi.floor() as i32).saturating_add(1))) +} + +impl SoftDraw<'_> { + /// Rasterize one clipped convex polygon once. Pixel-square clipping + /// computes geometric coverage; centroid sampling is exact for affine + /// datums and an approximation for perspective-correct varyings. + pub fn polygon_coverage(&self, v: &[SoftVertex], n: usize, flat_color: &[f32], offset: f64) { + let mut flat = SoftFrag::default(); + let sub = self.rules.subpixel_bits as i32; + let scale = (1i32 << sub) as f64; + let mut polygon_area = 0.0f64; + + let bounds = if (3..=usize::MAX - 8).contains(&n) { self.bounds() } else { None }; + let Some((mut px0, mut py0, mut px1, mut py1)) = bounds else { + return; + }; + let capacity = n + 8; + let mut a = vec![SoftAaPoint::default(); capacity]; + let mut b = vec![SoftAaPoint::default(); capacity]; + let mut min_x = v[0].x; + let mut max_x = v[0].x; + let mut min_y = v[0].y; + let mut max_y = v[0].y; + for i in 0..n { + let next = &v[(i + 1) % n]; + + min_x = min_x.min(v[i].x); + max_x = max_x.max(v[i].x); + min_y = min_y.min(v[i].y); + max_y = max_y.max(v[i].y); + polygon_area += v[i].x.wrapping_mul(next.y).wrapping_sub(next.x.wrapping_mul(v[i].y)) as f64; + } + if !(polygon_area.abs() > 0.0) { + return; + } + px0 = px0.max((min_x >> sub) as i32); + px1 = px1.min((max_x >> sub) as i32); + py0 = py0.max((min_y >> sub) as i32); + py1 = py1.min((max_y >> sub) as i32); + for k in 0..4 { + flat.rgba[k] = flat_color[k] as f64; + } + + let whole_box = SOFT_AA_WHOLE_BOX.with(|c| c.get()); + for py in py0..=py1 { + let (mut x0, mut x1) = (px0, px1); + + if !whole_box { + // Pixels beyond the polygon's extent in this row are left + // with no overlap by the clipping below. + match soft_aa_row_span(v, n, scale, py) { + Some((lo, hi)) => { + x0 = x0.max(lo); + x1 = x1.min(hi); + } + None => continue, + } + } + for px in x0..=x1 { + let coverage: f64; + let x: f64; + let y: f64; + let z: f64; + let mut bary = [0.0f64; 3]; + let mut w = [0.0f64; 3]; + let mut fan_i = 0usize; + let mut frag = flat; + let mut found = false; + let Some(background) = self.stipple_pass(px, py) else { + continue; + }; + if soft_aa_contains_pixel(v, n, px, py, scale, polygon_area > 0.0) { + coverage = 1.0; + x = px as f64 + 0.5; + y = py as f64 + 0.5; + } else { + let mut count = n; + let mut twice_area = 0.0f64; + let mut cx = 0.0f64; + let mut cy = 0.0f64; + let mut src_is_a = true; + + // Restore the polygon before clipping this pixel. + for i in 0..n { + a[i] = SoftAaPoint { x: v[i].x as f64 / scale, y: v[i].y as f64 / scale }; + } + for e in AA_LEFT..=AA_TOP { + let bound = if e == AA_LEFT { + px as f64 + } else if e == AA_RIGHT { + (px + 1) as f64 + } else if e == AA_BOTTOM { + py as f64 + } else { + (py + 1) as f64 + }; + let ok = if src_is_a { + soft_aa_clip(&a, count, &mut b, capacity, e, bound, &mut count) + } else { + soft_aa_clip(&b, count, &mut a, capacity, e, bound, &mut count) + }; + + if !ok { + crate::dlog!(LogModule::Odyssey, "sgi-gl: polygon coverage clip exceeded its bound"); + return; + } + src_is_a = !src_is_a; + if count < 3 { + break; + } + } + if count < 3 { + continue; + } + let src = if src_is_a { &a } else { &b }; + for i in 0..count { + let p = &src[i]; + let q = &src[(i + 1) % count]; + let pxp = p.x - px as f64; + let pyp = p.y - py as f64; + let qxp = q.x - px as f64; + let qyp = q.y - py as f64; + let cross = pxp * qyp - qxp * pyp; + + twice_area += cross; + cx += (pxp + qxp) * cross; + cy += (pyp + qyp) * cross; + } + if !(twice_area.abs() > 0.0) { + continue; + } + coverage = cmin(1.0, twice_area.abs() * 0.5); + x = px as f64 + cx / (3.0 * twice_area); + y = py as f64 + cy / (3.0 * twice_area); + } + + // Find the fan triangle containing the overlap centroid. + let mut i = 1usize; + while i + 1 < n { + let den = v[0].aa_edge(&v[i], v[i + 1].x as f64, v[i + 1].y as f64); + + if den == 0.0 { + i += 1; + continue; + } + bary[0] = v[i].aa_edge(&v[i + 1], x * scale, y * scale) / den; + bary[1] = v[i + 1].aa_edge(&v[0], x * scale, y * scale) / den; + bary[2] = 1.0 - bary[0] - bary[1]; + if bary[0] >= -0.000000001 && bary[1] >= -0.000000001 && bary[2] >= -0.000000001 { + found = true; + fan_i = i; + break; + } + i += 1; + } + if !found { + continue; + } + { + let i = fan_i; + + w[0] = bary[0] * v[0].inv_w; + w[1] = bary[1] * v[i].inv_w; + w[2] = bary[2] * v[i + 1].inv_w; + let sum = w[0] + w[1] + w[2]; + let depth_w = bary[0] * v[0].depth_w + bary[1] * v[i].depth_w + bary[2] * v[i + 1].depth_w; + + if !(sum > 0.0) || !sum.is_finite() { + continue; + } + let fan = [v[0], v[i], v[i + 1]]; + + z = bary[0] * v[0].z + bary[1] * v[i].z + bary[2] * v[i + 1].z; + for k in 0..4 { + frag.texcoord[k] = + (w[0] * v[0].texcoord[k] + w[1] * v[i].texcoord[k] + w[2] * v[i + 1].texcoord[k]) / sum; + if self.st.shade_model != GL_FLAT { + frag.rgba[k] = (w[0] * v[0].rgba[k] + w[1] * v[i].rgba[k] + w[2] * v[i + 1].rgba[k]) / sum; + } + } + if self.st.clip_planes != 0 { + let mut d = [0.0f64; GL_CLIP_PLANES as usize]; + + for k in 0..GL_CLIP_PLANES as usize { + d[k] = (w[0] * fan[0].clip[k] + w[1] * fan[1].clip[k] + w[2] * fan[2].clip[k]) / sum; + } + if self.st.clipped(&d) { + continue; + } + } + if self.texture_base.is_some() { + let mut value = [0.0f64; 4]; + let mut out = [0.0f64; 5]; + let Some(gradient) = soft_texture_plane(&fan, sub as u32, self.image().volume, self.image().one_dimensional) else { + continue; + }; + for k in 0..4 { + value[k] = gradient.value[k] + + (px as f64 + 0.5 - gradient.x) * gradient.dx[k] + + (py as f64 + 0.5 - gradient.y) * gradient.dy[k]; + } + frag.texture_rho = if soft_texture_derivatives( + &value, + &gradient.dx, + &gradient.dy, + self.base_level().width as f64 * soft_texture_lod_scale(self.image().lod_scale_s), + if self.image().one_dimensional { + 0.0 + } else { + self.base_level().height as f64 * soft_texture_lod_scale(self.image().lod_scale_t) + }, + if self.image().volume { + self.base_level().depth as f64 * soft_texture_lod_scale(self.image().lod_scale_r) + } else { + 0.0 + }, + &mut out, + ) { + out[4] + } else { + f64::NAN + }; + } + if background { + self.stipple_background(&mut frag); + } + self.fragment_coverage(px, py, z, offset, depth_w, &frag, coverage, true); + } + } + } + } + + pub fn polygon_smooth(&self, poly: &[VolumeVertex], n: usize, provoking: &[f32], front: bool, slope: f64) { + let mut v: Vec = vec![SoftVertex::default(); n]; + let mut offset = 0.0f64; + let mut flat_color = &provoking[vtx::COLOR..vtx::COLOR + 4]; + + if !front && self.back_colors { + flat_color = &provoking[vtx::BACK_COLOR..vtx::BACK_COLOR + 4]; + } + for i in 0..n { + let Some(p) = self.project(&poly[i]) else { + return; + }; + v[i] = p; + if !front && self.back_colors { + let back = &poly[i].attr[vtx::BACK_COLOR..vtx::BACK_COLOR + 4]; + + for k in 0..4 { + v[i].rgba[k] = cmin(cmax(back[k], 0.0f32), 1.0f32) as f64; + } + } + } + if self.texture_base.is_some() { + let mut i = 1usize; + while i + 1 < n { + let fan = [v[0], v[i], v[i + 1]]; + + if !self.texture_coefficients(&fan) { + return; + } + i += 1; + } + } + if self.st.polygon_offset & GL_OFFSET_FILL != 0 { + offset = self.rules.offset_amount(&self.st, slope); + } + self.polygon_coverage(&v, n, flat_color, offset); + } + + /// Replay the original fan, including its validation and fragment ordering. + pub fn polygon_fill(&self, d: &GlDraw, indices: Option<&[u32]>, n: usize, provoking: &[f32]) { + let v0 = d.soft_polygon_vertex(indices, 0); + let prevalidate = self.texture.is_some() && !(d.mode == GL_POLYGON && d.fill_by_triangle); + + if prevalidate { + let mut i = 1usize; + while i + 1 < n { + if !self.texture_triangle_valid( + v0, + d.soft_polygon_vertex(indices, i), + d.soft_polygon_vertex(indices, i + 1), + ) { + return; + } + i += 1; + } + } + let mut i = 1usize; + while i + 1 < n { + self.triangle( + v0, + d.soft_polygon_vertex(indices, i), + d.soft_polygon_vertex(indices, i + 1), + provoking, + ); + i += 1; + } + } + + /// Workspace bounds are internal representations, not hardware maxima. + pub fn polygon(&self, d: &GlDraw, indices: Option<&[u32]>, count: usize, provoking: &[f32], force_edges: bool) -> GlDrawResult { + let mut positions = *self; + let mut coverage = *self; + let mut fan: [SoftVertex; 3] = Default::default(); + let mut previous: SoftVertex; + let mut current: SoftVertex; + let mut stipple = SoftLineStipple { + enabled: d.line_stipple_enabled, + repeat: cmax(1, cmin(d.line_stipple_repeat, 256)), + pattern: d.line_stipple_pattern, + ..Default::default() + }; + let mut flat = [0.0f32; vtx::FLOATS]; + let mut n = count; + let mut area = 0.0f64; + let mut slope = 0.0f64; + let mut offset = 0.0f64; + + if count < 3 { + return GL_DRAW_OK; + } + if count > usize::MAX - CLIP_PLANES { + return GL_DRAW_OUT_OF_MEMORY; + } + let capacity = count + CLIP_PLANES; + let mut poly: Vec = vec![VolumeVertex::default(); capacity]; + let mut other: Vec = vec![VolumeVertex::default(); capacity]; + // Facing must not prevalidate texture Q and erase a valid FILL prefix. + positions.texture = None; + for (i, v) in poly.iter_mut().enumerate().take(count) { + *v = self.st.volume_from_draw_vertex(d.soft_polygon_vertex(indices, i)); + if !v.clip.iter().chain(&v.eye).all(|c| c.is_finite()) { + crate::dlog!(LogModule::Odyssey, "sgi-gl: invalid polygon position dropped"); + return GL_DRAW_OK; + } + if force_edges { + v.set_edge(true); + } + } + // Edges clipping adds are boundary edges; kept ones keep their flags. + let clip = self.st.volume_clip(true); + let any = poly[..count].iter().fold(0, |o, v| o | clip.outcode_by::(v)); + let over; + if any & NearW::W_PLANE != 0 { + report_w_plane_clip(); + } + (n, over) = clip.clip_polygon_by::(&mut poly, &mut other, n, any); + if over { + crate::dlog!(LogModule::Odyssey, "sgi-gl: polygon exceeds convex clipping bound"); + return GL_DRAW_OK; + } + let projected = if n == 0 { None } else { positions.project(&poly[0]) }; + let Some(first) = projected else { + return GL_DRAW_OK; + }; + previous = first; + fan[0] = first; + for (i, v) in poly.iter().enumerate().take(n).skip(1) { + let Some(p) = positions.project(v) else { + return GL_DRAW_OK; + }; + current = p; + if i >= 2 { + area += first.edge(&previous, current.x, current.y) as f64; + fan[1] = previous; + fan[2] = current; + slope = cmax(slope, self.slope(&fan)); + } + previous = current; + } + if self.st.window_matrix_given && self.st.window_rows_down { + area = -area; + } + if self.st.front_face == GL_CW { + area = -area; + } + let front: bool = area > 0.0; // Zero remains back-facing through both reversals. + if self.st.cull_face + && (self.st.cull_face_mode == GL_FRONT_AND_BACK + || self.st.cull_face_mode == (if front { GL_FRONT } else { GL_BACK })) + { + return GL_DRAW_OK; + } + let mode: u32 = d.polygon_modes.map_or(GL_FILL, |m| m[if front { 0 } else { 1 }]); + if mode == GL_FILL { + if self.polygon_smooth { + self.polygon_smooth(&poly, n, provoking, front, slope); + return GL_DRAW_OK; + } + self.polygon_fill(d, indices, count, provoking); + return GL_DRAW_OK; + } + if mode != GL_LINE && mode != GL_POINT { + crate::dlog!(LogModule::Odyssey, "sgi-gl: invalid polygon mode {:#x} ignored", mode); + return GL_DRAW_OK; + } + if mode == GL_LINE { + let Some(width) = soft_dimension(d.line_width) else { + return GL_DRAW_OK; + }; + coverage.line_width = width; + } else if mode == GL_POINT { + let Some(size) = soft_dimension(d.point_size) else { + return GL_DRAW_OK; + }; + coverage.point_size = size; + } + flat.copy_from_slice(&provoking[..vtx::FLOATS]); + if !front && self.back_colors { + flat[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&provoking[vtx::BACK_COLOR..vtx::BACK_COLOR + 4]); + for v in &mut poly[..n] { + v.attr.copy_within(vtx::BACK_COLOR..vtx::BACK_COLOR + 4, vtx::COLOR); + } + } + // Validate the entire boundary before any LINE/POINT fragment or sample. + for i in 0..n { + let Some(p) = self.project(&poly[i]) else { + return GL_DRAW_OK; + }; + current = p; + if self.texture_base.is_some() + && mode == GL_LINE + && !self.project(&poly[(i + 1) % n]).is_some_and(|next| { + current + .texture_line_gradient( + &next, + self.rules.subpixel_bits as u32, + self.image().volume, + self.image().one_dimensional, + ) + .is_some() + }) + { + self.texture_report(16, "invalid primitive coefficients, primitive dropped"); + return GL_DRAW_OK; + } + } + let offset_enable = if mode == GL_LINE { GL_OFFSET_LINE } else { GL_OFFSET_POINT }; + if self.st.polygon_offset & offset_enable != 0 { + offset = self.rules.offset_amount(&self.st, slope); + } + for i in 0..n { + if !poly[i].edge() { + continue; + } + // Validated in full above, so every projection here succeeds. + let current = self.project(&poly[i]).unwrap_or_default(); + if mode == GL_POINT { + coverage.point_projected(¤t, Some(&flat), offset); + } else { + let next = self.project(&poly[(i + 1) % n]).unwrap_or_default(); + coverage.line_projected(¤t, &next, &flat, offset, Some(&mut stipple), false); + } + } + GL_DRAW_OK + } + + /// Dispatch each independently assembled polygon, preserving provoking rules. + pub fn polygon_modes(&self, d: &GlDraw) -> GlDrawResult { + let mut indices = [0u32; 4]; + let n = d.count; + + if d.polygon_modes.is_none() { + let mut fill = *d; + + fill.polygon_modes = Some([GL_FILL, GL_FILL]); + return self.polygon_modes(&fill); + } + + if d.mode == GL_POLYGON { + return self.polygon(d, None, n as usize, d.soft_polygon_vertex(None, 0), false); + } + let stride: u32 = if d.mode == GL_TRIANGLES { + 3 + } else if d.mode == GL_QUADS { + 4 + } else if d.mode == GL_QUAD_STRIP { + 2 + } else { + 1 + }; + let mut i = 0u32; + while i < n { + let count: u32 = if d.mode == GL_QUADS || d.mode == GL_QUAD_STRIP { 4 } else { 3 }; + let provoking = i + count - 1; + let force = stride < 3; + + if n - i < count { + break; + } + for k in 0..count { + indices[k as usize] = i + k; + } + if d.mode == GL_TRIANGLE_STRIP && (i & 1) != 0 { + indices[0] = i + 1; + indices[1] = i; + } else if d.mode == GL_TRIANGLE_FAN { + indices[0] = 0; + } else if d.mode == GL_QUAD_STRIP { + indices[2] = i + 3; + indices[3] = i + 2; + } + let result = self.polygon( + d, + Some(&indices[..count as usize]), + count as usize, + d.soft_polygon_vertex(None, provoking as usize), + force, + ); + if result != GL_DRAW_OK { + return result; + } + i += stride; + } + GL_DRAW_OK + } + + /// The filled primitives of a draw, from GL_TRIANGLES on, in order. + pub fn draw_triangles(&self, d: &GlDraw) { + let n = d.count as usize; + let v = |i: usize| -> &[f32] { d.soft_polygon_vertex(None, i) }; + + match d.mode { + GL_TRIANGLES => { + let mut i = 0usize; + while i + 2 < n { + self.triangle(v(i), v(i + 1), v(i + 2), v(i + 2)); + i += 3; + } + } + GL_TRIANGLE_STRIP => { + let mut i = 0usize; + while i + 2 < n { + if i & 1 != 0 { + self.triangle(v(i + 1), v(i), v(i + 2), v(i + 2)); + } else { + self.triangle(v(i), v(i + 1), v(i + 2), v(i + 2)); + } + i += 1; + } + } + GL_TRIANGLE_FAN | GL_POLYGON => { + if self.texture.is_some() && d.mode == GL_POLYGON && !d.fill_by_triangle { + let mut i = 0usize; + while i + 2 < n { + if !self.texture_triangle_valid(v(0), v(i + 1), v(i + 2)) { + return; + } + i += 1; + } + } + let mut i = 0usize; + while i + 2 < n { + self.triangle(v(0), v(i + 1), v(i + 2), if d.mode == GL_POLYGON { v(0) } else { v(i + 2) }); + i += 1; + } + } + GL_QUADS => { + let mut i = 0usize; + while i + 3 < n { + if self.texture.is_some() { + self.texture_quad(v(i), v(i + 1), v(i + 2), v(i + 3), [v(i + 3), v(i + 3)]); + i += 4; + continue; + } + self.triangle(v(i), v(i + 1), v(i + 2), v(i + 3)); + self.triangle(v(i), v(i + 2), v(i + 3), v(i + 3)); + i += 4; + } + } + GL_QUAD_STRIP => { + // Vertices 2i - 1, 2i, 2i + 2 and 2i + 1 in order (2.6.1). + let mut i = 0usize; + while i + 3 < n { + if self.texture.is_some() { + self.texture_quad(v(i), v(i + 1), v(i + 3), v(i + 2), [v(i + 3), v(i + 3)]); + i += 2; + continue; + } + self.triangle(v(i), v(i + 1), v(i + 3), v(i + 3)); + self.triangle(v(i), v(i + 3), v(i + 2), v(i + 3)); + i += 2; + } + } + _ => unreachable!(), + } + } + + /// Whether every vertex of a draw has the texture coordinates its image + /// uses: a draw with one that has not drops that primitive unread, so it + /// reads its texels as it reaches them rather than all of them first. + pub fn fixed_coordinates_valid(&self, d: &GlDraw) -> bool { + for i in 0..d.count as usize { + let v = self.st.volume_from_draw_vertex(d.soft_polygon_vertex(None, i)); + + if !self.image().soft_texcoord_valid(self.texture_base, &v) { + return false; + } + } + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::dev::odyssey::gl_context::{vtx, GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_MODULATE, GL_RGBA}; + use crate::dev::odyssey::raster_bench::{BENCH_H, BENCH_W, Bench, BenchTexture, Lcg}; + + + const SIDE: i32 = 160; + + /// A vertex at window pixel (x, y) with w, depth d, colour c and + /// texture coordinates s and t. + fn vertex(x: f32, y: f32, w: f32, d: f32, c: [f32; 4], s: f32, t: f32) -> [f32; vtx::FLOATS] { + let mut o = [0.0f32; vtx::FLOATS]; + + o[vtx::POSITION..vtx::POSITION + 4] + .copy_from_slice(&[(x / BENCH_W as f32 * 2.0 - 1.0) * w, (y / BENCH_H as f32 * 2.0 - 1.0) * w, 2.0 * d - w, w]); + o[vtx::COLOR..vtx::COLOR + 4].copy_from_slice(&c); + o[vtx::TEXCOORD..vtx::TEXCOORD + 4].copy_from_slice(&[s * w, t * w, 0.0, w]); + o + } + + /// Smooth-shaded, depth-tested, trilinear modulated draws of mode, one + /// draw for each of prims, and the colour and depth planes after. + fn draw(mode: u32, prims: Vec>) -> (Vec, Vec, Vec) { + let mut b = Bench::empty(); + + b.st.depth_test = true; + b.st.texture_2d = true; + b.st.texture_env_mode = GL_MODULATE; + b.tex = Some(BenchTexture::new(32, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_RGBA)); + b.mode = mode; + b.prims = prims; + b.prepare(); + b.run(); + b.snapshot(SIDE) + } + + /// n random vertices in perspective inside the square, some crossing + /// the near or far plane. + fn vertices(rng: &mut Lcg, n: usize) -> Vec<[f32; vtx::FLOATS]> { + (0..n) + .map(|_| { + let c = [rng.range(0.0, 1.0), rng.range(0.0, 1.0), rng.range(0.0, 1.0), 1.0]; + vertex(rng.range(0.0, SIDE as f32), rng.range(0.0, SIDE as f32), rng.range(0.5, 3.0), rng.range(-0.2, 1.2), c, rng.range(-1.0, 2.0), rng.range(-1.0, 2.0)) + }) + .collect() + } + + // A textured quad, or a quad strip's quad, draws its two triangles, + // each clipped as a triangle of its own would be. + #[test] + fn textured_quads_draw_their_two_triangles() { + let mut rng = Lcg(77); + let mut drawn = 0; + for _ in 0..30 { + let v = vertices(&mut rng, 4); + let quad: Vec = v.iter().flatten().copied().collect(); + let tris = |order: [usize; 6]| -> Vec> { vec![order.iter().flat_map(|&i| v[i]).collect()] }; + + let want = draw(GL_TRIANGLES, tris([0, 1, 2, 0, 2, 3])); + assert!(draw(GL_QUADS, vec![quad.clone()]) == want, "quads"); + // Strip vertices 0 1 3 2 make the quad 0 1 2 3. + let strip: Vec = [0, 1, 3, 2].iter().flat_map(|&i| v[i]).collect(); + assert!(draw(GL_QUAD_STRIP, vec![strip]) == want, "quad strip"); + drawn += want.0.iter().filter(|&&c| c != 0).count(); + } + assert!(drawn > 20000, "the quads drew too little: {drawn}"); + } + + // A textured quad one of whose triangles has no valid texture planes is + // dropped whole, the valid triangle too. + #[test] + fn a_textured_quad_with_an_invalid_triangle_draws_nothing() { + let white = [1.0; 4]; + let mut v = [ + vertex(10.0, 10.0, 1.0, 0.5, white, 0.0, 0.0), + vertex(150.0, 10.0, 1.0, 0.5, white, 1.0, 0.0), + vertex(150.0, 150.0, 1.0, 0.5, white, 1.0, 1.0), + vertex(10.0, 150.0, 1.0, 0.5, white, 0.0, 1.0), + ]; + let first: Vec = v[..3].iter().flatten().copied().collect(); + assert!(draw(GL_TRIANGLES, vec![first]).0.iter().any(|&c| c != 0), "the valid triangle draws alone"); + v[3][vtx::TEXCOORD] = f32::NAN; + let quad: Vec = v.iter().flatten().copied().collect(); + assert!(draw(GL_QUADS, vec![quad]).0.iter().all(|&c| c == 0), "quads"); + let strip: Vec = [0, 1, 3, 2].iter().flat_map(|&i| v[i]).collect(); + assert!(draw(GL_QUAD_STRIP, vec![strip]).0.iter().all(|&c| c == 0), "quad strip"); + } +} diff --git a/src/dev/odyssey/raster_unit.rs b/src/dev/odyssey/raster_unit.rs new file mode 100644 index 000000000..75bb7ea28 --- /dev/null +++ b/src/dev/odyssey/raster_unit.rs @@ -0,0 +1,866 @@ +//! BUZZ's raster unit on a thread of its own, and the queue that feeds it. +//! +//! BUZZ is the board's one graphics chip. Its front end parses the command +//! FIFO, writes registers, transforms, lights and sets up primitives; behind +//! it the raster unit rasterises, textures, shades and writes the framebuffer. +//! US 6,417,858 puts an output FIFO between the transform engine and the +//! rasteriser (inferred to apply to BUZZ). The model splits there: the +//! board's thread (worker.rs) is the front end, and this file's thread, +//! `odyssey-raster`, is the raster unit. The back end, PB&J, stays with the +//! display thread. +//! +//! What crosses the queue is the model's own encoding, because the +//! hardware's is unknown: each entry is one job, a closure over everything +//! the raster work needs, captured when the front end sets it up. A GL +//! primitive's job carries its raster rules, draw state, vertices, texture +//! chain and the texture tables it reads; a fence carries its address and +//! token. The state a job captures is the state at its place in the +//! command stream, so raster state reaches the raster unit in order. The +//! guest sees nothing of the split. +//! +//! The planes belong to the raster unit. They stay where the board keeps +//! them, and a job reaches them through raw spans (`PlaneSpans`), as the +//! IMPACT model's raster engines reach their pixel memory through pointers. +//! Two other threads touch them: +//! +//! - The front end's own plane work (integer and image drawing, pixel DMA, +//! the object store, reset) first waits for the raster unit to finish +//! every job queued (`Board::raster_wait`). That orders the command +//! stream, in which the board's pipeline runs a plane access behind the +//! drawing before it. A DMA's texels for an object that already has its +//! rows go to the raster unit as a job instead (`Board::object_stage`). +//! - The display thread copies rows out with no lock and no wait, as the +//! hardware's back end reads memory while the pipeline writes it. A row +//! it copies half-drawn is drawn again by the next frame (reader.rs). +//! +//! The rows a job wrote are the display's business too. The raster thread +//! keeps them in its context while it draws and publishes them to words +//! the display takes from (`RasterCtx::publish`): before it runs a job +//! that tells the guest something (`RasterQueue::submit_completion`), when +//! the queue drains, and when a slice ends. A guest that waits for a +//! fence or a query before it arms a swap therefore has all of its rows +//! published before the swap can latch. +//! +//! The front end waits when the queued work, estimated in pixels per job, +//! passes `RASTER_WORK_BUDGET`, so the drawing stays close behind the commands. +//! +//! The ring takes the shared `GFifo`. With no raster thread running (unit +//! tests, a stopped machine) a job runs inside the submit. + + +use super::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use super::consts::{ + dispmode, ODSY_BUFFERS, ODSY_DID_MAIN_SHIFT, ODSY_ID_COMPARE_ENABLE, + ODSY_ID_COMPARE_MASK_SHIFT, +}; +use super::gl::GlDrawJob; +use super::gl_context::{GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP, GL_POINTS, GL_TRIANGLES}; +use super::memory::BoardMemory; +use super::reader::Differ; +use super::scanout::{DirtySite, RowMarks}; +use std::cell::{Cell, UnsafeCell}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering::{Acquire, Release, SeqCst}}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use parking_lot::{Condvar, Mutex}; + +use crate::gfifo::GFifo; + +/// The raster thread's name. +pub(super) const ODYSSEY_RASTER_THREAD: &str = "odyssey-raster"; + +/// Jobs the queue holds. +pub(super) const ODSY_RASTER_QUEUE_MAX: usize = 65536; + +/// Work, in pixels, the front end may have queued for the raster unit +/// before it waits for the raster unit to catch up, as the board's front +/// end waits on a full output FIFO between transform and rasteriser +/// (US 6,417,858; the depth on BUZZ is unknown). It keeps the drawing +/// close behind the commands, so the display does not show a frame the +/// processor sent long ago. Measured in work, not jobs: Smoke & Mirrors +/// queues thousands of tiny triangles, which a job-count bound stalled +/// the processor behind. The model rasterises 5 to 30 Mpixel/s a +/// textured or blended fill (`raster_bench`, release build), so 2^17 pixels +/// is a few milliseconds to a few tens of milliseconds of raster time, and +/// well past any one frame's small primitives. +pub(super) const RASTER_WORK_BUDGET: u64 = 1 << 17; + +/// What a job costs before its pixels: setup, dispatch and the state it +/// carries, in pixels of fill. A queue of tiny primitives still adds up. +pub(super) const RASTER_JOB_OVERHEAD: u32 = 64; + +/// Clears and block fills run about this many times faster per pixel than +/// a shaded fill (`raster_bench`: 200 against 5 to 30 Mpixel/s), so their +/// pixels count for this fraction. +pub(super) const RASTER_CLEAR_DIVISOR: u32 = 8; + +/// The cost a ring entry's word can hold. +const COST_MAX: u64 = (1 << 30) - 1; + +/// The longest the raster thread runs jobs before it publishes the rows it +/// drew, checked between jobs. +pub(super) const RASTER_SLICE: Duration = Duration::from_millis(1); + +/// What a GL draw hands back to its context in place of the line-stipple +/// phase while the raster unit keeps the phase: the next draw that carries +/// it continues from the raster unit's own count. The context resets its +/// phase to 0 at glBegin and glEnd, which replaces this marker. +pub(super) const LINE_PHASE_ON_RASTER: u32 = u32::MAX; + +/// Words of a row set: one bit a plane row. +pub(super) const ROW_WORDS: usize = (ODSY_HEIGHT as usize).div_ceil(64); + +/// The rows a draw wrote, a bit each, by where it wrote: colour buffer 0, +/// colour buffer 1 and the overlay byte. +pub(super) type TouchedRows = [[Cell; ROW_WORDS]; ODSY_BUFFERS as usize + 1]; + +/// Slot of the overlay byte in `TouchedRows`. +pub(super) const TOUCHED_OVERLAY: usize = ODSY_BUFFERS as usize; + +/// Note that a draw wrote row y of `slot`. +#[inline(always)] +pub(super) fn touch_row(rows: &TouchedRows, slot: usize, y: i32) { + if (y as u32) < ODSY_HEIGHT { + let w = &rows[slot][y as usize >> 6]; + w.set(w.get() | 1 << (y & 63)); + } +} + +/// The colour rows the raster unit has written since the display last +/// looked, for the display to resolve those and no others. A row's pixels +/// show a colour buffer by the window their id plane names and that +/// window's displayed-buffer bit, so a row written to a buffer is kept by +/// the windows the draw's id test lets it reach. The overlay shows +/// whatever the windows do. +pub(super) struct DrawnRows { + by_id: Box<[[[u64; ROW_WORDS]; dispmode::COUNT as usize]; ODSY_BUFFERS as usize]>, + overlay: [u64; ROW_WORDS], + /// Some row is held. + any: bool, +} + +impl Default for DrawnRows { + fn default() -> Self { + DrawnRows { by_id: Box::new([[[0; ROW_WORDS]; dispmode::COUNT as usize]; ODSY_BUFFERS as usize]), overlay: [0; ROW_WORDS], any: false } + } +} + +impl DrawnRows { + /// Keep what a draw wrote, which passed the id test `id_compare` (the + /// board's 0x840 word) at each pixel it stored. + pub fn add(&mut self, touched: &TouchedRows, id_compare: u32) { + self.any = true; + let value = id_compare as u8; + let mask = (id_compare >> ODSY_ID_COMPARE_MASK_SHIFT) as u8; + + for (slot, rows) in touched.iter().enumerate() { + if rows.iter().all(|w| w.get() == 0) { + continue; + } + if slot == TOUCHED_OVERLAY { + for (o, w) in self.overlay.iter_mut().zip(rows) { + *o |= w.get(); + } + continue; + } + for id in 0..dispmode::COUNT { + // The main drawable is the id byte's bits 7:3. + if id_compare & ODSY_ID_COMPARE_ENABLE != 0 && (((id << ODSY_DID_MAIN_SHIFT) as u8 ^ value) & mask) >> ODSY_DID_MAIN_SHIFT != 0 { + continue; + } + for (o, w) in self.by_id[slot][id as usize].iter_mut().zip(rows) { + *o |= w.get(); + } + } + } + } +} + +/// The rows the raster unit has published and the display has not yet +/// taken: the same sets as [`DrawnRows`], as atomic words. The raster thread +/// ORs its rows in after the stores that drew them and the display swaps +/// each word out, so neither waits for the other. A word the display takes +/// just before a publish lands is taken at its next frame. +pub(super) struct PublishedRows { + by_id: Box<[[[AtomicU64; ROW_WORDS]; dispmode::COUNT as usize]; ODSY_BUFFERS as usize]>, + overlay: [AtomicU64; ROW_WORDS], + /// Some row may be held. + any: AtomicBool, +} + +impl Default for PublishedRows { + fn default() -> Self { + PublishedRows { + by_id: Box::new(std::array::from_fn(|_| std::array::from_fn(|_| std::array::from_fn(|_| AtomicU64::new(0))))), + overlay: std::array::from_fn(|_| AtomicU64::new(0)), + any: AtomicBool::new(false), + } + } +} + +impl PublishedRows { + /// Move every row `from` holds into the published set, leaving `from` + /// empty. + pub fn absorb(&self, from: &mut DrawnRows) { + if !std::mem::take(&mut from.any) { + return; + } + for (o, f) in self.overlay.iter().zip(from.overlay.iter_mut()) { + if *f != 0 { + o.fetch_or(std::mem::take(f), Release); + } + } + for (ids, from_ids) in self.by_id.iter().zip(from.by_id.iter_mut()) { + for (rows, from_rows) in ids.iter().zip(from_ids.iter_mut()) { + for (o, f) in rows.iter().zip(from_rows.iter_mut()) { + if *f != 0 { + o.fetch_or(std::mem::take(f), Release); + } + } + } + } + self.any.store(true, Release); + } + + /// Hand the display the rows the draws so far changed, and forget + /// them. A row shows a change when a buffer a window shows was written + /// there: the scanout repaints it. A row written to either buffer + /// joins the rows where the window's buffers may differ (`differ`), + /// which a swap of that window repaints. + pub fn take(&self, displayed: u32, differ: &mut Differ, marks: &RowMarks) { + if !self.any.swap(false, Acquire) { + return; + } + let mut shown: [u64; ROW_WORDS] = std::array::from_fn(|i| self.overlay[i].swap(0, Acquire)); + + for (b, ids) in self.by_id.iter().enumerate() { + for (id, rows) in ids.iter().enumerate() { + for (w, (d, row)) in differ[id].iter_mut().zip(rows.iter()).enumerate() { + let bits = row.swap(0, Acquire); + + *d |= bits; + if (displayed >> id) & 1 == b as u32 { + shown[w] |= bits; + } + } + } + } + for (i, mut w) in shown.into_iter().enumerate() { + while w != 0 { + marks.repaint(DirtySite::Raster, (i as u32 * 64 + w.trailing_zeros()) as i32, 1); + w &= w - 1; + } + } + } +} + +/// The raster unit's own state. +#[derive(Default)] +pub(super) struct RasterCtx { + /// The standalone line stipple's phase after the last draw that + /// carried one. + pub line_phase: u32, + /// The colour rows drawn since the last publish. A job that writes the + /// colour planes records its rows here, or the display never resolves + /// them. + pub drawn: DrawnRows, + /// Where `publish` puts them. + published: Arc, +} + +impl RasterCtx { + fn new(published: Arc) -> Self { + RasterCtx { line_phase: 0, drawn: DrawnRows::default(), published } + } + + /// Hand the rows drawn so far to the display. Called before anything the + /// guest can see that follows the drawing, when the queue drains and + /// when a slice ends: the guest's own fences and swaps are what order the + /// display's view of the drawing, as on the board. + pub fn publish(&mut self) { + if self.drawn.any { + self.published.absorb(&mut self.drawn); + } + } +} + +/// One unit of raster work. +pub(super) type RasterJob = Box; + +/// The ring entry's address word: the value is a boxed `RasterJob`. +const ENTRY_JOB: u32 = 1; +/// The value is a boxed `GlDrawJob`, which goes back to the front end +/// when drawn. +const ENTRY_DRAW: u32 = 2; +/// The entry's address word is its kind in the low bits and its cost +/// (`RasterQueue::push`) above them. +const ENTRY_KIND_MASK: u32 = 3; +const ENTRY_COST_SHIFT: u32 = 2; + +/// The cost of a rectangle of pixels clipped to the screen, a clear's or a +/// block fill's counted at `RASTER_CLEAR_DIVISOR`, plus the job's overhead. +pub(super) fn rect_cost(x0: i32, y0: i32, x1: i32, y1: i32, fast: bool) -> u32 { + let w = (x1.min(ODSY_WIDTH as i32) - x0.max(0)).max(0) as u64; + let h = (y1.min(ODSY_HEIGHT as i32) - y0.max(0)).max(0) as u64; + let px = w * h / if fast { RASTER_CLEAR_DIVISOR as u64 } else { 1 }; + + RASTER_JOB_OVERHEAD.saturating_add(px.min(COST_MAX) as u32) +} + +/// The cost of a primitive from its vertices' window positions, None for a +/// vertex with no position (behind the eye, where clipping will cut the +/// primitive and what is left may be any size, so the whole screen counts). +/// Points cost their square, lines their length times their width, a +/// triangle its area and any other polygon its bounding box, each within +/// the screen. An estimate, from the corners alone. +pub(super) fn primitive_cost(mode: u32, points: impl Iterator>, point_size: f32, line_width: f32) -> u32 { + let (w, h) = (ODSY_WIDTH as f64, ODSY_HEIGHT as f64); + let (mut lo, mut hi) = ([f64::MAX; 2], [f64::MIN; 2]); + let mut first = [(0.0f64, 0.0f64); 3]; + let mut n = 0; + + for p in points { + let Some((x, y)) = p else { return RASTER_JOB_OVERHEAD.saturating_add((w * h) as u32) }; + if n < 3 { + first[n] = (x, y); + } + n += 1; + lo = [lo[0].min(x), lo[1].min(y)]; + hi = [hi[0].max(x), hi[1].max(y)]; + } + if n == 0 { + return RASTER_JOB_OVERHEAD; + } + // A NaN position is no extent; the clamp then yields zero. + let (bw, bh) = ((hi[0].min(w) - lo[0].max(0.0)).max(0.0), (hi[1].min(h) - lo[1].max(0.0)).max(0.0)); + let boxed = bw * bh; + let px = match mode { + GL_POINTS => (point_size.max(1.0) as f64).powi(2) * n as f64, + GL_LINES | GL_LINE_LOOP | GL_LINE_STRIP => (bw.max(bh) + 1.0) * (line_width.max(1.0) as f64), + GL_TRIANGLES if n == 3 => { + let (a, b, c) = (first[0], first[1], first[2]); + (((b.0 - a.0) * (c.1 - a.1) - (c.0 - a.0) * (b.1 - a.1)).abs() / 2.0).min(boxed) + } + _ => boxed, + }; + RASTER_JOB_OVERHEAD.saturating_add(px.min(COST_MAX as f64) as u32) +} + +/// Finished GL draw jobs kept for the front end to fill again. A draw +/// that finds none allocates; one finished while this many wait is freed. +const SPARE_DRAWS: usize = 1024; + +/// The queue between the front end and the raster unit. `HEADS` is the +/// GFifo's consumer heads: one, because the board has one raster unit. A +/// second would need the jobs to split the rows between the heads, as the +/// IMPACT model's two engines interleave scanlines. +pub(super) struct RasterQueue { + ring: Box>, + /// Finished draw jobs, from the raster thread (or a draw run inside + /// its submit) back to the front end, which takes them under the board + /// lock. + spare: Box>, + /// The raster unit's own state (the stipple phase and the rows drawn, + /// not the planes). The raster thread owns it while it runs jobs; the + /// front end uses it only for a job it runs itself, with no raster thread + /// (`RasterQueue::ctx`). + ctx: UnsafeCell, + /// The rows the raster unit has published and the display has not yet + /// taken (RasterCtx::publish()). + published: Arc, + /// Jobs submitted and not yet finished. + pending: AtomicU32, + /// The estimated work of those jobs, in pixels (`RASTER_WORK_BUDGET`). + queued: AtomicU64, + /// The work the front end waits under (`RASTER_WORK_BUDGET`; tests + /// set their own). + budget: AtomicU64, + /// The front end waits for the raster unit to bring `queued` down; the + /// raster unit wakes it at half the budget, and a stop wakes it. + room_waiting: AtomicBool, + closing: AtomicBool, + room: Mutex<()>, + room_kick: Condvar, + /// Jobs pushed onto the ring, and taken off it by the raster thread, + /// for its wake-up. Both run on across a stop and a start. + pushed: AtomicU64, + taken: AtomicU64, + /// The raster thread runs, so jobs queue. + running: AtomicBool, + exit: AtomicBool, + sleeping: AtomicBool, + wake: Mutex<()>, + kick: Condvar, + idle: Mutex<()>, + idle_kick: Condvar, + thread: Mutex>>, + /// Tests: the raster thread is in a slice. + #[cfg(test)] + test_in_slice: AtomicBool, + /// Tests: each slice first runs this long, as a long draw would. + #[cfg(test)] + pub test_slice_sleep_ms: AtomicU32, + /// Tests: each slice sleeps this long between its last job and its + /// publish, so a test can look at the cell in between. + #[cfg(test)] + pub test_slice_end_ms: AtomicU32, + /// Tests: the longest a slice runs, in milliseconds, in place of + /// `RASTER_SLICE`; 0 keeps it. + #[cfg(test)] + pub test_slice_limit_ms: AtomicU32, +} + +// SAFETY: the jobs in the ring are Send; the ring itself is shared as GFifo +// documents (one producer at a time, the front end; one consumer, the raster +// thread). `ctx` is the raster thread's while it runs; the front end touches +// it only with no raster thread (`!threaded()`), as the other boards' CPU +// threads touch an engine's state only after its threads have stopped. A guest that stores while +// it waits for a drain gets whatever ordering it asked for, not a lock. +unsafe impl Send for RasterQueue {} +unsafe impl Sync for RasterQueue {} + +impl Default for RasterQueue { + fn default() -> Self { + const { assert!(HEADS == 1, "the board has one raster unit") }; + let published = Arc::::default(); + RasterQueue { + // SAFETY: a zeroed GFifo is an empty one (GFifo::new); built in + // place because the ring is too big for the stack. + ring: unsafe { Box::new_zeroed().assume_init() }, + // SAFETY: as the ring. + spare: unsafe { Box::new_zeroed().assume_init() }, + ctx: UnsafeCell::new(RasterCtx::new(published.clone())), + published, + pending: AtomicU32::new(0), + queued: AtomicU64::new(0), + budget: AtomicU64::new(RASTER_WORK_BUDGET), + room_waiting: AtomicBool::new(false), + closing: AtomicBool::new(false), + room: Mutex::new(()), + room_kick: Condvar::new(), + pushed: AtomicU64::new(0), + taken: AtomicU64::new(0), + running: AtomicBool::new(false), + exit: AtomicBool::new(false), + sleeping: AtomicBool::new(false), + wake: Mutex::new(()), + kick: Condvar::new(), + idle: Mutex::new(()), + idle_kick: Condvar::new(), + thread: Mutex::new(None), + #[cfg(test)] + test_in_slice: AtomicBool::new(false), + #[cfg(test)] + test_slice_sleep_ms: AtomicU32::new(0), + #[cfg(test)] + test_slice_end_ms: AtomicU32::new(0), + #[cfg(test)] + test_slice_limit_ms: AtomicU32::new(0), + } + } +} + +impl Drop for RasterQueue { + fn drop(&mut self) { + // Jobs never run still own their captures. + while let Some((kind, value)) = self.ring.peek() { + self.ring.consume(); + // SAFETY: every entry holds a job from submit() or + // submit_draw(), taken once. + unsafe { drop_entry(kind & ENTRY_KIND_MASK, value) }; + } + while let Some((_, value)) = self.spare.peek() { + self.spare.consume(); + // SAFETY: give_back() put each there, taken once. + drop(unsafe { Box::from_raw(value as *mut GlDrawJob) }); + } + } +} + +/// Free a ring entry's job without running it. +/// +/// # Safety +/// The entry came from submit() or submit_draw() and is taken once. +unsafe fn drop_entry(kind: u32, value: u64) { + unsafe { + if kind == ENTRY_DRAW { + drop(Box::from_raw(value as *mut GlDrawJob)); + } else { + drop(Box::from_raw(value as *mut RasterJob)); + } + } +} + +impl RasterQueue { + /// Whether jobs queue for the raster thread. + pub fn threaded(&self) -> bool { + self.running.load(SeqCst) + } + + /// Jobs submitted and not yet finished. + pub fn pending(&self) -> u32 { + self.pending.load(SeqCst) + } + + /// The estimated work of the jobs submitted and not yet finished. + pub fn queued(&self) -> u64 { + self.queued.load(SeqCst) + } + + /// Hand a job to the raster unit, behind every job before it, at the + /// cost of a job with no pixels of its own (`submit_work`). + pub fn submit(&self, job: impl FnOnce(&mut RasterCtx) + Send + 'static) { + self.submit_work(RASTER_JOB_OVERHEAD, job); + } + + /// Hand a job to the raster unit, behind every job before it, with an + /// estimate of its work in pixels (`rect_cost`, `primitive_cost`). With + /// no raster thread it runs now. A job that writes the colour planes + /// records the rows it wrote in the context (RasterCtx::drawn). The + /// caller is the front end. + pub fn submit_work(&self, cost: u32, job: impl FnOnce(&mut RasterCtx) + Send + 'static) { + if !self.threaded() { + // SAFETY: no raster thread runs, so the front end owns the + // context (see the Sync impl). + let ctx = unsafe { &mut *self.ctx.get() }; + job(ctx); + ctx.publish(); + return; + } + let entry = Box::into_raw(Box::new(Box::new(job) as RasterJob)) as u64; + self.push(ENTRY_JOB, entry, cost); + } + + /// Hand the raster unit a job whose effect the guest can see, a fence's + /// token or a query's answer, behind every job before it. The rows those + /// jobs drew are published before it runs: a guest that sees the + /// completion then arms a swap, and the swap's repaint must already + /// know which rows differ. + pub fn submit_completion(&self, job: impl FnOnce(&mut RasterCtx) + Send + 'static) { + self.submit_work(RASTER_JOB_OVERHEAD, move |ctx| { + ctx.publish(); + job(ctx); + }); + } + + /// Hand a GL draw to the raster unit, behind every job before it. The + /// raster thread gives the job back when drawn (`take_draw`). The + /// caller is the front end, with the raster + /// thread running. + pub fn submit_draw(&self, job: Box, cost: u32) { + debug_assert!(self.threaded()); + self.push(ENTRY_DRAW, Box::into_raw(job) as u64, cost); + } + + /// Tests: bound the queued work to `pixels`. + #[cfg(test)] + pub fn set_budget(&self, pixels: u64) { + self.budget.store(pixels, SeqCst); + } + + /// Wait while the raster unit holds more work than the budget, down to + /// half of it. The caller is the front end; the + /// raster thread needs neither that nor anything the front end holds. A + /// stop, or no raster thread, ends the wait. + fn wait_room(&self) { + let mut g = self.room.lock(); + // Raised before the level is looked at, so a raster unit that + // drains past half after the look sees it and wakes this. + self.room_waiting.store(true, SeqCst); + while self.threaded() && !self.closing.load(SeqCst) && self.queued() > self.budget.load(SeqCst) / 2 { + self.room_kick.wait(&mut g); + } + self.room_waiting.store(false, SeqCst); + } + + fn push(&self, kind: u32, entry: u64, cost: u32) { + let cost = (cost as u64).min(COST_MAX); + let kind = kind | (cost as u32) << ENTRY_COST_SHIFT; + + if self.queued() >= self.budget.load(SeqCst) { + self.wait_room(); + } + self.queued.fetch_add(cost, SeqCst); + self.pending.fetch_add(1, SeqCst); + // The ring is deep; it fills only when the raster unit is far behind, + // and then the front end waits for it, as the board's would. + while !self.ring.try_push(kind, entry) { + std::thread::yield_now(); + } + self.pushed.fetch_add(1, SeqCst); + if self.sleeping.load(SeqCst) { + let _g = self.wake.lock(); + self.kick.notify_one(); + } + } + + /// A finished draw job to fill again, if one is back. The caller is the + /// front end: the only taker. + pub fn take_draw(&self) -> Option> { + let (_, value) = self.spare.peek()?; + self.spare.consume(); + // SAFETY: give_back() put it there, and it is taken once. + Some(unsafe { Box::from_raw(value as *mut GlDrawJob) }) + } + + /// Keep a finished draw job for a later draw, or free it when enough + /// wait already. + pub fn give_back(&self, job: Box) { + let entry = Box::into_raw(job) as u64; + while !self.spare.try_push(0, entry) { + if self.spare.len() >= SPARE_DRAWS - 1 { + // SAFETY: the entry was not pushed, so it is still ours. + drop(unsafe { Box::from_raw(entry as *mut GlDrawJob) }); + return; + } + // The other giver holds the push lock for a moment. + std::hint::spin_loop(); + } + } + + /// Run a job now, on the caller's thread, for a caller that needs its + /// answer: with no raster thread, a GL draw's result and stipple phase. + pub fn run_now(&self, job: impl FnOnce(&mut RasterCtx) -> R) -> R { + self.wait_idle(); + // SAFETY: the queue is idle and this is the front end, which queues + // the jobs, so nothing else runs one (see the Sync impl). + let ctx = unsafe { &mut *self.ctx.get() }; + let result = job(ctx); + ctx.publish(); + result + } + + /// Wait until every job submitted has finished. + pub fn wait_idle(&self) { + if self.pending() == 0 { + return; + } + crate::pf_scope!(crate::perfetto::events::ODSY_RASTER_WAIT, self.pending()); + let mut g = self.idle.lock(); + while self.pending() != 0 { + self.idle_kick.wait(&mut g); + } + } + + /// Tests: finished draw jobs waiting to be filled again. + #[cfg(test)] + pub fn spare_draws(&self) -> usize { + self.spare.len() + } + + /// The rows the raster unit published and the display has not taken: + /// those a window shows go to `marks`, and all join `differ`. It waits + /// for no slice in flight. + pub fn take_published_rows(&self, displayed: u32, differ: &mut Differ, marks: &RowMarks) { + self.published.take(displayed, differ, marks); + } + + /// Tests: the front end is waiting for room in the queue. + #[cfg(test)] + pub fn waiting_for_room(&self) -> bool { + self.room_waiting.load(SeqCst) + } + + /// Tests: the raster thread is running a slice of jobs. + #[cfg(test)] + pub fn in_slice(&self) -> bool { + self.test_in_slice.load(SeqCst) + } + + /// Start the raster thread. + pub fn start(self: &std::sync::Arc) { + let mut thread = self.thread.lock(); + if thread.is_some() { + return; + } + self.exit.store(false, SeqCst); + self.closing.store(false, SeqCst); + self.running.store(true, SeqCst); + let queue = self.clone(); + *thread = Some( + std::thread::Builder::new() + .name(ODYSSEY_RASTER_THREAD.into()) + .spawn(move || queue.run()) + .expect("odyssey raster thread"), + ); + } + + /// Finish every job queued, then stop the raster thread. Later jobs + /// run inside their submit. + pub fn stop(&self) { + let thread = self.thread.lock().take(); + if let Some(t) = thread { + // A front end waiting for room goes on and queues behind the + // rest, which the wait below finishes. + self.closing.store(true, SeqCst); + { + let _g = self.room.lock(); + self.room_kick.notify_all(); + } + self.wait_idle(); + self.running.store(false, SeqCst); + self.exit.store(true, SeqCst); + { + let _g = self.wake.lock(); + self.kick.notify_one(); + } + let _ = t.join(); + } + self.running.store(false, SeqCst); + } + + /// The raster thread: sleep until a job is pushed, then run the ring in + /// slices. + fn run(&self) { + loop { + { + let mut g = self.wake.lock(); + self.sleeping.store(true, SeqCst); + while self.pushed.load(SeqCst) == self.taken.load(SeqCst) && !self.exit.load(SeqCst) { + self.kick.wait(&mut g); + } + self.sleeping.store(false, SeqCst); + } + if self.pushed.load(SeqCst) == self.taken.load(SeqCst) && self.exit.load(SeqCst) { + return; + } + // SAFETY: this is the raster thread, which owns the context + // while it runs (see the Sync impl). + let ctx = unsafe { &mut *self.ctx.get() }; + #[cfg(test)] + self.test_in_slice.store(true, SeqCst); + #[cfg(test)] + { + let ms = self.test_slice_sleep_ms.swap(0, SeqCst); + if ms != 0 { + std::thread::sleep(Duration::from_millis(ms as u64)); + } + } + #[cfg(test)] + let slice = match self.test_slice_limit_ms.load(SeqCst) { + 0 => RASTER_SLICE, + ms => Duration::from_millis(ms as u64), + }; + #[cfg(not(test))] + let slice = RASTER_SLICE; + let deadline = Instant::now() + slice; + crate::pf_begin!(crate::perfetto::events::ODSY_RASTER_SLICE, self.pending()); + while let Some((kind, value)) = self.ring.peek() { + self.ring.consume(); + self.taken.fetch_add(1, SeqCst); + let cost = (kind >> ENTRY_COST_SHIFT) as u64; + let kind = kind & ENTRY_KIND_MASK; + if kind == ENTRY_DRAW { + // SAFETY: submit_draw() put it there, and it is taken once. + let job = unsafe { Box::from_raw(value as *mut GlDrawJob) }; + job.run(ctx); + self.give_back(job); + } else { + // SAFETY: submit() put it there, and it is taken once. + let job = unsafe { Box::from_raw(value as *mut RasterJob) }; + job(ctx); + } + let left = self.queued.fetch_sub(cost, SeqCst) - cost; + if self.pending.load(SeqCst) == 1 { + // The last job: whoever sees the queue empty then looks + // at the planes, so the rows are out before the count + // says so. + ctx.publish(); + } + if self.pending.fetch_sub(1, SeqCst) == 1 { + let _g = self.idle.lock(); + self.idle_kick.notify_all(); + } + if left <= self.budget.load(SeqCst) / 2 && self.room_waiting.load(SeqCst) { + let _g = self.room.lock(); + self.room_kick.notify_all(); + } + if Instant::now() >= deadline { + break; + } + } + #[cfg(test)] + { + let ms = self.test_slice_end_ms.load(SeqCst); + if ms != 0 { + std::thread::sleep(Duration::from_millis(ms as u64)); + } + } + // A slice's rows are out by its end, bounding how long the + // display can go without them. + ctx.publish(); + crate::pf_end!(crate::perfetto::events::ODSY_RASTER_SLICE); + #[cfg(test)] + self.test_in_slice.store(false, SeqCst); + } + } +} + +/// A byte span of one of the board's planes, for a raster job. +#[derive(Clone, Copy)] +pub(super) struct Span { + ptr: *mut u8, + len: usize, +} + +impl Span { + pub fn of(bytes: &mut [u8]) -> Self { + Span { ptr: bytes.as_mut_ptr(), len: bytes.len() } + } + + /// The bytes, for the raster unit. + /// + /// # Safety + /// The span's storage outlives the use, and nothing else writes it + /// meanwhile: the front end waits for the raster unit before it + /// touches the planes, and never moves or frees them while a job is + /// queued (`Board::raster_wait`). + pub unsafe fn bytes<'a>(self) -> &'a mut [u8] { + unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) } + } +} + +/// The board's planes as a raster job reaches them: each colour buffer, +/// depth and stencil plane from its base, the overlay and id bytes, and the +/// object store the texels come from. +#[derive(Clone, Copy)] +pub(super) struct PlaneSpans { + pub colour: [Span; ODSY_BUFFERS as usize], + pub depth: Span, + pub stencil: Span, + pub overlay: Span, + pub did: Span, + pub obj: Span, +} + +// SAFETY: the spans point into the board's planes, which outlive every job +// (Board::raster_wait), and only the raster unit uses them while one runs. +unsafe impl Send for PlaneSpans {} + +impl Board { + /// Wait for the raster unit to finish its queued jobs. The front end + /// calls it before it touches the planes or the object store itself, + /// and before it writes a completion word that must follow the drawing. + pub fn raster_wait(&self) { + self.raster.wait_idle(); + } + + /// The planes as a raster job reaches them. + pub fn plane_spans(&mut self) -> PlaneSpans { + fn rows(m: &mut BoardMemory, plane: u32) -> Span { + let p = m.plane(plane).expect("the board's planes are allocated"); + let end = (p.base + p.len).min(m.size) as usize; + Span::of(&mut m.bytes[p.base as usize..end]) + } + PlaneSpans { + colour: [rows(&mut self.planes.colour, self.planes.colour_plane), rows(&mut self.planes.buffer1, self.planes.buffer1_plane)], + depth: rows(&mut self.planes.depth, self.planes.depth_plane), + stencil: rows(&mut self.planes.stencil, self.planes.stencil_plane), + overlay: Span::of(&mut self.planes.overlay), + did: Span::of(&mut self.planes.did), + obj: Span::of(&mut self.objects.mem.bytes), + } + } +} diff --git a/src/dev/odyssey/reader.rs b/src/dev/odyssey/reader.rs new file mode 100644 index 000000000..ea93c6d2b --- /dev/null +++ b/src/dev/odyssey/reader.rs @@ -0,0 +1,964 @@ +//! What the display thread reads of the board, and nothing else. +//! +//! The display copies the board's planes to the window sixty times a second. +//! It reads the planes the way the raster unit and the front end write them, +//! without a lock, because the writers mark the rows they change after they +//! change them (`RowMarks`) and the display takes the marks before it reads +//! (`ScanoutReader::stage`): a row copied half-written is marked again after +//! the writer's last store and copied whole by the next frame. The rest of +//! what a frame reads is shared the same way, with no lock between the +//! writers and the display: +//! +//! - the rows the raster unit drew, published as atomic words at +//! completions, at drain and at the end of a slice (`RasterCtx::publish`, +//! `PublishedRows`); +//! - the lookup RAM, the display modes and the cursor (`SharedRegs`), one +//! atomic word an entry, with a generation that moves after each store; +//! - the scanout mode and the retrace restart (`DisplayShared::publish_mode`); +//! - the swap latch and the displayed-buffer bits, which are the host +//! interface's (`HostInterface::retrace`): the buffer swap is the +//! hardware's own synchronisation point, so it keeps its register lock. +//! +//! The display never calls a `Board` method. Plane memory is fixed at +//! `Board::allocate`, and `PlaneView` holds its base pointers, lengths and +//! geometry from then on; `PlaneView::copy_row` is the one function that +//! reads memory a writer may be storing to. + +use crate::devlog::LogModule; +use super::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use super::consts::{ + cursor, dispmode, lut, overlay, ODSY_CMAP_UNVERIFIED_GROUP, ODSY_DID_MAIN_SHIFT, + ODSY_DID_OVERLAY_MASK, ODSY_GAMMA_BASE, ODSY_GAMMA_GROUP, ODSY_INDEX_MASK, +}; +use super::display::{display_channel, ResolveCtx, ScanoutJob, ScanoutStage}; +use super::interface::HostInterface; +use super::memory::{plane_bpp, Plane}; +use super::raster_unit::{ROW_WORDS, RasterQueue}; +use super::registers::cursor_rgb; +use super::scanout::{DirtySite, DirtyStats, RetraceClock, RowMarks, ScanoutMode}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering, Ordering::{Acquire, Relaxed, Release}}; +use std::sync::{Arc, OnceLock}; + +use parking_lot::Mutex; + +/// The rows of each main drawable id where its two colour buffers may +/// differ, a bit a row (see [`ScanoutReader::differ`]). +pub(super) type Differ = [[u64; ROW_WORDS]; dispmode::COUNT as usize]; + +/// The back end's registers the scanout resolves through: the colour +/// lookup RAM, the per-drawable display modes, the overlay modes and the +/// hardware cursor. +#[derive(Clone)] +pub(super) struct DisplayRegs { + /// The colour lookup RAM (0x2000 entries), one 30-bit colour per entry. + pub lut: Vec, + pub dispmode: [u32; 32], + /// The overlay ids' display modes, backend entries 0x2920 + id. + pub overlay_mode: [u32; 8], + /// The cursor's backend entries 0x2500-0x25ff. + pub cursor: [u32; 0x100], +} + +impl Default for DisplayRegs { + fn default() -> Self { + DisplayRegs { lut: vec![0; lut::SIZE as usize], dispmode: [0; 32], overlay_mode: [0; 8], cursor: [0; 0x100] } + } +} + +impl DisplayRegs { + /// Back to the reset values: everything zero, cursor off. + pub fn reset(&mut self) { + self.lut.fill(0); + self.dispmode = [0; 32]; + self.overlay_mode = [0; 8]; + self.cursor = [0; 0x100]; + } + + /// The top eight bits of a lookup-RAM entry's ten-bit channels, as RGB. + pub fn lut_rgb(&self, entry: u32) -> u32 { + let word = if entry < lut::SIZE { self.lut[entry as usize] } else { 0 }; + + (((word >> lut::RED_SHIFT) & 0xff) << 16) | (((word >> lut::GREEN_SHIFT) & 0xff) << 8) | ((word >> lut::BLUE_SHIFT) & 0xff) + } + + /// One 8-bit RGB channel through the 1,536-entry gamma table: widened to 12 + /// bits as the X server widens it (c << 4 | c >> 4; the board's own widening + /// is unknown), mapped to the table in three ranges -- v below 0x200, v / 2 + + /// 0x100 below 0x400, else v / 4 + 0x200 -- and shown as the top eight bits of + /// that entry's channel at shift. Inferred: that the table applies to RGB + /// pixels at all (the blank zeroes it to blacken the screen). + pub fn gamma(&self, c: u32, shift: u32) -> u32 { + let v = (c << 4) | (c >> 4); + let t = if v < 0x200 { + v + } else if v < 0x400 { + v / 2 + 0x100 + } else { + v / 4 + 0x200 + }; + + (self.lut[(ODSY_GAMMA_BASE + t) as usize] >> shift) & 0xff + } + + /// The colormap group main drawable main_id shows through: bits 8:4 of its + /// display mode. Below ODSY_GAMMA_GROUP the plane word is an index. + pub fn display_group(&self, main_id: u32) -> u32 { + (self.dispmode[main_id as usize] >> dispmode::GROUP_SHIFT) & dispmode::GROUP_MASK + } + + /// The position's components, each a signed 16-bit value (inferred). + pub fn cursor_x(&self) -> i32 { + (self.cursor[cursor::POSITION as usize] >> 16) as i16 as i32 + } + + pub fn cursor_y(&self) -> i32 { + self.cursor[cursor::POSITION as usize] as i16 as i32 + } + + /// Whether anything is drawn: bit 0 set and bit 5 clear. APPROXIMATION: on + /// the second channel the cursor is on a display the model does not have. + pub fn cursor_shown(&self) -> bool { + let ctl = self.cursor[cursor::CONTROL as usize]; + + (ctl & cursor::CTL_ENABLE) != 0 && (ctl & cursor::CTL_CHANNEL1) == 0 + } + + /// The first and last display line the cursor covers as it stands in a + /// frame whose last line is `bottom`, or None when it covers none. + pub fn cursor_span(&self, bottom: i32) -> Option<(i32, i32)> { + cursor_span_of(self.cursor[cursor::CONTROL as usize], self.cursor[cursor::POSITION as usize], bottom) + } + + /// Draw the cursor over display line y, already resolved into dst. With bit + /// 2 it is a crosshair: APPROXIMATION, a one-pixel horizontal and vertical + /// line across the whole raster through the position, in colour word 1. The + /// crosshair's extent, width and colour are unknown. + pub fn cursor_row(&self, y: u32, dst: &mut [u32], width: u32) { + let cx = self.cursor_x(); + let cy = self.cursor_y(); + let row = (y as i32).wrapping_sub(cy); + + if !self.cursor_shown() { + return; + } + if self.cursor[cursor::CONTROL as usize] & cursor::CTL_CROSSHAIR != 0 { + let colour = cursor_rgb(self.cursor[cursor::COLOURS as usize + 1]); + + if row == 0 { + dst[..width as usize].fill(colour); + } else if cx >= 0 && cx < width as i32 { + dst[cx as usize] = colour; + } + return; + } + if row < 0 || row >= cursor::SIZE as i32 { + return; + } + for col in 0..cursor::SIZE as i32 { + let x = cx.wrapping_add(col); + let plane = (self.cursor[(cursor::PLANE as i32 + row * 2 + col / 16) as usize] >> ((col % 16) * 2)) & 3; + let glyph = (self.cursor[(cursor::GLYPH as i32 + row * 4 + col / 8) as usize] >> ((col % 8) * 4)) & 0xf; + + if x < 0 || x >= width as i32 || plane != 3 || glyph == 0xf { + continue; + } + dst[x as usize] = cursor_rgb(self.cursor[cursor::COLOURS as usize + glyph as usize]); + } + } +} + +/// The first and last display line a cursor with these control and position +/// words covers in a frame whose last line is `bottom`, or None when it +/// covers none. +fn cursor_span_of(control: u32, position: u32, bottom: i32) -> Option<(i32, i32)> { + if (control & cursor::CTL_ENABLE) == 0 || (control & cursor::CTL_CHANNEL1) != 0 { + return None; + } + if control & cursor::CTL_CROSSHAIR != 0 { + // The vertical line crosses every line. + return Some((0, bottom)); + } + let y = position as i16 as i32; + Some((y.max(0), (y + cursor::SIZE as i32 - 1).min(bottom))) +} + +/// The back end's registers as the front end stores them and the display +/// reads them: one atomic word per entry, no lock. The board's back end +/// reads its lookup RAM and cursor registers while the front end writes +/// them, so a frame that races a store shows the old or the new entry; the +/// store marks every line it changes afterwards, and the next frame +/// resolves them again. +/// +/// `gen` moves after each store. The display reads it before it copies +/// ([`SharedRegs::snapshot`]), so a store that lands during the copy leaves +/// the generation ahead of the one the copy is filed under, and the next +/// frame copies again. +pub(super) struct SharedRegs { + lut: Box<[AtomicU32]>, + dispmode: [AtomicU32; 32], + overlay_mode: [AtomicU32; 8], + cursor: [AtomicU32; 0x100], + gen: AtomicU64, + /// Tests: run once the copy is done, where a store can land that the + /// copy missed. + #[cfg(test)] + pub test_after_copy: Mutex>>, +} + +impl Default for SharedRegs { + fn default() -> Self { + SharedRegs { + lut: (0..lut::SIZE).map(|_| AtomicU32::new(0)).collect(), + dispmode: std::array::from_fn(|_| AtomicU32::new(0)), + overlay_mode: std::array::from_fn(|_| AtomicU32::new(0)), + cursor: std::array::from_fn(|_| AtomicU32::new(0)), + gen: AtomicU64::new(0), + #[cfg(test)] + test_after_copy: Mutex::new(None), + } + } +} + +impl SharedRegs { + /// Store a register. Mark the lines the store changes afterwards. + pub fn set_lut(&self, index: usize, value: u32) { + self.lut[index].store(value, Relaxed); + self.gen.fetch_add(1, Release); + } + + pub fn set_dispmode(&self, index: usize, value: u32) { + self.dispmode[index].store(value, Relaxed); + self.gen.fetch_add(1, Release); + } + + pub fn set_overlay_mode(&self, index: usize, value: u32) { + self.overlay_mode[index].store(value, Relaxed); + self.gen.fetch_add(1, Release); + } + + pub fn set_cursor(&self, index: usize, value: u32) { + self.cursor[index].store(value, Relaxed); + self.gen.fetch_add(1, Release); + } + + /// Back to the reset values (DisplayRegs::reset()). + pub fn reset(&self) { + for w in self.lut.iter().chain(&self.dispmode).chain(&self.overlay_mode).chain(&self.cursor) { + w.store(0, Relaxed); + } + self.gen.fetch_add(1, Release); + } + + pub fn cursor(&self, index: usize) -> u32 { + self.cursor[index].load(Relaxed) + } + + /// The glyph the display draws, for the host's pointer steering + /// (`GfxDisplay::hw_cursor`): where it sits and which pixels are + /// opaque, by the same plane and glyph rules `cursor_row` draws with. + /// None while the cursor is off, on the second channel, or a crosshair, + /// which has no glyph. + pub fn hw_cursor(&self) -> Option { + let control = self.cursor(cursor::CONTROL as usize); + + if (control & cursor::CTL_ENABLE) == 0 || (control & (cursor::CTL_CHANNEL1 | cursor::CTL_CROSSHAIR)) != 0 { + return None; + } + let position = self.cursor(cursor::POSITION as usize); + let mask = (0..cursor::SIZE as usize) + .map(|row| { + (0..cursor::SIZE as usize).fold(0u64, |m, col| { + let plane = (self.cursor(cursor::PLANE as usize + row * 2 + col / 16) >> ((col % 16) * 2)) & 3; + let glyph = (self.cursor(cursor::GLYPH as usize + row * 4 + col / 8) >> ((col % 8) * 4)) & 0xf; + + m | u64::from(plane == 3 && glyph != 0xf) << col + }) + }) + .collect(); + + Some(crate::gfx_display::HwCursor { + x: (position >> 16) as i16 as i32, + y: position as i16 as i32, + size: cursor::SIZE as usize, + mask, + }) + } + + /// The colormap group main drawable `main_id` shows through + /// (DisplayRegs::display_group()). + pub fn display_group(&self, main_id: u32) -> u32 { + (self.dispmode[main_id as usize].load(Relaxed) >> dispmode::GROUP_SHIFT) & dispmode::GROUP_MASK + } + + /// The lines the cursor covers (DisplayRegs::cursor_span()). + pub fn cursor_span(&self, bottom: i32) -> Option<(i32, i32)> { + cursor_span_of(self.cursor(cursor::CONTROL as usize), self.cursor(cursor::POSITION as usize), bottom) + } + + /// Copy the registers into `out` when a store has landed since `seen` + /// (a generation `snapshot` returned), and return the generation the + /// copy is filed under: the one read before the copy began. + pub fn snapshot(&self, out: &mut DisplayRegs, seen: u64) -> u64 { + let gen = self.gen.load(Acquire); + + if gen != seen { + for (o, w) in out.lut.iter_mut().zip(self.lut.iter()) { + *o = w.load(Relaxed); + } + for (o, w) in out.dispmode.iter_mut().zip(&self.dispmode) { + *o = w.load(Relaxed); + } + for (o, w) in out.overlay_mode.iter_mut().zip(&self.overlay_mode) { + *o = w.load(Relaxed); + } + for (o, w) in out.cursor.iter_mut().zip(&self.cursor) { + *o = w.load(Relaxed); + } + #[cfg(test)] + if let Some(hook) = self.test_after_copy.lock().as_mut() { + hook(self); + } + } + gen + } + + /// The registers as they are now, for the tests and the benchmark, + /// which resolve on a board they hold. + #[cfg(test)] + pub fn read(&self) -> DisplayRegs { + let mut regs = DisplayRegs::default(); + + self.snapshot(&mut regs, u64::MAX); + regs + } +} + +/// The scanout mode as the front end last set it, and the counts a display +/// compares to notice a new one. The fields are separate atomic words: the +/// counts move after the mode is stored and the display reads them before +/// it reads the mode, so a mode read mid-store is read again at the next +/// frame. +struct ModeCell { + width: AtomicU32, + height: AtomicU32, + frame_clocks: AtomicU64, + clock_hz: AtomicU32, + /// Moves when the mode is set. + gen: AtomicU64, + /// Moves when the retrace phase restarts: a new mode or a reset. + restart: AtomicU64, +} + +impl ModeCell { + fn new() -> Self { + ModeCell { + width: AtomicU32::new(0), + height: AtomicU32::new(0), + frame_clocks: AtomicU64::new(0), + clock_hz: AtomicU32::new(0), + gen: AtomicU64::new(1), + restart: AtomicU64::new(1), + } + } +} + +/// One byte range of board memory, as the display sees it. +#[derive(Clone, Copy)] +struct RawBytes { + ptr: *const u8, + len: usize, +} + +impl RawBytes { + fn of(bytes: &[u8]) -> Self { + RawBytes { ptr: bytes.as_ptr(), len: bytes.len() } + } +} + +/// Which plane a row is copied from. +#[derive(Clone, Copy)] +pub(super) enum RowOf { + Colour(usize), + Did, + Overlay, +} + +/// The board's colour, id and overlay planes as the display reaches them: +/// base pointers, lengths and the colour planes' geometry, captured when +/// the board allocates them and valid while the board lives. Plane memory +/// is allocated once and never resized or moved; reset fills it in place. +pub(super) struct PlaneView { + colour: [RawBytes; 2], + planes: [Plane; 2], + did: RawBytes, + overlay: RawBytes, +} + +// SAFETY: the view holds pointers into the board's plane memory, which +// is allocated once (Board::allocate) and lives as long as the board; the +// display thread holds a handle to the board for as long as it runs. The +// view only copies bytes out. +unsafe impl Send for PlaneView {} +unsafe impl Sync for PlaneView {} + +impl PlaneView { + /// The view of `b`'s planes, once they are allocated. + pub fn of(b: &Board) -> Option { + let planes = [b.buffer_plane(0)?, b.buffer_plane(1)?]; + + Some(PlaneView { + colour: [RawBytes::of(&b.planes.colour.bytes), RawBytes::of(&b.planes.buffer1.bytes)], + planes, + did: RawBytes::of(&b.planes.did), + overlay: RawBytes::of(&b.planes.overlay), + }) + } + + /// Where colour buffer b's line y starts, and whether its words are + /// big-endian, when `width` pixels of it are whole in a four-byte plane. + fn colour_at(&self, b: usize, y: u32, width: usize) -> Option<(usize, bool)> { + let p = &self.planes[b]; + let at = usize::try_from(p.offset(0, y as i32, 4)).ok()?; + + if plane_bpp(p.format) != 4 || !p.contains(width as i32 - 1, y as i32) { + return None; + } + (at.checked_add(width * 4)? <= self.colour[b].len).then_some((at, !p.is_le())) + } + + /// Copy row y of `what` into `out`: `out.len() / 4` colour words, or + /// `out.len()` id or overlay bytes. For a colour row, whether its words + /// are big-endian; None unless the row is whole in a four-byte plane + /// (and for the id and overlay bytes, unless it is in them). + /// + /// This is the one place the display reads memory a writer may be + /// storing to. The race is tolerated: the writer marks the row after + /// its last store, so a copy that saw part of a write is made again at + /// the next frame. Every comparison and resolve works on the copy. + pub fn copy_row(&self, what: RowOf, y: u32, out: &mut [u8]) -> Option { + let (raw, at, be) = match what { + RowOf::Colour(b) => { + let (at, be) = self.colour_at(b, y, out.len() / 4)?; + (self.colour[b], at, be) + } + RowOf::Did | RowOf::Overlay => { + let raw = if let RowOf::Did = what { self.did } else { self.overlay }; + (raw, y as usize * ODSY_WIDTH as usize, false) + } + }; + if at.checked_add(out.len())? > raw.len { + return None; + } + // SAFETY: the range is inside the allocation (checked above), which + // lives as long as the board and is never moved; `out` is ours. + unsafe { std::ptr::copy_nonoverlapping(raw.ptr.add(at), out.as_mut_ptr(), out.len()) }; + Some(be) + } + + /// Colour buffer b's word at (x, y), or 0 outside the plane. + fn load(&self, b: usize, x: i64, y: i64) -> u32 { + let p = &self.planes[b]; + let bpp = plane_bpp(p.format); + let mut bytes = [0u8; 4]; + + if x < 0 || y < 0 || x > i32::MAX as i64 || y > i32::MAX as i64 || !p.contains(x as i32, y as i32) { + return 0; + } + let Ok(at) = usize::try_from(p.offset(x as i32, y as i32, bpp)) else { return 0 }; + if at.checked_add(bpp as usize).is_none_or(|end| end > self.colour[b].len) { + return 0; + } + // SAFETY: as copy_row(). + unsafe { std::ptr::copy_nonoverlapping(self.colour[b].ptr.add(at), bytes.as_mut_ptr(), bpp as usize) }; + p.get(bpp, &bytes) + } + + /// One id or overlay byte, 0 outside. + fn byte(&self, what: RowOf, at: usize) -> u8 { + let mut b = [0u8]; + + self.copy_row_bytes(what, at, &mut b); + b[0] + } + + fn copy_row_bytes(&self, what: RowOf, at: usize, out: &mut [u8]) { + let raw = if let RowOf::Did = what { self.did } else { self.overlay }; + + if at.checked_add(out.len()).is_some_and(|end| end <= raw.len) { + // SAFETY: as copy_row(). + unsafe { std::ptr::copy_nonoverlapping(raw.ptr.add(at), out.as_mut_ptr(), out.len()) }; + } + } + + /// resolve_row_generic() on the view: the per-pixel form, which any + /// plane shape can use. Row y of `width` pixels into `dst`, with + /// colour buffer (displayed >> id) & 1 showing for each pixel's drawable. + /// Whether a pixel showed through colour map group 25. + pub fn resolve_generic(&self, regs: &DisplayRegs, displayed: u32, y: u32, width: u32, dst: &mut [u32]) -> bool { + let mut saw_unverified = false; + + for x in 0..width { + let at = (y * ODSY_WIDTH + x) as usize; + let did = self.byte(RowOf::Did, at); + let main_id = ((did as u32) >> ODSY_DID_MAIN_SHIFT) & (dispmode::COUNT - 1); + let word = self.load(((displayed >> main_id) & 1) as usize, x as i64, y as i64); + let group = regs.display_group(main_id); + + let omode = regs.overlay_mode[((did as u32) & ODSY_DID_OVERLAY_MASK) as usize]; + let opixel = self.byte(RowOf::Overlay, at); + + if (omode & overlay::MODE_ENABLE) != 0 && opixel != 0 { + dst[x as usize] = regs.lut_rgb(256 * ((omode >> overlay::MODE_GROUP_SHIFT) & overlay::MODE_GROUP_MASK) + opixel as u32); + continue; + } + if group >= ODSY_GAMMA_GROUP { + let mode = regs.dispmode[main_id as usize]; + let r = display_channel(mode, word >> 24); + let g = display_channel(mode, (word >> 16) & 0xff); + let b = display_channel(mode, (word >> 8) & 0xff); + + dst[x as usize] = regs.gamma(r, lut::RED_SHIFT) << 16 | regs.gamma(g, lut::GREEN_SHIFT) << 8 | regs.gamma(b, lut::BLUE_SHIFT); + continue; + } + saw_unverified |= group == ODSY_CMAP_UNVERIFIED_GROUP; + dst[x as usize] = regs.lut_rgb(if group != 0 { 256 * group + (word & 0xff) } else { word & ODSY_INDEX_MASK }); + } + regs.cursor_row(y, dst, width); + saw_unverified + } +} + +/// What the board shares with the display thread. +pub(super) struct DisplayShared { + pub marks: Arc, + pub regs: SharedRegs, + mode: ModeCell, + pub planes: OnceLock, + pub front: Arc>, + pub raster: Arc, + /// Swaps latched at a retrace. + pub swaps: AtomicU64, + /// Colour map group 25 was resolved and reported. + pub group25: AtomicBool, +} + +impl DisplayShared { + pub fn new(marks: Arc, front: Arc>, raster: Arc) -> Self { + DisplayShared { + marks, + regs: SharedRegs::default(), + mode: ModeCell::new(), + planes: OnceLock::new(), + front, + raster, + swaps: AtomicU64::new(0), + group25: AtomicBool::new(false), + } + } + + /// A new mode: the display reallocates its frame and restarts the + /// retrace phase when it next looks. + pub fn publish_mode(&self, mode: ScanoutMode) { + let cell = &self.mode; + + cell.width.store(mode.width, Relaxed); + cell.height.store(mode.height, Relaxed); + cell.frame_clocks.store(mode.frame_clocks, Relaxed); + cell.clock_hz.store(mode.clock_hz, Relaxed); + cell.restart.fetch_add(1, Release); + cell.gen.fetch_add(1, Release); + } + + /// The retrace phase restarts, as for a reset. + pub fn restart_retrace(&self) { + self.mode.restart.fetch_add(1, Release); + } + + /// A vertical retrace: the host interface latches the swaps armed and + /// raises the retrace source (HostInterface::retrace()). + pub fn retrace(&self) { + if self.front.lock().retrace() { + self.swaps.fetch_add(1, Ordering::Relaxed); + } + } + + /// Report colour map group 25 once. + pub fn report_group25(&self) { + if !self.group25.swap(true, Ordering::Relaxed) { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: colormap group 25 (0x1900) resolved as an index table; unverified"); + } + } +} + +/// The display's own state: the presented frame, the retrace clock, the +/// rows where a window's buffers differ, and the lines of the update in +/// progress. Only the display thread (or a test standing in for it) uses it. +#[derive(Default)] +pub(super) struct ScanoutReader { + shared: Option>, + /// The presented frame: `frame_width` by `frame_height` of 0x00RRGGBB; + /// empty with no mode. + pub frame: Vec, + pub frame_width: u32, + pub frame_height: u32, + mode_gen: u64, + restart_gen: u64, + /// The mode of the frame, and its retrace clock. + pub clock: RetraceClock, + /// The frame was just allocated: every line is resolved, whatever the + /// marks say. + reshaped: bool, + /// Per main drawable id, the rows where the two colour buffers may + /// differ, a bit a row. A swap of a window shows its other buffer, so + /// only those rows can change (flip_rows()). The rows the raster unit + /// published join it; one a writer touched without naming its buffer is + /// measured at the swap (measure_differ()). + pub differ: Box, + /// The displayed-buffer bits the frame was last staged from. + staged_displayed: u32, + /// The displayed-buffer bits of the frame in progress: read once, after + /// the retrace latched, and used for the flip, the lines staged and the + /// resolve. + displayed: u32, + pub stats: DirtyStats, + swaps_seen: u64, + /// The registers of the frame in progress, copied after the marks were + /// taken. + snap: DisplayRegs, + snap_gen: u64, + stage: ScanoutStage, + rows: Vec, + dirty: Vec, + /// The last frame's update, kept to take its buffers back at the start + /// of the next. + last: Option, + scratch: [Vec; 3], + /// Tests: run between the display taking the marks and copying the + /// registers, where a front-end store can land. + #[cfg(test)] + pub test_between: Option>, +} + +impl ScanoutReader { + /// Read the board `shared` belongs to. + pub fn attach(&mut self, shared: &Arc) { + if self.shared.as_ref().is_some_and(|s| Arc::ptr_eq(s, shared)) { + return; + } + self.shared = Some(shared.clone()); + self.mode_gen = 0; + self.restart_gen = 0; + self.snap_gen = u64::MAX; + self.frame = Vec::new(); + self.frame_width = 0; + self.frame_height = 0; + self.staged_displayed = 0; + *self.differ = [[0; ROW_WORDS]; dispmode::COUNT as usize]; + } + + fn shared(&self) -> Arc { + self.shared.clone().expect("a reader reads a board it is attached to") + } + + /// Take a new mode and retrace restart the front end published. + fn sync(&mut self, shared: &DisplayShared) { + let cell = &shared.mode; + let (gen, restart) = (cell.gen.load(Acquire), cell.restart.load(Acquire)); + let mode = ScanoutMode { + width: cell.width.load(Relaxed), + height: cell.height.load(Relaxed), + frame_clocks: cell.frame_clocks.load(Relaxed), + clock_hz: cell.clock_hz.load(Relaxed), + }; + + if gen != self.mode_gen { + self.mode_gen = gen; + self.clock.mode = mode; + let (w, h) = (mode.width, mode.height); + if w != self.frame_width || h != self.frame_height { + self.frame = Vec::new(); + self.frame_width = 0; + self.frame_height = 0; + if w != 0 && h != 0 { + self.frame = vec![0u32; w as usize * h as usize]; + self.frame_width = w; + self.frame_height = h; + } + } + self.reshaped = true; + } + if restart != self.restart_gen { + self.restart_gen = restart; + self.clock.restart(); + } + } + + /// Every retrace due at `now_ns`, in turn: the host interface latches + /// the swaps armed and raises the retrace source. + pub fn poll(&mut self, now_ns: i64) { + let shared = self.shared(); + + self.sync(&shared); + while self.clock.next_retrace(now_ns) { + shared.retrace(); + } + } + + /// The registers as of now, copied when the front end has stored to + /// them since the last copy. + fn snapshot(&mut self, shared: &DisplayShared) { + self.snap_gen = shared.regs.snapshot(&mut self.snap, self.snap_gen); + } + + /// The rows the raster unit published since the last call: the rows + /// where a window's buffers may differ, and those it shows, to resolve. + pub fn take_published(&mut self, displayed: u32) { + let shared = self.shared(); + + shared.raster.take_published_rows(displayed, &mut self.differ, &shared.marks); + } + + /// A frame's update up to the plane reads: take the rows published and + /// the marks, copy each marked line's plane bytes aside, so that the + /// resolve itself (`take()`, `ScanoutJob::resolve()`) can run while the + /// writers go on. A line of an unusual plane shape is resolved at once, + /// pixel by pixel. A processor's write of the displayed-buffer register + /// reached only the front: the windows whose buffer it changed repaint + /// the rows their buffers differ in. + /// + /// The marks are taken before the planes are read and the registers + /// copied after (a lookup store changes the table and then marks every + /// line), and the displayed bits are read once. + pub fn stage(&mut self) { + let shared = self.shared(); + + self.sync(&shared); + let displayed = { + let mut f = shared.front.lock(); + f.displayed_written = false; + f.displayed_buffer + }; + self.displayed = displayed; + let swaps = shared.swaps.load(Ordering::Relaxed); + self.stats.swaps += swaps.wrapping_sub(std::mem::replace(&mut self.swaps_seen, swaps)); + self.take_published(displayed); + let flipped = std::mem::replace(&mut self.staged_displayed, displayed) ^ displayed; + if flipped != 0 && !self.frame.is_empty() { + self.flip_rows(&shared, flipped); + } + let mut stage = std::mem::take(&mut self.stage); + stage.lines.clear(); + stage.bytes.clear(); + stage.be.clear(); + if !self.frame.is_empty() { + let h = self.clock.mode.height as usize; + let w = self.clock.mode.width as usize; + let width = self.clock.mode.width.min(ODSY_WIDTH) as usize; + let mut dirty = std::mem::take(&mut self.dirty); + + dirty.clear(); + shared.marks.take_dirty(h, &mut dirty); + if std::mem::take(&mut self.reshaped) { + dirty.clear(); + dirty.extend(0..h as u32); + } + #[cfg(test)] + if let Some(hook) = self.test_between.as_mut() { + hook(); + } + self.snapshot(&shared); + let view = shared.planes.get(); + let mut row = Vec::new(); + for &y in &dirty { + let staged = view.is_some_and(|view| stage.push_row(view, y, width)); + if !staged { + row.resize(w, 0); + if let Some(view) = view { + if view.resolve_generic(&self.snap, displayed, y, width as u32, &mut row) { + shared.report_group25(); + } + } + self.frame[y as usize * w..(y as usize + 1) * w].copy_from_slice(&row); + } + stage.lines.push((y, staged)); + } + self.dirty = dirty; + } + self.stats.staged(stage.lines.len()); + self.stage = stage; + } + + /// Repaint what a flip of the displayed buffers `flipped` (a bit per + /// main drawable id) changes: the rows where that window's two buffers + /// differ. A row where they are equal shows the same pixels either way. + fn flip_rows(&mut self, shared: &DisplayShared, flipped: u32) { + if self.clock.mode.height > ODSY_HEIGHT { + return shared.marks.repaint(DirtySite::Flip, 0, self.clock.mode.height as i32); + } + if shared.marks.unnamed_pending() { + self.measure_differ(shared); + } + let mut rows = [0u64; ROW_WORDS]; + for (id, differ) in self.differ.iter().enumerate() { + if (flipped >> id) & 1 != 0 { + for (r, d) in rows.iter_mut().zip(differ) { + *r |= d; + } + } + } + for (i, mut w) in rows.into_iter().enumerate() { + while w != 0 { + shared.marks.repaint(DirtySite::Flip, (i as u32 * 64 + w.trailing_zeros()) as i32, 1); + w &= w - 1; + } + } + } + + /// Find, in the lines a writer did not name its buffer or drawable for + /// (RowMarks::unnamed), the rows where each main drawable's pixels + /// differ between the two colour buffers. A row the planes do not cover + /// whole differs for every id. + /// + /// The marks are taken first (a write that lands after sets its line + /// again, for the next swap), the rows are copied, and the result is + /// set in `differ`, which only the display writes. + fn measure_differ(&mut self, shared: &DisplayShared) { + let width = self.clock.mode.width.min(ODSY_WIDTH) as usize; + let unnamed = shared.marks.take_unnamed(); + let [l0, l1, did] = &mut self.scratch; + + l0.resize(width * 4, 0); + l1.resize(width * 4, 0); + did.resize(width, 0); + for (i, mut word) in unnamed.into_iter().enumerate() { + while word != 0 { + let y = (i * 64 + word.trailing_zeros() as usize) as u32; + word &= word - 1; + if y >= self.clock.mode.height { + continue; + } + let bit = (y as usize >> 6, 1u64 << (y & 63)); + let planes = shared.planes.get().and_then(|view| { + let be0 = view.copy_row(RowOf::Colour(0), y, l0)?; + let be1 = view.copy_row(RowOf::Colour(1), y, l1)?; + view.copy_row(RowOf::Did, y, did)?; + (be0 == be1).then_some(()) + }); + // The ids whose pixels differ in this row. + let mut ids = 0u32; + + match planes { + None => ids = u32::MAX, + Some(()) if l0 != l1 => { + let mut x = 0; + while x < width { + let d = did[x]; + let end = x + 1 + did[x + 1..].iter().take_while(|&&v| v == d).count(); + if l0[x * 4..end * 4] != l1[x * 4..end * 4] { + ids |= 1 << ((d >> ODSY_DID_MAIN_SHIFT) as usize & (dispmode::COUNT as usize - 1)); + } + x = end; + } + } + Some(()) => {} + } + for (id, d) in self.differ.iter_mut().enumerate() { + d[bit.0] = d[bit.0] & !bit.1 | if (ids >> id) & 1 != 0 { bit.1 } else { 0 }; + } + } + } + } + + /// The staged lines and everything their resolve reads outside the + /// planes, to resolve (ScanoutJob::resolve()). None when no line was + /// dirty. + pub fn take(&mut self) -> Option { + if self.stage.lines.is_empty() { + return None; + } + let stage = std::mem::take(&mut self.stage); + let ctx = stage.lines.iter().any(|&(_, staged)| staged).then(|| ResolveCtx::new(&self.snap, self.displayed)); + Some(ScanoutJob { + stage, + ctx, + w: self.clock.mode.width as usize, + width: self.clock.mode.width.min(ODSY_WIDTH), + frame: (self.frame_width, self.frame_height), + rows: std::mem::take(&mut self.rows), + saw_unverified: false, + committed: false, + }) + } + + /// Put a resolved job's lines into the presented frame, with the cursor + /// over them. If the frame changed shape meanwhile the lines are dropped + /// and the whole new frame is dirty. The span of lines changed. Afterwards + /// the job's rows (ScanoutJob::lines()) are the presented lines, for the + /// display to convert; hand the job to `finish()` when done. + pub fn commit(&mut self, job: &mut ScanoutJob) -> Option<(usize, usize)> { + let shared = self.shared(); + let mut span: Option<(usize, usize)> = None; + job.committed = false; + if job.w == 0 || job.frame != (self.frame_width, self.frame_height) || job.w != self.clock.mode.width as usize { + shared.marks.repaint(DirtySite::Reshape, 0, self.clock.mode.height as i32); + return None; + } + let w = job.w; + job.rows.resize(job.stage.lines.len() * w, 0); + for (&(y, staged), row) in job.stage.lines.iter().zip(job.rows.chunks_exact_mut(w)) { + let y = y as usize; + if staged { + self.snap.cursor_row(y as u32, row, job.width); + self.frame[y * w..(y + 1) * w].copy_from_slice(row); + } else { + // Resolved into the frame when it was staged. + row.copy_from_slice(&self.frame[y * w..(y + 1) * w]); + } + span = Some(span.map_or((y, y + 1), |(a, b)| (a.min(y), b.max(y + 1)))); + } + if job.saw_unverified { + shared.report_group25(); + } + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: scanout update: {} lines", job.stage.lines.len()); + job.committed = true; + span + } + + /// Take a finished job's buffers back for the next frame. + pub fn recycle(&mut self, job: ScanoutJob) { + self.stage = job.stage; + self.rows = job.rows; + } + + /// Keep the frame's job until the next frame, then take its buffers back. + pub fn keep(&mut self, job: Option) { + self.last = job; + } + + /// The previous frame's job, back to the reader. + pub fn recycle_last(&mut self) { + if let Some(done) = self.last.take() { + self.recycle(done); + } + } + + /// The whole update for a caller that holds the planes throughout: + /// stage, resolve and commit. The span of lines changed. + pub fn update(&mut self) -> Option<(usize, usize)> { + self.stage(); + self.resolve() + } + + /// The rest of an update after stage(): resolve the staged lines into the + /// presented frame. + pub fn resolve(&mut self) -> Option<(usize, usize)> { + let mut job = self.take(); + if let Some(job) = &mut job { + job.resolve(); + } + let mut job = job?; + let span = self.commit(&mut job); + self.recycle(job); + span + } +} diff --git a/src/dev/odyssey/record.rs b/src/dev/odyssey/record.rs new file mode 100644 index 000000000..9ddf68933 --- /dev/null +++ b/src/dev/odyssey/record.rs @@ -0,0 +1,185 @@ +//! A replayable recording of what reaches the VPro board, for regression +//! tests (`golden/replay.rs`): every bus store the processors make, the +//! loads that are not plain repeats, and the guest memory the board's DMA +//! reads. Replaying the stores from power-on through a fresh board, with the +//! DMA reads answered from the recording, must reproduce the planes the live +//! run drew; the replay checks them against hash checkpoints kept beside the +//! recording. +//! +//! Started with `IRIS_ODSY_REC=` when the board is built (power-on is +//! what a replay starts from). The recording is gzip-compressed whatever the +//! name. It stays out of git, as IMPACT's `.rec` files do; the checkpoints +//! are what the tree keeps (`src/dev/odyssey/testdata/replay/`). +//! +//! File: `ODSYREC` + NUL + u32 version, then records, little-endian: +//! +//! | tag | fields | what | +//! |-----|--------------------------------|---------------------------------------| +//! | `W` | size u8, off u32, val u64 | bus store, `off` within the aperture | +//! | `R` | size u8, off u32, val u64 | bus load and what it answered; an access identical to the one before it is not kept | +//! | `B` | addr u64, n u32, n x u64 | DMA read of n qwords from system memory | +//! | `b` | addr u64, val u8 | DMA read of one byte | +//! +//! Loads the board answered busy are not kept: the processor retries them. + +use std::io::{Read, Write}; +use std::sync::Arc; + +use parking_lot::Mutex; + +use crate::traits::{BusDevice, BusRead8, BUS_OK}; + +const MAGIC: &[u8; 8] = b"ODSYREC\0"; +const VERSION: u32 = 1; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum Rec { + Write { size: u8, off: u32, val: u64 }, + Read { size: u8, off: u32, val: u64 }, + DmaBlock { addr: u64, words: Vec }, + DmaByte { addr: u64, val: u8 }, +} + +pub(super) struct Recorder { + out: flate2::write::GzEncoder>, + pub records: u64, + last_read: Option<(u8, u32, u64)>, +} + +/// Shared so the DMA path can record through `RecordingMem` while the bus +/// path records through the widget. +pub(super) type RecHandle = Arc>; + +impl Recorder { + pub fn create(path: &str) -> std::io::Result { + let file = std::io::BufWriter::with_capacity(1 << 20, std::fs::File::create(path)?); + let mut out = flate2::write::GzEncoder::new(file, flate2::Compression::fast()); + out.write_all(MAGIC)?; + out.write_all(&VERSION.to_le_bytes())?; + Ok(Arc::new(Mutex::new(Recorder { out, records: 0, last_read: None }))) + } + + /// Sync-flush now and then, so a recording cut off by a kill still reads. + fn tick(&mut self) { + if self.records % 4096 == 0 { + let _ = self.out.flush(); + } + } + + pub fn write(&mut self, size: u8, off: u32, val: u64) { + self.records += 1; + self.tick(); + self.last_read = None; + let _ = self.out.write_all(&[b'W', size]).and_then(|_| self.out.write_all(&off.to_le_bytes())).and_then(|_| self.out.write_all(&val.to_le_bytes())); + } + + pub fn read(&mut self, size: u8, off: u32, val: u64) { + if self.last_read == Some((size, off, val)) { + return; + } + self.last_read = Some((size, off, val)); + self.records += 1; + self.tick(); + let _ = self.out.write_all(&[b'R', size]).and_then(|_| self.out.write_all(&off.to_le_bytes())).and_then(|_| self.out.write_all(&val.to_le_bytes())); + } + + fn dma_block(&mut self, addr: u64, words: &[u64]) { + self.records += 1; + let _ = self.out.write_all(b"B").and_then(|_| self.out.write_all(&addr.to_le_bytes())).and_then(|_| self.out.write_all(&(words.len() as u32).to_le_bytes())); + for w in words { + let _ = self.out.write_all(&w.to_le_bytes()); + } + } + + fn dma_byte(&mut self, addr: u64, val: u8) { + self.records += 1; + let _ = self.out.write_all(b"b").and_then(|_| self.out.write_all(&addr.to_le_bytes())).and_then(|_| self.out.write_all(&[val])); + } + + pub fn flush(&mut self) { + let _ = self.out.flush(); + } +} + +impl Drop for Recorder { + fn drop(&mut self) { + let _ = self.out.try_finish(); + } +} + +/// System memory as the board's DMA sees it, recording what it reads. +pub(super) struct RecordingMem { + pub inner: Arc, + pub rec: RecHandle, +} + +impl BusDevice for RecordingMem { + fn read8(&self, addr: u64) -> BusRead8 { + let r = self.inner.read8(addr); + if r.is_ok() { + self.rec.lock().dma_byte(addr, r.data); + } + r + } + fn write8(&self, addr: u64, val: u8) -> u32 { + self.inner.write8(addr, val) + } + fn read_block(&self, addr: u64, buf: &mut [u64]) -> u32 { + let st = self.inner.read_block(addr, buf); + if st == BUS_OK { + self.rec.lock().dma_block(addr, buf); + } + st + } + fn write_block(&self, addr: u64, buf: &[u64]) -> u32 { + self.inner.write_block(addr, buf) + } +} + +/// Parse a whole recording. +pub(super) fn load(path: &str) -> std::io::Result> { + let bad = |what: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, what.to_string()); + let mut data = Vec::new(); + // A recording the emulator was killed in the middle of ends without the + // gzip trailer: keep what decoded. + match std::io::BufReader::new(flate2::read::MultiGzDecoder::new(std::fs::File::open(path)?)).read_to_end(&mut data) { + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof && !data.is_empty() => {} + Err(e) => return Err(e), + } + if data.len() < 12 || &data[..8] != MAGIC { + return Err(bad("not a VPro recording")); + } + if u32::from_le_bytes(data[8..12].try_into().unwrap()) != VERSION { + return Err(bad("unsupported recording version")); + } + let mut out = Vec::new(); + let mut i = 12; + let u32_at = |i: usize| u32::from_le_bytes(data[i..i + 4].try_into().unwrap()); + let u64_at = |i: usize| u64::from_le_bytes(data[i..i + 8].try_into().unwrap()); + while i < data.len() { + let need = match data[i] { + b'W' | b'R' => 14, + b'b' => 10, + b'B' => { + if i + 13 > data.len() { + break; + } + 13 + 8 * u32_at(i + 9) as usize + } + t => return Err(bad(&format!("bad record tag {t:#x} at byte {i}"))), + }; + if i + need > data.len() { + // Cut off mid-record (the emulator was killed): keep what is complete. + break; + } + out.push(match data[i] { + b'W' => Rec::Write { size: data[i + 1], off: u32_at(i + 2), val: u64_at(i + 6) }, + b'R' => Rec::Read { size: data[i + 1], off: u32_at(i + 2), val: u64_at(i + 6) }, + b'b' => Rec::DmaByte { addr: u64_at(i + 1), val: data[i + 9] }, + _ => Rec::DmaBlock { addr: u64_at(i + 1), words: (0..u32_at(i + 9) as usize).map(|k| u64_at(i + 13 + 8 * k)).collect() }, + }); + i += need; + } + Ok(out) +} diff --git a/src/dev/odyssey/registers.rs b/src/dev/odyssey/registers.rs new file mode 100644 index 000000000..a0c86f1cd --- /dev/null +++ b/src/dev/odyssey/registers.rs @@ -0,0 +1,568 @@ +//! Register-level behaviour: identity, cursor, channel and back-end registers, +//! and the reports made for offsets and commands the board does not implement. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. + + +use crate::devlog::LogModule; +use std::sync::atomic::Ordering; +use super::board::{Board, ODSY_HEIGHT, ODSY_UNIMP_REPORTS, ODSY_WIDTH}; +use super::consts::{ + backend, cfifo, config, ctx, cursor, dispmode, flctl, host, i2c, intr, lut, mem, nic, overlay, + route, stats, swap, ODSY_BLOCK_END, ODSY_BUZZ_1, ODSY_BUZZ_2, ODSY_BUZZ_3, ODSY_BUZZ_4, + ODSY_BUZZ_5, ODSY_BUZZ_6, ODSY_CHANNEL_HEIGHT, ODSY_CHANNEL_WIDTH, ODSY_DISPLAYED_BUFFER, + ODSY_F32_ONE, ODSY_F32_POINT_EIGHT, ODSY_F32_POINT_TWO, ODSY_FRAME_CLOCKS, ODSY_MANUFACTURER, + ODSY_PART, ODSY_PIXEL_CLOCK_HZ, ODSY_RESET_CONTROL, ODSY_STATUS0, ODSY_SW_TOKEN, + ODSY_UNIMP_WRITE, +}; +use super::interface::front_register; +use super::reader::SharedRegs; +use super::scanout::{DirtySite, ScanoutMode}; +impl Board { + pub fn identity(&mut self) -> u32 { + (self.widget.revision << 28) | (ODSY_PART << 12) | (ODSY_MANUFACTURER << 1) | 1 + } + + pub fn unimplemented(&self, addr: u64, size: u32, write: bool) { + let token = (addr as u32) | if write { ODSY_UNIMP_WRITE } else { 0 }; + + // A processor's access to an unmodelled offset reaches this while the + // board's thread may be running a store of its own, so the report + // table is atomic and the count bounded on every use; a race can only + // log an offset twice. + let len = self.diag.reported_addr.len(); + let seen = (self.diag.reported_addr_count.load(Ordering::Relaxed) as usize).min(len); + if self.diag.reported_addr[..seen].iter().any(|a| a.load(Ordering::Relaxed) == token) || seen == len { + return; + } + let slot = self.diag.reported_addr_count.fetch_add(1, Ordering::Relaxed) as usize; + if slot >= len { + return; + } + self.diag.reported_addr[slot].store(token, Ordering::Relaxed); + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: {}-byte {} at {:#x} is not modeled{}", + size, + if write { "write" } else { "read" }, + addr, + if slot + 1 == len { "; further offsets stay silent" } else { "" } + ); + } + + /// Report an unmodeled command word once. note follows the message: the + /// parser's framing decision for a word it does not know, or "". + pub fn cmd_report(&mut self, word: u32, note: &str) { + if self.diag.reported_cmd[..self.diag.reported_cmd_count as usize].contains(&word) { + return; + } + if self.diag.reported_cmd_count == ODSY_UNIMP_REPORTS { + return; + } + self.diag.reported_cmd[self.diag.reported_cmd_count as usize] = word; + self.diag.reported_cmd_count += 1; + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: command FIFO word {:#010x} is not modeled{}{}", + word, + note, + if self.diag.reported_cmd_count == ODSY_UNIMP_REPORTS { "; further words stay silent" } else { "" } + ); + } + + pub fn cmd_unimplemented(&mut self, word: u32) { + self.cmd_report(word, ""); + } +} + +/// The modeled register an access names, or None. A configuration-block +/// register answers its 64-bit address and the 32-bit spelling of either +/// half identically (a live V12 board reads the same value at a register's +/// base and at base+4 across the block). Every other register has only been +/// seen as a 32-bit access. Other widths, alignments and offsets are +/// unmodeled rather than an error. +pub(super) fn reg(addr: u64, size: u32) -> Option { + if addr < ODSY_BLOCK_END { + if (size == 8 || size == 4) && (addr & 7) == 0 { + return Some(addr); + } + if size == 4 && (addr & 7) == 4 { + return Some(addr & !7); + } + return None; + } + if size != 4 { + return None; + } + match addr { + intr::FLAGS + | intr::CLEAR + | intr::ENABLE_SET + | intr::ENABLE_CLEAR + | route::DEST_UPPER + | route::DEST_LOWER + | route::VECTORS + | ODSY_STATUS0 + | cfifo::SELFTEST + | mem::CONFIG + | flctl::CONFIG + | flctl::TARGET + | ODSY_SW_TOKEN + | nic::PROM + | nic::KERNEL + | ODSY_BUZZ_1 + | ODSY_BUZZ_2 + | ODSY_BUZZ_3 + | ODSY_BUZZ_4 + | ODSY_BUZZ_5 + | ODSY_BUZZ_6 + | swap::ARM + | swap::PENDING + | swap::CONTROL + | swap::CONTROL_COMPANION + | ODSY_DISPLAYED_BUFFER + | i2c::MAIN_CMD_READ + | i2c::MAIN_STATUS_READ + | i2c::OPT_CMD_READ + | i2c::OPT_STATUS_READ + | host::WRITE_ADDR_UPPER + | host::WRITE_ADDR_LOWER + | stats::ADDR_UPPER + | stats::ADDR_LOWER + | stats::CONTROL + | ctx::SHADOW_ADDR_UPPER + | ctx::SHADOW_ADDR_LOWER + | ODSY_RESET_CONTROL + | mem::CONTROL + | backend::VERSION => Some(addr), + _ => None, + } +} + +impl Board { + /// One bit slot, reported finished inside the write the guest polls after. + pub fn nic_slot(&mut self, value: u32) { + let pulse = (value & nic::PULSE_MASK) >> nic::PULSE_SHIFT; + let sample = (value & nic::SAMPLE_MASK) >> nic::SAMPLE_SHIFT; + let level = match self.nic_bus.as_mut() { + Some(bus) => crate::dev::w1::slot::slot(bus, pulse, sample), + None => false, + }; + + self.widget.nic = (value & !(nic::LEVEL | nic::DONE)) | nic::DONE | if level { nic::LEVEL } else { 0 }; + } + + // The display back end's 32 by 32 hardware cursor, drawn over the resolved picture: after the lookup RAM + // and the gamma table, over the main and overlay planes alike. Entries: + // 0x2500 128 words glyph: 32 rows of four words, eight 4-bit pixels a + // word, the leftmost pixel in bits 3:0 + // 0x2580 position: x in 31:16, y in 15:0 + // 0x2581 control + // 0x2590 15 words colours, ten bits a channel, as in the lookup RAM + // 0x25c0 64 words plane: 32 rows of two words, sixteen 2-bit pixels a + // word, the leftmost pixel in bits 1:0 + // The layouts are established; what the board makes of the values is + // inferred. APPROXIMATION: under plane 3 a glyph value k shows colour word k + // and glyph 0 shows word 0; under plane 0 the pixel is transparent. Glyph + // value 15 and plane values 1 and 2 have no known meaning: reported once and + // drawn transparent. A channel shows the top eight bits of its ten. Reset + // values are unknown; the model resets every entry to zero, cursor off. + + pub fn cursor_report(&mut self, bit: u32, what: &str, index: u32, data: u32) { + if self.diag.cursor_reported & bit != 0 { + return; + } + self.diag.cursor_reported |= bit; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: cursor entry {:#x} word {:#010x}: {}", cursor::BASE + index, data, what); + } + + /// The registers the display resolves through (DisplayRegs). + pub fn regs(&self) -> &SharedRegs { + &self.scanout.shared.regs + } + + /// The first and last display line the cursor covers as it stands, or + /// None when it covers none. + pub fn cursor_span(&mut self) -> Option<(i32, i32)> { + self.regs().cursor_span(self.scanout.mode.height as i32 - 1) + } + + /// Repaint the lines of a span from cursor_span(); a span past the + /// bottom of the raster is empty. + pub fn cursor_invalidate(&mut self, span: Option<(i32, i32)>) { + if let Some((first, last)) = span.filter(|&(first, last)| first <= last) { + self.scanout.repaint_for(DirtySite::Cursor, first, last - first + 1); + } + } +} + +/// A colour word, ten bits a channel, as 0x00RRGGBB from the top eight. +pub(super) fn cursor_rgb(word: u32) -> u32 { + (((word >> lut::RED_SHIFT) & 0xff) << 16) | (((word >> lut::GREEN_SHIFT) & 0xff) << 8) | ((word >> lut::BLUE_SHIFT) & 0xff) +} + +impl Board { + /// Report the values a stored word carries that have no known meaning. + pub fn cursor_check(&mut self, index: u32, data: u32) { + if index < cursor::POSITION { + let mut i = 0; + while i < 32 { + if (data >> i) & 0xf == 0xf { + self.cursor_report( + cursor::REPORT_GLYPH15, + "glyph value 15 has no colour word; drawn transparent", + index, + data, + ); + break; + } + i += 4; + } + } else if index >= cursor::PLANE { + let mut i = 0; + while i < 32 { + let v = (data >> i) & 3; + + if v == 1 || v == 2 { + self.cursor_report( + cursor::REPORT_PLANE, + "plane values 1 and 2 are not modeled; drawn transparent", + index, + data, + ); + break; + } + i += 2; + } + } else if index == cursor::CONTROL { + if (data & cursor::CTL_ENABLE) != 0 && (data & cursor::CTL_CROSSHAIR) != 0 { + self.cursor_report( + cursor::REPORT_CROSSHAIR, + "crosshair drawn as full-raster lines in colour word 1, an approximation", + index, + data, + ); + } + if data & cursor::CTL_CHANNEL1 != 0 { + self.cursor_report( + cursor::REPORT_CHANNEL1, + "second display channel is not modeled; nothing drawn", + index, + data, + ); + } + if data & !cursor::CTL_KNOWN != 0 { + self.cursor_report( + cursor::REPORT_CONTROL, + "control bits outside 0-5 and 31:30 have no known meaning", + index, + data, + ); + } + } else if index != cursor::POSITION && !(cursor::COLOURS..cursor::COLOURS + cursor::COLOUR_COUNT).contains(&index) { + self.cursor_report(cursor::REPORT_ENTRY, "no known function; stored", index, data); + } + } + + /// One backend data word for a cursor entry: store it, report what it + /// carries that has no known meaning, and repaint the lines the cursor + /// covered before and covers now. + pub fn cursor_write(&mut self, index: u32, data: u32) { + let old = self.cursor_span(); + + self.regs().set_cursor(index as usize, data); + self.cursor_check(index, data); + let new = self.cursor_span(); + self.cursor_invalidate(old); + self.cursor_invalidate(new); + } + + /// A backend read-window offset that names a cursor entry: its stored word. + pub fn cursor_read(&mut self, addr: u64, size: u32, value: &mut u64) -> bool { + let first = backend::READ_BASE + ((cursor::BASE as u64) << 3); + + if size != 4 || addr < first || (addr & 7) != 0 || addr >= first + ((cursor::ENTRIES as u64) << 3) { + return false; + } + *value = self.regs().cursor(((addr - first) >> 3) as usize) as u64; + true + } +} + +// Back-end entries 0x2000-0x2fff: the line and frame tables, timing, cursor, +// DAC, clock, I2C, genlock, scanout and display-mode entries. Every word +// written there is kept, and a 32-bit load at offset 4 of the entry's 8-byte +// slot in the read window returns it (established by the kernel's +// read-modify-writes of 0x26c0, 0x2482, 0x2483 and 0x2700; that the board +// returns the last word written is inferred). Entries the board itself +// updates answer from their own models. Reset values are unknown; the model +// resets them to zero. +// +// The line table (0x2000-0x23ff) and frame table (0x2400-0x247f) are only +// stored. APPROXIMATION: the retrace period stays the 1280x1024 60 Hz one +// (ODSY_FRAME_CLOCKS) rather than following the frame table. +// +// The scanout entries 0x2800 and 0x2801, 0x2807 and 0x2809 are stored and +// not used: APPROXIMATION, the model scans out its own colour buffers +// (resolve_row()) rather than locating the displayed buffer in board +// memory by these entries. The 16 x 16-pixel tiling of board memory is not +// modeled. + +/// The entries the kernel writes. +pub(super) fn backend_ctl_known(index: u32) -> bool { + (0x2000..=0x247f).contains(&index) + || (0x2481..=0x2483).contains(&index) + || index == 0x2600 + || index == 0x2680 + || index == 0x26c0 + || index == 0x2700 + || index == 0x2741 + || index == 0x2781 + || (0x27c0..=0x27cd).contains(&index) + || (0x2800..=0x2809).contains(&index) + || (0x2929..=0x292c).contains(&index) + || index == 0x2b00 +} + +impl Board { + pub fn backend_ctl_read(&mut self, addr: u64, size: u32, value: &mut u64) -> bool { + let first = backend::READ_BASE + ((backend::CTL_BASE as u64) << 3); + + if size != 4 || addr < first || (addr & 7) != 4 || addr >= first + ((backend::CTL_ENTRIES as u64) << 3) { + return false; + } + *value = self.widget.backend_ctl[((addr - first) >> 3) as usize] as u64; + true + } + + /// +0x1004 (mem::CONTROL): kept, and checked against the strap. + pub fn memory_control(&mut self, value: u32) { + let strap = self.widget.memory_config; + let size = (value >> mem::CONTROL_SIZE_SHIFT) & mem::CONTROL_SIZE_MASK; + // The kernel programs strap code 3 like 128 MB, code 2. + let strap_size = (strap >> config::SIZE_SHIFT).min(2); + + self.widget.memory_control = value; + if !self.diag.memory_control_reported { + self.diag.memory_control_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: memory controller configuration {:#x} stored; the controller is not modeled", value); + } + // The master id goes in first, with the strap fields still zero. + if value & !(0xfu32 << 19) == 0 { + return; + } + // MODEL CHOICE: a mismatch is reported; see mem::CONTROL. + if size != strap_size + || ((value & mem::CONTROL_FOUR_BANKS) != 0) != ((strap & config::FOUR_BANKS) != 0) + || ((value & mem::CONTROL_LATENCY_2) != 0) != ((strap & config::LATENCY_2) != 0) + { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: memory controller configuration {:#x} disagrees with the strap word {:#x}", value, strap); + } + } +} + +pub(super) fn i2c_status(command: u32) -> u32 { + i2c::STATUS_READY | if command != 0 { i2c::STATUS_DONE } else { 0 } +} + +impl Board { + pub fn read(&mut self, addr: u64, size: u32) -> u64 { + // Registers that do not wait for the board's work live in the front + // (interface.rs). + if front_register(addr, size, false) { + return self.front.lock().read(addr, size); + } + let mut value: u64 = 0; + let sequence = self.trace_sequence.fetch_add(1, Ordering::Relaxed); + + match reg(addr, size) { + Some(cfifo::SELFTEST) => { + // Answers OK only after the kernel driver's command-FIFO + // initialisation preamble; zero before. + value = if self.widget.cfifo_selftest_ready { cfifo::SELFTEST_OK as u64 } else { 0 }; + } + Some(mem::CONFIG) => { + // Bits 15:7: the field and the drain waits are ESTABLISHED, + // its unit UNKNOWN, and a count of unreturned credits + // INFERRED. MODEL CHOICE: pending doublewords, the model's + // pending words rounded up. Only reading 0 once the board has + // run the queue is needed, by the start-up and flow-reset + // drain waits. + let doublewords = self.fifo.level().div_ceil(2).min(0x1ff); + value = (self.widget.memory_config | (doublewords << 7)) as u64; + } + Some(flctl::CONFIG) => value = self.widget.flctl_config as u64, + Some(ODSY_BUZZ_1) => value = self.widget.buzz[0] as u64, + Some(ODSY_BUZZ_2) => value = self.widget.buzz[1] as u64, + Some(ODSY_BUZZ_3) => value = self.widget.buzz[2] as u64, + Some(ODSY_BUZZ_4) => value = self.widget.buzz[3] as u64, + Some(ODSY_BUZZ_5) => value = self.widget.buzz[4] as u64, + Some(ODSY_BUZZ_6) => value = self.widget.buzz[5] as u64, + Some(nic::PROM | nic::KERNEL) => value = self.widget.nic as u64, + Some(i2c::MAIN_CMD_READ) => value = self.widget.i2c_main_cmd as u64, + Some(i2c::MAIN_STATUS_READ) => value = i2c_status(self.widget.i2c_main_cmd) as u64, + Some(i2c::OPT_CMD_READ) => value = self.widget.i2c_opt_cmd as u64, + Some(i2c::OPT_STATUS_READ) => value = i2c_status(self.widget.i2c_opt_cmd) as u64, + Some(backend::VERSION) => value = (self.widget.backend_version & 7) as u64, + Some(host::WRITE_ADDR_UPPER + | host::WRITE_ADDR_LOWER + | stats::ADDR_UPPER + | stats::ADDR_LOWER + | stats::CONTROL + | ctx::SHADOW_ADDR_UPPER + | ctx::SHADOW_ADDR_LOWER + | ODSY_RESET_CONTROL + | mem::CONTROL) => { + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: read of the write-only register at {:#x}", addr); + } + _ => { + if self.cursor_read(addr, size, &mut value) || self.backend_ctl_read(addr, size, &mut value) { + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: MMIO read seq {} offset {:#x} size {} value {:#x}", sequence, addr, size, value); + return value; + } + self.unimplemented(addr, size, false); + } + } + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: MMIO read seq {} offset {:#x} size {} value {:#x}", sequence, addr, size, value); + value + } + + /// Present the plane's top-left width x height at the 1280x1024 rate. + pub fn set_scanout_size(&mut self, width: u32, height: u32) { + self.scanout.set_mode( + &ScanoutMode { width, height, frame_clocks: ODSY_FRAME_CLOCKS as u64, clock_hz: ODSY_PIXEL_CLOCK_HZ }, + ); + } + + /// One of the display channel's two size entries. A zero is the guest + /// blanking the channel while it changes format and leaves the picture as it + /// was. A size past the modeled planes is reported and kept to them. + pub fn channel_size(&mut self, index: u32, data: u32) { + let mut width = self.scanout.mode.width; + let mut height = self.scanout.mode.height; + + if data == 0 { + return; + } + if index == ODSY_CHANNEL_WIDTH { + width = data; + } else { + height = data; + } + if width > ODSY_WIDTH || height > ODSY_HEIGHT { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: display channel {}x{} is larger than the modeled {}x{} planes", + width, + height, + ODSY_WIDTH, + ODSY_HEIGHT + ); + width = width.min(ODSY_WIDTH); + height = height.min(ODSY_HEIGHT); + } + if width != self.scanout.mode.width || height != self.scanout.mode.height { + self.set_scanout_size(width, height); + } + } + + /// One data word of a backend record, delivered to the entry the record's + /// header selected. The colour lookup RAM and the per-drawable display-mode + /// registers take their words here; the two I2C command indices keep their + /// existing dispatch. Entries 0x2000-0x2fff are also kept for the read + /// window (Board::backend_ctl_read()); one the kernel is not known to write + /// is still reported. + pub fn backend_data(&mut self, index: u32, data: u32) { + if (backend::CTL_BASE..backend::CTL_BASE + backend::CTL_ENTRIES).contains(&index) { + self.widget.backend_ctl[(index - backend::CTL_BASE) as usize] = data; + } + // Scanout resolves through both, so either change repaints. + if index < lut::SIZE { + self.regs().set_lut(index as usize, data & lut::ENTRY_MASK); + self.scanout.repaint_all_for(DirtySite::Lut); + return; + } + if (dispmode::BASE..dispmode::BASE + dispmode::COUNT).contains(&index) { + self.regs().set_dispmode((index - dispmode::BASE) as usize, data); + self.scanout.repaint_all_for(DirtySite::Lut); + return; + } + if (cursor::BASE..cursor::BASE + cursor::ENTRIES).contains(&index) { + self.cursor_write(index - cursor::BASE, data); + return; + } + if (overlay::MODE_BASE..overlay::MODE_BASE + overlay::IDS).contains(&index) { + self.regs().set_overlay_mode((index - overlay::MODE_BASE) as usize, data); + if (data & !overlay::MODE_KNOWN) != 0 && !self.diag.overlay_mode_reported { + self.diag.overlay_mode_reported = true; + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: overlay mode {:#x} has bits this model does not decode", data); + } + self.scanout.repaint_all_for(DirtySite::Lut); + return; + } + match index { + ODSY_CHANNEL_WIDTH | ODSY_CHANNEL_HEIGHT => self.channel_size(index, data), + i2c::MAIN_CMD_INDEX => self.widget.i2c_main_cmd = data, + i2c::OPT_CMD_INDEX => self.widget.i2c_opt_cmd = data, + _ => { + if !backend_ctl_known(index) { + self.unimplemented(backend::SELECT, 8, true); + } + } + } + } + + /// One 32-bit word of the backend word stream. While no record is open the + /// word is a header: its 0b11 tag opens a record of `count` data words + /// starting at `index` (a zero count is a pad). Any word without the tag at a + /// header position has no established meaning and is reported rather than + /// guessed. While a record is open the word is a data word for the running + /// index, which then advances (see Board::backend_data). + pub fn backend_word(&mut self, word: u32) { + if self.widget.backend_count == 0 { + if ((word >> backend::TAG_SHIFT) & backend::TAG_MASK) != backend::TAG_HEADER { + self.unimplemented(backend::SELECT, 8, true); + return; + } + self.widget.backend_index = (word >> backend::SELECTOR_INDEX_SHIFT) & backend::SELECTOR_INDEX_MASK; + self.widget.backend_count = word & backend::COUNT_MASK; + return; + } + self.backend_data(self.widget.backend_index, word); + self.widget.backend_index = (self.widget.backend_index + 1) & backend::SELECTOR_INDEX_MASK; + self.widget.backend_count -= 1; + } + + /// The indexed register file's write port: an 8-byte store is two words, + /// upper half first, a 4-byte store is one word (see backend::SELECT). + pub fn backend_write(&mut self, value: u64, size: u32) { + if size == 8 { + self.backend_word((value >> 32) as u32); + self.backend_word(value as u32); + } else { + self.backend_word(value as u32); + } + } +} + +/// Deterministic model defaults; physical back-material reset is unknown. +/// (The query:: constants and the hardware shadow-page constants are in +/// draw.rs and consts.rs.) +pub(super) const BACK_MATERIAL_DEFAULTS: [u32; 16] = [ + ODSY_F32_POINT_TWO, + ODSY_F32_POINT_TWO, + ODSY_F32_POINT_TWO, + ODSY_F32_ONE, + ODSY_F32_POINT_EIGHT, + ODSY_F32_POINT_EIGHT, + ODSY_F32_POINT_EIGHT, + ODSY_F32_ONE, + 0, + 0, + 0, + ODSY_F32_ONE, + 0, + 0, + 0, + ODSY_F32_ONE, +]; diff --git a/src/dev/odyssey/sampler.rs b/src/dev/odyssey/sampler.rs new file mode 100644 index 000000000..b62c55a06 --- /dev/null +++ b/src/dev/odyssey/sampler.rs @@ -0,0 +1,1035 @@ +//! Texture setup: the board's texture words decoded into one sampler +//! description, the check of what the model can draw from it, and the level +//! chain built from that description. +//! +//! Ported from hw/misc/sgi-odyssey.c and the shared SGI graphics code in jamesbraid/qemu-sgi-origami. +//! +//! The words are the sampler block (0x1c16, seven words), the LOD block +//! (0x1013), the level table (0x0e80 + i, three words an entry), the first +//! level index (block 0x006), the path selection word's target bits (block +//! 0x005) and the environment block (0x141b). The field layout is what the +//! GL library writes (ESTABLISHED as the library's setters unless a comment +//! says otherwise). What the board does with each field is mostly UNKNOWN: +//! the model samples with OpenGL's rules through the shared sampler, and a +//! comment marks each place where it picks a reading the library alone does +//! not settle. + +use crate::devlog::LogModule; +use super::board::{Board, ODSY_OBJECTS, ODSY_OBJ_POOL_WORDS, ODSY_TEX_LEVELS, ODSY_VOLUME_SLICES}; +use super::consts::{tex, ODSY_VOLUME_SLICE_SHIFT_MAX}; +use super::gl::{ + texture_detail_sampler, texture_lod_limit, volume_slice_stride, PyramidShape, Texture, TextureChain, + TextureDetailSampler, TextureVolume, +}; +use super::gl_context::{ + GL_ALPHA, GL_CLAMP, GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE, GL_INTENSITY, GL_LINEAR, GL_LINEAR_DETAIL_ALPHA_SGIS, + GL_LINEAR_DETAIL_COLOR_SGIS, GL_LINEAR_DETAIL_SGIS, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, + GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_REPEAT, + GL_RGB, GL_RGBA, +}; +use super::raster::{cmax, cmin, GlTexImage, GlTexLevel, GL_TEX_LEVELS_MAX}; + +/// Sampler word 4 bits 25:23 are the class of the texel format: 1 alpha, 2 +/// luminance, 3 luminance-alpha, 4 intensity, 5 RGB, 6 RGBA. What the board +/// does with the class is UNKNOWN. MODEL CHOICE: the class is read as the +/// components the texel words hold, so class 5 samples with alpha 1 through +/// the base format GL_RGB; that the board forces alpha to 1 for class 5 is +/// INFERRED, not established. `components` is the word masked by +/// tex::COMPONENTS_MASK. +pub fn texture_class_format(components: u32) -> Option { + match components { + 0x00800000 => Some(GL_ALPHA), + 0x01000000 => Some(GL_LUMINANCE), + 0x01800000 => Some(GL_LUMINANCE_ALPHA), + 0x02000000 => Some(GL_INTENSITY), + 0x02800000 => Some(GL_RGB), + 0x03000000 => Some(GL_RGBA), + _ => None, + } +} + +/// One row of the board's texel format index table: what a level +/// descriptor's word 1 bits 25:20 names. The index gives the layout and +/// storage, and index 12 is the 4/4/4/4 layout in every use; the component +/// set comes from the class in sampler word 4, not from the index. +/// ESTABLISHED as the library's whole mapping: the sixteen indices it emits, +/// with the size code and the classes texture activation pairs with each. +/// No software checks the class against the index, and the pixel-to-texture +/// move sends class 6 with indices the table pairs with other classes (index +/// 10 among them), so `classes` records the library's pairing only: MODEL +/// CHOICE, the model never refuses a pair. The board's own packing of a +/// texel, and whether it checks the pair, are UNKNOWN; the model keeps whole +/// texel words, so a row decides what the model accepts, not a bit layout. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TexelFormat { + /// Word 1 bits 31:30, the size code: log2 of the booked bytes a texel + /// minus 1. Texture activation and the pixel-to-texture move both send + /// it (ESTABLISHED for activation, INFERRED for every move source). + pub size: u32, + /// The classes texture activation pairs with the index, one bit at each + /// class value. + pub classes: u8, +} + +impl TexelFormat { + /// Whether the library pairs the index with sampler class `class`. + pub const fn pairs_with(&self, class: u32) -> bool { + class < 8 && (self.classes & (1 << class)) != 0 + } + + + /// Bytes the library books for a texel of this format, `2 << size`. + /// A bound on the packing, not the packing itself. + pub const fn booked(&self) -> u32 { + 2 << self.size + } +} + +const fn texel_classes(list: &[u32]) -> u8 { + let mut m = 0u8; + let mut i = 0; + + while i < list.len() { + m |= 1 << list[i]; + i += 1; + } + m +} + +/// The format index table. None for every index the library does not emit: +/// 1, 3, 5, 6 and 7 have width entries that nothing sends, and 14, 15, 19 to +/// 31, 34 to 47 and 50 to 63 have none (the library leaves the size code +/// unwritten and returns class 0). UNKNOWN: whether the board accepts them, +/// so a level that names one is refused. +/// +/// MODEL CHOICES: 32 and 33 are the same 12-bit RGBA layout and read alike +/// (the twin pair is unexplained; only the library's pixel-to-texture move +/// sends 32), and 48 and 49, the ICC pair, are read as their component sets +/// alone (their stored widths are INFERRED from the enumerant names). +pub const fn texel_format(index: u32) -> Option { + const A: u8 = texel_classes(&[1]); + const L: u8 = texel_classes(&[2]); + const LA: u8 = texel_classes(&[3]); + const I: u8 = texel_classes(&[4]); + const RGB: u8 = texel_classes(&[5]); + const RGBA: u8 = texel_classes(&[6]); + + Some(match index { + 0 | 8 => TexelFormat { size: 0, classes: A }, + 2 | 9 => TexelFormat { size: 0, classes: I }, + 4 | 10 => TexelFormat { size: 0, classes: L }, + 11 => TexelFormat { size: 0, classes: LA }, + 12 | 13 => TexelFormat { size: 0, classes: RGB | RGBA }, + 16 => TexelFormat { size: 1, classes: LA }, + 17 | 18 => TexelFormat { size: 1, classes: RGB | RGBA }, + 32 | 33 => TexelFormat { size: 2, classes: RGB | RGBA }, + 48 => TexelFormat { size: 0, classes: RGB }, + 49 => TexelFormat { size: 1, classes: RGBA }, + _ => return None, + }) +} + +/// The format a level descriptor's word 1 names, from its bits 31:30 (size +/// code) and 25:20 (format index; ESTABLISHED as one six-bit field). None +/// for an index the table does not list, or a size code that is not the +/// index's. +pub fn texture_level_texel_format(word1: u32) -> Option { + texel_format(level_index(word1)).filter(|f| f.size == word1 >> 30) +} + +/// The texel components of a detail level whose entry word 1 is `word1`, +/// under the primary's base format `primary`: the primary's, since only the +/// bound texture's mode word, with its class, reaches the board (ESTABLISHED), +/// while the detail level carries its own index. None for an index the table +/// does not list or a size code that is not the index's. +pub fn texture_level_format(word1: u32, primary: u32) -> Option { + texture_level_texel_format(word1).map(|_| primary) +} + +fn level_index(word1: u32) -> u32 { + (word1 >> 20) & 0x3f +} + +/// Level descriptor word 1 bits 31:30 and 25:20, the size code and format +/// index. +pub(super) const LEVEL_FORMAT_MASK: u32 = 0xc3f00000; +/// Level descriptor word 1 bits 11:0, the row in 16-texel units. +const LEVEL_STRIDE_MASK: u32 = 0xfff; +/// Word 1 bits 28:26 are the texture-offset code, which only the texture move +/// writes, and then 0 or 2 (ESTABLISHED). Bit 29 and bits 19:12 are never +/// written. The model has no reading for offset code 2, so all of these +/// must be clear. +const LEVEL_WORD1_UNMODELLED: u32 = 0x3c0ff000; +/// Word 0 bits 28:0 are the level's address. Bits 31:29 are kept from the +/// object by the texture move and have no known meaning. +const LEVEL_ADDRESS_MASK: u32 = 0x1fffffff; + +/// A level's row, in 16-texel units rounded up to even, for a level `width` +/// texels wide (ESTABLISHED as the library's rule for a primary level). +pub(super) fn level_stride_units(width: u32) -> u32 { + (width.div_ceil(16) + 1) & !1u32 +} + +/// A level stored with a texel offset sits this many texels in from its +/// allocation's corner on each axis it has, its border at one less +/// (TexelOffset). +const OFFSET_TEXELS: u32 = 16; + +/// Why a draw's texture state is refused. Every refusal is logged with the +/// state's words (Board::texture_setup()). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct Refusal(pub &'static str); + +/// The image's dimensionality, from the path selection word's texture bits +/// (block 0x005 bits 5:3: 3D, 2D, 1D). A 3D image with a 1D or 2D bit as +/// well is not a state the library sets. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Target { + OneD, + TwoD, + ThreeD, +} + +impl Target { + /// The axes the image has: S, then T, then R. + fn axes(self) -> usize { + match self { + Target::OneD => 1, + Target::TwoD => 2, + Target::ThreeD => 3, + } + } +} + +/// Sampler word 4's wrap code for one axis: 0 repeat, 1 clamp to edge, 2 +/// clamp, 3 clamp to border. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Wrap { + Repeat, + ClampToEdge, + Clamp, + ClampToBorder, +} + +impl Wrap { + fn from_code(code: u32) -> Wrap { + match code & 3 { + 0 => Wrap::Repeat, + 1 => Wrap::ClampToEdge, + 2 => Wrap::Clamp, + _ => Wrap::ClampToBorder, + } + } + + /// Words 1 to 3 bits 23:18, the value the library writes with each wrap + /// mode: 0 repeat or clamp, 1 clamp to edge, 63 clamp to border. Its + /// meaning on the board is UNKNOWN; the model reads only that it agrees + /// with the wrap. + fn size_word_mark(self) -> u32 { + match self { + Wrap::Repeat | Wrap::Clamp => 0, + Wrap::ClampToEdge => 1, + Wrap::ClampToBorder => 63, + } + } + + fn gl(self) -> u32 { + match self { + Wrap::Repeat => GL_REPEAT, + Wrap::ClampToEdge => GL_CLAMP_TO_EDGE, + Wrap::Clamp => GL_CLAMP, + Wrap::ClampToBorder => GL_CLAMP_TO_BORDER, + } + } +} + +/// Sampler word 4 bits 22:18 and bit 0, which the library takes from context +/// state rather than the texture object. What they select on the board is +/// UNKNOWN. The model reads the values it has seen and refuses any other: +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum SamplerContext { + /// Bits 22:18 0, or 20 or 24 with bit 0 clear: an ordinary draw. + Plain, + /// Bits 22:18 25 (bits 22, 21 and 18) with bit 0 clear: the image path's + /// default block word, which the texture move samples under (ESTABLISHED: + /// the move runs with that block's word, 0x03640000 with its class forced + /// to 6) and the mip-level builder sends too. MODEL CHOICE: sampled as + /// the plain image. + Image, + /// Bits 22:18 20 or 24 with bit 0 set: a colour table looked up after + /// filtering, the 0x3e00 and 0x3e10 tables, and with 24 its alpha table + /// too. INFERRED from the draws that send these values while those tables + /// are loaded; what the bits do on the board is UNKNOWN. + ColourTable { alpha: bool }, +} + +impl SamplerContext { + fn decode(word: u32) -> Option { + let enable = (word & 1) != 0; + + match ((word >> 18) & 0x1f, enable) { + (0, false) | (20, false) | (24, false) => Some(SamplerContext::Plain), + (25, false) => Some(SamplerContext::Image), + (20, true) => Some(SamplerContext::ColourTable { alpha: false }), + (24, true) => Some(SamplerContext::ColourTable { alpha: true }), + _ => None, + } + } +} + +/// Whether sampler word 4 is the mip-level builder's. The builder samples +/// under the image path's context (SamplerContext::Image) with bit 28 set, +/// and writes no wrap, filter or bit 17: the words seen from it differ only +/// in the class. MODEL CHOICE: any class is taken, against any index, since +/// the copy keeps whole texel words and no software checks the pair. +pub(super) fn texture_mip_build_word(word4: u32) -> bool { + !( (word4 & MODE_WORD_UNKNOWN) != 0 + || (word4 & CORNER_FLAG) == 0 + || (word4 & 0x3fffe) != 0 + || SamplerContext::decode(word4) != Some(SamplerContext::Image) + || texture_class_format(word4 & tex::COMPONENTS_MASK).is_none()) +} + +/// How a level is placed in its allocation, from word 4 bit 28 and word 6 +/// bit 24, which the library sets as complements from the object's +/// texel-offset flag (ESTABLISHED). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Placement { + /// Bit 28 set and bit 24 clear: the level starts at its allocation's + /// corner. + Corner, + /// Bit 28 clear and bit 24 set: the level sits 16 texels in from its + /// allocation's corner on each of its axes, with its one-texel border + /// stored around it. INFERRED from the captured stored-border uploads + /// (1D, 2D and a 2x2x2 volume) and a 512x512 ten-level 2D image whose + /// every level sits there, border at row and column 15; MODEL CHOICE for + /// every other size and for the volume's slices. + TexelOffset, + /// Both clear: the texture move's form. The move samples under the image + /// path's default block, whose bit 28 is clear (ESTABLISHED), and writes + /// no border colour. Read as Corner, with the move's rows (Sampler::rows). + Move, +} + +/// The texture state of one draw, decoded field by field. +#[derive(Clone, Copy)] +pub(super) struct Sampler { + pub target: Target, + /// Word 0 bits 14:10, 9:5 and 4:0: log2 of the base level's size on S, T + /// and R. + pub log2: [u32; 3], + /// Words 1 to 3 bits 16:0: each axis's maximum clamp in whole texels. + pub max_clamp: [u32; 3], + pub wrap: [Wrap; 3], + /// The class's components (texture_class_format()). + pub base_format: u32, + pub context: SamplerContext, + pub placement: Placement, + /// Word 4 bits 7:5: 0 nearest, 1 linear, 2 to 5 the four mipmap modes in + /// GL order. + pub min: u32, + /// Word 4 bits 4:1: 0 nearest, 1 linear, 2 linear detail, 3 detail + /// alpha, 4 detail colour. + pub mag: u32, + /// Word 4 bit 17 under a minification code other than 5: GL_PHASE_SGIX. + pub phase: bool, + /// The detail image word 0 names, under magnification codes 2 to 4. + pub detail: Option, + /// Words 5 and 6 bits 23:0: the border colour, four 12-bit channels. + pub border_colour: [u32; 2], + /// Block 0x006 bits 5:0: the level table entry of level 0. + pub first: u32, + /// The minimum and maximum LOD in 1/256 (texture_lod_limit()). + pub lod_min: i32, + pub lod_max: i32, + /// The environment mode, 0x141b word 0 bits 2:0: 1 replace, 2 modulate, + /// 3 decal, 4 blend, 5 add. + pub env: u32, +} + +const MIN_FILTERS: [u32; 6] = [ + GL_NEAREST, + GL_LINEAR, + GL_NEAREST_MIPMAP_NEAREST, + GL_LINEAR_MIPMAP_NEAREST, + GL_NEAREST_MIPMAP_LINEAR, + GL_LINEAR_MIPMAP_LINEAR, +]; + +/// Word 4's minification code for GL_LINEAR_MIPMAP_LINEAR. +const MIN_CODE_LINEAR_MIPMAP_LINEAR: u32 = 5; +/// Word 4's minification code for GL_NEAREST_MIPMAP_NEAREST. +const MIN_CODE_NEAREST_MIPMAP_NEAREST: u32 = 2; + +/// Word 0 bit 30 and word 4 bits 31:26 and 16:14 are never written. +const SIZE_WORD_UNKNOWN: u32 = 0x40000000; +const MODE_WORD_UNKNOWN: u32 = 0xec01c000; +/// Words 1 to 3: bits 31:24 and 17 are never written. +const AXIS_WORD_UNKNOWN: u32 = 0xff020000; +/// Words 5 and 6 bits 31:25, and word 5 bit 24, are never written. +const BORDER_WORD_UNKNOWN: u32 = 0xfe000000; +const OFFSET_FLAG: u32 = 1 << 24; +const CORNER_FLAG: u32 = 1 << 28; +/// The largest log2 size whose maximum clamp, 2^log2 texels, fits the 17-bit +/// field. +const LOG2_SIZE_MAX: u32 = 16; + +impl Sampler { + pub fn size(&self) -> [u32; 3] { + self.log2.map(|l| 1u32 << l) + } + + pub fn mipmap(&self) -> bool { + self.min >= 2 + } + + /// The axes of the image, in S, T, R order. + fn axis_wraps(&self) -> &[Wrap] { + &self.wrap[..self.target.axes()] + } + + pub fn detail_filter(&self) -> u32 { + match self.mag { + 2 => GL_LINEAR_DETAIL_SGIS, + 3 => GL_LINEAR_DETAIL_ALPHA_SGIS, + 4 => GL_LINEAR_DETAIL_COLOR_SGIS, + _ => 0, + } + } + + /// How many levels the chain has: one unless a mipmap filter and a + /// positive maximum LOD, then down to the terminal level or the maximum + /// LOD, whichever comes first. The library sends a maximum LOD of at + /// most MAX_LEVEL - BASE_LEVEL (ESTABLISHED). + fn level_count(&self) -> u32 { + let terminal = match self.target { + Target::ThreeD => cmax(cmax(self.log2[0], self.log2[1]), self.log2[2]), + _ => cmax(self.log2[0], self.log2[1]), + }; + + if self.mipmap() && self.lod_max > 0 { + cmin(terminal, ((self.lod_max + 255) / 256) as u32) + 1 + } else { + 1 + } + } +} + +impl Board { + /// The draw's texture words decoded. A field holding a value the library + /// never writes is unknown state, and refuses the draw. + pub(super) fn texture_sampler(&self) -> Result { + let t = &self.tex; + let [size, s_word, t_word, r_word, mode, border0, border1] = t.sampler; + + let target = match t.mode & (tex::MODE_1D | tex::MODE_2D | tex::MODE_3D) { + tex::MODE_3D => Target::ThreeD, + m if (m & tex::MODE_3D) != 0 => return Err(Refusal("a 3D target with a 1D or 2D bit")), + m if (m & tex::MODE_2D) != 0 => Target::TwoD, + m if (m & tex::MODE_1D) != 0 => Target::OneD, + _ => return Err(Refusal("no texture target")), + }; + if (size & SIZE_WORD_UNKNOWN) != 0 { + return Err(Refusal("sampler word 0 bit 30")); + } + if (mode & MODE_WORD_UNKNOWN) != 0 { + return Err(Refusal("sampler word 4 bits the library never writes")); + } + let log2 = [(size >> 10) & 31, (size >> 5) & 31, size & 31]; + if log2.iter().any(|&l| l > LOG2_SIZE_MAX) { + return Err(Refusal("a size past the maximum clamp's field")); + } + if target != Target::ThreeD && log2[2] != 0 { + return Err(Refusal("an R size on a 1D or 2D image")); + } + let wrap = [Wrap::from_code(mode >> 12), Wrap::from_code(mode >> 10), Wrap::from_code(mode >> 8)]; + // The library writes repeat for R unless the target is 3D. + if target != Target::ThreeD && wrap[2] != Wrap::Repeat { + return Err(Refusal("an R wrap on a 1D or 2D image")); + } + let mut max_clamp = [0u32; 3]; + for (axis, &word) in [s_word, t_word, r_word].iter().enumerate() { + if (word & AXIS_WORD_UNKNOWN) != 0 { + return Err(Refusal("sampler size word bits the library never writes")); + } + if (word >> 18) & 0x3f != wrap[axis].size_word_mark() { + return Err(Refusal("a size word's wrap mark that disagrees with its wrap")); + } + max_clamp[axis] = word & 0x1ffff; + // trunc(2^log2size * GL_TEXTURE_MAX_CLAMP_*_SGIX) with the clamp + // at most 1.0 (ESTABLISHED as the library's setter). + if max_clamp[axis] > 1 << log2[axis] { + return Err(Refusal("a maximum clamp past the image's size")); + } + } + let Some(base_format) = texture_class_format(mode & tex::COMPONENTS_MASK) else { + return Err(Refusal("sampler word 4 class")); + }; + let Some(context) = SamplerContext::decode(mode) else { + return Err(Refusal("sampler word 4 bits 22:18 and 0")); + }; + if ((border0 | border1) & BORDER_WORD_UNKNOWN) != 0 || (border0 & OFFSET_FLAG) != 0 { + return Err(Refusal("border colour word bits the library never writes")); + } + let placement = match ((mode & CORNER_FLAG) != 0, (border1 & OFFSET_FLAG) != 0) { + (true, false) => Placement::Corner, + (false, true) => Placement::TexelOffset, + (false, false) if context == SamplerContext::Image => Placement::Move, + _ => return Err(Refusal("word 4 bit 28 and word 6 bit 24 are not complements")), + }; + let min = (mode >> 5) & 7; + let mag = (mode >> 1) & 0xf; + if min > 5 { + return Err(Refusal("minification code")); + } + if mag > 4 { + return Err(Refusal("magnification code")); + } + let detail = if (2..=4).contains(&mag) { + // With a detail magnification filter the library sends GL_NEAREST + // as code 2 and GL_LINEAR as code 5 (ESTABLISHED). + if min != MIN_CODE_NEAREST_MIPMAP_NEAREST && min != MIN_CODE_LINEAR_MIPMAP_LINEAR { + return Err(Refusal("a detail filter with a minification code the library never pairs with one")); + } + match texture_detail_sampler(size) { + Some(d) => Some(d), + None => return Err(Refusal("a detail filter whose sampler word 0 names no detail level")), + } + } else { + // Word 0 bits 29:25 are the detail level the setter clamps to 15; + // the move writes 0. Bits 31 and 24:15 are stale without a detail + // filter (ESTABLISHED), so they are not read here. + if (size >> 25) & 0x1f > 15 { + return Err(Refusal("a detail level past 15")); + } + None + }; + if (t.level0 & !0x3f) != 0 { + return Err(Refusal("first level index bits past 5:0")); + } + if t.lod.iter().any(|&w| (w & !tex::LOD_MASK) != 0) { + return Err(Refusal("LOD word bits past 13:0")); + } + if !self.texture_scale_bias_known() { + return Err(Refusal("post-filter scale or bias bits past 15:0")); + } + // The library always sends 0, 0, 0 (ESTABLISHED); their meaning is + // UNKNOWN. + if t.offset.iter().any(|&w| w != 0) { + return Err(Refusal("texture offset words")); + } + let env = t.env[0]; + if !(1..=5).contains(&env) { + return Err(Refusal("environment mode")); + } + // MODEL CHOICE: the environment colour's spare bits and words 3 and + // 4 are checked only where the colour is used, under blend. + if env == 4 && (((t.env[1] | t.env[2]) & 0xff000000) != 0 || t.env[3] != 0 || t.env[4] != 0) { + return Err(Refusal("environment colour bits the library never writes")); + } + Ok(Sampler { + target, + log2, + max_clamp, + wrap, + base_format, + context, + placement, + min, + mag, + phase: (mode & tex::SAMPLER_BIT17) != 0 && min != MIN_CODE_LINEAR_MIPMAP_LINEAR, + detail, + border_colour: [border0, border1], + first: t.level0, + lod_min: texture_lod_limit(t.lod[2]), + lod_max: texture_lod_limit(t.lod[3]), + env, + }) + } + + /// Whether the model can draw with sampler `s`. Each refusal names what + /// is missing from the model, not a fault in the state. + pub(super) fn texture_support(&self, s: &Sampler) -> Result<(), Refusal> { + let axes = s.axis_wraps(); + let bf = s.base_format; + + if self.gl_index_mode() { + return Err(Refusal("texturing in colour-index mode is not modelled")); + } + if s.phase { + return Err(Refusal("GL_PHASE_SGIX is not modelled")); + } + if s.env == 5 { + return Err(Refusal("the add environment is not modelled")); + } + // GL leaves decal undefined for these components. + if s.env == 3 && bf != GL_RGB && bf != GL_RGBA { + return Err(Refusal("decal of a texture without colour")); + } + if s.mipmap() && s.lod_min > s.lod_max { + return Err(Refusal("a minimum LOD above the maximum")); + } + // MODEL CHOICE: GL_CLAMP and clamp to border are taken only on every + // axis of the image or none. No client has been seen to mix them + // with other wraps, and what the board does then is UNKNOWN. + for wrap in [Wrap::Clamp, Wrap::ClampToBorder] { + if axes.contains(&wrap) && !axes.iter().all(|&a| a == wrap) { + return Err(Refusal("GL_CLAMP or clamp to border on some axes only")); + } + } + if axes.contains(&Wrap::ClampToBorder) && s.placement == Placement::TexelOffset { + return Err(Refusal("clamp to border on an image with a stored border")); + } + if let SamplerContext::ColourTable { .. } = s.context { + if s.target == Target::ThreeD { + return Err(Refusal("a colour table on a volume is not modelled")); + } + if self.tex.colour_table_valid.iter().any(|&v| v != 0xffff) { + return Err(Refusal("a colour table lookup before both tables are loaded")); + } + } + if s.detail.is_some() { + if s.target != Target::TwoD || s.first != 0 { + return Err(Refusal("a detail texture on other than a 2D image from entry 0")); + } + if s.placement != Placement::Corner || s.context != SamplerContext::Plain { + return Err(Refusal("a detail texture with a stored border or a lookup table is not modelled")); + } + if axes.iter().any(|&a| a != Wrap::Repeat) { + return Err(Refusal("a detail texture under a wrap other than repeat is not modelled")); + } + if self.texture_detail_function_weight().is_none() { + return Err(Refusal("a detail texture before its whole function table is loaded")); + } + } + if s.target == Target::ThreeD { + // The volume path draws replace and modulate only. + if s.env > 2 { + return Err(Refusal("a volume under decal or blend is not modelled")); + } + if s.placement == Placement::Move { + return Err(Refusal("the texture move on a volume")); + } + if axes.contains(&Wrap::ClampToBorder) { + return Err(Refusal("clamp to border on a volume is not modelled")); + } + if s.size()[2] > ODSY_VOLUME_SLICES { + return Err(Refusal("a volume deeper than the model's slice table")); + } + // Limitation: the R axis's maximum clamp is not applied. + if s.max_clamp[2] != s.size()[2] { + return Err(Refusal("a volume's R maximum clamp below its depth is not modelled")); + } + // The model reads a volume's levels only from entry 0; a nonzero + // selector has no captured layout. + if s.first != 0 { + return Err(Refusal("a volume from a level table entry other than 0")); + } + // A volume without mipmaps is drawn as one image, which the shared + // sampler filters with a single filter. + if !s.mipmap() && s.min != s.mag { + return Err(Refusal("a volume with different minification and magnification filters is not modelled")); + } + } + Ok(()) + } + + /// Build the draw's texture from its texture words: decode them, check + /// the model can draw them, and find each level in the object store. A + /// refused state is logged and drops the draw. + pub fn texture_setup(&mut self, image: &mut GlTexImage<'static>, chain: &mut TextureChain) -> bool { + let result = self + .texture_sampler() + .and_then(|s| self.texture_support(&s).map(|()| s)) + .and_then(|s| self.texture_chain(&s, image, chain)); + + match result { + Ok(()) => true, + Err(Refusal(why)) => { + crate::dlog!( + LogModule::Odyssey, + "sgi-odyssey: texture state refused, {}: sampler {:08x?} mode {:#x} first level {} environment {}, draw dropped", + why, + self.tex.sampler, + self.tex.mode, + self.tex.level0, + self.tex.env[0] + ); + false + } + } + } + + /// The image fields every target shares. + fn texture_image(&self, s: &Sampler, image: &mut GlTexImage<'static>) { + let [width, height, _] = s.size(); + + image.base_format = s.base_format; + if s.detail.is_none() { + image.texture_max_clamp(s.size(), s.max_clamp, s.wrap); + } + image.width = width; + image.height = height; + image.border = (s.placement == Placement::TexelOffset) as u32; + image.wrap_s = s.wrap[0].gl(); + // MODEL CHOICE: a 1D image has no T axis, so T never takes a border + // tap whatever its wrap word says. + image.wrap_t = if s.target == Target::OneD { GL_CLAMP_TO_EDGE } else { s.wrap[1].gl() }; + if s.axis_wraps().iter().any(|&w| w == Wrap::Clamp || w == Wrap::ClampToBorder) { + let [c0, c1] = s.border_colour; + image.border_color = [ + ((c0 >> 12) & 0xfff) as f32 / 4095.0f32, + (c0 & 0xfff) as f32 / 4095.0f32, + ((c1 >> 12) & 0xfff) as f32 / 4095.0f32, + (c1 & 0xfff) as f32 / 4095.0f32, + ]; + } + image.mag_filter = match s.mag { + 0 => GL_NEAREST, + 1 => GL_LINEAR, + _ => s.detail_filter(), + }; + image.min_filter = MIN_FILTERS[s.min as usize]; + self.texture_lod_bias(image); + } + + /// Level table entry `i`'s three words. + fn level_entry(&self, i: u32) -> [u32; 3] { + let e = (i * 3) as usize; + [self.tex.levels[e], self.tex.levels[e + 1], self.tex.levels[e + 2]] + } + + /// The format a level's word 1 names, when the model can read it: an + /// index the table lists, with its own size code, and no texture-offset + /// code. The size code is checked the same way for the texture move, + /// which sends the same quantity. The class is never checked against the + /// index (TexelFormat). + fn level_format_check(word1: u32) -> Result { + if (word1 & LEVEL_WORD1_UNMODELLED) != 0 { + return Err(Refusal("a level descriptor's word 1 bits the model has no reading for")); + } + texture_level_texel_format(word1) + .ok_or(Refusal("a level's format index or size code is not one the library emits")) + } + + fn texture_chain(&mut self, s: &Sampler, image: &mut GlTexImage<'static>, chain: &mut TextureChain) -> Result<(), Refusal> { + self.texture_image(s, image); + if s.target == Target::ThreeD { + return self.texture_volume_chain(s, image, chain); + } + let [width, height, _] = s.size(); + let first = s.first; + let count = s.level_count(); + let offset = s.placement == Placement::TexelOffset; + let two_d = s.target == Target::TwoD; + + if count > GL_TEX_LEVELS_MAX || first + count > ODSY_TEX_LEVELS { + return Err(Refusal("a chain past the level table")); + } + // An object holds only the rows a transfer wrote, and a client may + // define a level with no data and then write part of it, as SDL + // streams a frame into a larger texture. The rest of the level is + // still its storage, with contents GL leaves undefined: grow the + // object to the level's rows, the new ones reading 0. Before any + // level's place is taken, since growing may move objects. Growth + // that fits the store always succeeds (object_rows() forgets others + // to make room), and one that cannot is not tried, so a draw never + // waits on the raster unit for a growth that fails. MODEL POLICY: + // only an ordinary level that holds some rows grows; an object that + // holds none stays missing, and a level with a stored border keeps + // its own check below. Sampling is not use, so finding a level here + // does not touch its object (Board::object_touch()). + if s.placement == Placement::Corner { + for i in 0..count { + let entry = self.level_entry(first + i); + let object = self.object_find(entry[0] & LEVEL_ADDRESS_MASK); + let stride_units = entry[1] & LEVEL_STRIDE_MASK; + let (level_width, level_height) = (cmax(1, width >> i), cmax(1, height >> i)); + + if let Some(o) = object { + if self.objects.width[o] >= level_width + && (stride_units == 0 || self.objects.width[o] == stride_units * 16) + && self.objects.rows[o] != 0 + && self.objects.rows[o] < level_height + && self.objects.width[o] as u64 * level_height as u64 <= ODSY_OBJ_POOL_WORDS as u64 + { + self.object_rows(o, level_height); + } + } + } + } + chain.clear(); + if let SamplerContext::ColourTable { alpha } = s.context { + chain.volume.colour_table = true; + chain.volume.colour_table_alpha = alpha; + } + chain.volume.scale_bias = !self.texture_scale_bias_identity(); + if chain.volume.colour_table || chain.volume.scale_bias { + image.post_filter = true; + } + let format_word = self.level_entry(first)[1] & LEVEL_FORMAT_MASK; + // A stored border's interior starts 16 texels in on each axis the + // image has (Placement::TexelOffset). + let interior_x = if offset { OFFSET_TEXELS } else { 0 }; + let interior_y = if offset && two_d { OFFSET_TEXELS } else { 0 }; + for i in 0..count { + let entry = self.level_entry(first + i); + let level_width = cmax(1, width >> i); + let level_height = cmax(1, height >> i); + let stride_units = entry[1] & LEVEL_STRIDE_MASK; + // A stored border's row is its width plus 16 texels each side, + // rounded as an ordinary row: INFERRED from the captured strides. + let expected_units = level_stride_units(level_width + 2 * interior_x); + let base = entry[0] & LEVEL_ADDRESS_MASK; + let rows = level_height + interior_y + (offset && two_d) as u32; + + Self::level_format_check(entry[1])?; + // The library writes the primary's level-0 format into every + // primary entry (ESTABLISHED). Word 2 is not written for a 1D or + // 2D level, so its value is not this texture's and is not read. + if (entry[1] & LEVEL_FORMAT_MASK) != format_word { + return Err(Refusal("levels of one chain with different formats")); + } + if base != 0 && stride_units != expected_units && !(count == 1 && stride_units == 0) { + return Err(Refusal("a level row that is not the library's for its width")); + } + chain.touch(i); + chain.levels[i as usize] = GlTexLevel { + level: i, + width: level_width, + height: level_height, + base_format: s.base_format, + internal_format: s.base_format, + ..Default::default() + }; + if base == 0 || self.objects.count > ODSY_OBJECTS { + continue; + } + let Some(o) = self.object_find(base) else { + continue; + }; + // MODEL CHOICE: the texture move draws only the rows it wrote, + // from an object sized to them, so a moved level needs only the + // rows its object holds; the rectangle's coordinates reach no + // further. + let rows = if s.placement == Placement::Move { rows.min(self.objects.rows[o]).max(1) } else { rows }; + let end = ((rows - 1) as u64) + .checked_mul(self.objects.width[o] as u64) + .and_then(|e| e.checked_add(self.objects.offset[o] as u64)) + .and_then(|e| e.checked_add(interior_x as u64)) + .and_then(|e| e.checked_add(level_width as u64 + offset as u64)); + if self.objects.width[o] < level_width + || (stride_units != 0 && self.objects.width[o] != stride_units * 16) + || self.objects.rows[o] < rows + || !end.is_some_and(|e| e <= self.objects.used as u64 && e <= self.objects.mem.size / 4) + { + continue; + } + // A stored border is read, so all of it and the level must have + // been written; MODEL POLICY: otherwise the level is missing. + if offset + && !self.object_region_written( + o, + interior_x - 1, + if two_d { interior_y - 1 } else { 0 }, + level_width + 2, + if two_d { level_height + 2 } else { 1 }, + ) + { + continue; + } + chain.touch(i); + chain.storage[i as usize] = Texture { + offset: self.objects.offset[o].wrapping_add(interior_y.wrapping_mul(self.objects.width[o])).wrapping_add(interior_x), + stride: self.objects.width[o], + width: level_width, + height: level_height, + base_format: s.base_format, + words: self.objects.used, + border_rows: offset && two_d, + }; + chain.levels[i as usize].read_texel = true; + if offset { + chain.levels[i as usize].border = 1; + chain.levels[i as usize].read_border_texel = true; + } + } + chain.pyramid = Some(PyramidShape { + count, + base_level: 0, + max_level: count - 1, + min_lod: s.lod_min as f64 / 256.0, + max_lod: if s.mipmap() { s.lod_max as f64 / 256.0 } else { 15.0 }, + }); + if let Some(detail) = s.detail { + // The detail image's entry lies past the base chain's. + if first + count > detail.descriptor { + return Err(Refusal("a detail image whose entry overlaps the base chain")); + } + self.texture_detail_backing_setup(chain, s.base_format, detail.descriptor, detail.width, detail.height)?; + chain.detail_weight = self.texture_detail_function_weight().unwrap_or_default(); + image.detail_image = Some(chain.detail_image); + image.detail_level = detail.level; + image.detail_mode = detail.mode; + image.read_filter_weight = true; + } + Ok(()) + } + + /// A volume's levels. Level 0 is entry 0; each slice is its own object, + /// the slices 64 << word 2 address units apart (volume_slice_stride()). + /// + /// MODEL POLICY for mipmaps: the library fills entry 0 alone, and the + /// model derives each further level as following the one before, one + /// slice stride per slice. That rule is taken only for what it was seen + /// on: repeat-wrapped volumes of at most 16 on each axis, with 128-byte + /// slices (word 2 1) and rows of 32 texels (stride 2), and for any + /// minification filter the shared sampler draws. Whether the board + /// mipmaps volumes at all is UNKNOWN (published accounts say it does not). + fn texture_volume_chain(&mut self, s: &Sampler, image: &mut GlTexImage<'static>, chain: &mut TextureChain) -> Result<(), Refusal> { + let [width, height, depth] = s.size(); + let entry = self.level_entry(0); + let stride_units = entry[1] & LEVEL_STRIDE_MASK; + let offset = s.placement == Placement::TexelOffset; + let mipmap = s.mipmap(); + // A stored border's slices hold the level 16 texels in, border at 15. + let interior = if offset { OFFSET_TEXELS } else { 0 }; + let border_texels = if offset { 2 } else { 0 }; + + let format = Self::level_format_check(entry[1])?; + if stride_units != level_stride_units(width + 2 * interior) { + return Err(Refusal("a volume row that is not the library's for its width")); + } + if (entry[0] & LEVEL_ADDRESS_MASK) == 0 || entry[2] == 0 || entry[2] > ODSY_VOLUME_SLICE_SHIFT_MAX { + return Err(Refusal("a volume level with no address or slice spacing")); + } + if mipmap + && (s.wrap.iter().any(|&w| w != Wrap::Repeat) + || width > 16 + || height > 16 + || depth > 16 + || stride_units != 2 + || entry[2] != 1) + { + return Err(Refusal("a mipmapped volume outside the derived-level policy")); + } + if self.objects.count > ODSY_OBJECTS { + return Err(Refusal("the object table is past its size")); + } + let slice_stride = volume_slice_stride(entry[2], format); + // The border slice sits at 15 slice strides from the level's base + // and the level's own slices from 16: INFERRED from the captured + // stored 2x2x2 volume, whose first upload is 15 strides above the + // level base at two bases; MODEL CHOICE for other sizes, as the + // same 16-texel rule as rows and columns. + let start = slice_stride + .checked_mul((interior - offset as u32) as u64) + .and_then(|s| s.checked_add((entry[0] & LEVEL_ADDRESS_MASK) as u64)) + .and_then(|s| u32::try_from(s).ok()); + let Some(mut base) = start else { + return Err(Refusal("a volume past the address space")); + }; + let count = s.level_count(); + if count > GL_TEX_LEVELS_MAX { + return Err(Refusal("a chain past the level table")); + } + chain.clear(); + let mut slice_next: u32 = 0; + for i in 0..count { + let level_width = cmax(1, width >> i); + let level_height = cmax(1, height >> i); + let level_depth = cmax(1, depth >> i); + if i != 0 && self.level_entry(i) != [0; 3] { + return Err(Refusal("a mipmapped volume with level table entries past 0")); + } + let level_slice = slice_next; + for z in 0..level_depth + border_texels { + let slice_base = (z as u64) + .checked_mul(slice_stride) + .and_then(|o| o.checked_add(base as u64)) + .and_then(|s| u32::try_from(s).ok()); + let Some(slice_base) = slice_base else { + return Err(Refusal("a volume past the address space")); + }; + let Some(o) = self.object_find(slice_base) else { + return Err(Refusal("a volume slice with no object")); + }; + let first_row = interior.saturating_sub(offset as u32); + let storage_end = (first_row as u64 + level_height as u64 - 1 + border_texels as u64) + .checked_mul(self.objects.width[o] as u64) + .and_then(|e| e.checked_add(self.objects.offset[o] as u64)) + .and_then(|e| e.checked_add(level_width as u64 + if offset { interior as u64 + 2 } else { 0 })); + if self.objects.width[o] != stride_units * 16 + || self.objects.rows[o] < first_row + level_height + border_texels + || !storage_end.is_some_and(|e| e <= self.objects.used as u64 && e <= self.objects.mem.size / 4) + { + return Err(Refusal("a volume slice whose object does not hold it")); + } + if offset && !self.object_region_written(o, first_row, first_row, level_width + 2, level_height + 2) { + return Err(Refusal("a stored volume slice not wholly written")); + } + if slice_next as usize >= self.tex.slice_offset.len() { + return Err(Refusal("a volume deeper than the model's slice table")); + } + self.tex.slice_offset[slice_next as usize] = self.objects.offset[o]; + self.tex.slice_width[slice_next as usize] = self.objects.width[o]; + slice_next += 1; + } + chain.touch(i); + chain.volumes[i as usize] = TextureVolume { + base, + stride_units, + level_word2: entry[2], + width: level_width, + height: level_height, + depth: level_depth, + base_format: s.base_format, + words: self.objects.used, + border: offset, + first_slice: level_slice, + slices: slice_next - level_slice, + ..Default::default() + }; + chain.levels[i as usize] = GlTexLevel { + level: i, + width: level_width, + height: level_height, + depth: level_depth, + base_format: s.base_format, + internal_format: s.base_format, + read_texel_3d: true, + ..Default::default() + }; + if i + 1 < count { + let next_base = (level_depth as u64) + .checked_mul(slice_stride) + .and_then(|n| n.checked_add(base as u64)) + .and_then(|n| u32::try_from(n).ok()); + let Some(next_base) = next_base else { + return Err(Refusal("a volume past the address space")); + }; + base = next_base; + } + } + chain.volume = chain.volumes[0]; + image.depth = depth; + image.volume = true; + image.wrap_r = s.wrap[2].gl(); + image.read_texel_3d = true; + image.read_border_texel_3d = offset; + if !self.texture_scale_bias_identity() { + chain.volume.scale_bias = true; + image.post_filter = true; + } + if mipmap { + chain.pyramid = Some(PyramidShape { + count, + base_level: 0, + max_level: count - 1, + min_lod: s.lod_min as f64 / 256.0, + max_lod: s.lod_max as f64 / 256.0, + }); + } + Ok(()) + } +} diff --git a/src/dev/odyssey/scanout.rs b/src/dev/odyssey/scanout.rs new file mode 100644 index 000000000..a2cc335ae --- /dev/null +++ b/src/dev/odyssey/scanout.rs @@ -0,0 +1,522 @@ +//! An SGI graphics head's scanout: its presented frame, dirty lines and +//! retrace. Display line 0 is the top. +//! +//! `frame` holds the presented frame (0x00RRGGBB, `frame_width` by +//! `frame_height`), which the board's display thread copies to the window; +//! `marks` holds the lines the board resolves again +//! (Board::scanout_update()), which any thread that writes a plane sets and +//! the display consumes. The retrace is counted on the host's +//! monotonic clock: `retrace_deadline` (-1 when disarmed) is when the next +//! one is due, and [`Scanout::next_retrace`] reports each as it comes due. +//! +//! Ported from include/hw/display/sgi/scanout.h and the shared SGI graphics +//! code in jamesbraid/qemu-sgi-origami. + +use crate::devlog::LogModule; +use super::raster_unit::ROW_WORDS; +use super::reader::DisplayShared; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use std::sync::Arc; + +pub(super) const SCANOUT_MAX_DIM: u32 = 16384; +/// 1 kHz: well above any SGI display's field rate. +pub(super) const SCANOUT_MIN_PERIOD_NS: i64 = 1000000; + +#[derive(Clone, Copy, Default, Debug)] +pub(super) struct ScanoutMode { + pub width: u32, // visible pixels and lines + pub height: u32, + pub frame_clocks: u64, // clocks per frame, blanking included + pub clock_hz: u32, // the clock they count; 0: no retrace +} + +/// What marked display lines dirty, for [`DirtyStats`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum DirtySite { + /// A caller that does not say. + Other, + /// A new mode or a reset: the whole frame. + Mode, + /// A raster job's rows (DrawnRows::take()). + Raster, + /// A swap: the rows the window's buffers differ in (Board::flip_rows()). + Flip, + /// A frame whose shape changed while it was resolved (Board::scanout_commit()). + Reshape, + /// The lookup RAM, a display mode or an overlay mode. + Lut, + /// The cursor's old and new place. + Cursor, + /// A pixel store from the front end (Board::paint_pixel()). + Pixel, + /// An image transfer's row. + Image, + /// A solid rectangle fill (Board::fill_rect()). + FillRect, + /// A write to the id or overlay planes (Board::clear_planes()). + ClearPlanes, + /// A screen-to-screen blit (Board::copy_rect()). + CopyRect, + /// A pattern quad (Board::pattern_fill()). + PatternFill, + /// A DMA into the colour buffer (Board::dma_to_buffer()). + Dma, + /// An integer pixel that goes through a logic operation or to the id planes. + Integer, +} + +impl DirtySite { + pub const ALL: [DirtySite; 15] = [ + DirtySite::Other, + DirtySite::Mode, + DirtySite::Raster, + DirtySite::Flip, + DirtySite::Reshape, + DirtySite::Lut, + DirtySite::Cursor, + DirtySite::Pixel, + DirtySite::Image, + DirtySite::FillRect, + DirtySite::ClearPlanes, + DirtySite::CopyRect, + DirtySite::PatternFill, + DirtySite::Dma, + DirtySite::Integer, + ]; + + pub fn name(self) -> &'static str { + match self { + DirtySite::Other => "other", + DirtySite::Mode => "mode", + DirtySite::Raster => "raster", + DirtySite::Flip => "flip", + DirtySite::Reshape => "reshape", + DirtySite::Lut => "lut", + DirtySite::Cursor => "cursor", + DirtySite::Pixel => "pixel", + DirtySite::Image => "image", + DirtySite::FillRect => "fill_rect", + DirtySite::ClearPlanes => "clear_planes", + DirtySite::CopyRect => "copy_rect", + DirtySite::PatternFill => "pattern_fill", + DirtySite::Dma => "dma", + DirtySite::Integer => "integer", + } + } +} + +/// Standing counters of the display's dirty lines, read by the monitor's +/// `vpro dirty`: per display frame the lines resolved, and the time to copy +/// their plane bytes aside. Plain integers the display thread owns; the +/// per-site marks the writers make are atomics in [`RowMarks`]. +#[derive(Clone, Default, Debug)] +pub(super) struct DirtyStats { + /// Display frames that staged their lines, and those with any line. + pub frames: u64, + pub busy_frames: u64, + /// Lines resolved in all frames, and in the widest one. + pub rows: u64, + pub max_rows: u64, + /// Swaps latched at a retrace. + pub swaps: u64, + /// The frame's copy of the plane bytes: total and longest, in + /// nanoseconds. + pub copy_ns: u64, + pub copy_max_ns: u64, +} + +impl DirtyStats { + pub fn staged(&mut self, rows: usize) { + self.frames += 1; + if rows != 0 { + self.busy_frames += 1; + self.rows += rows as u64; + self.max_rows = self.max_rows.max(rows as u64); + } + } + + /// Time one frame took copying the planes. + pub fn copied(&mut self, copy: std::time::Duration) { + let copy = copy.as_nanos() as u64; + + self.copy_ns += copy; + self.copy_max_ns = self.copy_max_ns.max(copy); + } + + pub fn report(&self, marks: &RowMarks, w: &mut dyn std::io::Write) -> std::io::Result<()> { + writeln!(w, "frames {} busy {} rows {} max {} swaps {}", self.frames, self.busy_frames, self.rows, self.max_rows, self.swaps)?; + writeln!( + w, + "planes copy_us {} max {}", + self.copy_ns / 1000, + self.copy_max_ns / 1000 + )?; + for site in DirtySite::ALL { + let c = marks.site(site); + if c[0] != 0 { + writeln!(w, "site {} marks {} rows {}", site.name(), c[0], c[1])?; + } + } + Ok(()) + } +} + +/// The scanout as the front end sees it: the mode it set, and the marks it +/// makes for the display to take. The presented frame, the retrace clock and +/// the rest of what the display keeps are the display's own +/// ([`ScanoutReader`]). +#[derive(Clone)] +pub(super) struct Scanout { + pub mode: ScanoutMode, + /// The lines to resolve again, and the lines a writer touched without + /// naming a buffer: shared with every thread that writes a plane. + pub marks: Arc, + /// Everything the board shares with the display. + pub shared: Arc, +} + +/// The retrace clock: the mode's frame period on the host's monotonic +/// clock. `deadline` (-1 when disarmed) is when the next retrace is due, +/// and [`RetraceClock::next_retrace`] reports each as it comes due. +#[derive(Clone, Debug)] +pub(super) struct RetraceClock { + pub mode: ScanoutMode, + pub deadline: i64, // virtual ns of the next retrace; -1 disarmed + pub epoch: i64, // virtual time of retrace 0 + pub frames: u64, // retraces since the epoch +} + +impl Default for RetraceClock { + fn default() -> Self { + RetraceClock { mode: ScanoutMode::default(), deadline: -1, epoch: 0, frames: 0 } + } +} + +impl Scanout { + /// A scanout over `shared`, with no mode. + pub fn new(shared: Arc) -> Self { + Scanout { mode: ScanoutMode::default(), marks: shared.marks.clone(), shared } + } + + /// A new mode: everything dirty, retrace phase restarted when the + /// display next looks. + pub fn set_mode(&mut self, mode: &ScanoutMode) { + self.mode = *mode; + self.mode.sanitize(); + crate::dlog!(LogModule::Odyssey, "sgi-odyssey: scanout mode: {}x{} frame {} clocks at {} Hz", self.mode.width, self.mode.height, self.mode.frame_clocks, self.mode.clock_hz); + let frame = self.mode.width != 0 && self.mode.height != 0; + self.shared.publish_mode(self.mode); + self.marks.set_height(if frame { self.mode.height } else { 0 }); + self.invalidate_all_for(DirtySite::Mode); + } + /// Repaint and restart the retrace phase. + pub fn reset(&mut self) { + self.shared.restart_retrace(); + self.invalidate_all_for(DirtySite::Mode); + } + + /// Resolve lines again because something other than the colour planes + /// changed (the lookup RAM, a display mode, the cursor, a swap). + pub fn repaint(&mut self, first: i32, count: i32) { + self.repaint_for(DirtySite::Other, first, count); + } + pub fn repaint_for(&mut self, site: DirtySite, first: i32, count: i32) { + self.marks.repaint(site, first, count); + } + pub fn repaint_all_for(&mut self, site: DirtySite) { + self.repaint_for(site, 0, self.mode.height as i32); + } + + /// Resolve lines again because the planes may have changed, in either + /// colour buffer: the lines, and the next swap measures the buffers. + pub fn invalidate(&mut self, first: i32, count: i32) { + self.invalidate_for(DirtySite::Other, first, count); + } + pub fn invalidate_for(&mut self, site: DirtySite, first: i32, count: i32) { + self.marks.invalidate(site, first, count); + } + /// invalidate_for() the lines [y, y + h) of a rectangle, which a caller + /// has not clipped to the frame. + pub fn invalidate_rows_for(&mut self, site: DirtySite, y: u32, h: u32) { + let first = y.min(i32::MAX as u32); + let end = (y as u64 + h as u64).min(i32::MAX as u64) as u32; + + self.invalidate_for(site, first as i32, (end - first) as i32); + } + #[cfg(test)] + pub fn invalidate_all(&mut self) { + self.invalidate_all_for(DirtySite::Other); + } + pub fn invalidate_all_for(&mut self, site: DirtySite) { + self.invalidate_for(site, 0, self.mode.height as i32); + } +} + +/// Words of the dirty-line bitset: a bit a display line of the largest mode. +pub(super) const DIRTY_WORDS: usize = (SCANOUT_MAX_DIM as usize).div_ceil(64); + +/// The display lines that must be resolved again, and the lines a writer +/// touched without naming a colour buffer, as bitsets any thread may set. +/// +/// A writer stores its pixels first and marks them second, with Release; the +/// display takes the marks with `swap(0, Acquire)` and only then reads the +/// planes. A row the display copied while a writer was still storing is +/// therefore marked again after the writer's last store, and the next frame +/// copies it whole. A mark published before its pixels would let the display +/// consume it, copy the old pixels and never hear of the new ones. +/// +/// The display takes marks and must put back any it does not apply +/// ([`RowMarks::repaint`]). +pub(super) struct RowMarks { + dirty: Box<[AtomicU64; DIRTY_WORDS]>, + unnamed: [AtomicU64; ROW_WORDS], + /// Lines of the frame the marks are for; 0 with no frame. A mark past it + /// is dropped. + height: AtomicU32, + /// By DirtySite: [marks, lines named]. + sites: [[AtomicU64; 2]; DirtySite::ALL.len()], + /// Tests: run between invalidate()'s two marks. + #[cfg(test)] + pub test_between: parking_lot::Mutex>>, +} + +impl Default for RowMarks { + fn default() -> Self { + RowMarks { + dirty: Box::new(std::array::from_fn(|_| AtomicU64::new(0))), + unnamed: std::array::from_fn(|_| AtomicU64::new(0)), + height: AtomicU32::new(0), + sites: std::array::from_fn(|_| std::array::from_fn(|_| AtomicU64::new(0))), + #[cfg(test)] + test_between: Default::default(), + } + } +} + +impl RowMarks { + /// The frame is `height` lines tall: marks past it are dropped, and a + /// new height forgets the lines marked for the old one (the caller marks + /// the new frame whole). + pub fn set_height(&self, height: u32) { + if self.height.swap(height, Ordering::AcqRel) != height { + for w in self.dirty.iter() { + w.store(0, Ordering::Release); + } + } + } + + /// The lines of the frame, 0 with none. + pub fn height(&self) -> u32 { + self.height.load(Ordering::Acquire) + } + + /// Mark lines [first, first + count) to be resolved again, for a change + /// that is not in the colour planes (the lookup RAM, the cursor, a + /// swap) or that the caller has already named in `unnamed`. + pub fn repaint(&self, site: DirtySite, first: i32, count: i32) { + let h = self.height() as i32; + let last = first.wrapping_add(count).wrapping_sub(1); + + let first = first.max(0); + let last = last.min(h - 1); + if first > last { + return; + } + let c = &self.sites[site as usize]; + c[0].fetch_add(1, Ordering::Relaxed); + c[1].fetch_add((last - first + 1) as u64, Ordering::Relaxed); + set_range(&self.dirty[..], first as usize, last as usize); + } + + /// Mark lines [first, first + count) after a store to the planes, in + /// either colour buffer: the lines, and the next swap measures the + /// buffers there. The caller has stored its pixels already. The lines + /// are named in `unnamed` before they are marked dirty: the other order + /// would let a frame take the dirty mark and a swap latch before the + /// unnamed mark, and the swap would not repaint a line whose buffers + /// differ, for good. + pub fn invalidate(&self, site: DirtySite, first: i32, count: i32) { + self.name_unnamed(first, count); + #[cfg(test)] + if let Some(hook) = self.test_between.lock().as_ref() { + hook(self); + } + self.repaint(site, first, count); + } + + /// The first half of invalidate(): lines whose buffer the writer did not + /// name. + pub fn name_unnamed(&self, first: i32, count: i32) { + let last = first.wrapping_add(count).wrapping_sub(1).min(ROW_WORDS as i32 * 64 - 1); + + if first.max(0) <= last { + set_range(&self.unnamed[..], first.max(0) as usize, last as usize); + } + } + + /// Take every dirty line below `height`, in order, and clear the marks. + /// Read the planes only after this. + pub fn take_dirty(&self, height: usize, rows: &mut Vec) { + for (i, word) in self.dirty.iter().enumerate() { + let mut w = word.swap(0, Ordering::Acquire); + + while w != 0 { + let y = i * 64 + w.trailing_zeros() as usize; + w &= w - 1; + if y < height { + rows.push(y as u32); + } + } + } + } + + /// Whether any line was touched without naming its buffer and not yet + /// measured. + pub fn unnamed_pending(&self) -> bool { + self.unnamed.iter().any(|w| w.load(Ordering::Acquire) != 0) + } + + /// Take the lines touched without naming a buffer, and clear them. A + /// write that lands after this sets its line again, and the next swap + /// measures it. + pub fn take_unnamed(&self) -> [u64; ROW_WORDS] { + std::array::from_fn(|i| self.unnamed[i].swap(0, Ordering::Acquire)) + } + + /// [marks, lines named] by `site`. + pub fn site(&self, site: DirtySite) -> [u64; 2] { + let c = &self.sites[site as usize]; + [c[0].load(Ordering::Relaxed), c[1].load(Ordering::Relaxed)] + } + + pub fn reset_sites(&self) { + for c in &self.sites { + c[0].store(0, Ordering::Relaxed); + c[1].store(0, Ordering::Relaxed); + } + } + + /// Tests: the lines marked dirty now, not taken. + #[cfg(test)] + pub fn dirty_rows(&self) -> Vec { + (0..DIRTY_WORDS * 64).filter(|&y| self.dirty[y >> 6].load(Ordering::Acquire) >> (y & 63) & 1 != 0).collect() + } + + /// Tests: forget every dirty line. + #[cfg(test)] + pub fn clear_dirty(&self) { + for w in self.dirty.iter() { + w.store(0, Ordering::Release); + } + } +} + +/// Set bits first..=last of a bitset, a word at a time, with Release. +fn set_range(words: &[AtomicU64], first: usize, last: usize) { + for i in first >> 6..=last >> 6 { + let lo = if i == first >> 6 { first & 63 } else { 0 }; + let hi = if i == last >> 6 { last & 63 } else { 63 }; + let mask = (u64::MAX >> (63 - (hi - lo))) << lo; + + words[i].fetch_or(mask, Ordering::Release); + } +} + +/// Nanoseconds in a second. +pub(super) const NS_PER_SECOND: u64 = 1_000_000_000; + +/// The host's monotonic clock in nanoseconds since the first call: the +/// time base of every scanout's retrace. +pub(super) fn monotonic_ns() -> i64 { + static EPOCH: std::sync::OnceLock = std::sync::OnceLock::new(); + EPOCH.get_or_init(std::time::Instant::now).elapsed().as_nanos() as i64 +} + +/// a * b / c without overflowing the product. +pub(super) fn mul_div(a: u64, b: u64, c: u64) -> u64 { + ((a as u128 * b as u128) / c as u128) as u64 +} + +impl ScanoutMode { + /// A mode may be derived from guest-programmed timing tables, so it is + /// checked before anything is allocated or armed from it. A size past + /// SCANOUT_MAX_DIM presents nothing, and a period shorter than + /// SCANOUT_MIN_PERIOD_NS, which would re-arm the retrace faster than + /// the host can run it, or a frame too long to count, has no retrace. + /// Returns whether the mode was used as given. + pub fn sanitize(&mut self) -> bool { + let mut ok = true; + + if self.width > SCANOUT_MAX_DIM || self.height > SCANOUT_MAX_DIM { + crate::dlog!( + LogModule::Odyssey, + "sgi-scanout: {}x{} exceeds the largest mode, {}x{}", + self.width, + self.height, + SCANOUT_MAX_DIM, + SCANOUT_MAX_DIM + ); + self.width = 0; + self.height = 0; + ok = false; + } + if self.clock_hz != 0 + && (self.frame_clocks > u32::MAX as u64 + || (mul_div(self.frame_clocks, NS_PER_SECOND, self.clock_hz as u64) as i64) + < SCANOUT_MIN_PERIOD_NS) + { + crate::dlog!( + LogModule::Odyssey, + "sgi-scanout: a frame of {} clocks at {} Hz is outside the retrace periods modelled", + self.frame_clocks, + self.clock_hz + ); + self.clock_hz = 0; + ok = false; + } + ok + } +} + +impl RetraceClock { + /// Virtual time of retrace n of the current mode, or -1 with no retrace. + pub fn retrace_time(&self, n: u64) -> i64 { + let hz = self.mode.clock_hz as u64; + let fc = self.mode.frame_clocks; + + if hz == 0 || fc == 0 { + return -1; + } + // n * fc / hz seconds, exactly, without overflowing for any n a run + // reaches: whole seconds' worth of frames first, then the rest, whose + // product fits because both factors are below 2^32. + (self.epoch as u64) + .wrapping_add((n / hz).wrapping_mul(fc).wrapping_mul(NS_PER_SECOND)) + .wrapping_add(mul_div((n % hz).wrapping_mul(fc), NS_PER_SECOND, hz)) as i64 + } + + /// Whether a retrace has come due at `now_ns`. If so it is counted and + /// the next one armed, and the caller runs it; a caller that has fallen + /// behind gets each missed retrace in turn. + pub fn next_retrace(&mut self, now_ns: i64) -> bool { + if self.deadline < 0 || now_ns < self.deadline { + return false; + } + self.frames += 1; + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: scanout retrace: frame {}", self.frames); + self.arm(); + true + } + + pub fn arm(&mut self) { + let when = self.retrace_time(self.frames + 1); + + self.deadline = if when < 0 { -1 } else { when }; + } + + pub fn restart(&mut self) { + self.epoch = monotonic_ns(); + self.frames = 0; + self.arm(); + } +} diff --git a/src/dev/odyssey/testdata/README.md b/src/dev/odyssey/testdata/README.md new file mode 100644 index 000000000..eb37888fd --- /dev/null +++ b/src/dev/odyssey/testdata/README.md @@ -0,0 +1,60 @@ +# VPro (Odyssey) regression nets + +Three checks guard the VPro model's rendering against a change that moves +pixels, vertices, board state or speed without meaning to. Everything runs +headless. `scripts/vpro-goldens.sh` wraps the commands; the plain forms are +shown here. The tests live in `src/dev/odyssey/golden/`. + +| Net | What it pins | Data | Runs | +|---|---|---|---| +| Frames | 78 scenes drawn through `Board::gl_draw`: flat, smooth and textured triangles, quads and polygons (modes, stipple, smoothing, culling, offset), lines (width, stipple, smoothing), points, lights of every kind, fog, depth, stencil, alpha test, blending (factors, equations, constant colour, logic op, colour mask), scissor, frustum and user-plane clipping, w at and behind the eye, projection and window-matrix modes, texture formats, environments, filters, wraps and detail | `golden/scenes.tsv`, `golden/frames/*.png` | `cargo test --release --lib odyssey::golden` | +| Vertices | Object-space points through six matrix and viewport setups (eye, clip and window positions) and six light setups (lit colour) | `golden/vertices.txt` | same | +| Replay | Recorded guest sessions (Smoke & Mirrors and perfly, from power-on): the bus stores replayed through a fresh board, its DMA reads answered from the recording, both colour buffers, depth, stencil and object memory hashed every 500,000 records | `replay/*.hashes`; the recordings are not in git | same, with the recordings in place | + +## Frames and vertices + +The default check is bit-exact: each scene's colour, depth and stencil +planes over its 128 x 128 window hash to the value in `scenes.tsv`. A +failure names the scene and shows how far the frame moved from the +reference PNGs (`VPRO_GOLDEN_OUT=` writes the failing colour plane to look at). + +A change that moves numbers on purpose (the vertex side moving to 32-bit +floats) is judged by `VPRO_GOLDEN=tolerance` instead. A scene whose hash +changed then passes when, against its reference images, at most `colour_bad` +pixels differ by more than `colour_delta` in a channel, at most `depth_bad` by +more than `depth_delta` in the 24-bit depth word, and at most `stencil_bad` +stencil pixels differ. `scenes.tsv` holds those five limits per scene (new +scenes get 2 / 64 / 64 / 64 / 64); edit them by hand where a scene is +touchier. The vertex numbers are compared against the limits in the header of +`vertices.txt` the same way. + +To re-take the goldens after a change that is meant to move the frames: + + VPRO_GOLDEN_BLESS=1 cargo test --release --lib odyssey::golden::bless -- --ignored --nocapture + +Look at what changed (`git diff --stat`, open the PNGs) before committing: the +hashes alone say nothing about whether the new frame is right. Blessing keeps +each scene's tolerance and drops goldens of scenes that are gone. A new scene +is a function in `golden/scenes.rs` and a line in its `scenes!` list. + +## Replay + +The recordings are large (tens of MB) and stay out of git, as IMPACT's `.rec` +files do. They are found in `ignore/vpro-golden/` (git-ignored) or in +`VPRO_REPLAY_DIR`. Without them the replay test prints SKIPPED and passes; +`scripts/vpro-goldens.sh replay` fails instead. + +Record from power-on with `IRIS_ODSY_REC=.rec iris ...` (the board is +built with the recorder; the file is gzip-compressed and stays readable if the +emulator is killed). Boot to the desktop, start the application for a few +seconds, stop. Name it for the scene, put it in the directory, then take its +checkpoints: + + VPRO_REPLAY_BLESS=1 cargo test --release --lib odyssey::golden::replay::replay_bless -- --ignored --nocapture + +The checkpoints (`replay/.hashes`) are what is committed. `VPRO_REPLAY=` +replays one file; `VPRO_REPLAY_OUT=` saves the final colour buffer +(unmapped: no colour map, window ids or cursor, so it is the planes, not the +screen). The replay refuses to bless a recording whose DMA reads it cannot +follow. Re-take the checkpoints only for a change that is meant to move board +state, after the frames above say the change is right. diff --git a/src/dev/odyssey/testdata/golden/frames/alpha_test.colour.png b/src/dev/odyssey/testdata/golden/frames/alpha_test.colour.png new file mode 100644 index 000000000..1742f5c29 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/alpha_test.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/alpha_test.depth.png b/src/dev/odyssey/testdata/golden/frames/alpha_test.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/alpha_test.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/alpha_test.stencil.png b/src/dev/odyssey/testdata/golden/frames/alpha_test.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/alpha_test.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.colour.png b/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.colour.png new file mode 100644 index 000000000..e47b34abb Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.depth.png b/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.stencil.png b/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_constant_colour.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_equations.colour.png b/src/dev/odyssey/testdata/golden/frames/blend_equations.colour.png new file mode 100644 index 000000000..1a70e065c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_equations.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_equations.depth.png b/src/dev/odyssey/testdata/golden/frames/blend_equations.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_equations.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_equations.stencil.png b/src/dev/odyssey/testdata/golden/frames/blend_equations.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_equations.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.colour.png b/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.colour.png new file mode 100644 index 000000000..39445b102 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.depth.png b/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.stencil.png b/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_factor_grid.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.colour.png b/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.colour.png new file mode 100644 index 000000000..e261e8d8d Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.depth.png b/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.stencil.png b/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/blend_triangles_over.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.colour.png b/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.colour.png new file mode 100644 index 000000000..1e2b15297 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.depth.png b/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.stencil.png b/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_frustum_xy.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.colour.png b/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.colour.png new file mode 100644 index 000000000..6af9e903b Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.depth.png b/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.depth.png new file mode 100644 index 000000000..bd8e4ab7c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.stencil.png b/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_frustum_z.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_plane.colour.png b/src/dev/odyssey/testdata/golden/frames/clip_user_plane.colour.png new file mode 100644 index 000000000..2cf24a49c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_plane.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_plane.depth.png b/src/dev/odyssey/testdata/golden/frames/clip_user_plane.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_plane.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_plane.stencil.png b/src/dev/odyssey/testdata/golden/frames/clip_user_plane.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_plane.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.colour.png b/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.colour.png new file mode 100644 index 000000000..85cb0d356 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.depth.png b/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.depth.png new file mode 100644 index 000000000..05d40ccae Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.stencil.png b/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_plane_textured_smooth.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.colour.png b/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.colour.png new file mode 100644 index 000000000..de4e6282a Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.depth.png b/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.stencil.png b/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_user_planes_rotated.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.colour.png b/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.colour.png new file mode 100644 index 000000000..7399e0a72 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.depth.png b/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.stencil.png b/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/clip_w_behind_eye.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.colour.png b/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.colour.png new file mode 100644 index 000000000..a8a969ef3 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.depth.png b/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.stencil.png b/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/cull_and_front_face.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_always_masked.colour.png b/src/dev/odyssey/testdata/golden/frames/depth_always_masked.colour.png new file mode 100644 index 000000000..ec41833ca Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_always_masked.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_always_masked.depth.png b/src/dev/odyssey/testdata/golden/frames/depth_always_masked.depth.png new file mode 100644 index 000000000..ab5009303 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_always_masked.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_always_masked.stencil.png b/src/dev/odyssey/testdata/golden/frames/depth_always_masked.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_always_masked.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.colour.png b/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.colour.png new file mode 100644 index 000000000..82fe7e8b5 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.depth.png b/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.depth.png new file mode 100644 index 000000000..8737aec7a Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.stencil.png b/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_equal_notequal.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.colour.png b/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.colour.png new file mode 100644 index 000000000..034c5e0f4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.depth.png b/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.depth.png new file mode 100644 index 000000000..ddf49bbd6 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.stencil.png b/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_lequal_greater.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_less.colour.png b/src/dev/odyssey/testdata/golden/frames/depth_less.colour.png new file mode 100644 index 000000000..a0413c9d9 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_less.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_less.depth.png b/src/dev/odyssey/testdata/golden/frames/depth_less.depth.png new file mode 100644 index 000000000..3d4d980ac Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_less.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_less.stencil.png b/src/dev/odyssey/testdata/golden/frames/depth_less.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_less.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_range_wide.colour.png b/src/dev/odyssey/testdata/golden/frames/depth_range_wide.colour.png new file mode 100644 index 000000000..4494d0a4a Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_range_wide.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_range_wide.depth.png b/src/dev/odyssey/testdata/golden/frames/depth_range_wide.depth.png new file mode 100644 index 000000000..7fedb0039 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_range_wide.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/depth_range_wide.stencil.png b/src/dev/odyssey/testdata/golden/frames/depth_range_wide.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/depth_range_wide.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.colour.png b/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.colour.png new file mode 100644 index 000000000..d2658771d Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.depth.png b/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.stencil.png b/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_lines_and_points.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_over_blend.colour.png b/src/dev/odyssey/testdata/golden/frames/fog_over_blend.colour.png new file mode 100644 index 000000000..3810d1e7a Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_over_blend.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_over_blend.depth.png b/src/dev/odyssey/testdata/golden/frames/fog_over_blend.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_over_blend.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_over_blend.stencil.png b/src/dev/odyssey/testdata/golden/frames/fog_over_blend.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_over_blend.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.colour.png b/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.colour.png new file mode 100644 index 000000000..ac4bda801 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.depth.png b/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.depth.png new file mode 100644 index 000000000..fc27fbcd4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.stencil.png b/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/fog_ramp_triangles.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_directional.colour.png b/src/dev/odyssey/testdata/golden/frames/light_directional.colour.png new file mode 100644 index 000000000..8458a9eef Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_directional.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_directional.depth.png b/src/dev/odyssey/testdata/golden/frames/light_directional.depth.png new file mode 100644 index 000000000..17acb072c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_directional.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_directional.stencil.png b/src/dev/odyssey/testdata/golden/frames/light_directional.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_directional.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.colour.png b/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.colour.png new file mode 100644 index 000000000..49f8b8806 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.depth.png b/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.depth.png new file mode 100644 index 000000000..5bf814ae6 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.stencil.png b/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_lit_blended_textured.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.colour.png b/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.colour.png new file mode 100644 index 000000000..b7fe42bc2 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.depth.png b/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.depth.png new file mode 100644 index 000000000..17acb072c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.stencil.png b/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_local_viewer_specular.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.colour.png b/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.colour.png new file mode 100644 index 000000000..97fa7d12d Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.depth.png b/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.depth.png new file mode 100644 index 000000000..17acb072c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.stencil.png b/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_positional_attenuated.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_spot.colour.png b/src/dev/odyssey/testdata/golden/frames/light_spot.colour.png new file mode 100644 index 000000000..410fb09a2 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_spot.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_spot.depth.png b/src/dev/odyssey/testdata/golden/frames/light_spot.depth.png new file mode 100644 index 000000000..17acb072c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_spot.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_spot.stencil.png b/src/dev/odyssey/testdata/golden/frames/light_spot.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_spot.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.colour.png b/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.colour.png new file mode 100644 index 000000000..6763c9c58 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.depth.png b/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.depth.png new file mode 100644 index 000000000..17acb072c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.stencil.png b/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_three_lights_colour_material.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.colour.png b/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.colour.png new file mode 100644 index 000000000..a31267cfe Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.depth.png b/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.depth.png new file mode 100644 index 000000000..17acb072c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.stencil.png b/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/light_unlit_emission_only.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_flat.colour.png b/src/dev/odyssey/testdata/golden/frames/lines_flat.colour.png new file mode 100644 index 000000000..2bc565dbb Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_flat.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_flat.depth.png b/src/dev/odyssey/testdata/golden/frames/lines_flat.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_flat.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_flat.stencil.png b/src/dev/odyssey/testdata/golden/frames/lines_flat.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_flat.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_smooth.colour.png b/src/dev/odyssey/testdata/golden/frames/lines_smooth.colour.png new file mode 100644 index 000000000..b8a2fa0f1 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_smooth.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_smooth.depth.png b/src/dev/odyssey/testdata/golden/frames/lines_smooth.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_smooth.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_smooth.stencil.png b/src/dev/odyssey/testdata/golden/frames/lines_smooth.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_smooth.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.colour.png b/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.colour.png new file mode 100644 index 000000000..13c12264f Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.depth.png b/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.stencil.png b/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_smooth_colour.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_stippled.colour.png b/src/dev/odyssey/testdata/golden/frames/lines_stippled.colour.png new file mode 100644 index 000000000..4c0ba3476 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_stippled.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_stippled.depth.png b/src/dev/odyssey/testdata/golden/frames/lines_stippled.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_stippled.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_stippled.stencil.png b/src/dev/odyssey/testdata/golden/frames/lines_stippled.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_stippled.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_wide.colour.png b/src/dev/odyssey/testdata/golden/frames/lines_wide.colour.png new file mode 100644 index 000000000..c2231e9b3 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_wide.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_wide.depth.png b/src/dev/odyssey/testdata/golden/frames/lines_wide.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_wide.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/lines_wide.stencil.png b/src/dev/odyssey/testdata/golden/frames/lines_wide.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/lines_wide.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.colour.png b/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.colour.png new file mode 100644 index 000000000..c34cdd693 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.depth.png b/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.stencil.png b/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/logic_op_and_colour_mask.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.colour.png b/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.colour.png new file mode 100644 index 000000000..018bd5631 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.depth.png b/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.depth.png new file mode 100644 index 000000000..f900b2837 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.stencil.png b/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/perspective_correct_colour.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/points.colour.png b/src/dev/odyssey/testdata/golden/frames/points.colour.png new file mode 100644 index 000000000..dba09177a Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/points.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/points.depth.png b/src/dev/odyssey/testdata/golden/frames/points.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/points.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/points.stencil.png b/src/dev/odyssey/testdata/golden/frames/points.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/points.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/points_smooth.colour.png b/src/dev/odyssey/testdata/golden/frames/points_smooth.colour.png new file mode 100644 index 000000000..f2adf64a4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/points_smooth.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/points_smooth.depth.png b/src/dev/odyssey/testdata/golden/frames/points_smooth.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/points_smooth.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/points_smooth.stencil.png b/src/dev/odyssey/testdata/golden/frames/points_smooth.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/points_smooth.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_convex.colour.png b/src/dev/odyssey/testdata/golden/frames/polygon_convex.colour.png new file mode 100644 index 000000000..7d2ec4073 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_convex.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_convex.depth.png b/src/dev/odyssey/testdata/golden/frames/polygon_convex.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_convex.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_convex.stencil.png b/src/dev/odyssey/testdata/golden/frames/polygon_convex.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_convex.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.colour.png b/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.colour.png new file mode 100644 index 000000000..e57e78f03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.depth.png b/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.stencil.png b/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_modes_line_point.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_offset.colour.png b/src/dev/odyssey/testdata/golden/frames/polygon_offset.colour.png new file mode 100644 index 000000000..37a003466 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_offset.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_offset.depth.png b/src/dev/odyssey/testdata/golden/frames/polygon_offset.depth.png new file mode 100644 index 000000000..830fcbb9b Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_offset.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_offset.stencil.png b/src/dev/odyssey/testdata/golden/frames/polygon_offset.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_offset.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_smooth.colour.png b/src/dev/odyssey/testdata/golden/frames/polygon_smooth.colour.png new file mode 100644 index 000000000..dc007bbb4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_smooth.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_smooth.depth.png b/src/dev/odyssey/testdata/golden/frames/polygon_smooth.depth.png new file mode 100644 index 000000000..acc451dad Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_smooth.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_smooth.stencil.png b/src/dev/odyssey/testdata/golden/frames/polygon_smooth.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_smooth.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_stipple.colour.png b/src/dev/odyssey/testdata/golden/frames/polygon_stipple.colour.png new file mode 100644 index 000000000..3ba7c1c72 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_stipple.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_stipple.depth.png b/src/dev/odyssey/testdata/golden/frames/polygon_stipple.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_stipple.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/polygon_stipple.stencil.png b/src/dev/odyssey/testdata/golden/frames/polygon_stipple.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/polygon_stipple.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/projection_frustum.colour.png b/src/dev/odyssey/testdata/golden/frames/projection_frustum.colour.png new file mode 100644 index 000000000..571b823e0 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/projection_frustum.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/projection_frustum.depth.png b/src/dev/odyssey/testdata/golden/frames/projection_frustum.depth.png new file mode 100644 index 000000000..4e8a2f326 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/projection_frustum.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/projection_frustum.stencil.png b/src/dev/odyssey/testdata/golden/frames/projection_frustum.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/projection_frustum.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.colour.png b/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.colour.png new file mode 100644 index 000000000..cc13c049c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.depth.png b/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.depth.png new file mode 100644 index 000000000..b6f1b91b0 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.stencil.png b/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/projection_ortho_rotated.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.colour.png b/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.colour.png new file mode 100644 index 000000000..2a04abce5 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.depth.png b/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.stencil.png b/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/quad_shared_edges.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/scissor.colour.png b/src/dev/odyssey/testdata/golden/frames/scissor.colour.png new file mode 100644 index 000000000..2fa46547e Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/scissor.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/scissor.depth.png b/src/dev/odyssey/testdata/golden/frames/scissor.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/scissor.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/scissor.stencil.png b/src/dev/odyssey/testdata/golden/frames/scissor.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/scissor.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.colour.png b/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.colour.png new file mode 100644 index 000000000..ce57863de Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.depth.png b/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.depth.png new file mode 100644 index 000000000..968359e2f Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.stencil.png b/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.stencil.png new file mode 100644 index 000000000..66feb5453 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_depth_fail.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.colour.png b/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.colour.png new file mode 100644 index 000000000..57c766de5 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.depth.png b/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.depth.png new file mode 100644 index 000000000..772132c6e Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.stencil.png b/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.stencil.png new file mode 100644 index 000000000..c837e7065 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_mark_then_masked.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_ops.colour.png b/src/dev/odyssey/testdata/golden/frames/stencil_ops.colour.png new file mode 100644 index 000000000..5b9575fff Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_ops.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_ops.depth.png b/src/dev/odyssey/testdata/golden/frames/stencil_ops.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_ops.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/stencil_ops.stencil.png b/src/dev/odyssey/testdata/golden/frames/stencil_ops.stencil.png new file mode 100644 index 000000000..64147e45c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/stencil_ops.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.colour.png new file mode 100644 index 000000000..ebcc1513c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_clamp_to_edge_linear.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.colour.png new file mode 100644 index 000000000..341fa9e0e Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.colour.png new file mode 100644 index 000000000..f76b3f1b5 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_detail_colour_modulate.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.colour.png new file mode 100644 index 000000000..f7c80072d Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_luminance.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.colour.png new file mode 100644 index 000000000..723e4a220 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_blend_rgba.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.colour.png new file mode 100644 index 000000000..d85ff9854 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgb.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.colour.png new file mode 100644 index 000000000..91abf7611 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_decal_rgba.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.colour.png new file mode 100644 index 000000000..5d19c919c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_luminance_alpha.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.colour.png new file mode 100644 index 000000000..fb15d1157 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_modulate_rgba.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.colour.png new file mode 100644 index 000000000..aa16f038a Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_alpha.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.colour.png new file mode 100644 index 000000000..c1c506b3d Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_env_replace_rgba.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.colour.png new file mode 100644 index 000000000..45d0b8689 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_alpha.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.colour.png new file mode 100644 index 000000000..f95b5cc02 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_intensity.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.colour.png new file mode 100644 index 000000000..51956c1b8 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.colour.png new file mode 100644 index 000000000..97c9c6025 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_luminance_alpha.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.colour.png new file mode 100644 index 000000000..41cf2a5f5 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_rgb.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.colour.png new file mode 100644 index 000000000..7a849619c Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_format_rgba.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.colour.png new file mode 100644 index 000000000..09746055d Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.depth.png new file mode 100644 index 000000000..db106fee4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_linear.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.colour.png new file mode 100644 index 000000000..fccbb00d5 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.depth.png new file mode 100644 index 000000000..db106fee4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_linear_nearest.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.colour.png new file mode 100644 index 000000000..6629019eb Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.depth.png new file mode 100644 index 000000000..db106fee4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_linear.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.colour.png new file mode 100644 index 000000000..5b91a9dd9 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.depth.png new file mode 100644 index 000000000..db106fee4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_mip_nearest_nearest.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.colour.png new file mode 100644 index 000000000..bc19ae039 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_nearest_repeat.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.colour.png b/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.colour.png new file mode 100644 index 000000000..5db1dde92 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.depth.png b/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.depth.png new file mode 100644 index 000000000..cbfba67a6 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.stencil.png b/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tex_with_fog_blend_depth.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_flat.colour.png b/src/dev/odyssey/testdata/golden/frames/tri_flat.colour.png new file mode 100644 index 000000000..12bc509a1 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_flat.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_flat.depth.png b/src/dev/odyssey/testdata/golden/frames/tri_flat.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_flat.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_flat.stencil.png b/src/dev/odyssey/testdata/golden/frames/tri_flat.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_flat.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_smooth.colour.png b/src/dev/odyssey/testdata/golden/frames/tri_smooth.colour.png new file mode 100644 index 000000000..c620ae5ad Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_smooth.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_smooth.depth.png b/src/dev/odyssey/testdata/golden/frames/tri_smooth.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_smooth.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_smooth.stencil.png b/src/dev/odyssey/testdata/golden/frames/tri_smooth.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_smooth.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.colour.png b/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.colour.png new file mode 100644 index 000000000..07d2a8b83 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.depth.png b/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.stencil.png b/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/tri_tiny_and_thin.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/two_sided_colours.colour.png b/src/dev/odyssey/testdata/golden/frames/two_sided_colours.colour.png new file mode 100644 index 000000000..9b14dfed4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/two_sided_colours.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/two_sided_colours.depth.png b/src/dev/odyssey/testdata/golden/frames/two_sided_colours.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/two_sided_colours.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/two_sided_colours.stencil.png b/src/dev/odyssey/testdata/golden/frames/two_sided_colours.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/two_sided_colours.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.colour.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.colour.png new file mode 100644 index 000000000..07f5ad762 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.depth.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.depth.png new file mode 100644 index 000000000..cacebcaa0 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.stencil.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_clip_and_lines.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.colour.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.colour.png new file mode 100644 index 000000000..f054a9ffb Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.depth.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.depth.png new file mode 100644 index 000000000..ed35eb5c4 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.stencil.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_rows_down.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.colour.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.colour.png new file mode 100644 index 000000000..bf627c1a0 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.depth.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.depth.png new file mode 100644 index 000000000..f0fe5f9f6 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.stencil.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_scaled.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.colour.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.colour.png new file mode 100644 index 000000000..2b2b1046e Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.colour.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.depth.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.depth.png new file mode 100644 index 000000000..831a5891a Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.depth.png differ diff --git a/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.stencil.png b/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.stencil.png new file mode 100644 index 000000000..2f3ad0a03 Binary files /dev/null and b/src/dev/odyssey/testdata/golden/frames/window_matrix_triangles.stencil.png differ diff --git a/src/dev/odyssey/testdata/golden/scenes.tsv b/src/dev/odyssey/testdata/golden/scenes.tsv new file mode 100644 index 000000000..53192b440 --- /dev/null +++ b/src/dev/odyssey/testdata/golden/scenes.tsv @@ -0,0 +1,85 @@ +# VPro draw-level goldens (src/dev/odyssey/golden). One scene a line: +# name hash(blake3 of colour, depth, stencil) colour_delta colour_bad depth_delta depth_bad stencil_bad +# The hash is the default, bit-exact check. The five numbers are the limits for +# VPRO_GOLDEN=tolerance: pixels past colour_delta (per channel) or depth_delta (24-bit +# word) may number at most colour_bad / depth_bad, and at most stencil_bad stencil pixels +# may differ. Edit them by hand per scene; blessing keeps what is there. +alpha_test e44221018a0c6042d64889a69ad814c679ae94d01e700dca20d8d2f6a9c91e6d 2 64 64 64 64 +blend_constant_colour 68b70da35ac7646a3e6a3a587f89188905df49db445a7990e0ed1dbf13bb3b1f 2 64 64 64 64 +blend_equations a3417410ec868e34e5af1a15f43c6900516a649ac1703b5eaa29208546d2a5a1 2 64 64 64 64 +blend_factor_grid 9b4146f641e5a22333883a20e27b712cea74270e9173c029c26dd915e41e25cc 2 64 64 64 64 +blend_triangles_over 0741a64890d8351fb747e34d3bbfba33b0999c2a6c4ba791239737dc87a3d7b4 2 64 64 64 64 +clip_frustum_xy 183508d6e31c20bd7b34a07cde607db3de0c0636d9b7f80a720963e13f133cff 2 64 64 64 64 +clip_frustum_z b1a156adaba369357f0efb83f6cbd910533da13158e655eddf3a7b3a9d73b126 2 64 64 64 64 +clip_user_plane d23026f68295d764fdc39f4f4133dd82588dd3746f7a064276f01ec2a16c23f4 2 64 64 64 64 +clip_user_plane_textured_smooth 2f4c38f8b462d2270176b28d4204965ac0290591ad17ec87459c5df057c2f93d 2 64 64 64 64 +clip_user_planes_rotated 6af4c4766cfca317c13850efbed64bc785bf69aecafd65f8d0bf023065733026 2 64 64 64 64 +clip_w_behind_eye 14abd1b2b48b59384aaeeeed79fb209e34cef744aba2923306ed03c1161f3aea 2 64 64 64 64 +cull_and_front_face 7feef158a1a4100ac4183a705c8560cf4f07ce79c720e16a02182c662343e59a 2 64 64 64 64 +depth_always_masked 73799eae0d6e6dd72cf781cdb77c34f78be1c2c56c22aa27b00a11df71102a5d 2 64 64 64 64 +depth_equal_notequal ed4426f79304ee1fb863960ab172efc64670d371ea748b56137411e016d14020 2 64 64 64 64 +depth_lequal_greater 842d4a13f7ca7d2ffb8db39842c5b455c39a568ea88a5551e8c9c324bb69823f 2 64 64 64 64 +depth_less ac636d4655f7f97754d94bc1aa9a8957d8d0fd223fa76532fd02ccc9870cd2dc 2 64 64 64 64 +depth_range_wide 519c92d30f8a86425386e97605e344db611b15bd1bdd18c3cadc3af6bff2bc37 2 64 64 64 64 +fog_lines_and_points f80f51a3d76444f07416e4eb2e3f363b0f04bddd4b5271e7f484cda3653b9cd4 2 64 64 64 64 +fog_over_blend cfe464c3fbe8339e0543dacd0ff409bfe87b8077047ef8e72d073dfa3ed679f5 2 64 64 64 64 +fog_ramp_triangles bb210e856a057dc7385eb5a12874340310cca8378d68644d5e4389127982ddd6 2 64 64 64 64 +light_directional 3c7c2fd1338480a50263aacfc67c0b65ca67b9e08022f1b050710261966f542b 2 64 64 64 64 +light_lit_blended_textured 4c08c0926756e0f3c8b4093836244f619d6b2fb7ef01f82b486b5f1b8539e241 2 64 64 64 64 +light_local_viewer_specular ff772e5342b13fef409f0ae81f2c2a6332ff3d2076aec11477e299446dbe07c5 2 64 64 64 64 +light_positional_attenuated fc1bb3c1786947b75e8524e2650859521049566093a243b6181984b76e09f0e6 2 64 64 64 64 +light_spot ce5d68144b2d7969333d9094708dbda133575522925939ac1a55b84872885295 2 64 64 64 64 +light_three_lights_colour_material f1ce7e0b2830c83e0a31aa8014c9257d3f2363b18a6dba3acd29276d179ab4a1 2 64 64 64 64 +light_unlit_emission_only 350095aadd0f5d0e44bd7605c3b23c1d2987e05e0f8ed7a554270dbba3ffee99 2 64 64 64 64 +lines_flat 3210a57425187cf4a7f807f110afde4656012fc97596bfeab8dc06df9a864cb0 2 64 64 64 64 +lines_smooth 2e9326a10d7818ffbe35dfe9214b6480d3b13c978b34de6de1305c518ca16f0e 2 64 64 64 64 +lines_smooth_colour 19c8179dda2fcffde4e09c6d4cffaa55d00c13bca67592d6112ab4b7f738a366 2 64 64 64 64 +lines_stippled 6b0c2e00eeab2acef311393e8d308626f42c5d9915e6e1396edf677edc9180f0 2 64 64 64 64 +lines_wide 9290e0c4dd03f0c06f49cedefca8d97ba58f1117c1e045cd147fdd9da7284e40 2 64 64 64 64 +logic_op_and_colour_mask fc721cfae3b96d59cb3945a30709a0ccc71632ac6d505d266829615f3206b99f 2 64 64 64 64 +perspective_correct_colour 3f7e9a56a6c8e80e63ac253b581b558909aeff2df2f226d7142038df27a6d55e 2 64 64 64 64 +points 686f8cece29a072fc93a06f29ee0fb3e1bec1298ca570d6306048e18fea9f6fc 2 64 64 64 64 +points_smooth 41af2a4af8f613a54691a25bc1f86111a8c6a37e40d7a2c3542642f22ffdadb4 2 64 64 64 64 +polygon_convex 35ef619a7c096c10b4412e497cc8bdf8aa0645390520c5039309c417f8737904 2 64 64 64 64 +polygon_modes_line_point b5b1b743cf62fc986fdefc37113e96444cc1da7f640399f1549e92406227b6f4 2 64 64 64 64 +polygon_offset b1fd796a9be5ed33d1e8d8a2ac4371ae4d5734129b9e2cad13c40b7dcc948e41 2 64 64 64 64 +polygon_smooth 25ef97fcca71ff8b4f2c4a92f27fd1269623a4d06ce4145c255cc56decb7d402 2 64 64 64 64 +polygon_stipple 1b54d0f1a15a18d81600f740452265395f8efb23518f3c5ebc48ec13caa9dfbe 2 64 64 64 64 +projection_frustum 9afcce11d6353f458cec2ac79622a0bf74d0333cf2f0cbdc22e23082341fdc19 2 64 64 64 64 +projection_ortho_rotated b835e3a20668d3765d6254961a680930e54a532b64dcf9fa4f796b639374eb9f 2 64 64 64 64 +quad_shared_edges 1482f7bd18c14744afdb0eccf47a815e27ce3eb49a5764b7e9c25b70eb3ba633 2 64 64 64 64 +scissor e1488694685b5f947ded5afe23690281f90f474c0649d938d9367231ea1066dd 2 64 64 64 64 +stencil_depth_fail d8bb106eba4c9d3df4c94ca8b847e07d1641345550879e80c67f2141e1790c64 2 64 64 64 64 +stencil_mark_then_masked ce6e621a4b556971bed7364082a1f6b0477abfdebf776401e1ca57355f4b1acb 2 64 64 64 64 +stencil_ops 4ca5b4e8a332c806d146b71907b60aee98eb689ec1adce5088e760463dfd61e6 2 64 64 64 64 +tex_clamp_to_edge_linear 6e41c180c5026836b24731f82fde906b308b985b3b7e15e1818468c374de0b0f 2 64 64 64 64 +tex_detail_colour 0ace9125fcb63dfb623e5f29c77c2c2d372f3dbbfad23f22284a9f83f5fc8c74 2 64 64 64 64 +tex_detail_colour_modulate b61e01beb3b02cb3d00955e237dd9d667d9313df3e8890224dbdc3c6a57f826b 2 64 64 64 64 +tex_env_blend_luminance e1f92aabc4a5934d06c9557c22ebc61321f695e9fc361152be69a305315b0114 2 64 64 64 64 +tex_env_blend_rgba 5fcdf50ad87ba69c1543d40b149c19601c78f8ef4169b13e0afce40ab47cdb38 2 64 64 64 64 +tex_env_decal_rgb 63a45b0b81d1984b1c695b8f2e6c83b1f04138a9697b06ea99989dc130d884fb 2 64 64 64 64 +tex_env_decal_rgba 4d73c8a3ccbb1bf9cccfefa85169284105d85a54a77ada68bd5df6f8f9f0ccef 2 64 64 64 64 +tex_env_modulate_luminance_alpha c498fc502e4cfd698f78db4356eff3c42ed8a86b92bf53159eb8dab1c660138c 2 64 64 64 64 +tex_env_modulate_rgba 2a3bebc0140a9c79dff79fc1aa396620caab11ce3a7f0fcd5b323112c31382ca 2 64 64 64 64 +tex_env_replace_alpha 723dbf8ffa874682c9dc759bb2064e30e70615d83f586ac410d65d3bb03044bb 2 64 64 64 64 +tex_env_replace_rgba 26704907768873d8c98994b012b66cef1a3fe2df40b711bed4c4a17a161a10e5 2 64 64 64 64 +tex_format_alpha 8bc00176d0ce546f2d5affd8adb0bf86243597386336c4fde9113607235875a7 2 64 64 64 64 +tex_format_intensity 235669c79dacae181e4a19884f43e28a327c4e7905117f3072ff0f8f338a13ba 2 64 64 64 64 +tex_format_luminance adcb5092543c35d624edd21a5e7e5a8c30bf04520dbf879096f4433540f86be1 2 64 64 64 64 +tex_format_luminance_alpha 55ff38a81d1f071e5b34768d2babb7a05e3f33629322353f3c4ea5425b458c22 2 64 64 64 64 +tex_format_rgb f1acf23df5076312db9cf7599eca5fa04368e949e715ed734596e7f7dfee650a 2 64 64 64 64 +tex_format_rgba 1f7640b8142aec4b9f034a44a9bfa86c5013c5d16554511a563647599baf2844 2 64 64 64 64 +tex_mip_linear_linear f1257fb74478a423872a2d52b2b839ae7ab60970cce0e1c88e2ae36e692b5d07 2 64 64 64 64 +tex_mip_linear_nearest 50bc400e35721fb15df1b2ab4cbcdc259dac69eeef6ff585e2d84733d8560ff7 2 64 64 64 64 +tex_mip_nearest_linear 30bc74a1af2148802400db691ee22b8df1151131f0b299c7e6a37cae12a3ae1a 2 64 64 64 64 +tex_mip_nearest_nearest d2ed7534833a06876a87671f233a011c90dadf074218085986ab2fc1c862e1a4 2 64 64 64 64 +tex_nearest_repeat df0a0ac19bf915cf6f605a0495616978d41debf586cda3eca1d28dd87797fe58 2 64 64 64 64 +tex_with_fog_blend_depth 53e1424ee4cd80dfa938b68ff72eb027c79230e276f3bb163bfda2ee181a5f94 2 64 64 64 64 +tri_flat 052a24e2883e621c134ce55e271ad842a1b850578e2d0c1d1f08a17e12195803 2 64 64 64 64 +tri_smooth b76a93e02625bffbbea05e5df1539c68cf36073f9a76570b1a27a8121165899e 2 64 64 64 64 +tri_tiny_and_thin c47346afd3b60df8e63387a017b022dd4aeb4d4b1dbf872d7d53baadb6d79872 2 64 64 64 64 +two_sided_colours 41e79b4d8a5a54c3154d5f37c8fc3c9d74697f8d35050a2e174f95f14c651caf 2 64 64 64 64 +window_matrix_clip_and_lines 345b2992cb690d4d235afee1db21a8b5445b012a37f39fba592fff5ae8ef2c28 2 64 64 64 64 +window_matrix_rows_down c06fe04e47cd382d2482abc69d011e4b8194069561716b0ab8eb029b11655b73 2 64 64 64 64 +window_matrix_scaled 9419d585dd18d50c19922d5f0e3975cce01512080a94ca6c882e9ede28429fae 2 64 64 64 64 +window_matrix_triangles 86960a356fb8190cf4af0ec8bfb054a847cdc4f1ccf6d6afa3f350a41cbcf2da 2 64 64 64 64 diff --git a/src/dev/odyssey/testdata/golden/vertices.txt b/src/dev/odyssey/testdata/golden/vertices.txt new file mode 100644 index 000000000..14602a0af --- /dev/null +++ b/src/dev/odyssey/testdata/golden/vertices.txt @@ -0,0 +1,869 @@ +# VPro vertex-side goldens (src/dev/odyssey/golden/vertices.rs). +# name kind values, as the shortest decimal that reads back exactly: kind eye is the eye +# position, clip the clip coordinates, win the window x, y and depth in [0, 1], rgba the +# lit front colour. Limits for VPRO_GOLDEN=tolerance: +# tolerance eye 1e-5 clip 1e-5 win_xy 0.01 win_z 1e-5 rgba 0.004 +identity/0 eye 0e0 0e0 0e0 1e0 +identity/0 clip 0e0 0e0 0e0 1e0 +identity/0 win 6.4e1 6.4e1 5e-1 +identity/1 eye 1e0 1e0 1e0 1e0 +identity/1 clip 1e0 1e0 1e0 1e0 +identity/1 win 1.28e2 1.28e2 1e0 +identity/2 eye -1e0 5e-1 -2e0 1e0 +identity/2 clip -1e0 5e-1 -2e0 1e0 +identity/2 win 0e0 9.6e1 -5e-1 +identity/3 eye 3.0000001192092896e-1 -3.0000001192092896e-1 5e0 1e0 +identity/3 clip 3.0000001192092896e-1 -3.0000001192092896e-1 5e0 1e0 +identity/3 win 8.319921875e1 4.4796875e1 3e0 +identity/4 eye -8.444819450378418e-1 -1.1755660772323608e0 1.1033172607421875e0 1e0 +identity/4 clip -8.444819450378418e-1 -1.1755660772323608e0 1.1033172607421875e0 1e0 +identity/4 win 9.953125e0 -1.123828125e1 1.0516586303710938e0 +identity/5 eye -1.4692661762237549e0 2.4675474166870117e0 -1.8420186042785645e0 6.873982548713684e-1 +identity/5 clip -1.4692661762237549e0 2.4675474166870117e0 -1.8420186042785645e0 6.873982548713684e-1 +identity/5 win -7.2796875e1 2.9373828125e2 -8.398481607437134e-1 +identity/6 eye -1.4260280132293701e0 2.485703468322754e0 -3.8388168811798096e0 1e0 +identity/6 clip -1.4260280132293701e0 2.485703468322754e0 -3.8388168811798096e0 1e0 +identity/6 win -2.726953125e1 2.2308203125e2 -1.4194084405899048e0 +identity/7 eye -1.4485691785812378e0 5.255732536315918e-1 1.7684712409973145e0 1e0 +identity/7 clip -1.4485691785812378e0 5.255732536315918e-1 1.7684712409973145e0 1e0 +identity/7 win -2.87109375e1 9.76328125e1 1.3842356204986572e0 +identity/8 eye 2.1442747116088867e0 2.6038084030151367e0 2.8756237030029297e0 1e0 +identity/8 clip 2.1442747116088867e0 2.6038084030151367e0 2.8756237030029297e0 1e0 +identity/8 win 2.0123046875e2 2.30640625e2 1.9378118515014648e0 +identity/9 eye -2.94674015045166e0 -1.8373887538909912e0 -8.516864776611328e-1 1e0 +identity/9 clip -2.94674015045166e0 -1.8373887538909912e0 -8.516864776611328e-1 1e0 +identity/9 win -1.2459375e2 -5.359375e1 7.41567611694336e-2 +identity/10 eye -2.935354709625244e0 1.790482521057129e0 -3.4873361587524414e0 9.945236444473267e-1 +identity/10 clip -2.935354709625244e0 1.790482521057129e0 -3.4873361587524414e0 9.945236444473267e-1 +identity/10 win -1.248984375e2 1.7921875e2 -1.2532696723937988e0 +identity/11 eye -2.3967864513397217e0 -2.3199880123138428e0 -2.719097137451172e-1 1e0 +identity/11 clip -2.3967864513397217e0 -2.3199880123138428e0 -2.719097137451172e-1 1e0 +identity/11 win -8.939453125e1 -8.448046875e1 3.640451431274414e-1 +identity/12 eye 6.714825630187988e-1 1.762575626373291e0 -1.0426411628723145e0 1e0 +identity/12 clip 6.714825630187988e-1 1.762575626373291e0 -1.0426411628723145e0 1e0 +identity/12 win 1.0697265625e2 1.768046875e2 -2.1320581436157227e-2 +identity/13 eye 9.570157527923584e-1 -2.876133918762207e0 -3.825482130050659e0 1e0 +identity/13 clip 9.570157527923584e-1 -2.876133918762207e0 -3.825482130050659e0 1e0 +identity/13 win 1.2524609375e2 -1.2007421875e2 -1.4127410650253296e0 +identity/14 eye -2.468637466430664e0 -2.137317657470703e-1 -7.349433898925781e-1 1e0 +identity/14 clip -2.468637466430664e0 -2.137317657470703e-1 -7.349433898925781e-1 1e0 +identity/14 win -9.399609375e1 5.03203125e1 1.3252830505371094e-1 +identity/15 eye -9.146459102630615e-1 2.480156421661377e0 4.666614532470703e-2 5.456457138061523e-1 +identity/15 clip -9.146459102630615e-1 2.480156421661377e0 4.666614532470703e-2 5.456457138061523e-1 +identity/15 win -4.328125e1 3.5490234375e2 5.427623391151428e-1 +identity/16 eye 7.035422325134277e-1 1.1831026077270508e0 2.768284797668457e0 1e0 +identity/16 clip 7.035422325134277e-1 1.1831026077270508e0 2.768284797668457e0 1e0 +identity/16 win 1.090234375e2 1.3971484375e2 1.8841423988342285e0 +identity/17 eye 1.9378662109375e0 2.7254409790039063e0 -4.995843887329102e0 1e0 +identity/17 clip 1.9378662109375e0 2.7254409790039063e0 -4.995843887329102e0 1e0 +identity/17 win 1.880234375e2 2.3842578125e2 -1.9979219436645508e0 +identity/18 eye 2.9621806144714355e0 2.349513530731201e0 -5.273796558380127e0 1e0 +identity/18 clip 2.9621806144714355e0 2.349513530731201e0 -5.273796558380127e0 1e0 +identity/18 win 2.53578125e2 2.143671875e2 -2.1368982791900635e0 +identity/19 eye -3.95904541015625e-1 2.1086788177490234e0 1.2435517311096191e0 1e0 +identity/19 clip -3.95904541015625e-1 2.1086788177490234e0 1.2435517311096191e0 1e0 +identity/19 win 3.866015625e1 1.98953125e2 1.1217758655548096e0 +identity/20 eye 7.693195343017578e-2 1.2135505676269531e-1 -2.651926279067993e0 1.4196527004241943e0 +identity/20 clip 7.693195343017578e-2 1.2135505676269531e-1 -2.651926279067993e0 1.4196527004241943e0 +identity/20 win 6.746484375e1 6.946875e1 -4.340052604675293e-1 +identity/21 eye -2.6007933616638184e0 1.5055150985717773e0 7.488951683044434e-1 1e0 +identity/21 clip -2.6007933616638184e0 1.5055150985717773e0 7.488951683044434e-1 1e0 +identity/21 win -1.02453125e2 1.603515625e2 8.744475841522217e-1 +identity/22 eye -8.483996391296387e-1 -2.4047884941101074e0 -3.561929941177368e0 1e0 +identity/22 clip -8.483996391296387e-1 -2.4047884941101074e0 -3.561929941177368e0 1e0 +identity/22 win 9.69921875e0 -8.991015625e1 -1.280964970588684e0 +identity/23 eye 2.663618564605713e0 -2.887369394302368e0 -2.2882697582244873e0 1e0 +identity/23 clip 2.663618564605713e0 -2.887369394302368e0 -2.2882697582244873e0 1e0 +identity/23 win 2.3446875e2 -1.2079296875e2 -6.441348791122437e-1 +identity/24 eye 4.2499303817749023e-1 2.7970938682556152e0 2.7438201904296875e0 1e0 +identity/24 clip 4.2499303817749023e-1 2.7970938682556152e0 2.7438201904296875e0 1e0 +identity/24 win 9.119921875e1 2.4301171875e2 1.8719100952148438e0 +identity/25 eye -6.747696399688721e-1 -2.9331979751586914e0 -5.414085865020752e0 9.6866774559021e-1 +identity/25 clip -6.747696399688721e-1 -2.9331979751586914e0 -5.414085865020752e0 9.6866774559021e-1 +identity/25 win 1.94140625e1 -1.29796875e2 -2.2946043014526367e0 +identity/26 eye 1.857090950012207e0 2.032998561859131e0 -8.544602394104004e-1 1e0 +identity/26 clip 1.857090950012207e0 2.032998561859131e0 -8.544602394104004e-1 1e0 +identity/26 win 1.828515625e2 1.94109375e2 7.27698802947998e-2 +identity/27 eye -1.324293613433838e-1 7.567057609558105e-1 -2.71038818359375e0 1e0 +identity/27 clip -1.324293613433838e-1 7.567057609558105e-1 -2.71038818359375e0 1e0 +identity/27 win 5.55234375e1 1.1242578125e2 -8.55194091796875e-1 +identity/28 eye 1.3259358406066895e0 -1.4977810382843018e0 1.9540891647338867e0 1e0 +identity/28 clip 1.3259358406066895e0 -1.4977810382843018e0 1.9540891647338867e0 1e0 +identity/28 win 1.48859375e2 -3.1859375e1 1.4770445823669434e0 +identity/29 eye -1.6898351907730103e0 1.5140070915222168e0 -4.375731945037842e0 1e0 +identity/29 clip -1.6898351907730103e0 1.5140070915222168e0 -4.375731945037842e0 1e0 +identity/29 win -4.415234375e1 1.6089453125e2 -1.687865972518921e0 +identity/30 eye 1.93402099609375e0 -2.587156057357788e0 3.218860626220703e-1 1.3266997337341309e0 +identity/30 clip 1.93402099609375e0 -2.587156057357788e0 3.218860626220703e-1 1.3266997337341309e0 +identity/30 win 1.57296875e2 -6.08046875e1 6.21310830116272e-1 +identity/31 eye -1.5318844318389893e0 -2.3015475273132324e0 -3.8558685779571533e0 1e0 +identity/31 clip -1.5318844318389893e0 -2.3015475273132324e0 -3.8558685779571533e0 1e0 +identity/31 win -3.404296875e1 -8.330078125e1 -1.4279342889785767e0 +identity/32 eye 1.0982446670532227e0 -2.8169829845428467e0 -4.693785667419434e0 1e0 +identity/32 clip 1.0982446670532227e0 -2.8169829845428467e0 -4.693785667419434e0 1e0 +identity/32 win 1.3428515625e2 -1.162890625e2 -1.8468928337097168e0 +identity/33 eye -7.015681266784668e-1 1.6764545440673828e0 -8.5308837890625e-1 1e0 +identity/33 clip -7.015681266784668e-1 1.6764545440673828e0 -8.5308837890625e-1 1e0 +identity/33 win 1.909765625e1 1.7129296875e2 7.3455810546875e-2 +identity/34 eye -2.4207539558410645e0 -2.6146445274353027e0 -1.1310782432556152e0 1e0 +identity/34 clip -2.4207539558410645e0 -2.6146445274353027e0 -1.1310782432556152e0 1e0 +identity/34 win -9.09296875e1 -1.0333984375e2 -6.553912162780762e-2 +identity/35 eye 6.538419723510742e-1 2.2641525268554688e0 -2.213336229324341e0 1.5709975957870483e0 +identity/35 clip 6.538419723510742e-1 2.2641525268554688e0 -2.213336229324341e0 1.5709975957870483e0 +identity/35 win 9.06328125e1 1.56234375e2 -2.044365406036377e-1 +identity/36 eye -2.598539352416992e0 -2.9223251342773438e0 5.782499313354492e-1 1e0 +identity/36 clip -2.598539352416992e0 -2.9223251342773438e0 5.782499313354492e-1 1e0 +identity/36 win -1.0230859375e2 -1.2303125e2 7.891249656677246e-1 +identity/37 eye -2.815436840057373e0 2.54249906539917e0 3.7764358520507813e0 1e0 +identity/37 clip -2.815436840057373e0 2.54249906539917e0 3.7764358520507813e0 1e0 +identity/37 win -1.1619140625e2 2.2671875e2 2.3882179260253906e0 +identity/38 eye 1.0990290641784668e0 9.989614486694336e-1 2.2380542755126953e0 1e0 +identity/38 clip 1.0990290641784668e0 9.989614486694336e-1 2.2380542755126953e0 1e0 +identity/38 win 1.343359375e2 1.279296875e2 1.6190271377563477e0 +identity/39 eye -2.224998950958252e0 1.4580249786376953e-2 -5.528273582458496e0 1e0 +identity/39 clip -2.224998950958252e0 1.4580249786376953e-2 -5.528273582458496e0 1e0 +identity/39 win -7.840234375e1 6.49296875e1 -2.264136791229248e0 +ortho_rot/0 eye 6.99999988079071e-1 -4.000000059604645e-1 -6e0 1e0 +ortho_rot/0 clip 2.5e-1 1.0000000149011612e-1 -2.999999523162842e-1 1e0 +ortho_rot/0 win 3.85e2 2.84e2 3.500000238418579e-1 +ortho_rot/1 eye 8.999999761581421e-1 1.0000001192092896e0 -5e0 1e0 +ortho_rot/1 clip 3.499999940395355e-1 4.500000476837158e-1 -3.999999761581421e-1 1e0 +ortho_rot/1 win 4.15e2 3.68e2 3.0000001192092896e-1 +ortho_rot/2 eye -4.0000003576278687e-1 -6.000000238418579e-1 -8e0 1e0 +ortho_rot/2 clip -3.0000001192092896e-1 4.999999701976776e-2 -9.999996423721313e-2 1e0 +ortho_rot/2 win 2.2e2 2.72e2 4.5000001788139343e-1 +ortho_rot/3 eye 1.1200000047683716e0 -4.6000000834465027e-1 -1e0 1e0 +ortho_rot/3 clip 4.6000000834465027e-1 8.500000089406967e-2 -7.999999523162842e-1 1e0 +ortho_rot/3 win 4.48e2 2.803984375e2 1.0000002384185791e-1 +ortho_rot/4 eye 7.297540903091431e-1 -1.8471421003341675e0 -4.8966827392578125e0 1e0 +ortho_rot/4 clip 2.64877051115036e-1 -2.6178550720214844e-1 -4.1033169627189636e-1 1e0 +ortho_rot/4 win 3.894609375e2 1.9716796875e2 2.948341369628906e-1 +ortho_rot/5 eye -2.174762725830078e0 8.17518949508667e-1 -5.9664082527160645e0 6.873982548713684e-1 +ortho_rot/5 clip -1.1561211347579956e0 3.418594002723694e-1 -2.2017598152160645e-2 6.873982548713684e-1 +ortho_rot/5 win -1.9456640625e2 3.7935546875e2 4.839848279953003e-1 +ortho_rot/6 eye -1.9322445392608643e0 7.329460382461548e-1 -9.83881664276123e0 1e0 +ortho_rot/6 clip -1.06612229347229e0 3.8323652744293213e-1 8.3881676197052e-2 1e0 +ortho_rot/6 win -9.83984375e0 3.519765625e2 5.419408082962036e-1 +ortho_rot/7 eye -7.741993069648743e-1 -8.486828804016113e-1 -4.2315287590026855e0 1e0 +ortho_rot/7 clip -4.8709964752197266e-1 -1.2170717120170593e-2 -4.768470823764801e-1 1e0 +ortho_rot/7 win 1.638671875e2 2.57078125e2 2.6157647371292114e-1 +ortho_rot/8 eye 8.531346917152405e-1 2.969611406326294e0 -3.1243762969970703e0 1e0 +ortho_rot/8 clip 3.265673518180847e-1 9.424028396606445e-1 -5.875623226165771e-1 1e0 +ortho_rot/8 win 4.0796875e2 4.8617578125e2 2.0621883869171143e-1 +ortho_rot/9 eye -5.549587607383728e-1 -3.6379551887512207e0 -6.851686477661133e0 1e0 +ortho_rot/9 clip -3.774793744087219e-1 -7.094888091087341e-1 -2.1483129262924194e-1 1e0 +ortho_rot/9 win 1.9675390625e2 8.972265625e1 3.9258435368537903e-1 +ortho_rot/10 eye -2.7264068126678467e0 -7.266364097595215e-1 -9.45447826385498e0 9.945236444473267e-1 +ortho_rot/10 clip -1.462655782699585e0 1.7245635390281677e-2 5.037659406661987e-2 9.945236444473267e-1 +ortho_rot/10 win -1.3121484375e2 2.6416015625e2 5.253269672393799e-1 +ortho_rot/11 eye 1.7456358671188354e-1 -3.6940624713897705e0 -6.271909713745117e0 1e0 +ortho_rot/11 clip -1.2718208134174347e-2 -7.235156297683716e-1 -2.728089690208435e-1 1e0 +ortho_rot/11 win 3.0618359375e2 8.635546875e1 3.6359551548957825e-1 +ortho_rot/12 eye 1.7964065074920654e-1 1.4129501581192017e0 -7.0426411628723145e0 1e0 +ortho_rot/12 clip -1.0179676115512848e-2 5.532375574111938e-1 -1.957358717918396e-1 1e0 +ortho_rot/12 win 3.069453125e2 3.927734375e2 4.021320641040802e-1 +ortho_rot/13 eye 3.1912930011749268e0 -2.1266977787017822e0 -9.825482368469238e0 1e0 +ortho_rot/13 clip 1.4956464767456055e0 -3.316744565963745e-1 8.254826068878174e-2 1e0 +ortho_rot/13 win 7.5869140625e2 1.8039453125e2 5.412741303443909e-1 +ortho_rot/14 eye -1.1466710567474365e0 -2.052168130874634e0 -6.734943389892578e0 1e0 +ortho_rot/14 clip -6.733355522155762e-1 -3.130420446395874e-1 -2.2650563716888428e-1 1e0 +ortho_rot/14 win 1.0799609375e2 1.848671875e2 3.8674718141555786e-1 +ortho_rot/15 eye -1.8378584384918213e0 1.2170792818069458e0 -3.227208137512207e0 5.456457138061523e-1 +ortho_rot/15 clip -9.734938144683838e-1 4.1339898109436035e-1 -1.6836029291152954e-1 5.456457138061523e-1 +ortho_rot/15 win -2.25234375e2 4.41828125e2 3.4572380781173706e-1 +ortho_rot/16 eye 5.529721975326538e-1 9.686075448989868e-1 -3.231715202331543e0 1e0 +ortho_rot/16 clip 1.7648610472679138e-1 4.4215190410614014e-1 -5.768284797668457e-1 1e0 +ortho_rot/16 win 3.629453125e2 3.6611328125e2 2.1158576011657715e-1 +ortho_rot/17 eye 6.150283217430115e-1 2.943072557449341e0 -1.0995843887329102e1 1e0 +ortho_rot/17 clip 2.0751416683197021e-1 9.357681274414063e-1 1.995844841003418e-1 1e0 +ortho_rot/17 win 3.7225390625e2 4.8458203125e2 5.997922420501709e-1 +ortho_rot/18 eye 1.660036325454712e0 3.2569193840026855e0 -1.1273796081542969e1 1e0 +ortho_rot/18 clip 7.30018138885498e-1 1.0142298936843872e0 2.273796796798706e-1 1e0 +ortho_rot/18 win 5.2900390625e2 5.034140625e2 6.136898398399353e-1 +ortho_rot/19 eye -8.819308876991272e-1 1.0494003295898438e0 -4.756448268890381e0 1e0 +ortho_rot/19 clip -5.409654378890991e-1 4.62350070476532e-1 -4.24355149269104e-1 1e0 +ortho_rot/19 win 1.4770703125e2 3.709609375e2 2.87822425365448e-1 +ortho_rot/20 eye 9.824894070625305e-1 -4.2461785674095154e-1 -1.1169841766357422e1 1.4196527004241943e0 +ortho_rot/20 clip 3.492794334888458e-1 1.7777606844902039e-1 -1.6070318222045898e-1 1.4196527004241943e0 +ortho_rot/20 win 3.8380859375e2 2.9005078125e2 4.434005320072174e-1 +ortho_rot/21 eye -2.2839438915252686e0 -7.560639381408691e-1 -5.251104831695557e0 1e0 +ortho_rot/21 clip -1.2419719696044922e0 1.0984018445014954e-2 -3.748894929885864e-1 1e0 +ortho_rot/21 win -6.259375e1 2.626328125e2 3.125552535057068e-1 +ortho_rot/22 eye 1.4641534090042114e0 -2.8328707218170166e0 -9.561929702758789e0 1e0 +ortho_rot/22 clip 6.320766806602478e-1 -5.082176923751831e-1 5.6192994117736816e-2 1e0 +ortho_rot/22 win 4.9962109375e2 1.3802734375e2 5.280964970588684e-1 +ortho_rot/23 eye 4.563316345214844e0 -1.1117242574691772e0 -8.288269996643066e0 1e0 +ortho_rot/23 clip 2.1816582679748535e0 -7.793106138706207e-2 -7.117295265197754e-2 1e0 +ortho_rot/23 win 9.6449609375e2 2.4129296875e2 4.6441352367401123e-1 +ortho_rot/24 eye -6.382619738578796e-1 2.0926709175109863e0 -3.2561798095703125e0 1e0 +ortho_rot/24 clip -4.1913098096847534e-1 7.231677174568176e-1 -5.74381947517395e-1 1e0 +ortho_rot/24 win 1.842578125e2 4.3355859375e2 2.128090262413025e-1 +ortho_rot/25 eye 1.8981704711914063e0 -3.1388871669769287e0 -1.1226092338562012e1 9.6866774559021e-1 +ortho_rot/25 clip 8.522184491157532e-1 -5.909882187843323e-1 2.508082985877991e-1 9.6866774559021e-1 +ortho_rot/25 win 5.7393359375e2 1.1357421875e2 6.294604539871216e-1 +ortho_rot/26 eye 9.65873658657074e-1 2.340653419494629e0 -6.8544602394104e0 1e0 +ortho_rot/26 clip 3.8293683528900146e-1 7.851633429527283e-1 -2.1455395221710205e-1 1e0 +ortho_rot/26 win 4.2487890625e2 4.484375e2 3.92723023891449e-1 +ortho_rot/27 eye 1.4003300666809082e-1 1.2590697407722473e-1 -8.71038818359375e0 1e0 +ortho_rot/27 clip -2.998349815607071e-2 2.3147675395011902e-1 -2.8961122035980225e-2 1e0 +ortho_rot/27 win 3.0100390625e2 3.1555078125e2 4.855194389820099e-1 +ortho_rot/28 eye 2.6594173908233643e0 -8.026633262634277e-1 -4.045910835266113e0 1e0 +ortho_rot/28 clip 1.2297086715698242e0 -6.658285856246948e-4 -4.9540889263153076e-1 1e0 +ortho_rot/28 win 6.7891015625e2 2.5983984375e2 2.522955536842346e-1 +ortho_rot/29 eye -1.560272455215454e0 -2.0269539952278137e-1 -1.0375732421875e1 1e0 +ortho_rot/29 clip -8.80136251449585e-1 1.493261456489563e-1 1.375732421875e-1 1e0 +ortho_rot/29 win 4.595703125e1 2.958359375e2 5.6878662109375e-1 +ortho_rot/30 eye 4.028200149536133e0 -1.4399919509887695e0 -7.638312339782715e0 1.3266997337341309e0 +ortho_rot/30 clip 1.8814301490783691e0 -9.465804696083069e-2 -4.3019843101501465e-1 1.3266997337341309e0 +ortho_rot/30 win 7.354375e2 2.42875e2 3.3786892890930176e-1 +ortho_rot/31 eye 8.554208874702454e-1 -3.1603689193725586e0 -9.855868339538574e0 1e0 +ortho_rot/31 clip 3.2771044969558716e-1 -5.900922417640686e-1 8.558684587478638e-2 1e0 +ortho_rot/31 win 4.083125e2 1.18375e2 5.427933931350708e-1 +ortho_rot/32 eye 3.2687857151031494e0 -1.9946397542953491e0 -1.0693785667419434e1 1e0 +ortho_rot/32 clip 1.5343928337097168e0 -2.9865992069244385e-1 1.693786382675171e-1 1e0 +ortho_rot/32 win 7.7031640625e2 1.883203125e2 5.846893191337585e-1 +ortho_rot/33 eye -8.671272397041321e-1 5.202227830886841e-1 -6.85308837890625e0 1e0 +ortho_rot/33 clip -5.335636138916016e-1 3.3005571365356445e-1 -2.1469110250473022e-1 1e0 +ortho_rot/33 win 1.499296875e2 3.392109375e2 3.926544487476349e-1 +ortho_rot/34 eye 3.321835398674011e-1 -3.9441680908203125e0 -7.131078243255615e0 1e0 +ortho_rot/34 clip 6.609176844358444e-2 -7.860420346260071e-1 -1.8689215183258057e-1 1e0 +ortho_rot/34 win 3.2982421875e2 7.134765625e1 4.065539240837097e-1 +ortho_rot/35 eye 2.6428037881851196e-1 1.575228214263916e0 -1.1639321327209473e1 1.5709975957870483e0 +ortho_rot/35 clip -2.495957911014557e-2 7.080066204071045e-1 -2.4996554851531982e-1 1.5709975957870483e0 +ortho_rot/35 win 3.0523046875e2 3.6816015625e2 4.2044368386268616e-1 +ortho_rot/36 eye 3.745637536048889e-1 -4.29698371887207e0 -5.421750068664551e0 1e0 +ortho_rot/36 clip 8.728187531232834e-2 -8.742459416389465e-1 -3.5782498121261597e-1 1e0 +ortho_rot/36 win 3.3618359375e2 5.01796875e1 3.21087509393692e-1 +ortho_rot/37 eye -3.0778491497039795e0 -5.526295304298401e-2 -2.2235641479492188e0 1e0 +ortho_rot/37 clip -1.6389245986938477e0 1.8618425726890564e-1 -6.776435375213623e-1 1e0 +ortho_rot/37 win -1.816796875e2 3.0468359375e2 1.6117823123931885e-1 +ortho_rot/38 eye 9.798463582992554e-1 1.0585867166519165e0 -3.7619457244873047e0 1e0 +ortho_rot/38 clip 3.8992318511009216e-1 4.6464669704437256e-1 -5.238053798675537e-1 1e0 +ortho_rot/38 win 4.269765625e2 3.7151171875e2 2.3809731006622314e-1 +ortho_rot/39 eye -1.088747262954712e0 -1.7233352661132813e0 -1.1528273582458496e1 1e0 +ortho_rot/39 clip -6.443736553192139e-1 -2.3083381354808807e-1 2.5282740592956543e-1 1e0 +ortho_rot/39 win 1.166875e2 2.0459765625e2 6.264137029647827e-1 +frustum_rot/0 eye 6.99999988079071e-1 -4.000000059604645e-1 -6e0 1e0 +frustum_rot/0 clip 2.5999999046325684e0 -8.00000011920929e-1 4.344828128814697e0 6e0 +frustum_rot/0 win 9.1733203125e2 4.4373046875e2 8.620690107345581e-1 +frustum_rot/1 eye 8.999999761581421e-1 1.0000001192092896e0 -5e0 1e0 +frustum_rot/1 clip 2.8000001907348633e0 2.000000238418579e0 3.275862216949463e0 5e0 +frustum_rot/1 win 9.983984375e2 7.16796875e2 8.275862336158752e-1 +frustum_rot/2 eye -4.0000003576278687e-1 -6.000000238418579e-1 -8e0 1e0 +frustum_rot/2 clip 8.000000715255737e-1 -1.2000000476837158e0 6.48275899887085e0 8e0 +frustum_rot/2 win 7.04e2 4.3519921875e2 9.051724672317505e-1 +frustum_rot/3 eye 1.1200000047683716e0 -4.6000000834465027e-1 -1e0 1e0 +frustum_rot/3 clip 2.440000057220459e0 -9.200000166893005e-1 -9.999998807907104e-1 1e0 +frustum_rot/3 win 2.20159765625e3 4.095703125e1 5.960464477539063e-8 +frustum_rot/4 eye 7.297540903091431e-1 -1.8471421003341675e0 -4.8966827392578125e0 1e0 +frustum_rot/4 clip 2.438844680786133e0 -3.694284200668335e0 3.165419578552246e0 4.8966827392578125e0 +frustum_rot/4 win 9.587578125e2 1.2572265625e2 8.232208490371704e-1 +frustum_rot/5 eye -2.174762725830078e0 8.17518949508667e-1 -5.9664082527160645e0 6.873982548713684e-1 +frustum_rot/5 clip -3.1562438011169434e0 1.635037899017334e0 4.955681800842285e0 5.9664082527160645e0 +frustum_rot/5 win 3.014375e2 6.5230859375e2 9.15298581123352e-1 +frustum_rot/6 eye -1.9322445392608643e0 7.329460382461548e-1 -9.83881664276123e0 1e0 +frustum_rot/6 clip -1.8967255353927612e0 1.4658920764923096e0 8.44839096069336e0 9.83881664276123e0 +frustum_rot/6 win 5.166171875e2 5.8828125e2 9.293397665023804e-1 +frustum_rot/7 eye -7.741993069648743e-1 -8.486828804016113e-1 -4.2315287590026855e0 1e0 +frustum_rot/7 clip -7.020927667617798e-1 -1.6973657608032227e0 2.454392910003662e0 4.2315287590026855e0 +frustum_rot/7 win 5.3380859375e2 3.0662109375e2 7.90012538433075e-1 +frustum_rot/8 eye 8.531346917152405e-1 2.969611406326294e0 -3.1243762969970703e0 1e0 +frustum_rot/8 clip 2.3311448097229004e0 5.939222812652588e0 1.2708852291107178e0 3.1243762969970703e0 +frustum_rot/8 win 1.11751171875e3 1.4852734375e3 7.033822536468506e-1 +frustum_rot/9 eye -5.549587607383728e-1 -3.6379551887512207e0 -6.851686477661133e0 1e0 +frustum_rot/9 clip 2.604198455810547e-1 -7.275910377502441e0 5.255251407623291e0 6.851686477661133e0 +frustum_rot/9 win 6.6432421875e2 -3.1703125e1 8.835005760192871e-1 +frustum_rot/10 eye -2.7264068126678467e0 -7.266364097595215e-1 -9.45447826385498e0 9.945236444473267e-1 +frustum_rot/10 clip -3.561917781829834e0 -1.453272819519043e0 8.048876762390137e0 9.45447826385498e0 +frustum_rot/10 win 3.988828125e2 4.33296875e2 9.256647825241089e-1 +frustum_rot/11 eye 1.7456358671188354e-1 -3.6940624713897705e0 -6.271909713745117e0 1e0 +frustum_rot/11 clip 1.6035091876983643e0 -7.388124942779541e0 4.6354899406433105e0 6.271909713745117e0 +frustum_rot/11 win 8.03625e2 -9.112109375e1 8.69543731212616e-1 +frustum_rot/12 eye 1.7964065074920654e-1 1.4129501581192017e0 -7.0426411628723145e0 1e0 +frustum_rot/12 clip 1.7678096294403076e0 2.8259003162384033e0 5.459375381469727e0 7.0426411628723145e0 +frustum_rot/12 win 8.006484375e2 7.1744140625e2 8.875943422317505e-1 +frustum_rot/13 eye 3.1912930011749268e0 -2.1266977787017822e0 -9.825482368469238e0 1e0 +frustum_rot/13 clip 8.34768295288086e0 -4.2533955574035645e0 8.434137344360352e0 9.825482368469238e0 +frustum_rot/13 win 1.18373828125e3 2.9035546875e2 9.291971325874329e-1 +frustum_rot/14 eye -1.1466710567474365e0 -2.052168130874634e0 -6.734943389892578e0 1e0 +frustum_rot/14 clip -9.463533163070679e-1 -4.104336261749268e0 5.130456924438477e0 6.734943389892578e0 +frustum_rot/14 win 5.500703125e2 1.9998046875e2 8.808834552764893e-1 +frustum_rot/15 eye -1.8378584384918213e0 1.2170792818069458e0 -3.227208137512207e0 5.456457138061523e-1 +frustum_rot/15 clip -3.0302751064300537e0 2.4341585636138916e0 2.320852279663086e0 3.227208137512207e0 +frustum_rot/15 win 3.905078125e1 8.981796875e2 8.595758676528931e-1 +frustum_rot/16 eye 5.529721975326538e-1 9.686075448989868e-1 -3.231715202331543e0 1e0 +frustum_rot/16 clip 1.75228750705719e0 1.9372150897979736e0 1.3856267929077148e0 3.231715202331543e0 +frustum_rot/16 win 9.87015625e2 8.1891015625e2 7.143794894218445e-1 +frustum_rot/17 eye 6.150283217430115e-1 2.943072557449341e0 -1.0995843887329102e1 1e0 +frustum_rot/17 clip 3.429225444793701e0 5.886145114898682e0 9.685213088989258e0 1.0995843887329102e1 +frustum_rot/17 win 8.3959375e2 7.8607421875e2 9.404033422470093e-1 +frustum_rot/18 eye 1.660036325454712e0 3.2569193840026855e0 -1.1273796081542969e1 1e0 +frustum_rot/18 clip 5.574831962585449e0 6.513838768005371e0 9.98233413696289e0 1.1273796081542969e1 +frustum_rot/18 win 9.564765625e2 8.0782421875e2 9.427228569984436e-1 +frustum_rot/19 eye -8.819308876991272e-1 1.0494003295898438e0 -4.756448268890381e0 1e0 +frustum_rot/19 clip -8.125720620155334e-1 2.0988006591796875e0 3.0155138969421387e0 4.756448268890381e0 +frustum_rot/19 win 5.306640625e2 7.37921875e2 8.169921636581421e-1 +frustum_rot/20 eye 9.824894070625305e-1 -4.2461785674095154e-1 -1.1169841766357422e1 1.4196527004241943e0 +frustum_rot/20 clip 4.198947429656982e0 -8.492357134819031e-1 9.00296401977539e0 1.1169841766357422e1 +frustum_rot/20 win 8.805859375e2 4.730703125e2 9.030032157897949e-1 +frustum_rot/21 eye -2.2839438915252686e0 -7.560639381408691e-1 -5.251104831695557e0 1e0 +frustum_rot/21 clip -3.517666816711426e0 -1.5121278762817383e0 3.5442848205566406e0 5.251104831695557e0 +frustum_rot/21 win 2.1126953125e2 3.645625e2 8.374799489974976e-1 +frustum_rot/22 eye 1.4641534090042114e0 -2.8328707218170166e0 -9.561929702758789e0 1e0 +frustum_rot/22 clip 4.84069299697876e0 -5.665741443634033e0 8.152408599853516e0 9.561929702758789e0 +frustum_rot/22 win 9.6399609375e2 2.0862109375e2 9.262951612472534e-1 +frustum_rot/23 eye 4.563316345214844e0 -1.1117242574691772e0 -8.288269996643066e0 1e0 +frustum_rot/23 clip 1.0784286499023438e1 -2.2234485149383545e0 6.790909290313721e0 8.288269996643066e0 +frustum_rot/23 win 1.472734375e3 3.746484375e2 9.096698760986328e-1 +frustum_rot/24 eye -6.382619738578796e-1 2.0926709175109863e0 -3.2561798095703125e0 1e0 +frustum_rot/24 clip -6.252879500389099e-1 4.185341835021973e0 1.4117786884307861e0 3.2561798095703125e0 +frustum_rot/24 win 5.1709765625e2 1.17009765625e3 7.167844772338867e-1 +frustum_rot/25 eye 1.8981704711914063e0 -3.1388871669769287e0 -1.1226092338562012e1 9.6866774559021e-1 +frustum_rot/25 clip 6.04155969619751e0 -6.277774333953857e0 9.996166229248047e0 1.1226092338562012e1 +frustum_rot/25 win 9.8442578125e2 2.256796875e2 9.452201724052429e-1 +frustum_rot/26 eye 9.65873658657074e-1 2.340653419494629e0 -6.8544602394104e0 1e0 +frustum_rot/26 clip 3.3026394844055176e0 4.681306838989258e0 5.258216381072998e0 6.8544602394104e0 +frustum_rot/26 win 9.4836328125e2 8.61671875e2 8.835616707801819e-1 +frustum_rot/27 eye 1.4003300666809082e-1 1.2590697407722473e-1 -8.71038818359375e0 1e0 +frustum_rot/27 clip 2.022143840789795e0 2.5181394815444946e-1 7.2421393394470215e0 8.71038818359375e0 +frustum_rot/27 win 7.8857421875e2 5.2680078125e2 9.157185554504395e-1 +frustum_rot/28 eye 2.6594173908233643e0 -8.026633262634277e-1 -4.045910835266113e0 1e0 +frustum_rot/28 clip 6.128016948699951e0 -1.6053266525268555e0 2.2559738159179688e0 4.045910835266113e0 +frustum_rot/28 win 1.60935546875e3 3.0884765625e2 7.787967920303345e-1 +frustum_rot/29 eye -1.560272455215454e0 -2.0269539952278137e-1 -1.0375732421875e1 1e0 +frustum_rot/29 clip -1.045398235321045e0 -4.0539079904556274e-1 9.022335052490234e0 1.0375732421875e1 +frustum_rot/29 win 5.75515625e2 4.919921875e2 9.347805976867676e-1 +frustum_rot/30 eye 4.028200149536133e0 -1.4399919509887695e0 -7.638312339782715e0 1.3266997337341309e0 +frustum_rot/30 clip 9.584062576293945e0 -2.879983901977539e0 5.420196533203125e0 7.638312339782715e0 +frustum_rot/30 win 1.44302734375e3 3.18953125e2 8.548032641410828e-1 +frustum_rot/31 eye 8.554208874702454e-1 -3.1603689193725586e0 -9.855868339538574e0 1e0 +frustum_rot/31 clip 3.6820156574249268e0 -6.320737838745117e0 8.466617584228516e0 9.855868339538574e0 +frustum_rot/31 win 8.7909375e2 1.8364453125e2 9.295216798782349e-1 +frustum_rot/32 eye 3.2687857151031494e0 -1.9946397542953491e0 -1.0693785667419434e1 1e0 +frustum_rot/32 clip 8.676328659057617e0 -3.9892795085906982e0 9.362323760986328e0 1.0693785667419434e1 +frustum_rot/32 win 1.1592578125e3 3.21e2 9.37745988368988e-1 +frustum_rot/33 eye -8.671272397041321e-1 5.202227830886841e-1 -6.85308837890625e0 1e0 +frustum_rot/33 clip -3.6363673210144043e-1 1.0404455661773682e0 5.256750106811523e0 6.85308837890625e0 +frustum_rot/33 win 6.060390625e2 5.8973046875e2 8.835314512252808e-1 +frustum_rot/34 eye 3.321835398674011e-1 -3.9441680908203125e0 -7.131078243255615e0 1e0 +frustum_rot/34 clip 2.090582847595215e0 -7.888336181640625e0 5.5539116859436035e0 7.131078243255615e0 +frustum_rot/34 win 8.27625e2 -5.437109375e1 8.89415979385376e-1 +frustum_rot/35 eye 2.6428037881851196e-1 1.575228214263916e0 -1.1639321327209473e1 1.5709975957870483e0 +frustum_rot/35 clip 2.8564252853393555e0 3.150456428527832e0 9.191694259643555e0 1.1639321327209473e1 +frustum_rot/35 win 7.970625e2 6.5058203125e2 8.94855260848999e-1 +frustum_rot/36 eye 3.745637536048889e-1 -4.29698371887207e0 -5.421750068664551e0 1e0 +frustum_rot/36 clip 1.8334776163101196e0 -8.59396743774414e0 3.726698398590088e0 5.421750068664551e0 +frustum_rot/36 win 8.5642578125e2 -2.995703125e2 8.436803817749023e-1 +frustum_rot/37 eye -3.0778491497039795e0 -5.526295304298401e-2 -2.2235641479492188e0 1e0 +frustum_rot/37 clip -5.71098518371582e0 -1.1052590608596802e-1 3.0794811248779297e-1 2.2235641479492188e0 +frustum_rot/37 win -1.0037734375e3 4.86546875e2 5.692465305328369e-1 +frustum_rot/38 eye 9.798463582992554e-1 1.0585867166519165e0 -3.7619457244873047e0 1e0 +frustum_rot/38 clip 2.7120819091796875e0 2.117173433303833e0 1.9524250030517578e0 3.7619457244873047e0 +frustum_rot/38 win 1.101390625e3 8.0014453125e2 7.594966888427734e-1 +frustum_rot/39 eye -1.088747262954712e0 -1.7233352661132813e0 -1.1528273582458496e1 1e0 +frustum_rot/39 clip 1.281604766845703e-1 -3.4466705322265625e0 1.0254362106323242e1 1.1528273582458496e1 +frustum_rot/39 win 6.4711328125e2 3.58921875e2 9.447484016418457e-1 +frustum_tilt_vp/0 eye 0e0 3.0000001192092896e-1 -9e0 1e0 +frustum_tilt_vp/0 clip 1.8000001907348633e0 6.000000238418579e-1 7.551724910736084e0 9e0 +frustum_tilt_vp/0 win 1.69796875e2 1.5206640625e2 9.19540286064148e-1 +frustum_tilt_vp/1 eye 1.4000000953674316e0 1.2999999523162842e0 -8.800000190734863e0 1e0 +frustum_tilt_vp/1 clip 4.560000419616699e0 2.5999999046325684e0 7.337932109832764e0 8.800000190734863e0 +frustum_tilt_vp/1 win 2.227734375e2 1.8146484375e2 9.169279336929321e-1 +frustum_tilt_vp/2 eye -2e0 8.00000011920929e-1 -1e1 1e0 +frustum_tilt_vp/2 clip -1.999999761581421e0 1.600000023841858e0 8.620689392089844e0 1e1 +frustum_tilt_vp/2 win 1.0319921875e2 1.6405859375e2 9.310344457626343e-1 +frustum_tilt_vp/3 eye 3.240000009536743e0 0e0 -5.180000305175781e0 1e0 +frustum_tilt_vp/3 clip 7.516000270843506e0 0e0 3.4682765007019043e0 5.180000305175781e0 +frustum_tilt_vp/3 win 3.7808203125e2 1.435e2 8.347756862640381e-1 +frustum_tilt_vp/4 eye -1.3595163822174072e-2 -8.755660653114319e-1 -7.61065673828125e0 1e0 +frustum_tilt_vp/4 clip 1.4949411153793335e0 -1.7511321306228638e0 6.066564083099365e0 7.61065673828125e0 +frustum_tilt_vp/4 win 1.69203125e2 1.139296875e2 8.98557186126709e-1 +frustum_tilt_vp/5 eye -2.2806243896484375e0 2.673766851425171e0 -6.778639793395996e0 6.873982548713684e-1 +frustum_tilt_vp/5 clip -3.2055206298828125e0 5.347533702850342e0 5.823929309844971e0 6.778639793395996e0 +frustum_tilt_vp/5 win 5.776171875e1 2.4487109375e2 9.29579496383667e-1 +frustum_tilt_vp/6 eye -3.444112539291382e0 2.785703420639038e0 -1.1215436935424805e1 1e0 +frustum_tilt_vp/6 clip -4.645137786865234e0 5.571406841278076e0 9.919950485229492e0 1.1215436935424805e1 +frustum_tilt_vp/6 win 6.75390625e1 2.0733203125e2 9.42245364189148e-1 +frustum_tilt_vp/7 eye -9.777247905731201e-2 8.255732655525208e-1 -6.716081619262695e0 1e0 +frustum_tilt_vp/7 clip 1.1476714611053467e0 1.6511465311050415e0 5.110294342041016e0 6.716081619262695e0 +frustum_tilt_vp/7 win 1.6494921875e2 1.7508984375e2 8.804520964622498e-1 +frustum_tilt_vp/8 eye 3.440793991088867e0 2.903808355331421e0 -7.986065864562988e0 1e0 +frustum_tilt_vp/8 clip 8.478801727294922e0 5.807616710662842e0 6.4678635597229e0 7.986065864562988e0 +frustum_tilt_vp/8 win 3.1326953125e2 2.369453125e2 9.049468040466309e-1 +frustum_tilt_vp/9 eye -2.868403911590576e0 -1.537388801574707e0 -7.913305282592773e0 1e0 +frustum_tilt_vp/9 clip -4.154146671295166e0 -3.074777603149414e0 6.390085697174072e0 7.913305282592773e0 +frustum_tilt_vp/9 win 4.909375e1 9.356640625e1 9.03755784034729e-1 +frustum_tilt_vp/10 eye -4.440685272216797e0 2.088839530944824e0 -9.979369163513184e0 9.945236444473267e-1 +frustum_tilt_vp/10 clip -6.885496616363525e0 4.177679061889648e0 8.609966278076172e0 9.979369163513184e0 +frustum_tilt_vp/10 win 2.16171875e1 1.9729296875e2 9.31388258934021e-1 +frustum_tilt_vp/11 eye -2.0805749893188477e0 -2.0199880599975586e0 -7.779455661773682e0 1e0 +frustum_tilt_vp/11 clip -2.6052587032318115e0 -4.039976119995117e0 6.247004985809326e0 7.779455661773682e0 +frustum_tilt_vp/11 win 8.073828125e1 7.6765625e1 9.015065431594849e-1 +frustum_tilt_vp/12 eye -8.839863538742065e-2 2.062575578689575e0 -1.02370023727417e1 1e0 +frustum_tilt_vp/12 clip 1.8706034421920776e0 4.12515115737915e0 8.87403678894043e0 1.02370023727417e1 +frustum_tilt_vp/12 win 1.66921875e2 1.9527734375e2 9.33429479598999e-1 +frustum_tilt_vp/13 eye -1.5296766757965088e0 -2.576133966445923e0 -1.2634594917297363e1 1e0 +frustum_tilt_vp/13 clip -5.324342250823975e-1 -5.152267932891846e0 1.1436981201171875e1 1.2634594917297363e1 +frustum_tilt_vp/13 win 1.2948046875e2 9.109765625e1 9.526057839393616e-1 +frustum_tilt_vp/14 eye -2.4158761501312256e0 8.626824617385864e-2 -8.106772422790527e0 1e0 +frustum_tilt_vp/14 clip -3.210397720336914e0 1.7253649234771729e-1 6.59689474105835e0 8.106772422790527e0 +frustum_tilt_vp/14 win 7.05625e1 1.46234375e2 9.06875491142273e-1 +frustum_tilt_vp/15 eye -7.03717052936554e-1 2.643850088119507e0 -4.324690818786621e0 5.456457138061523e-1 +frustum_tilt_vp/15 clip -5.42495846748352e-1 5.287700176239014e0 3.494023323059082e0 4.324690818786621e0 +frustum_tilt_vp/15 win 1.1561328125e2 3.0061328125e2 9.03962254524231e-1 +frustum_tilt_vp/16 eye 2.2238047122955322e0 1.483102560043335e0 -7.207497596740723e0 1e0 +frustum_tilt_vp/16 clip 5.889109134674072e0 2.96620512008667e0 5.635601043701172e0 7.207497596740723e0 +frustum_tilt_vp/16 win 2.7254296875e2 1.963828125e2 8.909540772438049e-1 +frustum_tilt_vp/17 eye -1.4472134113311768e0 3.0254409313201904e0 -1.4159395217895508e1 1e0 +frustum_tilt_vp/17 clip -6.254744529724121e-2 6.050881862640381e0 1.3066940307617188e1 1.4159395217895508e1 +frustum_tilt_vp/17 win 1.3576171875e2 1.9841015625e2 9.614229798316956e-1 +frustum_tilt_vp/18 eye -7.945334911346436e-1 2.6495134830474854e0 -1.4996345520019531e1 1e0 +frustum_tilt_vp/18 clip 1.4102025032043457e0 5.299026966094971e0 1.3961610794067383e1 1.4996345520019531e1 +frustum_tilt_vp/18 win 1.5215625e2 1.8890234375e2 9.655004739761353e-1 +frustum_tilt_vp/19 eye 4.2940741777420044e-1 2.4086787700653076e0 -7.767615795135498e0 1e0 +frustum_tilt_vp/19 clip 2.4123382568359375e0 4.817357540130615e0 6.234347820281982e0 7.767615795135498e0 +frustum_tilt_vp/19 win 1.8820703125e2 2.2319140625e2 9.01303768157959e-1 +frustum_tilt_vp/20 eye -1.529610276222229e0 5.472508668899536e-1 -1.4944574356079102e1 1.4196527004241943e0 +frustum_tilt_vp/20 clip -7.030534744262695e-2 1.0945017337799072e0 1.3038022994995117e1 1.4944574356079102e1 +frustum_tilt_vp/20 win 1.3571484375e2 1.5291015625e2 9.362125992774963e-1 +frustum_tilt_vp/21 eye -1.6312977075576782e0 1.8055150508880615e0 -6.840407848358154e0 1e0 +frustum_tilt_vp/21 clip -1.894513726234436e0 3.611030101776123e0 5.243195056915283e0 6.840407848358154e0 +frustum_tilt_vp/21 win 9.03828125e1 2.1133203125e2 8.832516670227051e-1 +frustum_tilt_vp/22 eye -2.815877914428711e0 -2.1047885417938232e0 -1.1340503692626953e1 1e0 +frustum_tilt_vp/22 clip -3.363654851913452e0 -4.2095770835876465e0 1.0053642272949219e1 1.1340503692626953e1 +frustum_tilt_vp/22 win 8.711328125e1 9.580078125e1 9.432625770568848e-1 +frustum_tilt_vp/23 eye 7.579330205917358e-1 -2.587369441986084e0 -1.2428787231445313e1 1e0 +frustum_tilt_vp/23 clip 4.001623630523682e0 -5.174738883972168e0 1.121697998046875e1 1.2428787231445313e1 +frustum_tilt_vp/23 win 1.9010546875e2 8.999609375e1 9.512499570846558e-1 +frustum_tilt_vp/24 eye 1.9862866401672363e0 3.0970938205718994e0 -7.059939384460449e0 1e0 +frustum_tilt_vp/24 clip 5.384561538696289e0 6.194187641143799e0 5.4778666496276855e0 7.059939384460449e0 +frustum_tilt_vp/24 win 2.63484375e2 2.562421875e2 8.879542350769043e-1 +frustum_tilt_vp/25 eye -3.7882673740386963e0 -2.6425976753234863e0 -1.2644416809082031e1 9.6866774559021e-1 +frustum_tilt_vp/25 clip -5.047651290893555e0 -5.285195350646973e0 1.1512306213378906e1 1.2644416809082031e1 +frustum_tilt_vp/25 win 7.003125e1 8.978515625e1 9.552327990531921e-1 +frustum_tilt_vp/26 eye 9.729966521263123e-1 2.332998514175415e0 -1.0797822952270508e1 1e0 +frustum_tilt_vp/26 clip 4.105557918548584e0 4.66599702835083e0 9.473535537719727e0 1.0797822952270508e1 +frustum_tilt_vp/26 win 1.998046875e2 1.9902734375e2 9.386780261993408e-1 +frustum_tilt_vp/27 eye -1.732176423072815e0 1.0567057132720947e0 -1.1088852882385254e1 1e0 +frustum_tilt_vp/27 clip -1.24658203125e0 2.1134114265441895e0 9.784635543823242e0 1.1088852882385254e1 +frustum_tilt_vp/27 win 1.1778125e2 1.6798828125e2 9.411923885345459e-1 +frustum_tilt_vp/28 eye 2.2332022190093994e0 -1.1977810859680176e0 -8.232290267944336e0 1e0 +frustum_tilt_vp/28 clip 6.112862586975098e0 -2.395562171936035e0 6.731069087982178e0 8.232290267944336e0 +frustum_tilt_vp/28 win 2.601328125e2 1.0610546875e2 9.08821165561676e-1 +frustum_tilt_vp/29 eye -3.9773073196411133e0 1.814007043838501e0 -1.1486684799194336e1 1e0 +frustum_tilt_vp/29 clip -5.657277584075928e0 3.628014087677002e0 1.0209905624389648e1 1.1486684799194336e1 +frustum_tilt_vp/29 win 5.449609375e1 1.840859375e2 9.444235563278198e-1 +frustum_tilt_vp/30 eye 1.7403484582901e0 -2.189146041870117e0 -1.2843201637268066e1 1.3266997337341309e0 +frustum_tilt_vp/30 clip 6.049337387084961e0 -4.378292083740234e0 1.0984044075012207e1 1.2843201637268066e1 +frustum_tilt_vp/30 win 2.14921875e2 9.969140625e1 9.276209473609924e-1 +frustum_tilt_vp/31 eye -3.5390286445617676e0 -2.0015475749969482e0 -1.1165563583374023e1 1e0 +frustum_tilt_vp/31 clip -4.844944477081299e0 -4.0030951499938965e0 9.86663818359375e0 1.1165563583374023e1 +frustum_tilt_vp/31 win 6.425e1 9.74296875e1 9.418333768844604e-1 +frustum_tilt_vp/32 eye -1.9376757144927979e0 -2.5169830322265625e0 -1.341397476196289e1 1e0 +frustum_tilt_vp/32 clip -1.1925561428070068e0 -5.033966064453125e0 1.2270111083984375e1 1.341397476196289e1 +frustum_tilt_vp/32 win 1.216953125e2 9.52734375e1 9.573630094528198e-1 +frustum_tilt_vp/33 eye -1.0731074810028076e0 1.976454496383667e0 -9.261529922485352e0 1e0 +frustum_tilt_vp/33 clip -2.9390883445739746e-1 3.952908992767334e0 7.8312907218933105e0 9.261529922485352e0 +frustum_tilt_vp/33 win 1.3121484375e2 1.9834375e2 9.227859973907471e-1 +frustum_tilt_vp/34 eye -2.6152501106262207e0 -2.3146445751190186e0 -8.452409744262695e0 1e0 +frustum_tilt_vp/34 clip -3.540018081665039e0 -4.629289150238037e0 6.966369152069092e0 8.452409744262695e0 +frustum_tilt_vp/34 win 6.6765625e1 7.312109375e1 9.120936393737793e-1 +frustum_tilt_vp/35 eye -8.049281239509583e-1 2.7354516983032227e0 -1.6301952362060547e1 1.5709975957870483e0 +frustum_tilt_vp/35 clip 1.6505345106124878e0 5.470903396606445e0 1.4175886154174805e1 1.6301952362060547e1 +frustum_tilt_vp/35 win 1.5335546875e2 1.8662109375e2 9.34791088104248e-1 +frustum_tilt_vp/36 eye -1.7318814992904663e0 -2.6223251819610596e0 -6.978276252746582e0 1e0 +frustum_tilt_vp/36 clip -2.0681076049804688e0 -5.244650363922119e0 5.390571594238281e0 6.978276252746582e0 +frustum_tilt_vp/36 win 8.715234375e1 4.6921875e1 8.862394690513611e-1 +frustum_tilt_vp/37 eye 1.3511896133422852e-2 2.842499017715454e0 -4.289588928222656e0 1e0 +frustum_tilt_vp/37 clip 8.849416375160217e-1 5.684998035430908e0 2.5164575576782227e0 4.289588928222656e0 +frustum_tilt_vp/37 win 1.7084765625e2 3.1380078125e2 7.933214902877808e-1 +frustum_tilt_vp/38 eye 2.2220559120178223e0 1.2989614009857178e0 -7.868973731994629e0 1e0 +frustum_tilt_vp/38 clip 6.017906665802002e0 2.5979228019714355e0 6.342696666717529e0 7.868973731994629e0 +frustum_tilt_vp/38 win 2.6383203125e2 1.85921875e2 9.030193090438843e-1 +frustum_tilt_vp/39 eye -5.096963405609131e0 3.145802617073059e-1 -1.208761978149414e1 1e0 +frustum_tilt_vp/39 clip -7.776402473449707e0 6.291605234146118e-1 1.0852283477783203e1 1.208761978149414e1 +frustum_tilt_vp/39 win 2.93828125e1 1.501875e2 9.489007592201233e-1 +window_matrix/0 eye 0e0 0e0 0e0 1e0 +window_matrix/0 clip 2.4444580078125e-2 1.83258056640625e-2 -1e0 1e0 +window_matrix/0 win 4.005e2 3.0025e2 0e0 +window_matrix/1 eye 1e0 1e0 1e0 1e0 +window_matrix/1 clip 2.5177001953125e-2 2.34527587890625e-2 5e-1 1e0 +window_matrix/1 win 4.125e2 3.8425e2 7.5e-1 +window_matrix/2 eye -1e0 5e-1 -2e0 1e0 +window_matrix/2 clip 2.0416259765625e-2 1.75933837890625e-2 -4e0 1e0 +window_matrix/2 win 3.345e2 2.8825e2 -1.5e0 +window_matrix/3 eye 3.0000001192092896e-1 -3.0000001192092896e-1 5e0 1e0 +window_matrix/3 clip 2.598266676068306e-2 1.810607872903347e-2 6.5e0 1e0 +window_matrix/3 win 4.2569921875e2 2.966484375e2 3.75e0 +window_matrix/4 eye -8.444819450378418e-1 -1.1755660772323608e0 1.1033172607421875e0 1e0 +window_matrix/4 clip 2.4553542956709862e-2 1.3026213273406029e-2 6.549758911132813e-1 1e0 +window_matrix/4 win 4.0228515625e2 2.1341796875e2 8.274879455566406e-1 +window_matrix/5 eye -1.4692661762237549e0 2.4675474166870117e0 -1.8420186042785645e0 6.873982548713684e-1 +window_matrix/5 clip 7.076813839375973e-3 1.6597900539636612e-2 -3.4504261016845703e0 6.873982548713684e-1 +window_matrix/5 win 1.68671875e2 3.9560546875e2 -2.009772300720215e0 +window_matrix/6 eye -1.4260280132293701e0 2.485703468322754e0 -3.8388168811798096e0 1e0 +window_matrix/6 clip 1.4805013313889503e-2 2.2474776953458786e-2 -6.758225440979004e0 1e0 +window_matrix/6 win 2.425625e2 3.682265625e2 -2.879112720489502e0 +window_matrix/7 eye -1.4485691785812378e0 5.255732536315918e-1 1.7684712409973145e0 1e0 +window_matrix/7 clip 1.904590055346489e-2 1.6682680696249008e-2 1.6527068614959717e0 1e0 +window_matrix/7 win 3.12046875e2 2.73328125e2 1.3263534307479858e0 +window_matrix/8 eye 2.1442747116088867e0 2.6038084030151367e0 2.8756237030029297e0 1e0 +window_matrix/8 clip 2.500537596642971e-2 3.0665691941976547e-2 3.3134355545043945e0 1e0 +window_matrix/8 win 4.096875e2 5.0242578125e2 2.1567177772521973e0 +window_matrix/9 eye -2.94674015045166e0 -1.8373887538909912e0 -8.516864776611328e-1 1e0 +window_matrix/9 clip 1.9848782569169998e-2 6.468060426414013e-3 -2.277529716491699e0 1e0 +window_matrix/9 win 3.2519921875e2 1.0597265625e2 -6.387648582458496e-1 +window_matrix/10 eye -2.935354709625244e0 1.790482521057129e0 -3.4873361587524414e0 9.945236444473267e-1 +window_matrix/10 clip 1.1776875704526901e-2 1.7021246254444122e-2 -6.225527763366699e0 9.945236444473267e-1 +window_matrix/10 win 1.9401171875e2 2.8041015625e2 -2.629904270172119e0 +window_matrix/11 eye -2.3967864513397217e0 -2.3199880123138428e0 -2.719097137451172e-1 1e0 +window_matrix/11 clip 2.252037450671196e-2 6.262589246034622e-3 -1.4078645706176758e0 1e0 +window_matrix/11 win 3.6897265625e2 1.0260546875e2 -2.039322853088379e-1 +window_matrix/12 eye 6.714825630187988e-1 1.762575626373291e0 -1.0426411628723145e0 1e0 +window_matrix/12 clip 2.2538967430591583e-2 2.496502734720707e-2 -2.5639617443084717e0 1e0 +window_matrix/12 win 3.6927734375e2 4.090234375e2 -7.819808721542358e-1 +window_matrix/13 eye 9.570157527923584e-1 -2.876133918762207e0 -3.825482130050659e0 1e0 +window_matrix/13 clip 3.3567965030670166e-2 1.2002449482679367e-2 -6.738223075866699e0 1e0 +window_matrix/13 win 5.499765625e2 1.9664453125e2 -2.8691115379333496e0 +window_matrix/14 eye -2.468637466430664e0 -2.137317657470703e-1 -7.349433898925781e-1 1e0 +window_matrix/14 clip 1.7681868746876717e-2 1.2275386601686478e-2 -2.102415084838867e0 1e0 +window_matrix/14 win 2.8969921875e2 2.011171875e2 -5.512075424194336e-1 +window_matrix/15 eye -9.146459102630615e-1 2.480156421661377e0 4.666614532470703e-2 5.456457138061523e-1 +window_matrix/15 clip 5.208891816437244e-3 1.525576040148735e-2 -4.756464958190918e-1 5.456457138061523e-1 +window_matrix/15 win 1.5640625e2 4.58078125e2 6.414344906806946e-2 +window_matrix/16 eye 7.035422325134277e-1 1.1831026077270508e0 2.768284797668457e0 1e0 +window_matrix/16 clip 2.3906148970127106e-2 2.3337796330451965e-2 3.1524271965026855e0 1e0 +window_matrix/16 win 3.9167578125e2 3.8236328125e2 2.0762135982513428e0 +window_matrix/17 eye 1.9378662109375e0 2.7254409790039063e0 -4.995843887329102e0 1e0 +window_matrix/17 clip 2.4133404716849327e-2 3.0568502843379974e-2 -8.493765830993652e0 1e0 +window_matrix/17 win 3.953984375e2 5.0083203125e2 -3.746882915496826e0 +window_matrix/18 eye 2.9621806144714355e0 2.349513530731201e0 -5.273796558380127e0 1e0 +window_matrix/18 clip 2.7960337698459625e-2 3.171784430742264e-2 -8.91069507598877e0 1e0 +window_matrix/18 win 4.581015625e2 5.196640625e2 -3.9553475379943848e0 +window_matrix/19 eye -3.95904541015625e-1 2.1086788177490234e0 1.2435517311096191e0 1e0 +window_matrix/19 clip 1.865137554705143e-2 2.3633668199181557e-2 8.653275966644287e-1 1e0 +window_matrix/19 win 3.0558203125e2 3.872109375e2 9.326637983322144e-1 +window_matrix/20 eye 7.693195343017578e-2 1.2135505676269531e-1 -2.651926279067993e0 1.4196527004241943e0 +window_matrix/20 clip 3.466155380010605e-2 2.6540853083133698e-2 -5.3975419998168945e0 1.4196527004241943e0 +window_matrix/20 win 4.000234375e2 3.0630078125e2 -1.401007890701294e0 +window_matrix/21 eye -2.6007933616638184e0 1.5055150985717773e0 7.488951683044434e-1 1e0 +window_matrix/21 clip 1.3517051935195923e-2 1.702186092734337e-2 1.2334275245666504e-1 1e0 +window_matrix/21 win 2.214609375e2 2.788828125e2 5.616713762283325e-1 +window_matrix/22 eye -8.483996391296387e-1 -2.4047884941101074e0 -3.561929941177368e0 1e0 +window_matrix/22 clip 2.724299393594265e-2 9.4163678586483e-3 -6.342895030975342e0 1e0 +window_matrix/22 win 4.4634765625e2 1.5427734375e2 -2.671447515487671e0 +window_matrix/23 eye 2.663618564605713e0 -2.887369394302368e0 -2.2882697582244873e0 1e0 +window_matrix/23 clip 3.8592465221881866e-2 1.5719393268227577e-2 -4.432404518127441e0 1e0 +window_matrix/23 win 6.32296875e2 2.5754296875e2 -1.7162022590637207e0 +window_matrix/24 eye 4.2499303817749023e-1 2.7970938682556152e0 2.7438201904296875e0 1e0 +window_matrix/24 clip 1.9543718546628952e-2 2.7454238384962082e-2 3.1157302856445313e0 1e0 +window_matrix/24 win 3.20203125e2 4.4980859375e2 2.0578651428222656e0 +window_matrix/25 eye -6.747696399688721e-1 -2.9331979751586914e0 -5.414085865020752e0 9.6866774559021e-1 +window_matrix/25 clip 2.8146827593445778e-2 7.675616070628166e-3 -9.089797019958496e0 9.6866774559021e-1 +window_matrix/25 win 4.760703125e2 1.2982421875e2 -4.191906929016113e0 +window_matrix/26 eye 1.857090950012207e0 2.032998561859131e0 -8.544602394104004e-1 1e0 +window_matrix/26 clip 2.541823871433735e-2 2.8362378478050232e-2 -2.2816903591156006e0 1e0 +window_matrix/26 win 4.1644921875e2 4.646875e2 -6.408451795578003e-1 +window_matrix/27 eye -1.324293613433838e-1 7.567057609558105e-1 -2.71038818359375e0 1e0 +window_matrix/27 clip 2.239391952753067e-2 2.025173418223858e-2 -5.065582275390625e0 1e0 +window_matrix/27 win 3.668984375e2 3.3180078125e2 -2.0327911376953125e0 +window_matrix/28 eye 1.3259358406066895e0 -1.4977810382843018e0 1.9540891647338867e0 1e0 +window_matrix/28 clip 3.162018209695816e-2 1.6851209104061127e-2 1.93113374710083e0 1e0 +window_matrix/28 win 5.180625e2 2.7608984375e2 1.465566873550415e0 +window_matrix/29 eye -1.6898351907730103e0 1.5140070915222168e0 -4.375731945037842e0 1e0 +window_matrix/29 clip 1.616721600294113e-2 1.9048355519771576e-2 -7.563597679138184e0 1e0 +window_matrix/29 win 2.648828125e2 3.120859375e2 -3.281798839569092e0 +window_matrix/30 eye 1.93402099609375e0 -2.587156057357788e0 3.218860626220703e-1 1.3266997337341309e0 +window_matrix/30 clip 4.378136619925499e-2 2.0982839167118073e-2 -8.438706398010254e-1 1.3266997337341309e0 +window_matrix/30 win 5.40671875e2 2.59125e2 1.8196621537208557e-1 +window_matrix/31 eye -1.5318844318389893e0 -2.3015475273132324e0 -3.8558685779571533e0 1e0 +window_matrix/31 clip 2.5013748556375504e-2 8.217033930122852e-3 -6.7838029861450195e0 1e0 +window_matrix/31 win 4.0982421875e2 1.34625e2 -2.8919014930725098e0 +window_matrix/32 eye 1.0982446670532227e0 -2.8169829845428467e0 -4.693785667419434e0 1e0 +window_matrix/32 clip 3.3851753920316696e-2 1.2486061081290245e-2 -8.040678024291992e0 1e0 +window_matrix/32 win 5.54625e2 2.045703125e2 -3.520339012145996e0 +window_matrix/33 eye -7.015681266784668e-1 1.6764545440673828e0 -8.5308837890625e-1 1e0 +window_matrix/33 clip 1.8705587834119797e-2 2.1695762872695923e-2 -2.279632568359375e0 1e0 +window_matrix/33 win 3.0646875e2 3.554609375e2 -6.398162841796875e-1 +window_matrix/34 eye -2.4207539558410645e0 -2.6146445274353027e0 -1.1310782432556152e0 1e0 +window_matrix/34 clip 2.3097597062587738e-2 5.346674472093582e-3 -2.696617364883423e0 1e0 +window_matrix/34 win 3.784296875e2 8.759765625e1 -8.483086824417114e-1 +window_matrix/35 eye 6.538419723510742e-1 2.2641525268554688e0 -2.213336229324341e0 1.5709975957870483e0 +window_matrix/35 clip 3.53429839015007e-2 3.6859720945358276e-2 -4.891002178192139e0 1.5709975957870483e0 +window_matrix/35 win 3.6858984375e2 3.8441015625e2 -1.056654930114746e0 +window_matrix/36 eye -2.598539352416992e0 -2.9223251342773438e0 5.782499313354492e-1 1e0 +window_matrix/36 clip 2.325279638171196e-2 4.05462458729744e-3 -1.3262510299682617e-1 1e0 +window_matrix/36 win 3.8097265625e2 6.64296875e1 4.336874485015869e-1 +window_matrix/37 eye -2.815436840057373e0 2.54249906539917e0 3.7764358520507813e0 1e0 +window_matrix/37 clip 1.0609684512019157e-2 1.9588271155953407e-2 4.664653778076172e0 1e0 +window_matrix/37 win 1.73828125e2 3.2093359375e2 2.832326889038086e0 +window_matrix/38 eye 1.0990290641784668e0 9.989614486694336e-1 2.2380542755126953e0 1e0 +window_matrix/38 clip 2.5469407439231873e-2 2.3667309433221817e-2 2.357081413269043e0 1e0 +window_matrix/38 win 4.172890625e2 3.8776171875e2 1.6785407066345215e0 +window_matrix/39 eye -2.224998950958252e0 1.4580249786376953e-2 -5.528273582458496e0 1e0 +window_matrix/39 clip 1.7893992364406586e-2 1.3479607179760933e-2 -9.292409896850586e0 1e0 +window_matrix/39 win 2.93171875e2 2.2084765625e2 -4.146204948425293e0 +window_matrix_rot/0 eye 6.99999988079071e-1 -4.000000059604645e-1 -6e0 1e0 +window_matrix_rot/0 clip 2.7374267578125e-2 1.86920166015625e-2 -1e1 1e0 +window_matrix_rot/0 win 4.485e2 3.0625e2 -4.5e0 +window_matrix_rot/1 eye 8.999999761581421e-1 1.0000001192092896e0 -5e0 1e0 +window_matrix_rot/1 clip 2.488403208553791e-2 2.323303371667862e-2 -8.5e0 1e0 +window_matrix_rot/1 win 4.0769921875e2 3.806484375e2 -3.75e0 +window_matrix_rot/2 eye -4.0000003576278687e-1 -6.000000238418579e-1 -8e0 1e0 +window_matrix_rot/2 clip 2.459106408059597e-2 1.568908616900444e-2 -1.3e1 1e0 +window_matrix_rot/2 win 4.028984375e2 2.57046875e2 -6e0 +window_matrix_rot/3 eye 1.1200000047683716e0 -4.6000000834465027e-1 -1e0 1e0 +window_matrix_rot/3 clip 2.8736572712659836e-2 1.9439086318016052e-2 -2.5e0 1e0 +window_matrix_rot/3 win 4.7081640625e2 3.1848828125e2 -7.5e-1 +window_matrix_rot/4 eye 7.297540903091431e-1 -1.8471421003341675e0 -4.8966827392578125e0 1e0 +window_matrix_rot/4 clip 3.064119443297386e-2 1.451772078871727e-2 -8.345024108886719e0 1e0 +window_matrix_rot/4 win 5.020234375e2 2.3785546875e2 -3.6725120544433594e0 +window_matrix_rot/5 eye -2.174762725830078e0 8.17518949508667e-1 -5.9664082527160645e0 6.873982548713684e-1 +window_matrix_rot/5 clip 8.635480888187885e-3 1.0213670320808887e-2 -9.63701057434082e0 6.873982548713684e-1 +window_matrix_rot/5 win 2.0582421875e2 2.434375e2 -6.509772777557373e0 +window_matrix_rot/6 eye -1.9322445392608643e0 7.329460382461548e-1 -9.83881664276123e0 1e0 +window_matrix_rot/6 clip 1.717323064804077e-2 1.622745394706726e-2 -1.5758224487304688e1 1e0 +window_matrix_rot/6 win 2.8136328125e2 2.658671875e2 -7.379112243652344e0 +window_matrix_rot/7 eye -7.741993069648743e-1 -8.486828804016113e-1 -4.2315287590026855e0 1e0 +window_matrix_rot/7 clip 2.4041200056672096e-2 1.4138308353722095e-2 -7.347292900085449e0 1e0 +window_matrix_rot/7 win 3.93890625e2 2.31640625e2 -3.1736464500427246e0 +window_matrix_rot/8 eye 8.531346917152405e-1 2.969611406326294e0 -3.1243762969970703e0 1e0 +window_matrix_rot/8 clip 2.041897177696228e-2 2.8900403529405594e-2 -5.6865644454956055e0 1e0 +window_matrix_rot/8 win 3.3454296875e2 4.7350390625e2 -2.3432822227478027e0 +window_matrix_rot/9 eye -5.549587607383728e-1 -3.6379551887512207e0 -6.851686477661133e0 1e0 +window_matrix_rot/9 clip 3.0812278389930725e-2 6.448341533541679e-3 -1.12775297164917e1 1e0 +window_matrix_rot/9 win 5.04828125e2 1.056484375e2 -5.13876485824585e0 +window_matrix_rot/10 eye -2.7264068126678467e0 -7.266364097595215e-1 -9.45447826385498e0 9.945236444473267e-1 +window_matrix_rot/10 clip 1.7919806763529778e-2 1.0105988942086697e-2 -1.5176240921020508e1 9.945236444473267e-1 +window_matrix_rot/10 win 2.952109375e2 1.66484375e2 -7.129904747009277e0 +window_matrix_rot/11 eye 1.7456358671188354e-1 -3.6940624713897705e0 -6.271909713745117e0 1e0 +window_matrix_rot/11 clip 3.3072832971811295e-2 7.886919192969799e-3 -1.0407864570617676e1 1e0 +window_matrix_rot/11 win 5.4186328125e2 1.2921875e2 -4.703932285308838e0 +window_matrix_rot/12 eye 1.7964065074920654e-1 1.4129501581192017e0 -7.0426411628723145e0 1e0 +window_matrix_rot/12 clip 2.1866243332624435e-2 2.2860025987029076e-2 -1.156396198272705e1 1e0 +window_matrix_rot/12 win 3.5825390625e2 3.7453515625e2 -5.281980991363525e0 +window_matrix_rot/13 eye 3.1912930011749268e0 -2.1266977787017822e0 -9.825482368469238e0 1e0 +window_matrix_rot/13 clip 3.846698999404907e-2 1.9107364118099213e-2 -1.5738224029541016e1 1e0 +window_matrix_rot/13 win 6.302421875e2 3.130546875e2 -7.369112014770508e0 +window_matrix_rot/14 eye -1.1466710567474365e0 -2.052168130874634e0 -6.734943389892578e0 1e0 +window_matrix_rot/14 clip 2.5594349950551987e-2 9.794052690267563e-3 -1.1102415084838867e1 1e0 +window_matrix_rot/14 win 4.193359375e2 1.6046484375e2 -5.051207542419434e0 +window_matrix_rot/15 eye -1.8378584384918213e0 1.2170792818069458e0 -3.227208137512207e0 5.456457138061523e-1 +window_matrix_rot/15 clip 5.279483273625374e-3 9.526796638965607e-3 -5.386457920074463e0 5.456457138061523e-1 +window_matrix_rot/15 win 1.585234375e2 2.8605859375e2 -4.435856819152832e0 +window_matrix_rot/16 eye 5.529721975326538e-1 9.686075448989868e-1 -3.231715202331543e0 1e0 +window_matrix_rot/16 clip 2.3936327546834946e-2 2.2378548979759216e-2 -5.8475728034973145e0 1e0 +window_matrix_rot/16 win 3.92171875e2 3.666484375e2 -2.4237864017486572e0 +window_matrix_rot/17 eye 6.150283217430115e-1 2.943072557449341e0 -1.0995843887329102e1 1e0 +window_matrix_rot/17 clip 1.9779708236455917e-2 2.8299469500780106e-2 -1.749376678466797e1 1e0 +window_matrix_rot/17 win 3.240703125e2 4.6365625e2 -8.246883392333984e0 +window_matrix_rot/18 eye 1.660036325454712e0 3.2569193840026855e0 -1.1273796081542969e1 1e0 +window_matrix_rot/18 clip 2.2151650860905647e-2 3.1515102833509445e-2 -1.7910694122314453e1 1e0 +window_matrix_rot/18 win 3.629296875e2 5.1633984375e2 -8.455347061157227e0 +window_matrix_rot/19 eye -8.819308876991272e-1 1.0494003295898438e0 -4.756448268890381e0 1e0 +window_matrix_rot/19 clip 1.95549875497818e-2 1.9462384283542633e-2 -8.134672164916992e0 1e0 +window_matrix_rot/19 win 3.2038671875e2 3.1887109375e2 -3.567336082458496e0 +window_matrix_rot/20 eye 9.824894070625305e-1 -4.2461785674095154e-1 -1.1169841766357422e1 1.4196527004241943e0 +window_matrix_rot/20 clip 3.851420059800148e-2 2.6931073516607285e-2 -1.8174415588378906e1 1.4196527004241943e0 +window_matrix_rot/20 win 4.44484375e2 3.108046875e2 -5.901007652282715e0 +window_matrix_rot/21 eye -2.2839438915252686e0 -7.560639381408691e-1 -5.251104831695557e0 1e0 +window_matrix_rot/21 clip 1.9414611160755157e-2 1.1092343367636204e-2 -8.876657485961914e0 1e0 +window_matrix_rot/21 win 3.180859375e2 1.81734375e2 -3.938328742980957e0 +window_matrix_rot/22 eye 1.4641534090042114e0 -2.8328707218170166e0 -9.561929702758789e0 1e0 +window_matrix_rot/22 clip 3.495866060256958e-2 1.324351318180561e-2 -1.5342894554138184e1 1e0 +window_matrix_rot/22 win 5.7276171875e2 2.1698046875e2 -7.171447277069092e0 +window_matrix_rot/23 eye 4.563316345214844e0 -1.1117242574691772e0 -8.288269996643066e0 1e0 +window_matrix_rot/23 clip 4.0256425738334656e-2 2.50956192612648e-2 -1.3432405471801758e1 1e0 +window_matrix_rot/23 win 6.5955859375e2 4.111640625e2 -6.216202735900879e0 +window_matrix_rot/24 eye -6.382619738578796e-1 2.0926709175109863e0 -3.2561798095703125e0 1e0 +window_matrix_rot/24 clip 1.7976518720388412e-2 2.3054245859384537e-2 -5.884269714355469e0 1e0 +window_matrix_rot/24 win 2.945234375e2 3.7771875e2 -2.4421348571777344e0 +window_matrix_rot/25 eye 1.8981704711914063e0 -3.1388871669769287e0 -1.1226092338562012e1 9.6866774559021e-1 +window_matrix_rot/25 clip 3.6136694252491e-2 1.2726443819701672e-2 -1.780780601501465e1 9.6866774559021e-1 +window_matrix_rot/25 win 6.112109375e2 2.1525390625e2 -8.691906929016113e0 +window_matrix_rot/26 eye 9.65873658657074e-1 2.340653419494629e0 -6.8544602394104e0 1e0 +window_matrix_rot/26 clip 2.213125117123127e-2 2.7305468916893005e-2 -1.128169059753418e1 1e0 +window_matrix_rot/26 win 3.6259765625e2 4.4737109375e2 -5.14084529876709e0 +window_matrix_rot/27 eye 1.4003300666809082e-1 1.2590697407722473e-1 -8.71038818359375e0 1e0 +window_matrix_rot/27 clip 2.457818202674389e-2 1.9002363085746765e-2 -1.4065582275390625e1 1e0 +window_matrix_rot/27 win 4.026875e2 3.1133203125e2 -6.5327911376953125e0 +window_matrix_rot/28 eye 2.6594173908233643e0 -8.026633262634277e-1 -4.045910835266113e0 1e0 +window_matrix_rot/28 clip 3.399950638413429e-2 2.1817699074745178e-2 -7.06886625289917e0 1e0 +window_matrix_rot/28 win 5.57046875e2 3.574609375e2 -3.034433126449585e0 +window_matrix_rot/29 eye -1.560272455215454e0 -2.0269539952278137e-1 -1.0375732421875e1 1e0 +window_matrix_rot/29 clip 2.031884528696537e-2 1.4303638599812984e-2 -1.65635986328125e1 1e0 +window_matrix_rot/29 win 3.3290234375e2 2.3434765625e2 -7.78179931640625e0 +window_matrix_rot/30 eye 4.028200149536133e0 -1.4399919509887695e0 -7.638312339782715e0 1.3266997337341309e0 +window_matrix_rot/30 clip 4.739603027701378e-2 2.8945140540599823e-2 -1.2784168243408203e1 1.3266997337341309e0 +window_matrix_rot/30 win 5.853125e2 3.57453125e2 -4.3180341720581055e0 +window_matrix_rot/31 eye 8.554208874702454e-1 -3.1603689193725586e0 -9.855868339538574e0 1e0 +window_matrix_rot/31 clip 3.3894866704940796e-2 1.0946499183773994e-2 -1.5783802032470703e1 1e0 +window_matrix_rot/31 win 5.5533203125e2 1.7934375e2 -7.391901016235352e0 +window_matrix_rot/32 eye 3.2687857151031494e0 -1.9946397542953491e0 -1.0693785667419434e1 1e0 +window_matrix_rot/32 clip 3.840385377407074e-2 1.9664525985717773e-2 -1.7040678024291992e1 1e0 +window_matrix_rot/32 win 6.2920703125e2 3.2218359375e2 -8.020339012145996e0 +window_matrix_rot/33 eye -8.671272397041321e-1 5.202227830886841e-1 -6.85308837890625e0 1e0 +window_matrix_rot/33 clip 2.0761100575327873e-2 1.7944587394595146e-2 -1.1279632568359375e1 1e0 +window_matrix_rot/33 win 3.401484375e2 2.9400390625e2 -5.1398162841796875e0 +window_matrix_rot/34 eye 3.321835398674011e-1 -3.9441680908203125e0 -7.131078243255615e0 1e0 +window_matrix_rot/34 clip 3.408415988087654e-2 7.500520907342434e-3 -1.1696617126464844e1 1e0 +window_matrix_rot/34 win 5.5843359375e2 1.2288671875e2 -5.348308563232422e0 +window_matrix_rot/35 eye 2.6428037881851196e-1 1.575228214263916e0 -1.1639321327209473e1 1.5709975957870483e0 +window_matrix_rot/35 clip 3.5715438425540924e-2 3.398541733622551e-2 -1.9029979705810547e1 1.5709975957870483e0 +window_matrix_rot/35 win 3.724765625e2 3.5443359375e2 -5.556654453277588e0 +window_matrix_rot/36 eye 3.745637536048889e-1 -4.29698371887207e0 -5.421750068664551e0 1e0 +window_matrix_rot/36 clip 3.4983549267053604e-2 6.560002453625202e-3 -9.132625579833984e0 1e0 +window_matrix_rot/36 win 5.7316796875e2 1.074765625e2 -4.066312789916992e0 +window_matrix_rot/37 eye -3.0778491497039795e0 -5.526295304298401e-2 -2.2235641479492188e0 1e0 +window_matrix_rot/37 clip 1.5548871830105782e-2 1.1401050724089146e-2 -4.335346221923828e0 1e0 +window_matrix_rot/37 win 2.5475e2 1.8679296875e2 -1.667673110961914e0 +window_matrix_rot/38 eye 9.798463582992554e-1 1.0585867166519165e0 -3.7619457244873047e0 1e0 +window_matrix_rot/38 clip 2.4989226832985878e-2 2.3580117151141167e-2 -6.642918586730957e0 1e0 +window_matrix_rot/38 win 4.09421875e2 3.863359375e2 -2.8214592933654785e0 +window_matrix_rot/39 eye -1.088747262954712e0 -1.7233352661132813e0 -1.1528273582458496e1 1e0 +window_matrix_rot/39 clip 2.504151687026024e-2 1.0884704999625683e-2 -1.8292409896850586e1 1e0 +window_matrix_rot/39 win 4.1027734375e2 1.7833203125e2 -8.646204948425293e0 +light_directional/0 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/1 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/2 rgba 1e0 9.363667964935303e-1 8.58412504196167e-1 8.999999761581421e-1 +light_directional/3 rgba 1e0 1e0 9.54419732093811e-1 8.999999761581421e-1 +light_directional/4 rgba 8.310951590538025e-1 4.872409999370575e-1 4.3243804574012756e-1 8.999999761581421e-1 +light_directional/5 rgba 1.5556012094020844e-1 1.072525680065155e-1 1.6222405433654785e-1 8.999999761581421e-1 +light_directional/6 rgba 5.519773364067078e-1 3.302372395992279e-1 3.2079094648361206e-1 8.999999761581421e-1 +light_directional/7 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/8 rgba 4.964945614337921e-1 2.990281879901886e-1 2.985978424549103e-1 8.999999761581421e-1 +light_directional/9 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/10 rgba 1.0881997644901276e-1 8.096399158239365e-2 1.4353126287460327e-1 8.999999761581421e-1 +light_directional/11 rgba 4.934362769126892e-1 2.973078787326813e-1 2.9737451672554016e-1 8.999999761581421e-1 +light_directional/12 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/13 rgba 6.265013813972473e-1 3.721570074558258e-1 3.5060054063796997e-1 8.999999761581421e-1 +light_directional/14 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/15 rgba 5.23048996925354e-1 3.210620880126953e-1 3.1763094663619995e-1 8.999999761581421e-1 +light_directional/16 rgba 1.0469182580709457e-1 7.863915711641312e-2 1.418767273426056e-1 8.999999761581421e-1 +light_directional/17 rgba 1.868714541196823e-1 1.2486518919467926e-1 1.7474858462810516e-1 8.999999761581421e-1 +light_directional/18 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/19 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/20 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/21 rgba 1.5679462254047394e-1 1.0794697701931e-1 1.6271784901618958e-1 8.999999761581421e-1 +light_directional/22 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_directional/23 rgba 1.0000000894069672e-1 7.600000500679016e-2 1.4000000059604645e-1 8.999999761581421e-1 +light_positional/0 rgba 4.664922505617142e-2 2.664922922849655e-2 8.33115354180336e-2 8.999999761581421e-1 +light_positional/1 rgba 5.432311072945595e-2 3.432311490178108e-2 9.290389716625214e-2 8.999999761581421e-1 +light_positional/2 rgba 4.731997847557068e-1 4.111497402191162e-1 4.562510848045349e-1 8.999999761581421e-1 +light_positional/3 rgba 1.6628292202949524e-1 1.3966019451618195e-1 1.9371725618839264e-1 8.999999761581421e-1 +light_positional/4 rgba 1.4289157092571259e-1 8.637256920337677e-2 1.3057635724544525e-1 8.999999761581421e-1 +light_positional/5 rgba 8.140215277671814e-2 4.92437407374382e-2 1.0243586450815201e-1 8.999999761581421e-1 +light_positional/6 rgba 1.572643369436264e-1 9.592749178409576e-2 1.3890671730041504e-1 8.999999761581421e-1 +light_positional/7 rgba 4.846982657909393e-2 2.8469830751419067e-2 8.55872854590416e-2 8.999999761581421e-1 +light_positional/8 rgba 1.3878759741783142e-1 8.423050493001938e-2 1.2937031686306e-1 8.999999761581421e-1 +light_positional/9 rgba 9.559518843889236e-2 5.794477462768555e-2 1.0919146984815598e-1 8.999999761581421e-1 +light_positional/10 rgba 4.934525489807129e-2 2.9345257207751274e-2 8.668157458305359e-2 8.999999761581421e-1 +light_positional/11 rgba 1.500590294599533e-1 9.42390114068985e-2 1.3884246349334717e-1 8.999999761581421e-1 +light_positional/12 rgba 4.6346455812454224e-2 2.6346459984779358e-2 8.293307572603226e-2 8.999999761581421e-1 +light_positional/13 rgba 1.2492940574884415e-1 7.483506202697754e-2 1.2097308039665222e-1 8.999999761581421e-1 +light_positional/14 rgba 4.577576369047165e-2 2.5775764137506485e-2 8.221970498561859e-2 8.999999761581421e-1 +light_positional/15 rgba 4.918034002184868e-2 2.8950458392500877e-2 8.600343763828278e-2 8.999999761581421e-1 +light_positional/16 rgba 1.0036862641572952e-1 6.141933798789978e-2 1.1186961084604263e-1 8.999999761581421e-1 +light_positional/17 rgba 9.902530163526535e-2 5.9415750205516815e-2 1.0955700278282166e-1 8.999999761581421e-1 +light_positional/18 rgba 4.742776229977608e-2 2.7427762746810913e-2 8.4284707903862e-2 8.999999761581421e-1 +light_positional/19 rgba 5.6527309119701385e-2 3.652731329202652e-2 9.565913677215576e-2 8.999999761581421e-1 +light_positional/20 rgba 5.599468573927879e-2 3.599468618631363e-2 9.499336034059525e-2 8.999999761581421e-1 +light_positional/21 rgba 9.677205234766006e-2 5.833122134208679e-2 1.0908341407775879e-1 8.999999761581421e-1 +light_positional/22 rgba 4.734107479453087e-2 2.7341077104210854e-2 8.417634665966034e-2 8.999999761581421e-1 +light_positional/23 rgba 4.6764928847551346e-2 2.676493115723133e-2 8.345616608858109e-2 8.999999761581421e-1 +light_spot/0 rgba 4.036587104201317e-2 2.0365871489048004e-2 7.286587357521057e-2 8.999999761581421e-1 +light_spot/1 rgba 4.2718306183815e-2 2.271830290555954e-2 7.52183049917221e-2 8.999999761581421e-1 +light_spot/2 rgba 4.371064901351929e-2 2.3710649460554123e-2 7.621064782142639e-2 8.999999761581421e-1 +light_spot/3 rgba 4.029412567615509e-2 1.995985023677349e-2 7.2210893034935e-2 8.999999761581421e-1 +light_spot/4 rgba 1.4883184432983398e-1 8.748795092105865e-2 1.1518163233995438e-1 8.999999761581421e-1 +light_spot/5 rgba 1.0198067873716354e-1 5.967435985803604e-2 9.879057109355927e-2 8.999999761581421e-1 +light_spot/6 rgba 2.303476631641388e-1 1.388668566942215e-1 1.4847838878631592e-1 8.999999761581421e-1 +light_spot/7 rgba 1.017640009522438e-1 5.968694016337395e-2 9.894070029258728e-2 8.999999761581421e-1 +light_spot/8 rgba 1.6404370963573456e-1 9.81520265340805e-2 1.2311702966690063e-1 8.999999761581421e-1 +light_spot/9 rgba 1.386348158121109e-1 8.073357492685318e-2 1.1049018800258636e-1 8.999999761581421e-1 +light_spot/10 rgba 3.94098162651062e-2 1.9409814849495888e-2 7.19098150730133e-2 8.999999761581421e-1 +light_spot/11 rgba 1.1048073321580887e-1 6.395458430051804e-2 1.0053890198469162e-1 8.999999761581421e-1 +light_spot/12 rgba 7.419256865978241e-2 4.083600267767906e-2 8.53220671415329e-2 8.999999761581421e-1 +light_spot/13 rgba 1.7894218862056732e-1 1.0630087554454803e-1 1.2721610069274902e-1 8.999999761581421e-1 +light_spot/14 rgba 3.9711374789476395e-2 1.971137523651123e-2 7.22113698720932e-2 8.999999761581421e-1 +light_spot/15 rgba 2.7721720933914185e-1 2.4941308796405792e-1 2.54482626914978e-1 8.999999761581421e-1 +light_spot/16 rgba 3.859630227088928e-2 1.859630085527897e-2 7.109630107879639e-2 8.999999761581421e-1 +light_spot/17 rgba 4.1043274104595184e-2 2.104327455163002e-2 7.354327291250229e-2 8.999999761581421e-1 +light_spot/18 rgba 4.075831547379494e-2 2.0758314058184624e-2 7.325831055641174e-2 8.999999761581421e-1 +light_spot/19 rgba 4.6889983117580414e-2 2.688998356461525e-2 7.938998192548752e-2 8.999999761581421e-1 +light_spot/20 rgba 1.5250208973884583e-1 9.32127833366394e-2 1.2213920056819916e-1 8.999999761581421e-1 +light_spot/21 rgba 1.7339468002319336e-1 1.0322057455778122e-1 1.2561611831188202e-1 8.999999761581421e-1 +light_spot/22 rgba 4.010423272848129e-2 2.010423317551613e-2 7.26042315363884e-2 8.999999761581421e-1 +light_spot/23 rgba 5.1270727068185806e-2 2.7608003467321396e-2 7.791036367416382e-2 8.999999761581421e-1 +light_local_viewer/0 rgba 6.358101963996887e-2 4.3581023812294006e-2 1.0636253654956818e-1 8.999999761581421e-1 +light_local_viewer/1 rgba 2.0879732072353363e-1 1.3573935627937317e-1 1.9014528393745422e-1 8.999999761581421e-1 +light_local_viewer/2 rgba 6.835858523845673e-2 4.835858196020126e-2 1.1299803853034973e-1 8.999999761581421e-1 +light_local_viewer/3 rgba 6.1228152364492416e-2 4.122815281152725e-2 1.030946597456932e-1 8.999999761581421e-1 +light_local_viewer/4 rgba 1.3294902443885803e-1 8.579982072114944e-2 1.423754245042801e-1 8.999999761581421e-1 +light_local_viewer/5 rgba 1.1238151788711548e-1 7.520221173763275e-2 1.359647512435913e-1 8.999999761581421e-1 +light_local_viewer/6 rgba 3.055807948112488e-1 1.9626633822917938e-1 2.4255859851837158e-1 8.999999761581421e-1 +light_local_viewer/7 rgba 3.821677565574646e-1 2.460940033197403e-1 2.9090195894241333e-1 8.999999761581421e-1 +light_local_viewer/8 rgba 1.5795235335826874e-1 1.0361994057893753e-1 1.61139577627182e-1 8.999999761581421e-1 +light_local_viewer/9 rgba 3.818405270576477e-1 2.774706482887268e-1 3.3458778262138367e-1 8.999999761581421e-1 +light_local_viewer/10 rgba 6.329066306352615e-2 4.329065978527069e-2 1.0595925897359848e-1 8.999999761581421e-1 +light_local_viewer/11 rgba 7.683844864368439e-2 5.296152085065842e-2 1.1616023629903793e-1 8.999999761581421e-1 +light_local_viewer/12 rgba 1.8345744907855988e-1 1.1756774038076401e-1 1.7088046669960022e-1 8.999999761581421e-1 +light_local_viewer/13 rgba 2.7737292647361755e-1 1.7769676446914673e-1 2.2607067227363586e-1 8.999999761581421e-1 +light_local_viewer/14 rgba 6.195501983165741e-2 4.1955020278692245e-2 1.0410419851541519e-1 8.999999761581421e-1 +light_local_viewer/15 rgba 6.905853748321533e-2 4.905853793025017e-2 1.1397019773721695e-1 8.999999761581421e-1 +light_local_viewer/16 rgba 7.173575460910797e-2 5.1735758781433105e-2 1.1768855899572372e-1 8.999999761581421e-1 +light_local_viewer/17 rgba 7.057944685220718e-2 5.057944729924202e-2 1.160825714468956e-1 8.999999761581421e-1 +light_local_viewer/18 rgba 6.905737519264221e-2 4.905737191438675e-2 1.1396858096122742e-1 8.999999761581421e-1 +light_local_viewer/19 rgba 2.321857511997223e-1 1.5191364288330078e-1 2.065977305173874e-1 8.999999761581421e-1 +light_local_viewer/20 rgba 3.9314937591552734e-1 2.530512809753418e-1 2.972117066383362e-1 8.999999761581421e-1 +light_local_viewer/21 rgba 2.798535227775574e-1 1.7837253212928772e-1 2.2567209601402283e-1 8.999999761581421e-1 +light_local_viewer/22 rgba 7.8621506690979e-2 5.270793288946152e-2 1.1411082744598389e-1 8.999999761581421e-1 +light_local_viewer/23 rgba 2.2242523729801178e-1 1.4524361491203308e-1 1.9990921020507813e-1 8.999999761581421e-1 +light_three/0 rgba 3.2211995124816895e-1 1.529109627008438e-1 1.5070664882659912e-1 7.5e-1 +light_three/1 rgba 3.8527390360832214e-1 2.0472781360149384e-1 7.032457590103149e-1 7.5e-1 +light_three/2 rgba 5.660682320594788e-1 1.6370508074760437e-1 1.5902039408683777e-1 7.5e-1 +light_three/3 rgba 3.6633720993995667e-1 1.342293918132782e-1 1.5073329210281372e-1 7.5e-1 +light_three/4 rgba 1e0 2.705341875553131e-1 2.3520393669605255e-1 7.5e-1 +light_three/5 rgba 8.759123682975769e-1 5.360540151596069e-1 3.0811697244644165e-1 7.5e-1 +light_three/6 rgba 7.020934224128723e-1 3.0961883068084717e-1 4.4964170455932617e-1 7.5e-1 +light_three/7 rgba 4.0573424100875854e-1 3.120558261871338e-1 3.386705815792084e-1 7.5e-1 +light_three/8 rgba 1e0 4.3959730863571167e-1 2.3081913590431213e-1 7.5e-1 +light_three/9 rgba 4.212196469306946e-1 2.6093655824661255e-1 6.051255464553833e-1 7.5e-1 +light_three/10 rgba 1e0 2.3599961400032043e-1 2.2330473363399506e-1 7.5e-1 +light_three/11 rgba 1e0 2.905045747756958e-1 1.8472470343112946e-1 7.5e-1 +light_three/12 rgba 4.0107765793800354e-1 2.554994821548462e-1 4.1283118724823e-1 7.5e-1 +light_three/13 rgba 1e0 3.5534143447875977e-1 2.70237535238266e-1 7.5e-1 +light_three/14 rgba 8.966069221496582e-1 1.9030117988586426e-1 2.0195117592811584e-1 7.5e-1 +light_three/15 rgba 3.2840678095817566e-1 1.479893922805786e-1 1.811327040195465e-1 7.5e-1 +light_three/16 rgba 1e0 4.6089109778404236e-1 2.2252580523490906e-1 7.5e-1 +light_three/17 rgba 9.64160144329071e-1 4.239793121814728e-1 1.9132915139198303e-1 7.5e-1 +light_three/18 rgba 3.285428583621979e-1 1.7788885533809662e-1 1.5284761786460876e-1 7.5e-1 +light_three/19 rgba 3.724369704723358e-1 1.9285254180431366e-1 5.901982188224792e-1 7.5e-1 +light_three/20 rgba 4.080149531364441e-1 3.4516483545303345e-1 3.160569965839386e-1 7.5e-1 +light_three/21 rgba 4.485481381416321e-1 3.746458888053894e-1 4.5314663648605347e-1 7.5e-1 +light_three/22 rgba 3.660910427570343e-1 2.944508194923401e-1 2.0904046297073364e-1 7.5e-1 +light_three/23 rgba 3.938528597354889e-1 2.1752336621284485e-1 6.133416295051575e-1 7.5e-1 +light_none/0 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/1 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/2 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/3 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/4 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/5 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/6 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/7 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/8 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/9 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/10 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/11 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/12 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/13 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/14 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/15 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/16 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/17 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/18 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/19 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/20 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/21 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/22 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 +light_none/23 rgba 9.599999785423279e-1 7.599999904632568e-1 3.75e-1 8.999999761581421e-1 diff --git a/src/dev/odyssey/testdata/replay/mirrors.hashes b/src/dev/odyssey/testdata/replay/mirrors.hashes new file mode 100644 index 000000000..53e175466 --- /dev/null +++ b/src/dev/odyssey/testdata/replay/mirrors.hashes @@ -0,0 +1,62 @@ +# Checkpoints of mirrors.rec (record index, blake3 of the planes), every 500000 records. +# 29868031 records, 22921 DMA reads. +500000 81b519473d4983298b21f195b8c321b692d8965ec09f7818cda255d00801b189 +1000000 e1f8c2393f48045250084c5b21df8bb04d105831a85986668d7f28741d910ab0 +1500000 ec3cacb3391f7ccceed269ef38c419884ac968e9f973de05674f23eb6e6cd02d +2000000 0988afed52ecf6f4145df970d06a1ab9fa549af3a7b140313971e4a53493b416 +2500000 bdda781d8b42e2171b2f18c7450f80dae7ca57546733e6e5a4d2bdd2ae400a92 +3000000 a824ca9e4271250388ca4a97b141a54e257604a3ee8b591c5f3006ce431eddc7 +3500000 2308e7cc75804325d11514da60532615ea5aaf50f0edf2c04691b734810e4f10 +4000000 d2707d8983479cd48d8ee8d35f4380f9df85f9e5222f083887cb4ce27fe69f01 +4500000 4122c819366dbae2a3b9810e9b5cd69fef34000a3774af04fe40aa561cb462da +5000000 90db7f7750687fbbc977874cf8bd7fcef2738d650ba50c2313abb9976fb274ed +5500000 bdd1f0518b24f969f2635a42f9e4d35d9cf359e0136791da610068e508b1298c +6000000 68c9fb28c96bf628614c7a0495dacf9257f3f215bfbe1b102810a6b5bebae084 +6500000 72242f85eaf37f4f92c2cab1b026cc16d96f3b386d755d9dccf69cc2a21d8806 +7000000 1ae3d0bad8c8bd54db5a41fbdbf04af2c897ec13f447006b8414736a5e14ea20 +7500000 9298714223ee0a0f226196bc9f0f7aa10e1aa1752e801ddffee1647ea352444b +8000000 feba3ba5cc9460d1582cc971b5dd4d28d6685bd86bf85127f9bc6ea009e41a29 +8500000 c78dcdd586cdb5ebf3dd84396e813c6ca7eb49912b76ebcda329beb608c2a447 +9000000 02e29ac511712416f74ec34f8946dc35bb165c9980971b7995f4988c115c53d9 +9500000 8732f98ffff1cfe7466f056841422032b4491b60798357703f00de212e5d6a4d +10000000 bb87d5ea87d7270d52150dc5152879761e68e028a2bcecea3740a297b4493b6a +10500000 038c036ab7089fc5b8c4ec66db8cd17f554d63b5788ea681257a86b41be44e14 +11000000 77def83510651bf73700a80f3396318b1e26d71036c28b2527e923e25b82448d +11500000 d0dc3ec63fb94434f8cf44ac1a44468e29fe5fcb36bbfc5bac26c217cf7e2ea1 +12000000 1351333a625060f4cc14948dbe6ca288f417c719fd98b67d818a570393769c8c +12500000 a93c52858f97a203c3250eb31d9711a45895da67361826d98bc5073fb2855ac5 +13000000 bbbd567abf70bf785861ed01f1a79d2e3e048ee9a9ec6deb72075f62b4934087 +13500000 0856098a348293152b266db581e24186805d7b0df4584b0f56a09deb29c80207 +14000000 14be32e1238f163c7eeab6134dc149b2f8511f0698080d64adfce8be5e513b41 +14500000 aede25f417ed22bbc356f580e4ef1d2477d6489b7a2fa3ec9b72a324dab64820 +15000000 f8c47019a601d21b111910949466750192910f642b4cbf5e047dc5ffeeb45a09 +15500000 813d6bb82edf809a90ed9e514505870bbfe8209adf6726114ede7cd012d63aac +16000000 57558cc6ebe087624f0e9bdf9745b63db16367fb8101d902642af2966818e09d +16500000 723483f6f7b527c7408aaaf5e98b2dae9a563dc0d270526001fb2f6e81ab2c1f +17000000 ce8c5e1fdd52f9d130015c8c62b2c55f240bb0c8593c6097cc931aec23d8876b +17500000 a8b8689b513bd5974aceaa54745d6670f3efc28dddc04dacae24ac786b02b3fc +18000000 46d128dc1f74bdef2b83c6ca2777f4b4ec667f7fb3a5cfb664ec12663d72115b +18500000 27391557a81a9bef45fc5cd01b06cb2e3f9b5bd134fd88104a6ed449be93cb7a +19000000 6c9f46134282165cee97ae375a8892abc7e662017ac78deeb83c7dfb47778c2e +19500000 d4eb04cc9e4b862e54016a75d4c9e04e1039d8c9bcade79080be78a6c1288205 +20000000 70760d341a42450da0058f286914bf196417982403a8b2ed07a545305e29f4ae +20500000 b852b4e4b03a53eef78d11e9b05f51a4b51c37686332015e5aeeb4e49e3adba1 +21000000 e8be6a3c07f4f131a7670a5cdd88ad822decf6b273e468cfef361ce7aa3e4cec +21500000 5bcce5527be4113847e5db1e10d317e431ecd9f3d956be80914811fec27b422d +22000000 6227b3239da72e1155667a14a70ae409724652cbb5676b84faa6e9d996868aeb +22500000 4dc1e720f791f26baaf3b578496e5bb8d8603af268cca208110b31897e9e7972 +23000000 4a577885653ab5ce6fb8bbcee7e7882cffe87ca5b328f66a8b2b608d0c7cca82 +23500000 e16834fcae528e6086c31ee3a0795b3e201435763d3261abe63a3ad329ccef30 +24000000 15297140dcd3367b71e334d8717734b4a6e0a5827090df9d56c41e6c54427338 +24500000 415ee05925617333f30a730fd0c749e403941d348993766cbcd74c397d569a47 +25000000 3004033571b2991f27a9591c0b39399eec97e6e9aee59145eb07fb378258cfbb +25500000 957b69e6a9cdd54dd28ac9bb331a15af1b5cf7066bec0ba49d4e85996cf655df +26000000 3e6c016496a74b65823fa8e85a27b0f92943b121308076d9a11a88733fead94f +26500000 764c7b65103b1f7f395f45cdf4fefd49f46c0eff8a53acac73dd988e18de3f99 +27000000 20e77ba184cfac4d809fda7b721ea8fec1deff676e7b1bffdcfa68a3e82f1940 +27500000 f9fb998d299ded8b4116140201de81be51c131667f638caa4fc3bbf6d7a6effb +28000000 7104d178fbf3062d0a893ac7ff33210719c074910c0be23fcbe66cf16b0e7739 +28500000 ac5eb263aeaabb607e22c0950b66ea6504960509c337bdd42b53db706a41ee5b +29000000 c9f7168b6ba8086909cece288ff29421f53a007814920fe88bdb6d1acbe2de6d +29500000 8a219654d33e5823331e9003478e2b0d371ed87c80ebcf6aa473b7b89b541205 +29868031 94467345fa2ad56dc3e6091d30475c1e69bab48c67ab875029a45ec15e407563 diff --git a/src/dev/odyssey/testdata/replay/perfly.hashes b/src/dev/odyssey/testdata/replay/perfly.hashes new file mode 100644 index 000000000..b646b39f3 --- /dev/null +++ b/src/dev/odyssey/testdata/replay/perfly.hashes @@ -0,0 +1,18 @@ +# Checkpoints of perfly.rec (record index, blake3 of the planes), every 500000 records. +# 7528447 records, 5350 DMA reads. +500000 81b519473d4983298b21f195b8c321b692d8965ec09f7818cda255d00801b189 +1000000 e1f8c2393f48045250084c5b21df8bb04d105831a85986668d7f28741d910ab0 +1500000 93b539256340f46f702ad66088c1cd49b6a0ed3ebd5a0cdf39aa18415a2349fb +2000000 03bccc50ff394a23dc9b68673c5e36632c64c9c4910ffcd9d1206743e8873e57 +2500000 2008509b8bd6543d22e8db07af890d54bc05f07cf18caa0f84952945a01b4862 +3000000 4387a26d537c9cb48c5e3bf47deab49a2ba8261e32b22c24daf700582544785a +3500000 11026d439474edb7027692f76c6b3b50a09c8db1b496eb2cc96c524d7856d5e6 +4000000 c400e024f21388f97548f867882f443ff8af54c47389d57055b26115e38a0a3a +4500000 c7556584039ba0c372432de81fccf852c9c7d55b48c42c2338dd0c9c776ae2bc +5000000 598a4c08a8ef2fd4126b0a5529740849b7b98e2744ca9786b7ed7a3348dfa1db +5500000 6f1ddccf6f83157a36225fb189720f5c788437290229e3a76e003ecf2e1afad9 +6000000 1c2b1201e77e350820a362f1a2ac872afa3fc3e400a3ffee61290c15b89b5003 +6500000 c862ea206a1584f5a2e261eee755e42df3093b01fc1fdf5672d02923f4ec3aac +7000000 9e8a6f7ee5b6ed52cf7e3bf59443cfe237f527525a9894efb9bc3a81a0363dda +7500000 0630b9aba6fdea2cca4a285b529584752d257ca05926f3188c4ddb3b3b6cc271 +7528447 5d911dfe1ad12c3d750994c45c82206908cc11859033deb2351c4a654eb0fa48 diff --git a/src/dev/odyssey/tests/catalogue.rs b/src/dev/odyssey/tests/catalogue.rs new file mode 100644 index 000000000..d178fb7ae --- /dev/null +++ b/src/dev/odyssey/tests/catalogue.rs @@ -0,0 +1,224 @@ +//! The board catalogue and the Octane attach: what each marketed VPro +//! answers as, and how the widget windows reach it. + +use crate::dev::odyssey::Odyssey; +use crate::dev::odyssey::consts::{backend, reg, ODSY_CONTROL_WIDGET_MASK}; +use crate::dev::odyssey::tests::{board, cmd, reg_read, select, VPRO_WIDGET}; +use crate::ip30::{widget_large_base, widget_small_base}; +use std::sync::Arc; +use super::*; + +#[test] +fn the_board_answers_with_its_identity_and_widget_number() { + let (mut s, _) = board("v12"); + assert_eq!(s.mmio_read(IDENTITY, 4) as u32, IDENTITY_DEFAULT); + assert_eq!(s.mmio_read(CONTROL, 4) as u32 & ODSY_CONTROL_WIDGET_MASK, VPRO_WIDGET); +} + +#[test] +fn each_marketed_profile_keeps_its_generation_and_capacity_class() { + // (product, revision, memory size code) + let cases = [("v6", 2u32, 0u32), ("v8", 2, 2), ("v10", 0xa, 0), ("v12", 0xa, 2)]; + + for (name, revision, size_code) in cases { + let (mut s, rec) = board(name); + let identity = s.mmio_read(IDENTITY, 4) as u32; + let memory = s.mmio_read(MEMORY_CONFIG, 4) as u32; + + assert_eq!(identity >> 28, revision, "{name}"); + assert_eq!(identity & 0x0fff_ffff, IDENTITY_DEFAULT & 0x0fff_ffff, "{name}"); + assert_eq!((memory >> 30) & 3, size_code, "{name}"); + assert_eq!(s.mmio_read(CONTROL, 4) as u32 & ODSY_CONTROL_WIDGET_MASK, VPRO_WIDGET); + + s.mmio_write(ROUTE_DEST_UPPER, HEART_WIDGET << 25, 4); + s.mmio_write(ROUTE_DEST_LOWER, HEART_PACKET, 4); + s.mmio_write(ROUTE_VECTORS, TEST_VECTOR as u64, 4); + s.mmio_write(INTR_ENABLE_SET, INTR_FIFO_LOW_WATER, 4); + assert!(rec.level(TEST_VECTOR), "{name}: vector raised"); + s.mmio_write(INTR_CLEAR, INTR_FIFO_LOW_WATER, 4); + assert!(!rec.level(TEST_VECTOR), "{name}: vector lowered"); + } +} + + +mod widget { + //! The Octane VPro integration checks: + //! the board answers through both widget-11 windows, the Crossbow + //! carries its link only when it is fitted, and a routed interrupt + //! sets and clears its vector in HEART's pending register. + use super::*; + use crate::dev::heart::{host_clock, Heart, HeartConfig, PIU_BASE}; + use crate::dev::heart_mem::HeartRam; + use crate::dev::xbow::Xbow; + + const WIDGET: u8 = 11; + const VECTOR: u32 = 18; + + fn board() -> (Arc, Arc) { + let heart = Arc::new(Heart::new(HeartConfig::octane([128 << 20, 0, 0, 0]), host_clock())); + let ram = Arc::new(HeartRam::try_new([128, 0, 0, 0]).expect("ram")); + let v = Odyssey::new(WIDGET, "v12", heart.clone(), ram.dma_view(), crate::disp::StatusSource::detached()).expect("board"); + (heart, v) + } + + fn pending(heart: &Heart) -> u64 { + heart.read64(PIU_BASE + 0x10030).data + } + + #[test] + fn the_board_answers_through_both_widget_windows() { + let (_heart, v) = board(); + for base in [widget_small_base(WIDGET), widget_large_base(WIDGET)] { + assert_eq!(v.read32(base + 0x4).data, 0xac01_3047); + assert_eq!(v.read32(base + 0x24).data & 0xf, WIDGET as u32); + } + } + + #[test] + fn the_crossbow_carries_the_graphics_link_only_when_fitted() { + let fitted = Xbow::new(4, (1 << 8) | (1 << 15) | (1 << WIDGET)); + let empty = Xbow::new(4, (1 << 8) | (1 << 15)); + assert!(fitted.carries(WIDGET)); + assert!(!empty.carries(WIDGET)); + } + + /// Starting the device starts the board's thread and the display + /// thread; stopping it joins both and finishes the work still + /// queued, so nothing is left behind a stopped machine. + /// + /// QEMU: command-work-stop-and-reset. + #[test] + fn stopping_the_device_finishes_the_work_the_board_thread_had_queued() { + use crate::traits::Device; + let (_heart, v) = board(); + assert!(!v.is_running()); + + v.start(); + v.start(); + assert!(v.is_running(), "a second start changes nothing"); + v.stop(); + + assert!(!v.is_running()); + assert!(v.state.fifo.thread.lock().is_none()); + assert!(!v.state.fifo.pending()); + } + + #[test] + fn a_routed_interrupt_sets_and_clears_its_heart_vector() { + let (heart, v) = board(); + let b = widget_small_base(WIDGET); + v.write32(b + 0x1044, 8 << 25); + v.write32(b + 0x104c, 0x80); + v.write32(b + 0x1054, VECTOR); + assert_eq!(pending(&heart) & 1 << VECTOR, 0); + // The FIFO is empty, so its low-water flag is already pending. + v.write32(b + 0x1034, 0x200); + assert_ne!(pending(&heart) & 1 << VECTOR, 0); + v.write32(b + 0x101c, 0x200); + assert_eq!(pending(&heart) & 1 << VECTOR, 0); + } +} + + +/// Each marketed board presents its own identity and memory word. The +/// kernel turns widget revision 1-3 into the first part generation and +/// 9-15 into the next; the 128 MB boards report four banks and the 32 MB +/// boards two. +/// +/// QEMU: profile-selects-identity-and-memory. +#[test] +fn each_profile_presents_its_own_identity_and_memory_word() { + for (product, identity, memory) in [ + ("v12", IDENTITY_DEFAULT, 0xa100_0000), + ("v10", IDENTITY_DEFAULT, 0x0100_0000), + ("v8", 0x2c01_3047, 0xa100_0000), + ("v6", 0x2c01_3047, 0x0100_0000), + ] { + let (mut s, _) = board(product); + assert_eq!(reg_read(&mut s, IDENTITY), identity, "{product}"); + assert_eq!(reg_read(&mut s, MEMORY_CONFIG), memory, "{product}"); + } +} + +/// Bits 29 (four banks) and 28 (CAS latency 2) of the memory word, +/// decomposed from the V12's measured 0xa1000000: four banks, latency 3, +/// and the constant copy field 1 in bits 27:24. +/// +/// QEMU: v12-profile-memory-word-bits. +#[test] +fn the_v12_memory_word_decodes_to_four_banks_and_latency_three() { + let (mut s, _) = board("v12"); + let word = reg_read(&mut s, MEMORY_CONFIG); + + assert_eq!(word, 0xa100_0000); + assert_eq!(word & 1 << 29, 1 << 29); + assert_eq!(word & 1 << 28, 0); + assert_eq!((word >> 24) & 0xf, 1); +} + +/// gfxinfo's "PB&J version" is bits 2:0 of 0x23380c. Captured V6, V10 and +/// V12 boards all print 1, so every profile answers 1, and a write changes +/// nothing. +/// +/// QEMU: backend-version-is-pinned. +#[test] +fn every_profile_answers_back_end_version_one_and_ignores_a_write() { + for product in ["v12", "v10", "v8", "v6"] { + let (mut s, _) = board(product); + assert_eq!(s.mmio_read(backend::VERSION, 4), 1, "{product}"); + s.mmio_write(backend::VERSION, 0, 4); + assert_eq!(s.mmio_read(backend::VERSION, 4), 1, "{product}"); + } +} + +/// A profile's board memory is a reported capacity, not storage: objects +/// come from one fixed pool. A destination base at or beyond what a 32 MiB +/// profile reports is a guest bug against the board it was told it has, so +/// the selection is dropped and noted, and the board keeps answering. The +/// first value out of range is 0x200000 in 16-byte units. +/// +/// QEMU: board-memory-bound-is-a-guest-error. +#[test] +fn a_destination_beyond_the_reported_memory_is_dropped() { + let (mut s, _) = board("v10"); + cmd(&mut s, select(reg::DEST_SELECT)); + for operand in [0x20_0000, 16, 0, 0] { + cmd(&mut s, operand); + } + assert!(s.diag.board_oob_reported); + assert_eq!(s.objects.count, 0, "no object was made for the dropped base"); + assert_eq!(reg_read(&mut s, IDENTITY), IDENTITY_DEFAULT); + + // The same base is in range on a 128 MiB board. + let (mut s, _) = board("v12"); + cmd(&mut s, select(reg::DEST_SELECT)); + for operand in [0x20_0000, 16, 0, 0] { + cmd(&mut s, operand); + } + assert!(!s.diag.board_oob_reported); +} + +/// Each name the configuration accepts selects a board, VPro boards are +/// offered on the Octane only, and the PROM revision check the upgrade kit +/// calls for is a property of the board. +/// +/// QEMU: octane-vpro-test absent/widget-identity (the machine offers the +/// board only where its Crossbow and supply allow it). +#[test] +fn vpro_boards_are_offered_on_the_octane_only() { + use crate::config::{GraphicsBoard, MachineProfile}; + + for (name, board) in [ + ("v6", GraphicsBoard::V6), + ("vpro:v8", GraphicsBoard::V8), + ("vpro_v10", GraphicsBoard::V10), + ("V12", GraphicsBoard::V12), + ] { + assert_eq!(name.parse::().unwrap(), board, "{name}"); + assert!(board.is_vpro()); + assert!(board.supports(MachineProfile::OctaneIp30)); + assert!(!board.supports(MachineProfile::Indigo2Ip22)); + } + assert!(!GraphicsBoard::SolidImpact.is_vpro()); + assert!("v14".parse::().is_err()); +} diff --git a/src/dev/odyssey/tests/display.rs b/src/dev/odyssey/tests/display.rs new file mode 100644 index 000000000..b746324ba --- /dev/null +++ b/src/dev/odyssey/tests/display.rs @@ -0,0 +1,880 @@ +//! What the display shows: swaps and the displayed-buffer bank, the two +//! colour buffers, colour resolved at scanout, the cursor, the channel +//! size and what a reset clears. + +use crate::dev::odyssey::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use crate::dev::odyssey::consts::{ + backend, cursor, dispmode, gl_cmd, reg, swap, ODSY_CHANNEL_HEIGHT, ODSY_DID_MAIN_SHIFT, + ODSY_DISPLAYED_BUFFER, ODSY_USER_CMD_FIFO, +}; +use crate::dev::odyssey::reader::{DisplayRegs, SharedRegs}; +use crate::dev::odyssey::scanout::{DirtySite, DirtyStats}; +use crate::dev::odyssey::tests::{ + board, cmd, cmd64, pixel, pixel_write, reg_read, reg_write, route_to_heart, select, + RETRACE_VECTOR, SOFTSYNC_VECTOR, VPRO_WIDGET, +}; +use super::*; + +const WIDTH: i32 = ODSY_WIDTH as i32; +/// The display mode of a drawable showing 24-bit RGB through the gamma +/// ramp, and of one showing colour map group 17 (the X server's modes). +const DISPMODE_RGB: u32 = 0x0052_15a6; +const DISPMODE_GROUP17: u32 = 0x004b_5f10; +const LUT_RED: u32 = 0x3ff0_0000; +const LUT_BLUE: u32 = 0x0000_03ff; + +fn backend(s: &mut Board, word: u32) { + s.mmio_write(backend::SELECT, word as u64, 4); +} + +/// A record of one back-end word at `index`. +fn backend_entry(s: &mut Board, index: u32, word: u32) { + backend(s, 0x3000_0000 | index << 14 | 1); + backend(s, word); +} + +fn dispmode(s: &mut Board, did: u32, mode: u32) { + backend_entry(s, dispmode::BASE + did, mode); +} + +/// What a guest loads before any RGB pixel is visible: the identity gamma +/// ramp at lookup-RAM 0x1a00 and drawable `did` in an RGB mode. Out of +/// reset the lookup RAM is zero and the screen black. +fn display_identity(s: &mut Board, did: u32) { + dispmode(s, did, DISPMODE_RGB); + backend(s, 0x3000_0000 | 0x1a00 << 14 | 0x600); + for i in 0..0x600u32 { + let v = if i < 0x200 { + i >> 2 + } else if i < 0x400 { + ((i - 0x200) >> 1) + 0x80 + } else { + (i - 0x300) + 0x100 + }; + backend(s, v << 20 | v << 10 | v); + } +} + +/// The display's pixel at (x, y) as 0x00RRGGBB, after the dirty lines +/// have been resolved. +fn shown(s: &mut Board, x: i32, y: i32) -> u32 { + s.scanout_update(); + s.reader.frame[(y * s.reader.frame_width as i32 + x) as usize] & 0xff_ffff +} + +/// Put main drawable `id` (bits 7:3 of a pixel's id byte) on every pixel of the box [x0, x1) x [y0, y1). +fn drawable(s: &mut Board, id: u8, x0: i32, y0: i32, x1: i32, y1: i32) { + for y in y0..y1 { + for x in x0..x1 { + s.planes.did[(y * WIDTH + x) as usize] = id << ODSY_DID_MAIN_SHIFT; + } + } + s.scanout.invalidate(y0, y1 - y0); +} + +/// Colour index `index` on every pixel of the box, as the plane keeps it. +fn index_box(s: &mut Board, index: u32, x0: i32, y0: i32, x1: i32, y1: i32) { + for y in y0..y1 { + for x in x0..x1 { + pixel_write(s, x, y, index); + } + } +} + +/// One solid RGB QUADS fill of the half-open box [x0, x1) x [y0, y1). +#[allow(clippy::too_many_arguments, reason = "a colour and a box")] +fn rgb_fill(s: &mut Board, r: u32, g: u32, b: u32, x0: u32, y0: u32, x1: u32, y1: u32) { + cmd(s, select(reg::GL_BEGIN)); + cmd(s, gl_cmd::QUADS); + cmd(s, gl_cmd::COLOR_3UB); + for c in [r, g, b] { + cmd(s, c); + } + for (x, y) in [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] { + cmd(s, gl_cmd::VERTEX_2I); + cmd(s, x); + cmd(s, y); + } + cmd(s, select(reg::GL_END)); +} + +fn draw_mask(s: &mut Board, mask: u32) { + cmd(s, select(reg::DRAW_MASK)); + cmd(s, mask); +} + +/// The vertical retrace, as the scanout's timer fires it. +fn retrace(s: &mut Board) { + s.retrace(); +} + +/// The retrace interrupt carries the retrace byte's vector, not the +/// softsync byte's, and fires at every retrace once routing is programmed +/// whether or not a swap is armed: the driver's vblank counters advance +/// only in that handler. Before routing, a retrace sends nothing. +/// +/// QEMU: swap-retrace-vector. +#[test] +fn a_retrace_interrupts_on_its_own_vector_once_routed() { + let (mut s, rec) = board("v12"); + retrace(&mut s); + assert!(rec.silent()); + + route_to_heart(&mut s); + retrace(&mut s); + assert_eq!(rec.events(RETRACE_VECTOR), [true]); + assert!(rec.events(SOFTSYNC_VECTOR).is_empty()); + retrace(&mut s); + assert_eq!(rec.events(RETRACE_VECTOR), [true, true], "every retrace, armed or not"); +} + +/// The arm is a toggle mask latched at the retrace: the window-24 bit +/// flips bit 24 of the displayed-buffer status, a second swap flips it +/// back, and a two-window mask flips both. A retrace with nothing armed +/// flips nothing, and the buffer-select doublewords the driver pushes +/// ahead of the arm neither flip a buffer nor raise an interrupt. +/// +/// QEMU: swap-arm-toggles-displayed-buffer. +#[test] +fn an_armed_swap_toggles_the_displayed_buffer_at_the_retrace() { + const ARM_BIT: u32 = 0x0100_0000; + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0); + + cmd64(&mut s, swap::SYNC_A, 0x8000_2000); + cmd64(&mut s, swap::SYNC_B, 0x8000_2000); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0); + assert!(rec.silent()); + assert_eq!(s.diag.reported_cmd_count, 0); + + reg_write(&mut s, swap::ARM, ARM_BIT); + assert_eq!(reg_read(&mut s, swap::ARM), ARM_BIT); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0, "nothing flips before the retrace"); + retrace(&mut s); + assert_eq!(reg_read(&mut s, swap::ARM), 0); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), ARM_BIT); + assert!(rec.level(RETRACE_VECTOR)); + + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), ARM_BIT, "an idle retrace flips nothing"); + + reg_write(&mut s, swap::ARM, ARM_BIT); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0); + + reg_write(&mut s, swap::ARM, 0x5); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0x5); +} + +/// Arm writes between two retraces accumulate: separate windows issued +/// before one retrace all flip there, and the interrupt handler's re-arm +/// of a window still pending does not cancel the others. +/// +/// QEMU: swap-arm-accumulates. +#[test] +fn arm_writes_between_two_retraces_accumulate() { + let (mut s, _) = board("v12"); + route_to_heart(&mut s); + + reg_write(&mut s, swap::ARM, 0x1); + reg_write(&mut s, swap::ARM, 0x0100_0000); + assert_eq!(reg_read(&mut s, swap::ARM), 0x0100_0001); + reg_write(&mut s, swap::ARM, 0x0100_0000); + assert_eq!(reg_read(&mut s, swap::ARM), 0x0100_0001, "a re-arm leaves the mask as it was"); + + retrace(&mut s); + assert_eq!(reg_read(&mut s, swap::ARM), 0); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0x0100_0001); +} + +/// For the windows whose swap is due the kernel writes those that must +/// show buffer 1 to the set register and the rest to the companion. The +/// selects latch at the next retrace and not before; the last write for a +/// window before the retrace wins. +/// +/// QEMU: swap-set-and-clear-at-retrace. +#[test] +fn set_and_clear_selects_latch_at_the_next_retrace() { + let (mut s, _) = board("v12"); + + reg_write(&mut s, swap::ARM, 0x3); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0x3); + + reg_write(&mut s, swap::CONTROL_COMPANION, 0xffff_ffff); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0x3); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0); + + reg_write(&mut s, swap::CONTROL, 0x8000_0010); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0x8000_0010); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0x8000_0010, "an idle retrace keeps the selects"); + + reg_write(&mut s, swap::CONTROL_COMPANION, 0x10); + reg_write(&mut s, swap::CONTROL, 0x11); + reg_write(&mut s, swap::CONTROL_COMPANION, 0x8000_0001); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 0x10); +} + +/// The drawable-register block as a context switch sends it: eight +/// doublewords of a word and its one operand, 0x608 to 0x60f. Each takes +/// its operand, so none is parsed as a command and a fill sent after them +/// still draws. +/// +/// QEMU: command-fifo-drawable-words. +#[test] +fn the_drawable_register_block_takes_its_operands() { + let (mut s, _) = board("v12"); + for (word, operand) in [ + (0x608, 0xff00_ff00), + (0x60a, 0x0268_0008), + (0x609, 0xff00_ff00), + (0x60b, 0x0268_0008), + (0x60c, 0x004c_aef3), + (0x60d, 0x004b_6a8b), + (0x60e, 0), + (0x60f, 0), + ] { + cmd64(&mut s, word, operand); + } + rgb_fill(&mut s, 0, 0, 0xff, 64, 100, 80, 110); + + assert_eq!(pixel(&s, 0, 64, 100), 0x0000_ffff); + assert_eq!(s.diag.reported_cmd_count, 0); +} + +/// The bound drawing (0x608) and reading (0x609) drawable words are kept +/// independently for the readback path, and neither moves drawing: their +/// decoding is not known, so the drawing mask alone selects the buffer. +/// Reset clears both. +/// +/// QEMU: buffers-read-and-draw-words-are-independent. +#[test] +fn the_draw_and_read_drawable_words_are_kept_apart() { + let (mut s, _) = board("v12"); + + cmd64(&mut s, swap::SYNC_A, 0x8000_2000); + assert_eq!((s.draw_word, s.read_word), (0x8000_2000, 0)); + cmd64(&mut s, swap::SYNC_B, 0x0000_2000); + assert_eq!((s.draw_word, s.read_word), (0x8000_2000, 0x0000_2000)); + + rgb_fill(&mut s, 0x12, 0x34, 0x56, 0, 0, 1, 1); + assert_eq!(pixel(&s, 0, 0, 0), 0x1234_56ff); + assert_eq!(pixel(&s, 1, 0, 0), 0, "the words do not select the buffer"); + + s.reset(); + assert_eq!((s.draw_word, s.read_word), (0, 0)); +} + +/// The same box drawn red into buffer 0 and blue into buffer 1 holds both +/// colours at once. The window shows buffer 0 until its swap latches at a +/// retrace, then buffer 1. A mask naming both buffers writes both, and a +/// zero mask writes neither. +/// +/// QEMU: buffers-hold-opposite-colours. +#[test] +fn the_two_colour_buffers_hold_opposite_colours() { + let (mut s, _) = board("v12"); + display_identity(&mut s, 0); + + rgb_fill(&mut s, 0xff, 0, 0, 100, 100, 110, 110); + draw_mask(&mut s, 0x4); + rgb_fill(&mut s, 0, 0, 0xff, 100, 100, 110, 110); + assert_eq!(pixel(&s, 0, 105, 105), 0xff00_00ff); + assert_eq!(pixel(&s, 1, 105, 105), 0x0000_ffff); + assert_eq!(shown(&mut s, 105, 105), 0xff0000); + + reg_write(&mut s, swap::ARM, 0x1); + assert_eq!(shown(&mut s, 105, 105), 0xff0000, "an armed swap shows nothing yet"); + retrace(&mut s); + assert_eq!(shown(&mut s, 105, 105), 0x0000ff); + + draw_mask(&mut s, 0x5); + rgb_fill(&mut s, 0, 0xff, 0, 100, 100, 102, 102); + assert_eq!(pixel(&s, 0, 100, 100), 0x00ff_00ff); + assert_eq!(pixel(&s, 1, 100, 100), 0x00ff_00ff); + + draw_mask(&mut s, 0); + rgb_fill(&mut s, 0xff, 0xff, 0xff, 100, 100, 110, 110); + assert_eq!(pixel(&s, 0, 105, 105), 0xff00_00ff); + assert_eq!(pixel(&s, 1, 105, 105), 0x0000_ffff); +} + +/// The display copies a frame's lines aside, with no lock, and resolves the +/// copy while the raster unit goes on: what is drawn after the copy shows +/// only in the next frame, never part-way through this one. +#[test] +fn a_frame_resolves_the_lines_it_copied_while_the_raster_unit_draws_on() { + let (mut s, _) = board("v12"); + display_identity(&mut s, 0); + rgb_fill(&mut s, 0xff, 0, 0, 100, 100, 110, 110); + s.scanout_update(); + + s.scanout.invalidate_all(); + s.scanout_stage(); + rgb_fill(&mut s, 0, 0, 0xff, 100, 100, 110, 110); + assert!(s.scanout_resolve().is_some()); + let at = (105 * s.reader.frame_width + 105) as usize; + assert_eq!(s.reader.frame[at] & 0xff_ffff, 0xff0000, "the frame shows the copy"); + assert_eq!(shown(&mut s, 105, 105), 0x0000ff, "and the next frame the new drawing"); +} + +/// A double-buffered client draws each frame in two halves into the buffer +/// the window does not show, arms the swap, and only the retrace shows it. +/// The kernel exchanges the mask's pairs as the drawable's buffer state +/// changes, so the client's back buffer alternates. Neither a half-drawn +/// frame nor an armed swap reaches the display before the retrace, over +/// four swaps. +/// +/// QEMU: buffers-swap-shows-only-complete-frames. +#[test] +fn a_swap_shows_only_complete_frames() { + let frames = [(0xff, 0, 0), (0, 0xff, 0), (0, 0, 0xff), (0xff, 0xff, 0)]; + let (mut s, _) = board("v12"); + display_identity(&mut s, 0); + route_to_heart(&mut s); + let mut visible = 0u32; + + for (i, (r, g, b)) in frames.into_iter().enumerate() { + let back = if i & 1 != 0 { 0 } else { 1 }; + let colour = r << 16 | g << 8 | b; + draw_mask(&mut s, if back == 1 { 0x4 } else { 0x1 }); + rgb_fill(&mut s, r, g, b, 0, 0, 64, 32); + assert_eq!(shown(&mut s, 10, 10), visible, "frame {i}: first half"); + rgb_fill(&mut s, r, g, b, 0, 32, 64, 64); + assert_eq!(shown(&mut s, 10, 40), visible, "frame {i}: second half"); + + reg_write(&mut s, swap::ARM, 0x1); + assert_eq!(shown(&mut s, 10, 10), visible, "frame {i}: armed"); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), 1 - back); + retrace(&mut s); + assert_eq!(reg_read(&mut s, ODSY_DISPLAYED_BUFFER), back); + assert_eq!(shown(&mut s, 10, 10), colour, "frame {i}: swapped"); + assert_eq!(shown(&mut s, 10, 40), colour); + + retrace(&mut s); + assert_eq!(shown(&mut s, 10, 40), colour, "an idle retrace keeps the frame"); + visible = colour; + } +} + +/// The colour plane keeps the index or RGB the guest drew; what the +/// display shows follows the pixel's drawable, its display mode and the +/// lookup RAM as they stand now. The X server's blank zeroes colour map +/// group 17; a pixel painted during the blank must come back when the +/// unblank reloads it, with no repaint. +/// +/// QEMU: scanout-wipe-repaint-reload. +#[test] +fn colour_is_resolved_at_scanout_so_a_reloaded_map_brings_pixels_back() { + let (mut s, _) = board("v12"); + dispmode(&mut s, 1, DISPMODE_GROUP17); + backend_entry(&mut s, 0x1105, LUT_RED); + drawable(&mut s, 1, 100, 100, 116, 116); + index_box(&mut s, 5, 100, 100, 116, 116); + assert_eq!(shown(&mut s, 108, 108), 0xff0000); + + // The blank: group 17's 256 entries zeroed, no repaint. + backend(&mut s, 0x3000_0000 | 0x1100 << 14 | 0x100); + for _ in 0..0x100 { + backend(&mut s, 0); + } + assert_eq!(shown(&mut s, 108, 108), 0); + + // Painted during the blank: black too. + index_box(&mut s, 5, 100, 100, 116, 116); + assert_eq!(shown(&mut s, 108, 108), 0); + + // The unblank reloads the entry: red again. + backend_entry(&mut s, 0x1105, LUT_RED); + assert_eq!(shown(&mut s, 108, 108), 0xff0000); + assert_eq!(shown(&mut s, 100, 115), 0xff0000); +} + +/// The X server's colour map install rewrites the drawable's display mode; +/// its pixels change colour with no repaint and the plane keeps the index. +/// +/// QEMU: scanout-display-mode-recolours. +#[test] +fn rewriting_a_display_mode_recolours_its_pixels() { + const GROUP18_CI: u32 = 0x004b_5f20; + let (mut s, _) = board("v12"); + dispmode(&mut s, 1, DISPMODE_GROUP17); + dispmode(&mut s, 2, GROUP18_CI); + backend_entry(&mut s, 0x1105, LUT_RED); + backend_entry(&mut s, 0x1205, LUT_BLUE); + drawable(&mut s, 1, 100, 100, 116, 116); + drawable(&mut s, 2, 200, 100, 216, 116); + index_box(&mut s, 5, 100, 100, 116, 116); + index_box(&mut s, 5, 200, 100, 216, 116); + assert_eq!(shown(&mut s, 108, 108), 0xff0000); + assert_eq!(shown(&mut s, 208, 108), 0x0000ff); + + dispmode(&mut s, 2, DISPMODE_GROUP17); + + assert_eq!(shown(&mut s, 208, 108), 0xff0000); + assert_eq!(pixel(&s, 0, 208, 108), 5); +} + +/// An RGB pixel passes through the gamma table at 0x1a00, which the RGB +/// display modes name as their group: zeroing the table blackens it, +/// reloading brings it back. +/// +/// QEMU: scanout-rgb-through-gamma. +#[test] +fn an_rgb_pixel_passes_through_the_gamma_table() { + let (mut s, _) = board("v12"); + display_identity(&mut s, 8); + drawable(&mut s, 8, 100, 100, 116, 116); + rgb_fill(&mut s, 0x11, 0x22, 0x33, 100, 100, 116, 116); + assert_eq!(pixel(&s, 0, 108, 108), 0x1122_33ff); + assert_eq!(shown(&mut s, 108, 108), 0x112233); + + backend(&mut s, 0x3000_0000 | 0x1a00 << 14 | 0x600); + for _ in 0..0x600 { + backend(&mut s, 0); + } + assert_eq!(shown(&mut s, 108, 108), 0); + + display_identity(&mut s, 8); + assert_eq!(shown(&mut s, 108, 108), 0x112233); +} + +/// Pixels painted first and the drawable and colour map programmed after +/// still show their colour. +/// +/// QEMU: scanout-paint-order. +#[test] +fn pixels_painted_before_the_map_is_programmed_show_it_afterwards() { + let (mut s, _) = board("v12"); + drawable(&mut s, 1, 100, 100, 116, 116); + index_box(&mut s, 5, 100, 100, 116, 116); + assert_eq!(shown(&mut s, 108, 108), 0); + + dispmode(&mut s, 1, DISPMODE_GROUP17); + backend_entry(&mut s, 0x1105, LUT_RED); + + assert_eq!(shown(&mut s, 108, 108), 0xff0000); +} + +/// A raw 0xRRGGBBAA written to the colour plane reaches the display with +/// the alpha byte dropped, through an RGB mode on the screen's drawable. +/// An untouched pixel stays at its post-reset value. +/// +/// QEMU: framebuffer-pixel-reaches-scanout. +#[test] +fn a_plane_pixel_reaches_the_display_without_its_alpha() { + let (mut s, _) = board("v12"); + display_identity(&mut s, 0); + assert_eq!(pixel(&s, 0, 5, 5), 0); + assert_eq!(shown(&mut s, 5, 5), 0); + + pixel_write(&mut s, 5, 5, 0x1122_3344); + + assert_eq!(pixel(&s, 0, 5, 5), 0x1122_3344); + assert_eq!(shown(&mut s, 5, 5), 0x112233); + assert_eq!(shown(&mut s, 6, 5), 0); +} + +/// The display channel's active size: the kernel and the X server write +/// the selected format's pixels and lines to back-end entries 0x2802 and +/// 0x2803. The scanout presents the latest nonzero size, kept within the +/// modelled planes, zeros (a format change) leave the picture as it was, +/// and a reset returns it to 1280x1024. +/// +/// QEMU: display-channel-size-sets-scanout. +#[test] +fn the_display_channel_size_sets_the_scanout_mode() { + let (mut s, _) = board("v12"); + let size = |s: &Board| (s.scanout.mode.width, s.scanout.mode.height); + assert_eq!(size(&s), (1280, 1024)); + + backend(&mut s, 0x3000_0000 | 0x2802 << 14 | 5); + for w in [1280, 720, 0, 0, 0] { + backend(&mut s, w); + } + assert_eq!(size(&s), (1280, 720)); + pixel_write(&mut s, 5, 5, 0x1122_3344); + display_identity(&mut s, 0); + assert_eq!(shown(&mut s, 5, 5), 0x112233); + + // The zeros of a format change leave the picture as it was. + backend(&mut s, 0x3000_0000 | 0x2802 << 14 | 2); + backend(&mut s, 0); + backend(&mut s, 0); + assert_eq!(size(&s), (1280, 720)); + + // A narrower channel presents the left of each line. + backend(&mut s, 0x3000_0000 | 0x2802 << 14 | 2); + backend(&mut s, 1024); + backend(&mut s, 768); + assert_eq!(size(&s), (1024, 768)); + assert_eq!(shown(&mut s, 5, 5), 0x112233); + + // Larger than the modelled planes: kept to them. + backend(&mut s, 0x3000_0000 | 0x2802 << 14 | 2); + backend(&mut s, 1600); + backend(&mut s, 1200); + assert_eq!(size(&s), (1280, 1024)); + + backend_entry(&mut s, ODSY_CHANNEL_HEIGHT, 720); + s.reset(); + assert_eq!(size(&s), (1280, 1024)); +} + +/// A reset clears the framebuffer along with the registers. +/// +/// QEMU: reset-clears-the-framebuffer. +#[test] +fn a_reset_clears_the_framebuffer() { + let (mut s, _) = board("v12"); + pixel_write(&mut s, 5, 5, 0x1122_3344); + assert_eq!(pixel(&s, 0, 5, 5), 0x1122_3344); + + s.reset(); + + assert_eq!(pixel(&s, 0, 5, 5), 0); + assert_eq!(shown(&mut s, 5, 5), 0); +} + +/// A guest reset that lands with a primitive still open (a BEGIN with no +/// END) completes and leaves the board responsive. +/// +/// QEMU: reset-mid-primitive-does-not-hang. +#[test] +fn a_reset_with_a_primitive_open_leaves_the_board_responsive() { + let (mut s, _) = board("v12"); + for w in [select(reg::GL_BEGIN), gl_cmd::TRIANGLES, gl_cmd::VERTEX_2F, 0x4220_0000, 0x4220_0000] { + s.mmio_write(ODSY_USER_CMD_FIFO, w as u64, 4); + } + + s.reset(); + + assert_eq!(reg_read(&mut s, CONTROL), VPRO_WIDGET); + rgb_fill(&mut s, 1, 2, 3, 0, 0, 4, 4); + assert_eq!(pixel(&s, 0, 1, 1), 0x0102_03ff, "the next primitive draws"); +} + +// The hardware cursor. + +const CURSOR_BACKGROUND: u32 = 0x4060_80ff; +const WORD0_RGB: u32 = 0xff0000; +const WORD1_RGB: u32 = 0x00b100; +const WORD2_RGB: u32 = 0x0000ff; + +fn cursor_entry(s: &mut Board, index: u32, word: u32) { + backend_entry(s, cursor::BASE + index, word); +} + +fn cursor_readback(s: &mut Board, index: u32) -> u32 { + s.mmio_read(backend::READ_BASE + (((cursor::BASE + index) as u64) << 3), 4) as u32 +} + +/// An RGB screen with a known colour over the corner the cursor uses, the +/// test glyph, plane and colours, and the cursor at (x, y), still off. +fn cursor_load(s: &mut Board, x: u32, y: u32) { + display_identity(s, 0); + for py in 0..8 { + for px in 0..12 { + pixel_write(s, px, py, CURSOR_BACKGROUND); + } + } + backend(s, 0x3000_0000 | cursor::BASE << 14 | 128); + for i in 0..128 { + backend(s, if i == 0 { 0x1021 } else { 0 }); + } + backend(s, 0x3000_0000 | (cursor::BASE + cursor::PLANE) << 14 | 64); + for i in 0..64 { + backend(s, if i == 0 { 0x3f } else { 0 }); + } + backend(s, 0x3000_0000 | (cursor::BASE + cursor::COLOURS) << 14 | 15); + for w in [0x3ff << 20, 0x2c4 << 10, 0x3ff] { + backend(s, w); + } + for _ in 3..15 { + backend(s, 0); + } + cursor_entry(s, cursor::POSITION, (x & 0xffff) << 16 | (y & 0xffff)); +} + +/// Plane 3 shows glyph value k in colour word k and glyph 0 in word 0; +/// plane 0 is transparent whatever the glyph. The cursor is drawn after +/// colour resolution, and every entry reads back as written. +/// +/// QEMU: cursor/glyph-and-plane. +#[test] +fn the_cursor_draws_its_glyph_through_the_plane_over_the_picture() { + let (mut s, _) = board("v12"); + cursor_load(&mut s, 2, 3); + let bg = shown(&mut s, 11, 7); + assert_eq!(shown(&mut s, 2, 3), bg, "the cursor is off"); + + cursor_entry(&mut s, cursor::CONTROL, 1); + assert_eq!(shown(&mut s, 2, 3), WORD1_RGB); + assert_eq!(shown(&mut s, 3, 3), WORD2_RGB); + assert_eq!(shown(&mut s, 4, 3), WORD0_RGB); + // Glyph 1 and glyph 0 under plane 0 are transparent. + assert_eq!(shown(&mut s, 5, 3), bg); + assert_eq!(shown(&mut s, 6, 3), bg); + assert_eq!(shown(&mut s, 2, 4), bg); + assert_eq!(shown(&mut s, 2, 2), bg); + assert_eq!(shown(&mut s, 1, 3), bg); + assert_eq!(cursor_readback(&mut s, cursor::GLYPH), 0x1021); + assert_eq!(cursor_readback(&mut s, cursor::PLANE), 0x3f); + assert_eq!(cursor_readback(&mut s, cursor::COLOURS + 1), 0x2c4 << 10); + assert_eq!(cursor_readback(&mut s, cursor::POSITION), 0x0002_0003); + assert_eq!(cursor_readback(&mut s, cursor::CONTROL), 1); + + cursor_entry(&mut s, cursor::CONTROL, 0); + for x in 2..7 { + assert_eq!(shown(&mut s, x, 3), bg, "x {x}"); + } +} + +/// The position is the glyph's top-left pixel, read as signed 16-bit +/// components and clipped to the raster. A move repaints the old place. +/// +/// QEMU: cursor/moves-and-clips. +#[test] +fn the_cursor_position_is_signed_and_clipped_to_the_raster() { + let (mut s, _) = board("v12"); + cursor_load(&mut s, 2, 3); + let bg = shown(&mut s, 11, 7); + cursor_entry(&mut s, cursor::CONTROL, 1); + assert_eq!(shown(&mut s, 2, 3), WORD1_RGB); + + // x = -1: column 0 of the glyph is off the left edge. + cursor_entry(&mut s, cursor::POSITION, 0xffff_0005); + assert_eq!(shown(&mut s, 0, 5), WORD2_RGB); + assert_eq!(shown(&mut s, 1, 5), WORD0_RGB); + assert_eq!(shown(&mut s, 2, 5), bg); + assert_eq!(shown(&mut s, 2, 3), bg, "the old place is repainted"); + assert_eq!(shown(&mut s, 3, 3), bg); + + // y = -2: glyph row 2 lands on line 0. Read unsigned, 0xfffe would put + // the whole cursor below the raster and draw nothing. + cursor_entry(&mut s, cursor::GLYPH + 2 * 4, 0x1); + cursor_entry(&mut s, cursor::PLANE + 2 * 2, 0x3); + cursor_entry(&mut s, cursor::POSITION, 0x0002_fffe); + assert_eq!(shown(&mut s, 2, 0), WORD1_RGB); + assert_eq!(shown(&mut s, 3, 0), bg); + assert_eq!(shown(&mut s, 2, 1), bg); + assert_eq!(shown(&mut s, 0, 5), bg); + + // The PROM's first position after its enable, x 1280 y 0, is the first + // column right of the raster and draws nothing. + cursor_entry(&mut s, cursor::POSITION, 0x0500_0000); + assert_eq!(shown(&mut s, 0, 5), bg); + assert_eq!(shown(&mut s, 2, 0), bg); + assert_eq!(shown(&mut s, 1279, 0), 0); +} + +/// The cursor request writes the position through a type 7 header when it +/// defers it. A deferred position flushed when control bit 5 changes +/// arrives as four records, each applying in turn, so the cursor ends +/// hidden on the second channel with every entry reading back as written. +/// +/// QEMU: cursor/deferred-records. +#[test] +fn deferred_cursor_records_apply_in_turn() { + let (mut s, _) = board("v12"); + cursor_load(&mut s, 2, 3); + let bg = shown(&mut s, 11, 7); + let deferred = |s: &mut Board, index: u32, word: u32| { + backend(s, 0x3000_0000 | (cursor::BASE + index) << 14 | 1 | 0x4000_0000); + backend(s, word); + }; + + cursor_entry(&mut s, cursor::CONTROL, 3); + deferred(&mut s, cursor::POSITION, 0x0004_0005); + assert_eq!(shown(&mut s, 4, 5), WORD1_RGB); + assert_eq!(shown(&mut s, 2, 3), bg); + deferred(&mut s, cursor::CONTROL, 1); + assert_eq!(shown(&mut s, 4, 5), WORD1_RGB); + + deferred(&mut s, cursor::CONTROL, 0x21); + deferred(&mut s, cursor::POSITION, 0x0002_0003); + deferred(&mut s, cursor::CONTROL, 0x20); + assert_eq!(shown(&mut s, 2, 3), bg); + assert_eq!(shown(&mut s, 4, 5), bg); + assert_eq!(cursor_readback(&mut s, cursor::POSITION), 0x0002_0003); + assert_eq!(cursor_readback(&mut s, cursor::CONTROL), 0x20); +} + +/// Control bits 0 and 2 are the X server's crosshair: a one-pixel line +/// across the raster each way through the position, in colour word 1. Bit +/// 2 alone draws nothing, and neither does the second display channel. +/// +/// QEMU: cursor/crosshair. +#[test] +fn the_crosshair_draws_a_full_raster_line_each_way() { + let (mut s, _) = board("v12"); + cursor_load(&mut s, 2, 3); + let bg = shown(&mut s, 11, 7); + + cursor_entry(&mut s, cursor::CONTROL, 4); + assert_eq!(shown(&mut s, 2, 3), bg); + assert_eq!(shown(&mut s, 9, 3), bg); + + cursor_entry(&mut s, cursor::CONTROL, 5); + for (x, y) in [(0, 3), (9, 3), (1279, 3), (2, 0), (2, 1023)] { + assert_eq!(shown(&mut s, x, y), WORD1_RGB, "({x}, {y})"); + } + assert_eq!(shown(&mut s, 3, 4), bg, "no glyph beside the point"); + + cursor_entry(&mut s, cursor::CONTROL, 0x25); + assert_eq!(shown(&mut s, 9, 3), bg); + assert_eq!(shown(&mut s, 2, 6), bg); +} + +/// The host steers its pointer by the glyph the guest set: where its +/// top-left is and which pixels are opaque, the same ones the display draws. +/// It reports nothing while the cursor is off, on the second channel or a +/// crosshair. +#[test] +fn the_host_reads_the_cursor_glyph_and_position_from_the_registers() { + use crate::dev::odyssey::Display; + use crate::gfx_display::GfxDisplay; + + let (mut s, _) = board("v12"); + let display = Display::new(crate::disp::StatusSource::detached()); + display.attach_registers(s.scanout.shared.clone()); + assert_eq!(display.hw_cursor(), None, "out of reset the cursor is off"); + + cursor_load(&mut s, 2, 3); + assert_eq!(display.hw_cursor(), None, "loaded but not enabled"); + + cursor_entry(&mut s, cursor::CONTROL, 1); + let c = display.hw_cursor().expect("enabled"); + assert_eq!((c.x, c.y, c.size), (2, 3, 32)); + // The test glyph is opaque in columns 0 to 2 of row 0 only: the plane + // word 0x3f is opaque for those three columns. + assert_eq!(c.mask.len(), 32); + assert_eq!(c.mask[0], 0b111); + assert!(c.mask[1..].iter().all(|&m| m == 0)); + + // Position components are signed. + cursor_entry(&mut s, cursor::POSITION, 0xffff_fffe); + let c = display.hw_cursor().expect("enabled"); + assert_eq!((c.x, c.y), (-1, -2)); + + // A transparent glyph value (15) in an opaque plane draws nothing. + cursor_entry(&mut s, cursor::GLYPH, 0x0f21); + assert_eq!(display.hw_cursor().expect("enabled").mask[0], 0b011, "glyph 15 in column 2 is transparent"); + + cursor_entry(&mut s, cursor::CONTROL, 0x21); + assert_eq!(display.hw_cursor(), None, "second channel"); + cursor_entry(&mut s, cursor::CONTROL, 5); + assert_eq!(display.hw_cursor(), None, "crosshair"); + cursor_entry(&mut s, cursor::CONTROL, 0); + assert_eq!(display.hw_cursor(), None, "off"); +} + +/// A reset clears every cursor entry, so the cursor is off and its glyph, +/// plane and colours are gone: turning it on draws nothing. +/// +/// QEMU: cursor/reset-leaves-it-off. +#[test] +fn a_reset_leaves_the_cursor_off_and_empty() { + let (mut s, _) = board("v12"); + cursor_load(&mut s, 2, 3); + cursor_entry(&mut s, cursor::CONTROL, 1); + assert_eq!(shown(&mut s, 2, 3), WORD1_RGB); + + s.reset(); + + assert_eq!(shown(&mut s, 2, 3), 0); + for e in [cursor::CONTROL, cursor::POSITION, cursor::GLYPH, cursor::COLOURS + 1] { + assert_eq!(cursor_readback(&mut s, e), 0, "{e:#x}"); + } + cursor_entry(&mut s, cursor::CONTROL, 1); + assert_eq!(shown(&mut s, 2, 3), 0); +} + +/// The dirty-line counters name each mark's site and the lines a frame +/// resolved, which `vpro dirty` reports. +#[test] +fn the_dirty_counters_name_the_site_and_the_frame_rows() { + let (mut s, _) = board("v12"); + display_identity(&mut s, 0); + s.scanout_update(); + s.reader.stats = DirtyStats::default(); + + s.scanout.invalidate_for(DirtySite::Pixel, 7, 3); + s.scanout.repaint_for(DirtySite::Cursor, 1000, 100); + s.scanout_update(); + + let h = s.scanout.mode.height as u64; + let st = &s.reader.stats; + assert_eq!(s.scanout.marks.site(DirtySite::Pixel), [1, 3]); + assert_eq!(s.scanout.marks.site(DirtySite::Cursor), [1, h - 1000], "clipped to the frame"); + assert_eq!((st.frames, st.busy_frames, st.rows, st.max_rows), (1, 1, 3 + h - 1000, 3 + h - 1000)); + s.scanout_update(); + assert_eq!((s.reader.stats.frames, s.reader.stats.busy_frames), (2, 1), "an idle frame counts, with no rows"); +} + +/// The display lines a draw marked to resolve again. +fn dirty_rows(s: &Board) -> Vec { + s.scanout.marks.dirty_rows() +} + +/// A 2D draw marks only the lines it wrote, not the whole frame: the +/// display resolves those and the idle rest of the frame stays as it was. +#[test] +fn a_2d_draw_dirties_only_its_rows() { + let (mut s, _) = board("v12"); + display_identity(&mut s, 0); + s.scanout_update(); + assert!(dirty_rows(&s).is_empty()); + + rgb_fill(&mut s, 0xff, 0, 0, 20, 200, 60, 204); + assert_eq!(dirty_rows(&s), vec![200, 201, 202, 203], "a solid fill"); + s.scanout_update(); + + s.pattern_fill(10, 300, 16, 3, 0, 0, false, 0xff_0000); + assert_eq!(dirty_rows(&s), vec![300, 301, 302], "a pattern fill"); + s.scanout_update(); + + s.clear_planes(0, 400, 8, 2, 0x10, 0xff); + assert_eq!(dirty_rows(&s), vec![400, 401], "an id-plane write"); + s.scanout_update(); + + s.copy_src = 5 << 16 | 20; + s.copy_dst = 600 << 16 | 20; + s.copy_size = 7 << 16 | 40; + s.copy_rect(); + assert_eq!(dirty_rows(&s), (600..607).collect::>(), "a screen-to-screen copy marks its destination"); + s.scanout_update(); + + s.copy_dst = (ODSY_HEIGHT - 2) << 16; + s.copy_size = 50 << 16 | 8; + s.copy_rect(); + let h = ODSY_HEIGHT as usize; + assert_eq!(dirty_rows(&s), vec![h - 2, h - 1], "a copy past the bottom is clipped to the plane"); +} + +/// A register store that lands after the display copied the registers and +/// before it filed the copy is not lost: the copy is filed under the +/// generation read before it began, so the next frame copies again and sees +/// the store. +#[test] +fn a_register_store_during_the_copy_is_copied_by_the_next_frame() { + let regs = SharedRegs::default(); + let mut copy = DisplayRegs::default(); + + regs.set_lut(0, 1); + *regs.test_after_copy.lock() = Some(Box::new(|r| { + r.set_lut(0, 0x2a5); + })); + let seen = regs.snapshot(&mut copy, u64::MAX); + assert_eq!(copy.lut[0], 1, "the store landed after the word was copied"); + *regs.test_after_copy.lock() = None; + regs.snapshot(&mut copy, seen); + assert_eq!(copy.lut[0], 0x2a5, "the store during the copy was never copied"); +} diff --git a/src/dev/odyssey/tests/dma.rs b/src/dev/odyssey/tests/dma.rs new file mode 100644 index 000000000..d9a62bc06 --- /dev/null +++ b/src/dev/odyssey/tests/dma.rs @@ -0,0 +1,487 @@ +//! Completion words and pixel DMA: the host-write fence the driver waits +//! on, and the page-table transfers that move pixels between guest memory +//! and the board. + +use crate::dev::odyssey::board::Board; +use crate::dev::odyssey::consts::{ctx, dma, host, reg, ODSY_USER_CMD_FIFO}; +use crate::dev::odyssey::mmio::BoardLocks; +use crate::dev::odyssey::tests::{ + board_with_ram, cmd, cmd64, pixel, pixel_write, reg_write, route_to_heart, select, DMA_VECTOR, + FLAG_VECTOR, Ram, SOFTSYNC_VECTOR, +}; +use std::sync::Arc; +use super::*; + +const FENCE_BASE: u64 = 0xb70000; +pub(super) const TABLE: u64 = 0xb80000; +pub(super) const PAGE_A: u64 = 0xb84000; +const PAGE_B: u64 = 0xb88000; +/// A 16-pixel-wide RGBA8 object the transfers create. +const OBJECT: u32 = 0x0016_a800; + +/// The way the driver's descriptor builder packs one 24-bit half of a +/// table address: bits 16:0 stay, bits 23:17 move to 29:23. +fn pack_half(half: u64) -> u32 { + (half & 0x1ffff) as u32 | ((half & 0xfe_0000) << 6) as u32 +} + +/// Push a two-doubleword DMA descriptor the way the driver builds it: the +/// opcode over the packed high half of the table address, then the packed +/// low half over a zero word. +pub(super) fn descriptor(s: &mut Board, table: u64) { + cmd64(s, dma::DESC_UPLOAD, pack_half(table >> 24)); + cmd64(s, pack_half(table & 0xff_ffff), 0); +} + +/// An entry of the translation table: a page address over a byte count. +pub(super) fn pte(addr: u64, len: u32) -> u64 { + addr << dma::PTE_ADDR_SHIFT | len as u64 +} + +pub(super) fn put_table(ram: &Ram, table: u64, header: [u32; 8], entries: &[u64]) { + let mut t = vec![0u8; dma::TABLE_HEADER as usize + 16 * 8]; + for (i, w) in header.iter().enumerate() { + t[i * 4..][..4].copy_from_slice(&w.to_be_bytes()); + } + for (i, e) in entries.iter().enumerate() { + t[dma::TABLE_HEADER as usize + i * 8..][..8].copy_from_slice(&e.to_be_bytes()); + } + ram.put(table, &t); +} + +/// A header naming the 16-pixel-wide RGBA8 object at `OBJECT`: origin +/// (0, 0), `rows` rows of 64 bytes, 4 bytes a pixel, source format 0x4b, +/// and the DMA-done request when `irq` is set. +fn object_header(rows: u32, irq: bool) -> [u32; 8] { + [OBJECT, 0, 1, rows << 16 | 16, 0, 0, 64, if irq { dma::IRQ_REQUEST } else { 0 } | 1 << 16 | 0x4b] +} + +/// Word `i` of the object, 16 texels a row. +fn object_word(s: &mut Board, i: u32) -> u32 { + let obj = s.object_find(OBJECT); + let obj = obj.expect("the transfer made the object"); + s.object_load(obj, i % 16, i / 16) +} + +/// The colour destination select the X server sends, naming buffer 0x2000. +pub(super) fn colour_destination(s: &mut Board) { + for w in [select(reg::DEST_SELECT), 0x2000, 0x50, 0x2000_6950, 0] { + s.mmio_write(ODSY_USER_CMD_FIFO, w as u64, 4); + } +} + +/// A board with the fence slots at `FENCE_BASE` and the interrupts routed. +fn fenced_board() -> (Box, Arc, Arc) { + let (mut s, rec, ram) = board_with_ram(); + reg_write(&mut s, host::WRITE_ADDR_UPPER, 0); + reg_write(&mut s, host::WRITE_ADDR_LOWER, FENCE_BASE as u32); + route_to_heart(&mut s); + (s, rec, ram) +} + +/// The host-write/sync command the driver's DMA completion wait uses: a +/// write of the command's 16-bit token into guest RAM at the host-write +/// base plus twice its slot number, and, only when the command's interrupt +/// bit is set, a one-shot softsync packet. The interrupt handler is the +/// only place that polls the halfword, so both effects are needed to +/// unblock the driver. +/// +/// QEMU: command-fifo-host-write-sync. +#[test] +fn a_host_write_fence_lands_in_its_slot_and_interrupts_only_when_asked() { + let (mut s, rec, ram) = fenced_board(); + assert_eq!(ram.half(FENCE_BASE + 4), 0); + + // Slot 2, no interrupt bit: the write lands, nothing is sent. + for w in [select(reg::HOST_WRITE), 2, 0x5a00] { + cmd(&mut s, w); + } + assert_eq!(ram.half(FENCE_BASE + 4), 0x5a00); + assert!(rec.silent()); + + // Slot 3, interrupt bit set: a second write and the softsync packet. + for w in [select(reg::HOST_WRITE), dma::FENCE_INTERRUPT | 3, 0xa5a5] { + cmd(&mut s, w); + } + assert_eq!(ram.half(FENCE_BASE + 6), 0xa5a5); + assert_eq!(rec.events(SOFTSYNC_VECTOR), [true]); + assert_eq!(rec.events(FLAG_VECTOR), Vec::::new()); +} + +/// The page-table pixel DMA engine: a scatter-gather buffer and its +/// translation table laid in guest RAM, a 0xe01 descriptor pushed, and the +/// whole buffer lands, across the page boundary, in the off-screen object +/// the header names, 16 texels a row. The table address is packed as the +/// driver packs it, with bits 23:17 set and a table above 16 MiB so the +/// high half is non-zero. The first entry starts part-way into its page. +/// The descriptor writes no fence and raises the DMA-done interrupt. +/// +/// QEMU: command-fifo-dma-page-table. +#[test] +fn a_page_table_transfer_lands_across_pages_and_raises_dma_done() { + let table_high = 0x1fe5300u64; // high half 1, low half 0xfe5300 + let first_off = 0x3000u64; + let first_len = dma::PAGE_SIZE as u32 - first_off as u32; + let page1_len = 0x40u32; + let (mut s, rec, ram) = fenced_board(); + assert_eq!(pack_half(TABLE >> 24), 0); + assert_eq!(pack_half(TABLE & 0xff_ffff), 0x2e00_0000); + + // A distinct big-endian word per pixel, so a misordered fetch shows. + let buf0: Vec = (0..first_len / 4).flat_map(|i| (0xaa00_0000u32 | i).to_be_bytes()).collect(); + let buf1: Vec = (0..page1_len / 4).flat_map(|i| (0xbb00_0000u32 | i).to_be_bytes()).collect(); + ram.put(PAGE_A + first_off, &buf0); + ram.put(PAGE_B, &buf1); + put_table( + &ram, + TABLE, + object_header((first_len + page1_len) / 64, true), + &[pte(PAGE_A + first_off, first_len), pte(PAGE_B, page1_len)], + ); + + descriptor(&mut s, TABLE); + + let words = first_len / 4; + assert_eq!(object_word(&mut s, 0), 0xaa00_0000); + assert_eq!(object_word(&mut s, 1), 0xaa00_0001); + assert_eq!(object_word(&mut s, words - 1), 0xaa00_0000 | (words - 1)); + assert_eq!(object_word(&mut s, words), 0xbb00_0000, "the first pixel of the second page"); + assert_eq!(object_word(&mut s, words + page1_len / 4 - 1), 0xbb00_0000 | (page1_len / 4 - 1)); + assert_eq!(object_word(&mut s, words + page1_len / 4), 0, "one texel past the transfer holds nothing"); + assert_eq!(ram.half(FENCE_BASE), 0, "no fence"); + assert!(rec.events(SOFTSYNC_VECTOR).is_empty()); + assert_eq!(rec.events(DMA_VECTOR), [true]); + + // A table above 16 MiB, one row: the high half reaches the decode. + put_table(&ram, table_high, object_header(1, false), &[pte(PAGE_B, page1_len)]); + descriptor(&mut s, table_high); + assert_eq!(object_word(&mut s, 0), 0xbb00_0000); + assert_eq!(object_word(&mut s, page1_len / 4 - 1), 0xbb00_0000 | (page1_len / 4 - 1)); +} + +/// A transfer into an object lands on the raster unit, in order behind +/// the raster work queued before it, which may still read the texels it +/// replaces: the descriptor returns while that work is still queued. A +/// later read of the object waits for the transfer and sees it. (A +/// transfer that grows its object still waits: growing may move it.) +#[test] +fn an_object_transfer_queues_behind_the_raster_work_before_it() { + let (s, _rec, ram) = fenced_board(); + let h = BoardLocks::new(s); + put_table(&ram, TABLE, object_header(1, false), &[pte(PAGE_A, 64)]); + // The object exists with its row before the raster unit has work. + descriptor(&mut h.test_board(), TABLE); + h.raster.start(); + // The raster unit's next slice holds it for 300 ms, as a long draw would. + h.raster.test_slice_sleep_ms.store(300, std::sync::atomic::Ordering::SeqCst); + h.raster.submit(|_| {}); + let row: Vec = (0..16u32).flat_map(|i| (0xdd00_0000u32 | i).to_be_bytes()).collect(); + ram.put(PAGE_A, &row); + { + let mut s = h.test_board(); + descriptor(&mut s, TABLE); + assert_eq!(h.raster.pending(), 2, "the transfer queues behind the raster work"); + assert_eq!(object_word(&mut s, 0), 0xdd00_0000); + assert_eq!(object_word(&mut s, 15), 0xdd00_000f); + assert_eq!(h.raster.pending(), 0, "reading the object waited for the transfer"); + } + h.raster.stop(); +} + +/// A descriptor whose entries disagree with its header, one entry +/// claiming more bytes than the header's rectangle, fails the transfer +/// before anything lands. The board still consumed the descriptor, so the +/// DMA-done source the header asks for fires. Before the driver programs +/// the routing, a consumed descriptor sends nothing. +/// +/// QEMU: command-fifo-dma-page-table (the failing and unrouted cases). +#[test] +fn a_transfer_that_disagrees_with_its_header_lands_nothing_but_still_completes() { + let (mut s, rec, ram) = fenced_board(); + put_table(&ram, TABLE, object_header(1, true), &[pte(PAGE_A, 0xffff)]); + ram.put(PAGE_A, &[0x11; 0x40]); + + descriptor(&mut s, TABLE); + + assert_eq!(rec.events(DMA_VECTOR), [true]); + assert_eq!(object_word(&mut s, 0), 0); + + let (mut s, rec, ram) = board_with_ram(); + put_table(&ram, TABLE, object_header(1, true), &[pte(PAGE_A, 0xffff)]); + descriptor(&mut s, TABLE); + assert!(rec.silent(), "no destination is programmed"); +} + +/// One kick names one 128-byte work-list entry: the 32-byte header and up +/// to 12 page pairs, with no terminator when all twelve are used; the next +/// entry's words follow at once. Twelve one-row pairs fill the entry and +/// the 2 KiB after it are nonzero, as a following entry would be. The +/// transfer takes exactly the twelve rows. +/// +/// QEMU: command-fifo-dma-full-entry. +#[test] +fn a_full_work_list_entry_takes_exactly_twelve_pages() { + let (mut s, _, ram) = board_with_ram(); + let source = 0xb90000u64; + let mut entries = Vec::new(); + for k in 0..12u64 { + let row: Vec = (0..16u32).flat_map(|i| (0xcc00_0000u32 | (k as u32) << 8 | i).to_be_bytes()).collect(); + ram.put(source + k * dma::PAGE_SIZE, &row); + entries.push(pte(source + k * dma::PAGE_SIZE, 64)); + } + put_table(&ram, TABLE, object_header(12, false), &entries); + // What would be the next entry: nonzero, so a missing terminator is noticed. + let after = TABLE + dma::TABLE_HEADER + 12 * 8; + ram.put(after, &[0xff; 0x800]); + + descriptor(&mut s, TABLE); + + for k in 0..12u32 { + assert_eq!(object_word(&mut s, k * 16), 0xcc00_0000 | k << 8, "row {k}"); + assert_eq!(object_word(&mut s, k * 16 + 15), 0xcc00_0000 | k << 8 | 15, "row {k}"); + } +} + +/// A clipped pixel transfer arrives as several operations: block 0x258 +/// ahead of each, one entry per chunk whose header carries that chunk's +/// rows, the interrupt request on the last chunk only, and a host-write +/// tag after the last. Each chunk draws its own rows. A failed +/// operation's drain, block 0x044 with operand 0 and then a host-write +/// with slot 0xd0a, is accepted and writes its tag. +/// +/// QEMU: command-fifo-dma-chunks. +#[test] +fn a_clipped_transfer_draws_chunk_by_chunk_and_interrupts_on_the_last() { + let (mut s, rec, ram) = fenced_board(); + colour_destination(&mut s); + let tables = [0xb80000u64, 0xb81000]; + let pages = [0xb84000u64, 0xb88000]; + let rows = [300u32, 364]; + + for c in 0..2usize { + let header = [ + 0x2000, + rows[c] << 16 | 200, + 0x50, + 1 << 16 | 2, + 0x8223_305a, + 0, + 8, + if c == 1 { dma::IRQ_REQUEST } else { 0 } | 0x3201_0000, + ]; + let mut px = [0u8; 8]; + px[..4].copy_from_slice(&(0x0010_2030u32 + c as u32).to_be_bytes()); + px[4..].copy_from_slice(&(0x0040_5060u32 + c as u32).to_be_bytes()); + ram.put(pages[c], &px); + put_table(&ram, tables[c], header, &[pte(pages[c], 8)]); + cmd(&mut s, 0x0001_0658); + cmd(&mut s, 0x0012_0000); + descriptor(&mut s, tables[c]); + assert_eq!(rec.events(DMA_VECTOR).len(), c, "only the last chunk asks for the interrupt"); + } + for w in [select(reg::HOST_WRITE), 3, 0x5a01] { + cmd(&mut s, w); + } + + assert_eq!(pixel(&s, 0, 200, 300), 0x1020_30ff); + assert_eq!(pixel(&s, 0, 201, 300), 0x4050_60ff); + assert_eq!(pixel(&s, 0, 200, 364), 0x1020_31ff); + assert_eq!(pixel(&s, 0, 201, 364), 0x4050_61ff); + assert_eq!(pixel(&s, 0, 200, 301), 0); + assert_eq!(ram.half(FENCE_BASE + 6), 0x5a01); + + cmd(&mut s, 0x0001_0444); + cmd(&mut s, 0); + for w in [select(reg::HOST_WRITE), 0xd0a, 0x5a02] { + cmd(&mut s, w); + } + assert_eq!(ram.half(FENCE_BASE + 2 * 0xd0a), 0x5a02); +} + +/// A 3x2 image of 4-byte pixels at (200, 300) with a 4-byte gap after each +/// row: row 0 starts 8 bytes before the end of page A and finishes in page +/// B, then the gap, then row 1. The gap is never a pixel, and the header +/// asks for no interrupt unless word 7 says so. +/// +/// QEMU: command-fifo-dma-image. +#[test] +fn an_image_transfer_skips_the_row_gap_and_crosses_pages() { + let (mut s, rec, ram) = fenced_board(); + colour_destination(&mut s); + let mut header = [0x2000, 300 << 16 | 200, 0x50, 2 << 16 | 3, 0x8223_305a, 4, 12, 0x3201_0000]; + let entries = [pte(PAGE_A + 0x3ff8, 8), pte(PAGE_B, 16)]; + ram.put(PAGE_A + 0x3ff8, &[0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00]); + let b: Vec = [0x0000_00ffu32, 0xdead_beef, 0x0011_2233, 0x0044_5566, 0x0077_8899] + .iter() + .flat_map(|w| w.to_be_bytes()) + .collect(); + ram.put(PAGE_B, &b); + put_table(&ram, TABLE, header, &entries); + + descriptor(&mut s, TABLE); + + assert_eq!(pixel(&s, 0, 200, 300), 0xff00_00ff); + assert_eq!(pixel(&s, 0, 201, 300), 0x00ff_00ff); + assert_eq!(pixel(&s, 0, 202, 300), 0x0000_ffff); + assert_eq!(pixel(&s, 0, 200, 301), 0x1122_33ff); + assert_eq!(pixel(&s, 0, 201, 301), 0x4455_66ff); + assert_eq!(pixel(&s, 0, 202, 301), 0x7788_99ff); + assert_eq!(pixel(&s, 0, 203, 300), 0); + assert_eq!(pixel(&s, 0, 200, 302), 0); + assert!(rec.events(DMA_VECTOR).is_empty(), "the header did not ask for the interrupt"); + + // The same transfer asking for it: the pixels again, and the packet. + header[7] |= dma::IRQ_REQUEST; + put_table(&ram, TABLE, header, &entries); + descriptor(&mut s, TABLE); + assert_eq!(pixel(&s, 0, 200, 300), 0xff00_00ff); + assert_eq!(rec.events(DMA_VECTOR), [true]); +} + +/// A transfer to another destination, an off-screen object, lands in that +/// object and leaves the colour plane alone. Its word 7 carries no format +/// code, so the 4-byte pixels keep the 0x00RRGGBB rule. +/// +/// QEMU: command-fifo-dma-image (the object destination). +#[test] +fn an_image_transfer_to_an_object_leaves_the_plane_alone() { + let (mut s, _, ram) = board_with_ram(); + colour_destination(&mut s); + let b: Vec = [0x0000_00ffu32, 0xdead_beef, 0x0011_2233].iter().flat_map(|w| w.to_be_bytes()).collect(); + ram.put(PAGE_B, &b); + let header = [0x16a000, 0, 0x10, 1 << 16 | 3, 0x8002_5000, 0, 12, 0x3201_0000]; + put_table(&ram, TABLE, header, &[pte(PAGE_B, 12)]); + + descriptor(&mut s, TABLE); + + let obj = s.object_find(0x16a000); + let obj = obj.unwrap(); + assert_eq!(s.object_load(obj, 0, 0), 0x0000_ffff); + assert_eq!(s.object_load(obj, 1, 0), 0xadbe_efff); + assert_eq!(s.object_load(obj, 2, 0), 0x1122_33ff); + assert_eq!(s.object_load(obj, 3, 0), 0); + assert_eq!(pixel(&s, 0, 0, 0), 0); +} + +/// A guest page with a guard byte pattern, and the transfer table at +/// `TABLE` naming the 4 bytes at its offset 16 for one pixel. +fn readback_table(s: &mut Board, ram: &Ram, header: [u32; 8]) { + put_table(ram, TABLE, header, &[pte(PAGE_A + 16, 4)]); + descriptor(s, TABLE); +} + +/// Direction is word 7 bit 24 alone. The X server's 24-bit visual header +/// with the 0x4c format byte reads a screen pixel back as the bytes A, B, +/// G, R with alpha zero, and leaves the plane alone. The same header with +/// bit 24 clear paints the host bytes and writes nothing to the guest. +/// With a zero format byte the GL vocabulary applies: this word 4 is +/// class 2 code 11, ABGR bytes, so the same pixel comes back A, B, G, R +/// with its own alpha. +/// +/// QEMU: readback/direction. +#[test] +fn a_readback_sends_the_pixel_to_the_guest_as_abgr() { + let (mut s, rec, ram) = fenced_board(); + colour_destination(&mut s); + let mut header = [0x2000, 300 << 16 | 200, 0x50, 0x10001, 0x8223_305a, 0, 4, 0x3301_004c]; + pixel_write(&mut s, 200, 300, 0xc0ff_eeff); + ram.put(PAGE_A, &[0xa5; 36]); + let mut expect = [0xa5u8; 36]; + + readback_table(&mut s, &ram, header); + expect[16..20].copy_from_slice(&[0x00, 0xee, 0xff, 0xc0]); + assert_eq!(ram.get(PAGE_A, 36), expect); + assert_eq!(pixel(&s, 0, 200, 300), 0xc0ff_eeff, "the plane is left alone"); + assert!(rec.events(DMA_VECTOR).is_empty(), "bit 31 clear: no DMA-done"); + + // Bit 24 clear: an upload of the seed, and the guest stays as it was. + ram.put(PAGE_A + 16, &[0x00, 0x11, 0x22, 0x33]); + expect[16..20].copy_from_slice(&[0x00, 0x11, 0x22, 0x33]); + header[7] = 0x3201_004c; + readback_table(&mut s, &ram, header); + assert_eq!(pixel(&s, 0, 200, 300), 0x3322_11ff); + assert_eq!(ram.get(PAGE_A, 36), expect); + + // A zero format byte with this word 4: the GL read of ABGR bytes. + ram.put(PAGE_A, &[0xa5; 36]); + header[7] = 0x3301_0000; + readback_table(&mut s, &ram, header); + expect[16..20].copy_from_slice(&[0xff, 0x11, 0x22, 0x33]); + assert_eq!(ram.get(PAGE_A, 36), expect); + assert_eq!(pixel(&s, 0, 200, 300), 0x3322_11ff); +} + +/// The X server's 24-bit TrueColor visuals have red mask 0xff and blue +/// mask 0xff0000, so an XPutImage pixel is 0x00BBGGRR and the server's DMA +/// put names it with source code 0x4c, bytes A, B, G, R. An orange pixel +/// (red 0xfd, green 0x9b, blue 0x01) arrives as 00 01 9b fd and must land +/// orange, not blue. +/// +/// QEMU: readback/put-abgr-x-image. +#[test] +fn an_xputimage_pixel_in_abgr_order_lands_orange_not_blue() { + let (mut s, _, ram) = board_with_ram(); + colour_destination(&mut s); + ram.put(PAGE_A + 16, &[0x00, 0x01, 0x9b, 0xfd]); + + readback_table(&mut s, &ram, [0x2000, 300 << 16 | 200, 0x50, 0x10001, 0x8223_305a, 0, 4, 0x3201_004c]); + + assert_eq!(pixel(&s, 0, 200, 300), 0xfd9b_01ff); +} + +/// The kernel driver's context switch: one 8-byte store to the switch port +/// completes by writing a nonzero word into the shadow page the shadow +/// address registers name, at the offset the driver polls. No interrupt +/// and no command-FIFO words are involved. +/// +/// QEMU: context-switch-completion. +#[test] +fn a_context_switch_writes_its_done_word_into_the_shadow_page() { + let (mut s, rec, ram) = board_with_ram(); + let base = 0xb70000u64; + reg_write(&mut s, ctx::SHADOW_ADDR_UPPER, 0); + reg_write(&mut s, ctx::SHADOW_ADDR_LOWER, base as u32); + let done = |ram: &Ram| ram.get(base + ctx::SWITCH_DONE_OFFSET, 4); + assert_eq!(done(&ram), [0; 4]); + + s.mmio_write(ctx::SWITCH_PORT, 0x0000_0700_0001_0001, 8); + + assert_ne!(done(&ram), [0; 4]); + assert!(rec.silent()); +} + +/// With no shadow address programmed, the reset value, the switch has +/// nowhere to complete into. It writes nothing rather than guess an +/// address. +/// +/// QEMU: context-switch-without-shadow-address-is-a-guest-error. +#[test] +fn a_context_switch_with_no_shadow_address_writes_nothing() { + let (mut s, _, ram) = board_with_ram(); + let before = ram.get(0, 0x2000); + + s.mmio_write(ctx::SWITCH_PORT, 0x0000_0700_0001_0001, 8); + + assert_eq!(ram.get(0, 0x2000), before); +} + +/// A transfer into the colour buffer marks the rows it covers to resolve +/// again, and a transfer into an off-screen object marks none: the display +/// does not scan an object. +#[test] +fn a_dma_transfer_dirties_only_the_rows_it_wrote() { + let (mut s, _, ram) = board_with_ram(); + let lines = |s: &Board| s.scanout.marks.dirty_rows(); + colour_destination(&mut s); + ram.put(PAGE_A, &[0x10; 24]); + s.scanout_update(); + + put_table(&ram, TABLE, [0x2000, 300 << 16 | 200, 0x50, 2 << 16 | 3, 0x8223_305a, 4, 12, 0x3201_0000], &[pte(PAGE_A, 24)]); + descriptor(&mut s, TABLE); + assert_eq!(lines(&s), vec![300, 301]); + s.scanout_update(); + + put_table(&ram, TABLE, [0x16a000, 0, 0x10, 1 << 16 | 3, 0x8002_5000, 0, 12, 0x3201_0000], &[pte(PAGE_A, 12)]); + descriptor(&mut s, TABLE); + assert!(lines(&s).is_empty(), "an object is not on the screen"); +} diff --git a/src/dev/odyssey/tests/fifo.rs b/src/dev/odyssey/tests/fifo.rs new file mode 100644 index 000000000..1ba7f683b --- /dev/null +++ b/src/dev/odyssey/tests/fifo.rs @@ -0,0 +1,946 @@ +//! The command FIFO: queueing, the board's thread, status. + +use crate::dev::odyssey::board::Board; +use crate::dev::odyssey::command::{CFIFO_PREAMBLE, CfifoPreamble}; +use crate::dev::odyssey::consts::{ + cfifo, ctx, flctl, host, intr, reg, self, status0, ODSY_BUZZ_1, ODSY_STATUS0, ODSY_SW_TOKEN, +}; +use crate::dev::odyssey::fifo::{CommandFifo, RING_STORES, RUN_MAX, Run}; +use crate::dev::odyssey::interface::HostInterface; +use crate::dev::odyssey::mmio::BoardLocks; +use crate::dev::odyssey::tests::{ + board, board_with_ram, cmd, cmd64, reg_read, reg_write, route_to_heart, select, FLAG_VECTOR, + Ram, VPRO_WIDGET, +}; +use crate::dev::odyssey::worker::Budget; +use parking_lot::Mutex; +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use super::*; +use std::sync::atomic::Ordering::SeqCst; + +/// A board on its own thread, routed to the recorder and with the +/// low-water source enabled and clear. +fn threaded_board() -> (BoardLocks, Arc) { + let (s, rec) = board("v12"); + let h = BoardLocks::new(s); + + h.work_start(); + h.write(ROUTE_DEST_UPPER, HEART_WIDGET << 25, 4); + h.write(ROUTE_DEST_LOWER, HEART_PACKET, 4); + h.write(ROUTE_VECTORS, TEST_VECTOR as u64, 4); + h.write(INTR_CLEAR, INTR_FIFO_LOW_WATER, 4); + h.write(INTR_ENABLE_SET, INTR_FIFO_LOW_WATER, 4); + assert!(!rec.level(TEST_VECTOR)); + (h, rec) +} + +/// The self-test preamble twice: it completes only in order. +fn preamble_twice() -> Vec<&'static CfifoPreamble> { + let words: Vec<_> = CFIFO_PREAMBLE.iter().chain(CFIFO_PREAMBLE.iter()).collect(); + assert!(words.len() as u32 > cfifo::LOW_WATER); + words +} + +/// Queue every FIFO store and have the first run last +/// `hold_ms` as a long draw would; store the first word and return +/// once that run is under way. +fn start_long_slice(h: &BoardLocks, words: &[&CfifoPreamble], hold_ms: u32) { + h.fifo.test_words.store(1, SeqCst); + h.test_board().test_slice_sleep_ms = hold_ms; + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + h.write(words[0].port, words[0].value, words[0].size); + while !(h.fifo.slice.load(SeqCst)) { + assert!(std::time::Instant::now() < deadline, "the first slice did not start"); + std::thread::yield_now(); + } +} + +/// A burst of command-FIFO stores queued behind a long slice runs on the +/// board's thread in the order stored, and the low-water flag rises, and +/// its interrupt with it, once the queue drains below the low-water +/// mark. While the slice holds the board, the stores queue and the +/// status register answers without waiting for it. +#[test] +fn the_board_thread_runs_queued_stores_in_order_and_latches_low_water() { + const STATUS0: u64 = 0x1064; + let (h, rec) = threaded_board(); + let words = preamble_twice(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + + start_long_slice(&h, &words, 2000); + // The rest go in while the first run is under way: only + // the front lock is needed. + for w in &words[1..] { + h.write(w.port, w.value, w.size); + } + { + let level = h.fifo.level(); + assert!(level >= cfifo::LOW_WATER, "level {level}: the first slice should still hold the queue"); + assert_eq!(h.front.lock().intr_flags & intr::FIFO_LOW_WATER, 0); + // The status register does not wait: not empty, above low water. + let status = h.read(STATUS0, 4) as u32; + assert_eq!(status & (status0::FIFO_EMPTY | status0::FIFO_LOW_WATER), 0); + assert_eq!(h.fifo.level(), level); + // All of that while the slice still held the board. + assert!(h.fifo.slice.load(SeqCst)); + } + + // Watch the queue drain on the board's thread, without draining it + // from here: the flag is up exactly when the level is below the mark. + loop { + { + let f = h.front.lock(); + let level = h.fifo.level(); + let flag = f.intr_flags & intr::FIFO_LOW_WATER != 0; + assert_eq!(flag, level < cfifo::LOW_WATER, "level {level}"); + if level == 0 && !h.fifo.slice.load(SeqCst) { + break; + } + } + assert!(std::time::Instant::now() < deadline, "work thread did not drain the queue"); + std::thread::sleep(std::time::Duration::from_millis(1)); + } + { + let s = h.test_board(); + assert!(s.widget.cfifo_selftest_ready); + assert_eq!(s.widget.selftest_progress, 0); + } + assert!(rec.level(TEST_VECTOR)); + let status = h.read(STATUS0, 4) as u32; + assert_eq!( + status & (status0::FIFO_EMPTY | status0::FIFO_LOW_WATER), + status0::FIFO_EMPTY | status0::FIFO_LOW_WATER + ); + h.work_stop(); + assert!(h.fifo.thread.lock().is_none()); +} + +/// A read that waits for the work, issued while the board's thread is in a +/// long run, is answered busy until every store queued before it has run: +/// the self-test register reads OK, though the board's thread runs one store +/// per run and most were still queued when the read began. +#[test] +fn a_read_that_waits_sees_every_earlier_store() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + + start_long_slice(&h, &words, 300); + for w in &words[1..] { + h.write(w.port, w.value, w.size); + } + assert!(h.fifo.level() > 1); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + let selftest = loop { + if let Some(v) = h.bus_read(cfifo::SELFTEST, 4) { + break v as u32; + } + assert!(std::time::Instant::now() < deadline, "the read stayed busy"); + std::thread::yield_now(); + }; + assert_eq!(selftest, cfifo::SELFTEST_OK); + assert_eq!(h.fifo.level(), 0); + h.work_stop(); +} + +/// A processor store that finds the host ring full while the board's thread +/// runs is refused with the bus busy, and nothing runs on the processor's +/// thread: the level stays at the brim and the slice in flight keeps the +/// board. The retried store goes in, once, when the board's thread has made +/// room, and the level never passes the model's FIFO size. +#[test] +fn a_full_ring_holds_the_store_back_until_the_board_makes_room() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + + start_long_slice(&h, &words, 2000); + while h.fifo.stores() < RING_STORES { + assert!(h.bus_write(consts::cmd::FIFO, 0, 4)); + } + let stores = h.fifo.stores(); + assert!(!h.bus_write(consts::cmd::FIFO, 0, 4), "a full ring refuses the store"); + assert!(!h.bus_write(consts::cmd::FIFO, 0, 8)); + assert_eq!((h.fifo.level(), h.fifo.stores()), (cfifo::WORDS, stores), "nothing ran on the processor's thread"); + assert!(h.fifo.slice.load(SeqCst), "the board's thread still holds the board"); + + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while !h.bus_write(consts::cmd::FIFO, 0, 8) { + assert!(h.fifo.level() <= cfifo::WORDS); + assert!(std::time::Instant::now() < deadline, "the board's thread made no room"); + std::thread::yield_now(); + } + assert!(h.fifo.level() <= cfifo::WORDS); + h.fifo.test_words.store(0, SeqCst); + h.work_stop(); + assert_eq!(h.fifo.level(), 0); +} + +/// A read that waits for the work, issued on the bus while stores are +/// queued behind a long slice, answers busy and runs nothing on the +/// processor's thread; retried, it answers once the board's thread has run +/// every store before it, and sees them all. A store that waits answers +/// busy the same way. +#[test] +fn a_waiting_bus_read_is_busy_until_the_board_thread_has_run_the_stores() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + + start_long_slice(&h, &words, 300); + for w in &words[1..] { + h.write(w.port, w.value, w.size); + } + let level = h.fifo.level(); + assert!(level > 1); + assert_eq!(h.bus_read(cfifo::SELFTEST, 4), None, "busy while the stores wait"); + assert!(!h.bus_write(ODSY_BUZZ_1, 0, 4), "a waiting store is busy too"); + assert_eq!(h.fifo.level(), level, "nothing ran on the processor's thread"); + + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + let selftest = loop { + if let Some(v) = h.bus_read(cfifo::SELFTEST, 4) { + break v as u32; + } + assert!(std::time::Instant::now() < deadline, "the board's thread did not run the stores"); + std::thread::yield_now(); + }; + assert_eq!(selftest, cfifo::SELFTEST_OK); + assert_eq!(h.fifo.level(), 0); + assert!(h.bus_write(ODSY_BUZZ_1, 0, 4)); + h.fifo.test_words.store(0, SeqCst); + h.work_stop(); +} + +/// A run of stores the board's thread has looked at and then runs leaves +/// the FIFO together. The stores run in order; the level and the store count +/// end where taking the stores out one by one leaves them, including when +/// more stores join the queue after the run was taken. +#[test] +fn a_run_retires_with_the_levels_of_one_by_one() { + fn queue(s: &mut Board) { + for _ in 0..70 { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + } + fn join(s: &mut Board) { + for _ in 0..10 { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + } + // One at a time: the store count and the level are the reference. + let (mut one, _, _) = queued_board(); + queue(&mut one); + join(&mut one); + for _ in 0..RUN_MAX { + step(&mut one); + } + + let (mut run, _, _) = queued_board(); + queue(&mut run); + let mut taken = Run::new(); + run.fifo.take_run(&mut taken); + assert_eq!(taken.stores().len(), RUN_MAX, "a full run"); + join(&mut run); + assert_eq!(run.fifo.stores(), 80); + run.fifo.retire(&run.front, &taken, RUN_MAX); + + assert_eq!(run.fifo.stores(), one.fifo.stores()); + assert_eq!(run.fifo.level(), one.fifo.level()); +} + +/// A slice cut short by its budget retires only the stores it ran: the rest +/// stay queued, in order, for the next slice. +#[test] +fn a_slice_cut_short_leaves_the_rest_of_its_run_queued() { + let (mut s, _, ram) = queued_board(); + push_fences(&mut s); + assert_eq!(s.fifo.stores(), 6); + + s.work_step(&mut Budget::stores(4)); + assert_eq!(s.fifo.stores(), 2); + assert_eq!(s.fifo.level(), 2); + assert_eq!(ram.half(HOST_WRITE_BASE + 4), 0x5a00, "the first fence ran"); + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0, "the second fence has stores left to run"); + + finish(&mut s); + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0xa5a5); +} + +/// The model's low-water threshold, in waiting words (a model choice). +const LOW_WATER_MARK: u32 = cfifo::LOW_WATER; +const HOST_WRITE_BASE: u64 = 0xb70000; + +/// A board whose stores wait in the queue until a test steps them one +/// store a slice, with RAM for the fences they write. +fn queued_board() -> (Box, Arc, Arc) { + let (mut s, rec, ram) = board_with_ram(); + reg_write(&mut s, host::WRITE_ADDR_UPPER, 0); + reg_write(&mut s, host::WRITE_ADDR_LOWER, HOST_WRITE_BASE as u32); + s.fifo.test_words.store(1, SeqCst); + (s, rec, ram) +} + +/// Two host-write fences, slot 2 then slot 3, each three words. +fn push_fences(s: &mut Board) { + for (slot, token) in [(2, 0x5a00), (3, 0xa5a5)] { + cmd(s, select(reg::HOST_WRITE)); + cmd(s, slot); + cmd(s, token); + } +} + +/// Run one store, as a one-store slice on the board's thread would. +fn step(s: &mut Board) { + let before = s.fifo.stores(); + assert!(before > 0, "nothing waits to be stepped"); + s.work_step(&mut Budget::stores(1)); + assert_eq!(s.fifo.stores(), before - 1); +} + +/// Run everything that waits. +fn finish(s: &mut Board) { + s.work_step(&mut Budget::unlimited()); + assert!(!s.fifo.pending()); +} + +/// The stores return with their work waiting; it runs one store a slice, +/// in order, and its fences land when it does. +/// +/// QEMU: command-work-runs-in-slices. +#[test] +fn stores_wait_and_run_one_slice_at_a_time() { + let (mut s, _, ram) = queued_board(); + push_fences(&mut s); + + assert!(s.work_pending()); + assert_eq!(ram.half(HOST_WRITE_BASE + 4), 0); + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0); + let mut steps = 0; + while s.work_pending() { + steps += 1; + assert!(steps < 100); + step(&mut s); + } + assert!(steps >= 3); + assert_eq!(ram.half(HOST_WRITE_BASE + 4), 0x5a00); + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0xa5a5); +} + +/// A context switch waits behind the commands before it: the kernel polls +/// the done word, which the board writes only once those have run. +/// +/// QEMU: command-work-orders-context-switch. +#[test] +fn a_context_switch_completes_only_after_the_commands_before_it() { + let (mut s, _, ram) = queued_board(); + let base = 0xb80000u64; + push_fences(&mut s); + reg_write(&mut s, ctx::SHADOW_ADDR_UPPER, 0); + reg_write(&mut s, ctx::SHADOW_ADDR_LOWER, base as u32); + s.mmio_write(ctx::SWITCH_PORT, 0x0000_0700_0001_0001, 8); + + let done = |ram: &Ram| u32::from_be_bytes(ram.get(base + ctx::SWITCH_DONE_OFFSET, 4).try_into().unwrap()); + assert_eq!(done(&ram), 0); + while s.work_pending() { + step(&mut s); + if done(&ram) != 0 { + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0xa5a5, "the fences before the switch had landed"); + } + } + assert_ne!(done(&ram), 0); +} + +/// A read of a register the work may change finishes the work first; the +/// software token, which no command touches, is answered at once. +/// +/// QEMU: command-work-read-waits. +#[test] +fn a_read_of_a_register_the_work_may_change_finishes_the_work_first() { + let (mut s, _, ram) = queued_board(); + push_fences(&mut s); + + reg_write(&mut s, ODSY_SW_TOKEN, 1); + assert_eq!(reg_read(&mut s, ODSY_SW_TOKEN), 1); + assert!(s.work_pending(), "the token is answered without waiting"); + + reg_read(&mut s, ODSY_BUZZ_1); + assert!(!s.work_pending()); + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0xa5a5); +} + +/// The level word is a plain load: with words queued it answers at once +/// with their count in bits 15:7, in doublewords, and runs nothing. +/// +/// QEMU: command-work-level-word-reads-live. +#[test] +fn the_level_word_reports_the_queue_without_draining_it() { + let (mut s, _, ram) = queued_board(); + push_fences(&mut s); + let (words, stores) = (s.fifo.level(), s.fifo.stores()); + assert!(words > 0); + + let level = (reg_read(&mut s, MEMORY_CONFIG) >> 7) & 0x1ff; + assert_eq!(level, words.div_ceil(2)); + assert_ne!(level, 0); + assert_eq!((s.fifo.level(), s.fifo.stores()), (words, stores), "the read left the queue alone"); + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0, "nothing ran"); + assert_eq!(reg_read(&mut s, MEMORY_CONFIG) & 0xff00_0000, 0xa100_0000, "the straps are still there"); +} + +/// The drain waits poll bits 15:7 until they read 0, so the level falls to +/// 0 as the board empties the queue. +/// +/// QEMU: command-work-level-word-falls-to-zero. +#[test] +fn the_level_word_reads_zero_once_the_board_has_run_the_queue() { + let (mut s, _, _) = queued_board(); + push_fences(&mut s); + let mut last = (reg_read(&mut s, MEMORY_CONFIG) >> 7) & 0x1ff; + assert_ne!(last, 0); + while s.work_pending() { + step(&mut s); + let now = (reg_read(&mut s, MEMORY_CONFIG) >> 7) & 0x1ff; + assert!(now <= last, "the level never rises while the board drains"); + last = now; + } + assert_eq!(last, 0); + assert_eq!(reg_read(&mut s, MEMORY_CONFIG) & 0xff80, 0); +} + +/// MODEL CONTRACT ONLY, no evidence for the hardware: while stores wait the +/// model's status is neither empty nor below its low-water threshold; it +/// reads empty and low again once they have run. +/// +/// QEMU: command-work-status-shows-waiting-stores. +#[test] +fn the_status_shows_waiting_stores_until_they_have_run() { + let (mut s, _, _) = queued_board(); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1003_0000); + + push_fences(&mut s); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1002_0000, "not empty, still below the threshold"); + push_fences(&mut s); + push_fences(&mut s); + assert_eq!(s.fifo.level(), 18); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1000_0000); + + while s.fifo.level() > LOW_WATER_MARK { + step(&mut s); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1000_0000); + } + step(&mut s); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1002_0000, "below the threshold once fewer than 16 wait"); + + finish(&mut s); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1003_0000); +} + +/// MODEL CONTRACT ONLY, no evidence for the hardware: the roles of `0x6b2` +/// are unknown. The level counts the FIFO's 32-bit words (a model choice): a +/// doubleword store fills two. The model reads status bit 15 set at or above +/// the written `0x6b2`, and reaching it raises flags bit 25; with no `0x6b2` +/// written it never reads set. The level stops at the model's 32,768-word +/// FIFO however many stores wait. +#[test] +fn the_level_counts_words_against_the_model_fifo_and_the_6b2_threshold() { + let (mut s, _, _) = queued_board(); + let (high, flag) = (status0::FIFO_HIGH_WATER, intr::FIFO_HIGH_WATER); + + s.mmio_write(consts::cmd::FIFO, 0, 8); + s.mmio_write(consts::cmd::FIFO, 0, 4); + assert_eq!((s.fifo.stores(), s.fifo.level()), (2, 3)); + finish(&mut s); + + // No threshold yet: a FIFO filled to the brim never reads high. + for _ in 0..cfifo::WORDS / 2 { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert_eq!(s.fifo.level(), cfifo::WORDS); + assert_eq!(reg_read(&mut s, ODSY_STATUS0) & high, 0); + // Full: the next store still queues, and the level stays at the brim. + s.mmio_write(consts::cmd::FIFO, 0, 4); + assert_eq!(s.fifo.level(), cfifo::WORDS); + finish(&mut s); + + // The 0x6b2 value the kernel writes, 576 words below the model's size. + cmd64(&mut s, 0x6b2, 32192); + finish(&mut s); + reg_write(&mut s, intr::CLEAR, intr::FIFO_HIGH_WATER); + for _ in 0..32190 / 2 { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert_eq!(reg_read(&mut s, ODSY_STATUS0) & high, 0); + assert_eq!(reg_read(&mut s, intr::FLAGS) & flag, 0); + s.mmio_write(consts::cmd::FIFO, 0, 8); + assert_eq!(s.fifo.level(), 32192); + assert_eq!(reg_read(&mut s, ODSY_STATUS0) & high, high); + assert_eq!(reg_read(&mut s, intr::FLAGS) & flag, flag); + step(&mut s); + assert_eq!(reg_read(&mut s, ODSY_STATUS0) & high, 0, "below the threshold again"); + finish(&mut s); +} + +/// MODEL CONTRACT ONLY, no evidence for the hardware: the roles of `0x6b2` and +/// `0x6b3` are unknown. The model applies `0x6b2` to status bit 15 and only +/// stores `0x6b3`. With `0x6b2` = 12 and `0x6b3` = 8, eight waiting words +/// leave bit 15 clear; swapped, they set it. Flags bit 25 is covered by +/// `the_level_counts_words_against_the_model_fifo_and_the_6b2_threshold`. +/// +/// QEMU: command-fifo-high-mark-follows-6b2. +#[test] +fn the_threshold_follows_6b2() { + let (mut s, _, _) = queued_board(); + let high = status0::FIFO_HIGH_WATER; + + for (mark_6b2, word_6b3, expect) in [(12, 8, 0), (8, 12, high)] { + cmd64(&mut s, 0x6b1, 256); + cmd64(&mut s, 0x6b3, word_6b3); + cmd64(&mut s, 0x6b2, mark_6b2); + finish(&mut s); + for _ in 0..4 { + cmd64(&mut s, 0x6b0, 0); + } + assert_eq!(s.fifo.level(), 8); + assert_eq!(reg_read(&mut s, ODSY_STATUS0) & high, expect, "0x6b2 = {mark_6b2}, 0x6b3 = {word_6b3}"); + finish(&mut s); + } +} + +/// MODEL CONTRACT ONLY: the FIFO's size is unknown and the model's size is +/// its own choice. Whatever `0x6b1` the set-up stores, the level clamps at +/// cfifo::WORDS however many stores wait. +#[test] +fn the_fifo_size_ignores_the_set_up_words() { + for word_6b1 in [1u32, 256, 0x7fff_ffff] { + let (mut s, _, _) = queued_board(); + + cmd64(&mut s, 0x6b1, word_6b1); + finish(&mut s); + for _ in 0..cfifo::WORDS / 2 { + cmd64(&mut s, 0x6b0, 0); + } + assert_eq!(s.fifo.level(), cfifo::WORDS, "0x6b1 = {word_6b1:#x}"); + // Full: the next store still queues, and the level stays at the brim. + s.mmio_write(consts::cmd::FIFO, 0, 4); + assert_eq!(s.fifo.level(), cfifo::WORDS, "0x6b1 = {word_6b1:#x}"); + finish(&mut s); + } +} + +/// MODEL CONTRACT ONLY, no evidence for the hardware: the threshold and the +/// edge are the model's choice. Low water is not raised while 16 or more +/// stores wait, and is raised, with its interrupt, as the level falls below +/// the threshold while stores still wait. +/// +/// QEMU: command-work-low-water-after-drain. +#[test] +fn low_water_is_raised_as_the_queue_falls_below_the_mark() { + let (mut s, rec, _) = queued_board(); + route_to_heart(&mut s); + for _ in 0..3 { + push_fences(&mut s); + } + reg_write(&mut s, intr::CLEAR, intr::FIFO_LOW_WATER); + reg_write(&mut s, intr::ENABLE_SET, intr::FIFO_LOW_WATER); + + // 18 wait; two slices leave 16, still at the mark. + step(&mut s); + step(&mut s); + assert!(rec.events(FLAG_VECTOR).is_empty()); + assert_eq!(reg_read(&mut s, intr::FLAGS) & intr::FIFO_LOW_WATER, 0); + + // The next leaves 15, below the mark. + step(&mut s); + assert_eq!(s.fifo.level(), LOW_WATER_MARK - 1); + assert_eq!(reg_read(&mut s, intr::FLAGS) & intr::FIFO_LOW_WATER, intr::FIFO_LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true]); +} + +/// A hurry ends the board thread's pause before a slice. The pause is set +/// far longer than the wait below, so only the hurry runs the stores in +/// time; the stores wait while the thread sits in it. +#[test] +fn a_hurry_ends_the_board_threads_pause() { + let (s, _rec) = board("v12"); + let h = BoardLocks::new(s); + h.fifo.test_gap_ms.store(60_000, SeqCst); + h.work_start(); + // One store starts the thread, and the burst waits until the thread is + // in its pause: a burst that lands before it would find the request + // already made and prove nothing about waking a sleeper. + h.write(consts::cmd::FIFO, 0, 8); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + while h.fifo.test_parked.load(SeqCst) == 0 { + assert!(std::time::Instant::now() < deadline, "the board's thread never reached its pause"); + std::thread::yield_now(); + } + for _ in 1..200 { + h.write(consts::cmd::FIFO, 0, 8); + } + assert_eq!(h.fifo.stores(), 200, "the stores wait out the pause"); + h.fifo.hurry(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + while h.fifo.stores() != 0 { + assert!(std::time::Instant::now() < deadline, "the board's thread kept its pause: {} stores wait", h.fifo.stores()); + std::thread::yield_now(); + } + h.work_stop(); +} + +/// A stopping machine finishes the work waiting; a reset abandons it. +/// +/// QEMU: command-work-stop-and-reset. +#[test] +fn stopping_finishes_the_waiting_work_and_a_reset_abandons_it() { + let (mut s, _, ram) = queued_board(); + push_fences(&mut s); + let h = BoardLocks::new(s); + + h.work_stop(); + assert!(!h.fifo.pending()); + assert_eq!(ram.half(HOST_WRITE_BASE + 6), 0xa5a5); + + let mut s = h.test_board(); + for w in [select(reg::HOST_WRITE), 4, 0x1234] { + cmd(&mut s, w); + } + assert!(s.work_pending()); + s.reset(); + assert!(!s.work_pending()); + assert_eq!(ram.half(HOST_WRITE_BASE + 8), 0); +} + +/// Two processors storing to the command FIFO at once (`[machine] cpus = 2`) +/// each find their stores come out in the order they made them, and none is +/// lost or run twice. The producers share no lock but the ring's own, so +/// one can find the ring's lock taken and have its store refused; it tries +/// again, as the bus's retry does. +#[test] +fn two_processors_storing_to_the_fifo_lose_nothing_and_keep_each_ones_order() { + const PER: u64 = 30_000; + let (s, _) = board("v12"); + let h = Arc::new(BoardLocks::new(s)); + let done = Arc::new(std::sync::atomic::AtomicU32::new(0)); + let producers: Vec<_> = (0..2u64) + .map(|id| { + let (h, done) = (h.clone(), done.clone()); + std::thread::spawn(move || { + // A panic in a push must end the test, not hang the consumer. + let pushed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + for seq in 0..PER { + let size = if seq % 3 == 0 { 8 } else { 4 }; + while !h.fifo.push(&h.front, consts::cmd::FIFO, id << 32 | seq, size) { + std::thread::yield_now(); + } + } + })); + done.fetch_add(1, SeqCst); + pushed.is_ok() + }) + }) + .collect(); + + let mut next = [0u64; 2]; + let mut run = Run::new(); + loop { + h.fifo.take_run(&mut run); + let n = run.stores().len(); + if n == 0 { + if done.load(SeqCst) == 2 && h.fifo.stores() == 0 { + break; + } + std::thread::yield_now(); + continue; + } + for q in run.stores() { + let id = (q.value >> 32) as usize; + assert_eq!(q.value & 0xffff_ffff, next[id], "processor {id}'s stores arrive in order"); + next[id] += 1; + } + h.fifo.retire(&h.front, &run, n); + } + for p in producers { + assert!(p.join().unwrap(), "a push panicked"); + } + assert_eq!(next, [PER, PER]); + assert_eq!((h.fifo.stores(), h.fifo.level()), (0, 0)); +} + +/// The board's thread can take a store out of the ring the moment it lands, +/// before the processor's push has returned. The level already counts it +/// then: the retire takes off no more words than the level holds, so the +/// level reads empty rather than wrapping to a full FIFO, and the fall +/// from 20 words to none latches the low-water flag. +#[test] +fn a_store_run_before_its_push_returns_is_already_in_the_level() { + let (s, _) = board("v12"); + let h = BoardLocks::new(s); + h.front.lock().intr_flags &= !intr::FIFO_LOW_WATER; + for seq in 0..20 { + assert!(h.fifo.push(&h.front, consts::cmd::FIFO, seq, 4)); + } + fn run_everything(fifo: &CommandFifo, front: &Mutex) { + let mut run = Run::new(); + fifo.take_run(&mut run); + let n = run.stores().len(); + assert_eq!(n, 21, "the store being pushed is in the ring"); + fifo.retire(front, &run, n); + assert_eq!((fifo.stores(), fifo.level()), (0, 0), "the run took out what the level counted"); + } + *h.fifo.test_after_ring_push.lock() = Some(run_everything); + assert!(h.fifo.push(&h.front, consts::cmd::FIFO, 20, 8)); + assert!(h.fifo.test_after_ring_push.lock().is_none(), "the consumer ran inside the push"); + assert_eq!((h.fifo.stores(), h.fifo.level()), (0, 0)); + assert_ne!(h.front.lock().intr_flags & intr::FIFO_LOW_WATER, 0, "the level fell through the mark"); +} + +/// The deep ring takes stores past the model's 32,768 words, and the guest +/// still reads the model's own FIFO: the level clamped at the brim, so +/// STATUS0 and the `0x106c` doubleword count read a full FIFO and the +/// high-water flag is up, never a count past the model's size. Level 0 +/// returns only once the backlog has run. +#[test] +fn the_deep_ring_accepts_past_the_brim_and_the_guest_reads_a_full_fifo() { + let (mut s, _, _) = queued_board(); + cmd64(&mut s, 0x6b2, 32192); + finish(&mut s); + reg_write(&mut s, intr::CLEAR, intr::FIFO_HIGH_WATER); + assert_eq!(s.fifo.level(), 0); + assert_ne!(reg_read(&mut s, ODSY_STATUS0) & status0::FIFO_EMPTY, 0, "idle reads empty"); + + for _ in 0..cfifo::WORDS / 2 + 5000 { + s.mmio_write(consts::cmd::FIFO, 0, 8); + } + assert!(s.fifo.queued_words() > cfifo::WORDS, "the ring took more than the model's FIFO"); + assert_eq!(s.fifo.level(), cfifo::WORDS); + let status = reg_read(&mut s, ODSY_STATUS0); + assert_ne!(status & status0::FIFO_HIGH_WATER, 0); + assert_eq!(status & (status0::FIFO_EMPTY | status0::FIFO_LOW_WATER), 0); + assert_ne!(reg_read(&mut s, intr::FLAGS) & intr::FIFO_HIGH_WATER, 0); + assert_eq!((reg_read(&mut s, MEMORY_CONFIG) >> 7) & 0x1ff, 0x1ff, "the doubleword count is at its field's top"); + + // Draining the backlog keeps the guest's view pinned at the brim until + // the ring is under it, and level 0 only when everything has run. + s.work_step(&mut Budget::stores(1000)); + assert_eq!(s.fifo.level(), cfifo::WORDS); + finish(&mut s); + assert_eq!(s.fifo.level(), 0); + let status = reg_read(&mut s, ODSY_STATUS0); + assert_eq!(status & status0::FIFO_HIGH_WATER, 0); + assert_ne!(status & status0::FIFO_EMPTY, 0); + assert_eq!((reg_read(&mut s, MEMORY_CONFIG) >> 7) & 0x1ff, 0); +} + +/// A store is refused with the bus busy only when the host ring is full, not +/// at the model's 32,768 words, and the guest never reads past the brim. +#[test] +fn the_bus_is_busy_only_when_the_ring_is_full() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + start_long_slice(&h, &words, 3000); + // The slice's own first word may have left the ring: count what waits. + while h.fifo.stores() < RING_STORES { + assert!(h.bus_write(consts::cmd::FIFO, 0, 4), "refused at {} stores, {} words", h.fifo.stores(), h.fifo.queued_words()); + assert!(h.fifo.level() <= cfifo::WORDS); + } + assert_eq!(h.fifo.stores(), RING_STORES); + assert!(!h.bus_write(consts::cmd::FIFO, 0, 4), "full refuses"); + assert_eq!(h.fifo.stores(), RING_STORES); + assert_eq!(h.fifo.level(), cfifo::WORDS); + h.fifo.test_words.store(0, SeqCst); + h.work_stop(); + assert_eq!(h.fifo.level(), 0, "idle reads zero"); +} + +/// The flow-control target word is stored, not routed, so a store to it +/// goes in at once while the FIFO holds queued work: the bus answers +/// ready, no refusal is counted, and the word reads back from the front. +#[test] +fn a_flow_control_target_store_does_not_wait_for_the_fifo() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + + start_long_slice(&h, &words, 2000); + assert!(h.bus_write(consts::cmd::FIFO, 0, 4)); + assert!(h.fifo.pending(), "work is queued behind the slice in flight"); + + let word = 1 << 17 | 0x1b << 12 | 0xc8; + assert!(h.bus_write(flctl::TARGET, word, 4), "the store is accepted at once"); + assert_eq!(h.front.lock().flctl_target as u64, word); + assert_eq!(h.bus_read(flctl::TARGET, 4), Some(word)); + assert!(h.fifo.pending(), "it did not drain the FIFO"); + + let mut report = Vec::new(); + h.refusals.report(&mut report).unwrap(); + let report = String::from_utf8(report).unwrap(); + assert!(report.contains("reason store-waits-fifo 0"), "{report}"); + assert!(!report.contains("offset 0x1074"), "{report}"); + h.work_stop(); +} + +/// Both processors can find room for one more store in a ring that has +/// room for one. The check and the push are not one step, so the second +/// push meets a full ring: exactly one store goes in, the other is +/// refused rather than lost or overrunning the ring, and the levels count +/// what is in it. +#[test] +fn two_processors_racing_for_the_last_place_in_the_ring_leave_one_store_in() { + const ROUNDS: u32 = 24; + let (s, _) = board("v12"); + let h = Arc::new(BoardLocks::new(s)); + // The ring has room for one more store when it holds RING_STORES - 1. + let fill = |h: &BoardLocks| { + for _ in 0..RING_STORES - 1 { + assert!(h.fifo.push(&h.front, consts::cmd::FIFO, 0, 4)); + } + }; + fill(&h); + let start = Arc::new(std::sync::Barrier::new(3)); + let end = Arc::new(std::sync::Barrier::new(3)); + let accepted = Arc::new(std::sync::atomic::AtomicU32::new(0)); + let racers: Vec<_> = (0..2) + .map(|_| { + let (h, start, end, accepted) = (h.clone(), start.clone(), end.clone(), accepted.clone()); + std::thread::spawn(move || { + let mut panics = 0; + for _ in 0..ROUNDS { + start.wait(); + // A panic in a push must fail the round, not leave the others waiting. + let pushed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| h.fifo.push(&h.front, consts::cmd::FIFO, 0, 4))); + match pushed { + Ok(true) => { + accepted.fetch_add(1, SeqCst); + } + Ok(false) => {} + Err(_) => panics += 1, + } + end.wait(); + } + panics + }) + }) + .collect(); + + let mut run = Run::new(); + for round in 0..ROUNDS { + accepted.store(0, SeqCst); + start.wait(); + end.wait(); + assert_eq!(accepted.load(SeqCst), 1, "round {round}: one place, one store"); + assert_eq!((h.fifo.stores(), h.fifo.queued_words()), (RING_STORES, RING_STORES)); + // Empty the ring, which also publishes the consumer's head to the + // producers, and fill it again. + while h.fifo.stores() != 0 { + h.fifo.take_run(&mut run); + let n = run.stores().len(); + h.fifo.retire(&h.front, &run, n); + } + fill(&h); + } + for r in racers { + assert_eq!(r.join().unwrap(), 0, "a push panicked"); + } +} + +/// Two processors are serialised against each other and not against the +/// board's thread: while one holds the processors' lock in the middle of an +/// access, a second processor's waiting read, command store and waiting +/// store all wait for it, and the board's thread, which never takes it, runs +/// the queue meanwhile. +#[test] +fn processors_wait_for_each_other_and_the_board_thread_does_not_wait_for_them() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + let held = h.test_submit(); + let started = Arc::new(AtomicU64::new(0)); + let done = Arc::new(AtomicU64::new(0)); + let mut others = Vec::new(); + for which in 0..3u32 { + let (h, started, done) = (h.clone(), started.clone(), done.clone()); + others.push(std::thread::spawn(move || { + started.fetch_add(1, SeqCst); + match which { + 0 => while h.bus_read(cfifo::SELFTEST, 4).is_none() {}, + 1 => assert!(h.bus_write(consts::cmd::FIFO, 0, 4)), + _ => while !h.bus_write(ODSY_BUZZ_1, 1, 4) {}, + } + done.fetch_add(1, SeqCst); + })); + } + while started.load(SeqCst) != 3 { + std::thread::yield_now(); + } + std::thread::sleep(std::time::Duration::from_millis(150)); + assert_eq!(done.load(SeqCst), 0, "a processor ran an access under another's"); + // The board's thread takes no processor lock: it runs what was queued + // before the lock was held. Nothing is queued here, so queue through the + // unlocked ring and see it drain. + for w in &words[..4] { + assert!(h.fifo.push(&h.front, w.port, w.value, w.size as u32)); + } + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while h.fifo.pending() { + assert!(std::time::Instant::now() < deadline, "the board's thread did not run under the processors' lock"); + std::thread::yield_now(); + } + assert_eq!(done.load(SeqCst), 0); + drop(held); + for t in others { + t.join().unwrap(); + } + assert_eq!(done.load(SeqCst), 3); + h.work_stop(); +} + +/// A drain by an access, with the board's thread running, runs nothing: the +/// stores are the thread's to run, in order, and a second runner would be +/// two threads in the board's state. +#[test] +fn a_drain_with_the_board_thread_running_runs_no_store() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + + start_long_slice(&h, &words, 1000); + for w in &words[1..] { + h.write(w.port, w.value, w.size); + } + let (stores, level) = (h.fifo.stores(), h.fifo.level()); + assert!(stores > 1); + // The test stands in for a caller that drains while the thread runs; + // the guard is what keeps it from running anything. + h.test_board().work_drain(cfifo::SELFTEST); + assert_eq!((h.fifo.stores(), h.fifo.level()), (stores, level), "the drain ran stores"); + h.fifo.test_words.store(0, SeqCst); + h.work_stop(); + assert_eq!(h.fifo.level(), 0); +} + +/// A command store the ring refuses while the board's thread runs is not +/// dropped and does not run around the thread: the caller retries the push +/// until the thread has made room, then it goes in once. +#[test] +fn a_refused_command_push_retries_behind_the_board_thread() { + let (h, _rec) = threaded_board(); + let words = preamble_twice(); + + start_long_slice(&h, &words, 1000); + while h.fifo.stores() < RING_STORES { + assert!(h.bus_write(consts::cmd::FIFO, 0, 4)); + } + let stores = h.fifo.stores(); + let done = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let store = { + let (h, done) = (h.clone(), done.clone()); + std::thread::spawn(move || { + h.write(consts::cmd::FIFO, 0, 4); + done.store(true, SeqCst); + }) + }; + std::thread::sleep(std::time::Duration::from_millis(150)); + assert!(!done.load(SeqCst), "the store was dropped or ran around the board's thread"); + assert_eq!(h.fifo.stores(), stores); + store.join().unwrap(); + assert!(h.fifo.level() <= cfifo::WORDS); + h.fifo.test_words.store(0, SeqCst); + h.work_stop(); + assert_eq!(h.fifo.level(), 0); +} diff --git a/src/dev/odyssey/tests/interrupts.rs b/src/dev/odyssey/tests/interrupts.rs new file mode 100644 index 000000000..6bbb67ca9 --- /dev/null +++ b/src/dev/odyssey/tests/interrupts.rs @@ -0,0 +1,439 @@ +//! Interrupt flags, routing and the packets that reach HEART. + +use crate::dev::odyssey::consts::{ + intr, route, ODSY_INT_DEST_LOWER, ODSY_INT_DEST_UPPER, ODSY_STATUS0, ODSY_SW_TOKEN, +}; +use crate::dev::odyssey::tests::{ + board, cmd, reg_read, reg_write, route_to_heart, DMA_VECTOR, FLAG_VECTOR, RETRACE_VECTOR, + SOFTSYNC_VECTOR, +}; +use super::*; + +/// A raised and a lowered board interrupt each reach HEART, setting and +/// clearing the vector's pending bit. +#[test] +fn a_raised_and_a_lowered_board_interrupt_each_reach_heart() { + let (mut s, rec) = board("v12"); + + s.mmio_write(ROUTE_DEST_UPPER, HEART_WIDGET << 25, 4); + s.mmio_write(ROUTE_DEST_LOWER, HEART_PACKET, 4); + s.mmio_write(ROUTE_VECTORS, TEST_VECTOR as u64, 4); + assert!(!rec.level(TEST_VECTOR)); + + // The FIFO is empty, so its low-water flag is already pending. + s.mmio_write(INTR_ENABLE_SET, INTR_FIFO_LOW_WATER, 4); + assert!(rec.level(TEST_VECTOR)); + + s.mmio_write(INTR_CLEAR, INTR_FIFO_LOW_WATER, 4); + assert!(!rec.level(TEST_VECTOR)); +} + + +/// The mask the kernel driver's interrupt set-up writes, from a boot trace. +/// It leaves the low-water source off. +const INIT_MASK: u32 = 0x79e4_259e; +const LOW_WATER: u32 = intr::FIFO_LOW_WATER; +const DMA_DONE: u32 = 0x0000_0800; + +/// The flag, enable and acknowledge trio, and the rule that an enable on +/// its own raises nothing unless a source is already pending: the empty +/// FIFO leaves low water pending out of reset, so enabling asserts. +/// +/// QEMU: interrupt-registers. +#[test] +fn enabling_a_pending_source_asserts_and_acknowledging_clears_it() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + assert_eq!(reg_read(&mut s, intr::FLAGS), LOW_WATER); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), 0); + assert!(rec.events(FLAG_VECTOR).is_empty()); + + reg_write(&mut s, intr::ENABLE_SET, 0xffff_ffff); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), 0xffff_ffff); + assert!(rec.level(FLAG_VECTOR)); + + // Write-one-to-clear over the flags, with nothing readable behind it. + reg_write(&mut s, intr::CLEAR, 0xffff_ffff); + assert_eq!(reg_read(&mut s, intr::FLAGS), 0); + assert_eq!(reg_read(&mut s, intr::CLEAR), 0); + assert!(!rec.level(FLAG_VECTOR)); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), 0xffff_ffff, "clearing flags leaves the enables alone"); + + // The software-sync pair: a token written, then read back. + reg_write(&mut s, ODSY_SW_TOKEN, 0x5a5a_5a5a); + assert_eq!(reg_read(&mut s, ODSY_SW_TOKEN), 0x5a5a_5a5a); +} + +/// The enable mask's two halves, the way the driver drives them: the set +/// half adds the bits written and reads back the whole mask, the clear +/// half removes them. Clearing an enable leaves its flag pending. +/// +/// QEMU: interrupt-enable-set-and-clear. +#[test] +fn the_enable_mask_is_set_and_cleared_by_its_two_halves() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + + // The initial mask leaves low water off: no level. + reg_write(&mut s, intr::ENABLE_SET, INIT_MASK); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), INIT_MASK); + assert!(!rec.level(FLAG_VECTOR)); + + // The high-water path adds one bit; the rest of the mask stays. + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), INIT_MASK | LOW_WATER); + assert!(rec.level(FLAG_VECTOR)); + + // The low-water handler removes it: the level drops, the flag stays. + reg_write(&mut s, intr::ENABLE_CLEAR, LOW_WATER); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), INIT_MASK); + assert_eq!(reg_read(&mut s, intr::FLAGS) & LOW_WATER, LOW_WATER); + assert!(!rec.level(FLAG_VECTOR)); + + // Disabling with all ones empties the mask. + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + reg_write(&mut s, intr::ENABLE_CLEAR, 0xffff_ffff); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), 0); + assert!(!rec.level(FLAG_VECTOR)); +} + +/// The flag handler's acknowledge writes the mask it dispatched on and +/// clears those flags and nothing else. The DMA submission's write of +/// 0x800 clears a flag the model never raises, so it changes nothing. +/// +/// QEMU: interrupt-clear-acknowledges-flags. +#[test] +fn an_acknowledge_clears_only_the_flags_it_names() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert!(rec.level(FLAG_VECTOR)); + + reg_write(&mut s, intr::CLEAR, DMA_DONE); + assert_eq!(reg_read(&mut s, intr::FLAGS), LOW_WATER); + assert!(rec.level(FLAG_VECTOR)); + + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert_eq!(reg_read(&mut s, intr::FLAGS), 0); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), LOW_WATER); + assert!(!rec.level(FLAG_VECTOR)); +} + +/// A driver writes the flags register itself: its flow control stores the +/// low-water bit before enabling that source, and a DMA diagnostic clears +/// one flag by writing back the flags it read without it. A write replaces +/// the flags, and the interrupt follows. +/// +/// QEMU: interrupt-flags-take-written-value. +#[test] +fn a_write_to_the_flags_replaces_them() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert!(!rec.level(FLAG_VECTOR)); + + reg_write(&mut s, intr::FLAGS, LOW_WATER); + assert_eq!(reg_read(&mut s, intr::FLAGS), LOW_WATER); + assert!(rec.level(FLAG_VECTOR)); + + // Without the low-water bit it is dropped, as a set-only write would not. + reg_write(&mut s, intr::FLAGS, DMA_DONE); + assert_eq!(reg_read(&mut s, intr::FLAGS), DMA_DONE); + assert!(!rec.level(FLAG_VECTOR)); + + reg_write(&mut s, intr::FLAGS, 0); + assert_eq!(reg_read(&mut s, intr::FLAGS), 0); +} + +/// A submission that does not wait never lifts the FIFO to its low-water +/// mark, so it raises nothing where the driver has disabled the source. +/// The driver's high-water path then latches the flag itself through the +/// flags register: no interrupt until it enables the source, and that +/// asserts at once. +/// +/// QEMU: lowater-not-raised-while-disabled. +#[test] +fn low_water_is_not_raised_while_the_source_is_disabled() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + reg_write(&mut s, intr::ENABLE_SET, INIT_MASK | LOW_WATER); + reg_write(&mut s, intr::ENABLE_CLEAR, LOW_WATER); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert!(!rec.level(FLAG_VECTOR)); + + cmd(&mut s, 0x0001_0046); + assert_eq!(reg_read(&mut s, intr::FLAGS) & LOW_WATER, 0); + assert_eq!(reg_read(&mut s, ODSY_STATUS0) & 0x20000, 0x20000); + + reg_write(&mut s, intr::FLAGS, LOW_WATER); + assert!(!rec.level(FLAG_VECTOR), "latched, but the source is off"); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert!(rec.level(FLAG_VECTOR)); +} + +/// The status register the driver's periodic credit check reads: with the +/// command FIFO empty it reports enabled, empty and below the low-water +/// mark. A clear low-water or enabled bit makes the driver log a graphics +/// error and take its board-reset recovery path. +/// +/// QEMU: board-status-reports-fifo-ready. +#[test] +fn the_board_status_reports_an_empty_fifo_as_ready() { + let (mut s, _) = board("v12"); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1003_0000); +} + +/// The low-water interrupt the driver waits on to replenish its credits: +/// pending out of reset, so enabling asserts; acknowledging clears it; and +/// a later submission that does not wait leaves it clear. Without the +/// reset-pending latch the kernel's post-reset watchdog never sees it and +/// gives up starting the X server. +/// +/// QEMU: command-fifo-lowater-interrupt. +#[test] +fn the_reset_latched_low_water_interrupt_asserts_once_and_stays_clear() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert_eq!(reg_read(&mut s, intr::FLAGS) & LOW_WATER, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true]); + + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true, false], "clearing the flag sends the clearing packet"); + + cmd(&mut s, 0x0001_0046); + assert_eq!(reg_read(&mut s, intr::FLAGS) & LOW_WATER, 0); + assert_eq!(rec.events(FLAG_VECTOR), [true, false]); +} + +/// A reset lowers the flag level with the flags and enables, quietly: the +/// destination is reset too, so there is nowhere to send a clearing packet +/// and none is sent. The next edge is a rising one and sends its set +/// packet; a different flag vector after the reset tells it from the one +/// before. +/// +/// QEMU: reset-clears-flag-level. +#[test] +fn a_reset_lowers_the_flag_level_without_a_packet() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true]); + + s.reset(); + assert_eq!(rec.events(FLAG_VECTOR), [true], "the reset sends no clearing packet"); + + route_to_heart(&mut s); + reg_write(&mut s, route::VECTORS, FLAG_VECTOR + 1); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR + 1), [true]); + assert_eq!(rec.events(FLAG_VECTOR), [true]); +} + +/// The packet an edge composes decodes the routed vector and destination, +/// and nothing is sent before an edge or with no destination programmed. +/// The crosstalk error destination reads back and carries nothing. +/// +/// QEMU: interrupt-packet-composition. +#[test] +fn a_packet_needs_a_destination_and_an_edge() { + let (mut s, rec) = board("v12"); + + // No destination: the rising edge sends nothing but is held. + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert!(rec.silent()); + + reg_write(&mut s, ODSY_INT_DEST_UPPER, 0x1234_5678); + reg_write(&mut s, ODSY_INT_DEST_LOWER, 0x9abc_def0); + assert_eq!(reg_read(&mut s, ODSY_INT_DEST_UPPER), 0x1234_5678); + assert_eq!(reg_read(&mut s, ODSY_INT_DEST_LOWER), 0x9abc_def0); + assert!(rec.silent(), "the crosstalk error destination carries nothing"); + route_to_heart(&mut s); + assert_eq!(rec.events(FLAG_VECTOR), [true], "the held level goes out with the routing"); + + // Acknowledging the still-pending flag is the falling edge. + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true, false]); + + // The flag set again readdresses the same destination, setting it. + reg_write(&mut s, intr::FLAGS, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true, false, true]); +} + +/// MODEL CHOICE: the kernel can +/// raise low water through flow control before its interrupt set-up +/// programs the routing, so a flag raised with no destination is held and +/// sent once the routing is written, in the kernel's order (vectors, lower +/// half, upper half), and only once. A level that falls while held sends +/// nothing. +#[test] +fn a_flag_raised_before_the_routing_is_sent_once_it_is_programmed() { + let (mut s, rec) = board("v12"); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert!(rec.silent(), "no packet without a destination"); + + reg_write(&mut s, route::VECTORS, SOFTSYNC_VECTOR << 24 | RETRACE_VECTOR << 16 | DMA_VECTOR << 8 | FLAG_VECTOR); + reg_write(&mut s, route::DEST_LOWER, HEART_PACKET as u32); + assert!(rec.silent(), "half a destination takes no packet"); + reg_write(&mut s, route::DEST_UPPER, (HEART_WIDGET << 25) as u32); + assert_eq!(rec.events(FLAG_VECTOR), [true]); + + // Writing the routing again sends nothing more. + route_to_heart(&mut s); + assert_eq!(rec.0.lock().len(), 1, "exactly one packet"); + + // A level that rises and falls before any routing sends nothing. + s.reset(); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + route_to_heart(&mut s); + assert_eq!(rec.0.lock().len(), 1, "a level gone before the routing sends nothing"); +} + +/// A level that falls while its packet is held sends nothing, not even a +/// clear, with a routing only partly programmed. Both halves of the +/// destination without the vector table is a partial routing that composes +/// a packet address, so only the rule that all three are written keeps it +/// silent. Writing the rest of the routing afterwards finds no level and +/// sends nothing. +#[test] +fn a_flag_that_falls_while_held_sends_nothing() { + let (mut s, rec) = board("v12"); + reg_write(&mut s, route::VECTORS, SOFTSYNC_VECTOR << 24 | RETRACE_VECTOR << 16 | DMA_VECTOR << 8 | FLAG_VECTOR); + reg_write(&mut s, route::DEST_LOWER, HEART_PACKET as u32); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert!(rec.silent(), "vectors and a lower half are no destination"); + + s.reset(); + reg_write(&mut s, route::DEST_UPPER, (HEART_WIDGET << 25) as u32); + reg_write(&mut s, route::DEST_LOWER, HEART_PACKET as u32); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert!(rec.silent(), "both halves without the vectors take no packet"); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert!(rec.silent(), "a held level that falls was never sent, so it sends no clear"); + + reg_write(&mut s, route::VECTORS, SOFTSYNC_VECTOR << 24 | RETRACE_VECTOR << 16 | DMA_VECTOR << 8 | FLAG_VECTOR); + assert!(rec.silent(), "the level is gone when the routing is complete"); +} + +/// A reset drops a held flag with the level: programming the routing +/// afterwards sends nothing, and the next rise and fall is an ordinary +/// pair of packets. +#[test] +fn a_reset_drops_a_held_flag() { + let (mut s, rec) = board("v12"); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert!(rec.silent(), "no packet without a destination"); + + s.reset(); + route_to_heart(&mut s); + assert!(rec.silent(), "the reset dropped the held flag"); + + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true, false], "a rise and its fall are both sent"); +} + +/// The kernel gives the routed table's low byte to its flag handler and +/// the crosstalk error destination to its error handler, so low water goes +/// through the routed destination with the flag vector. +/// +/// QEMU: flag-interrupt-uses-routed-vector. +#[test] +fn the_flag_interrupt_uses_the_routed_flag_vector() { + let (mut s, rec) = board("v12"); + reg_write(&mut s, ODSY_INT_DEST_UPPER, 0x7000_0000); + reg_write(&mut s, ODSY_INT_DEST_LOWER, 0x40); + route_to_heart(&mut s); + reg_write(&mut s, route::VECTORS, (SOFTSYNC_VECTOR << 24) | 4); + + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert_eq!(rec.events(4), [true]); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert_eq!(rec.events(4), [true, false]); +} + +/// Program the routing with `upper` as the destination's upper half and +/// raise the retrace, softsync and DMA-done sources once each, in that +/// order. The returned vectors are the sources that sent a packet. +fn raise_each_source(upper: u32) -> Vec { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + reg_write(&mut s, route::DEST_UPPER, upper); + s.retrace(); + s.front.lock().deliver_softsync(); + s.dma_done(); + [RETRACE_VECTOR, SOFTSYNC_VECTOR, DMA_VECTOR].into_iter().filter(|v| rec.events(*v) == [true]).collect() +} + +const HEART_UPPER: u32 = (HEART_WIDGET << 25) as u32; + +/// MODEL CHOICE: each of bits 16, 17 +/// and 18 of the destination's upper half enables one non-flag source (16 +/// DMA-done, 17 retrace, 18 softsync). A source raised while its bit is +/// clear sends nothing and is dropped; the other two are unaffected. The +/// bit-to-source assignment is the model's, not a measurement. +#[test] +fn a_clear_source_enable_bit_suppresses_only_its_source() { + for (bit, silent) in [ + (route::ENABLE_DMA, DMA_VECTOR), + (route::ENABLE_RETRACE, RETRACE_VECTOR), + (route::ENABLE_SOFTSYNC, SOFTSYNC_VECTOR), + ] { + let sent = raise_each_source(HEART_UPPER | (ROUTE_ENABLES & !bit)); + let want: Vec = [RETRACE_VECTOR, SOFTSYNC_VECTOR, DMA_VECTOR].into_iter().filter(|v| *v != silent).collect(); + assert_eq!(sent, want, "bit {:#x} clear suppresses vector {}", bit, silent); + } +} + +/// The control for the test above: with all three bits set every source +/// sends its packet, and with all three clear none does. +#[test] +fn a_set_source_enable_bit_sends_its_source() { + assert_eq!(raise_each_source(HEART_UPPER | ROUTE_ENABLES), [RETRACE_VECTOR, SOFTSYNC_VECTOR, DMA_VECTOR]); + for bit in [route::ENABLE_DMA, route::ENABLE_RETRACE, route::ENABLE_SOFTSYNC] { + let sent = raise_each_source(HEART_UPPER | bit); + assert_eq!(sent.len(), 1, "bit {:#x} alone sends exactly one source", bit); + } + assert!(raise_each_source(HEART_UPPER).is_empty(), "no enable bit set sends nothing"); +} + +/// The value the shipped kernel stores in the upper half, 0x35cf8000, has +/// all three enable bits set. Its address (15:0) and target widget (28:25) +/// are replaced with HEART's so the packets can be recorded; every other bit +/// is the kernel's. All three sources are delivered. +#[test] +fn the_kernels_upper_half_enables_every_source() { + let kernel = 0x35cf_8000u32; + assert_eq!(kernel & ROUTE_ENABLES, ROUTE_ENABLES); + let upper = (kernel & !(0xffff | (0xf << 25))) | HEART_UPPER; + assert_eq!(raise_each_source(upper), [RETRACE_VECTOR, SOFTSYNC_VECTOR, DMA_VECTOR]); +} + +/// The flag has no bit in the upper half: it is gated by the enable mask +/// alone, so a low-water flag is delivered with every source enable clear. +#[test] +fn the_flag_ignores_the_source_enable_bits() { + let (mut s, rec) = board("v12"); + route_to_heart(&mut s); + reg_write(&mut s, route::DEST_UPPER, HEART_UPPER); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true]); + reg_write(&mut s, intr::CLEAR, LOW_WATER); + assert_eq!(rec.events(FLAG_VECTOR), [true, false]); +} + +/// A flag held for want of a routing is sent when the routing completes +/// without the enable bits. +#[test] +fn a_held_flag_is_sent_with_the_source_enables_clear() { + let (mut s, rec) = board("v12"); + reg_write(&mut s, intr::ENABLE_SET, LOW_WATER); + reg_write(&mut s, route::VECTORS, SOFTSYNC_VECTOR << 24 | RETRACE_VECTOR << 16 | DMA_VECTOR << 8 | FLAG_VECTOR); + reg_write(&mut s, route::DEST_LOWER, HEART_PACKET as u32); + reg_write(&mut s, route::DEST_UPPER, HEART_UPPER); + assert_eq!(rec.events(FLAG_VECTOR), [true]); +} diff --git a/src/dev/odyssey/tests/marks.rs b/src/dev/odyssey/tests/marks.rs new file mode 100644 index 000000000..98aafc287 --- /dev/null +++ b/src/dev/odyssey/tests/marks.rs @@ -0,0 +1,276 @@ +//! The ordering rules between the threads that write the planes and the +//! display that copies them, checked on small models over every +//! interleaving of their steps. +//! +//! The steps run in sequentially consistent order, so these tests pin the +//! program order the rules need (store, then mark; take, then read), not +//! the Release and Acquire the marks carry on a weaker machine. Each rule +//! has a negative control: the same schedule with the order reversed must +//! leave a stale row, or the model proves nothing. + +use crate::dev::odyssey::scanout::{DirtySite, RowMarks}; +use std::cell::{Cell, RefCell}; + +const ROWS: usize = 4; + +/// Everything a model run shares: the real marks, plane cells, a lookup +/// value, and what the display made of them. +struct Model { + marks: RowMarks, + cells: [Cell; ROWS], + lut: Cell, + lut_seen: Cell, + taken: RefCell>, + /// What the display holds per row: the cell and the lookup it saw. + shown: [Cell; ROWS], +} + +impl Model { + fn new() -> Model { + let marks = RowMarks::default(); + marks.set_height(ROWS as u32); + Model { marks, cells: Default::default(), lut: Cell::new(0), lut_seen: Cell::new(0), taken: Default::default(), shown: Default::default() } + } + + fn take(&self) { + self.taken.borrow_mut().clear(); + self.marks.take_dirty(ROWS, &mut self.taken.borrow_mut()); + } + + fn see_lut(&self) { + self.lut_seen.set(self.lut.get()); + } + + fn copy(&self) { + for &y in self.taken.borrow().iter() { + self.shown[y as usize].set(self.cells[y as usize].get() + 100 * self.lut_seen.get()); + } + } + + /// A quiet frame after the writers stopped: take, look, copy. + fn settle(&self) { + self.take(); + self.see_lut(); + self.copy(); + } + + fn stale_rows(&self) -> Vec { + (0..ROWS).filter(|&y| self.shown[y].get() != self.cells[y].get() + 100 * self.lut.get()).collect() + } +} + +type Step = fn(&Model); + +/// Run every interleaving of the writer's steps with the display's, each +/// from a fresh model, then a quiet frame. The schedules that end with a +/// stale row. +fn failing_schedules(writer: &[Step], display: &[Step]) -> usize { + let (n, m) = (writer.len(), display.len()); + let mut failing = 0; + + for pattern in 0u32..1 << (n + m) { + if pattern.count_ones() as usize != n { + continue; + } + let model = Model::new(); + let (mut w, mut d) = (0, 0); + + for step in 0..n + m { + if pattern >> step & 1 != 0 { + writer[w](&model); + w += 1; + } else { + display[d](&model); + d += 1; + } + } + model.settle(); + if !model.stale_rows().is_empty() { + failing += 1; + } + } + failing +} + +fn store_a(m: &Model) { + m.cells[1].set(7); +} +fn mark_a(m: &Model) { + m.marks.invalidate(DirtySite::Pixel, 1, 1); +} +fn store_b(m: &Model) { + m.cells[2].set(9); +} +fn mark_b(m: &Model) { + m.marks.invalidate(DirtySite::Pixel, 2, 1); +} + +/// Two frames of the display, each taking the marks and then copying the +/// rows it took. +const FRAMES: [Step; 4] = [Model::take, Model::copy, Model::take, Model::copy]; + +/// A writer that stores its pixels and then marks them leaves no stale row +/// whenever the display's frames fall; the reverse order does. +#[test] +fn a_row_marked_after_its_pixels_is_never_left_stale() { + assert_eq!(failing_schedules(&[store_a, mark_a, store_b, mark_b], &FRAMES), 0); + assert!(failing_schedules(&[mark_a, store_a, mark_b, store_b], &FRAMES) > 0, "marking first must be able to lose a row"); +} + +fn set_lut(m: &Model) { + m.lut.set(1); +} +fn repaint_all(m: &Model) { + m.marks.repaint(DirtySite::Lut, 0, ROWS as i32); +} + +/// The display takes the marks and only then reads what they cover (the +/// lookup RAM, the display modes and the cursor): the other order resolves +/// with the old table and drops the mark. +#[test] +fn the_lookup_is_read_after_the_marks_are_taken() { + let ordered: [Step; 6] = [Model::take, Model::see_lut, Model::copy, Model::take, Model::see_lut, Model::copy]; + let reversed: [Step; 6] = [Model::see_lut, Model::take, Model::copy, Model::see_lut, Model::take, Model::copy]; + + assert_eq!(failing_schedules(&[set_lut, repaint_all], &ordered), 0); + assert!(failing_schedules(&[set_lut, repaint_all], &reversed) > 0, "reading the lookup first must be able to lose the repaint"); +} + +/// The display's own clear, measure, set of the lines a writer did not +/// name: a line marked after the clear is measured at the next swap, where +/// a load-modify-store of the whole word would lose it. +#[test] +fn an_unnamed_line_marked_during_a_measure_is_kept_for_the_next() { + let marks = RowMarks::default(); + marks.set_height(8); + marks.invalidate(DirtySite::Pixel, 2, 1); + + let taken = marks.take_unnamed(); + assert_eq!(taken[0], 1 << 2); + // A write lands while the display measures. + marks.invalidate(DirtySite::Pixel, 3, 1); + assert!(marks.unnamed_pending(), "the later line stays marked"); + assert_eq!(marks.take_unnamed()[0], 1 << 3); +} + +/// The swap model: a writer stores into the buffer a window does not show +/// and marks the line; a swap shows that buffer and repaints the lines the +/// buffers differ in, found among the lines marked unnamed. A line that +/// the display took as dirty before the swap, but that was not yet named +/// unnamed when the swap measured, would stay stale for good. +struct Swap { + marks: RowMarks, + buffer: [Cell; 2], + displayed: Cell, + shown: Cell, + taken: RefCell>, +} + +impl Swap { + fn run(writer: &[fn(&Swap)], display: &[fn(&Swap)]) -> usize { + let (n, m) = (writer.len(), display.len()); + let mut failing = 0; + + for pattern in 0u32..1 << (n + m) { + if pattern.count_ones() as usize != n { + continue; + } + let model = Swap { marks: RowMarks::default(), buffer: Default::default(), displayed: Cell::new(0), shown: Cell::new(0), taken: Default::default() }; + model.marks.set_height(1); + let (mut w, mut d) = (0, 0); + + for step in 0..n + m { + if pattern >> step & 1 != 0 { + writer[w](&model); + w += 1; + } else { + display[d](&model); + d += 1; + } + } + model.frame_take(); + model.frame_copy(); + if model.shown.get() != model.buffer[model.displayed.get()].get() { + failing += 1; + } + } + failing + } + + fn frame_take(&self) { + self.taken.borrow_mut().clear(); + self.marks.take_dirty(1, &mut self.taken.borrow_mut()); + } + fn frame_copy(&self) { + if !self.taken.borrow().is_empty() { + self.shown.set(self.buffer[self.displayed.get()].get()); + } + } + /// The retrace latches the swap and the display repaints the lines + /// where the buffers differ among those not named. + fn flip(&self) { + self.displayed.set(1); + let unnamed = self.marks.take_unnamed(); + if unnamed[0] & 1 != 0 && self.buffer[0].get() != self.buffer[1].get() { + self.marks.repaint(DirtySite::Flip, 0, 1); + } + } + fn store(&self) { + self.buffer[1].set(5); + } + fn name(&self) { + self.marks.name_unnamed(0, 1); + } + fn mark(&self) { + self.marks.repaint(DirtySite::Pixel, 0, 1); + } +} + +/// A line is named unnamed before it is marked dirty: in that order a swap +/// never misses a line whose buffers differ, and in the other it can. +#[test] +fn a_line_is_named_unnamed_before_it_is_marked_dirty() { + let display: [fn(&Swap); 5] = [Swap::frame_take, Swap::frame_copy, Swap::flip, Swap::frame_take, Swap::frame_copy]; + assert_eq!(Swap::run(&[Swap::store, Swap::name, Swap::mark], &display), 0); + assert!(Swap::run(&[Swap::store, Swap::mark, Swap::name], &display) > 0, "marking dirty first must be able to lose the swap's repaint"); +} + +/// Marks past the frame are dropped, and a new height forgets the old marks. +#[test] +fn marks_are_bounded_by_the_frame() { + let marks = RowMarks::default(); + let mut rows = Vec::new(); + + marks.repaint(DirtySite::Other, 0, 10); + marks.take_dirty(1024, &mut rows); + assert!(rows.is_empty(), "no frame, no marks"); + + marks.set_height(100); + marks.repaint(DirtySite::Other, 90, 50); + marks.take_dirty(100, &mut rows); + assert_eq!(rows, (90..100).collect::>()); + + marks.repaint(DirtySite::Other, 0, 3); + marks.set_height(50); + rows.clear(); + marks.take_dirty(50, &mut rows); + assert!(rows.is_empty()); +} + +/// invalidate() itself names the lines unnamed before it marks them dirty: +/// at the point between its two marks the unnamed one is there and the +/// dirty one is not. +#[test] +fn invalidate_names_the_lines_before_it_marks_them_dirty() { + let marks = std::sync::Arc::new(RowMarks::default()); + marks.set_height(8); + let seen = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); + let s = seen.clone(); + *marks.test_between.lock() = Some(Box::new(move |m: &RowMarks| { + assert!(m.unnamed_pending(), "the line is not named unnamed yet"); + assert!(m.dirty_rows().is_empty(), "the line is already dirty"); + s.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + })); + marks.invalidate(DirtySite::Pixel, 3, 1); + assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 1); +} diff --git a/src/dev/odyssey/tests/mod.rs b/src/dev/odyssey/tests/mod.rs new file mode 100644 index 000000000..20e8b2fa4 --- /dev/null +++ b/src/dev/odyssey/tests/mod.rs @@ -0,0 +1,225 @@ +//! Unit tests for the VPro (Odyssey) board model. +//! +//! The cases are register-contract tests of the board, grounded in captures +//! from real boards. A doc comment names the QEMU case a test comes from. +//! Cases about other machines' fabrics, QOM properties, migration streams +//! or the rasteriser's pixel maths are not repeated here. +//! +//! The tests reach the board through `Board` and `Odyssey`, the same +//! surface the machine uses: memory-mapped reads and writes, and the +//! command stream pushed through the command-FIFO port. With no board +//! thread running, a command store runs in the store, which is how a +//! test sees its effect at once. + +use super::board::Board; +use super::consts::{cmd, route}; +use crate::irq::InterruptController; +use crate::traits::BusDevice; +use parking_lot::Mutex; +use std::sync::Arc; + +mod catalogue; +mod display; +mod dma; +mod fifo; +mod interrupts; +mod marks; +mod objects; +mod pixels; +mod program; +mod query; +mod raster; +mod registers; +mod stop; + +pub(super) const VPRO_WIDGET: u32 = 11; +const HEART_WIDGET: u64 = 8; +const HEART_PACKET: u64 = 0x80; +/// HEART vectors the tests route the board's four interrupt sources to. +pub(super) const FLAG_VECTOR: u32 = 18; +pub(super) const DMA_VECTOR: u32 = 19; +pub(super) const RETRACE_VECTOR: u32 = 20; +pub(super) const SOFTSYNC_VECTOR: u32 = 21; + +// Aperture offsets the older tests name directly. +const IDENTITY: u64 = 0x0004; +const MEMORY_CONFIG: u64 = 0x106c; +const CONTROL: u64 = 0x0024; +const ROUTE_DEST_UPPER: u64 = 0x1044; +const ROUTE_DEST_LOWER: u64 = 0x104c; +const ROUTE_VECTORS: u64 = 0x1054; +pub(super) const ROUTE_ENABLES: u32 = route::ENABLE_DMA | route::ENABLE_RETRACE | route::ENABLE_SOFTSYNC; +const INTR_CLEAR: u64 = 0x101c; +const INTR_ENABLE_SET: u64 = 0x1034; +const INTR_FIFO_LOW_WATER: u64 = 0x200; +const TEST_VECTOR: u32 = 18; +const IDENTITY_DEFAULT: u32 = 0xac013047; + +/// Records every (line, level) the board drives. +#[derive(Default)] +pub(super) struct Recorder(Mutex>); + +impl InterruptController for Recorder { + fn set_line(&self, line: u32, level: bool) { + self.0.lock().push((line, level)); + } +} + +impl Recorder { + /// The level of `line` after the last event on it. + pub(super) fn level(&self, line: u32) -> bool { + self.0.lock().iter().rev().find(|e| e.0 == line).map(|e| e.1).unwrap_or(false) + } + + /// Every event on `line`, oldest first. + pub(super) fn events(&self, line: u32) -> Vec { + self.0.lock().iter().filter(|e| e.0 == line).map(|e| e.1).collect() + } + + /// Whether the board has sent no packet at all. + pub(super) fn silent(&self) -> bool { + self.0.lock().is_empty() + } +} + +pub(super) fn board(product: &str) -> (Box, Arc) { + let rec = Arc::new(Recorder::default()); + let s = Board::new(product, VPRO_WIDGET).expect("board"); + s.front.lock().irq = Some(rec.clone()); + (s, rec) +} + +/// Guest RAM for the board's DMA: a flat big-endian byte array that +/// starts at address zero. +pub(super) struct Ram(Mutex>); + +impl Ram { + pub(super) fn new(len: usize) -> Arc { + Arc::new(Ram(Mutex::new(vec![0; len]))) + } + + pub(super) fn put(&self, addr: u64, bytes: &[u8]) { + self.0.lock()[addr as usize..][..bytes.len()].copy_from_slice(bytes); + } + + pub(super) fn get(&self, addr: u64, len: usize) -> Vec { + self.0.lock()[addr as usize..][..len].to_vec() + } + + pub(super) fn half(&self, addr: u64) -> u16 { + u16::from_be_bytes(self.get(addr, 2).try_into().unwrap()) + } +} + +impl BusDevice for Ram { + fn read8(&self, addr: u64) -> crate::traits::BusRead8 { + match self.0.lock().get(addr as usize) { + Some(b) => crate::traits::BusRead8 { status: crate::traits::BUS_OK, data: *b }, + None => crate::traits::BusRead8::err(), + } + } + fn write8(&self, addr: u64, val: u8) -> u32 { + match self.0.lock().get_mut(addr as usize) { + Some(b) => { + *b = val; + crate::traits::BUS_OK + } + None => crate::traits::BUS_ERR, + } + } + fn read32(&self, addr: u64) -> crate::traits::BusRead32 { + match self.0.lock().get(addr as usize..addr as usize + 4) { + Some(b) => crate::traits::BusRead32 { + status: crate::traits::BUS_OK, + data: u32::from_be_bytes(b.try_into().unwrap()), + }, + None => crate::traits::BusRead32::err(), + } + } + fn write32(&self, addr: u64, val: u32) -> u32 { + match self.0.lock().get_mut(addr as usize..addr as usize + 4) { + Some(b) => { + b.copy_from_slice(&val.to_be_bytes()); + crate::traits::BUS_OK + } + None => crate::traits::BUS_ERR, + } + } + fn read64(&self, addr: u64) -> crate::traits::BusRead64 { + match self.0.lock().get(addr as usize..addr as usize + 8) { + Some(b) => crate::traits::BusRead64 { + status: crate::traits::BUS_OK, + data: u64::from_be_bytes(b.try_into().unwrap()), + }, + None => crate::traits::BusRead64::err(), + } + } + fn write64(&self, addr: u64, val: u64) -> u32 { + match self.0.lock().get_mut(addr as usize..addr as usize + 8) { + Some(b) => { + b.copy_from_slice(&val.to_be_bytes()); + crate::traits::BUS_OK + } + None => crate::traits::BUS_ERR, + } + } +} + +/// A board whose DMA reaches 40 MiB of RAM. +pub(super) fn board_with_ram() -> (Box, Arc, Arc) { + let (mut s, rec) = board("v12"); + let ram = Ram::new(40 << 20); + s.guest = Some(ram.clone()); + (s, rec, ram) +} + +pub(super) fn reg_read(s: &mut Board, off: u64) -> u32 { + s.mmio_read(off, 4) as u32 +} + +pub(super) fn reg_write(s: &mut Board, off: u64, value: u32) { + s.mmio_write(off, value as u64, 4); +} + +/// One command-FIFO word. +pub(super) fn cmd(s: &mut Board, word: u32) { + s.mmio_write(cmd::FIFO, word as u64, 4); +} + +/// Two command-FIFO words in one 64-bit store, upper half first. +pub(super) fn cmd64(s: &mut Board, first: u32, second: u32) { + s.mmio_write(cmd::FIFO, (first as u64) << 32 | second as u64, 8); +} + +/// A register-select word for `reg`, as the driver sends it. +pub(super) fn select(reg: u32) -> u32 { + cmd::REGISTER_TAG | reg +} + +/// Program the routed destination to HEART's packet register, with the +/// four sources on their own vectors and all three source enables set, +/// as the shipped kernel programs it. +pub(super) fn route_to_heart(s: &mut Board) { + reg_write(s, route::DEST_UPPER, (HEART_WIDGET << 25) as u32 | ROUTE_ENABLES); + reg_write(s, route::DEST_LOWER, HEART_PACKET as u32); + reg_write( + s, + route::VECTORS, + SOFTSYNC_VECTOR << 24 | RETRACE_VECTOR << 16 | DMA_VECTOR << 8 | FLAG_VECTOR, + ); +} + +/// The pixel at (x, y) of colour buffer `buffer`, as the plane's 0xRRGGBBAA. +pub(super) fn pixel(s: &Board, buffer: u32, x: i32, y: i32) -> u32 { + let (mem, plane) = if buffer == 0 { (&s.planes.colour, s.planes.colour_plane) } else { (&s.planes.buffer1, s.planes.buffer1_plane) }; + let p = mem.plane(plane).expect("the colour plane"); + mem.plane_load(&p, x, y) +} + +/// Store a pixel into colour buffer 0, as a draw would, and mark its +/// display line for the scanout to resolve again. +pub(super) fn pixel_write(s: &mut Board, x: i32, y: i32, value: u32) { + let p = s.planes.colour.plane(s.planes.colour_plane).expect("the colour plane"); + s.planes.colour.plane_store(&p, x, y, value); + s.scanout.invalidate(y, 1); +} diff --git a/src/dev/odyssey/tests/objects.rs b/src/dev/odyssey/tests/objects.rs new file mode 100644 index 000000000..f243f9572 --- /dev/null +++ b/src/dev/odyssey/tests/objects.rs @@ -0,0 +1,728 @@ +//! Board-memory objects as texture levels: the model's object store keeps +//! what a game uploads, and texture setup finds a level's storage there. + +use crate::dev::odyssey::board::{Board, ODSY_OBJECTS}; +use crate::dev::odyssey::consts::tex; +use crate::dev::odyssey::gl::{texture_detail_sampler, volume_slice_stride, BoardTexels, TextureChain, TextureDetailWeight}; +use crate::dev::odyssey::gl_context::{ + GL_ADD, GL_ALPHA, GL_CLAMP, GL_CLAMP_TO_EDGE, GL_INTENSITY, GL_LINEAR, GL_LINEAR_DETAIL_COLOR_SGIS, GL_LUMINANCE, + GL_LUMINANCE_ALPHA, GL_MODULATE, GL_RGB, GL_RGBA, +}; +use crate::dev::odyssey::sampler::{ + texel_format, texture_class_format, texture_level_texel_format, Placement, SamplerContext, Target, Wrap, +}; +use crate::dev::odyssey::raster::GlTexImage; +use crate::dev::odyssey::tests::board_with_ram; + +/// TyrQuake on IRIX 6.5.30 keeps some 1,500 texture levels on the board at +/// once, each its own board-memory object, and samples the first ones it +/// loaded, the console font and the menu pictures, every frame. A table of +/// 1024 objects forgot the oldest and those draws went untextured, white. +/// The table holds ODSY_OBJECTS, and only the one past that forgets. +#[test] +fn the_object_table_keeps_every_level_a_game_loads() { + let (mut s, _, _) = board_with_ram(); + let base = |i: u32| 0x16b000 + i * 0x100; + + for i in 0..ODSY_OBJECTS { + let o = s.object_select(base(i), 16); + assert!(s.object_rows(o, 1), "object {i}"); + } + assert!(s.object_find(base(0)).is_some(), "the first object loaded was forgotten"); + s.object_select(base(ODSY_OBJECTS), 16); + assert!(s.object_find(base(0)).is_none(), "a full table forgets the oldest"); + assert!(s.object_find(base(1)).is_some() && s.object_find(base(ODSY_OBJECTS)).is_some()); +} + +/// The texture state of a plain 2D RGBA level, as chocolate-doom's SDL +/// renderer sends it for its 512x256 frame texture: clamp to edge on both +/// axes, nearest, replace, one level at `base` (sampler word 4 0x13001400, +/// level entry 0x41100020, replayed from a guest trace). +fn rgba_512x256(s: &mut Board, base: u32) { + s.tex.mode = tex::MODE_2D; + s.tex.level0 = 0; + s.tex.levels[..3].copy_from_slice(&[base, 0x4110_0020, 0]); + s.tex.sampler = [0x0800_0000 | (9 << 10) | (8 << 5), 0x0004_0000 | 512, 0x0004_0000 | 256, 1, 0x1300_1400, 0, 0]; + s.tex.env = [1, 0, 0, 0, 0]; + s.tex.lod = [0; 4]; +} + +/// glTexImage2D with no data and then glTexSubImage2D of a 320x200 frame, +/// SDL's way of streaming a texture: the object holds only the 200 rows the +/// subimage wrote of the level's 256. The level is still the client's +/// texture, so setup must take it, with the rows nobody wrote reading 0. +/// Before, setup called the level missing and drew the frame untextured, +/// a white window. +#[test] +fn a_level_written_only_in_part_is_still_the_texture() { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b800; + let obj = s.object_select(base, 512); + + for y in 0..200 { + s.object_store(obj, 0, y, 0x1122_33ff); + } + rgba_512x256(&mut s, base); + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + + assert!(s.texture_setup(&mut image, &mut chain)); + assert!(chain.levels[0].read_texel, "level 0 is missing"); + assert_eq!((chain.storage[0].width, chain.storage[0].height), (512, 256)); + assert_eq!(s.object_load(obj, 0, 199), 0x1122_33ff); + assert_eq!(s.object_load(obj, 0, 255), 0, "a row the client never wrote"); +} + +/// The rows an object holds after texture setup under `word` (sampler word +/// 4), `sampler6` (word 6) and level entry word `entry1`, for the 512x256 +/// level of an object `width` wide that holds 200 rows. The rest of the +/// state is rgba_512x256()'s. +fn rows_after_setup(width: u32, word: u32, sampler6: u32, entry1: u32) -> u32 { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b800; + let obj = s.object_select(base, width); + + assert!(s.object_rows(obj, 200)); + rgba_512x256(&mut s, base); + s.tex.sampler[4] = word; + s.tex.sampler[6] = sampler6; + s.tex.levels[1] = entry1; + if word & 0x3000 == 0 { + // No clamp to edge, so no clamp mark in the size words. + s.tex.sampler[1] = 512; + s.tex.sampler[2] = 256; + } + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + s.texture_setup(&mut image, &mut chain); + s.objects.rows[obj as usize] +} + +/// Only a level of the client's own row length grows: an object narrower +/// than the level, or one whose row length is not the level entry's, is +/// some other buffer and stays as it is. +#[test] +fn a_level_grows_only_into_an_object_of_its_row_length() { + const ENTRY: u32 = 0x4110_0020; + // A one-level entry may leave its row length 0 (no stride to check). + const NO_STRIDE: u32 = 0x4110_0000; + + assert_eq!(rows_after_setup(512, 0x1300_1400, 0, ENTRY), 256, "the control"); + assert_eq!(rows_after_setup(512, 0x1300_1400, 0, NO_STRIDE), 256, "the control with no row length"); + assert_eq!(rows_after_setup(256, 0x1300_1400, 0, NO_STRIDE), 200, "narrower than the level"); + assert_eq!(rows_after_setup(1024, 0x1300_1400, 0, ENTRY), 200, "not the entry's row length"); +} + +/// A level with its border stored around it keeps its own checks: its +/// object needs rows past the level's height, so growing it to that height +/// would claim a level the border check then reads as written. +#[test] +fn a_bordered_level_does_not_grow() { + // Word 4 bit 28 clear and word 6's top byte 1: the stored-border marks. + assert_eq!(rows_after_setup(512, 0x0300_0000, 0x0100_0000, 0x4110_0020), 200); +} + +/// The texture move draws only the rows it wrote, from a scratch object +/// sized to them; it must not grow that object to the power of two the +/// sampler names. +#[test] +fn a_texture_move_does_not_grow_its_object() { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b000; + let obj = s.object_select(base, 512); + + assert!(s.object_rows(obj, 2)); + s.tex.mode = tex::MODE_2D; + s.tex.level0 = 60; + s.tex.levels[180..183].copy_from_slice(&[base, 0x8210_0020, 0]); + s.tex.sampler = [(9 << 10) | (6 << 5), 512, 64, 0, 0x0364_0000, 0, 0]; + s.tex.env = [1, 0, 0, 0, 0]; + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + assert!(s.texture_setup(&mut image, &mut chain)); + assert_eq!(s.objects.rows[obj as usize], 2); +} + +/// Packing the store keeps every live object's texels and its place in +/// store order, closes the holes forgotten objects left, and puts them all +/// at the front. With a full table of 16384 objects, some moved to the end +/// by growing and a third forgotten, this is also the case where a packing +/// that searched the table for each object took a third of a second. +#[test] +fn packing_a_full_table_keeps_each_object_in_store_order() { + let (mut s, _, _) = board_with_ram(); + let n = ODSY_OBJECTS as usize; + + for i in 0..n { + let o = s.object_select(0x10_0000 + i as u32 * 0x100, 16); + assert_eq!(o, i); + s.object_store(o, 0, 0, i as u32); + } + for i in (0..n).step_by(7) { + // Growth moves the object to the store's end. + s.object_store(i, 0, 1, !(i as u32)); + } + for i in (1..n).step_by(3) { + s.object_forget(i); + } + let mut live: Vec = (0..n).filter(|&i| s.objects.base[i] != 0).collect(); + live.sort_by_key(|&i| s.objects.offset[i]); + + s.object_compact(None); + let mut next = 0u32; + for &i in &live { + let o = i; + assert_eq!(s.objects.offset[o], next, "object {i} is not packed in store order"); + next += s.objects.width[o] * s.objects.rows[o]; + assert_eq!(s.object_load(i, 0, 0), i as u32, "object {i}'s texel"); + } + assert_eq!(s.objects.used, next); +} + +/// A stored volume's slices keep their own check: every row and texel the +/// volume needs must be written. A slice written in part leaves the volume +/// unusable, and is not grown to look complete as a 2D level is. The +/// control, with every slice whole, shows the state is a usable volume. +#[test] +fn a_stored_volume_slice_written_in_part_does_not_grow() { + for partial in [false, true] { + let (mut s, _, _) = board_with_ram(); + let base = 0x10_0000; + // A 4x4x4 RGBA volume with its border stored: each slice an object + // 64 texels wide, the level 15 rows and columns in, plus the two + // border slices. + let (w, h, d) = (4u32, 4u32, 4u32); + let stride = volume_slice_stride(h + 2, texel_format(17).unwrap()) as u32; + let mut rows = Vec::new(); + + // The border slice sits fifteen slice strides up, as rows and + // columns sit 15 in. + for z in 0..d + 2 { + let o = s.object_select(base + (15 + z) * stride, 64); + let n = if partial && z == 2 { 18 } else { 15 + h + 2 }; + for y in 0..n { + for x in 0..64 { + s.object_store(o, x, y, 0x1020_30ff); + } + } + rows.push((o, n)); + } + s.tex.mode = tex::MODE_3D; + s.tex.level0 = 0; + s.tex.levels[..3].copy_from_slice(&[base, 0x4110_0004, h + 2]); + s.tex.sampler = [0x0800_0000 | (2 << 10) | (2 << 5) | 2, w, h, d, 0x0300_2a00, 0, 0x0100_0000]; + s.tex.env = [1, 0, 0, 0, 0]; + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + let taken = s.texture_setup(&mut image, &mut chain) && chain.levels[0].read_texel_3d; + + assert_eq!(taken, !partial, "partial {partial}"); + for (o, n) in rows { + assert_eq!(s.objects.rows[o as usize], n, "slice object {o}, partial {partial}"); + } + } +} + +/// Sampler word 0's detail fields: the six captured detail controls' words +/// decode to the level, size, entry and combine those controls were built +/// with, and SGI Quake's world word (0x0873a500, a 512x256 base) to the +/// 128x128 detail image at the default level -4, which sits in entry 60. +#[test] +fn detail_sampler_words_name_the_detail_image() { + let cases = [ + (0x0210_8000, 63, 2, 2, -1, GL_ADD), + (0x0410_8000, 62, 2, 2, -2, GL_ADD), + (0x0610_8000, 61, 2, 2, -3, GL_ADD), + (0x0420_8000, 62, 4, 2, -2, GL_ADD), + (0x0411_0000, 62, 2, 4, -2, GL_ADD), + (0x8410_8000, 62, 2, 2, -2, GL_MODULATE), + (0x0873_a500, 60, 128, 128, -4, GL_ADD), + ]; + for (word, descriptor, width, height, level, mode) in cases { + let d = texture_detail_sampler(word).unwrap_or_else(|| panic!("{word:#x} refused")); + assert_eq!((d.descriptor, d.width, d.height, d.level, d.mode), (descriptor, width, height, level, mode), "{word:#x}"); + } + assert!(texture_detail_sampler(0x0010_8000).is_none(), "level 0 is no detail level"); + assert!(texture_detail_sampler(0x4410_8000).is_none(), "bit 30 has no known meaning"); +} + +/// The library's default ramp, weight 0 at LOD 0 to 1 at LOD -4: the weight +/// is the end entry's beyond the table and interpolated between entries. +#[test] +fn the_detail_weight_follows_the_function_between_entries() { + let mut w = TextureDetailWeight::default(); + for (i, e) in w.table.iter_mut().enumerate() { + *e = (i as u16 * 32).min(4095); + } + assert_eq!(w.at(0.5), Some(0.0)); + assert_eq!(w.at(-1.0), Some(1024.0 / 4096.0)); + assert_eq!(w.at(-1.0 / 64.0), Some(16.0 / 4096.0)); + assert_eq!(w.at(-2.5), Some(2560.0 / 4096.0)); + assert_eq!(w.at(-9.0), Some(4095.0 / 4096.0)); + assert_eq!(w.at(f64::NAN), None); +} + +/// SGI Quake's world textures as the library left them: a 512x256 RGB base +/// with ten mip levels, sampler word 4 `word4`, modulate, and a 128x128 +/// detail image in entry 60 whose word 1 is `detail_word1`. +fn quake_world_state(s: &mut Board, word4: u32, detail_word1: u32) { + let base = 0x1d90c0; + let mut level_base = base; + + s.tex.mode = 0x2410; + s.tex.level0 = 0; + for i in 0..10u32 { + let (w, h) = ((512u32 >> i).max(1), (256u32 >> i).max(1)); + let units = (w.div_ceil(16) + 1) & !1; + let o = s.object_select(level_base, units * 16); + assert!(s.object_rows(o, h)); + s.tex.levels[i as usize * 3..][..3].copy_from_slice(&[level_base, 0x00d0_0000 | units, 0]); + level_base += units * 16 * h; + } + let detail = 0x16b800; + let o = s.object_select(detail, 128); + assert!(s.object_rows(o, 128)); + for y in 0..128 { + for x in 0..128 { + s.object_store(o, x, y, 0x8080_80ff); + } + } + s.tex.levels[60 * 3..][..3].copy_from_slice(&[detail, detail_word1, 0]); + s.tex.sampler = [0x0873_a500, 0x200, 0x100, 1, word4, 0, 0]; + s.tex.lod = [0, 0, 0x3100, 0xf00]; + s.tex.env = [2, 0, 0, 0, 0]; + for (i, e) in s.tex.detail_function.iter_mut().enumerate() { + *e = ((i as u32 * 32).min(4095)) << 12; + } + s.tex.detail_function_valid = 0xffff; +} + +/// SGI Quake's world textures, replayed from its texture state: a 512x256 +/// RGB base with ten mip levels under GL_LINEAR_DETAIL_COLOR_SGIS (sampler +/// word 4 0x128000a8: minification code 5, magnification code 4), modulate, +/// and a 128x128 detail image in entry 60. Setup must accept it: a +/// refused draw leaves the whole world undrawn, black. +#[test] +fn sgi_quake_world_texture_takes_its_detail_image() { + let (mut s, _, _) = board_with_ram(); + quake_world_state(&mut s, 0x1280_00a8, 0x00d0_0008); + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + + assert!(s.texture_setup(&mut image, &mut chain), "the draw was refused"); + assert_eq!(image.mag_filter, GL_LINEAR_DETAIL_COLOR_SGIS); + assert_eq!((image.width, image.height, image.base_format), (512, 256, GL_RGB)); + assert_eq!(chain.pyramid.map(|p| p.count), Some(10)); + assert_eq!((chain.detail_storage.width, chain.detail_storage.height), (128, 128)); + assert_eq!((image.detail_level, image.detail_mode), (-4, GL_ADD)); + assert_eq!(chain.detail_weight.table[128], 4095); +} + +/// Word 0 bits 24:15 name a detail image only while word 4 carries a detail +/// magnification code; the library leaves them behind after a change to a +/// plain filter. Quake's state with magnification code 1 (linear) is a plain +/// mipmapped image and takes no detail image, where code 4 takes one. +#[test] +fn stale_detail_size_bits_are_ignored_without_a_detail_magnification_code() { + let (mut s, _, _) = board_with_ram(); + quake_world_state(&mut s, 0x1280_00a2, 0x00d0_0008); + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + + assert!(s.texture_setup(&mut image, &mut chain), "the plain draw was refused"); + assert_eq!(image.mag_filter, GL_LINEAR); + assert!(image.detail_image.is_none()); + assert_eq!((image.width, image.height, image.base_format), (512, 256, GL_RGB)); + assert_eq!(chain.detail_storage.width, 0); +} + + +/// A detail level names its own format index, which must be a table row +/// with its own size code, but its components are the primary's class: only +/// the bound texture's mode word reaches the board. An index the table does +/// not list, or one whose size code is not its own, is refused. +#[test] +fn the_detail_image_takes_its_index_and_the_primary_class() { + const RGB_WORD4: u32 = 0x1280_00a8; + const RGBA_WORD4: u32 = 0x1300_00a8; + let cases = [ + // (sampler word 4, detail entry word 1, takes the draw) + (RGB_WORD4, 0x4110_0008, true), + (RGBA_WORD4, 0x4110_0008, true), + (RGB_WORD4, 0x00c0_0008, true), + (RGBA_WORD4, 0x00d0_0008, true), + (RGB_WORD4, 0x8210_0008, true), + (RGB_WORD4, 0x00b0_0008, true), + (RGBA_WORD4, 0x4100_0008, true), + (RGB_WORD4, 0x0040_0008, true), + (RGB_WORD4, 0x0000_0008, true), + (RGBA_WORD4, 0x0020_0008, true), + (RGBA_WORD4, 0x0300_0008, true), + (RGB_WORD4, 0x4310_0008, true), + // Indices nothing emits, and a size code that is not the index's. + (RGB_WORD4, 0x0230_0008, false), + (RGB_WORD4, 0x00e0_0008, false), + (RGB_WORD4, 0x0110_0008, false), + (RGB_WORD4, 0x4040_0008, false), + ]; + for (word4, word1, takes) in cases { + let (mut s, _, _) = board_with_ram(); + quake_world_state(&mut s, word4, word1); + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + let drew = s.texture_setup(&mut image, &mut chain); + let primary = if word4 == RGB_WORD4 { GL_RGB } else { GL_RGBA }; + + assert_eq!(drew, takes, "word 4 {word4:#x} entry {word1:#x}"); + if takes { + assert_eq!(chain.detail_storage.base_format, primary, "word 4 {word4:#x} entry {word1:#x}"); + assert_eq!(chain.detail_image.base_format, primary); + } + } +} + +/// The format index table: sixteen indices are emitted, each with its own +/// size code and classes, and every other index is refused. Index 12 holds +/// 4/4/4/4 whether its class says RGB or RGBA. +#[test] +fn the_format_index_table_lists_the_sixteen_emitted_indices() { + // (index, size code, classes as bits 1 to 6) + let rows = [ + (0, 0, 1 << 1), + (2, 0, 1 << 4), + (4, 0, 1 << 2), + (8, 0, 1 << 1), + (9, 0, 1 << 4), + (10, 0, 1 << 2), + (11, 0, 1 << 3), + (12, 0, 3 << 5), + (13, 0, 3 << 5), + (16, 1, 1 << 3), + (17, 1, 3 << 5), + (18, 1, 3 << 5), + (32, 2, 3 << 5), + (33, 2, 3 << 5), + (48, 0, 1 << 5), + (49, 1, 1 << 6), + ]; + for index in 0..64u32 { + let want = rows.iter().find(|r| r.0 == index); + + assert_eq!(texel_format(index).map(|f| (f.size, f.classes)), want.map(|r| (r.1, r.2)), "index {index}"); + } + // The size code is part of the word: an index with another size is no format. + assert_eq!(texture_level_texel_format(0x4110_0020).map(|f| f.size), Some(1)); + assert!(texture_level_texel_format(0x0110_0020).is_none()); + assert!(texture_level_texel_format(0x8110_0020).is_none()); +} + +/// Sampler word 4 bits 25:23 are the class: 1 alpha, 2 luminance, 3 +/// luminance-alpha, 4 intensity, 5 RGB, 6 RGBA. Class 0 and 7 name no +/// components. +#[test] +fn the_sampler_class_names_the_components() { + let want = [None, Some(GL_ALPHA), Some(GL_LUMINANCE), Some(GL_LUMINANCE_ALPHA), Some(GL_INTENSITY), Some(GL_RGB), Some(GL_RGBA), None]; + + for (class, format) in want.iter().enumerate() { + let word = (class as u32) << 23; + + assert_eq!(texture_class_format(word & tex::COMPONENTS_MASK), *format, "class {class}"); + } +} + +/// Sampler word 4 bits 27:26 are not part of the class and the library never +/// writes them, so a word that sets either is refused, not ignored. +#[test] +fn sampler_word_4_bits_27_26_are_refused() { + for bit in [26u32, 27] { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b800; + let obj = s.object_select(base, 512); + + assert!(s.object_rows(obj, 256)); + rgba_512x256(&mut s, base); + s.tex.levels[1] = 0x4110_0020; + assert!(s.texture_setup(&mut GlTexImage::default(), &mut Box::::default()), "the plain word is taken"); + s.tex.sampler[4] |= 1 << bit; + assert!(!s.texture_setup(&mut GlTexImage::default(), &mut Box::::default()), "bit {bit} is refused"); + } +} + +/// Whether texture setup takes a plain 512x256 level whose sampler class is +/// `class` and whose entry word 1 is `entry1`. +fn level_draws(class: u32, entry1: u32) -> bool { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b800; + let obj = s.object_select(base, 512); + + assert!(s.object_rows(obj, 256)); + rgba_512x256(&mut s, base); + s.tex.sampler[4] = (s.tex.sampler[4] & !tex::COMPONENTS_MASK) | (class << 23); + s.tex.levels[1] = entry1; + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + s.texture_setup(&mut image, &mut chain) +} + +/// A level must name an index the library emits, with that index's own size +/// code. Index 12 is 4/4/4/4 under class 5 or 6, so class does not choose +/// the layout, and no class and index pair is refused: the texture move +/// sends class 6 with index 10. A listed index with a wrong size code, or an +/// index nothing emits (1, 3, 5 to 7, 14, 15, 19 to 31, 34 to 47, 50 to 63), +/// drops the draw. +#[test] +fn a_level_needs_an_emitted_index_and_its_size_code() { + // (class, entry word 1, takes the draw) + let cases = [ + (6, 0x00a0_0020, true), + (2, 0x4110_0020, true), + (1, 0x8200_0020, true), + (6, 0x4110_0020, true), + (5, 0x4110_0020, true), + (6, 0x00c0_0020, true), + (5, 0x00c0_0020, true), + (3, 0x00b0_0020, true), + (1, 0x0000_0020, true), + (2, 0x0040_0020, true), + (4, 0x0020_0020, true), + (1, 0x0080_0020, true), + (6, 0x8210_0020, true), + (5, 0x4310_0020, true), + (6, 0x0300_0020, true), + (6, 0x0010_0020, false), + (2, 0x0030_0020, false), + (6, 0x0050_0020, false), + (6, 0x0070_0020, false), + (6, 0x00e0_0020, false), + (6, 0x00f0_0020, false), + (6, 0x0130_0020, false), + (6, 0x0230_0020, false), + (6, 0x03f0_0020, false), + (6, 0x0110_0020, false), + (6, 0x8110_0020, false), + (6, 0x4040_0020, false), + (6, 0xc110_0020, false), + ]; + for (class, entry1, want) in cases { + assert_eq!(level_draws(class, entry1), want, "class {class} entry {entry1:#x}"); + } +} + +/// The library's texture move stores class 6 whatever the pixel class's +/// index, so the move takes any emitted index, and only those. Its size code +/// is the same quantity texture levels send, and is checked the same way. +#[test] +fn a_texture_move_takes_any_emitted_index_with_its_size_code() { + for (index, wrong_size) in (0..64u32).flat_map(|i| [(i, false), (i, true)]) { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b000; + let obj = s.object_select(base, 512); + + assert!(s.object_rows(obj, 2)); + s.tex.mode = tex::MODE_2D; + s.tex.level0 = 60; + let size = texel_format(index).map_or(0, |f| f.size); + let size = if wrong_size { (size + 1) & 3 } else { size }; + s.tex.levels[180..183].copy_from_slice(&[base, (size << 30) | (index << 20) | 0x20, 0]); + s.tex.sampler = [(9 << 10) | (6 << 5), 512, 64, 0, 0x0364_0000, 0, 0]; + s.tex.env = [1, 0, 0, 0, 0]; + let emitted = texel_format(index).is_some() && !wrong_size; + + assert_eq!( + s.texture_setup(&mut GlTexImage::default(), &mut Box::::default()), + emitted, + "index {index}, wrong size code {wrong_size}" + ); + } +} + +/// The sampler words decoded field by field: chocolate-doom's 512x256 RGBA +/// frame texture, clamp to edge, nearest. +#[test] +fn the_sampler_words_decode_into_fields() { + let (mut s, _, _) = board_with_ram(); + rgba_512x256(&mut s, 0x16b800); + let d = s.texture_sampler().expect("the control decodes"); + + assert_eq!(d.target, Target::TwoD); + assert_eq!(d.size(), [512, 256, 1]); + assert_eq!(d.max_clamp, [512, 256, 1]); + assert_eq!(d.wrap, [Wrap::ClampToEdge, Wrap::ClampToEdge, Wrap::Repeat]); + assert_eq!((d.base_format, d.context, d.placement), (GL_RGBA, SamplerContext::Plain, Placement::Corner)); + assert_eq!((d.min, d.mag, d.first, d.env), (0, 0, 0, 1)); + assert!(d.detail.is_none()); +} + +/// Each state field the library never writes, or writes only in agreement +/// with another, is unknown state when it disagrees, and refuses the draw. +#[test] +fn words_the_library_never_writes_refuse_the_draw() { + let control = |s: &mut Board| rgba_512x256(s, 0x16b800); + let cases: [(&str, fn(&mut Board)); 12] = [ + ("size word bit 30", |s| s.tex.sampler[0] |= 0x4000_0000), + ("an R size on a 2D image", |s| s.tex.sampler[0] |= 1), + ("a size word's mark without clamp to edge", |s| s.tex.sampler[1] = 512), + ("a size word bit past the mark", |s| s.tex.sampler[2] |= 0x0100_0000), + ("a maximum clamp past the size", |s| s.tex.sampler[1] = 0x0004_0000 | 513), + ("class 7", |s| s.tex.sampler[4] |= 0x0380_0000), + ("word 4 bit 31", |s| s.tex.sampler[4] |= 0x8000_0000), + ("bits 22:18 the model has not seen", |s| s.tex.sampler[4] |= 0x0004_0000), + ("word 4 bit 28 and word 6 bit 24 both set", |s| s.tex.sampler[6] |= 0x0100_0000), + ("minification code 6", |s| s.tex.sampler[4] |= 0xc0), + ("an R wrap on a 2D image", |s| s.tex.sampler[4] |= 0x100), + ("an offset word", |s| s.tex.offset[1] = 1), + ]; + let (mut s, _, _) = board_with_ram(); + control(&mut s); + assert!(s.texture_sampler().is_ok(), "the control"); + for (what, change) in cases { + let (mut s, _, _) = board_with_ram(); + control(&mut s); + change(&mut s); + assert!(s.texture_sampler().is_err(), "{what} decoded"); + } +} + +/// What the model cannot draw is refused by the support check, after the +/// words decode. GL_CLAMP on one axis of a 2D image and not the other is one +/// such state; on both it is drawn. +#[test] +fn clamp_on_one_axis_of_two_is_not_drawn() { + let (mut s, _, _) = board_with_ram(); + rgba_512x256(&mut s, 0x16b800); + s.tex.sampler[1] = 512; + s.tex.sampler[4] = 0x1300_2400; + let d = s.texture_sampler().expect("mixed wraps decode"); + assert!(s.texture_support(&d).is_err()); + + s.tex.sampler[2] = 256; + s.tex.sampler[4] = 0x1300_2800; + let d = s.texture_sampler().expect("GL_CLAMP decodes"); + assert!(s.texture_support(&d).is_ok()); +} + +/// A level that holds no rows at all stays missing: the partial-level rule +/// grows only an object some transfer has written. Model policy. +#[test] +fn a_level_whose_object_holds_no_rows_is_missing() { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b800; + let obj = s.object_select(base, 512); + rgba_512x256(&mut s, base); + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + + assert!(s.texture_setup(&mut image, &mut chain)); + assert!(!chain.levels[0].read_texel, "an object with no rows was read"); + assert_eq!(s.objects.rows[obj as usize], 0); +} + +/// Level descriptor word 2 is not written for a 2D level, so a value left by +/// a volume that used the entry before is not this texture's and does not +/// refuse it. +#[test] +fn a_2d_level_ignores_a_stale_word_2() { + assert!(level_draws(6, 0x4110_0020)); + let (mut s, _, _) = board_with_ram(); + let base = 0x16b800; + let obj = s.object_select(base, 512); + assert!(s.object_rows(obj, 256)); + rgba_512x256(&mut s, base); + s.tex.levels[2] = 4; + let mut chain = Box::::default(); + + assert!(s.texture_setup(&mut GlTexImage::default(), &mut chain)); + assert!(chain.levels[0].read_texel); +} + +/// A 1D image of any width stored with a texel offset: its texels 16 in +/// and its two border texels at 15 and past its end, all of it written. The +/// border is read only when it was written. +#[test] +fn a_one_dimensional_stored_border_of_any_width_is_read() { + for (width, written) in [(16u32, true), (32, true), (16, false)] { + let (mut s, _, _) = board_with_ram(); + let base = 0x16b800; + let units = ((width + 32).div_ceil(16) + 1) & !1; + let o = s.object_select(base, units * 16); + let last = if written { 16 + width } else { 16 + width - 1 }; + for x in 15..=last { + s.object_store(o, x, 0, 0x1020_30ff + x); + } + s.tex.mode = tex::MODE_1D; + s.tex.level0 = 0; + s.tex.levels[..3].copy_from_slice(&[base, 0x4110_0000 | units, 0]); + s.tex.sampler = [0x0800_0000 | (width.trailing_zeros() << 10), width, 1, 1, 0x0300_2000, 0, 0x0100_0000]; + s.tex.env = [1, 0, 0, 0, 0]; + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + + assert!(s.texture_setup(&mut image, &mut chain), "width {width}"); + assert_eq!(image.border, 1); + assert_eq!(image.wrap_t, GL_CLAMP_TO_EDGE, "a 1D image takes no T border tap"); + assert_eq!(chain.levels[0].read_border_texel, written, "width {width} written {written}"); + if written { + let texels = BoardTexels { + chain: &chain, + obj: &s.objects.mem.bytes, + obj_words: s.objects.mem.size / 4, + slice_offset: &s.tex.slice_offset, + slice_width: &s.tex.slice_width, + colour_table: &s.tex.colour_table, + scale_bias: &s.tex.scale_bias, + }; + assert_eq!(texels.read_border_level(0, -1, 0), Some(0x1020_30ff + 15)); + assert_eq!(texels.read_border_level(0, width as i32, 0), Some(0x1020_30ff + 16 + width)); + assert_eq!(texels.read(&chain.storage[0], 0, 0), Some(0x1020_30ff + 16)); + } + } +} + +/// The captured stored-border volume: a 2x2x2 RGBA interior with its border, +/// level 0 at 0x16a800 with slices 0x400 apart, its four slice objects +/// uploaded from 0x16e400, fifteen slice strides up, each 64 texels wide +/// with the interior from (16, 16). At the other observed level base, +/// 0x16b800, the same gap. A volume whose slices start at the level base +/// itself has no object where the rule looks, and is refused. +#[test] +fn the_captured_stored_volume_reads_its_slices_fifteen_strides_up() { + for (level_base, first_slice, takes) in [(0x16a800u32, 0x16e400u32, true), (0x16b800, 0x16f400, true), (0x16a800, 0x16a800, false)] { + let (mut s, _, _) = board_with_ram(); + for z in 0..4u32 { + let o = s.object_select(first_slice + z * 0x400, 64); + for y in 15..19 { + for x in 15..19 { + s.object_store(o, x, y, (z << 24) | (y << 16) | (x << 8) | 0xff); + } + } + } + s.tex.mode = 0x2420; + s.tex.level0 = 0; + s.tex.levels[..3].copy_from_slice(&[level_base, 0x4110_0004, 4]); + s.tex.sampler = [0x0800_0421, 2, 2, 2, 0x0300_2a00, 0x404, 0x01c0_cfff]; + s.tex.env = [1, 0, 0, 0, 0]; + let mut image = GlTexImage::default(); + let mut chain = Box::::default(); + + assert_eq!(s.texture_setup(&mut image, &mut chain), takes, "level base {level_base:#x} slices from {first_slice:#x}"); + if takes { + assert_eq!((image.border, image.wrap_r, chain.volume.slices), (1, GL_CLAMP, 4)); + assert_eq!(chain.volume.base, level_base + 15 * 0x400); + } + } +} + +/// A volume's slices sit 2^(word 2) tiles of 256 texels apart at the bytes +/// the library books for the format, in 16-byte units: 0x80 for a 4-byte +/// format at word 2 1, as the captured 8x8x8 levels sit, and half or twice +/// that for a 2- or 8-byte format. +#[test] +fn the_volume_slice_stride_follows_the_booked_bytes() { + let f = |index| texel_format(index).unwrap(); + + assert_eq!(volume_slice_stride(1, f(17)), 0x80); + assert_eq!(volume_slice_stride(4, f(17)), 0x400); + assert_eq!(volume_slice_stride(6, f(17)), 0x1000); + assert_eq!(volume_slice_stride(1, f(12)), 0x40); + assert_eq!(volume_slice_stride(1, f(33)), 0x100); +} diff --git a/src/dev/odyssey/tests/pixels.rs b/src/dev/odyssey/tests/pixels.rs new file mode 100644 index 000000000..5cdea127d --- /dev/null +++ b/src/dev/odyssey/tests/pixels.rs @@ -0,0 +1,1239 @@ +//! GL pixel transfers: image writes by burst and by DMA, reads, and the +//! copy block, against the headers the OpenGL library sends. + +use crate::dev::odyssey::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use crate::dev::odyssey::consts::{ + dest, dispmode, dma, gl_cmd, image, mode, reg, self, swap, tex, write, ODSY_DRAW_MASK_BUFFER0, + ODSY_DRAW_MASK_BUFFER1, ODSY_GAMMA_GROUP, ODSY_PLANE_MASK_SHIFT, ODSY_USER_CMD_FIFO, +}; +use crate::dev::odyssey::dma::DMA_BPP; +use crate::dev::odyssey::gl_context::GL_CLEAR; +use crate::dev::odyssey::memory::{ClipRect, LOGIC_XOR}; +use crate::dev::odyssey::objects::CopyOperands; +use crate::dev::odyssey::pixel_write::write_form; +use crate::dev::odyssey::tests::{board_with_ram, cmd, cmd64, pixel, pixel_write, select, Ram}; +use std::sync::Arc; +use super::dma::{PAGE_A, TABLE, colour_destination, descriptor, pte, put_table}; + +/// A board with guest RAM whose one clip rectangle covers the screen, so +/// every in-plane pixel is the window's. +fn window() -> (Box, Arc) { + let (mut s, _, ram) = board_with_ram(); + s.win.clip_kept = 1; + s.win.clip_rect[0] = ClipRect { x0: 0, y0: 0, x1: ODSY_WIDTH as i32 - 1, y1: ODSY_HEIGHT as i32 - 1 }; + (s, ram) +} + +/// A GL image write by pixel burst, as the library sends a small +/// glDrawPixels: BEGIN with the image primitive, the raster box from +/// (x1, y1) to (x2, y2), the transfer header, then the data words under +/// the burst register for `fmt`, at most 16 a burst. +fn burst_image(s: &mut Board, bounds: (u32, u32, u32, u32), header: u32, fmt: u32, words: &[u32]) { + burst_image_groups(s, bounds, &[(header, &[(fmt, words)])]); +} + +/// The same for an image the library sends as several transfer headers, +/// each followed by its bursts (a tag and its words): the shape of a draw +/// wider than one burst, where a header finishes the previous row. +fn burst_image_groups(s: &mut Board, (x1, y1, x2, y2): (u32, u32, u32, u32), groups: &[(u32, &[(u32, &[u32])])]) { + cmd(s, select(reg::GL_BEGIN)); + cmd(s, gl_cmd::PRIMITIVE_IMAGE); + cmd(s, consts::cmd::IMAGE_BOX); + for i in 0..10 { + let v = match i { + image::BOX_X1_INDEX => x1, + image::BOX_Y1_INDEX => y1, + image::BOX_X2_INDEX => x2, + image::BOX_Y2_INDEX => y2, + _ => 0, + }; + cmd(s, (v as f32).to_bits()); + } + for &(header, bursts) in groups { + cmd(s, consts::cmd::IMAGE_HEADER); + cmd(s, header); + for &(fmt, words) in bursts { + for chunk in words.chunks(16) { + let n = if chunk.len() == 16 { consts::cmd::ARITY_MASK } else { chunk.len() as u32 }; + cmd(s, select(image::BURST_BASE | n << consts::cmd::ARITY_SHIFT | fmt)); + for &w in chunk { + cmd(s, w); + } + } + } + } + cmd(s, select(reg::GL_END)); +} + +/// glDrawPixels of RGBA bytes, the header the library sends for a 2x1 +/// image (count 2, source code 0x4b): the pixels keep the alpha they +/// carry. Established that the library sends the alpha, inferred that the +/// board stores it. +#[test] +fn a_gl_rgba_image_keeps_its_alpha() { + let (mut s, _) = window(); + burst_image(&mut s, (40, 10, 42, 11), 0x0021_104b, image::FMT_RGB, &[0x116c_c722, 0x2233_4400]); + assert_eq!(pixel(&s, 0, 40, 10), 0x116c_c722); + assert_eq!(pixel(&s, 0, 41, 10), 0x2233_4400); +} + +/// Luminance-alpha bytes (0x48) and A, B, G, R bytes (0x4c) keep their +/// alpha too; RGB bytes (0x49) carry none and store 255. +#[test] +fn luminance_alpha_and_abgr_images_keep_alpha_and_rgb_stores_255() { + let (mut s, _) = window(); + burst_image(&mut s, (40, 10, 42, 11), 0x0021_1048, image::FMT_CI, &[0x116c_2233]); + assert_eq!(pixel(&s, 0, 40, 10), 0x1111_116c); + assert_eq!(pixel(&s, 0, 41, 10), 0x2222_2233); + + burst_image(&mut s, (40, 12, 41, 13), 0x0011_104c, image::FMT_RGB, &[0x11c7_6c22]); + assert_eq!(pixel(&s, 0, 40, 12), 0x226c_c711); + + burst_image(&mut s, (40, 14, 41, 15), 0x0011_1049, image::FMT_RGB, &[0x1122_3300]); + assert_eq!(pixel(&s, 0, 40, 14), 0x1122_33ff); +} + +/// The X server's own 24-bit image, put by DMA into the colour buffer +/// under 0x4c, still stores 255: the alpha rule is for GL image writes. +#[test] +fn an_x_server_abgr_put_still_stores_255() { + let (mut s, ram) = window(); + for w in [select(reg::DEST_SELECT), 0x2000, 0x50, 0x2000_6950, 0] { + s.mmio_write(ODSY_USER_CMD_FIFO, w as u64, 4); + } + let table = 0xb80000u64; + let page = 0xb84000u64; + ram.put(page, &[0x7f, 0x01, 0x9b, 0xfd]); + let header = [0x2000u32, 300 << 16 | 200, 0x50, 0x10001, 0x8223_305a, 0, 4, 0x3201_004c]; + let mut t = vec![0u8; dma::TABLE_HEADER as usize + 16]; + for (i, w) in header.iter().enumerate() { + t[i * 4..][..4].copy_from_slice(&w.to_be_bytes()); + } + t[dma::TABLE_HEADER as usize..][..8].copy_from_slice(&(page << dma::PTE_ADDR_SHIFT | 4).to_be_bytes()); + ram.put(table, &t); + let half = |h: u64| (h & 0x1ffff) as u32 | ((h & 0xfe_0000) << 6) as u32; + cmd64(&mut s, dma::DESC_UPLOAD, half(table >> 24)); + cmd64(&mut s, half(table & 0xff_ffff), 0); + assert_eq!(pixel(&s, 0, 200, 300), 0xfd9b_01ff); +} + +/// Word 4 of a GL read from the bound read drawable, as the library builds +/// it: class and code, bit 12, the read selector and the front bit. +fn read_word4(class: u32, code: u32) -> u32 { + 0x9000_1000 | class | code << 3 +} + +/// Read the `w` x 1 pixels at (x, y) of the window by DMA with word 4 +/// `word4` at `bytes` bytes a pixel, into a guest page pre-filled with +/// 0xa5 guard bytes. Returns the 64 bytes at the start of the page. +fn dma_read(s: &mut Board, ram: &Ram, (x, y, w): (u32, u32, u32), word4: u32, bytes: u32) -> Vec { + let bpp_code = DMA_BPP.iter().position(|&b| b as u32 == bytes).expect("a size the header can name") as u32; + + ram.put(PAGE_A, &[0xa5; 64]); + put_table( + ram, + TABLE, + [dma::SENTINEL_READ, y << 16 | x, 0x50, 1 << 16 | w, word4, 0, w * bytes, dma::TO_HOST | bpp_code << 16], + &[pte(PAGE_A, w * bytes)], + ); + descriptor(s, TABLE); + ram.get(PAGE_A, 64) +} + +/// A drawn pixel as 0xRRGGBBAA. +const READ_PIXEL: u32 = 0xa55a_3cff; + +fn read_window() -> (Box, Arc) { + let (mut s, ram) = window(); + pixel_write(&mut s, 30, 40, READ_PIXEL); + // A colour-map-free RGB mode, so the pixel is a colour and not an index. + s.regs().set_dispmode((0) as usize, ODSY_GAMMA_GROUP << dispmode::GROUP_SHIFT); + (s, ram) +} + +/// glReadPixels of a narrow window's packed types: the library asks for +/// each type's own header, and the board hands back that packing of the +/// pixel's top bits. Before the read table, none of these headers was +/// modelled and the guest buffer was left as it was. +/// +/// Established as the library's request, the packing inferred. +/// The narrowing is an approximation, see DMA_GL_READS. +#[test] +fn a_read_of_a_packed_type_returns_that_packing_of_the_pixel() { + let (mut s, ram) = read_window(); + // (class, code, bytes, the word a pixel packs to) + let cases: [(u32, u32, u32, u32); 9] = [ + (1, 13, 2, 0xa53f), // RGBA, 4_4_4_4 + (1, 6, 2, 0xf35a), // RGBA, 4_4_4_4_REV + (1, 12, 2, 0xa2cf), // RGBA, 5_5_5_1 + (1, 9, 2, 0x9d74), // RGBA, 1_5_5_5_REV + (1, 3, 2, 0xd167), // BGRA, 1_5_5_5_REV + (1, 10, 2, 0xa2c7), // RGB, 5_6_5 + (2, 0, 4, 0xa596_93c3), // RGBA, 10_10_10_2 + (2, 12, 4, 0xcf05_a696), // RGBA, 2_10_10_10_REV + (1, 5, 2, 0x3ae9), // BGRA, 5_5_5_1 + ]; + + for (class, code, bytes, want) in cases { + let got = dma_read(&mut s, &ram, (30, 40, 1), read_word4(class, code), bytes); + + assert_eq!(got[..bytes as usize], want.to_be_bytes()[4 - bytes as usize..], "class {class} code {code}"); + assert_eq!(got[bytes as usize], 0xa5, "class {class} code {code} wrote past the pixel"); + } +} + +/// The byte forms: RGB, BGR, BGRA, ABGR and RGBA bytes of an eight-bit +/// window and the 16-bit luminance-alpha of a texture. +#[test] +fn a_read_of_a_byte_type_returns_the_pixel_bytes_in_the_requested_order() { + let (mut s, ram) = read_window(); + let cases: [(u32, u32, usize, &[u8]); 7] = [ + (2, 8, 4, &[0xa5, 0x5a, 0x3c, 0xff]), + (2, 1, 3, &[0xa5, 0x5a, 0x3c]), + (2, 3, 3, &[0x3c, 0x5a, 0xa5]), + (2, 4, 4, &[0x3c, 0x5a, 0xa5, 0xff]), + (2, 11, 4, &[0xff, 0x3c, 0x5a, 0xa5]), + (1, 1, 4, &[0xa5, 0x5a, 0x3c, 0xff]), + (2, 6, 2, &[0xa5, 0xff]), + ]; + + for (class, code, bytes, want) in cases { + let got = dma_read(&mut s, &ram, (30, 40, 1), read_word4(class, code), bytes as u32); + + assert_eq!(&got[..bytes], want, "class {class} code {code}"); + assert_eq!(got[bytes], 0xa5, "class {class} code {code} wrote past the pixel"); + } +} + +/// Word 4's field codes (bits 23:20, then 27:24) added to a read header. +fn with_fields(word4: u32, first: u32, second: u32) -> u32 { + word4 | first << 20 | second << 24 +} + +/// Class 2 code 2 is BGRA/UNSIGNED_INT_8_8_8_8_REV colour and also a +/// 32-bit depth word; class 0 code 0 is a stencil byte and also a colour +/// index byte. The field codes in word 4 tell them apart: a listed colour +/// pair is colour, (0,0) and (4,0) are depth, and (4,0) is stencil. The +/// (0,0) stencil or storage-0 index tie is served as an index. +/// +/// Established that the field codes differ; the tie is unknown and labelled. +#[test] +fn field_codes_separate_a_colour_read_from_a_depth_or_stencil_read() { + let (mut s, ram) = read_window(); + let colour = [0xff, 0xa5, 0x5a, 0x3c]; + + // Colour pairs: A, R, G, B bytes. + for (a, b) in [(6, 2), (2, 2), (3, 2), (7, 3), (2, 0)] { + let got = dma_read(&mut s, &ram, (30, 40, 1), with_fields(read_word4(2, 2), a, b), 4); + assert_eq!(got[..5], [0xff, 0xa5, 0x5a, 0x3c, 0xa5], "fields ({a},{b})"); + } + // Depth-looking pairs: not a colour. + for (a, b) in [(0, 0), (4, 0)] { + let got = dma_read(&mut s, &ram, (30, 40, 1), with_fields(read_word4(2, 2), a, b), 4); + assert_ne!(got[..4], colour, "fields ({a},{b}) are a depth read"); + } + // Stencil (4,0) is the stencil plane's byte, not a colour index. + let got = dma_read(&mut s, &ram, (30, 40, 1), with_fields(read_word4(0, 0), 4, 0), 1); + assert_eq!(got[..2], [0x00, 0xa5]); + + // The (0,0) tie is served as an index byte. On a window shown as colour + // there is no index to return and the read is refused. + let got = dma_read(&mut s, &ram, (30, 40, 1), with_fields(read_word4(0, 0), 0, 0), 1); + assert_eq!(got[..2], [0xa5, 0xa5], "refused on an RGB window"); + pixel_write(&mut s, 31, 40, 0x2a); + s.regs().set_dispmode((0) as usize, 0); + let got = dma_read(&mut s, &ram, (31, 40, 1), with_fields(read_word4(0, 0), 0, 0), 1); + assert_eq!(got[..2], [0x2a, 0xa5]); +} + +/// A colour-index window reads back its index: one byte under class 0 code +/// 1, a big-endian short under class 1 code 14. A pixel the display shows +/// as a colour reads its red byte under class 0 code 1. +#[test] +fn a_colour_index_read_returns_the_index() { + let (mut s, ram) = read_window(); + pixel_write(&mut s, 31, 40, 0x5a3); + s.regs().set_dispmode((0) as usize, 0); + + let got = dma_read(&mut s, &ram, (31, 40, 1), read_word4(1, 14), 2); + assert_eq!(got[..3], [0x05, 0xa3, 0xa5]); + pixel_write(&mut s, 31, 40, 0x2a); + let got = dma_read(&mut s, &ram, (31, 40, 1), read_word4(0, 1), 1); + assert_eq!(got[..2], [0x2a, 0xa5]); + + s.regs().set_dispmode((0) as usize, ODSY_GAMMA_GROUP << dispmode::GROUP_SHIFT); + let got = dma_read(&mut s, &ram, (30, 40, 1), read_word4(0, 1), 1); + assert_eq!(got[..2], [0xa5, 0xa5], "the red byte of 0xa55a3cff"); +} + +/// A GL image write by DMA to the draw drawable: the table at `TABLE` names +/// `payload` for a `w` x 1 box at (x, y) under word 4 `word4` and word 7's +/// low bits `code`, four bytes a pixel, rows bottom-up. +fn dma_write(s: &mut Board, ram: &Ram, (x, y, w): (u32, u32, u32), word4: u32, code: u32, payload: &[u8]) { + dma_write_sized(s, ram, (x, y, w), word4, code, payload.len() as u32 / w, payload); +} + +/// The same at `bytes` bytes a pixel. +fn dma_write_sized(s: &mut Board, ram: &Ram, (x, y, w): (u32, u32, u32), word4: u32, code: u32, bytes: u32, payload: &[u8]) { + let bpp_code = DMA_BPP.iter().position(|&b| b as u32 == bytes).expect("a size the header can name") as u32; + + ram.put(PAGE_A, payload); + put_table( + ram, + TABLE, + [dma::SENTINEL_DRAW, y << 16 | x, 0x50, 1 << 16 | w, word4, 0, w * bytes, 1 << 30 | bpp_code << 16 | code], + &[pte(PAGE_A, w * bytes)], + ); + descriptor(s, TABLE); +} + +fn stencil_at(s: &Board, x: i32, y: i32) -> u32 { + let p = s.planes.stencil.plane(s.planes.stencil_plane).expect("the stencil plane"); + s.planes.stencil.plane_load(&p, x, y) +} + +/// A depth read from a 12/12/12/12 window is class 1 code 0, two bytes a +/// pixel: the plane's 24-bit depth word in its 16-bit form (exponent and +/// the top ten mantissa bits). +/// +/// The request is established; that the board hands back the plane's 16-bit +/// form is inferred. +#[test] +fn a_16_bit_depth_read_returns_the_planes_16_bit_form() { + let (mut s, ram) = read_window(); + s.depth_store(30, 40, 0x002b_5a1f); + + let got = dma_read(&mut s, &ram, (30, 40, 1), read_word4(1, 0), 2); + assert_eq!(got[..3], [0x2b, 0x5a, 0xa5]); +} + +/// The field codes (0,0) and (4,0) on class 2 code 2 mark a depth read of +/// four bytes, the plane's 24-bit word; (4,0) on class 0 code 0 a stencil +/// read of one byte. The same header with a colour pair is colour, as the +/// test above has it. Nothing here is the colour planes. +/// +/// Established that the field codes separate them; that the plane holds the +/// board's word is inferred; the stencil plane is a model choice. +#[test] +fn a_depth_or_stencil_read_returns_the_planes_word_or_byte() { + let (mut s, ram) = read_window(); + s.depth_store(30, 40, 0x00d2_a45f); + s.stencil_store(30, 40, 0xc7); + + for (a, b) in [(0, 0), (4, 0)] { + let got = dma_read(&mut s, &ram, (30, 40, 1), with_fields(read_word4(2, 2), a, b), 4); + assert_eq!(got[..5], [0x00, 0xd2, 0xa4, 0x5f, 0xa5], "depth, fields ({a},{b})"); + } + let got = dma_read(&mut s, &ram, (30, 40, 1), with_fields(read_word4(0, 0), 4, 0), 1); + assert_eq!(got[..2], [0xc7, 0xa5]); +} + +/// A depth image is format 0, type 4, word 4's size field 1, and a stencil +/// image the same with size 0. Each goes to its own plane, one four-byte +/// word a pixel, and the colour planes stay as they were. +/// +/// Established as the library's request. +#[test] +fn a_depth_or_stencil_image_by_dma_writes_its_own_plane() { + let (mut s, ram) = read_window(); + + dma_write(&mut s, &ram, (30, 40, 2), 1 << 13 | 0x1000, 0x080, &[0xff, 0x12, 0x34, 0x56, 0x00, 0xab, 0xcd, 0xef]); + assert_eq!(s.depth_load(30, 40), 0x0012_3456, "bits 31:24 are not depth"); + assert_eq!(s.depth_load(31, 40), 0x00ab_cdef); + + dma_write(&mut s, &ram, (30, 40, 2), 0x1000, 0x080, &[0, 0, 0, 0x11, 0, 0, 0, 0xc7]); + assert_eq!(stencil_at(&s, 30, 40), 0x11); + assert_eq!(stencil_at(&s, 31, 40), 0xc7); + assert_eq!(s.depth_load(30, 40), 0x0012_3456, "a stencil write leaves depth"); + assert_eq!(pixel(&s, 0, 30, 40), READ_PIXEL, "and colour"); +} + +/// The burst carries no size field, so the plane comes from the write-mask +/// block (0x052) sent just before it. A burst writes depth when bit 24 is +/// set; stencil goes through bits 19:12 as `(data & mask) | (old & !mask)`. +/// Neither lands in the colour planes. The library's depth download clears +/// the stencil mask and its stencil download clears bit 24, so each leaves +/// the other plane alone. +/// +/// ESTABLISHED: the burst has +/// no plane selector and the library sets block 0x052 as above; INFERRED: +/// the enables pick the part that changes; UNKNOWN: depth and stencil tests +/// on the burst, which the model does not apply). +#[test] +fn a_depth_or_stencil_burst_writes_the_plane_the_write_mask_names() { + let (mut s, _) = window(); + let write_mask = |s: &mut Board, v: u32| { + cmd(s, select(reg::WRITE_MASK)); + cmd(s, v); + }; + // Depth download: bit 24 set, stencil mask clear. + write_mask(&mut s, write::DEPTH); + burst_image(&mut s, (40, 10, 42, 11), 0x0021_1080, image::FMT_RGB, &[0xff12_3456, 0x00ab_cdef]); + assert_eq!(s.depth_load(40, 10), 0x0012_3456, "bits 31:24 are not depth"); + assert_eq!(s.depth_load(41, 10), 0x00ab_cdef); + assert_eq!(stencil_at(&s, 40, 10), 0, "a depth download leaves stencil"); + + // Stencil download: bit 24 clear, mask 0xf0 in bits 19:12. + s.stencil_store(40, 10, 0x0f); + s.stencil_store(41, 10, 0x0f); + write_mask(&mut s, 0xf0 << 12); + burst_image(&mut s, (40, 10, 42, 11), 0x0021_1080, image::FMT_RGB, &[0x00a5_0000, 0x005a_0000]); + assert_eq!(stencil_at(&s, 40, 10), 0xaf, "the masked bits change, the rest keep"); + assert_eq!(stencil_at(&s, 41, 10), 0x5f); + assert_eq!(s.depth_load(40, 10), 0x0012_3456, "a stencil download leaves depth"); + + // The mask applies to the index in bits 23:16, not to the word as sent: + // data in bits 19:12 (where the mask sits) is not the index. + s.stencil_store(42, 10, 0x3c); + write_mask(&mut s, 0xff << 12); + burst_image(&mut s, (42, 10, 44, 11), 0x0021_1080, image::FMT_RGB, &[0x0000_f000, 0x0081_f0ff]); + assert_eq!(stencil_at(&s, 42, 10), 0x00, "bits 19:12 alone are index 0"); + assert_eq!(stencil_at(&s, 43, 10), 0x81, "the index is bits 23:16"); + + // A zero stencil mask and no depth bit: nothing is written. + write_mask(&mut s, 0); + burst_image(&mut s, (40, 10, 41, 11), 0x0011_1080, image::FMT_RGB, &[0x0099_0000]); + assert_eq!(s.depth_load(40, 10), 0x0012_3456); + assert_eq!(stencil_at(&s, 40, 10), 0xaf); + for buffer in 0..2 { + assert_eq!(pixel(&s, buffer, 40, 10), 0); + assert_eq!(pixel(&s, buffer, 41, 10), 0); + } +} + +/// glDrawPixels of types the model once dropped or laid out by the burst +/// tag. The library names each pair as a format (word 7 bits 4:0) and a type +/// (bits 8:5); a packed type is under the format with its memory order. +/// Small images come as pixel bursts, so the pixels here are the bytes of +/// the stream: two 16-bit pixels a word, a pixel every byte, and so on. +/// +/// Established as what the library sends; the widening of narrow fields to a +/// byte is inferred. +#[test] +fn a_burst_image_of_a_packed_or_single_channel_type_lands_as_that_pixel() { + let (mut s, _) = window(); + + // RGBA, 4_4_4_4 (format 0xb, type 6): two pixels in one word. + burst_image(&mut s, (40, 10, 42, 11), 0x0021_10cb, image::FMT_RGB, &[0xa53f_1234]); + assert_eq!(pixel(&s, 0, 40, 10), 0xaa55_33ff); + assert_eq!(pixel(&s, 0, 41, 10), 0x1122_3344); + + // RGBA, 5_5_5_1 (type 7). + burst_image(&mut s, (40, 11, 41, 12), 0x0011_10eb, image::FMT_RGB, &[0xa2cf_0000]); + assert_eq!(pixel(&s, 0, 40, 11), 0xa55a_3aff); + + // RGBA, 10_10_10_2 (type 8): a word a pixel. + burst_image(&mut s, (40, 12, 41, 13), 0x0011_110b, image::FMT_RGB, &[0xa596_93c3]); + assert_eq!(pixel(&s, 0, 40, 12), 0xa55a_3cff); + + // RED, ALPHA and BGR bytes (formats 4, 7, 0xd; type 2): the channels + // the source lacks are 0 and full alpha. + burst_image(&mut s, (40, 13, 41, 14), 0x0111_1044, image::FMT_RGB, &[0x8000_0000]); + assert_eq!(pixel(&s, 0, 40, 13), 0x8000_00ff); + burst_image(&mut s, (40, 14, 41, 15), 0x0111_1047, image::FMT_RGB, &[0x4000_0000]); + assert_eq!(pixel(&s, 0, 40, 14), 0x0000_0040); + burst_image(&mut s, (40, 15, 41, 16), 0x0111_104d, image::FMT_RGB, &[0x3c5a_a500]); + assert_eq!(pixel(&s, 0, 40, 15), 0xa55a_3cff); + + // RGBA, UNSIGNED_INT: four words a pixel, under either burst tag. + for (i, fmt) in [image::FMT_RGB, image::FMT_RGBA16].into_iter().enumerate() { + let (y, row) = (16 + i as u32, 16 + i as i32); + + burst_image(&mut s, (40, y, 42, y + 1), 0x0021_108b, fmt, &[0x8000_0000, 0xffff_ffff, 0, 0x4000_0000, 0xffff_ffff, 0, 0xffff_ffff, 0]); + assert_eq!(pixel(&s, 0, 40, row), 0x80ff_0040); + assert_eq!(pixel(&s, 0, 41, row), 0xff00_ff00); + } +} + +/// Image writes by DMA to the draw drawable of the types that came with a +/// larger image: unsigned-int components, 3_3_2 bytes, BGRA bytes and +/// 16-bit luminance. +#[test] +fn a_dma_image_of_an_int_or_packed_type_lands_as_that_pixel() { + let (mut s, ram) = window(); + + // RGBA, UNSIGNED_INT (format 0xb, type 4): 16 bytes a pixel. + let mut word = vec![]; + for c in [0x8000_0000u32, 0xffff_ffff, 0, 0x4000_0000] { + word.extend_from_slice(&c.to_be_bytes()); + } + dma_write_sized(&mut s, &ram, (50, 20, 1), 0x1000, 0x08b, 16, &word); + assert_eq!(pixel(&s, 0, 50, 20), 0x80ff_0040); + + // RGB, 3_3_2 (format 9, type 5): 0xe3 is red 7, green 0, blue 3. + dma_write_sized(&mut s, &ram, (50, 21, 1), 0x1000, 0x0a9, 1, &[0xe3]); + assert_eq!(pixel(&s, 0, 50, 21), 0xff00_ffff); + + // BGRA, UNSIGNED_BYTE (format 0x11, type 2). + dma_write_sized(&mut s, &ram, (50, 22, 1), 0x1000, 0x051, 4, &[0x3c, 0x5a, 0xa5, 0x22]); + assert_eq!(pixel(&s, 0, 50, 22), 0xa55a_3c22); + + // LUMINANCE, UNSIGNED_SHORT (format 2, type 3): spread over red, green + // and blue. + dma_write_sized(&mut s, &ram, (50, 23, 1), 0x1000, 0x062, 2, &[0x80, 0x80]); + assert_eq!(pixel(&s, 0, 50, 23), 0x8080_80ff); +} + +/// The table maps a few codes it should refuse: depth and stencil (format +/// 0) have their own planes, the 4:2:2 forms are not modelled, and a type +/// the format has no packing for is not a pixel. +#[test] +fn a_write_code_without_a_pixel_form_is_refused() { + assert!(write_form(0x080).is_none(), "depth and stencil"); + assert!(write_form(0x04a).is_none(), "4:2:2"); + assert!(write_form(0x0c1).is_none(), "colour index in a packed type"); + assert_eq!(write_form(0x0eb).map(|f| f.bytes), Some(2)); + assert_eq!(write_form(0x16c).map(|f| f.bytes), Some(4)); + assert_eq!(write_form(0x06b).map(|f| f.bytes), Some(8)); +} + +/// One pixel copy, block 0x2031 with its eight operands. +fn copy_block(s: &mut Board, ops: [u32; 8]) { + cmd(s, consts::cmd::COPY_RECT_TRIGGER); + for w in ops { + cmd(s, w); + } +} + +/// The operands as the library sends a glCopyPixels, taken from a run. +/// +/// Established as what the library sends. +#[test] +fn the_copy_operands_decode_into_source_size_destination_and_headers() { + let c = CopyOperands::decode(&[0x3000_0019, 0x00bb_003c, 0x0050_2050, 0x0009_0015, 0x1263_3042, 0x3000_0010, 0x00a2_0064, 0x3863_204b]); + + assert_eq!(c.src_base, dma::SENTINEL_READ); + assert_eq!(c.src, (0x3c, 0xbb)); + assert_eq!(c.size, (0x15, 9)); + assert_eq!(c.dest_base, dma::SENTINEL_DRAW); + assert_eq!(c.dest, (0x64, 0xa2)); + assert_eq!((c.dest_front, c.dest_drawable, c.dest_field, c.src_drawable), (false, 0x050, 2, 0x050)); + assert!(!c.read_front()); + assert_eq!(c.overlap(), (false, false)); + assert_eq!(c.write_header, 0x3863_204b); + + // The overlap bits are the write header's top two. + let c = CopyOperands::decode(&[0, 0, 0x8000_0000, 0, 0x9263_3042, 0, 0, 0xf802_204b]); + assert!(c.dest_front && c.read_front()); + assert_eq!(c.overlap(), (true, true)); +} + +/// A glCopyPixels from the read drawable to the draw drawable, front to +/// back by the draw mask, lands the rectangle at the destination, and a copy +/// onto itself, shifted, reads its source as it was before the copy. +/// +/// The model stages the source, so the overlap +/// bits (inferred) need no direction of their own. +#[test] +fn a_gl_copy_between_the_drawables_moves_the_rectangle() { + let (mut s, _) = window(); + for (i, w) in [0x1100_00ffu32, 0x2200_00ff, 0x3300_00ff, 0x4400_00ff].into_iter().enumerate() { + pixel_write(&mut s, 30 + i as i32, 40, w); + } + // Front read (bit 31 of word 4), drawing mask: buffer 1. + s.draw_mask = 4; + copy_block(&mut s, [0x3000_0019, 40 << 16 | 30, 0x0050_2050, 1 << 16 | 4, 0x9263_3042, 0x3000_0010, 70 << 16 | 60, 0x3863_204b]); + for (i, w) in [0x1100_00ffu32, 0x2200_00ff, 0x3300_00ff, 0x4400_00ff].into_iter().enumerate() { + assert_eq!(pixel(&s, 1, 60 + i as i32, 70), w); + } + assert_eq!(pixel(&s, 0, 60, 70), 0, "buffer 0 is not drawn"); + + s.draw_mask = 1; + copy_block(&mut s, [0x3000_0019, 40 << 16 | 30, 0x0050_2050, 1 << 16 | 4, 0x9263_3042, 0x3000_0010, 40 << 16 | 32, 0xf863_204b]); + let got: Vec = (30..36).map(|x| pixel(&s, 0, x, 40)).collect(); + assert_eq!(got, [0x1100_00ff, 0x2200_00ff, 0x1100_00ff, 0x2200_00ff, 0x3300_00ff, 0x4400_00ff]); +} + +/// The id-plane destination as the X server selects it for a menu: the +/// overlay byte under drawing mask 0xc, every bit writable. +fn select_overlay_destination(s: &mut Board) { + s.dest_buffer_word = dest::ID_PLANES; + s.dest_base = 0x14_2000; + s.draw_mask = ODSY_DRAW_MASK_BUFFER1; + s.plane_write_mask = 0xff << ODSY_PLANE_MASK_SHIFT; +} + +/// A rectangle copy between two points of the id-plane base. The operand +/// words are the ones a trace of a menu opening showed the X server sending +/// (no other source records them): both base words name the id planes. +fn overlay_copy(s: &mut Board, (sx, sy): (u32, u32), (w, h): (u32, u32), (dx, dy): (u32, u32)) { + copy_block(s, [0x14_2000, sy << 16 | sx, 0x0050_0050, h << 16 | w, 0x8000_1000, 0x14_2000, dy << 16 | dx, 0x3a00_0041]); +} + +fn overlay_at(s: &Board, x: u32, y: u32) -> u8 { + s.planes.overlay[(y * ODSY_WIDTH + x) as usize] +} + +/// A menu's pattern fill replicates a seed by copying the id-plane base onto +/// itself. The copy moves the overlay bytes and leaves the colour planes +/// alone: copying colour there smeared the window under every popup menu. +/// +/// The menu is drawn at the id-plane base; that its copies stay there is +/// inferred. +#[test] +fn a_copy_at_the_id_plane_base_moves_overlay_bytes_not_colour() { + let (mut s, _) = window(); + select_overlay_destination(&mut s); + for x in 0..4u32 { + s.planes.overlay[(40 * ODSY_WIDTH + 30 + x) as usize] = 0x11 * (x as u8 + 1); + pixel_write(&mut s, 30 + x as i32, 40, 0xaa00_00ff | x << 8); + } + let colour: Vec = (30..70).map(|x| pixel(&s, 0, x, 40)).collect(); + s.scanout_update(); + + overlay_copy(&mut s, (30, 40), (4, 1), (60, 41)); + for x in 0..4u32 { + assert_eq!(overlay_at(&s, 60 + x, 41), 0x11 * (x as u8 + 1), "overlay byte {x}"); + } + let after: Vec = (30..70).map(|x| pixel(&s, 0, x, 40)).collect(); + assert_eq!(after, colour, "colour row 40 is untouched"); + assert!((60..64).all(|x| pixel(&s, 0, x, 41) == 0), "colour row 41 is untouched"); + assert!((0..2).all(|b| (0..4).all(|x| pixel(&s, b, 60 + x, 41) == 0))); + assert_eq!(s.scanout.marks.dirty_rows(), vec![41], "the destination row is resolved again"); +} + +/// A copy of the overlay onto itself, shifted, reads its source as it was: +/// along a row (the shift is smaller than the width) and down a column. +#[test] +fn an_overlay_copy_onto_itself_reads_the_source_as_it_was() { + let (mut s, _) = window(); + select_overlay_destination(&mut s); + for x in 0..4u32 { + s.planes.overlay[(40 * ODSY_WIDTH + 30 + x) as usize] = x as u8 + 1; + } + overlay_copy(&mut s, (30, 40), (4, 1), (32, 40)); + let row: Vec = (30..36).map(|x| overlay_at(&s, x, 40)).collect(); + assert_eq!(row, [1, 2, 1, 2, 3, 4], "a copy to the right"); + + overlay_copy(&mut s, (32, 40), (4, 1), (30, 40)); + let row: Vec = (30..36).map(|x| overlay_at(&s, x, 40)).collect(); + assert_eq!(row, [1, 2, 3, 4, 3, 4], "a copy to the left"); + + for y in 0..4u32 { + s.planes.overlay[((50 + y) * ODSY_WIDTH + 70) as usize] = y as u8 + 1; + } + overlay_copy(&mut s, (70, 50), (1, 4), (70, 52)); + let column: Vec = (50..56).map(|y| overlay_at(&s, 70, y)).collect(); + assert_eq!(column, [1, 2, 1, 2, 3, 4], "a copy down"); +} + +/// The 0x1023 plane mask limits which overlay bits a copy changes, as it does +/// for every other write there. +#[test] +fn an_overlay_copy_changes_only_the_bits_of_the_plane_mask() { + let (mut s, _) = window(); + select_overlay_destination(&mut s); + s.plane_write_mask = 0x0f << ODSY_PLANE_MASK_SHIFT; + s.planes.overlay[(40 * ODSY_WIDTH + 30) as usize] = 0xa5; + s.planes.overlay[(40 * ODSY_WIDTH + 60) as usize] = 0xf0; + overlay_copy(&mut s, (30, 40), (1, 1), (60, 40)); + assert_eq!(overlay_at(&s, 60, 40), 0xf5); +} + +/// The same copy block with the base words given. +fn copy_bases(s: &mut Board, (src_base, dest_base): (u32, u32), (sx, sy): (u32, u32), (w, h): (u32, u32), (dx, dy): (u32, u32)) { + copy_block(s, [src_base, sy << 16 | sx, 0x0050_0050, h << 16 | w, 0x8000_1000, dest_base, dy << 16 | dx, 0x3a00_0041]); +} + +/// Seed the overlay, the id byte and colour buffer 0 at (30, 40) with three +/// distinct values, so a copy to (60, 41) shows which plane it moved. +fn seed_three_planes(s: &mut Board) { + s.planes.overlay[(40 * ODSY_WIDTH + 30) as usize] = 0x77; + s.planes.did[(40 * ODSY_WIDTH + 30) as usize] = 0x55; + pixel_write(s, 30, 40, 0xaa00_00ff); +} + +fn planes_at(s: &Board, x: u32, y: u32) -> (u8, u8, u32) { + (overlay_at(s, x, y), s.planes.did[(y * ODSY_WIDTH + x) as usize], pixel(s, 0, x as i32, y as i32)) +} + +/// What sends a copy to the overlay: both base words at the id-plane base +/// the last select named, with that select naming the id planes. Each +/// condition alone, dropped, would send some other copy there. +#[test] +fn a_copy_goes_to_the_overlay_only_when_both_bases_and_the_select_say_so() { + // Both bases at the base, but the select names a colour buffer: colour. + let (mut s, _) = window(); + seed_three_planes(&mut s); + select_overlay_destination(&mut s); + s.dest_buffer_word = 0x6950; + overlay_copy(&mut s, (30, 40), (1, 1), (60, 41)); + assert_eq!(planes_at(&s, 60, 41), (0, 0, 0xaa00_00ff), "a colour select keeps the copy in colour"); + + // One base at the id-plane base and the other not: refused, nothing moves. + for bases in [(0x14_2000, 0x2000), (0x2000, 0x14_2000)] { + let (mut s, _) = window(); + seed_three_planes(&mut s); + select_overlay_destination(&mut s); + copy_bases(&mut s, bases, (30, 40), (1, 1), (60, 41)); + assert_eq!(planes_at(&s, 60, 41), (0, 0, 0), "bases {bases:x?}"); + assert!(s.diag.copy_mixed_base_reported, "bases {bases:x?} are noted"); + } + + // The id-plane select with no base named (0) and zero base words is not + // "both at the base": the copy is a colour copy. + let (mut s, _) = window(); + seed_three_planes(&mut s); + select_overlay_destination(&mut s); + s.dest_base = 0; + copy_bases(&mut s, (0, 0), (30, 40), (1, 1), (60, 41)); + assert_eq!(planes_at(&s, 60, 41), (0, 0, 0xaa00_00ff)); + + // A copy at the colour base is colour even while the id planes are selected. + let (mut s, _) = window(); + seed_three_planes(&mut s); + select_overlay_destination(&mut s); + copy_bases(&mut s, (0x2000, 0x2000), (30, 40), (1, 1), (60, 41)); + assert_eq!(planes_at(&s, 60, 41), (0, 0, 0xaa00_00ff)); +} + +/// The drawing mask names the byte a copy moves, and the other byte stays. +#[test] +fn an_id_plane_copy_moves_the_byte_the_drawing_mask_names() { + for (mask, want) in [(ODSY_DRAW_MASK_BUFFER1, (0x77, 0, 0)), (ODSY_DRAW_MASK_BUFFER0, (0, 0x55, 0)), (0, (0, 0, 0))] { + let (mut s, _) = window(); + seed_three_planes(&mut s); + select_overlay_destination(&mut s); + s.draw_mask = mask; + overlay_copy(&mut s, (30, 40), (1, 1), (60, 41)); + let (o, d, c) = planes_at(&s, 60, 41); + assert_eq!((o, d, c), want, "drawing mask {mask:#x}"); + assert_eq!(s.diag.aux_mask_reported, mask == 0, "a mask naming neither byte is noted once"); + } +} + +/// A copy up a column onto itself reads the rows as they were. +#[test] +fn an_overlay_copy_up_onto_itself_reads_the_source_as_it_was() { + let (mut s, _) = window(); + select_overlay_destination(&mut s); + for y in 0..4u32 { + s.planes.overlay[((50 + y) * ODSY_WIDTH + 70) as usize] = y as u8 + 1; + } + overlay_copy(&mut s, (70, 52), (1, 4), (70, 50)); + let column: Vec = (50..56).map(|y| overlay_at(&s, 70, y)).collect(); + assert_eq!(column, [3, 4, 0, 0, 0, 0], "rows 52 and 53 moved up; rows below keep their old bytes"); +} + +/// A copy running off the plane is clipped by its destination, and a source +/// pixel outside the plane reads as zero (model policy, as the colour copy +/// does), past the right edge or the bottom. +#[test] +fn an_overlay_copy_is_clipped_to_the_plane_at_both_ends() { + let w = ODSY_WIDTH; + let h = ODSY_HEIGHT; + let (mut s, _) = window(); + select_overlay_destination(&mut s); + for x in 0..8u32 { + s.planes.overlay[(40 * w + w - 2 + x.min(1)) as usize] = 9; + s.planes.overlay[(60 * w + 10 + x) as usize] = x as u8 + 1; + } + // Source past the right edge: only its last two pixels exist, the rest + // read as zero and overwrite what was there (made non-zero), and the next + // row's first bytes (made distinct) are not read as if they followed. + for x in 0..6u32 { + s.planes.overlay[(41 * w + x) as usize] = 5; + s.planes.overlay[(41 * w + 12 + x) as usize] = 6; + } + overlay_copy(&mut s, (w - 2, 40), (8, 1), (10, 41)); + let row: Vec = (8..18).map(|x| overlay_at(&s, x, 41)).collect(); + assert_eq!(row, [0, 0, 9, 9, 0, 0, 0, 0, 0, 0]); + // Destination past the right edge: only two pixels land, and the next + // row's first bytes are not written. + overlay_copy(&mut s, (10, 60), (8, 1), (w - 2, 62)); + assert_eq!((overlay_at(&s, w - 2, 62), overlay_at(&s, w - 1, 62), overlay_at(&s, 0, 63)), (1, 2, 0)); + // Rows past the bottom, source or destination: source rows off the plane + // read as zero. + s.planes.overlay[(102 * w + 20) as usize] = 3; + s.planes.overlay[((h - 2) * w + 5) as usize] = 7; + s.planes.overlay[((h - 1) * w + 5) as usize] = 8; + overlay_copy(&mut s, (5, h - 2), (1, 6), (20, 100)); + assert_eq!((overlay_at(&s, 20, 100), overlay_at(&s, 20, 101), overlay_at(&s, 20, 102)), (7, 8, 0)); + overlay_copy(&mut s, (5, h - 2), (1, 6), (30, h - 1)); + assert_eq!((overlay_at(&s, 30, h - 1), overlay_at(&s, 30, 0)), (7, 0)); +} + +/// An object copies to the draw drawable and back: the scratch-object path +/// the library takes for the signed types, and for readback conversions. +#[test] +fn a_gl_copy_moves_pixels_between_an_object_and_the_drawables() { + let (mut s, _) = window(); + let base = 0x16a800; + let obj = s.object_select(base, 16); + for x in 0..3 { + s.object_store(obj, x, 0, 0x8000_0000 | x << 8 | 0xff); + } + s.draw_mask = 1; + copy_block(&mut s, [base, 0, 0x0050_2020, 1 << 16 | 3, 0x8002_500b, 0x3000_0010, 20 << 16 | 50, 0x3863_204b]); + for x in 0..3 { + assert_eq!(pixel(&s, 0, 50 + x, 20), 0x8000_0000 | (x as u32) << 8 | 0xff); + } + + // And from the window into the object, at an offset within it. + pixel_write(&mut s, 70, 21, 0x0a0b_0c0d); + copy_block(&mut s, [0x3000_0019, 21 << 16 | 70, 0x8020_0050, 1 << 16 | 1, 0x9263_3042, base, 1 << 16 | 5, 0x3802_204b]); + assert_eq!(s.object_load(obj, 5, 1), 0x0a0b_0c0d); +} + +/// A copy from the window into an object lists the rectangle's rows +/// bottom first. glReadPixels and glCopyTexImage copy the window into an +/// object (block 0x2031 with the source at the rectangle's top row, +/// operands replayed from a guest trace) and then read the object top row +/// first, with no bottom-up bit, yet expect the lowest row of the +/// rectangle first. Before this the host got the top row where it wanted +/// the bottom one. +#[test] +fn a_copy_from_the_window_into_an_object_lists_the_rows_bottom_first() { + let (mut s, _) = window(); + let base = 0x16b000; + let obj = s.object_select(base, 16); + // A 2x3 rectangle at (20, 221) down: row n holds n in its low byte. + for y in 0..3 { + for x in 0..2 { + pixel_write(&mut s, 20 + x, 221 + y, (x as u32) << 8 | y as u32); + } + } + s.draw_mask = 4; + copy_block(&mut s, [0x3000_0019, 221 << 16 | 20, 0x8020_0050, 3 << 16 | 2, 0x9263_3042, base, 0, 0x3802_204b]); + for y in 0..3u32 { + for x in 0..2u32 { + assert_eq!(s.object_load(obj, x, y), x << 8 | (2 - y), "object ({x}, {y})"); + } + } +} + +/// The copy that ends a signed-type glDrawPixels, replayed from a guest trace +/// (signed BYTE, RGBA, 1 pixel wide, 2 rows). The raw bursts reach an object +/// as the unsigned type's would: row 0 holds the bytes 0x91 0xec 0x47 0xa2 +/// (signed -111, -20, 71, -94), row 1 0x05 0x60 0xbb 0x16. The library then +/// programs block 0x203b with 0x2000, 0x0001 in each of R, G, B and A and +/// copies the object to the window (operands as traced). +/// +/// Two things must happen. The rows land bottom first (row 1 above row 0), +/// and the pass reads the bytes as signed: 2c + 1 of GL's conversion, clamped. +/// Before this the window held the raw bytes with the rows the wrong way up, +/// which a test pattern with 29 per row reads as raw + 0x74 for a 4-row image. +/// +/// The expected bytes are INFERRED from the 3.12 words (2 * c / 255 + 1/4096, +/// rounded), one below GL's 2c + 1 where that is positive. +#[test] +fn a_signed_type_copy_lists_rows_bottom_first_and_applies_the_signed_pass() { + let (mut s, _) = window(); + let base = 0x16b000; + let obj = s.object_select(base, 16); + s.object_store(obj, 0, 0, 0x91ec_47a2); + s.object_store(obj, 0, 1, 0x0560_bb16); + s.draw_mask = 1; + + // Identity filter first: raw bytes, only the rows move. + copy_block(&mut s, [base, 0, 0x0050_2020, 2 << 16 | 1, 0x8002_500b, 0x3000_0010, 20 << 16 | 50, 0x3863_204b]); + assert_eq!(pixel(&s, 0, 50, 20), 0x0560_bb16, "row 1 is the rectangle's top row"); + assert_eq!(pixel(&s, 0, 50, 21), 0x91ec_47a2); + + for c in 0..4 { + s.tex.scale_bias[c * 2] = 0x2000; + s.tex.scale_bias[c * 2 + 1] = 0x0001; + } + // A source without alpha programs the alpha bias 0x1000: alpha reads 1.0. + s.tex.scale_bias[7] = 0x1000; + copy_block(&mut s, [base, 0, 0x0050_2020, 2 << 16 | 1, 0x8002_500b, 0x3000_0010, 20 << 16 | 50, 0x3863_204b]); + assert_eq!(pixel(&s, 0, 50, 21) & 0xff, 0xff, "alpha lane"); + s.tex.scale_bias[7] = 1; + copy_block(&mut s, [base, 0, 0x0050_2020, 2 << 16 | 1, 0x8002_500b, 0x3000_0010, 20 << 16 | 50, 0x3863_204b]); + assert_eq!(pixel(&s, 0, 50, 20), 0x0a_c0_00_2c, "5, 96, -69, 22"); + assert_eq!(pixel(&s, 0, 50, 21), 0x00_00_8e_00, "-111, -20, 71, -94"); +} + +/// A copy from the window through a non-identity scale and bias reads its +/// bytes as unsigned: scale 0.5 and bias 0.25 (3.12 words 0x0800, 0x0400) +/// give b / 2 + 63.75 per lane, rounded, and a lane the filter does not change +/// (scale 1.0, bias 0) keeps its byte. INFERRED arithmetic: the 3.12 words +/// are the only established part. +#[test] +fn a_copy_from_the_window_applies_the_programmed_scale_and_bias_unsigned() { + let (mut s, _) = window(); + let base = 0x16b000; + let obj = s.object_select(base, 16); + pixel_write(&mut s, 70, 21, 0x00ff_8010); + s.draw_mask = 4; + for c in [0, 1, 2] { + s.tex.scale_bias[c * 2] = 0x0800; + s.tex.scale_bias[c * 2 + 1] = 0x0400; + } + copy_block(&mut s, [0x3000_0019, 21 << 16 | 70, 0x8020_0050, 1 << 16 | 1, 0x9263_3042, base, 0, 0x3802_204b]); + // 0x00 -> 64, 0xff -> 191, 0x80 -> 128, alpha lane untouched (0x10). + assert_eq!(s.object_load(obj, 0, 0), 0x40bf_8010); +} + +/// The read buffer register (0x609) names the drawable a read comes from. +fn bind_read_drawable(s: &mut Board, word: u32) { + cmd(s, select(swap::SYNC_B)); + cmd(s, word); +} + +/// A window's logical front is the buffer its id displays. A pixel buffer +/// has no id on screen, so its front is the buffer a front draw mask writes: +/// bit 31 of the read buffer register (0x609) set means the drawable's swap +/// state is zero and the mask is unexchanged, so front is buffer 0; clear +/// means front and back are exchanged, so front is buffer 1. The read +/// drawable is the off-screen object when 0x609 names one and not the +/// screen's colour buffer. A DMA read and a copy follow the same rule. +/// +/// ESTABLISHED: the kernel +/// alone writes bit 31 of 0x609, from the drawable's swap halfword, and a +/// pixel buffer's pane swaps like a window's; INFERRED: a read's front is +/// the buffer a front draw mask writes. +#[test] +fn a_pixel_buffers_front_is_the_buffer_a_front_draw_mask_writes() { + let (mut s, ram) = read_window(); + colour_destination(&mut s); + let p = s.planes.buffer1.plane(s.planes.buffer1_plane).expect("the second colour plane"); + s.planes.buffer1.plane_store(&p, 30, 40, 0x0102_0304); + let front = read_word4(2, 8); + let back = front & !dma::FMT_FRONT; + let in_buffer_0 = READ_PIXEL.to_be_bytes(); + let in_buffer_1 = 0x0102_0304u32.to_be_bytes(); + + // The window's id displays buffer 1: its front is buffer 1. + s.front.lock().displayed_buffer = 1; + bind_read_drawable(&mut s, 0x8000_2000); + assert_eq!(dma_read(&mut s, &ram, (30, 40, 1), front, 4)[..4], in_buffer_1, "window front: displayed"); + assert_eq!(dma_read(&mut s, &ram, (30, 40, 1), back, 4)[..4], in_buffer_0, "window back"); + + // A pixel buffer, whichever buffer the ids under it display. + for displayed in [0, 1] { + s.front.lock().displayed_buffer = displayed; + + bind_read_drawable(&mut s, 0x8016_a800); + assert_eq!(dma_read(&mut s, &ram, (30, 40, 1), front, 4)[..4], in_buffer_0, "unswapped front: buffer 0"); + assert_eq!(dma_read(&mut s, &ram, (30, 40, 1), back, 4)[..4], in_buffer_1, "unswapped back: buffer 1"); + + bind_read_drawable(&mut s, 0x0016_a800); + assert_eq!(dma_read(&mut s, &ram, (30, 40, 1), front, 4)[..4], in_buffer_1, "swapped front: buffer 1"); + assert_eq!(dma_read(&mut s, &ram, (30, 40, 1), back, 4)[..4], in_buffer_0, "swapped back: buffer 0"); + } + + // The copy into a scratch object takes the same buffer. + let base = 0x16b000; + let obj = s.object_select(base, 16); + copy_block(&mut s, [0x3000_0019, 40 << 16 | 30, 0x8020_0050, 1 << 16 | 1, front, base, 0, 0x3802_204b]); + assert_eq!(s.object_load(obj, 0, 0), 0x0102_0304, "swapped front"); +} + +/// The display lines still waiting to be resolved again. +fn dirty_lines(s: &Board) -> Vec { + s.scanout.marks.dirty_rows() +} + +/// A GL image write, by burst or by DMA, and a pixel copy write colour +/// planes from the front end and not as a raster job, so nothing else +/// records their rows: each marks the display lines it wrote, those and no +/// others, or the display would not show the pixels (or would resolve the +/// whole frame for one). +#[test] +fn a_gl_pixel_write_marks_the_display_lines_it_wrote() { + let (mut s, ram) = window(); + s.scanout_update(); + assert_eq!(dirty_lines(&s), Vec::::new(), "a settled display has nothing to resolve"); + + burst_image(&mut s, (40, 10, 42, 12), 0x0001_104b, image::FMT_RGB, &[0x1100_00ff, 0x2200_00ff, 0x3300_00ff, 0x4400_00ff]); + assert_eq!(dirty_lines(&s), [10, 11], "a burst image"); + s.scanout_update(); + + dma_write(&mut s, &ram, (50, 20, 2), 0x1000, 0x04b, &[1, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(dirty_lines(&s), [20], "a DMA image"); + s.scanout_update(); + + copy_block(&mut s, [0x3000_0019, 10 << 16 | 40, 0x0050_2050, 2 << 16 | 2, 0x9263_3042, 0x3000_0010, 30 << 16 | 60, 0x3863_204b]); + assert_eq!(dirty_lines(&s), [30, 31], "a pixel copy"); +} + +/// The library draws a pixel zoom as a texture move: the image sits in a +/// scratch object under level descriptor 60, BEGIN of type 7 opens quads, +/// one 14-float word per source row draws a rectangle, and END closes. The +/// floats are a constant colour (0, 0, 0, 1), the destination corners +/// (x0, ya, z) and (x1, yb, z) in window pixels, and the source corners +/// (s0, t0) and (s1, t1) as fractions of the object's power-of-two size, +/// which the image only partly fills. Here a 2x2 image is drawn at zoom 2 into a 4x4 square: each source texel fills a 2x2 block. +/// +/// ESTABLISHED: the word sequence and layout; INFERRED: the colour floats +/// and that the corners are window pixels. +#[test] +fn a_pixel_zoom_rectangle_draws_the_scratch_texture_over_the_destination() { + let s = zoom_2x2(0x0364_0000); + + for (x, y, want) in [(20, 10, 0x1100_00ffu32), (21, 11, 0x1100_00ff), (22, 10, 0x2200_00ff), (23, 11, 0x2200_00ff), + (20, 12, 0x3300_00ff), (23, 13, 0x4400_00ff)] { + assert_eq!(pixel(&s, 0, x, y), want, "pixel ({x}, {y})"); + } + assert_eq!(pixel(&s, 0, 24, 10), 0, "right of the rectangle"); + assert_eq!(pixel(&s, 0, 20, 14), 0, "below the rectangle"); +} + +/// The texture move writes no wrap or filter bit of the sampler's word 4 +/// (bits 13:0), so they are the context's, and the zoom samples with the +/// filter the context holds: a linear magnification blends the neighbouring +/// texels where nearest copies one. +/// +/// ESTABLISHED: the move sends no wrap or filter bit; INFERRED: magnification +/// is linear by default. +#[test] +fn a_pixel_zoom_samples_with_the_contexts_magnification_filter() { + let nearest = zoom_2x2(0x0364_0000); + let linear = zoom_2x2(0x0364_0002); + + assert_eq!(pixel(&nearest, 0, 21, 11), 0x1100_00ff, "nearest copies a texel"); + let blended = pixel(&linear, 0, 21, 11); + assert_ne!(blended, 0, "the linear zoom still draws"); + assert_ne!(blended, pixel(&nearest, 0, 21, 11), "linear blends where nearest copies"); +} + +/// The size word's texture-offset code is 2 only when the scratch object's +/// two offset fields are both 2, and 0 otherwise; bit 27 is that code's +/// low bit, so a move whose size word has it set is the same zoom, and bit +/// 26 is never set. +/// +/// ESTABLISHED. +#[test] +fn a_pixel_zoom_draws_with_the_offset_code_bit_set_or_clear() { + let plain = zoom_2x2_sized(0x0364_0000, 0); + let offset2 = zoom_2x2_sized(0x0364_0000, 0x0800_0000); + + for y in 10..14 { + for x in 20..24 { + assert_eq!(pixel(&offset2, 0, x, y), pixel(&plain, 0, x, y), "pixel ({x}, {y})"); + } + } + assert_eq!(pixel(&offset2, 0, 20, 10), 0x1100_00ff); +} + +fn zoom_2x2(word4: u32) -> Box { + zoom_2x2_sized(word4, 0) +} + +/// Draws the 2x2 image at zoom 2 into the 4x4 square at (20, 10), with +/// `word4` as the sampler's word 4 and `size_bits` or-ed into its size +/// word, and returns the board. +fn zoom_2x2_sized(word4: u32, size_bits: u32) -> Box { + let (mut s, _) = window(); + let base = 0x16b000; + let obj = s.object_select(base, 512); + let texels = [[0x1100_00ffu32, 0x2200_00ff], [0x3300_00ff, 0x4400_00ff]]; + + assert!(s.object_rows(obj, 2)); + for (y, row) in texels.iter().enumerate() { + for (x, &t) in row.iter().enumerate() { + s.object_store(obj, x as u32, y as u32, t); + } + } + // The move's own raster state: 2D texturing, replace, nearest, level 60. + s.tex.mode = tex::MODE_2D; + s.tex.level0 = 60; + s.tex.levels[180..183].copy_from_slice(&[base, 0x8210_0020, 0]); + s.tex.sampler = [(9 << 10) | (6 << 5) | size_bits, 512, 64, 0, word4, 0, 0]; + s.tex.env = [1, 0, 0, 0, 0]; + + cmd(&mut s, select(reg::GL_BEGIN)); + cmd(&mut s, gl_cmd::QUADS); + for (ya, yb, t0, t1) in [(10.0f32, 12.0f32, 0.0f32, 1.0 / 64.0), (12.0, 14.0, 1.0 / 64.0, 2.0 / 64.0)] { + cmd(&mut s, gl_cmd::TEX_RECT); + for f in [0.0f32, 0.0, 0.0, 1.0, 20.0, ya, 0.0, 24.0, yb, 0.0, 0.0, t0, 2.0 / 512.0, t1] { + cmd(&mut s, f.to_bits()); + } + } + cmd(&mut s, select(reg::GL_END)); + s +} + +/// An integer pixel through a logic operation, or to the id planes, marks +/// its own display line to resolve again. The end of the primitive no +/// longer marks the whole frame for them. +#[test] +fn an_integer_pixel_through_a_logic_op_or_to_the_id_planes_dirties_its_row() { + let (mut s, _) = window(); + let lines = |s: &Board| s.scanout.marks.dirty_rows(); + s.scanout_update(); + + s.stream.pattern_mode |= mode::COLOR_LOGIC_OP; + s.gl_ctx.cur.logic_op_mode = GL_CLEAR + LOGIC_XOR; + s.paint_integer(40, 120, false, 0x00ff_ff00); + assert_eq!(lines(&s), vec![120], "a logic-op pixel"); + s.scanout_update(); + + s.dest_buffer_word = dest::ID_PLANES; + s.paint_integer(40, 130, true, 0x10); + assert_eq!(lines(&s), vec![130], "an id-plane pixel"); +} + +/// Luminance sources reach the board under tags the byte-stream tables did +/// not pair with them. Replayed from guest traces of the GL test suite's +/// 13x5 draws (box 40,206 to 53,201, the y range decreasing): 16-bit +/// luminance (0x62) arrives as two 16-bit values a word under tag 0x12, +/// 16-bit luminance-alpha (0x68) as a luminance and an alpha half a pixel, +/// and 8-bit luminance-alpha (0x48) as byte pairs under tag 0x11. +#[test] +fn luminance_images_land_under_the_tags_the_library_sends_them_with() { + let (mut s, _) = window(); + + // LUMINANCE/UNSIGNED_SHORT: eight pixels, then the five that finish the row. + burst_image_groups( + &mut s, + (40, 206, 53, 205), + &[ + (0x0f00_1262, &[(image::FMT_RGBA16, &[0x1111_4646, 0x7b7b_b0b0, 0xe5e5_1a1a, 0x4f4f_8484])]), + (0x0fa5_1262, &[(image::FMT_RGBA16, &[0xb9b9_eeee, 0x2323_5858, 0x8d8d_5a5a])]), + ], + ); + for (x, l) in [0x11, 0x46, 0x7b, 0xb0, 0xe5, 0x1a, 0x4f, 0x84, 0xb9, 0xee, 0x23, 0x58, 0x8d].into_iter().enumerate() { + assert_eq!(pixel(&s, 0, 40 + x as i32, 205), l << 24 | l << 16 | l << 8 | 0xff, "pixel {x}"); + } + + // LUMINANCE_ALPHA/UNSIGNED_SHORT: a luminance half then an alpha half. + burst_image_groups(&mut s, (40, 210, 43, 211), &[(0x0f00_1268, &[(image::FMT_RGBA16, &[0x1111_6c6c, 0x4646_a1a1])])]); + assert_eq!(pixel(&s, 0, 40, 210), 0x1111_116c); + assert_eq!(pixel(&s, 0, 41, 210), 0x4646_46a1); + + // LUMINANCE_ALPHA/UNSIGNED_BYTE: a luminance and an alpha byte a pixel, + // under the RGB tag; the last word of a 13-pixel row carries two pad bytes. + burst_image_groups( + &mut s, + (40, 212, 53, 213), + &[(0x0fd5_1248, &[(image::FMT_RGB, &[0x116c_46a1, 0x7bd6_b00b, 0xe540_1a75, 0x4faa_84df, 0xb914_ee49, 0x237e_58b3, 0x8de8_5a5a])])], + ); + assert_eq!(pixel(&s, 0, 40, 212), 0x1111_116c); + assert_eq!(pixel(&s, 0, 41, 212), 0x4646_46a1); + assert_eq!(pixel(&s, 0, 52, 212), 0x8d8d_8de8); +} + +/// A 16-bit luminance texture upload: the library writes the level into a +/// scratch object (box 0,0 to 16,8, destination base 0x100800 on the V6, the +/// first row's eight words in two four-word bursts under tag 0x12, replayed +/// from the guest trace of the GL test suite's tex/RGBA8/LUMINANCE/ +/// UNSIGNED_SHORT case). The bursts must be taken: refused, they leave the level +/// holding whatever the object held before and the case failing on a board +/// whose object memory started out different. +#[test] +fn a_16_bit_luminance_texture_upload_lands_in_its_object() { + let (mut s, _) = window(); + let base = 0x10_0800; + let obj = s.object_select(base, 16); + for x in 0..16 { + s.object_store(obj, x, 0, 0xffff_ffff); + } + s.dest_base = base; + burst_image_groups( + &mut s, + (0, 0, 16, 8), + &[( + 0x0f00_1262, + &[ + (image::FMT_RGBA16, &[0x1111_4646, 0x7b7b_b0b0, 0xe5e5_1a1a, 0x4f4f_8484]), + (image::FMT_RGBA16, &[0xb9b9_eeee, 0x2323_5858, 0x8d8d_c2c2, 0xf7f7_2c2c]), + ], + )], + ); + let row = [0x11, 0x46, 0x7b, 0xb0, 0xe5, 0x1a, 0x4f, 0x84, 0xb9, 0xee, 0x23, 0x58, 0x8d, 0xc2, 0xf7, 0x2c]; + for (x, l) in row.into_iter().enumerate() { + assert_eq!(s.object_load(obj, x as u32, 0), l << 24 | l << 16 | l << 8 | 0xff, "texel {x}"); + } +} + +/// A row whose data does not fill its last word leaves pad in that word, +/// and the next row starts on a fresh word. Replayed from guest traces of +/// the GL test suite's draws (box 40,206 to 53,201, the y range +/// decreasing, so rows run from y 205 down). The pad is 0x5a; the row +/// that follows does not start with it. +#[test] +fn a_row_that_ends_inside_a_word_drops_the_rest_of_the_word() { + let (mut s, _) = window(); + let tag = image::FMT_RGBA16; + + // RGB/UNSIGNED_SHORT (0x69): 13 pixels are 39 halves, so the 20th word + // of the row carries one pad half. + burst_image_groups( + &mut s, + (40, 206, 53, 204), + &[ + ( + 0x0f00_1069, + &[( + tag, + &[ + 0x1111_6c6c, 0xc7c7_4646, 0xa1a1_fcfc, 0x7b7b_d6d6, 0x3131_b0b0, 0x0b0b_6666, 0xe5e5_4040, 0x9b9b_1a1a, 0x7575_d0d0, 0x4f4f_aaaa, 0x0505_8484, + 0xdfdf_3a3a, + ], + )], + ), + ( + 0x0fa5_1069, + &[ + (tag, &[0xb9b9_1414, 0x6f6f_eeee, 0x4949_a4a4, 0x2323_7e7e, 0xd9d9_5858, 0xb3b3_0e0e, 0x8d8d_e8e8, 0x4343_5a5a]), + ( + tag, + &[ + 0x2e2e_8989, 0xe4e4_6a6a, 0xc5c5_2020, 0xa6a6_0101, 0x5c5c_e2e2, 0x3d3d_9898, 0x1e1e_7979, 0xd4d4_5a5a, 0xb5b5_1010, 0x9696_f1f1, 0x4c4c_d2d2, + 0x2d2d_8888, + ], + ), + ], + ), + ], + ); + assert_eq!(pixel(&s, 0, 40, 205), 0x116c_c7ff); + assert_eq!(pixel(&s, 0, 52, 205), 0x8de8_43ff); + assert_eq!(pixel(&s, 0, 40, 204), 0x2e89_e4ff); + assert_eq!(pixel(&s, 0, 41, 204), 0x6ac5_20ff); + + // RED/UNSIGNED_SHORT (0x64): 13 halves, one pad half. + burst_image_groups( + &mut s, + (40, 216, 53, 214), + &[ + (0x0f00_1064, &[(tag, &[0x1111_4646, 0x7b7b_b0b0, 0xe5e5_1a1a, 0x4f4f_8484])]), + ( + 0x0fa5_1064, + &[(tag, &[0xb9b9_eeee, 0x2323_5858, 0x8d8d_5a5a]), (tag, &[0x2e2e_6a6a, 0xa6a6_e2e2, 0x1e1e_5a5a, 0x9696_d2d2])], + ), + ], + ); + assert_eq!(pixel(&s, 0, 52, 215), 0x8d00_00ff); + assert_eq!(pixel(&s, 0, 40, 214), 0x2e00_00ff); + assert_eq!(pixel(&s, 0, 41, 214), 0x6a00_00ff); + + // RGB/UNSIGNED_BYTE (0x49), 93 pixels a row: 279 bytes, one pad byte, + // the row sent as five 12-word bursts and a 10-word one. + let full: &[(u32, &[u32])] = &[ + ( + image::FMT_RGB, + &[0x116c_c746, 0xa1fc_7bd6, 0x31b0_0b66, 0xe540_9b1a, 0x75d0_4faa, 0x0584_df3a, 0xb914_6fee, 0x49a4_237e, 0xd958_b30e, 0x8de8_43c2, 0x1d78_f752, 0xad2c_87e2], + ), + ( + image::FMT_RGB, + &[0x61bc_1796, 0xf14c_cb26, 0x8100_5bb6, 0x3590_eb6a, 0xc520_9ffa, 0x55d4_2f8a, 0x0964_bf3e, 0x99f4_73ce, 0x29a8_035e, 0xdd38_9312, 0x6dc8_47a2, 0xfd7c_d732], + ), + ( + image::FMT_RGB, + &[0xb10c_67e6, 0x419c_1b76, 0xd150_ab06, 0x85e0_3bba, 0x1570_ef4a, 0xa524_7fda, 0x59b4_0f8e, 0xe944_c31e, 0x79f8_53ae, 0x2d88_e362, 0xbd18_97f2, 0x4dcc_2782], + ), + ( + image::FMT_RGB, + &[0x015c_b736, 0x91ec_6bc6, 0x21a0_fb56, 0xd530_8b0a, 0x65c0_3f9a, 0xf574_cf2a, 0xa904_5fde, 0x3994_136e, 0xc948_a3fe, 0x7dd8_33b2, 0x0d68_e742, 0x9d1c_77d2], + ), + ( + image::FMT_RGB, + &[0x51ac_0786, 0xe13c_bb16, 0x71f0_4ba6, 0x2580_db5a, 0xb510_8fea, 0x45c4_1f7a, 0xf954_af2e, 0x89e4_63be, 0x1998_f34e, 0xcd28_8302, 0x5db8_3792, 0xed6c_c722], + ), + ]; + let tail: &[(u32, &[u32])] = &[ + (image::FMT_RGB, &[0xa1fc_57d6, 0x318c_0b66, 0xc140_9bf6, 0x75d0_2baa, 0x0560_df3a, 0x9514_6fca, 0x49a4_ff7e, 0xd934_b30e, 0x69e8_439e, 0x1d78_d35a]), + ( + image::FMT_RGB, + &[0x2e89_e46a, 0xc520_a601, 0x5ce2_3d98, 0x1e79_d45a, 0xb510_96f1, 0x4cd2_2d88, 0x0e69_c44a, 0xa500_86e1, 0x3cc2_1d78, 0xfe59_b43a, 0x95f0_76d1, 0x2cb2_0d68], + ), + ]; + burst_image_groups(&mut s, (40, 226, 133, 224), &[(0x0f00_1049, full), (0x0fd3_1049, tail)]); + assert_eq!(pixel(&s, 0, 40, 225), 0x116c_c7ff); + assert_eq!(pixel(&s, 0, 132, 225), 0x1d78_d3ff); + assert_eq!(pixel(&s, 0, 40, 224), 0x2e89_e4ff); + assert_eq!(pixel(&s, 0, 41, 224), 0x6ac5_20ff); +} diff --git a/src/dev/odyssey/tests/program.rs b/src/dev/odyssey/tests/program.rs new file mode 100644 index 000000000..2926d6e82 --- /dev/null +++ b/src/dev/odyssey/tests/program.rs @@ -0,0 +1,60 @@ +//! Recognising the geometry program the GL library sets. + +use crate::dev::odyssey::board::Board; +use crate::dev::odyssey::consts::{ucode, ODSY_ENTRY_29, ODSY_ENTRY_2A, ODSY_ENTRY_2C, ODSY_ENTRY_33}; +use crate::dev::odyssey::tests::board; + +/// Set the three entries and the list at index 0 the way the library's +/// program words do, and let the board recognise the program. +fn set_program(s: &mut Board, list: &[u32], k: u32, entries: [u32; 3]) { + s.geom.constants[..list.len()].copy_from_slice(list); + s.geom.entry[ODSY_ENTRY_29] = entries[0]; + s.geom.entry[ODSY_ENTRY_2A] = entries[1]; + s.geom.entry[ODSY_ENTRY_2C] = entries[2]; + s.geom.entry[ODSY_ENTRY_33] = k; + s.gl_program(); +} + +/// The older GL libraries number every routine from 891 on 99 lower, their +/// program image having no spot routine. A one-light record and an +/// assembled two-light list in that numbering light as their current forms +/// do, and the current numbering still wins where it means something. +/// +/// QEMU: command-fifo-gl-program-older-numbering. +#[test] +fn the_older_libraries_numbering_lights_their_programs() { + let (mut s, _) = board("v12"); + + // Record 3 of the current numbering is 0x5cb 0x5d8 0x5e5; the older + // image's entries are 99 lower. + set_program(&mut s, &[0x41e, 0x203, 0x467, 0x462], 0, [0x568, 0x575, 0x582]); + assert!(s.geom.lit, "an older-numbered resident lit record is recognised"); + + // An assembled list: the head 0x290 is below the spot routine, so it is + // the same in both numberings; the routines past it move up by 99. + let list = [0x290, 0x2b6, 0x2cf, 0x335, 0x2de, 0x300, 0x392, 0x437, 0x203, 0x25e, 0x462]; + set_program(&mut s, &list, 1, [0x290, 0x462, 0x462]); + assert!(s.geom.lit, "an older-numbered assembled list is recognised"); + assert!(!s.geom.local_viewer); + + // The current numbering of the same record is recognised. + set_program(&mut s, &[], 0, [0x5cb, 0x5d8, 0x5e5]); + assert!(s.geom.lit); +} + +/// The model cannot tell which library runs, so a program that means +/// something in the current numbering is never remapped, and an entry past +/// the older image's 2,266 records is no entry of it. +#[test] +fn the_older_numbering_is_tried_only_where_the_current_one_fails() { + let (mut s, _) = board("v12"); + + // 0x568 0x575 0x582 is not a current record, and 0x9f0 is past the + // older image's end, so nothing recognises these. + set_program(&mut s, &[], 0, [0x568, 0x575, 0x9f0]); + assert!(!s.geom.lit); + + // The unlit record's entries sit below the spot routine in both numberings. + set_program(&mut s, &[ucode::END], 0, [ucode::UNLIT, ucode::UNLIT_2A, ucode::UNLIT_2C]); + assert!(!s.geom.lit); +} diff --git a/src/dev/odyssey/tests/query.rs b/src/dev/odyssey/tests/query.rs new file mode 100644 index 000000000..0581dd355 --- /dev/null +++ b/src/dev/odyssey/tests/query.rs @@ -0,0 +1,152 @@ +//! The current-value and raster-position queries (words 0x4c0 and 0x4c3). +//! +//! The raster-position stream carries no colour, the library may send its +//! query after later glColor calls, so the board is modelled as latching +//! index, colour and texture coordinate when the last block of that stream +//! (0x3cfb) arrives (*inferred*), answering kind 3 from the latch and kind 0 +//! live (*established* as software behaviour). + +use crate::dev::odyssey::board::Board; +use crate::dev::odyssey::consts::{gl_cmd, query, ODSY_F32_ONE}; +use crate::dev::odyssey::tests::{board_with_ram, cmd, Ram}; +use std::sync::Arc; + +const SHADOW: u64 = 0x2000; +const SYNC: u64 = 0x1000; +const F_ONE: u32 = ODSY_F32_ONE; +const F_HALF: u32 = 0x3f00_0000; +const F_QUARTER: u32 = 0x3e80_0000; + +fn word(ram: &Ram, at: u64) -> u32 { + u32::from_be_bytes(ram.get(at, 4).try_into().unwrap()) +} + +fn colour(s: &mut Board, r: u32, g: u32, b: u32) { + for w in [gl_cmd::COLOR_3F, r, g, b] { + cmd(s, w); + } +} + +fn index(s: &mut Board, i: u32) { + cmd(s, gl_cmd::COLOR_INDEX); + cmd(s, i); +} + +/// The tail of glRasterPos's stream: four class-0 blocks of zero operands. +fn raster_pos_stream(s: &mut Board) { + for block in 0x3cf8..=0x3cfb { + cmd(s, block); + for _ in 0..16 { + cmd(s, 0); + } + } +} + +/// Send query word `0x4c0 + kind` with its count, fence and sequence. +fn query(s: &mut Board, kind: u32) { + for w in [0x4c0 + kind, 7, 0x10a5a, 3, 0x5a5a] { + cmd(s, w); + } +} + +fn query_board() -> (Box, Arc) { + let (mut s, _, ram) = board_with_ram(); + s.widget.ctx_shadow_addr_lower = SHADOW as u32; + s.widget.host_write_addr_lower = SYNC as u32; + (s, ram) +} + +fn raster_colour(ram: &Ram) -> [u32; 4] { + let at = SHADOW + query::RASTER_COLOUR as u64; + [0, 4, 8, 12].map(|o| word(ram, at + o)) +} + +/// Colour A, glRasterPos, colour B, then the deferred query: the library +/// reads A, the colour from glRasterPos time. Reading the colour current +/// at query time returns B (what the glstate RASTER_COLOR case saw). +#[test] +fn the_raster_query_returns_the_colour_latched_at_raster_position_time() { + let (mut s, ram) = query_board(); + + colour(&mut s, F_HALF, F_QUARTER, F_ONE); + index(&mut s, 9); + raster_pos_stream(&mut s); + colour(&mut s, 0, 0, 0); + index(&mut s, 4); + query(&mut s, query::KIND_RASTER); + + assert_eq!(raster_colour(&ram), [gl_cmd::COLOR_3F, F_HALF, F_QUARTER, F_ONE]); + let at = SHADOW + query::RASTER_INDEX as u64; + assert_eq!([word(&ram, at), word(&ram, at + 4)], [gl_cmd::COLOR_INDEX, 9]); + assert_eq!(ram.half(SYNC + 6), 0x5a5a, "the query still completes"); +} + +/// A colour change after the latch does not move the latch, and a second +/// raster position takes the new colour. +#[test] +fn a_later_raster_position_replaces_the_latch() { + let (mut s, ram) = query_board(); + + colour(&mut s, F_HALF, F_HALF, F_HALF); + raster_pos_stream(&mut s); + colour(&mut s, F_ONE, 0, 0); + raster_pos_stream(&mut s); + colour(&mut s, 0, 0, F_ONE); + query(&mut s, query::KIND_RASTER); + + assert_eq!(raster_colour(&ram), [gl_cmd::COLOR_3F, F_ONE, 0, 0]); +} + +/// The latch is taken at the last block, not the first: a colour sent +/// before 0x3cfb but after 0x3cf8 would be in it, one sent after is not. +/// Only the 0x3cfb block latches. +#[test] +fn only_the_last_position_block_latches() { + let (mut s, ram) = query_board(); + + colour(&mut s, F_HALF, F_HALF, F_HALF); + raster_pos_stream(&mut s); + colour(&mut s, F_ONE, 0, 0); + cmd(&mut s, 0x3cf8); + for _ in 0..16 { + cmd(&mut s, 0); + } + query(&mut s, query::KIND_RASTER); + + assert_eq!(raster_colour(&ram), [gl_cmd::COLOR_3F, F_HALF, F_HALF, F_HALF]); +} + +/// Before any glRasterPos the raster records hold the GL initial values: +/// colour (1, 1, 1, 1) and index 1, whatever the current colour is. +#[test] +fn the_raster_query_before_any_raster_position_returns_the_initial_values() { + let (mut s, ram) = query_board(); + + colour(&mut s, F_HALF, F_QUARTER, 0); + index(&mut s, 7); + query(&mut s, query::KIND_RASTER); + + assert_eq!(raster_colour(&ram), [gl_cmd::COLOR_3F, F_ONE, F_ONE, F_ONE]); + let at = SHADOW + query::RASTER_INDEX as u64; + assert_eq!([word(&ram, at), word(&ram, at + 4)], [gl_cmd::COLOR_INDEX, 1]); +} + +/// The current-value query (kind 0) stays live: it reports the colour and +/// index current at the query, not the raster latch. +#[test] +fn the_current_value_query_stays_live() { + let (mut s, ram) = query_board(); + + colour(&mut s, F_HALF, F_QUARTER, F_ONE); + index(&mut s, 9); + raster_pos_stream(&mut s); + colour(&mut s, F_ONE, F_ONE, 0); + index(&mut s, 4); + query(&mut s, 0); + + let live = SHADOW + 0x914; + assert_eq!([0, 4, 8, 12].map(|o| word(&ram, live + o)), [gl_cmd::COLOR_3F, F_ONE, F_ONE, 0]); + let idx = SHADOW + 0x900; + assert_eq!([word(&ram, idx), word(&ram, idx + 4)], [gl_cmd::COLOR_INDEX, 4]); + assert_eq!(word(&ram, SHADOW + query::RASTER_COLOUR as u64), 0, "kind 0 writes no raster record"); +} diff --git a/src/dev/odyssey/tests/raster.rs b/src/dev/odyssey/tests/raster.rs new file mode 100644 index 000000000..3b059b89e --- /dev/null +++ b/src/dev/odyssey/tests/raster.rs @@ -0,0 +1,1385 @@ +//! The raster unit's queue: GL work runs on its own thread, and the +//! orderings the guest can see hold across both queues. + +use crate::dev::odyssey::Odyssey; +use crate::dev::odyssey::board::{Board, ODSY_HEIGHT, ODSY_WIDTH}; +use crate::dev::odyssey::consts::{ + colour, cursor, dispmode, dma, gl_cmd, light, lut, write, ODSY_BUZZ_1, ODSY_DID_MAIN_SHIFT, + ODSY_DRAW_MASK_BUFFER0, ODSY_DRAW_MASK_BUFFER1, ODSY_GAMMA_BASE, ODSY_GAMMA_GROUP, + ODSY_ID_COMPARE_ENABLE, ODSY_ID_COMPARE_MASK_SHIFT, ODSY_SW_TOKEN, +}; +use crate::dev::odyssey::display::Display; +use crate::dev::odyssey::gl::{PyramidShape, Texture, TextureChain, TextureVolume}; +use crate::dev::odyssey::gl_context::{ + vtx, GL_COLOR_BUFFER_BIT, GL_COLOUR_MASK_ALL, GL_DRAW_OK, GL_FLAT, GL_LINES, GL_QUADS, + GL_TRIANGLES, GlDraw, GlDrawState, GlDrawable, RASTER_CLEAR, RasterOp, +}; +use crate::dev::odyssey::memory::ClipRect; +use crate::dev::odyssey::mmio::BoardLocks; +use crate::dev::odyssey::raster::{GlTexLevel, LineRule}; +use crate::dev::odyssey::raster_unit::{ + primitive_cost, rect_cost, RASTER_CLEAR_DIVISOR, RASTER_JOB_OVERHEAD, RASTER_WORK_BUDGET, + ROW_WORDS, RasterCtx, +}; +use crate::dev::odyssey::reader::Differ; +use crate::dev::odyssey::scanout::{DirtySite, RowMarks}; +use crate::dev::odyssey::tests::{board, cmd, pixel, Ram}; +use parking_lot::Mutex; +use std::sync::Arc; +use super::*; +use crate::dev::odyssey::raster_bench::Bench; +use std::sync::atomic::Ordering::SeqCst; +use std::sync::atomic::{AtomicBool, AtomicU64}; +use std::time::{Duration, Instant}; + +const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0]; + +/// A v12 board whose every pixel belongs to the one window, and the GL +/// draw state the fill-rate benchmark draws with: flat, no depth test. +fn gl_board() -> (Box, GlDrawState) { + let mut b = Bench::empty(); + b.board.gl_write_mask |= write::COLOUR_MASK << write::COLOUR_SHIFT; + b.st.shade_model = GL_FLAT; + (b.board, b.st) +} + +/// Vertices at window pixels (x, y), in the colour given. +fn vertices(points: &[(f32, f32)], colour: [f32; 4]) -> Vec { + let mut v = vec![0.0f32; points.len() * vtx::FLOATS]; + for (i, &(x, y)) in points.iter().enumerate() { + let o = &mut v[i * vtx::FLOATS..][..vtx::FLOATS]; + // The normalised coordinate that the viewport maps to the window + // position p exactly (a library sends positions it has already put on + // the board's grid): p / size does not round-trip in single precision, + // so the nearest coordinate is stepped until the mapping reaches p. + let ndc = |p: f32, size: u32| { + let map = |n: f32| (n + 1.0) * 0.5 * size as f32; + let mut n = p / size as f32 * 2.0 - 1.0; + for _ in 0..64 { + if map(n) >= p { break; } + n = f32::from_bits(if n >= 0.0 { n.to_bits() + 1 } else { n.to_bits() - 1 }); + } + for _ in 0..64 { + let down = f32::from_bits(if n > 0.0 { n.to_bits() - 1 } else { n.to_bits() + 1 }); + if map(down) < p { break; } + n = down; + } + n + }; + o[vtx::POSITION..][..4].copy_from_slice(&[ndc(x, ODSY_WIDTH), ndc(y, ODSY_HEIGHT), 0.0, 1.0]); + o[vtx::COLOR..][..4].copy_from_slice(&colour); + } + v +} + +/// One red triangle over the left half of the screen, through the board's +/// own GL draw: set up by the front end, drawn by the raster unit. +fn red_triangle(s: &mut Board, st: &GlDrawState) { + let v = vertices(&[(0.0, 0.0), (ODSY_WIDTH as f32, 0.0), (0.0, ODSY_HEIGHT as f32)], RED); + let stipple = [0u32; 32]; + let mut d = GlDraw::new(GL_TRIANGLES, 3, &v, st, &stipple); + assert_eq!(s.gl_draw(&mut d), GL_DRAW_OK); +} + +/// A board with its raster unit on a thread of its own, whose next slice +/// holds the raster unit for `hold_ms` before it draws, as a long draw +/// would; and the draw state. +fn slow_raster(hold_ms: u32) -> (BoardLocks, GlDrawState) { + let (s, st) = gl_board(); + let h = BoardLocks::new(s); + h.raster.start(); + h.raster.test_slice_sleep_ms.store(hold_ms, SeqCst); + (h, st) +} + +/// Hold the raster unit with a job that runs until the returned flag is set +/// (or 20 s pass, so a failing test ends instead of hanging), as a long draw +/// would. The test, not a clock, decides how long the unit is busy. +fn hold_raster(h: &BoardLocks) -> Arc { + let release = Arc::new(AtomicBool::new(false)); + let flag = release.clone(); + h.raster.submit(move |_| { + let deadline = Instant::now() + Duration::from_secs(20); + while !flag.load(SeqCst) && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(1)); + } + }); + release +} + +/// Wait for `what`, up to 20 s. +fn eventually(what: &str, mut done: impl FnMut() -> bool) { + let deadline = Instant::now() + Duration::from_secs(20); + while !done() { + assert!(Instant::now() < deadline, "{what}"); + std::thread::sleep(Duration::from_millis(1)); + } +} + +/// A GL draw returns to the front end before it is drawn; a register read +/// that waits for the board's work, issued while the draw is still queued +/// for the raster unit, answers only once it is in the planes. A read that +/// does not wait answers at once. +#[test] +fn a_waiting_read_behind_a_queued_draw_sees_it_drawn() { + let (h, st) = slow_raster(300); + + red_triangle(&mut h.test_board(), &st); + assert_eq!(h.raster.pending(), 1, "the draw waits for the raster unit"); + h.write(ODSY_SW_TOKEN, 7, 4); + assert_eq!(h.read(ODSY_SW_TOKEN, 4), 7); + assert_eq!(h.raster.pending(), 1, "the token does not wait for the drawing"); + + h.read(ODSY_BUZZ_1, 4); + assert_eq!(h.raster.pending(), 0); + assert_ne!(pixel(h.test_board(), 0, 10, 10), 0, "drawn before the read answered"); + h.raster.stop(); +} + +/// The memory word at `0x106c` holds straps and FIFO levels, none of them +/// a drawing result, so a read of it answers while a draw is still queued +/// for the raster unit. It still runs the command stores before it +/// (`a_read_of_a_register_the_work_may_change_finishes_the_work_first`). +#[test] +fn the_memory_word_answers_while_a_draw_is_queued() { + let (h, st) = slow_raster(300); + + red_triangle(&mut h.test_board(), &st); + assert_eq!(h.raster.pending(), 1, "the draw waits for the raster unit"); + assert_eq!(h.read(MEMORY_CONFIG, 4) & 0xff00_0000, 0xa100_0000); + assert_eq!(h.raster.pending(), 1, "the memory word does not wait for the drawing"); + eventually("the draw did not finish", || h.raster.pending() == 0); + h.raster.stop(); +} + +/// A fence the front end runs behind a draw is written by the raster unit +/// after the draw: its token never shows while the draw is still queued. +/// +/// The raster unit is held by a job that waits for the test, not for a +/// time, and the work budget is lifted. The half-screen triangle costs more +/// than the budget, so with the default one the front end's fence waits +/// for room until the raster unit has drawn it, and the token is then +/// written in the moment the test looks. +#[test] +fn a_fence_behind_a_queued_draw_lands_after_the_draw() { + let (h, st) = slow_raster(0); + h.raster.set_budget(u64::MAX); + let hold = Arc::new(AtomicBool::new(true)); + let held = hold.clone(); + h.raster.submit(move |_| { + while held.load(SeqCst) { + std::thread::sleep(Duration::from_millis(1)); + } + }); + let ram = Ram::new(1 << 20); + { + let mut s = h.test_board(); + s.guest = Some(ram.clone()); + s.widget.host_write_addr_lower = 0x1000; + red_triangle(&mut s, &st); + s.fence(0x5a5a << dma::FENCE_TOKEN_SHIFT | 3); + } + assert_eq!(h.raster.pending(), 3, "the hold, the draw and the fence are all queued"); + assert_eq!(ram.half(0x1006), 0, "the fence waits for the draw before it"); + hold.store(false, SeqCst); + eventually("the fence did not land", || ram.half(0x1006) == 0x5a5a); + assert_ne!(pixel(h.test_board(), 0, 10, 10), 0, "the draw was in the planes before the fence"); + h.raster.stop(); +} + +/// The rows the raster unit has published, as the display would take them, +/// found without waiting for a slice in flight. +fn published_rows(h: &BoardLocks) -> Vec { + let mut scanout = fresh_scanout(); + h.raster.take_published_rows(0, &mut scanout.differ, &scanout.marks); + scanout.marks.dirty_rows().into_iter().map(|y| y as u32).collect() +} + +/// A job that holds the raster unit for `ms`, as a long draw would. +fn busy(h: &BoardLocks, ms: u64) { + h.raster.submit(move |_| std::thread::sleep(Duration::from_millis(ms))); +} + +/// The rows a draw wrote are published before the fence behind it writes +/// its token: a guest that sees the token and arms a swap has all of its +/// rows known to the display. A job queued behind the fence keeps the +/// queue from draining and the slice from ending meanwhile. +#[test] +fn a_fence_publishes_the_rows_drawn_before_it() { + // The first slice waits while everything is queued, and then runs it all. + let (h, st) = slow_raster(50); + h.raster.test_slice_limit_ms.store(10_000, SeqCst); + let ram = Ram::new(1 << 20); + { + let mut s = h.test_board(); + s.guest = Some(ram.clone()); + s.widget.host_write_addr_lower = 0x1000; + small_triangle(&mut s, &st, 100.0, RED); + s.fence(0x5a5a << dma::FENCE_TOKEN_SHIFT | 3); + busy(&h, 300); + } + eventually("the fence did not land", || ram.half(0x1006) == 0x5a5a); + assert!(published_rows(&h).contains(&110), "the token was visible before the draw's rows were"); + h.raster.stop(); +} + +/// A query's answer is a completion too. +#[test] +fn a_query_publishes_the_rows_drawn_before_it() { + let (h, st) = slow_raster(50); + h.raster.test_slice_limit_ms.store(10_000, SeqCst); + let ram = Ram::new(1 << 20); + { + let mut s = h.test_board(); + s.guest = Some(ram.clone()); + s.widget.ctx_shadow_addr_lower = 0x2000; + s.widget.host_write_addr_lower = 0x1000; + small_triangle(&mut s, &st, 100.0, RED); + for word in [0x4c0, 7, 0x10a5a, 3, 0x5a5a] { + cmd(&mut s, word); + } + busy(&h, 300); + } + eventually("the query did not sync", || ram.half(0x1006) == 0x5a5a); + assert!(published_rows(&h).contains(&110), "the sync word was visible before the draw's rows were published"); + h.raster.stop(); +} + +/// The queue publishes when it drains, before a caller that waited for it +/// looks at the planes. +#[test] +fn a_drained_queue_has_published_its_rows() { + let (h, st) = slow_raster(50); + h.raster.test_slice_limit_ms.store(10_000, SeqCst); + h.raster.test_slice_end_ms.store(400, SeqCst); + small_triangle(&mut h.test_board(), &st, 100.0, RED); + h.raster.wait_idle(); + assert!(published_rows(&h).contains(&110), "the queue was idle but its rows were not out"); + h.raster.test_slice_end_ms.store(0, SeqCst); + h.raster.stop(); +} + +/// A slice that ends with work still queued publishes what it drew, so the +/// display never goes a long job without the rows of the short ones. +#[test] +fn a_slice_end_publishes_the_rows_drawn_in_it() { + let (h, st) = slow_raster(50); + { + let mut s = h.test_board(); + small_triangle(&mut s, &st, 100.0, RED); + // Longer than a slice, so the slice ends after it. + busy(&h, 5); + } + let release = hold_raster(&h); + eventually("the first jobs did not finish", || h.raster.pending() == 1); + let mut rows = Vec::new(); + eventually("the slice ended without its rows", || { + rows.extend(published_rows(&h)); + rows.contains(&110) + }); + assert_eq!(h.raster.pending(), 1, "the long job is still running"); + release.store(true, SeqCst); + h.raster.stop(); +} + +/// A scanout with the whole frame resolved. +struct FreshRows { + differ: Box, + marks: RowMarks, +} + +fn fresh_scanout() -> FreshRows { + let marks = RowMarks::default(); + marks.set_height(ODSY_HEIGHT); + FreshRows { differ: Box::new([[0; ROW_WORDS]; dispmode::COUNT as usize]), marks } +} + +/// Stopping the machine finishes the raster unit's queued work; a reset +/// waits for it before it clears the planes, so nothing queued before the +/// reset lands after it. +#[test] +fn stop_and_reset_finish_the_raster_queue_first() { + let (h, st) = slow_raster(200); + red_triangle(&mut h.test_board(), &st); + h.work_stop(); + assert!(!h.raster.threaded()); + assert_eq!(h.raster.pending(), 0); + assert_ne!(pixel(&h.test_board(), 0, 10, 10), 0); + + h.raster.start(); + h.raster.test_slice_sleep_ms.store(200, SeqCst); + red_triangle(h.test_board(), &st); + h.test_board().reset(); + assert_eq!(h.raster.pending(), 0); + assert_eq!(pixel(h.test_board(), 0, 10, 10), 0, "the reset cleared the draw queued before it"); + h.raster.stop(); +} + +/// A stippled line drawn in two pieces continues its stipple across them +/// whether the raster unit runs on its own thread or inside the draw: the +/// raster unit keeps the phase, and the context carries a marker for it. +#[test] +fn the_stipple_phase_carries_across_draws_on_the_raster_thread() { + fn two_pieces(h: &BoardLocks, st: &GlDrawState) -> Vec { + let stipple = [0u32; 32]; + let mut phase = Some(0); + for (a, b) in [((10.5f32, 20.5f32), (17.5f32, 20.5f32)), ((17.5, 20.5), (60.5, 20.5))] { + let v = vertices(&[a, b], RED); + let mut d = GlDraw::new(GL_LINES, 2, &v, st, &stipple); + d.line_stipple_enabled = true; + d.line_stipple_repeat = 1; + d.line_stipple_pattern = 0x0f0f; + d.line_phase = phase; + assert_eq!(h.test_board().gl_draw(&mut d), GL_DRAW_OK); + phase = d.line_phase; + } + let s = h.test_board(); + s.raster_wait(); + (0..64).map(|x| pixel(&s, 0, x, 20)).collect() + } + + let (s, st) = gl_board(); + let inline = two_pieces(&BoardLocks::new(s), &st); + let (h, st) = slow_raster(0); + let threaded = two_pieces(&h, &st); + h.raster.stop(); + assert!(inline.iter().any(|&p| p != 0) && inline.iter().any(|&p| p == 0), "a stippled line"); + assert_eq!(threaded, inline); +} + +/// The raster unit gives a drawn GL job back and the next draw fills it +/// again; the refilled job draws only the new primitive, with its own +/// vertex count, whether it ran on the raster thread or inside its submit. +#[test] +fn a_reused_draw_job_draws_only_its_new_primitive() { + const BLUE: [f32; 4] = [0.0, 0.0, 1.0, 1.0]; + fn draws(h: &BoardLocks, st: &GlDrawState) -> Vec { + let stipple = [0u32; 32]; + let (w, ht) = (ODSY_WIDTH as f32, ODSY_HEIGHT as f32); + // A six-vertex pair of triangles, then once that is drawn a + // three-vertex one that covers less. + let quad = vertices(&[(0.0, 0.0), (64.0, 0.0), (0.0, 64.0), (64.0, 0.0), (64.0, 64.0), (0.0, 64.0)], RED); + let mut d = GlDraw::new(GL_TRIANGLES, 6, &quad, st, &stipple); + assert_eq!(h.test_board().gl_draw(&mut d), GL_DRAW_OK); + h.test_board().raster_wait(); + let tri = vertices(&[(w - 64.0, ht - 64.0), (w, ht - 64.0), (w - 64.0, ht)], BLUE); + let mut d = GlDraw::new(GL_TRIANGLES, 3, &tri, st, &stipple); + assert_eq!(h.test_board().gl_draw(&mut d), GL_DRAW_OK); + let s = h.test_board(); + s.raster_wait(); + let mut px = Vec::new(); + for y in (0..ODSY_HEIGHT as i32).step_by(8) { + for x in (0..ODSY_WIDTH as i32).step_by(8) { + px.push(pixel(&s, 0, x, y)); + } + } + px + } + + let (s, st) = gl_board(); + let inline = draws(&BoardLocks::new(s), &st); + let (h, st) = slow_raster(0); + let threaded = draws(&h, &st); + eventually("the second job did not come back", || h.raster.spare_draws() == 1); + h.raster.stop(); + let colours: std::collections::BTreeSet = inline.iter().copied().filter(|&p| p != 0).collect(); + assert_eq!(colours.len(), 2, "both primitives drew, each in its own colour"); + assert_eq!(threaded, inline); +} + +/// A current-state query behind a queued draw (a small one: a draw of a +/// screenful makes the front end wait for room): the front end goes on at +/// once, and the query's sync word, like a fence, lands only after the +/// draw is in the planes, its snapshot already in place. +#[test] +fn a_query_behind_a_queued_draw_syncs_after_the_draw_without_holding_the_front_end() { + let (h, st) = slow_raster(0); + let release = hold_raster(&h); + let ram = Ram::new(1 << 20); + { + let mut s = h.test_board(); + s.guest = Some(ram.clone()); + s.widget.ctx_shadow_addr_lower = 0x2000; + s.widget.host_write_addr_lower = 0x1000; + tiny_triangle(&mut s, &st); + for word in [0x4c0, 7, 0x10a5a, 3, 0x5a5a] { + cmd(&mut s, word); + } + } + // The raster unit has not run a job since the hold began, so the front + // end went on without it. + assert!(h.raster.pending() >= 3, "the hold, the draw and the query wait for the raster unit"); + assert_eq!(ram.half(0x1006), 0, "the sync word waits for the draw before it"); + release.store(true, SeqCst); + eventually("the query did not sync", || ram.half(0x1006) == 0x5a5a); + assert_eq!(ram.get(0x2000 + 0x97c, 4), 7u32.to_be_bytes(), "the counter was published before the sync word"); + assert_ne!(pixel(h.test_board(), 0, 10, 10), 0, "the draw was in the planes before the sync word"); + h.raster.stop(); +} + +/// On the bus, a waiting read behind a queued draw answers busy until the +/// raster unit has drawn it, without the processor waiting inside the +/// read; the memory word answers at once. +#[test] +fn a_waiting_bus_read_is_busy_until_the_raster_unit_has_drawn() { + let (h, st) = slow_raster(300); + h.work_start(); + + red_triangle(&mut h.test_board(), &st); + assert_eq!(h.bus_read(ODSY_BUZZ_1, 4), None, "busy while the draw is queued"); + assert_eq!(h.raster.pending(), 1, "and the read did not wait for it"); + assert_eq!(h.bus_read(MEMORY_CONFIG, 4).map(|v| v & 0xff00_0000), Some(0xa100_0000)); + eventually("the read stayed busy", || h.bus_read(ODSY_BUZZ_1, 4).is_some()); + assert_ne!(pixel(&h.test_board(), 0, 10, 10), 0, "drawn before the read answered"); + h.work_stop(); +} + +/// The rows the display would resolve after the draws so far, for a board +/// whose windows show the buffers `displayed` names. +fn drawn_rows(h: &BoardLocks, displayed: u32) -> Vec { + let mut scanout = fresh_scanout(); + h.raster.take_published_rows(displayed, &mut scanout.differ, &scanout.marks); + scanout.marks.dirty_rows().into_iter().map(|y| y as u32).collect() +} + +/// Which rows of colour buffer b differ between two snapshots. +fn changed_rows(s: &Board, b: u32, before: &[u32]) -> Vec { + let w = 64; + (0..ODSY_HEIGHT).filter(|&y| (0..w).any(|x| pixel(s, b, x, y as i32) != before[(y * w as u32 + x as u32) as usize])).collect() +} + +/// The pixels of the first 64 columns of colour buffer b. +fn snapshot(s: &Board, b: u32) -> Vec { + (0..ODSY_HEIGHT as i32).flat_map(|y| (0..64).map(move |x| (y, x))).map(|(y, x)| pixel(s, b, x, y)).collect() +} + +/// A small triangle at window row `y`, in the first 64 columns. +fn small_triangle(s: &mut Board, st: &GlDrawState, y: f32, colour: [f32; 4]) { + let v = vertices(&[(2.0, y), (60.0, y + 3.0), (4.0, y + 40.0)], colour); + let stipple = [0u32; 32]; + let mut d = GlDraw::new(GL_TRIANGLES, 3, &v, st, &stipple); + assert_eq!(s.gl_draw(&mut d), GL_DRAW_OK); +} + +/// What the display resolves again covers every row of the colour buffer +/// the windows show that a draw changed, and a draw into a buffer no window +/// shows asks for none: its window shows it only after a swap, which +/// repaints the frame. +#[test] +fn a_draw_asks_the_display_for_the_rows_it_changed_in_a_buffer_a_window_shows() { + for (target, displayed, shown) in [(0, 0, true), (1, 0, false), (1, 0xffff_ffff, true), (0, 0xffff_ffff, false), (1, 1, true)] { + let (s, mut st) = gl_board(); + let h = BoardLocks::new(s); + st.drawable.front = target; + st.drawable.back = target; + let mut s = h.test_board(); + let before = snapshot(&s, target); + + small_triangle(&mut s, &st, 100.0, RED); + small_triangle(&mut s, &st, 400.0, RED); + let changed = changed_rows(&s, target, &before); + let rows = drawn_rows(&h, displayed); + + assert!(changed.len() > 40, "the triangles drew"); + if shown { + assert!(changed.iter().all(|y| rows.contains(y)), "target {target} displayed {displayed:#x}: rows {changed:?} changed, {rows:?} asked for"); + assert!(rows.len() < 200, "and not the whole frame: {}", rows.len()); + } else { + assert!(rows.is_empty(), "target {target} displayed {displayed:#x}: no window shows it, yet {rows:?}"); + } + assert!(drawn_rows(&h, displayed).is_empty(), "the rows are handed over once"); + } +} + +/// A draw under a window-id test reaches only the windows that test names: +/// into the buffer the window it names does not show, it asks for nothing, +/// even while other windows show that buffer. +#[test] +fn a_draw_under_an_id_test_asks_only_for_the_window_it_reaches() { + let (mut s, mut st) = gl_board(); + s.planes.did.fill(3 << ODSY_DID_MAIN_SHIFT); + s.win.id_compare = ODSY_ID_COMPARE_ENABLE | 0xf8 << ODSY_ID_COMPARE_MASK_SHIFT | 3 << ODSY_DID_MAIN_SHIFT; + st.drawable.front = 1; + st.drawable.back = 1; + let h = BoardLocks::new(s); + let mut s = h.test_board(); + + small_triangle(&mut s, &st, 100.0, RED); + assert_ne!(pixel(&s, 1, 10, 110), 0, "the draw passed the id test"); + assert!(drawn_rows(&h, !(1 << 3)).is_empty(), "window 3 shows buffer 0 and the others show buffer 1"); + small_triangle(&mut s, &st, 100.0, RED); + assert!(drawn_rows(&h, 1 << 3).contains(&110), "window 3 shows buffer 1"); +} + +/// A clear's rows reach the display the same way. +#[test] +fn a_clear_asks_the_display_for_its_rows() { + let (s, _) = gl_board(); + let h = BoardLocks::new(s); + let s = h.test_board(); + let mut op = RasterOp { + kind: RASTER_CLEAR, + ntargets: 1, + x0: 0, + y0: 200, + x1: 63, + y1: 299, + clear_buffers: GL_COLOR_BUFFER_BIT, + clear_rgba: [0.2, 0.3, 0.4, 1.0], + clear_colour_mask: GL_COLOUR_MASK_ALL, + ..Default::default() + }; + op.clip.n = 1; + op.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: 1279, y1: 1023 }; + + s.gl_raster(&op); + h.raster.wait_idle(); + let rows = drawn_rows(&h, 0); + assert_eq!(rows, (200..300).collect::>()); +} + +/// The pixels a one-pixel line lights under line rule `rule`, in the +/// rectangle of the board's first 80 columns and 64 rows, as (x, y) pairs. +fn line_pixels(rule: LineRule, a: (f32, f32), b: (f32, f32)) -> Vec<(i32, i32)> { + let mut bench = Bench::empty(); + bench.board.gl_write_mask |= write::COLOUR_MASK << write::COLOUR_SHIFT; + bench.st.shade_model = GL_FLAT; + bench.rules.line_rule = rule; + bench.mode = GL_LINES; + bench.prims = vec![vertices(&[a, b], RED)]; + bench.run(); + (0..64).flat_map(|y| (0..80).map(move |x| (x, y))).filter(|&(x, y)| pixel(&bench.board, 0, x, y) != 0).collect() +} + +/// A one-pixel line takes the centre-sampled rule: a pixel per major-axis +/// column whose centre lies between the ends, the end not included. A line +/// that ends past a column centre but inside that pixel's diamond lights the +/// column, where the diamond-exit rule leaves it; a crossing exactly on a +/// pixel edge goes to the lower pixel whichever way the line slopes, where +/// the diamond rule's perturbation follows the slope. Reversed, the end +/// inside a diamond is the start, and both rules agree. A vertical line +/// exactly on a column edge takes the column to its left under both. +#[test] +fn a_line_takes_the_centre_sampled_rule() { + let cases: [((f32, f32), (f32, f32), bool, Vec<(i32, i32)>); 4] = [ + ((1.0, 3.5), (5.75, 3.5), true, vec![(1, 3), (2, 3), (3, 3), (4, 3), (5, 3)]), + ((5.75, 3.5), (1.0, 3.5), false, vec![(1, 3), (2, 3), (3, 3), (4, 3), (5, 3)]), + ((1.5, 2.0), (5.5, 4.0), true, vec![(1, 1), (2, 2), (3, 2), (4, 3)]), + ((4.0, 1.0), (4.0, 5.0), false, vec![(3, 1), (3, 2), (3, 3), (3, 4)]), + ]; + + for (a, b, diamond_differs, want) in cases { + let centre = line_pixels(LineRule::CentreSampled, a, b); + let diamond = line_pixels(LineRule::Diamond, a, b); + + assert_eq!(centre, want, "centre-sampled {a:?} to {b:?}"); + assert_eq!(diamond != want, diamond_differs, "diamond-exit {a:?} to {b:?}: {diamond:?}"); + } +} + +/// Centre-sampled lines at the board's line-test geometry: a horizontal +/// line from x 60.0 to 70.75 lights columns 60 to 70, eleven pixels, the one +/// past the last column centre included; a line rising at slope one half +/// crosses a row edge exactly at every other column centre, and each of those +/// columns takes the lower row, sharing it with the column after. +#[test] +fn centre_sampled_lines_light_the_pixels_the_rule_names() { + let horizontal = line_pixels(LineRule::CentreSampled, (60.0, 10.5), (70.75, 10.5)); + assert_eq!(horizontal, (60..=70).map(|x| (x, 10)).collect::>()); + + let rising = line_pixels(LineRule::CentreSampled, (60.0, 9.75), (68.0, 13.75)); + assert_eq!(rising, [(60, 9), (61, 10), (62, 10), (63, 11), (64, 11), (65, 12), (66, 12), (67, 13)]); +} + +/// Arm a swap of every window and let the retrace latch it. +fn swap_every_window(s: &mut Board) { + s.front.lock().swap_arm = u32::MAX; + s.retrace(); +} + +/// The rows of the frame a display update resolved, found by the marker +/// the test left in every row first. +fn resolved_rows(s: &mut Board, h: &BoardLocks) -> Vec { + const MARK: u32 = 0xdead_0000; + let (w, ht) = (s.reader.frame_width as usize, s.reader.frame_height as usize); + + s.reader.frame.fill(MARK); + let displayed = s.front.lock().displayed_buffer; + s.test_take_rows(h, displayed); + s.scanout_update(); + (0..ht as u32).filter(|&y| s.reader.frame[y as usize * w] != MARK).collect() +} + +/// A swap resolves the rows where the window's two buffers differ and no +/// others, however the buffers came to differ: a draw into the buffer +/// the window does not show asks for nothing until the swap, then the +/// swap resolves those rows, and the swap back resolves them again. +#[test] +fn a_swap_resolves_the_rows_drawn_into_the_other_buffer_and_no_others() { + let (mut s, mut st) = gl_board(); + s.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + let h = BoardLocks::new(s); + let mut s = h.test_board(); + st.drawable.front = 1; + st.drawable.back = 1; + s.scanout_update(); + // A swap and its swap back measure the buffers; from then on the draws' + // own rows are all the display knows. + swap_every_window(&mut s); + s.scanout_update(); + swap_every_window(&mut s); + s.scanout_update(); + assert!(!s.scanout.marks.unnamed_pending()); + + let before = snapshot(&s, 1); + small_triangle(&mut s, &st, 100.0, RED); + small_triangle(&mut s, &st, 400.0, RED); + let changed = changed_rows(&s, 1, &before); + assert!(changed.len() > 40, "the triangles drew"); + + assert!(resolved_rows(&mut s, &h).is_empty(), "the window shows buffer 0, which they did not change"); + swap_every_window(&mut s); + assert_eq!(resolved_rows(&mut s, &h), changed, "the swap shows buffer 1"); + assert!(resolved_rows(&mut s, &h).is_empty(), "and nothing is left to resolve"); + swap_every_window(&mut s); + assert_eq!(resolved_rows(&mut s, &h), changed, "the swap back shows buffer 0 in those rows"); +} + +/// A writer that does not name its rows makes the next swap measure the +/// buffers: it resolves the rows they differ in, not the whole frame. +#[test] +fn a_swap_after_an_unnamed_write_resolves_only_the_rows_the_buffers_differ_in() { + let (mut s, _) = gl_board(); + s.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + let h = BoardLocks::new(s); + let mut s = h.test_board(); + s.scanout_update(); + + for y in 50..60 { + s.buffer_store(1, 10, y, 0x1234); + } + s.scanout.invalidate(50, 10); + assert_eq!(resolved_rows(&mut s, &h), (50..60).collect::>()); + swap_every_window(&mut s); + assert_eq!(resolved_rows(&mut s, &h), (50..60).collect::>()); +} + +/// A front-end pixel write names only its display line: the next swap +/// measures that line and takes the rest from the draws' own rows. +#[test] +fn a_swap_measures_only_the_lines_an_unnamed_write_marked() { + let (mut s, mut st) = gl_board(); + s.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + let h = BoardLocks::new(s); + let mut s = h.test_board(); + st.drawable.front = 1; + st.drawable.back = 1; + swap_every_window(&mut s); + s.scanout_update(); + swap_every_window(&mut s); + s.scanout_update(); + + let before = snapshot(&s, 1); + small_triangle(&mut s, &st, 100.0, RED); + let mut want = changed_rows(&s, 1, &before); + s.buffer_store(1, 300, 700, 0x1234); + s.scanout.invalidate(700, 1); + want.push(700); + // A line that matches in both buffers is marked and found equal. + s.buffer_store(1, 300, 800, 0); + s.scanout.invalidate(800, 1); + + assert_eq!(resolved_rows(&mut s, &h), vec![700, 800], "the written lines repaint"); + swap_every_window(&mut s); + assert_eq!(resolved_rows(&mut s, &h), want, "the swap shows the drawn rows and the line that differs, not the one that does not"); + assert!(!s.scanout.marks.unnamed_pending(), "the swap measured the marked lines"); +} + +/// Whatever the draws, unnamed writes, swaps and lookup changes, the frame +/// the incremental updates keep is the one a whole resolve makes. +#[test] +fn incremental_updates_keep_the_frame_a_whole_resolve_makes() { + use crate::dev::odyssey::raster_bench::Lcg; + let (mut s, mut st) = gl_board(); + s.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + let mut rng = Lcg(0x5a17); + for i in 0..4096 { + s.regs().set_lut(i, rng.next_u32() & lut::ENTRY_MASK); + } + // Four windows in vertical bands. + for (i, d) in s.planes.did.iter_mut().enumerate() { + *d = (((i % ODSY_WIDTH as usize) / 320) as u8) << ODSY_DID_MAIN_SHIFT; + } + let h = BoardLocks::new(s); + let mut s = h.test_board(); + s.scanout_update(); + + for step in 0..120 { + match rng.next_u32() % 6 { + 0 | 1 => { + let target = rng.next_u32() & 1; + st.drawable.front = target; + st.drawable.back = target; + let y = (rng.next_u32() % 900) as f32; + small_triangle(&mut s, &st, y, [(rng.next_u32() % 2) as f32, (rng.next_u32() % 2) as f32, 1.0, 1.0]); + } + 2 => { + let (x, y) = ((rng.next_u32() % 1200) as i64, (rng.next_u32() % 1000) as i64); + s.buffer_store(rng.next_u32() & 1, x, y, rng.next_u32()); + s.scanout.invalidate(y as i32, 1); + } + 3 => { + let i = (rng.next_u32() % 4096) as usize; + s.regs().set_lut(i, rng.next_u32() & lut::ENTRY_MASK); + s.scanout.repaint_all_for(DirtySite::Other); + } + _ => { + s.front.lock().swap_arm = rng.next_u32() & 0xf; + s.retrace(); + } + } + if rng.next_u32() % 2 == 0 { + h.raster.wait_idle(); + let displayed = s.front.lock().displayed_buffer; + s.test_take_rows(&h, displayed); + s.scanout_update(); + let w = s.reader.frame_width; + let mut want = vec![0u32; w as usize]; + for y in 0..s.reader.frame_height { + s.resolve_row_generic(y, &mut want); + assert_eq!(&s.reader.frame[(y * w) as usize..((y + 1) * w) as usize], &want[..], "step {step}, row {y}"); + } + } + } +} + +/// The screen the display keeps holds the frame, cursor and all, however +/// few lines each frame presents: the first frame takes the whole picture +/// and the ones after it only the lines they resolved. +#[test] +fn the_screen_follows_the_frame_through_frames_that_present_a_few_lines() { + use crate::dev::odyssey::raster_bench::Lcg; + use std::sync::atomic::AtomicU64; + let (mut s, mut st) = gl_board(); + s.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + let mut rng = Lcg(0xfeed); + for i in 0..4096 { + s.regs().set_lut(i, rng.next_u32() & lut::ENTRY_MASK); + } + s.regs().set_cursor(cursor::POSITION as usize, 100 << 16 | 40); + for i in 0..cursor::COLOUR_COUNT { + s.regs().set_cursor((cursor::COLOURS + 1 + i) as usize, rng.next_u32() & lut::ENTRY_MASK); + } + for w in 0..128u32 { + s.regs().set_cursor((cursor::GLYPH + w) as usize, rng.next_u32()); + } + s.regs().set_cursor(cursor::CONTROL as usize, cursor::CTL_ENABLE); + let h = BoardLocks::new(s); + let display = Display::new(crate::disp::StatusSource::detached()); + let (w, ht) = (ODSY_WIDTH as usize, ODSY_HEIGHT as usize); + + for step in 0..8 { + { + let mut s = h.test_board(); + let target = step & 1; + st.drawable.front = target; + st.drawable.back = target; + small_triangle(&mut s, &st, 20.0 + 120.0 * step as f32, [1.0, (step & 1) as f32, 1.0, 1.0]); + if step == 5 { + s.regs().set_cursor(cursor::POSITION as usize, 200 << 16 | 500); + let span = s.cursor_span(); + s.cursor_invalidate(span); + } + if step % 3 == 2 { + s.front.lock().swap_arm = u32::MAX; + } + } + h.raster.wait_idle(); + Odyssey::frame(&h, &display); + let reader = display.reader.lock(); + let screen = display.screen.lock(); + for y in 0..ht { + for x in 0..w { + let rgb = reader.frame[y * w + x]; + let want = 0xff00_0000 | (rgb & 0xff) << 16 | rgb & 0xff00 | (rgb >> 16) & 0xff; + let at = y * crate::disp::FB_STRIDE + x; + assert_eq!((screen.fb_rgb[at], screen.rgba[at]), (want, want), "step {step} ({x}, {y})"); + } + } + } +} + +/// The drawable comparison that runs before every command looks at the clip +/// rectangles the list counts and no others: a change inside the count is +/// seen, and one past it, where nothing is drawn against, is not. +#[test] +fn the_drawable_comparison_covers_the_rectangles_the_list_counts() { + let mut a = GlDrawable::default(); + a.clip.n = 2; + a.clip.r[0] = ClipRect { x0: 0, y0: 0, x1: 9, y1: 9 }; + a.clip.r[1] = ClipRect { x0: 20, y0: 0, x1: 29, y1: 9 }; + let mut b = a; + assert!(a.drawable_eq(&b)); + + b.clip.r[5] = ClipRect { x0: 1, y0: 2, x1: 3, y1: 4 }; + assert!(a.drawable_eq(&b), "past the count"); + + for i in 0..2 { + let mut c = a; + c.clip.r[i].y1 += 1; + assert!(!a.drawable_eq(&c), "rectangle {i}"); + } + let mut c = a; + c.clip.n = 3; + assert!(!a.drawable_eq(&c), "the count"); +} + +/// A texture chain a setup filled is the default one again after `clear`, +/// though `clear` writes only the levels the setup touched: a deeper +/// texture's levels do not leak into the next draw's sampling, which reads +/// every volume's slice count and any level by number. +#[test] +fn a_cleared_texture_chain_is_the_default_one_again() { + let mut chain = TextureChain::default(); + + for i in [0u32, 1, 6] { + chain.touch(i); + chain.volumes[i as usize] = TextureVolume { slices: 3, width: 8 >> i.min(3), ..Default::default() }; + chain.storage[i as usize] = Texture { offset: 5, width: 4, border_rows: true, ..Default::default() }; + chain.levels[i as usize] = GlTexLevel { level: i, width: 4, read_texel: true, ..Default::default() }; + } + chain.volume = chain.volumes[0]; + chain.pyramid = Some(PyramidShape { count: 7, ..Default::default() }); + chain.detail_storage = Texture { stride: 32, ..Default::default() }; + chain.detail_image = GlTexLevel { detail: true, ..Default::default() }; + + chain.clear(); + let fresh = TextureChain::default(); + + assert!(chain.volumes == fresh.volumes && chain.storage == fresh.storage && chain.levels == fresh.levels); + assert!(chain.volume == fresh.volume && chain.pyramid.is_none()); + assert!(chain.detail_storage == fresh.detail_storage && chain.detail_image == fresh.detail_image); + // A second clear has nothing left to write. + chain.clear(); + assert!(chain.levels == fresh.levels); +} + +/// Lighting a vertex reuses one lighting state and puts back only the lights +/// the last vertex enabled, and the colour is what a fresh state gives: +/// across lights switching on, changing and off, with a spot, a positional +/// and a directional light, and with none. +#[test] +fn a_reused_lighting_state_lights_a_vertex_as_a_fresh_one_does() { + use crate::dev::gl::light::Lighting; + + fn fresh(s: &Board, obj: &[f32; 4]) -> [f32; 4] { + let mut lt = Lighting::default(); + lt.init(); + let v = s.gl_light_fill(&mut lt, obj); + let (front, _) = lt.light_vertex(v.eye, v.normal, v.colour); + [front[0], front[1], front[2], v.alpha] + } + fn set_light(s: &mut Board, i: usize, colour: [f32; 3], directional: bool, spot: bool) { + for k in 0..3 { + s.geom.light_colour[i * 12 + k] = (0.1f32 * (i + 1) as f32).to_bits(); + s.geom.light_colour[i * 12 + 4 + k] = colour[k].to_bits(); + s.geom.light_colour[i * 12 + 8 + k] = (colour[k] * 0.5).to_bits(); + } + let blk = &mut s.geom.light_vec[i * gl_cmd::LIGHT_VEC_FLOATS as usize..]; + for k in 0..3 { + blk[light::POS + k] = [2.0f32, 3.0, 4.0 + i as f32][k].to_bits(); + blk[light::HALF + k] = (if directional { [0.0f32, 0.6, 0.8][k] } else { 0.0 }).to_bits(); + } + blk[light::ATTEN] = 1.0f32.to_bits(); + blk[light::ATTEN + 1] = 0.1f32.to_bits(); + blk[light::ATTEN + 2] = 0.01f32.to_bits(); + blk[light::SPOT_COS] = (if spot { -0.5f32 } else { 1.0 }).to_bits(); + blk[light::SPOT_EXP] = (-2.0f32).to_bits(); + for k in 0..3 { + blk[light::SPOT_DIR + k] = [0.0f32, 0.0, 1.0][k].to_bits(); + } + } + fn clear_light(s: &mut Board, i: usize) { + s.geom.light_colour[i * 12..i * 12 + 12].fill(0); + } + + let (mut s, _) = board("v12"); + let identity: [f32; 16] = [1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]; + s.geom.modelview = identity.map(f32::to_bits); + s.geom.normal_matrix = identity.map(f32::to_bits); + s.geom.normal = [0.0f32, 0.6, 0.8].map(f32::to_bits); + s.geom.colour = [0.7f32, 0.5, 0.3, 0.9].map(f32::to_bits); + for k in 0..4 { + s.geom.material[k] = 0.2f32.to_bits(); + s.geom.material[4 + k] = [0.8f32, 0.4, 0.6, 0.5][k].to_bits(); + s.geom.material[8 + k] = 0.5f32.to_bits(); + } + s.geom.shininess_sqrt = 4.0f32.to_bits(); + let obj = [1.0, 2.0, 3.0, 1.0]; + let mut seen = Vec::new(); + let check = |s: &mut Board, what: &str| { + let out = s.gl_light(&obj); + let want = fresh(s, &obj); + assert_eq!(out.map(f32::to_bits), want.map(f32::to_bits), "{what}"); + want + }; + + seen.push(check(&mut s, "no lights")); + set_light(&mut s, 0, [1.0, 0.2, 0.1], true, false); + set_light(&mut s, 3, [0.3, 0.9, 0.2], false, true); + seen.push(check(&mut s, "a directional light and a spot")); + seen.push(check(&mut s, "the same again")); + clear_light(&mut s, 0); + set_light(&mut s, 5, [0.2, 0.2, 0.9], false, false); + seen.push(check(&mut s, "light 0 off, light 5 on")); + clear_light(&mut s, 3); + clear_light(&mut s, 5); + seen.push(check(&mut s, "all off again")); + // Light 3 was a spot and returns as a plain positional light: nothing of + // its spot may be left over. + set_light(&mut s, 3, [0.3, 0.9, 0.2], false, false); + seen.push(check(&mut s, "light 3 back without its spot")); + clear_light(&mut s, 3); + s.geom.colour_material = colour::MATERIAL_DIFFUSE; + set_light(&mut s, 7, [0.6, 0.6, 0.6], false, false); + seen.push(check(&mut s, "colour material, light 7")); + + // The cases differ, so the comparisons above could have failed. + assert_ne!(seen[0], seen[1]); + assert_ne!(seen[1], seen[3]); +} + +/// A triangle a few pixels across. +fn tiny_triangle(s: &mut Board, st: &GlDrawState) { + let v = vertices(&[(10.0, 10.0), (14.0, 10.0), (10.0, 14.0)], RED); + let stipple = [0u32; 32]; + let mut d = GlDraw::new(GL_TRIANGLES, 3, &v, st, &stipple); + assert_eq!(s.gl_draw(&mut d), GL_DRAW_OK); +} + +/// Many small primitives queue behind a busy raster unit without the front +/// end waiting, however many there are in a frame; the queue's work is the +/// sum of theirs. +#[test] +fn many_small_jobs_queue_without_the_front_end_waiting() { + let (h, st) = slow_raster(0); + let release = hold_raster(&h); + let mut s = h.test_board(); + + for _ in 0..1000 { + tiny_triangle(&mut s, &st); + } + // The held raster unit has run none of them, and the front end did not + // wait for it. + assert_eq!(h.raster.pending(), 1001, "no small job waited for room"); + assert!(h.raster.queued() < RASTER_WORK_BUDGET, "{} queued", h.raster.queued()); + assert!(h.raster.queued() >= 1000 * RASTER_JOB_OVERHEAD as u64); + release.store(true, SeqCst); + eventually("the queue did not drain", || h.raster.pending() == 0); + assert_eq!(h.raster.queued(), 0); + h.raster.stop(); +} + +/// One primitive that is a whole screen of work makes the next job wait +/// until the raster unit has drawn it. +#[test] +fn one_big_job_makes_the_front_end_wait() { + let (h, st) = slow_raster(0); + let release = hold_raster(&h); + // The held raster unit runs nothing, so the first job returning shows it + // did not wait. + red_triangle(h.test_board(), &st); + assert!(h.raster.queued() >= RASTER_WORK_BUDGET, "{} queued", h.raster.queued()); + std::thread::scope(|sc| { + let second = sc.spawn(|| tiny_triangle(h.test_board(), &st)); + eventually("the second job did not wait for room", || h.raster.waiting_for_room()); + assert!(!second.is_finished(), "the second job waits for the raster unit to draw"); + release.store(true, SeqCst); + second.join().unwrap(); + }); + assert!(h.raster.queued() <= RASTER_WORK_BUDGET / 2, "{} queued", h.raster.queued()); + eventually("the queue did not drain", || h.raster.pending() == 0); + assert_ne!(pixel(&h.test_board(), 0, 10, 10), 0); + h.raster.stop(); +} + +/// The wait ends once the raster unit has worked the queue down to half the +/// budget, not at the first job it finishes and not at the last; every job +/// runs, in order. +#[test] +fn the_bound_releases_as_the_raster_unit_works() { + let (h, _st) = slow_raster(200); + let ran = std::sync::Arc::new(Mutex::new(Vec::new())); + let job = |i: u32| { + let ran = ran.clone(); + move |_: &mut RasterCtx| ran.lock().push(i) + }; + + h.raster.set_budget(1000); + h.raster.submit_work(400, job(0)); + h.raster.submit_work(400, job(1)); + h.raster.submit_work(400, job(2)); + assert_eq!(h.raster.queued(), 1200, "under the budget, a job does not wait; at it, the next does"); + let start = Instant::now(); + h.raster.submit_work(400, job(3)); + assert!(start.elapsed() >= Duration::from_millis(150), "the fourth waited for the slice"); + assert!(h.raster.queued() <= 500 + 400, "{} queued", h.raster.queued()); + assert!(ran.lock().len() >= 2, "released after the queue fell to half the budget"); + eventually("the queue did not drain", || h.raster.pending() == 0); + assert_eq!(*ran.lock(), vec![0, 1, 2, 3]); + h.raster.stop(); +} + +/// A stop sets a waiting front end going at once, without waiting for the +/// raster unit's slice; the stop still finishes every job, in order. +#[test] +fn stop_releases_a_waiting_front_end() { + let (h, _st) = slow_raster(1500); + let ran = std::sync::Arc::new(Mutex::new(Vec::new())); + let job = |i: u32| { + let ran = ran.clone(); + move |_: &mut RasterCtx| ran.lock().push(i) + }; + + h.raster.set_budget(1000); + h.raster.submit_work(2000, job(0)); + let start = Instant::now(); + std::thread::scope(|sc| { + let waiter = sc.spawn(|| { + h.raster.submit_work(1, job(1)); + start.elapsed() + }); + std::thread::sleep(Duration::from_millis(200)); + assert!(!waiter.is_finished(), "the front end waits for room"); + let stopper = sc.spawn(|| h.raster.stop()); + let waited = waiter.join().unwrap(); + assert!(waited < Duration::from_millis(1000), "released by the stop after {waited:?}, not by the slice"); + stopper.join().unwrap(); + }); + assert_eq!(*ran.lock(), vec![0, 1]); + assert_eq!(h.raster.queued(), 0); +} + +// The display with no lock held, against live writers. + +/// A board whose every drawable is an RGB visual through a gamma ramp, four +/// windows in vertical bands; the raster unit running. +fn display_rig() -> (BoardLocks, GlDrawState, Arc) { + let (mut s, st) = gl_board(); + for i in 0..0x600u32 { + let v = (i * 0x3ff / 0x5ff) & 0x3ff; + s.regs().set_lut((ODSY_GAMMA_BASE + i) as usize, v << 20 | v << 10 | v); + } + for id in 0..dispmode::COUNT { + s.regs().set_dispmode(id as usize, ODSY_GAMMA_GROUP << dispmode::GROUP_SHIFT); + } + for (i, d) in s.planes.did.iter_mut().enumerate() { + *d = (((i % ODSY_WIDTH as usize) / 320) as u8) << ODSY_DID_MAIN_SHIFT; + } + s.set_scanout_size(ODSY_WIDTH, ODSY_HEIGHT); + let h = BoardLocks::new(s); + h.raster.start(); + let display = Arc::new(Display::new(crate::disp::StatusSource::detached())); + (h, st, display) +} + +/// Run the display until `stop`, as its thread does with no pause. +fn run_display(h: &BoardLocks, display: &Arc, stop: &Arc) -> std::thread::JoinHandle<()> { + let (h, display, stop) = (h.clone(), display.clone(), stop.clone()); + std::thread::spawn(move || { + while !stop.load(SeqCst) { + Odyssey::frame(&h, &display); + } + }) +} + +/// Frames at the display's pace after the writers stopped, so the retrace +/// comes due and the last marks are consumed. +fn settle(h: &BoardLocks, display: &Display, frames: u32) { + for _ in 0..frames { + std::thread::sleep(Duration::from_millis(20)); + Odyssey::frame(h, display); + } +} + +/// The presented frame against a whole per-pixel resolve of the planes. +fn assert_frame_is_whole_resolve(h: &BoardLocks, display: &Display, what: &str) { + h.raster.wait_idle(); + let s = h.test_board(); + let reader = display.reader.lock(); + let w = reader.frame_width as usize; + let mut want = vec![0u32; w]; + for y in 0..reader.frame_height { + s.resolve_row_generic(y, &mut want); + assert_eq!(&reader.frame[y as usize * w..(y as usize + 1) * w], &want[..], "{what}: row {y}"); + } +} + +/// One front-end thread, the raster unit, lookup and cursor stores and +/// swap arms run against `Odyssey::frame` with no pause; when the writers +/// stop and the raster queue drains the presented frame is a whole resolve +/// of the planes, and a hundred idle frames leave it so without resolving a +/// line. +fn stress(seed: u64, last_is_lut: bool) { + use crate::dev::odyssey::raster_bench::Lcg; + let (h, mut st, display) = display_rig(); + let stop = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let shown = run_display(&h, &display, &stop); + let mut rng = Lcg(seed); + let end = Instant::now() + Duration::from_millis(600); + + while Instant::now() < end { + let mut s = h.test_board(); + for _ in 0..8 { + match rng.next_u32() % 9 { + 0 | 1 => { + let target = rng.next_u32() & 1; + st.drawable.front = target; + st.drawable.back = target; + let y = (rng.next_u32() % 960) as f32; + small_triangle(&mut s, &st, y, [(rng.next_u32() % 2) as f32, (rng.next_u32() % 2) as f32, 1.0, 1.0]); + } + 2 | 3 => { + let (x, y) = ((rng.next_u32() % 1280) as i64, (rng.next_u32() % 1024) as i64); + s.draw_mask = if rng.next_u32() & 1 == 0 { ODSY_DRAW_MASK_BUFFER0 } else { ODSY_DRAW_MASK_BUFFER1 }; + s.paint_pixel(x, y, rng.next_u32()); + } + 4 => { + let (x, y) = ((rng.next_u32() % 1200) as i64, (rng.next_u32() % 1000) as i64); + s.buffer_store(rng.next_u32() & 1, x, y, rng.next_u32()); + s.scanout.invalidate(y as i32, 1); + } + 5 => { + let i = rng.next_u32() % 0x600; + s.backend_data(ODSY_GAMMA_BASE + i, rng.next_u32() & lut::ENTRY_MASK); + } + 6 => s.backend_data(cursor::BASE + cursor::POSITION, (rng.next_u32() % 1200) << 16 | (rng.next_u32() % 1000)), + 7 => { + let (x, y) = (rng.next_u32() % 1200, rng.next_u32() % 1000); + s.draw_mask = ODSY_DRAW_MASK_BUFFER0; + s.clear_planes(x, y, 40, 3, (rng.next_u32() & 0xf8) as u8, 0xff); + } + _ => h.front.lock().swap_arm |= rng.next_u32() & 0xf, + } + } + std::thread::yield_now(); + } + // A slow tail of single stores of one kind, so the last of them lands at + // a random point of a frame the display is making. A lookup store marks + // every line, so a tail of pixels and a tail of lookup stores are run + // apart: either would repair what the other left stale. + // No swap is left to repaint what the tail leaves stale. + h.front.lock().swap_arm = 0; + std::thread::sleep(Duration::from_millis(40)); + for _ in 0..100u32 { + { + let s = h.test_board(); + if !last_is_lut { + s.draw_mask = ODSY_DRAW_MASK_BUFFER0 | ODSY_DRAW_MASK_BUFFER1; + s.paint_pixel((rng.next_u32() % 1280) as i64, (rng.next_u32() % 1024) as i64, rng.next_u32()); + } else { + // The entry every black pixel is looked up in. + s.backend_data(ODSY_GAMMA_BASE, rng.next_u32() & lut::ENTRY_MASK); + } + } + std::thread::sleep(Duration::from_micros(1500)); + } + stop.store(true, SeqCst); + shown.join().unwrap(); + h.raster.wait_idle(); + settle(&h, &display, 4); + assert_frame_is_whole_resolve(&h, &display, "after the writers stopped"); + + let (before, rows) = { + let reader = display.reader.lock(); + (reader.frame.clone(), reader.stats.rows) + }; + for _ in 0..100 { + Odyssey::frame(&h, &display); + } + let reader = display.reader.lock(); + assert!(reader.frame == before, "an idle frame changed the picture"); + assert_eq!(reader.stats.rows, rows, "idle frames resolved lines nobody marked"); + drop(reader); + assert_frame_is_whole_resolve(&h, &display, "after idle frames"); + h.raster.stop(); +} + +#[test] +fn the_frame_ends_as_a_whole_resolve_under_live_writers() { + for (seed, lut) in [(0x5a17, false), (0x77, true), (0xbeef, false), (0x31, true), (0x42, false), (0x99, true)] { + stress(seed, lut); + } +} + +/// A guest that waits for its fence before it arms a swap never sees a +/// frame that is not the buffer the latch put on the screen: each frame the +/// display makes is one colour, the one drawn into the buffer it shows. +fn swap_atomicity() { + let (h, mut st, display) = display_rig(); + // One window, so the one swap bit is the whole picture. + { + let s = h.test_board(); + s.planes.did.fill(0); + s.planes.colour.bytes.fill(0); + s.planes.buffer1.bytes.fill(0); + s.scanout.invalidate_all(); + } + h.raster.test_slice_limit_ms.store(10_000, SeqCst); + let stop = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let colours = [Arc::new(AtomicU64::new(0)), Arc::new(AtomicU64::new(0))]; + let bad = Arc::new(Mutex::new(Vec::::new())); + let shown = { + let (h, display, stop, colours, bad) = (h.clone(), display.clone(), stop.clone(), colours.clone(), bad.clone()); + std::thread::spawn(move || { + while !stop.load(SeqCst) { + Odyssey::frame(&h, &display); + let reader = display.reader.lock(); + let w = reader.frame_width as usize; + let bit = (h.front.lock().displayed_buffer & 1) as usize; + let want = colours[bit].load(SeqCst) as u32; + // Every 8th pixel of every 8th line, and the corners. + let mut seen = std::collections::BTreeSet::new(); + for y in (0..reader.frame_height as usize).step_by(8).chain([1023]) { + for x in (0..w).step_by(8).chain([w - 1]) { + seen.insert(reader.frame[y * w + x] & 0xff_ffff); + } + } + if seen.len() != 1 || *seen.iter().next().unwrap() != want { + bad.lock().push(format!("displayed buffer {bit}: wanted {want:#08x}, saw {seen:06x?}")); + } + } + }) + }; + let ram = Ram::new(1 << 20); + let (wf, hf) = (ODSY_WIDTH as f32, ODSY_HEIGHT as f32); + + std::thread::sleep(Duration::from_millis(100)); + // Two swaps with nothing drawn: the first measures the buffers the + // mode change left unnamed, so from here the display knows the rows + // the buffers differ in only from the rows the raster unit publishes. + for _ in 0..2 { + h.front.lock().swap_arm |= 1; + eventually("the swap did not latch", || h.front.lock().swap_arm == 0); + std::thread::sleep(Duration::from_millis(40)); + } + for i in 1..=10u32 { + let back = 1 - (h.front.lock().displayed_buffer & 1); + let red = (i * 9) as f32 / 255.0; + { + let s = h.test_board(); + st.drawable.front = back; + st.drawable.back = back; + let v = vertices(&[(-wf, -hf), (3.0 * wf, -hf), (-wf, 3.0 * hf)], [red, 0.0, 0.0, 1.0]); + let stipple = [0u32; 32]; + let mut d = GlDraw::new(GL_TRIANGLES, 3, &v, &st, &stipple); + assert_eq!(s.gl_draw(&mut d), GL_DRAW_OK); + // The completion the guest waits for, slow to finish after it + // has written its token. + let r = ram.clone(); + s.raster.submit_completion(move |_| { + r.put(0x1000, &[1]); + std::thread::sleep(Duration::from_millis(150)); + }); + } + eventually("the completion did not land", || ram.get(0x1000, 1)[0] == 1); + ram.put(0x1000, &[0]); + // What the display shows of this buffer once it is latched: the + // red the client drew through the gamma ramp. + let want = { + let s = h.test_board(); + let regs = s.regs().read(); + let r = i * 9; + regs.gamma(r, lut::RED_SHIFT) << 16 | regs.gamma(0, lut::GREEN_SHIFT) << 8 | regs.gamma(0, lut::BLUE_SHIFT) + }; + colours[back as usize].store(want as u64, SeqCst); + h.front.lock().swap_arm |= 1; + eventually("the swap did not latch", || h.front.lock().swap_arm == 0); + } + std::thread::sleep(Duration::from_millis(100)); + stop.store(true, SeqCst); + shown.join().unwrap(); + h.raster.stop(); + let bad = bad.lock(); + assert!(bad.is_empty(), "{} mixed or stale frames, first: {:?}", bad.len(), bad.first()); +} + +#[test] +fn a_swap_after_a_fence_shows_the_whole_new_frame_with_no_lock() { + swap_atomicity(); +} + +/// The frame waits for no slice and holds no lock the front end needs: a +/// slice that holds the raster unit delays neither the board nor the frame. +/// The slice is held by a job that waits for the test, so the frame ends +/// while the slice is still running only if it waited for nothing; a frame +/// that waited for the slice would never end. +#[test] +fn a_frame_waits_for_no_slice_and_takes_no_lock() { + let (h, st) = slow_raster(0); + let display = Arc::new(Display::new(crate::disp::StatusSource::detached())); + let hold = Arc::new(AtomicBool::new(true)); + let held = hold.clone(); + h.raster.submit(move |_| { + while held.load(SeqCst) { + std::thread::sleep(Duration::from_millis(1)); + } + }); + + red_triangle(&mut h.test_board(), &st); + eventually("the raster unit began its slice", || h.raster.in_slice()); + // The processors' lock is held across the frame, as an access in + // progress holds it: the display needs nothing a processor holds. + let cpu = h.test_submit(); + let framed = AtomicBool::new(false); + std::thread::scope(|scope| { + scope.spawn(|| { + Odyssey::frame(&h, &display); + framed.store(true, SeqCst); + }); + // Let the slice go if the frame never ends, so the failure is + // reported rather than hanging the scope. + let deadline = Instant::now() + Duration::from_secs(20); + while !framed.load(SeqCst) && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(1)); + } + let ended = framed.load(SeqCst); + let in_slice = h.raster.in_slice(); + hold.store(false, SeqCst); + assert!(ended, "the frame waited for the slice"); + assert!(in_slice, "the slice was still running"); + }); + drop(cpu); + let reader = display.reader.lock(); + assert_eq!(reader.stats.frames, 1, "and the frame staged its lines"); + drop(reader); + h.raster.stop(); +} + +/// A lookup store that lands after the display took its marks and before it +/// copied the registers is in the frame it is making, and its repaint mark +/// is kept for the next: the frame ends up showing the new table. With the +/// registers copied first the store would be marked, taken and resolved +/// with the old table, and the picture would stay wrong. +#[test] +fn a_lookup_store_between_taking_the_marks_and_copying_the_registers_is_not_lost() { + let (s, _) = gl_board(); + let h = BoardLocks::new(s); + let display = Display::new(crate::disp::StatusSource::detached()); + Odyssey::frame(&h, &display); + { + let shared = h.shared.clone(); + display.reader.lock().test_between = Some(Box::new(move || { + shared.regs.set_lut((0) as usize, 0x0123_4567 & lut::ENTRY_MASK); + shared.marks.repaint(DirtySite::Lut, 0, ODSY_HEIGHT as i32); + })); + } + h.shared.marks.repaint(DirtySite::Other, 0, ODSY_HEIGHT as i32); + Odyssey::frame(&h, &display); + display.reader.lock().test_between = None; + Odyssey::frame(&h, &display); + Odyssey::frame(&h, &display); + let want = h.shared.regs.read().lut_rgb(0); + assert_ne!(want, 0); + assert_eq!(display.reader.lock().frame[10 * ODSY_WIDTH as usize + 10], want, "the picture shows the old table"); +} diff --git a/src/dev/odyssey/tests/registers.rs b/src/dev/odyssey/tests/registers.rs new file mode 100644 index 000000000..2b1f9a4ef --- /dev/null +++ b/src/dev/odyssey/tests/registers.rs @@ -0,0 +1,652 @@ +//! Registers and the rules for reaching them: the identity ladder the +//! firmware and the graphics driver walk, the registers they only store, +//! and the way anything past that ladder stays visibly unmodelled. + +use crate::dev::odyssey::board::{Board, ODSY_UNIMP_REPORTS}; +use crate::dev::odyssey::consts::{ + backend, cfifo, flctl, gl_cmd, i2c, intr, mem, nic, reg, self, stats, ODSY_BUZZ_1, ODSY_BUZZ_2, + ODSY_BUZZ_3, ODSY_BUZZ_4, ODSY_BUZZ_5, ODSY_BUZZ_6, ODSY_ERROR_COMMAND, ODSY_RESET_CONTROL, + ODSY_STATUS, ODSY_STATUS0, ODSY_SW_TOKEN, ODSY_XT_REQ_TIMEOUT, ODSY_XT_REQ_TIMEOUT_RESET, +}; +use crate::dev::odyssey::tests::{board, board_with_ram, cmd, cmd64, reg_read, reg_write, select, VPRO_WIDGET}; +use std::sync::atomic::Ordering; +use super::*; + +/// Where the one-wire access register's low-drive widths mean what: a +/// long drive is a bus reset, a short one a write-one or a read, one in +/// between a write-zero (the widths the stock PROM was captured using). +fn nic_slot(s: &mut Board, pulse: u32, sample: u32) -> bool { + reg_write(s, nic::PROM, pulse << nic::PULSE_SHIFT | sample << nic::SAMPLE_SHIFT); + let nic = reg_read(s, nic::PROM); + assert_eq!(nic & nic::DONE, nic::DONE, "a slot finishes inside the write"); + nic & nic::LEVEL != 0 +} + +fn nic_write_byte(s: &mut Board, byte: u8) { + for i in 0..8 { + if byte >> i & 1 != 0 { + nic_slot(s, 6, 13); + } else { + nic_slot(s, 80, 30); + } + } +} + +fn nic_read_byte(s: &mut Board) -> u8 { + (0..8).fold(0u8, |byte, i| byte | (nic_slot(s, 6, 13) as u8) << i) +} + +/// Presence, skip-ROM and a read of the identity part's first page. +fn nic_read_page(s: &mut Board) -> [u8; 32] { + assert!(!nic_slot(s, 520, 65), "a bus reset is answered by pulling the line low"); + nic_write_byte(s, 0xcc); + nic_write_byte(s, 0xf0); + nic_write_byte(s, 0x00); + nic_write_byte(s, 0x00); + let mut page = [0u8; 32]; + for b in page.iter_mut() { + *b = nic_read_byte(s); + } + page +} + +/// The header word that opens a record of `count` back-end words at `index`. +fn backend_header(index: u32, count: u32) -> u32 { + 0x3000_0000 | index << 14 | count +} + +fn backend_push(s: &mut Board, word: u32) { + s.mmio_write(backend::SELECT, word as u64, 4); +} + +/// A back-end entry's word, at offset 4 of its slot in the read window. +fn backend_readback(s: &mut Board, index: u32) -> u32 { + s.mmio_read(backend::READ_BASE + ((index as u64) << 3) + 4, 4) as u32 +} + +/// The driver reads the part and the revision nibble it dispatches on. +/// +/// QEMU: identity-carries-part-and-revision. +#[test] +fn the_identity_carries_the_part_and_the_revision() { + let (mut s, _) = board("v12"); + assert_eq!(reg_read(&mut s, IDENTITY), IDENTITY_DEFAULT); + + s.widget.revision = 3; + s.reset_registers(); + assert_eq!(reg_read(&mut s, IDENTITY), 0x3c013047); +} + +/// A live V12 reads the same value at a configuration-block register's +/// base as at base+4 across the whole block, not only on the identity. +/// +/// QEMU: configuration-block-mirrors-both-halves. +#[test] +fn the_configuration_block_answers_alike_at_either_half() { + let (mut s, _) = board("v12"); + assert_eq!(s.mmio_read(0x0, 4), s.mmio_read(IDENTITY, 4)); + + reg_write(&mut s, CONTROL, 0x0f003200 | VPRO_WIDGET); + assert_eq!(s.mmio_read(0x20, 4), s.mmio_read(CONTROL, 4)); + assert_eq!(reg_read(&mut s, CONTROL), 0x0f003200 | VPRO_WIDGET); +} + +/// The driver recovers its widget number from bits 3:0 of the control +/// register before anything writes it, so reset carries the slot. The +/// register is plain read/write; nothing forces the nibble back. +/// +/// QEMU: widget-control-reads-back. +#[test] +fn the_widget_control_register_resets_to_the_slot_and_reads_back() { + let (mut s, _) = board("v12"); + assert_eq!(reg_read(&mut s, CONTROL) & 0xf, VPRO_WIDGET); + + reg_write(&mut s, CONTROL, 0); + assert_eq!(reg_read(&mut s, CONTROL), 0); + // The error class the crosstalk error handler tests stays clear. + assert_eq!(reg_read(&mut s, ODSY_STATUS) & 0x1c0, 0); + reg_write(&mut s, ODSY_ERROR_COMMAND, 0xffff_ffff); + assert_eq!(reg_read(&mut s, ODSY_ERROR_COMMAND), 0, "the error command is acknowledged and discarded"); +} + +/// Memory size, bank count and column-address latency reach the driver +/// through one register: bits 31:30 the size code, bit 29 four banks, +/// bit 28 latency 2. Bit 24 is the constant copy field a live V12 reads +/// as 1. The default is that board's measured 0xa1000000. +/// +/// QEMU: memory-configuration-follows-properties. +#[test] +fn the_memory_word_follows_size_banks_and_latency() { + let (mut s, _) = board("v12"); + assert_eq!(reg_read(&mut s, MEMORY_CONFIG), 0xa100_0000); + + for (size, banks, cas, want) in [(32, 2, 3, 0x0100_0000), (64, 4, 2, 0x7100_0000), (64, 2, 2, 0x5100_0000)] { + s.widget.memory_size = size; + s.widget.memory_banks = banks; + s.widget.cas_latency = cas; + s.compose_memory_config().unwrap(); + assert_eq!(s.widget.memory_config, want, "{size} MB, {banks} banks, CAS {cas}"); + } +} + +/// A size, bank count or latency the board cannot have is refused rather +/// than composed into a word the driver would believe. +/// +/// QEMU: memory-configuration-follows-properties (the property checks). +#[test] +fn a_memory_configuration_the_board_cannot_have_is_refused() { + let (mut s, _) = board("v12"); + s.widget.memory_size = 48; + assert!(s.compose_memory_config().unwrap_err().contains("32, 64 or 128")); + s.widget.memory_size = 128; + s.widget.memory_banks = 3; + assert!(s.compose_memory_config().unwrap_err().contains("2 or 4")); + s.widget.memory_banks = 4; + s.widget.cas_latency = 4; + assert!(s.compose_memory_config().unwrap_err().contains("2 or 3")); + assert!(Board::new("v7", VPRO_WIDGET).is_err(), "an unknown product is refused"); +} + +/// The kernel's reset: a pulse on +0x100c, then the memory controller +/// set-up on +0x1004. Each register is accepted, the board keeps +/// answering identity, and the set-up is kept as written. +/// +/// QEMU: reset-and-memory-control-writes. +#[test] +fn the_kernels_reset_pulse_and_memory_set_up_are_accepted() { + let (mut s, _) = board("v12"); + let master = 0xb << 19; + let fields = master | 2 << 3 | 1 << 2; + + for v in [0x200, 0, 0x16d, 0] { + reg_write(&mut s, ODSY_RESET_CONTROL, v); + } + for v in [master, fields, fields | 1 << 5, fields, fields | 1 << 8] { + reg_write(&mut s, mem::CONTROL, v); + assert_eq!(s.widget.memory_control, v); + } + assert_eq!(reg_read(&mut s, IDENTITY), IDENTITY_DEFAULT); + assert_eq!(reg_read(&mut s, MEMORY_CONFIG) & 0xf000_0000, 0xa000_0000); + assert_eq!(s.diag.reported_addr_count.load(Ordering::Relaxed), 0, "neither register is reported as unmodelled"); +} + +/// The registers the stock firmware's graphics diagnostic reads before it +/// prints a passing board: the request timeout, the flow-control +/// configuration and six scratch ("Buzz") registers, plain storage. +/// +/// QEMU: baseio-prom-diagnostic-registers. +#[test] +fn the_firmware_diagnostic_registers_reset_and_store() { + let (mut s, _) = board("v12"); + assert_eq!(reg_read(&mut s, ODSY_XT_REQ_TIMEOUT), ODSY_XT_REQ_TIMEOUT_RESET); + assert_eq!(reg_read(&mut s, flctl::CONFIG), flctl::CONFIG_RESET); + let buzz = [ODSY_BUZZ_1, ODSY_BUZZ_2, ODSY_BUZZ_3, ODSY_BUZZ_4, ODSY_BUZZ_5, ODSY_BUZZ_6]; + for r in buzz { + assert_eq!(reg_read(&mut s, r), 0, "{r:#x}"); + } + + reg_write(&mut s, ODSY_XT_REQ_TIMEOUT, 0xaaaa_aaaa); + assert_eq!(reg_read(&mut s, ODSY_XT_REQ_TIMEOUT), 0xaaaa_aaaa); + reg_write(&mut s, flctl::CONFIG, 0x5555_5555); + assert_eq!(reg_read(&mut s, flctl::CONFIG), 0x5555_5555); + for (i, r) in buzz.into_iter().enumerate() { + let walk = if i % 2 == 0 { 0xaaaa_aaaa } else { 0x5555_5555 }; + reg_write(&mut s, r, walk); + assert_eq!(reg_read(&mut s, r), walk, "{r:#x}"); + } +} + +/// At flow-control attach the kernel stores a word with an interrupt target +/// shifted left 17, a credit target shifted left 12 and 0xc8; the roles of +/// those fields are not established. The model stores +/// the word; the targets change neither the FIFO status nor the config +/// register, which keeps its own reset value. +/// +/// QEMU: flow-control-credit-target. +#[test] +fn the_flow_control_target_is_stored_and_changes_nothing_else() { + let (mut s, _) = board("v12"); + assert_eq!(reg_read(&mut s, flctl::TARGET), 0); + + for (write, read) in [ + (1 << 17 | 0x1b << 12 | 0xc8, 0x0003_b0c8), + (0x0000_5000, 0x0000_5000), + (1 << 17 | 0x11 << 12 | 0xc8, 0x0003_10c8), + (1 << 17 | 0x0b << 12 | 0xc8, 0x0002_b0c8), + ] { + reg_write(&mut s, flctl::TARGET, write); + assert_eq!(reg_read(&mut s, flctl::TARGET), read); + assert_eq!(reg_read(&mut s, flctl::CONFIG), flctl::CONFIG_RESET); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1003_0000); + } +} + +/// The statistics region: the kernel writes a buffer's address to 0x401c +/// and 0x4024 and a control word to 0x402c. The model stores them and +/// writes nothing into the buffer, since what the board writes there is +/// unknown. +/// +/// QEMU: statistics-region-registers. +#[test] +fn the_statistics_region_is_stored_and_the_buffer_left_alone() { + let (mut s, _, ram) = board_with_ram(); + ram.put(0xb90000, &0x1234_5678u32.to_be_bytes()); + + reg_write(&mut s, stats::ADDR_UPPER, 0); + reg_write(&mut s, stats::ADDR_LOWER, 0xb90000); + reg_write(&mut s, stats::CONTROL, 0x8800_005f); + assert_eq!((s.widget.stats_addr_upper, s.widget.stats_addr_lower, s.widget.stats_control), (0, 0xb90000, 0x8800_005f)); + reg_write(&mut s, stats::ADDR_LOWER, 0); + assert_eq!(s.widget.stats_addr_lower, 0); + + assert_eq!(ram.get(0xb90000, 4), 0x1234_5678u32.to_be_bytes()); + assert_eq!(s.diag.reported_addr_count.load(Ordering::Relaxed), 0, "none of the three is reported as unmodelled"); +} + +/// The five FIFO set-up doublewords carry a register in the high word and +/// its value in the low word. The values are established and their roles are +/// not. The model stores them, reports none as an unmodelled command, and +/// keeps its own low-water threshold. +/// +/// QEMU: command-fifo-setup-registers. +#[test] +fn the_fifo_set_up_doublewords_are_stored() { + let (mut s, _) = board("v12"); + + cmd64(&mut s, 0x6b0, 0); + cmd64(&mut s, 0x6b1, 0x100); + cmd64(&mut s, 0x6b3, 0x5e50); + cmd64(&mut s, 0x6b2, 0x7dc0); + cmd64(&mut s, 0x7b5, 1); + + assert_eq!(s.widget.fifo_setup, [0, 256, 32192, 24144, 1]); + assert_eq!(reg_read(&mut s, ODSY_STATUS0), 0x1003_0000); + assert_eq!(s.diag.reported_cmd_count, 0); +} + +/// The kernels write the doubleword 0x7f0 = 10^9 after the set-up; its unit +/// and meaning are unknown. The board stores it, runs no timer, reports no +/// unmodelled command word, and the stream stays aligned behind it. Flags +/// bit 1 stays clear because no timer runs on the value. +/// +/// QEMU: command-fifo-sync-timeout-register. +#[test] +fn the_0x7f0_doubleword_is_stored_and_runs_no_timer() { + let (mut s, _) = board("v12"); + + cmd64(&mut s, 0x7f0, 1_000_000_000); + assert_eq!(s.widget.setup_7f0, 1_000_000_000); + cmd64(&mut s, 0x6b0, 7); + assert_eq!(s.widget.fifo_setup[0], 7); + assert_eq!(s.diag.reported_cmd_count, 0); + assert_eq!(reg_read(&mut s, intr::FLAGS) & 0x2, 0); +} + +/// Classes 0x20 to 0x35 are not commands the model fails to recognise, and +/// the stream stays aligned after them. The kernel writes 0x26 = 0x7f and +/// 0x2e = 0x380 at set-up: the values are established and the roles of 0x26 +/// and 0x2e are not. +/// +/// QEMU: command-fifo-sequencer-registers. +#[test] +fn the_classes_0x20_to_0x35_are_not_reported_as_unknown_commands() { + let (mut s, _) = board("v12"); + + cmd(&mut s, 0x2600807f); + cmd(&mut s, 0x2e008380); + for (i, class) in [0x20u32, 0x21, 0x24, 0x26, 0x2b, 0x2d, 0x2e, 0x2f, 0x30, 0x34].into_iter().enumerate() { + cmd(&mut s, class << 24 | 0x0080_8000 | i as u32); + } + + assert_eq!(s.diag.reported_cmd_count, 0); +} + +/// The command-FIFO self-test: the kernel's initialisation pushes a +/// nine-word preamble, then reads 0x201594 and requires 0x003d0046. Both +/// graphics bring-up paths give up otherwise. Before the preamble the +/// read-back is zero. +/// +/// QEMU: command-fifo-selftest. +#[test] +fn the_fifo_self_test_answers_only_after_the_preamble() { + let (mut s, _) = board("v12"); + assert_eq!(reg_read(&mut s, cfifo::SELFTEST), 0); + + cmd(&mut s, 0x00010854); + cmd(&mut s, 0); + cmd(&mut s, 0x0fffffff); + cmd64(&mut s, 0x6b0, 0); + cmd64(&mut s, 0x6b1, 0x100); + cmd64(&mut s, 0x6b3, 0x5e50); + cmd64(&mut s, 0x6b2, 0x7dc0); + cmd64(&mut s, 0x7b5, 1); + cmd(&mut s, 0x00010046); + + assert_eq!(reg_read(&mut s, cfifo::SELFTEST), cfifo::SELFTEST_OK); +} + +/// The command FIFO is write-only, and a store width or a command word it +/// does not recognise is reported once rather than guessed at. +/// +/// QEMU: unmodeled-offsets-are-visible. +#[test] +fn an_unrecognised_width_or_command_word_is_reported_not_guessed() { + let (mut s, _) = board("v12"); + + assert_eq!(s.mmio_read(consts::cmd::FIFO, 4), 0, "the FIFO has no read side"); + assert_eq!(s.mmio_read(consts::cmd::FIFO, 4), 0); + s.mmio_write(consts::cmd::FIFO, 0x1234, 2); + assert_eq!(s.diag.reported_addr_count.load(Ordering::Relaxed), 2, "a read and a write, once each however often they repeat"); + + cmd(&mut s, 0x0000_7777); + cmd(&mut s, 0x0000_7777); + assert_eq!(s.diag.reported_cmd_count, 1); + assert_eq!(s.diag.reported_cmd[0], 0x7777); +} + +/// A reset puts the registers back to their documented values. +/// +/// QEMU: reset-restores-documented-values. +#[test] +fn a_reset_restores_the_documented_register_values() { + let (mut s, _) = board("v12"); + reg_write(&mut s, CONTROL, 0xdead_beef); + reg_write(&mut s, intr::ENABLE_SET, 0xffff_ffff); + reg_write(&mut s, ODSY_SW_TOKEN, 0x1234_5678); + reg_write(&mut s, ODSY_XT_REQ_TIMEOUT, 0xdead_beef); + reg_write(&mut s, flctl::CONFIG, 0xdead_beef); + reg_write(&mut s, ODSY_BUZZ_1, 0xdead_beef); + reg_write(&mut s, ODSY_BUZZ_6, 0xdead_beef); + reg_write(&mut s, nic::PROM, 520 << nic::PULSE_SHIFT | 65 << nic::SAMPLE_SHIFT); + + s.reset(); + + assert_eq!(reg_read(&mut s, CONTROL), VPRO_WIDGET); + assert_eq!(reg_read(&mut s, intr::ENABLE_SET), 0); + assert_eq!(reg_read(&mut s, ODSY_SW_TOKEN), 0); + assert_eq!(reg_read(&mut s, nic::PROM), 0); + assert_eq!(reg_read(&mut s, ODSY_XT_REQ_TIMEOUT), ODSY_XT_REQ_TIMEOUT_RESET); + assert_eq!(reg_read(&mut s, flctl::CONFIG), flctl::CONFIG_RESET); + assert_eq!(reg_read(&mut s, ODSY_BUZZ_1), 0); + assert_eq!(reg_read(&mut s, ODSY_BUZZ_6), 0); + // What the product composes is not disturbed. + assert_eq!(reg_read(&mut s, IDENTITY), IDENTITY_DEFAULT); + assert_eq!(reg_read(&mut s, MEMORY_CONFIG), 0xa100_0000); +} + +/// The one-wire access register drives a real bus, not a status bit: the +/// firmware runs presence, selection and a memory read, and stops after +/// one 32-byte page. +/// +/// QEMU: board-identity-part-answers. +#[test] +fn the_identity_part_answers_on_the_one_wire_bus() { + let (mut s, _) = board("v12"); + let page = nic_read_page(&mut s); + + // A format byte, four spaces and the delimiter a scanning reader needs + // before the first field name, then the record. + assert_eq!(page[0], 0x01); + assert_eq!(page[5], b';'); + assert_eq!(&page[6..26], b"Name:ODY128VERSIONB;"); +} + +/// Each profile's identity record names its Octane board, as `hinv -mv` +/// prints it, and an explicit record still wins. +/// +/// QEMU: profile-names-its-board. +#[test] +fn each_profile_names_its_board_in_the_identity_record() { + for (product, record) in [ + ("v12", "Name:ODY128VERSIONB;"), + ("v10", "Name:ODY32VERSIONB;"), + ("v8", "Name:ODY128;"), + ("v6", "Name:ODY32;"), + ] { + let (mut s, _) = board(product); + let page = nic_read_page(&mut s); + assert_eq!(&page[6..6 + record.len()], record.as_bytes(), "{product}"); + } + + let (mut s, _) = board("v6"); + s.widget.nic_record = "Name:TEST".into(); + s.attach_nic().unwrap(); + let page = nic_read_page(&mut s); + assert_eq!(&page[6..15], b"Name:TEST"); +} + +/// The kernel's I2C engine reset polls bits 13:10 of the status word until +/// it reads 0 or 0xa. The model never asserts that field, so the poll ends +/// on its first read, after any command and across a reset. +/// +/// QEMU: i2c-reset-state-poll-terminates. +#[test] +fn the_i2c_reset_poll_ends_on_its_first_read() { + let (mut s, _) = board("v12"); + let state = |s: &mut Board| (reg_read(s, i2c::OPT_STATUS_READ) >> 10) & 0xf; + + s.mmio_write(backend::SELECT, (backend_header(i2c::OPT_CMD_INDEX, 1) as u64) << 32 | 0x1234, 8); + assert!(matches!(state(&mut s), 0 | 0xa)); + s.reset(); + assert!(matches!(state(&mut s), 0 | 0xa)); +} + +/// The kernel's I2C register write to the optional port (device 0x1f, +/// register 2, value 0x34). No downstream device is modelled, so DONE +/// follows the last non-zero command word. +/// +/// QEMU: i2c-opt-port-pll-register-write. +#[test] +fn an_i2c_register_write_reads_ready_and_done_after_each_command() { + let (mut s, _) = board("v12"); + let push = |s: &mut Board, data: u32| { + s.mmio_write(backend::SELECT, (backend_header(i2c::OPT_CMD_INDEX, 1) as u64) << 32 | data as u64, 8) + }; + + push(&mut s, 0x4000); + push(&mut s, 0); + assert_eq!(reg_read(&mut s, i2c::OPT_CMD_READ), 0); + assert_eq!(reg_read(&mut s, i2c::OPT_STATUS_READ), 0x0040_0000, "ready, not done, with no command"); + for data in [0x133e, 0x1202, 0x1234, 0x1834] { + push(&mut s, data); + assert_eq!(reg_read(&mut s, i2c::OPT_STATUS_READ), 0x0040_0200, "{data:#x}"); + } +} + +/// Step 15 of the I2C register read is the command-register readback the +/// driver composes its receive command from; the received byte is always +/// zero since no downstream device answers. +/// +/// QEMU: i2c-opt-port-ddc-register-read. +#[test] +fn an_i2c_register_read_echoes_the_command_and_receives_zero() { + let (mut s, _) = board("v12"); + let push = |s: &mut Board, data: u32| { + s.mmio_write(backend::SELECT, (backend_header(i2c::OPT_CMD_INDEX, 1) as u64) << 32 | data as u64, 8) + }; + + push(&mut s, 0x4000); + push(&mut s, 0); + push(&mut s, 0x133f); + push(&mut s, 0x1205); + assert_eq!(reg_read(&mut s, i2c::OPT_CMD_READ), 0x1205); + push(&mut s, 0x0605); + assert_eq!(reg_read(&mut s, i2c::OPT_STATUS_READ), 0x0040_0200); + assert_eq!(reg_read(&mut s, i2c::OPT_STATUS_READ) & 0xff, 0); +} + +/// The kernel's back-end loads: headers opening a record of words, across +/// 64-bit stores upper half first, wherever the header falls. The frame +/// and line tables and the timing, DAC, clock and scanout entries are +/// kept and read back at offset 4 of their read-window slot, which the +/// kernel's read-modify-writes of 0x26c0, 0x2482, 0x2483 and 0x2700 rely +/// on. None is reported as unmodelled, and 0x2701 bits 2:0 are the +/// back end's version, 1. +/// +/// QEMU: backend-tables-and-readback. +#[test] +fn back_end_tables_are_kept_and_read_back_from_the_window() { + let (mut s, _) = board("v12"); + let push64 = |s: &mut Board, a: u32, b: u32| s.mmio_write(backend::SELECT, (a as u64) << 32 | b as u64, 8); + + // The frame table: a header, 128 words, two to a store. + push64(&mut s, 0x3900_0080, 0x0001_0000); + for i in (1..127).step_by(2) { + push64(&mut s, 0x0001_0000 | i, 0x0001_0000 | (i + 1)); + } + // Its last word shares a store with the line table's header. + push64(&mut s, 0x0001_007f, 0x3800_0400); + for i in (0..1024).step_by(2) { + push64(&mut s, 0x0002_0000 | i, 0x0002_0000 | (i + 1)); + } + assert_eq!(backend_readback(&mut s, 0x2400), 0x0001_0000); + assert_eq!(backend_readback(&mut s, 0x247f), 0x0001_007f); + assert_eq!(backend_readback(&mut s, 0x2000), 0x0002_0000); + assert_eq!(backend_readback(&mut s, 0x23ff), 0x0002_03ff); + + let entries = [0x2481, 0x2482, 0x2483, 0x2600, 0x2680, 0x26c0, 0x2700, 0x27c0, 0x27cc, 0x2800, 0x2801, 0x2807, 0x2809, 0x2929, 0x2b00]; + for (i, e) in entries.into_iter().enumerate() { + // Type 3 for the even ones, type 7 for the odd. + backend_push(&mut s, backend_header(e, 1) | if i & 1 != 0 { 0x4000_0000 } else { 0 }); + backend_push(&mut s, 0x00a5_0000 | e); + } + for e in entries { + assert_eq!(backend_readback(&mut s, e), 0x00a5_0000 | e, "{e:#x}"); + } + + // The kernel's DAC-gain read-modify-write. + backend_push(&mut s, backend_header(0x26c0, 1)); + let gain = backend_readback(&mut s, 0x26c0) | 2; + backend_push(&mut s, gain); + assert_eq!(backend_readback(&mut s, 0x26c0), 0x00a5_26c2); + assert_eq!(backend_readback(&mut s, 0x2701) & 7, 1); + assert_eq!(s.diag.reported_addr_count.load(Ordering::Relaxed), 0); +} + +/// The display modes start at zero, keep each word as written, and a reset +/// clears them again. +/// +/// QEMU: backend-mode-reset. +#[test] +fn display_modes_start_at_zero_keep_their_words_and_clear_on_reset() { + let (mut s, _) = board("v12"); + let junk = 0x6c3a_9f17u32; + for id in 0..40 { + assert_eq!(backend_readback(&mut s, 0x2900 + id), 0); + } + + backend_push(&mut s, backend_header(0x2900, 32)); + for _ in 0..32 { + backend_push(&mut s, ((junk & 0xfe00_1fff) | 0x004b_5e00) & 0xffff_fe00); + } + backend_push(&mut s, backend_header(0x2920, 8)); + for _ in 0..8 { + backend_push(&mut s, junk & 0xffff_ff00); + } + assert_eq!(backend_readback(&mut s, 0x2900), 0x6c4b_5e00); + assert_eq!(backend_readback(&mut s, 0x291f), 0x6c4b_5e00); + assert_eq!(backend_readback(&mut s, 0x2927), 0x6c3a_9f00); + + s.reset(); + assert_eq!(backend_readback(&mut s, 0x2900), 0); + assert_eq!(backend_readback(&mut s, 0x2927), 0); +} + +/// The unmodelled-register reports and the unmodelled-command reports +/// keep separate budgets of 64 distinct entries. On a real boot the +/// command FIFO's own run of unmodelled words filled a shared table before +/// the driver ever touched an unmodelled register, so that access never +/// produced a report. +/// +/// QEMU: unimplemented-reports-independent-budgets. +#[test] +fn filling_the_command_report_budget_leaves_the_register_budget_alone() { + let (mut s, _) = board("v12"); + + // Distinct words of no recognised shape that take no operands, so each + // fills one slot of its own. + for i in 0..ODSY_UNIMP_REPORTS { + cmd(&mut s, 0x0000_0100 | i); + } + assert_eq!(s.diag.reported_cmd_count, ODSY_UNIMP_REPORTS); + cmd(&mut s, 0x0000_7800); + assert_eq!(s.diag.reported_cmd_count, ODSY_UNIMP_REPORTS, "further words stay silent"); + + assert_eq!(s.mmio_read(0x8, 1), 0); + assert_eq!(s.diag.reported_addr_count.load(Ordering::Relaxed), 1, "an unmodelled register is still reported"); +} + +/// An unmodelled word still names its operand count in bits 13:10, and the +/// parser passes over that many words: a 16-operand word the kernel's +/// context restore sends has operands that happen to spell a whole +/// triangle, and read as commands they drew it. Passed over, they are not +/// reported or run, and the word after them is parsed again. +/// +/// QEMU: command-fifo-unknown-word-skips-its-operands. +#[test] +fn an_unmodelled_word_passes_over_the_operands_its_arity_names() { + let (mut s, _) = board("v12"); + + cmd(&mut s, 0x0000_3cf8); + for w in [gl_cmd::COLOR_3UB, 1, 2, 3, select(reg::GL_BEGIN), gl_cmd::TRIANGLES, 0x0000_7777, 0x0000_7778] { + cmd(&mut s, w); + } + for w in 0..8 { + cmd(&mut s, 0x0000_0200 | w); + } + assert_eq!(s.diag.reported_cmd_count, 1, "only the word itself is reported"); + assert_eq!(s.diag.reported_cmd[0], 0x3cf8); + assert!(!s.prim.begin_open, "the operands that spell a primitive did not run"); + + cmd(&mut s, 0x0000_0123); + assert_eq!(s.diag.reported_cmd_count, 2); + assert_eq!(s.diag.reported_cmd[1], 0x123); +} + +/// The kernel's context switch ends with `0x0740 0x1ed` and `0x0741 0`. 0x1ed +/// is accepted, so 0x0740 is not reported, but 0x0741 is still reported. +#[test] +fn the_restart_select_1ed_is_accepted_and_0741_is_reported() { + let (mut s, _) = board("v12"); + + cmd(&mut s, 0x0000_0740); + cmd(&mut s, 0x1ed); + assert_eq!(s.diag.reported_cmd_count, 0, "0x0740 with 0x1ed is accepted"); + cmd(&mut s, 0x0000_0741); + cmd(&mut s, 0); + assert_eq!(s.diag.reported_cmd_count, 1, "0x0741 is reported"); + assert_eq!(s.diag.reported_cmd[0], 0x741); +} + +/// The alpha reference is a 12-bit field, x4096 with 1.0 sent as 4095 and +/// 0x800 = 0.5. The board's alpha test (`RasterRules::alpha_test`) compares at +/// 12 bits: a fragment alpha on the field's step passes GL_GEQUAL and fails +/// GL_GREATER; one field step below fails both, one above passes both. +#[test] +fn the_alpha_reference_field_is_a_fraction_of_4096() { + use crate::dev::odyssey::gl_context::{GL_GEQUAL, GL_GREATER}; + + for field in [1u32, 2048, 4094] { + let (mut s, _) = board("v12"); + for (func, at_passes) in [(GL_GEQUAL, true), (GL_GREATER, false)] { + cmd(&mut s, select(reg::ALPHA_FUNC)); + cmd(&mut s, func << 12 | field); + let r = s.gl_ctx.cur.alpha_ref; + let step = |k: i32| (field as i32 + k) as f64 / 4096.0; + + assert_eq!(r, field as f32 / 4096.0, "field {field}"); + let rules = crate::dev::odyssey::draw::gl_rules(); + let passes = |alpha: f64| rules.alpha_test(func, alpha, r as f64); + assert_eq!(passes(step(0)), at_passes, "field {field} at the reference, func {func:#x}"); + assert!(!passes(step(-1)), "field {field} one step below, func {func:#x}"); + assert!(passes(step(1)), "field {field} one step above, func {func:#x}"); + } + } + let (mut s, _) = board("v12"); + cmd(&mut s, select(reg::ALPHA_FUNC)); + cmd(&mut s, (crate::dev::odyssey::gl_context::GL_ALWAYS << 12) | 4095); + assert_eq!(s.gl_ctx.cur.alpha_ref, 1.0, "the top field is exactly 1.0"); + cmd(&mut s, select(reg::ALPHA_FUNC)); + cmd(&mut s, (crate::dev::odyssey::gl_context::GL_ALWAYS << 12) | 0x800); + assert_eq!(s.gl_ctx.cur.alpha_ref, 0.5, "0x800 is 0.5"); +} diff --git a/src/dev/odyssey/tests/stop.rs b/src/dev/odyssey/tests/stop.rs new file mode 100644 index 000000000..daa19c23e --- /dev/null +++ b/src/dev/odyssey/tests/stop.rs @@ -0,0 +1,125 @@ +//! Stopping a started board: its display, front-end and raster threads all +//! finish, from whichever thread asks, even when that thread is the one +//! the windowing system needs to answer a teardown request. + +use crate::dev::odyssey::Odyssey; +use crate::dev::odyssey::command::CFIFO_PREAMBLE; +use std::sync::Arc; +use crate::dev::heart::{host_clock, Heart, HeartConfig}; +use crate::dev::heart_mem::HeartRam; +use crate::dev::ng1::rex3::Renderer; +use crate::traits::Device; +use std::sync::atomic::Ordering::SeqCst; +use std::sync::mpsc; +use std::time::Duration; + +const BOUND: Duration = Duration::from_secs(10); + +/// A renderer whose `stop` asks the process's main thread for something +/// and waits for the answer, as releasing a GL context does on macOS +/// (glutin runs it on the main thread). `answer` is dropped unserved when +/// the main thread is itself waiting in the board's stop. +struct MainBoundRenderer { + ask: mpsc::Sender>, + stopped: Arc, +} + +impl Renderer for MainBoundRenderer { + fn present( + &mut self, + _screen: &mut crate::disp::Rex3Screen, + _overlay: &mut crate::debug_overlay::DebugOverlay, + _status: &mut crate::disp::StatusBar, + _sbtex: &mut crate::disp::StatusBarTexture, + _stats: &crate::disp::BarStats, + _need_readback: bool, + _live_fb_rgb: Option<&[u32]>, + _live_fb_aux: Option<&[u32]>, + ) { + } + + fn stop(&mut self) { + let (answer, answered) = mpsc::channel(); + if self.ask.send(answer).is_ok() { + let _ = answered.recv(); + } + self.stopped.store(true, SeqCst); + } +} + +fn board() -> Arc { + let heart = Arc::new(Heart::new(HeartConfig::octane([128 << 20, 0, 0, 0]), host_clock())); + let ram = Arc::new(HeartRam::try_new([128, 0, 0, 0]).expect("ram")); + Odyssey::new(11, "v12", heart, ram.dma_view(), crate::disp::StatusSource::detached()).expect("board") +} + +/// Queue the self-test preamble twice behind the board's thread. +fn queue_work(v: &Odyssey) { + for w in CFIFO_PREAMBLE.iter().chain(CFIFO_PREAMBLE.iter()) { + v.state.write(w.port, w.value, w.size); + } +} + +/// Start a board with a renderer installed and stop it from a thread +/// named like the process's main thread, which never answers the renderer. +/// Returns whether `stop` came back within the bound, and whether the +/// renderer was stopped. +fn stop_from_main(work: bool) -> bool { + let v = board(); + let (ask, _unserved) = mpsc::channel(); + let stopped = Arc::new(std::sync::atomic::AtomicBool::new(false)); + *v.display.renderer.lock() = Some(Box::new(MainBoundRenderer { ask, stopped })); + v.start(); + // Let the display thread run a frame or two. + std::thread::sleep(Duration::from_millis(100)); + if work { + queue_work(&v); + } + let (done, finished) = mpsc::channel(); + std::thread::Builder::new() + .name("main".into()) + .spawn(move || { + v.stop(); + let _ = done.send(()); + }) + .unwrap(); + finished.recv_timeout(BOUND).is_ok() +} + +#[test] +fn stopping_an_idle_board_from_the_main_thread_returns() { + assert!(stop_from_main(false), "Device::stop did not return within {BOUND:?}"); +} + +#[test] +fn stopping_a_board_with_work_queued_from_the_main_thread_returns() { + assert!(stop_from_main(true), "Device::stop did not return within {BOUND:?}"); +} + +/// Off the main thread the renderer is still released, on the display +/// thread that owns it, so a stop and start cycle (reset) begins clean. +#[test] +fn stopping_from_another_thread_releases_the_renderer() { + let v = board(); + let (ask, asked) = mpsc::channel::>(); + let stopped = Arc::new(std::sync::atomic::AtomicBool::new(false)); + *v.display.renderer.lock() = Some(Box::new(MainBoundRenderer { ask, stopped: stopped.clone() })); + v.start(); + queue_work(&v); + // This thread is not "main"; it answers the renderer like the + // event loop would. + let v2 = v.clone(); + let (done, finished) = mpsc::channel(); + std::thread::spawn(move || { + v2.stop(); + let _ = done.send(()); + }); + let deadline = std::time::Instant::now() + BOUND; + while finished.try_recv().is_err() { + assert!(std::time::Instant::now() < deadline, "Device::stop did not return within {BOUND:?}"); + if let Ok(answer) = asked.recv_timeout(Duration::from_millis(10)) { + let _ = answer.send(()); + } + } + assert!(stopped.load(SeqCst)); +} diff --git a/src/dev/odyssey/worker.rs b/src/dev/odyssey/worker.rs new file mode 100644 index 000000000..f8cc7c5ec --- /dev/null +++ b/src/dev/odyssey/worker.rs @@ -0,0 +1,199 @@ +//! The board's own thread, which runs the queued command-FIFO stores. +//! +//! The board decodes and draws behind the processor, so the model does its +//! decoding and drawing on a thread of its own rather than inside the +//! processor's store (interface.rs explains the front registers, fifo.rs the +//! queue). The thread sleeps until a store joins an empty queue, pauses +//! `GAP` so a burst of stores lands first, then runs the queue until it is +//! empty. It owns the board's state while it runs: a processor access that +//! must see the stores is answered busy until the queue and the raster queue +//! are empty (`BoardLocks::bus_read`), and whatever a processor may touch +//! meanwhile lives in the host interface. + + +use super::board::Board; +use super::fifo::Run; +use super::mmio::BoardLocks; +use std::sync::atomic::Ordering::SeqCst; +use std::time::Duration; + +/// The board thread's name. +pub(super) const ODYSSEY_WORK_THREAD: &str = "odyssey-work"; + +/// The pause before the board's thread runs a burst, so the stores of the +/// burst land first and one run takes them together. A processor that +/// waits on the thread cuts it short (`CommandFifo::hurry`): an access the +/// bus refused. +pub(super) const GAP: Duration = Duration::from_micros(100); + +/// How much one run may do: for tests a count of stores. The budget is +/// checked between stores, so a run always runs at least one. +pub(super) struct Budget { + words: Option, +} + +impl Budget { + /// A run on the board's thread: everything that waits, or `words` + /// stores when not zero. + pub fn slice(words: u32) -> Self { + Budget { words: (words != 0).then_some(words) } + } + + /// Exactly `stores` stores, or fewer if the queue runs dry. + pub fn stores(stores: u32) -> Self { + Budget { words: Some(stores) } + } + + /// Everything that waits. + pub fn unlimited() -> Self { + Budget { words: None } + } + + /// Count one store against the budget; true when the run must stop. + pub fn spent(&mut self) -> bool { + if let Some(w) = &mut self.words { + *w = w.saturating_sub(1); + return *w == 0; + } + false + } +} + +impl BoardLocks { + /// Start the board's thread, the front end, and the raster unit's + /// behind it. + /// + /// Devices start before the processors, so no processor store is in + /// flight on the unthreaded path when `running` is set below. + pub fn work_start(&self) { + let mut thread = self.fifo.thread.lock(); + if thread.is_some() { + return; + } + self.raster.start(); + self.fifo.start(); + self.fifo.running.store(true, SeqCst); + let handles = self.clone(); + *thread = Some( + std::thread::Builder::new() + .name(ODYSSEY_WORK_THREAD.into()) + .spawn(move || handles.work_run()) + .expect("odyssey work thread"), + ); + } + + /// The machine stops: stop the board's thread, then finish the waiting + /// work, so no work is left behind a stopped machine. + pub fn work_stop(&self) { + let thread = self.fifo.thread.lock().take(); + if let Some(t) = thread { + self.fifo.stop(); + let _ = t.join(); + } + self.fifo.running.store(false, SeqCst); + // SAFETY: the board's thread is joined; the caller stops the + // processors' threads before the devices. + let s = unsafe { &mut *self.board.get() }; + s.work_step(&mut Budget::unlimited()); + // Then the raster work those stores handed on. + self.raster.stop(); + } + + fn work_run(&self) { + while self.fifo.wait_for_work() { + // Let a burst of stores land before the run, unless the + // processor waits for it. + self.fifo.gap(); + if self.fifo.exiting() { + break; + } + #[cfg(test)] + let words = self.fifo.test_words.load(SeqCst); + #[cfg(not(test))] + let words = 0; + self.fifo.slice.store(true, SeqCst); + { + // SAFETY: this is the board's thread, which owns the state + // while it runs stores (BoardCell). + let s = unsafe { &mut *self.board.get() }; + crate::pf_value!(self.fifo.pf_level, crate::perfetto::events::ODSY_CFIFO_LEVEL, self.fifo.level()); + crate::pf_begin!(crate::perfetto::events::ODSY_SLICE, self.fifo.level()); + s.work_step(&mut Budget::slice(words)); + crate::pf_end!(crate::perfetto::events::ODSY_SLICE); + crate::pf_value!(self.fifo.pf_level, crate::perfetto::events::ODSY_CFIFO_LEVEL, self.fifo.level()); + } + self.fifo.slice.store(false, SeqCst); + } + } +} + +impl Board { + pub fn work_pending(&mut self) -> bool { + self.fifo.pending() + } + + /// Run the oldest waiting stores until the budget is spent. The caller + /// is the board's thread, or an access that drains with that thread + /// stopped; it does not hold the front lock. + pub fn work_step(&mut self, b: &mut Budget) { + #[cfg(test)] + { + // A run that takes as long as a long draw would. + let sleep_ms = std::mem::take(&mut self.test_slice_sleep_ms); + if sleep_ms != 0 { + std::thread::sleep(std::time::Duration::from_millis(sleep_ms as u64)); + } + } + + let fifo = self.fifo.clone(); + let mut run = Run::new(); + 'slice: loop { + fifo.take_run(&mut run); + if run.stores().is_empty() { + break; + } + let mut ran = 0; + let mut spent = false; + for q in run.stores() { + self.write_dispatch(q.addr as u64, q.value, q.size as u32); + ran += 1; + if b.spent() { + spent = true; + break; + } + } + // The low-water flag latches as the stores retire below the + // mark (CommandFifo::retire). + fifo.retire(&self.front, &run, ran); + if spent { + break 'slice; + } + } + if fifo.pending() { + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: worker behind: {} stores wait", fifo.stores()); + } + } + + /// Finish the waiting work now, for an access at addr that must see it, + /// with the board's thread stopped. The stores queued when the drain + /// starts run here; any a processor adds meanwhile are left to the + /// board's thread. + pub fn work_drain(&mut self, addr: u64) { + let waiting = self.fifo.stores(); + // With the board's thread running the stores are its to run: a + // second runner would be two threads in the state. The bus answers a + // waiting access busy until it has (BoardLocks::bus_read). + if waiting == 0 || self.fifo.running.load(SeqCst) { + return; + } + let start = std::time::Instant::now(); + crate::pf_scope!(crate::perfetto::events::ODSY_DRAIN, waiting); + self.work_step(&mut Budget::stores(waiting)); + crate::dlog_dev!(LogModule::Odyssey, "sgi-odyssey: worker: drained offset {:#x} in {:?}", addr, start.elapsed()); + } + + /// Reset clears the FIFO and abandons the waiting stores. + pub fn work_reset(&mut self) { + self.fifo.clear(); + } +} diff --git a/src/dev/xbow.rs b/src/dev/xbow.rs index 5243076f3..fc46e7b69 100644 --- a/src/dev/xbow.rs +++ b/src/dev/xbow.rs @@ -13,6 +13,10 @@ //! lock, with the registers that describe them. Interrupt packets and the //! callbacks (link change, link reset) go out after that lock is dropped. //! +//! Revision code 5 (the Octane2's part) adds the reset fences and the +//! resource lock; the fences are kept but gate nothing, since no warm reset +//! travels through the modelled switch. +//! //! Two approximations: a link reset on a fitted link brings the link back //! alive as it was before the reset (link training is not modelled, and the //! stock PROM resets each link it finds and expects it back), and register @@ -57,6 +61,17 @@ const ARB_RELOAD: u32 = 0x58; const PERF_A: u32 = 0x60; const PERF_B: u32 = 0x68; const NIC: u32 = 0x70; +/// Revision code 5 and later: widget 0's reset fence, then links 8 to 15's +/// at a stride of 8, then the resource lock and its clear. +const FENCE_BASE: u32 = 0x78; +const FENCES: usize = 1 + LINKS; +const FENCE_END: u32 = FENCE_BASE + 8 * FENCES as u32; +const LOCK: u32 = 0xc0; +const LOCK_CLEAR: u32 = 0xc8; +const FENCE_MASK: u32 = 0xff; +/// The first revision code with fences and the lock (Crossbow 2.0 in the +/// software tables; an Octane2's code-5 part shows as 1.4 in its inventory). +const REVISION_FENCES: u32 = 5; const LINK_BASE: u32 = 0x100; const LINK_STRIDE: u32 = 0x40; const LINK_END: u32 = LINK_BASE + LINK_STRIDE * LINKS as u32; @@ -132,6 +147,12 @@ struct Regs { initializing: u8, eight_bit: u8, interrupt_asserted: bool, + /// Revision code 5 and later: which ports' warm resets may reach the + /// core (fence 0) and each link (fences 1 to 8). Kept; iris sends no + /// warm resets through the switch. + fences: [u32; FENCES], + /// The resource lock's byte. + lock: u32, } impl Regs { @@ -157,6 +178,8 @@ impl Regs { link_arb_lower: [0; LINKS], link_aux_status: [0; LINKS], link_event: [0; LINKS], + fences: [FENCE_MASK; FENCES], + lock: 0, came_up: startup, initializing: 0, eight_bit: 0, @@ -384,7 +407,10 @@ impl Xbow { } } - fn accessible(slot: u32, write: bool) -> bool { + fn accessible(&self, slot: u32, write: bool) -> bool { + if self.revision >= REVISION_FENCES && ((FENCE_BASE..FENCE_END).contains(&slot) || slot == LOCK || slot == LOCK_CLEAR) { + return slot % 8 == 0; + } match slot { IDENTIFICATION | STATUS | ERR_UPPER | ERR_LOWER | STATUS_CLEAR => !write, CONTROL | PACKET_TIMEOUT | INT_UPPER | INT_LOWER | ERR_CMD | LLP_CONTROL | ARB_RELOAD | PERF_A @@ -409,7 +435,7 @@ impl Xbow { return None; } let slot = off.wrapping_sub(4); - if size != 4 || off % 8 != 4 || !Self::accessible(slot, write) { + if size != 4 || off % 8 != 4 || !self.accessible(slot, write) { self.note(off, size, write); self.record_access_error(r, out); return None; @@ -444,6 +470,8 @@ impl Xbow { PERF_A => r.perf_a, PERF_B => r.perf_b, NIC => r.nic, + s if (FENCE_BASE..FENCE_END).contains(&s) => r.fences[((s - FENCE_BASE) / 8) as usize], + LOCK | LOCK_CLEAR => r.lock, s => { let link = ((s - LINK_BASE) / LINK_STRIDE) as usize; match (s - LINK_BASE) % LINK_STRIDE { @@ -495,6 +523,14 @@ impl Xbow { PERF_A => r.perf_a = val & PERF_SELECT_MASK, PERF_B => r.perf_b = val & PERF_SELECT_MASK, NIC => r.nic = val & NIC_MASK, + s if (FENCE_BASE..FENCE_END).contains(&s) => r.fences[((s - FENCE_BASE) / 8) as usize] = val & FENCE_MASK, + // Test-and-set: stored only while the lock is free. + LOCK => { + if r.lock == 0 { + r.lock = val & 0xff; + } + } + LOCK_CLEAR => r.lock = 0, s => { let link = ((s - LINK_BASE) / LINK_STRIDE) as usize; match (s - LINK_BASE) % LINK_STRIDE { @@ -737,4 +773,30 @@ mod tests { assert!(x.write(LINK_BASE + LINK_STRIDE * 7 + LINK_RESET + 4, 4, 0)); assert_eq!(*seen.lock(), [(15, true, true)]); } + + /// An Octane2's Crossbow (revision code 5) has the reset fences and the + /// resource lock its PROM programs: the fences reset to all ones and keep + /// bits 7:0, the lock takes a value only while free and its clear frees + /// it. Revision code 4 has neither, and an access there is an error. + #[test] + fn revision_five_has_reset_fences_and_a_lock() { + let x = Xbow::new(5, 0x8100); + for fence in 0..FENCES as u32 { + let off = FENCE_BASE + 8 * fence + 4; + assert_eq!(x.read(off, 4), Some(0xff), "fence {fence} resets to all ones"); + assert!(x.write(off, 4, 0x1f7)); + assert_eq!(x.read(off, 4), Some(0xf7)); + } + assert_eq!(x.read(LOCK + 4, 4), Some(0)); + assert!(x.write(LOCK + 4, 4, 3)); + assert!(x.write(LOCK + 4, 4, 5)); + assert_eq!(x.read(LOCK_CLEAR + 4, 4), Some(3), "a held lock keeps its value"); + assert!(x.write(LOCK_CLEAR + 4, 4, 0x55)); + assert_eq!(x.read(LOCK + 4, 4), Some(0)); + assert_eq!(x.read(STATUS + 4, 4).unwrap() as u32 & STATUS_ACCESS_ERROR, 0); + + let old = octane(); + assert_eq!(old.read(FENCE_BASE + 4, 4), None); + assert_ne!(old.read(STATUS + 4, 4).unwrap() as u32 & STATUS_ACCESS_ERROR, 0); + } } diff --git a/src/devlog.rs b/src/devlog.rs index ea17db925..d082e7ba1 100644 --- a/src/devlog.rs +++ b/src/devlog.rs @@ -52,6 +52,9 @@ pub enum LogModule { /// The Octane's RAD1 PCI audio: rings, status DMA and register accesses /// the model does not support. Rad1 = 27, + /// The Octane's VPro (Odyssey) graphics board: guest accesses the + /// model does not support. + Odyssey = 28, } // Mask bits shared by L1i / L1d / L2c @@ -60,7 +63,7 @@ pub const CACHE_LOG_MISS: u32 = 0x2; // log misses and fills pub const CACHE_LOG_OP: u32 = 0x4; // log CACHE instruction ops impl LogModule { - pub const COUNT: usize = 28; + pub const COUNT: usize = 29; pub fn from_str(s: &str) -> Option { match s { @@ -92,6 +95,7 @@ impl LogModule { "ultra" => Some(Self::Ultra), "gr2" => Some(Self::Gr2), "rad1" => Some(Self::Rad1), + "odyssey" => Some(Self::Odyssey), _ => None, } } @@ -126,6 +130,7 @@ impl LogModule { Self::Ultra => "ultra", Self::Gr2 => "gr2", Self::Rad1 => "rad1", + Self::Odyssey => "odyssey", } } @@ -136,7 +141,7 @@ impl LogModule { Self::Vino, Self::Dcb, Self::Vc2, Self::Cmap, Self::Xmap, Self::Bt445, Self::Scc, Self::Ps2, Self::Rtc, Self::Eeprom, Self::L1i, Self::L1d, Self::L2c, Self::Ultra, Self::Scsi1, - Self::Nveeprom, Self::Gr2, Self::Rad1, + Self::Nveeprom, Self::Gr2, Self::Rad1, Self::Odyssey, ] } @@ -277,6 +282,7 @@ impl DevLog { ModuleLog::new(), // Nveeprom ModuleLog::new(), // Gr2 ModuleLog::new(), // Rad1 + ModuleLog::new(), // Odyssey ], } } diff --git a/src/display_stop_tests.rs b/src/display_stop_tests.rs index fa1b319d5..65954bf7f 100644 --- a/src/display_stop_tests.rs +++ b/src/display_stop_tests.rs @@ -4,7 +4,7 @@ //! stopping the machine (see `gfx_display::DisplayStop`). The renderer here //! does the same: its `stop` asks for an answer from the test thread. One //! machine per display family, stopped from a reset and then from main: -//! Newport (REX3), GR2, IMPACT (MGRAS) and the O2. +//! Newport (REX3), GR2, IMPACT (MGRAS), VPro and the O2. use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}; @@ -123,6 +123,7 @@ fn stopping_from_the_main_thread_never_waits_on_the_renderer() { (MachineProfile::IndyIp24, GraphicsBoard::Newport), (MachineProfile::Indigo2Ip22, GraphicsBoard::Xz), (MachineProfile::OctaneIp30, GraphicsBoard::SolidImpact), + (MachineProfile::OctaneIp30, GraphicsBoard::V6), (MachineProfile::O2Ip32, GraphicsBoard::Newport), ]; let mut failures = Vec::new(); diff --git a/src/gfifo.rs b/src/gfifo.rs index 5a25c179d..e498598e5 100644 --- a/src/gfifo.rs +++ b/src/gfifo.rs @@ -359,6 +359,22 @@ impl GFifo { Some((slot.addr, slot.val)) } + /// The entry `n` places behind the one `peek_at(h)` returns, if the + /// producer has published that far. Called only from head `h`'s thread; + /// nothing is consumed. + #[inline] + pub fn peek_nth_at(&self, h: usize, n: usize) -> Option<(u32, u64)> { + let head = self.local_head[h].get(); + let tail = self.tail.load(Ordering::Acquire); + if tail.wrapping_sub(head) & Self::MASK <= n { + return None; + } + // SAFETY: as in peek_at: the slot is published, and only this head's + // thread reads behind its own head. + let slot = unsafe { &*self.buf.as_ptr().add(head.wrapping_add(n) & Self::MASK) }; + Some((slot.addr, slot.val)) + } + /// Advance head past the current entry after it has been fully processed. /// Must follow a successful `peek()`. #[inline] @@ -483,6 +499,43 @@ and pixel data is being lost", slot.addr, GFIFO_PAYLOAD); } } + /// Copy up to `out.len()` of the oldest entries head `h` has not + /// consumed into `out`, leaving them queued, and return how many. One + /// look at `tail` serves the whole run, where `peek_nth_at` looks once + /// per entry. Called only from head `h`'s thread. + #[inline] + pub fn peek_run_at(&self, h: usize, out: &mut [(u32, u64)]) -> usize { + let head = self.local_head[h].get(); + let tail = self.tail.load(Ordering::Acquire); + self.shadow_tail[h].set(tail); + let n = (tail.wrapping_sub(head) & Self::MASK).min(out.len()); + for (i, e) in out[..n].iter_mut().enumerate() { + // SAFETY: as in peek_at: the slots are published, and only this + // head's thread reads behind its own head. + let slot = unsafe { &*self.buf.as_ptr().add(head.wrapping_add(i) & Self::MASK) }; + *e = (slot.addr, slot.val); + } + n + } + + /// `consume_at` for the first `n` entries of a `peek_run_at` run, with one + /// look at `tail` and at most one publication of the head. + #[inline] + pub fn consume_n_at(&self, h: usize, n: usize) { + if n == 0 { + return; + } + let head = self.local_head[h].get(); + let next_head = head.wrapping_add(n) & Self::MASK; + self.local_head[h].set(next_head); + let tail = self.tail.load(Ordering::Acquire); + // The head is published at each 64-entry boundary the run crosses, + // and whenever the run empties the queue (see `consume_at`). + if (head ^ next_head) >> 6 != 0 || next_head & 63 == 0 || next_head == tail { + self.head[h].store(next_head, Ordering::Release); + } + } + #[inline] pub fn flush_head(&self) { self.flush_head_at(0) @@ -581,6 +634,49 @@ mod tests { assert!(f.is_empty()); } + #[test] + fn peek_nth_looks_behind_the_head_across_the_wrap() { + let f: Box> = Box::new(GFifo::new()); + for i in 0..5 { + assert!(f.try_push(i, 0)); + } + for _ in 0..5 { + f.consume(); + } + // The next entries wrap past the end of the buffer. + for i in 10..16 { + assert!(f.try_push(i, i as u64)); + } + assert_eq!(f.peek_nth_at(0, 0), f.peek()); + assert_eq!(f.peek_nth_at(0, 5), Some((15, 15))); + assert_eq!(f.peek_nth_at(0, 6), None, "not pushed"); + f.consume(); + assert_eq!(f.peek_nth_at(0, 4), Some((15, 15))); + } + + #[test] + fn a_run_peeked_and_consumed_across_the_wrap_matches_one_at_a_time() { + let f: Box> = Box::new(GFifo::new()); + for i in 0..5 { + assert!(f.try_push(i, 0)); + } + f.consume_n_at(0, 5); + for i in 10..16 { + assert!(f.try_push(i, i as u64)); + } + let mut run = [(0u32, 0u64); 4]; + assert_eq!(f.peek_run_at(0, &mut run), 4); + assert_eq!(run, [(10, 10), (11, 11), (12, 12), (13, 13)]); + assert_eq!(f.len(), 6, "peeking consumes nothing"); + f.consume_n_at(0, 4); + let mut rest = [(0u32, 0u64); 4]; + assert_eq!(f.peek_run_at(0, &mut rest), 2, "only what was pushed"); + assert_eq!(rest[..2], [(14, 14), (15, 15)]); + f.consume_n_at(0, 2); + assert!(f.is_empty(), "the run that empties the queue publishes the head"); + assert_eq!(f.peek_run_at(0, &mut rest), 0); + } + #[test] fn the_slowest_head_limits_the_producer() { let f: Box> = Box::new(GFifo::new()); diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 60fb6ff07..fe9f77642 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -27,6 +27,7 @@ use crate::dev::ioc3::Ioc3; use crate::dev::ioc3_eth::Ioc3Eth; use crate::dev::isp1040::Isp1040; use crate::dev::mgras::hq4::ImpactXio; +use crate::dev::odyssey::Odyssey; use crate::dev::rad1::Rad1; use crate::dev::sflash::{CfiFlash, FLASH_SIZE}; use crate::dev::w1; @@ -49,8 +50,39 @@ pub const OCTANE_COUNT_HZ: u64 = 97_500_000; /// R10000 revision 2.7, as an Octane's hinv reports it; the raw PRId is /// inferred from the R10000 manual's encoding. pub const OCTANE_PRID: u32 = 0x0927; -/// The Crossbow on the Octane reports revision 4. +/// The Crossbow on the Octane reports revision code 4. const XBOW_REVISION: u32 = 4; +/// The Octane2's Crossbow: revision code 5, which IRIX's inventory names +/// Crossbow 1.4 (a PROM-monitor read of 0x50000001 beside an inventory of +/// 1.4 on an Octane2). SGI's VPro upgrade kit (007-4210-003) +/// requires Crossbow 1.4 for the V10 and V12, so a machine with one of +/// those presents it. +const XBOW_REVISION_OCTANE2: u32 = 5; +/// The Bridge's silicon revision: 3 on the 175 MHz Octane (an OpenBSD boot +/// there prints "Bridge revision 3"), 4 on the reference Octane2, whose PROM +/// monitor read the identification word as 0x4c00206d. +const BRIDGE_REVISION: u32 = 3; +const BRIDGE_REVISION_OCTANE2: u32 = 4; + +/// The PROM revision a VPro board needs (007-4210-003): 4.0 for the V6 and +/// V8, 4.5 for the V10 and V12. +fn vpro_prom_needed(board: crate::config::GraphicsBoard) -> (u32, u32) { + match board { + crate::config::GraphicsBoard::V10 | crate::config::GraphicsBoard::V12 => (4, 5), + _ => (4, 0), + } +} + +/// The PROM's own revision, from its banner ("SGI Version 6.5 Rev 4.9 +/// IP30 ..."), if the image carries one. +fn prom_revision(prom: &[u8]) -> Option<(u32, u32)> { + let text = String::from_utf8_lossy(prom); + let at = text.find(" Rev ")?; + let rest = &text[at + 5..]; + let end = rest.find(" IP30")?; + let (major, minor) = rest[..end].split_once('.')?; + Some((major.parse().ok()?, minor.parse().ok()?)) +} /// The graphics port the one IMPACT board sits in. A choice: it is where a /// measured Octane2 has its graphics board, not a measured SI placement. pub const WIDGET_GFX: u8 = 11; @@ -98,8 +130,10 @@ pub struct OctaneMachine { eth: Arc, rad: Arc, flash: Arc, - /// The IMPACT board, unless `[octane] graphics = "none"`. + /// The IMPACT board, when `[graphics] board` names one. pub gfx: Option>, + /// The VPro board, with `[graphics] board = "v6"`, `"v8"`, `"v10"` or `"v12"`. + pub vpro: Option>, nat: Option<(Arc, (std::net::Ipv4Addr, std::net::Ipv4Addr, std::net::Ipv4Addr))>, /// Guest frames the network backend has processed. frames: Option>, @@ -132,11 +166,35 @@ impl OctaneMachine { return Err(format!("[machine] cpus = {cpu_count}: IRIX runs an Octane with 1 or 2")); } let banks = heart_banks(cfg); - let board = match cfg.graphics_board() { + let kind = cfg.graphics_board(); + let board = match kind { crate::config::GraphicsBoard::None => None, + b if b.is_vpro() => None, b if b.is_impact() => Some(b), - b => return Err(format!("graphics.board \"{}\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, none)", b.as_str())), + b => return Err(format!("graphics.board \"{}\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, v6, v8, v10, v12, none)", b.as_str())), }; + let has_vpro = kind.is_vpro(); + if has_vpro { + let needed = vpro_prom_needed(kind); + if let Some(found) = prom_revision(&prom) { + if found < needed { + eprintln!( + "iris: the {} needs PROM {}.{} or later; {prom_path} is {}.{}", + kind.label(), + needed.0, + needed.1, + found.0, + found.1 + ); + } + } + } + // The V10 and V12 shipped only in the Octane2, and SGI's upgrade kit + // (007-4210-003) requires Crossbow 1.4 for them. Choosing one gives + // the machine the Octane2's Crossbow (revision 5) and Bridge + // (revision 4); there is no separate option for the machine. + let octane2 = matches!(kind, crate::config::GraphicsBoard::V10 | crate::config::GraphicsBoard::V12); + let xbow_revision = if octane2 { XBOW_REVISION_OCTANE2 } else { XBOW_REVISION }; let (event_tx, event_rx) = std::sync::mpsc::sync_channel(4); let timer_manager = Arc::new(TimerManager::new()); @@ -150,14 +208,15 @@ impl OctaneMachine { let module = if cpu_count > 1 { "PM20" } else { "PM10" }; heart.set_microlan(record_bus("EMUU00", "030-0888-004", "B", module, 0x5145_4d55_5500)); - let gfx_link = if board.is_some() { 1 << WIDGET_GFX } else { 0 }; - let xbow = Arc::new(Xbow::new(XBOW_REVISION, (1 << WIDGET_HEART) | (1 << WIDGET_BRIDGE) | gfx_link)); + let gfx_link = if board.is_some() || has_vpro { 1 << WIDGET_GFX } else { 0 }; + let xbow = Arc::new(Xbow::new(xbow_revision, (1 << WIDGET_HEART) | (1 << WIDGET_BRIDGE) | gfx_link)); xbow.connect_heart(heart.clone()); // The onboard Bridge: memory and interrupts through HEART, the boot // flash on its first chip select, the system board's record on its // MicroLAN. let bridge = Bridge::new(mem.clone(), heart.clone()); + bridge.set_revision(if octane2 { BRIDGE_REVISION_OCTANE2 } else { BRIDGE_REVISION }); // The flash holds the PROM's own records as well as its code; with // `[octane] flash` they last from run to run. let flash = Arc::new(match &cfg.octane.flash { @@ -214,6 +273,11 @@ impl OctaneMachine { let fasttick = Arc::new(AtomicU64::new(0)); let status = crate::disp::StatusSource::new(heartbeat.clone(), fasttick.clone()); let gfx = board.map(|b| ImpactXio::with_board(WIDGET_GFX, b, heart.clone(), mem.clone(), status.clone())); + let vpro = if has_vpro { + Some(Odyssey::new(WIDGET_GFX, kind.as_str(), heart.clone(), mem.clone(), status.clone())?) + } else { + None + }; // The bus over them, then the table. let phys = Ip30Bus::new(ram.clone()); @@ -226,6 +290,9 @@ impl OctaneMachine { if let Some(g) = &gfx { phys.attach_widget(WIDGET_GFX, g.clone(), false); } + if let Some(v) = &vpro { + phys.attach_widget(WIDGET_GFX, v.clone(), true); + } let phys = Arc::new(phys); phys.init(); Self::sync_links(&phys, &xbow); @@ -269,7 +336,7 @@ impl OctaneMachine { "iris: Octane with {} MB ({}), {}", banks.iter().sum::() >> 20, banks.iter().filter(|&&b| b != 0).map(|b| format!("{}", b >> 20)).collect::>().join("+"), - board.map_or("no graphics", |b| b.label()) + if has_vpro { kind.label() } else { board.map_or("no graphics", |b| b.label()) } ); let count_hz = cfg.clock.fixed_mhz.map_or(OCTANE_COUNT_HZ, |mhz| (mhz * 1_000_000.0) as u64); @@ -403,6 +470,9 @@ impl OctaneMachine { monitor.register_device(g.mgras().clone()); g.register_locks(); } + if let Some(v) = &vpro { + monitor.register_device(v.clone()); + } let monitor_port = cfg.monitor_port.unwrap_or(crate::monitor::DEFAULT_PORT); let ci_serial = cfg.ci.then(|| Arc::new(crate::dev::serial_host::CiSerialBackend::new())); Self::connect_serial(cfg, &ioc3, ci_serial.as_ref()); @@ -435,6 +505,7 @@ impl OctaneMachine { rad, flash, gfx, + vpro, nat, frames, scratch_path, @@ -527,6 +598,9 @@ impl OctaneMachine { if let Some(g) = &self.gfx { d.push(g.clone()); } + if let Some(v) = &self.vpro { + d.push(v.clone()); + } d } @@ -594,6 +668,9 @@ impl OctaneMachine { if let Some(g) = &self.gfx { g.power_on(); } + if let Some(v) = &self.vpro { + v.power_on(); + } Self::sync_links(&self.phys, &self.xbow); self.restart_peripherals(); } @@ -678,7 +755,10 @@ impl Workstation for OctaneMachine { self.lifecycle = state; } fn get_display(&self) -> Option> { - self.gfx.as_ref().map(|g| g.display()) + match &self.vpro { + Some(v) => Some(v.display()), + None => self.gfx.as_ref().map(|g| g.display()), + } } fn register_monitor_device(&self, device: Arc) { self.monitor.register_device(device); } fn get_ps2(&self) -> Arc { @@ -843,3 +923,19 @@ impl Device for Ip30Monitor { w.write_all(text.as_bytes()).map_err(|e| e.to_string()) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// The VPro fitment check reads the PROM's revision from its banner; + /// a board that needs a newer PROM than the image warns. + #[test] + fn the_prom_banner_gives_its_revision() { + let banner = b"\0\0SGI Version 6.5 Rev 4.9 IP30 Jan 20, 2004\0"; + assert_eq!(prom_revision(banner), Some((4, 9))); + assert_eq!(prom_revision(b"no banner here"), None); + assert!((4, 3) < vpro_prom_needed(crate::config::GraphicsBoard::V12)); + assert!((4, 3) >= vpro_prom_needed(crate::config::GraphicsBoard::V6)); + } +} diff --git a/src/perfetto/events.rs b/src/perfetto/events.rs index 125b90740..b81e8b9f1 100644 --- a/src/perfetto/events.rs +++ b/src/perfetto/events.rs @@ -204,6 +204,44 @@ pub static MGRAS_IRQ_LINE: EventDef = EventDef::new("irq line", cat::GFX).on_tra pub static MGRAS_HEART_PACKET: EventDef = EventDef::new("heart packet", cat::GFX).on_track("IMPACT irq").in_group(GFX_GROUP).with_arg("vector<<1|set"); pub static MGRAS_HQ4_ACK: EventDef = EventDef::new("hq4 ack", cat::GFX).on_track("IMPACT irq").in_group(GFX_GROUP).with_arg("off<<32|val"); +// ── VPro (Odyssey) ─────────────────────────────────────────────────────────── + +/// The command FIFO level STATUS0 is built from: stores pushed and not yet +/// run. Sampled where the board looks (slice start and end, STATUS0 reads, +/// the low-water edge) rather than at every store, which a textport sends +/// by the hundred thousand. +pub static ODSY_CFIFO_LEVEL: EventDef = EventDef::new("VPro cfifo level", cat::GFX).in_group(GFX_GROUP); +/// The FIFO fell below the low-water mark as a store retired (arg: level). +pub static ODSY_CFIFO_LOW_WATER: EventDef = EventDef::new("cfifo low water", cat::GFX).on_track("VPro irq").in_group(GFX_GROUP).with_arg("level"); +/// One slice of the board's front-end thread running queued stores (arg: +/// stores waiting when it began), and an access draining the queue on its own +/// thread (arg: stores waiting). +pub static ODSY_SLICE: EventDef = EventDef::new("slice", cat::GFX).with_arg("waiting"); +pub static ODSY_DRAIN: EventDef = EventDef::new("drain", cat::GFX).with_arg("waiting"); +/// One slice of the raster thread running raster jobs (arg: jobs pending +/// when it began), and the front end waiting for the raster unit to finish +/// its jobs before it touches the planes itself (arg: jobs pending). +pub static ODSY_RASTER_SLICE: EventDef = EventDef::new("raster slice", cat::GFX).with_arg("pending"); +pub static ODSY_RASTER_WAIT: EventDef = EventDef::new("raster wait", cat::GFX).with_arg("pending"); +/// A page-table DMA descriptor run (arg: table address). +pub static ODSY_DMA: EventDef = EventDef::new("dma descriptor", cat::GFX | cat::DMA).with_arg("table"); +/// The DMA-done interrupt raised after a transfer that asked for it. +pub static ODSY_DMA_DONE: EventDef = EventDef::new("dma done", cat::GFX | cat::DMA).on_track("VPro irq").in_group(GFX_GROUP); +/// An interrupt packet sent to HEART (arg: source shift << 9 | vector << 1 +/// | set). Sources: flag level, softsync, retrace and DMA-done. +pub static ODSY_IRQ: EventDef = EventDef::new("interrupt packet", cat::GFX | cat::IRQ).on_track("VPro irq").in_group(GFX_GROUP).with_arg("shift<<9|vector<<1|set"); +/// The driver armed window flips for the next retrace (arg: toggle mask +/// written). +pub static ODSY_SWAP_ARM: EventDef = EventDef::new("swap arm", cat::GFX).on_track("VPro display").in_group(GFX_GROUP).with_arg("mask"); +/// A retrace: pending flips latched (arg: displayed-buffer bits after). +pub static ODSY_RETRACE: EventDef = EventDef::new("retrace", cat::GFX).on_track("VPro display").in_group(GFX_GROUP).with_arg("displayed"); +/// The refresh thread resolving dirty lines, and handing the frame on. +pub static ODSY_CAPTURE: EventDef = EventDef::new("frame capture", cat::GFX).on_track("VPro capture").in_group(GFX_GROUP); +pub static ODSY_PRESENT: EventDef = EventDef::new("present", cat::GFX).on_track("VPro present").in_group(GFX_GROUP); +/// The display copying a frame's dirty lines aside, with no lock and no wait +/// for the raster unit (on the display thread). +pub static ODSY_FRAME_PLANES: EventDef = EventDef::new("frame planes", cat::GFX); + // ── jitv2 ──────────────────────────────────────────────────────────────────── /// A dispatch queued a page compile (arg: guest PC). diff --git a/test/gltest/glstate.c b/test/gltest/glstate.c index 17d2842df..133829f06 100644 --- a/test/gltest/glstate.c +++ b/test/gltest/glstate.c @@ -66,7 +66,7 @@ static const struct { const char *name; const char *why; int gr2_only; } xfails[ { "fb/INDEX_CLEAR_VALUE", "libGLcore EXPRESS drops glClearIndex in RGBA mode", 1 }, }; -static int gr2; +static int gr2, vpro; static const char *xfail_why(const char *name) { @@ -475,7 +475,10 @@ static void t_current(void) glTranslatef(0.0f, 0.0f, -0.5f); glRasterPos3f(0.0f, 0.0f, 0.0f); glPopMatrix(); - want_f(GL_CURRENT_RASTER_DISTANCE, 1, 0.5); + /* VPro: the library answers from host state and never asks the board; + the value is the raster vertex's eye-space z, unnegated, so -0.5 in + front of the eye. */ + want_f(GL_CURRENT_RASTER_DISTANCE, 1, vpro ? -0.5 : 0.5); end(); } (void)m; @@ -1362,6 +1365,7 @@ int main(int argc, char **argv) { const char *rn = (const char *)glGetString(GL_RENDERER); gr2 = rn && (!strncmp(rn, "GR", 2) || !strncmp(rn, "GU", 2)); + vpro = rn && !strncmp(rn, "VPRO", 4); } printf("glstate: %s %s %s (visual 0x%x)\n", glGetString(GL_VENDOR), glGetString(GL_RENDERER), glGetString(GL_VERSION), (unsigned)vi->visualid); diff --git a/test/gltest/pixtest.c b/test/gltest/pixtest.c index 80338df2b..5ee1c8f2f 100644 --- a/test/gltest/pixtest.c +++ b/test/gltest/pixtest.c @@ -115,7 +115,7 @@ static const struct { const char *name; const char *why; int gr2_only; } xfails[ "libGLcore EXPRESS's stencil Fetch masks the depth word with 0xF; z20 s4 keeps the stencil above Z (bits 23:20; inferred from its depth reads)", 1 }, }; -static int gr2; +static int gr2, vpro; static const char *xfail_why(const char *name) { @@ -613,9 +613,29 @@ static double fb_comp(const unsigned char *px, int comp) return 0; } +/* VPro: the library's host conversion steps the rows of an unpacked read by + its own stride, ceil(w * bpp / m) * m with bpp the pixel's size and + m = max(alignment, bpp), where GL pads the row to the alignment. They differ + only when bpp does not divide the alignment and the width makes w * bpp + miss a multiple of it: 3 shorts (bpp 6) at width 13 give 78-byte rows + against GL's 80. The board's data is right; the library lays it out so + (seen on the VPro RGB short read-back). Returns 0 where GL's layout + holds. Only the default pack state is covered. */ +static int vpro_read_stride(const Fmt *f, const Type *t, const Store *s, int w) +{ + int bpp, m; + if (!vpro || t->npacked || f->n != 3 || t->size != 2 || s->alignment != 4 || s->row_length || + s->skip_pixels || s->skip_rows) + return 0; + bpp = f->n * t->size; + m = bpp > s->alignment ? bpp : s->alignment; + return m * ((w * bpp + m - 1) / m); +} + static void read_case(const Fmt *f, const Type *t, const Store *s, int w, int h, const char *tag) { int epg = t->npacked ? 1 : f->n; + int lib_stride = vpro_read_stride(f, t, s, w); int nb = image_bytes(s, w, h, epg, t->size), i, j, k; unsigned char *buf, *fb; if (!begin("read/%s/%s%s", f->name, t->name, tag)) @@ -641,7 +661,8 @@ static void read_case(const Fmt *f, const Type *t, const Store *s, int w, int h, /* Pixel values. */ for (j = 0; j < h && !cur_bad; j++) for (i = 0; i < w && !cur_bad; i++) { - unsigned char *p = buf + px_offset(s, w, i, j, epg, t->size); + unsigned char *p = buf + (lib_stride ? j * lib_stride + i * epg * t->size + : px_offset(s, w, i, j, epg, t->size)); const unsigned char *px = fb + (j * w + i) * 4; if (t->npacked) { unsigned int v = (unsigned int)get_elem(p, t, s->swap); @@ -668,7 +689,8 @@ static void read_case(const Fmt *f, const Type *t, const Store *s, int w, int h, unsigned char *mark = calloc(nb, 1); for (j = 0; j < h; j++) for (i = 0; i < w; i++) - memset(mark + px_offset(s, w, i, j, epg, t->size), 1, epg * t->size); + memset(mark + (lib_stride ? j * lib_stride + i * epg * t->size + : px_offset(s, w, i, j, epg, t->size)), 1, epg * t->size); for (i = 0; i < nb; i++) if (!mark[i] && buf[i] != 0xcd) { note("byte %d outside the pixels written (0x%02x)", i, buf[i]); @@ -1337,6 +1359,29 @@ static void t_depth_colour(void) } } +/* VPro: the value a depth takes through the library's host path. The word + holds 2S * z with 2S = 2^33 - 2^15 (the depth range's far value), so the + library multiplies by 2S, keeps the single's exponent and top 18 mantissa + bits (truncating the rest), then on readback refills the low five mantissa + bits from the top five and divides by 2S. 2S is a hair under a + power of two, so the product's mantissa sits about one 18-bit step below + z's and the truncation lands one word lower than truncating z itself. + Other boards expect the value unchanged. */ +static float depth_expect(float z) +{ + union { float f; unsigned int u; } v; + const float two_s = 8589901824.0f; + if (!vpro) + return z; + v.f = z * two_s; + v.u &= ~0x1fu; + v.u |= (v.u >> 18) & 0x1f; + /* A depth read is a window depth, which GL keeps within [0, 1]; the far + plane's word, 2S, decodes a few steps over 2S and reads back as 1. */ + v.f /= two_s; + return v.f > 1.0f ? 1.0f : v.f; +} + static void t_depth_stencil(void) { int w = 13, h = 5, i, j; @@ -1360,8 +1405,8 @@ static void t_depth_stencil(void) for (i = 0; i < w * h + 8; i++) zout[i] = -7.0f; glReadPixels(40, 50, w, h, GL_DEPTH_COMPONENT, GL_FLOAT, zout); for (i = 0; i < w * h; i++) - if (fabs(zout[i] - zin[i]) > 1.0 / (1 << 20)) - note("(%d,%d) depth %.7f want %.7f", i % w, i / w, zout[i], zin[i]); + if (fabs(zout[i] - depth_expect(zin[i])) > 1.0 / (1 << 20)) + note("(%d,%d) depth %.7f want %.7f", i % w, i / w, zout[i], depth_expect(zin[i])); if (zout[w * h] != -7.0f) note("wrote past the image"); check_border(40, 50, w, h, 2); @@ -1388,8 +1433,8 @@ static void t_depth_stencil(void) ztol = dbits > 0 && dbits <= 20 ? 2.0 / (1 << dbits) : 1.0 / (1 << 20); for (i = 0; i < w * h; i++) { double got = zu[i] / 4294967295.0; - if (fabs(got - zin[i]) > ztol) - note("(%d,%d) depth %08x = %.7f want %.7f", i % w, i / w, zu[i], got, zin[i]); + if (fabs(got - depth_expect(zin[i])) > ztol) + note("(%d,%d) depth %08x = %.7f want %.7f", i % w, i / w, zu[i], got, depth_expect(zin[i])); } end(); } @@ -1410,8 +1455,8 @@ static void t_depth_stencil(void) reset_state(); glReadPixels(40, 50, w, h, GL_DEPTH_COMPONENT, GL_FLOAT, zout); for (i = 0; i < w * h; i++) - if (fabs(zout[i] - (zin[i] * 0.5 + 0.25)) > 1.0 / (1 << 20)) - note("(%d,%d) depth %.7f want %.7f", i % w, i / w, zout[i], zin[i] * 0.5 + 0.25); + if (fabs(zout[i] - depth_expect(zin[i] * 0.5f + 0.25f)) > 1.0 / (1 << 20)) + note("(%d,%d) depth %.7f want %.7f", i % w, i / w, zout[i], depth_expect(zin[i] * 0.5f + 0.25f)); end(); } } @@ -1431,8 +1476,8 @@ static void t_depth_stencil(void) glDisable(GL_DEPTH_TEST); glReadPixels(100, 60, w, h, GL_DEPTH_COMPONENT, GL_FLOAT, zout); for (i = 0; i < w * h; i++) - if (fabs(zout[i] - zin[i]) > 1.0 / (1 << 20)) - note("(%d,%d) depth %.7f want %.7f", i % w, i / w, zout[i], zin[i]); + if (fabs(zout[i] - depth_expect(zin[i])) > 1.0 / (1 << 20)) + note("(%d,%d) depth %.7f want %.7f", i % w, i / w, zout[i], depth_expect(zin[i])); end(); } } @@ -1485,6 +1530,18 @@ static void t_depth_stencil(void) glReadPixels(40, 50, w, h, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, sout); for (i = 0; i < w * h; i++) { int want = (((sin[i] << 1) + 3) & 0x0f) & smask; + if (vpro) { + /* VPro: the library does shift and offset on the host and + runs a stencil download under its default block, whose + write mask is all ones, so glStencilMask(0x0f) never + reaches it. The index is + stored whole: 219 for 108. The board gets value << 16 + and what it does with bits above 8 is unknown there, so + such pixels are not checked. */ + want = (sin[i] << 1) + 3; + if (want > 0xff) + continue; + } if (sout[i] != want) note("(%d,%d) stencil %d want %d", i % w, i / w, sout[i], want); } @@ -2728,6 +2785,7 @@ int main(int argc, char **argv) { const char *rn = (const char *)glGetString(GL_RENDERER); gr2 = rn && (!strncmp(rn, "GR", 2) || !strncmp(rn, "GU", 2)); + vpro = rn && !strncmp(rn, "VPRO", 4); } ext = (const char *)glGetString(GL_EXTENSIONS); ext_abgr = ext && strstr(ext, "GL_EXT_abgr") != 0;