diff --git a/CHANGELOG.md b/CHANGELOG.md index 01864cb6..e0cd44cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,54 @@ hashes are given where a change is easiest to understand by reading the commit. produced. (`src/ci.rs`, `src/gfx_display.rs`) ### SGI Octane (IP30) +- **IMPACT on the Octane passes the GL pixel and state suites.** On IRIX + 6.5.22 pixtest went from 52 to 366 passing cases (the two left fail on the + Indigo2 too) and glstate from 393 to 394. The Octane's microcode keeps the + pixel pipeline's state blocks 0x8C words higher than the Indigo2's, so + glDrawPixels drew RGB images as RGBA; the colour matrix reads back where + the Indigo2's does. Both boards: RGBA 12,12,12 has no alpha planes, and + glReadPixels now returns alpha 1 from it instead of colour bits. +- **No more "hq idle wait timeout" killing the X server.** The kernel waits + for an idle board by counting polls of a status register, and a context + switch behind long drawing ran out of polls. A status poll of a busy board + now gives it up to 100 µs first, so the wait is counted in work done. +- **Double-buffered GL on the Octane's IMPACT no longer shows half-drawn + frames** (the octahedra / `bongo` saver flickered and tore). The Octane + kernel flips buffers at the retrace after the swap is reported, without + waiting for the board to go idle; the swap was reported when the command + processor reached it, while the raster engine was still drawing the + frame. It is now reported once the frame is drawn. The Indigo2 kernel + drains the board before each swap, so it never showed this. The display + also takes its frame just before the retrace now, as scanout does, so a + flip shows from the next frame. +- **The Octane's board is `[graphics] board`, as on every other machine**, + and `--graphics` works on it. `[octane] graphics` is retired: a config + that still sets it is refused with a message naming the new key. Unset, + the Octane gets the SI; Newport, XZ and Extreme are refused there, and + `board = "none"` (Octane only) leaves the port empty. +- **Perfetto traces on the Octane and the O2** (`--features perfetto`): + the `perfetto` monitor command was registered only on the Indy and + Indigo2. IMPACT traces now show frame capture and present, STATUS reads, + 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 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; + hinv and gfxinfo name them as on the real machine. glstate, pixtest and + blendtest pass on both as on the Indigo2's High and Maximum IMPACT; + iris-gui offers them too. +- **OpenGL selection works on IMPACT** (both machines): glRenderMode + (GL_SELECT) keeps the name stack in the GE and records hits, so picking + with glSelectBuffer returns the objects under the cursor instead of + nothing. glstate now passes all 398 cases on IRIX 6.5.22, and + `test/gltest/seltest` checks hit records against GL's rules. +- **GR2 takes its display frame before the retrace too**, for the same + reason as IMPACT. +- **`mgras status` and the log name the microcode the kernel loaded**: a + fingerprint of the HQ microcode and of each GE11 program, kept as they are + written, and the IRIX release when it is a known one. + - **User programs run from the caches, two to four times faster.** The Octane's IRIX maps user pages with cache attribute 4 (cacheable coherent exclusive), which the TLB read as uncached, so every user fetch, load and diff --git a/FEATURES.md b/FEATURES.md index 2b03b7af..1c5663c8 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -174,7 +174,7 @@ the first `[section]`, because TOML tables continue until the next header. | Key | Default | Values and behavior | |---|---|---| -| `graphics.board` | `newport` | newport, xz, extreme, solidimpact, highimpact, maximpact. | +| `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.heads` | `1` | 1 or 2; dual-head requires Newport. | | `graphics.resolution` | `guest` | guest, 1024x768, 1280x960, 1280x1024; presets require Newport. | @@ -326,13 +326,12 @@ DS1685's NVRAM, behind IOC3), `banks` (the MB in each HEART bank: 64, 128, 256 or 512, filled from bank 0 with no gap, 2048 in all), `serial_port_a` and `serial_port_b`/`serial_log` (IOC3's two ports, the first the console), `headless`, `[clock] fixed_mhz` (CP0 Count; 97.5 for the 195 MHz R10000), -`[network]`, `[[port_forward]]`, `nat_subnet` and `[scsi.N]` (`controller` 0 -is the internal bus, 1 the external). See [the Octane guide](docs/octane-ip30.md). +`[network]`, `[[port_forward]]`, `nat_subnet`, `[scsi.N]` (`controller` 0 +is the internal bus, 1 the external) and `[graphics] board`. See [the Octane guide](docs/octane-ip30.md). | Key | Default | Values and behavior | |---|---|---| | `octane.flash` | unset | Boot flash file, keeping the PROM's records between runs. Created from `prom` when missing and then used as it is, so delete it after changing `prom`; a file of another size is refused. Unset, the flash is in memory and every start is from `prom`. | -| `octane.graphics` | `"solidimpact"` | `solidimpact` or `none`. With `none` the machine runs on its serial console, with no window. | ### Hardware constraints and precedence diff --git a/HELP.md b/HELP.md index c007bbfc..03d2b894 100644 --- a/HELP.md +++ b/HELP.md @@ -547,7 +547,7 @@ profile = "indy_ip24" # "indigo2_ip22" (--ip22), "indigo2_ip28", or "o2_ip cpu = "r4400" # "r5000" or "r10000" (--cpu); use R10000 on IP28 [graphics] -board = "newport" # "newport", "xz", "extreme", "solidimpact" ("impact:solid"), "highimpact" ("impact:high"), "maximpact" ("impact:max") +board = "newport" # "newport", "xz", "extreme", "solidimpact" ("impact:solid"), "highimpact" ("impact:high"), "maximpact" ("impact:max"); the Octane: IMPACT (default solidimpact) or "none" heads = 1 # 2 = dual-head Newport (second REX3 in GIO slot 1) resolution = "guest" # or "1024x768", "1280x960", "1280x1024" diff --git a/clanker/INDEX.md b/clanker/INDEX.md index a920e4f3..aba2d138 100644 --- a/clanker/INDEX.md +++ b/clanker/INDEX.md @@ -219,6 +219,8 @@ IMPACT (MGRAS/GR4): HQ3/GE HLE, RSS raster, pixel memory, the raster JIT. Hardwa | [DESIGN.md](mgras/DESIGN.md) | design | MGRAS (IMPACT / GR4) design | | [ci-copypixels-unloaded-mask.md](mgras/ci-copypixels-unloaded-mask.md) | rule | Colour-index glCopyPixels runs a pipeline whose state is never loaded | | [dcb-device-numbers.md](mgras/dcb-device-numbers.md) | design | DCB device numbers: trust the kernel's dcbctrl writes, not header names | +| [display-snapshot-before-retrace.md](mgras/display-snapshot-before-retrace.md) | rule | Take the IMPACT display frame before the retrace pulse, not after | +| [swap-waits-for-raster.md](mgras/swap-waits-for-raster.md) | rule | Report SCHEDULE_SWAP only once the raster engines have drawn the frame | | [engine-pipeline-performance.md](mgras/engine-pipeline-performance.md) | measure | The CPU -> HQ -> raster pipeline: what made it slow | | [eram-state-ideas.md](mgras/eram-state-ideas.md) | open | GE state and ERAM: ideas for later (not built) | | [eram-transfers-come-once-per-ge.md](mgras/eram-transfers-come-once-per-ge.md) | rule | ERAM transfers come once per GE11 (Maximum Impact) | @@ -227,8 +229,10 @@ IMPACT (MGRAS/GR4): HQ3/GE HLE, RSS raster, pixel memory, the raster JIT. Hardwa | [gr4-jit.md](mgras/gr4-jit.md) | design | The raster JIT (`gr4-jit`) and its Cranelift pitfalls | | [guest-gl-benchmarks-miss-board-time.md](mgras/guest-gl-benchmarks-miss-board-time.md) | rule | Board-bound GL work slows the guest clock (and guest benchmarks lie) | | [header-errata.md](mgras/header-errata.md) | design | ignore/gr4 headers: known errata (fold back into the headers) | +| [hq4-microcode-moves-eram-state.md](mgras/hq4-microcode-moves-eram-state.md) | rule | The Octane's IMPACT microcode keeps ERAM state 0x8C words higher | | [mirai-pixel-zoom-and-scissor-query.md](mgras/mirai-pixel-zoom-and-scissor-query.md) | rule | Mirai: flipped glDrawPixels and the scissor-enable readback | | [pixel-path-pixtest.md](mgras/pixel-path-pixtest.md) | howto | IMPACT pixel path: pixtest and what it settled | +| [texture-readback-follows-libglcore.md](mgras/texture-readback-follows-libglcore.md) | rule | glGetTexImage of ALPHA, LUMINANCE, INTENSITY and RGB textures is libGLcore's, not the board's | | [window-origin-is-signed.md](mgras/window-origin-is-signed.md) | rule | The GL window origin's halves are signed | ## odyssey/ @@ -349,6 +353,7 @@ Running IRIX as a guest: install, network setup, shell quirks, X defaults. | Note | Kind | Title | |---|---|---| | [cdrom-changer-eject-no-mediad-remount.md](irix/cdrom-changer-eject-no-mediad-remount.md) | open | CD changer `cdrom-eject` does not trigger a `mediad` remount | +| [delay-loop-calibrated-cold.md](irix/delay-loop-calibrated-cold.md) | open | IRIX calibrates its microsecond delay loop cold, so JIT runs make every delay short (Octane "RAD UST not incrementing") | | [default-visual-24bit.md](irix/default-visual-24bit.md) | rule | The ip28 guest's X server defaults to 24-bit TrueColor | | [irix-csh-scratch-raw-device-gotchas-when-you-cant-use-iris-ci.md](irix/irix-csh-scratch-raw-device-gotchas-when-you-cant-use-iris-ci.md) | howto | IRIX csh + scratch raw-device gotchas (when you can't use iris-ci) | | [irix-install.md](irix/irix-install.md) | howto | Installing IRIX on iris (from scratch) | diff --git a/clanker/irix/delay-loop-calibrated-cold.md b/clanker/irix/delay-loop-calibrated-cold.md new file mode 100644 index 00000000..eef9884b --- /dev/null +++ b/clanker/irix/delay-loop-calibrated-cold.md @@ -0,0 +1,55 @@ +# IRIX calibrates its microsecond delay loop cold, so JIT runs make every delay short + +Status: open (measured 2026-10-28 on IP30, IRIX 6.5.22; no fix chosen). + +**Symptom.** Some Octane boots print +`ALERT: /hw/node/xtalk/15/pci/3: RAD UST not incrementing` before +"The system is coming up". In the guest's SYSLOG it showed up on 5 of about +62 boots. Two of four jitv2 test boots raised it. The alert is cosmetic: the driver prints +it and goes on, and audio works. + +**What the driver checks.** It reads the RAD UST counter (BAR0 0x30), spins a +10 µs kernel delay, reads it again and alerts if it moved by fewer than 3 +ticks. The kernel converts with 960 ns per tick (it divides by 0x3c0), so +`UST_NS_PER_TICK` in `src/dev/rad1.rs` must stay 960. The counter itself is +fine. The model enables it on 0x0e then 0x0a, and it counts. + +**Cause.** The kernel's microsecond delay is a counted spin loop (11 taken +`bgtz` to self per outer pass). The kernel calibrates it once, early in boot, +against the HEART counter, which runs on host time. The figure kept is the +nanoseconds per outer pass, stored as a word at 0xffffffffffffc4b8 (read it +with `mem 0xffffffffffffc4b8 1 w`). At that point the loop runs cold: + +| build | calibrated ns/pass | the same loop later, quiet guest | +|---|---|---| +| interpreter (default features) | 567 (0x237) | ~610 measured in user mode | +| jitv2 | 1148–1267 (0x47c–0x4f3) | ~180–300 (9 passes in 1.6–2.7 µs) | + +Under jitv2 every kernel `us_delay` therefore lasts about one fifth of what +was asked. For the RAD check, "10 µs" took 1.6–2.7 µs of host time, which is +2 or 3 UST ticks: right at the threshold, hence one boot in a few. + +On the interpreter the calibration matches the steady speed and the check +passes (29 ticks in a traced boot). + +**Wider impact.** This is not specific to RAD. Every driver delay and every +delay-counted timeout in the kernel is about 5× short on jitv2. Most of our +devices answer at once, so it rarely matters. Suspect it when a jitv2-only +spurious timeout appears on a device that completes asynchronously (SCSI, +AIC-7880, ISP1040). + +**Seen biting.** IMPACT's context switch waits for an idle board with up +to 6 x 15000 DMABUSY polls 10 us apart (about 0.9 s nominal, about 0.2 s +under jitv2). Behind long drawing it ran out, logged "mgras: hq idle wait +timeout" and "FlushHQ (rss_sh) failed for ctxsw", and the X server died. +Mitigated in the model: a STATUS or DMABUSY read of a busy board waits up +to 100 us for it (`Mgras::settle`), so polls count board work. + +**Reproduce.** On an overlay boot, trace BAR0 0x30 reads in `Rad1::bar_read` +silently (store the first read, print both at the second). An `eprintln!` or +a gdb breakpoint on the first read stretches the delay and hides the bug. + +**Fix options (none chosen).** Make the calibration see steady-state speed +(for instance, have jitv2 compile a page synchronously when it spins in a +tight self-loop at boot). Or accept the short delays and keep device models +tolerant of them. Do not slow the RAD UST down: the kernel hard-codes 960 ns. diff --git a/clanker/mgras/display-snapshot-before-retrace.md b/clanker/mgras/display-snapshot-before-retrace.md new file mode 100644 index 00000000..00462ae3 --- /dev/null +++ b/clanker/mgras/display-snapshot-before-retrace.md @@ -0,0 +1,26 @@ +# Take the IMPACT display frame before the retrace pulse, not after + +Status: rule (2026-10-28). + +**Symptom.** Double-buffered GL (octahedra, the `bongo` saver) tore on the +Octane, although a trace showed the guest correct: SCHEDULE_SWAP, the +kernel's XMAP `buf_select` flip about 7 ms later in its retrace handler, and +the next frame's drawing only after the flip. + +**Cause.** The refresh thread raised the retrace, slept 500 us, then copied +the visible frame (a millisecond or more). The handler's flip landed inside +that copy, and the client began drawing into the buffer being copied. + +**Rule.** Snapshot first, then pulse the retrace (`Mgras::refresh_loop`). +A flip made at retrace N shows from frame N+1, as scanout does. The STATUS +vertical-blank window (`begin_frame`) moves with the pulse. + +GR2 (`Gr2::display_loop`) had the same order (pulse, hold the vertical +status 700 us, then compose) and now composes first too. REX3 was already +right: it copies the framebuffer and the XMAP/VC2 state into the screen +caches, then raises VBLANK; it only presents the live framebuffer when no +pixel changed since the last copy. + +This was not the whole Octane tearing: the main cause was the swap being +reported before the raster engines finished the frame +(`swap-waits-for-raster.md`). diff --git a/clanker/mgras/gl-state-map.md b/clanker/mgras/gl-state-map.md index 7039b003..056874cb 100644 --- a/clanker/mgras/gl-state-map.md +++ b/clanker/mgras/gl-state-map.md @@ -5,8 +5,10 @@ default has, reads it back with the matching getter, and checks the answer. It is board-neutral (GL, GLX, Xlib only), so the same program maps GR2 later; only the decoders are per board. -As of 2026-10-13 on MaxImpact (6.5.22): 397 pass, 1 fail (selection-mode -name stack, not modelled), 15 XFAIL (libGLcore's own departures, listed in +As of 2026-10-28 on IRIX 6.5.22 (IP28 High IMPACT, Octane SI with texture +and MXI): 398 pass, 0 fail; the selection name stack is modelled +(`Gl::sel`, test/gltest/seltest.c). Earlier (2026-10-13, MaxImpact): 397 +pass, 1 fail (name stack), 15 XFAIL (libGLcore's own departures, listed in the test), 20 skipped (extensions IMPACT rejects). The map itself is in ignore/gr4/GE11.h "STATE READBACK". The model is diff --git a/clanker/mgras/hq4-microcode-moves-eram-state.md b/clanker/mgras/hq4-microcode-moves-eram-state.md new file mode 100644 index 00000000..211cd532 --- /dev/null +++ b/clanker/mgras/hq4-microcode-moves-eram-state.md @@ -0,0 +1,32 @@ +# The Octane's IMPACT microcode keeps ERAM state 0x8C words higher + +Status: rule (IRIX 6.5.22, measured 2026-10-28). Check again on 6.5.30. + +**Symptom.** On the Octane, pixtest failed 314 of 486 cases on IRIX 6.5.22: +glDrawPixels of RGB images came out shifted by one component (8-byte RGBA16 +pixels read from 6-byte RGB16 rows), and glGetFloatv(GL_COLOR_MATRIX_SGI) +read zeros. The same binaries pass on an IP28 with 6.5.22. + +**Cause.** libGLcore and the GE11 microcode put state where the microcode +wants it, and the HQ4 build moves some blocks: + +| What | Indigo2 (HQ3) | Octane (HQ4) | +|---|---|---| +| GL state from the stipple enables up (viewport 0xB91, clear values, ...) | I2 address | +0x8C | +| pixel pipeline ERAM blocks (expand state 0xD05, transfer size 0xDA8, texture load 0xDB1-0xDBB) | I2 address | +0x8C | +| pipeline program (0x13B), module states in GE memory (0x187-0x1EF), zoom (0x191) | same | same | +| colour matrix readback (0x1B9-0x1C8) | same | same | + +The GE11 program is identical on both 6.5.22 boards (fingerprint +`5467b0da1d106495`); only the HQ microcode differs (`mgras status`). + +**Rule.** `Hq4Quirks` translates to the Indigo2 layout on the way in: +`ge_state` for readbacks, `pixel_state_address` for PIXEL_STATE / +INIT_PIXEL_STATE_ERAM and for the module state addresses in a program write +(`Hq3Engine::canonical_pixel_state`). The pixel HLE and `PixMem` only know +the Indigo2 layout. A new release's layout should be told apart by the +microcode fingerprint (`src/dev/mgras/microcode.rs`), not by the board. + +**Also.** IRIX base 6.5 on the IP28 reads viewport, scissor, clear values and +raster state from addresses the 6.5.22 map does not answer (glstate: 31 +failures). Not mapped yet. diff --git a/clanker/mgras/swap-waits-for-raster.md b/clanker/mgras/swap-waits-for-raster.md new file mode 100644 index 00000000..d45723c8 --- /dev/null +++ b/clanker/mgras/swap-waits-for-raster.md @@ -0,0 +1,31 @@ +# Report SCHEDULE_SWAP only once the raster engines have drawn the frame + +Status: rule (2026-10-28). + +**Symptom.** Octane (IP30) IMPACT, double-buffered GL fullscreen (`bongo +-geometry 1280x1024+0+0`, the octahedra saver): flicker and tearing, frames +shown half drawn. Shots taken with the CPU stopped (`stop`, `mgras shot`) +showed the displayed half partly cleared and redrawn. The IP28 was fine. + +**Cause.** The HQ3 model raised the swap flag (FLAG_CP0, HQ4 pending bit +10) when SCHEDULE_SWAP reached it in `hq_fifo`. The frame's raster writes +could still be queued in `rss_fifo`. The Octane kernel's retrace handler +reads STATUS and flips XMAP buf_select once the swap is pending, even on +0x55 (HQ idle, board busy). The real pipe behind the HQ is shallow; ours +is not. The Indigo2 kernel flushes FIFOSTATUS to 0 (whole board idle, +`cfifo_level`) before each swap, so it never saw the race. + +Not the cause, checked: bank tracking. VALIDATE_BANKS alternates [0,0] and +[1,1], context restores carry the same bank in image words 16-17, and every +flip shows the other half. The gr4 raster JIT was not the cause either: +`mgras jit off` showed the same partial frames. + +**Fix.** `Hq3Sink::finish_raster` (drain `rss_fifo`) before `set_flags +(FLAG_CP0)` in the CP_SCHEDULE_SWAP handler. Perfetto: `swap waits raster` +on `IMPACT swap wait`. + +**Visual note.** On the Octane the SI offers no double-buffered 24-bit +visual, so GLUT's double-buffered RGB gets the 12-bit pair visual +(DRAW_BUFFER [2, 1], XMAP format 7, both buffers in one word); on the IP28 +it gets 24-bit [4, 1]. A bug in one of these paths shows on one machine +only. diff --git a/clanker/mgras/texture-readback-follows-libglcore.md b/clanker/mgras/texture-readback-follows-libglcore.md new file mode 100644 index 00000000..86bff1b0 --- /dev/null +++ b/clanker/mgras/texture-readback-follows-libglcore.md @@ -0,0 +1,20 @@ +# glGetTexImage of ALPHA, LUMINANCE, INTENSITY and RGB textures is libGLcore's, not the board's + +Status: rule (IRIX 6.5.22, 2026-10-28). + +**Symptom.** texsweep's `texread` lines report differing bytes for most +sized formats: ALPHA textures read back `ff000000` (alpha in red), RGB, +LUMINANCE and INTENSITY with alpha 0. The output is byte-for-byte the same +on an IP28 High IMPACT, an Octane SI with texture and an MXI. + +**Cause.** libGLcore asks the TE for the texture's own components (XFRMODE +format 9 for one, 0xA two, 7 RGB, 8 RGBA; the DMA lengths match) and then +expands them on the host by component count alone: one component to red, +two to red and alpha, anything missing 0. GL 1.1 would give (0,0,0,A), +(L,0,0,1) and (R,G,B,1). Details in the local `ignore/gr4/GE11.h`. + +**Rule.** Do not "fix" the model's texel readback (`Te1::read_line`) to +produce GL's answer: it returns what the board returns, and libGLcore's +expansion is what an application on real IRIX sees. Counts that are GL- +correct: RGBA formats 0, rgba2/rgba4 176, luminance-alpha 120 (plus 4-bit +rounding). diff --git a/clanker/skills/irix-gfx-loop/SKILL.md b/clanker/skills/irix-gfx-loop/SKILL.md index 6503e91f..1c1a71ba 100644 --- a/clanker/skills/irix-gfx-loop/SKILL.md +++ b/clanker/skills/irix-gfx-loop/SKILL.md @@ -53,7 +53,7 @@ the compiled path. If the user has a build they use, build into your own `test/gltest` holds board-neutral GL/X programs: `glprim` (one primitive per run, `--help` lists every option), `gltest` (scenes and `--bench`), -`pixtest`, `blendtest`, `glstate`, plus helpers `xgetpix` (read root-window +`pixtest`, `blendtest`, `glstate`, `seltest` (selection hit records), plus helpers `xgetpix` (read root-window pixels through XGetImage), `xclick`, `xresize`. `irix.sh` builds them with MIPSpro. diff --git a/debug.md b/debug.md index 66caeb74..68dbce44 100644 --- a/debug.md +++ b/debug.md @@ -382,7 +382,7 @@ in the `irix-gfx-loop` skill (`clanker/skills/irix-gfx-loop/SKILL.md`). | Command | Description | |---|---| -| `mgras [status]` | Board summary and inventory | +| `mgras [status]` | Board summary and inventory, with the microcode fingerprints (HQ, each GE11) and the IRIX release they came with when known | | `mgras hq` | HQ3 flags, enables, DMA and raster-interface registers, FIFOs | | `mgras gl` | The GL front end's current state | | `mgras probe ` / `mgras probe off` | Make GE readbacks in `lo..=hi` answer `value` (to map what libGLcore decodes) | diff --git a/docs/octane-ip30.md b/docs/octane-ip30.md index eeb77f02..c16dc677 100644 --- a/docs/octane-ip30.md +++ b/docs/octane-ip30.md @@ -20,8 +20,8 @@ example `playaiff`) comes out of the host's audio device; `no_audio` turns it off. `reboot` comes back to the login prompt. jitv2 builds run it too. iris-gui runs the Octane as well: choose "SGI Octane (IP30)" as the platform. -The editor fixes the CPU at the R10000, offers Solid IMPACT or no graphics -board, limits banks to 64 to 512 MB filled from bank 0, and asks for the IP30 +The editor fixes the CPU at the R10000, offers the three IMPACT boards or no +graphics board, limits banks to 64 to 512 MB filled from bank 0, and asks for the IP30 PROM image and an optional flash file. The Indy and Indigo2 NVRAM seeding and Ethernet-address row do not apply and are skipped. @@ -44,7 +44,9 @@ cpu = "r10000" [octane] flash = "flash-ip30.bin" # keep the PROM's flash records between runs -# graphics = "none" # no graphics board (default: "solidimpact") + +[graphics] +# board = "maximpact" # "solidimpact" (default), "highimpact", "maximpact" or "none" [[port_forward]] proto = "tcp" @@ -114,11 +116,20 @@ controller works and what each driver does with it is in ## Graphics -The graphics board is Solid IMPACT on widget 11: the same raster chips as -the Indigo2 IP28's Solid IMPACT behind the Octane's XIO host interface (the -HQ4), with interrupts and DMA through HEART. Its screen is the iris window +The graphics board sits on widget 11: the same raster chips as the Indigo2 +IP28's IMPACT boards behind the Octane's XIO host interface (the HQ4), with +interrupts and DMA through HEART. `[graphics] board` (or `--graphics`) picks +the board, as on the other machines: + +| Value | The PROM's name | GEs / REs | Texture | +|---|---|---|---| +| `solidimpact` (default) | SI | 1 / 1 | none | +| `highimpact` | SI with texture option | 1 / 1 | 4 TRAMs | +| `maximpact` | MXI | 2 / 2 | 4 TRAMs per RE | + +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. `[octane] graphics = "none"` leaves the port empty, and the machine +ports. `board = "none"` leaves the port empty, and the machine then runs on its serial console with no window. ## Networking diff --git a/docs/perfetto.md b/docs/perfetto.md index 14f1e155..cbb4b0b0 100644 --- a/docs/perfetto.md +++ b/docs/perfetto.md @@ -80,7 +80,11 @@ host thread's name. | Audio | `HAL2`, `hptimer` | Codec `arm`/`disarm`, `host underrun` (the host audio callback found the ring empty), and a `codec A pace` slice per pacing wake on the timer thread. | | Graphics | `REX3 display`, `REX3 busy wait`, `REX3 gfifo full`, `REX3 gfifo level` | `vblank`; `xmap mode write` (how IRIX swaps Newport buffers); the guest reading GFXBUSY from STATUS (waiting for idle); CPU stores retried on a full GFIFO; the FIFO level as STATUS reports it. `REX3-Processor` has a `GO` slice per primitive (`prim`); the refresh thread has `present`. | | Graphics | `GR2 display`, `GR2 busy wait`, `GR2 fifo full` | `vblank`; reads and stores the board answered with bus-busy. | -| Graphics | `IMPACT display`, `IMPACT busy wait`, `IMPACT fifo full` | `schedule swap` (GL's flip request reaching the GE), `buf select` (the kernel's flip at retrace), `retrace`; reads waiting for idle; stores stalled on the FIFO. `MGRAS-HQ3` / `MGRAS-RSS*` threads show `HQ3 busy` / `RSS busy` and host DMA slices. | +| Graphics | `IMPACT display`, `IMPACT busy wait`, `IMPACT fifo full` | `schedule swap` (GL's flip request reaching the GE), `draw bank` (the bank GL draws into, from VALIDATE_BANKS or a context image), `buf select` (the kernel's flip at retrace), `xmap mode write` (a window ID's display mode), `retrace`; reads waiting for idle; stores stalled on the FIFO. `MGRAS-HQ3` / `MGRAS-RSS*` threads show `HQ3 busy` / `RSS busy` and host DMA slices. | +| Graphics | `IMPACT capture`, `IMPACT present` | The refresh thread copying the shown frame out of the framebuffer (arg: raster entries still queued), and handing it to the window. | +| 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). | | 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/iris-gui/locales/en/config.ftl b/iris-gui/locales/en/config.ftl index 890920d6..b51e1d60 100644 --- a/iris-gui/locales/en/config.ftl +++ b/iris-gui/locales/en/config.ftl @@ -403,5 +403,5 @@ config-octane-subtitle = An R10000 at 195 MHz. Needs the IP30 PROM image; there config-octane-flash = Flash file config-octane-flash-tip = Keeps the PROM's saved records between runs. Created from the PROM image when missing. config-octane-graphics-none = None -config-octane-graphics-subtitle = The Octane models the Solid IMPACT board, or no graphics board and a serial console. +config-octane-graphics-subtitle = The Octane models IMPACT as SI (Solid), SI with texture option (High) and MXI (Maximum), or no graphics board and a serial console. config-ram-banks-octane-subtitle = Each bank is a pair of DIMMs of 64, 128, 256 or 512 MB. Fill the banks from bank 0 with no gaps. diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 7e31a248..4fe8191d 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -222,10 +222,12 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) octane_defaults(cfg); } let octane = cfg.machine.profile == MachineProfile::OctaneIp30; - // GR2 Extreme is Indigo2-only; moving to Indy keeps the closest board - // rather than leaving a config `validate()` would reject at Start. - if !cfg.graphics.board.supports(cfg.machine.profile) { - cfg.graphics.board = GraphicsBoard::Xz; + // A board the new machine does not take would fail `validate()` at + // Start: GR2 Extreme moving to Indy keeps the closest board (XZ); any + // other falls back to the machine's default. + let board = cfg.graphics_board(); + if !board.supports(cfg.machine.profile) { + cfg.graphics.board = (board == GraphicsBoard::Extreme && !octane).then_some(GraphicsBoard::Xz); } // Only IP28 can express 256/512 MB banks. Clamp them when switching to // IP22/IP24 so the new profile passes `validate()` at Start. @@ -351,7 +353,7 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) } else { show_board_picker(ui, cfg, mem_ctx.running); } - if !octane && cfg.graphics.board == GraphicsBoard::Newport { + if !octane && cfg.graphics_board() == GraphicsBoard::Newport { ui.horizontal(|ui| { ui.label(t!("config-newport-heads")); ui.add(egui::DragValue::new(&mut cfg.graphics.heads).range(1..=2).speed(0.1)); @@ -546,7 +548,7 @@ fn show_rtc_offset(ui: &mut Ui, off: &mut RtcOffset, running: bool) { } fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { - let before = cfg.graphics.board; + let before = cfg.graphics_board(); let mut choice = before; ui.horizontal(|ui| { ui.label(t!("config-graphics-board")); @@ -566,7 +568,7 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { // Every non-Newport choice is single-head with no VC2 presets; // clear those so the config still passes `validate()`. if choice != before { - cfg.graphics.board = choice; + cfg.graphics.board = Some(choice); if choice != GraphicsBoard::Newport { cfg.graphics.heads = 1; cfg.graphics.resolution = NewportResolution::Guest; @@ -591,24 +593,30 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { } } -/// The Octane's graphics port holds the Solid IMPACT board or nothing -/// (`[octane] graphics`); the other boards are not modelled there. +/// The Octane's graphics port holds an IMPACT board (Solid, High as the SI +/// with texture option, Maximum as the MXI) or nothing (`[graphics] board +/// = "none"`); the other boards are not modelled there. fn show_octane_graphics_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { - let fitted = !matches!(cfg.octane.graphics_board(), Ok(None)); + let fitted = cfg.graphics_board(); let mut choice = fitted; + let name = |b: GraphicsBoard| if b == GraphicsBoard::None { t!("config-octane-graphics-none") } else { b.label().to_string() }; ui.horizontal(|ui| { ui.label(t!("config-graphics-board")); ui.add_enabled_ui(!running, |ui| { ComboBox::from_id_salt("octane_graphics") - .selected_text(if choice { GraphicsBoard::SolidImpact.label().to_string() } else { t!("config-octane-graphics-none") }) + .selected_text(name(choice)) .show_ui(ui, |ui| { - ui.selectable_value(&mut choice, true, GraphicsBoard::SolidImpact.label()); - ui.selectable_value(&mut choice, false, t!("config-octane-graphics-none")); + for b in [GraphicsBoard::SolidImpact, GraphicsBoard::HighImpact, GraphicsBoard::MaxImpact] { + ui.selectable_value(&mut choice, b, b.label()); + } + ui.selectable_value(&mut choice, GraphicsBoard::None, t!("config-octane-graphics-none")); }); }); }); if choice != fitted { - cfg.octane.graphics = Some(if choice { "solidimpact" } else { "none" }.into()); + cfg.graphics.board = Some(choice); + cfg.graphics.heads = 1; + cfg.graphics.resolution = NewportResolution::Guest; } if running { ui.label( @@ -633,13 +641,16 @@ fn octane_defaults(cfg: &mut MachineConfig) { let total: u32 = banks.iter().sum(); cfg.banks = crate::dialogs::new_machine::distribute_ram(total.clamp(64, 2048), MachineProfile::OctaneIp30); } - if cfg.octane.graphics_board().is_err() { - cfg.octane.graphics = None; + if !cfg.graphics_board().supports(MachineProfile::OctaneIp30) { + cfg.graphics.board = None; } + cfg.graphics.heads = 1; + cfg.graphics.resolution = NewportResolution::Guest; + cfg.octane.graphics = None; } fn show_resolution_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { - let newport = cfg.graphics.board == GraphicsBoard::Newport && !cfg.headless; + let newport = cfg.graphics_board() == GraphicsBoard::Newport && !cfg.headless; ui.horizontal(|ui| { let label = ui.label(t!("config-resolution")); if !newport { diff --git a/iris-gui/src/dialogs/new_machine.rs b/iris-gui/src/dialogs/new_machine.rs index feb008b7..640435d6 100644 --- a/iris-gui/src/dialogs/new_machine.rs +++ b/iris-gui/src/dialogs/new_machine.rs @@ -354,7 +354,7 @@ impl NewMachineDialog { // default straight to IMPACT so a fresh machine has a working // display instead of a blank Newport head. if self.profile == MachineProfile::Indigo2Ip28 { - cfg.graphics.board = iris::config::GraphicsBoard::SolidImpact; + cfg.graphics.board = Some(iris::config::GraphicsBoard::SolidImpact); } // Keep the PROM's flash records between runs. if self.profile == MachineProfile::OctaneIp30 { diff --git a/iris.toml b/iris.toml index 63989616..70cd2253 100644 --- a/iris.toml +++ b/iris.toml @@ -224,7 +224,8 @@ mode = "nat" # Graphics: board "newport" (default), or GR2 "xz" (Indy or Indigo2) / # "extreme" (Indigo2) - GR2 is in bring-up (PROM + single-user textport); -# "solidimpact", "highimpact", "maximpact" +# "solidimpact", "highimpact", "maximpact". The Octane takes only the +# IMPACT boards (default "solidimpact") or "none". # heads 1 or 2; resolution "guest" (IRIX/PROM programs VC2), "1024x768", # "1280x960" or "1280x1024". #[graphics] diff --git a/src/config.rs b/src/config.rs index 941356e8..22972c44 100644 --- a/src/config.rs +++ b/src/config.rs @@ -570,6 +570,10 @@ pub enum GraphicsBoard { /// Maximum IMPACT (2 GE11, 2 RE4). #[clap(name = "maximpact", alias = "impact:max", alias = "max_impact")] MaxImpact, + /// No graphics board: the machine runs on its serial console. Octane + /// only. + #[clap(name = "none")] + None, } impl GraphicsBoard { @@ -590,6 +594,7 @@ impl GraphicsBoard { Self::SolidImpact => "IMPACT Solid", Self::HighImpact => "IMPACT High", Self::MaxImpact => "IMPACT Maximum", + Self::None => "None", } } @@ -601,6 +606,7 @@ impl GraphicsBoard { Self::SolidImpact => "solidimpact", Self::HighImpact => "highimpact", Self::MaxImpact => "maximpact", + Self::None => "none", } } @@ -608,9 +614,23 @@ impl GraphicsBoard { matches!(self, Self::SolidImpact | Self::HighImpact | Self::MaxImpact) } - /// Whether `validate()` accepts this board on `profile`. + /// 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 { - self != Self::Extreme || profile == MachineProfile::Indigo2Ip22 + match profile { + MachineProfile::OctaneIp30 => self.is_impact() || self == Self::None, + MachineProfile::Indigo2Ip22 => self != Self::None, + _ => !matches!(self, Self::Extreme | Self::None), + } + } + + /// The board a machine gets when `[graphics] board` is not set. + pub fn default_for(profile: MachineProfile) -> Self { + match profile { + MachineProfile::OctaneIp30 => Self::SolidImpact, + _ => Self::Newport, + } } } @@ -626,8 +646,9 @@ 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), + "none" => Ok(Self::None), _ => Err(format!( - "invalid graphics board \"{s}\" (valid: newport, xz, extreme, solidimpact, highimpact, maximpact, impact:solid, impact:high, impact:max)" + "invalid graphics board \"{s}\" (valid: newport, xz, extreme, solidimpact, highimpact, maximpact, impact:solid, impact:high, impact:max, none)" )), } } @@ -662,11 +683,14 @@ impl std::fmt::Display for GraphicsBoard { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct GraphicsSection { - /// Graphics board: Newport, GR2 XZ/Extreme, or IMPACT Solid/High/Maximum. - /// Extreme is restricted to IP22; non-Newport boards require one head + /// Graphics board: Newport, GR2 XZ/Extreme, IMPACT Solid/High/Maximum, + /// or none (Octane only). Unset: the machine's default + /// ([`GraphicsBoard::default_for`]); read it with + /// [`MachineConfig::graphics_board`]. Extreme is restricted to IP22, + /// the Octane to IMPACT or none; non-Newport boards require one head /// and Guest resolution. See [`MachineConfig::validate`]. - #[serde(default)] - pub board: GraphicsBoard, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub board: Option, /// Newport heads to emulate (1 or 2). Dual-head maps a second REX3 at GIO slot 1. #[serde(default = "default_graphics_heads")] pub heads: u8, @@ -689,7 +713,7 @@ fn default_graphics_heads() -> u8 { 1 } impl Default for GraphicsSection { fn default() -> Self { Self { - board: GraphicsBoard::default(), + board: None, heads: default_graphics_heads(), resolution: crate::dev::ng1::vc2_timings::NewportResolution::default(), } @@ -890,30 +914,16 @@ pub struct OctaneSection { /// flash lives in memory only and every start is from `prom`. #[serde(default, skip_serializing_if = "Option::is_none")] pub flash: Option, - /// The board in the first graphics port: `"solidimpact"` (the default) - /// or `"none"`. As on the O2, `headless` changes only the window; the - /// board stays, and the PROM's `console` variable picks the console. - #[serde(default, skip_serializing_if = "Option::is_none")] + /// Retired: the board is `[graphics] board` on every machine. Kept + /// only so an old config gets an error naming the new key. + #[serde(default, skip_serializing)] pub graphics: Option, } impl OctaneSection { - /// The graphics board `graphics` names, or None for `"none"`. - pub fn graphics_board(&self) -> Result, String> { - match self.graphics.as_deref().map(str::trim) { - None => Ok(Some(GraphicsBoard::SolidImpact)), - Some(s) if s.eq_ignore_ascii_case("none") => Ok(None), - Some(s) => match s.parse::() { - Ok(GraphicsBoard::SolidImpact) => Ok(Some(GraphicsBoard::SolidImpact)), - Ok(b) => Err(format!("octane.graphics \"{s}\": {} is not modelled on the Octane (valid: solidimpact, none)", b.label())), - Err(e) => Err(format!("octane.graphics: {e}")), - }, - } - } - /// Left out of a written config when nothing in it was set. fn is_default(&self) -> bool { - self.flash.is_none() && self.graphics.is_none() + self.flash.is_none() } } @@ -1855,6 +1865,12 @@ impl MachineConfig { } /// Validate bank sizes, returns a description of any errors. + /// The graphics board: `[graphics] board`, or the machine's default + /// when it is not set (Solid IMPACT on the Octane, Newport elsewhere). + pub fn graphics_board(&self) -> GraphicsBoard { + self.graphics.board.unwrap_or_else(|| GraphicsBoard::default_for(self.machine.profile)) + } + pub fn validate(&self) -> Result<(), String> { if self.graphics.heads != 1 && self.graphics.heads != 2 { return Err(format!( @@ -1862,13 +1878,21 @@ impl MachineConfig { self.graphics.heads )); } - if self.graphics.board != GraphicsBoard::Newport { - let name = self.graphics.board.as_str(); - if self.graphics.board == GraphicsBoard::Extreme && self.machine.profile != MachineProfile::Indigo2Ip22 { + let board = self.graphics_board(); + if board != GraphicsBoard::Newport { + let name = board.as_str(); + if board == GraphicsBoard::Extreme && self.machine.profile != MachineProfile::Indigo2Ip22 { return Err( "graphics.board \"extreme\" is only valid on Indigo2 (machine.profile = indigo2_ip22)".into(), ); } + 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)") + } else { + format!("graphics.board \"{name}\" is only valid on the Octane") + }); + } if self.graphics.heads != 1 { return Err(format!("graphics.board \"{name}\" does not support dual-head (graphics.heads must be 1)")); } @@ -1948,7 +1972,15 @@ impl MachineConfig { return Err(format!("bank{i} size {sz} MB is invalid for an Octane (valid: 64, 128, 256, 512)")); } } - self.octane.graphics_board()?; + if let Some(old) = &self.octane.graphics { + return Err(format!( + "octane.graphics has moved: set the board in [graphics] as board = \"{}\"", + old.trim() + )); + } + if self.graphics.board == Some(GraphicsBoard::Newport) { + return Err("graphics.board \"newport\" is not modelled on the Octane (valid: solidimpact, highimpact, maximpact, none)".into()); + } } else { for (i, &sz) in self.banks.iter().enumerate().take(bank_count) { if !VALID_BANK_SIZES.contains(&sz) { @@ -2357,7 +2389,7 @@ impl Cli { if let Some(cpu) = self.cpu { cfg.machine.cpu = cpu; } if let Some(cache) = self.cache { cfg.machine.cache = cache; } - if let Some(b) = self.graphics { cfg.graphics.board = b; } + if let Some(b) = self.graphics { cfg.graphics.board = Some(b); } if self.scale2x { cfg.scale = 2; } if self.headless { cfg.headless = true; } if self.no_audio { cfg.no_audio = true; } @@ -2498,14 +2530,31 @@ mod export_tests { #[test] fn octane_graphics_defaults_to_solid_impact() { + let octane = |t: &str| -> MachineConfig { + let mut c: MachineConfig = + toml::from_str(&format!("banks = [128, 0, 0, 0]\n[machine]\nprofile = \"octane_ip30\"\ncpu = \"r10000\"\n{t}")) + .expect("parse"); + c.scsi.clear(); + c + }; + let cfg = octane(""); + 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)] { + let cfg = octane(&format!("[graphics]\nboard = \"{t}\"")); + assert_eq!(cfg.graphics_board(), b, "{t}"); + cfg.validate().expect(t); + } + for t in ["xz", "extreme", "newport"] { + assert!(octane(&format!("[graphics]\nboard = \"{t}\"")).validate().is_err(), "{t} is not modelled on the Octane"); + } + let err = octane("[octane]\ngraphics = \"maximpact\"").validate().unwrap_err(); + assert!(err.contains("[graphics]"), "the retired key names its replacement: {err}"); + // Elsewhere the default stays Newport, and none is the Octane's alone. let cfg: MachineConfig = toml::from_str("").expect("parse"); - assert_eq!(cfg.octane.graphics_board(), Ok(Some(GraphicsBoard::SolidImpact))); - let cfg: MachineConfig = toml::from_str("[octane]\ngraphics = \"none\"").expect("parse"); - assert_eq!(cfg.octane.graphics_board(), Ok(None)); - let cfg: MachineConfig = toml::from_str("[octane]\ngraphics = \"impact:solid\"").expect("parse"); - assert_eq!(cfg.octane.graphics_board(), Ok(Some(GraphicsBoard::SolidImpact))); - let cfg: MachineConfig = toml::from_str("[octane]\ngraphics = \"maximpact\"").expect("parse"); - assert!(cfg.octane.graphics_board().is_err(), "only Solid IMPACT is modelled"); + assert_eq!(cfg.graphics_board(), GraphicsBoard::Newport); + assert!(!GraphicsBoard::None.supports(MachineProfile::IndyIp24)); } #[test] diff --git a/src/dev/gr2/gr2disp.rs b/src/dev/gr2/gr2disp.rs index cca78215..eb9f0826 100644 --- a/src/dev/gr2/gr2disp.rs +++ b/src/dev/gr2/gr2disp.rs @@ -1,10 +1,11 @@ //! GR2 display thread: vertical retrace, composition, presentation. //! -//! Every ~16.7 ms the thread pulses the retrace line. The IOC latches it until -//! the host acknowledges: PORT_CONFIG on Indigo2, HPC3_GEN_CONTROL on Indy -//! (GR2.h "INTERRUPTS"). When something visible changed, or on a periodic -//! heartbeat for the status bar, it composes VRAM into `screen.fb_rgb` -//! (`prebuilt`) and presents through the installed renderer. +//! Every ~16.7 ms the thread composes VRAM into `screen.fb_rgb` (`prebuilt`) +//! when something visible changed, or on a periodic heartbeat for the status +//! bar, presents through the installed renderer, and then pulses the retrace +//! line, so a flip the guest makes in the blank shows from the next frame. +//! The IOC latches the retrace until the host acknowledges: PORT_CONFIG on +//! Indigo2, HPC3_GEN_CONTROL on Indy (GR2.h "INTERRUPTS"). use std::sync::atomic::Ordering; use std::thread; @@ -33,16 +34,6 @@ impl Gr2 { while self.running.load(Ordering::Relaxed) { let start = Instant::now(); - // Vertical retrace. The interrupt stays latched in the IOC until - // the guest clears it. The vertical status level (EXTIO SG_STAT_0) - // is held for the blanking interval (~40 of 1066 lines at 60 Hz), - // long enough for a polling CPU to see both edges. - if let Some(cb) = self.retrace_cb.lock().clone() { - cb(true); - thread::sleep(VBLANK); - cb(false); - } - let dirty = self.dirty.swap(false, Ordering::Acquire); let shot = self.screenshot_pending.load(Ordering::Relaxed); if dirty || shot || frames_since_render >= IDLE_HEARTBEAT_FRAMES { @@ -101,6 +92,20 @@ impl Gr2 { frames_since_render = frames_since_render.saturating_add(1); } + // Vertical retrace, after the frame is composed: scanout reads + // VRAM during the frame, and the guest flips buffers in the + // blank. Composed after the pulse, the copy raced the flip and + // the next frame's drawing into the buffer just shown. The + // interrupt stays latched in the IOC until the guest clears it. + // The vertical status level (EXTIO SG_STAT_0) is held for the + // blanking interval (~40 of 1066 lines at 60 Hz), long enough + // for a polling CPU to see both edges. + if let Some(cb) = self.retrace_cb.lock().clone() { + cb(true); + thread::sleep(VBLANK); + cb(false); + } + self.trace_flush(false); let elapsed = start.elapsed(); diff --git a/src/dev/mgras/dcb.rs b/src/dev/mgras/dcb.rs index f2822055..4ef6b771 100644 --- a/src/dev/mgras/dcb.rs +++ b/src/dev/mgras/dcb.rs @@ -230,6 +230,9 @@ impl Xmap { crs => { if crs == xmap::BUF_SELECT { crate::pf_instant!(crate::perfetto::events::MGRAS_BUF_SELECT, d); + } else if crs == xmap::MAIN_MODE || crs == xmap::OVERLAY_MODE { + crate::pf_instant!(crate::perfetto::events::MGRAS_XMAP_MODE, + (crs as u64) << 40 | (self.index as u64) << 32 | d as u64); } self.regs.insert(key(crs, self.index), d); self.autoinc(t); @@ -389,6 +392,7 @@ impl Vc3 { fn sram_write(&mut self, v: u16) { let a = self.regs[VC3_SRAM_POINTER] as usize & 0x7FFF; + crate::pf_instant!(crate::perfetto::events::MGRAS_VC3_SRAM, (a as u64) << 16 | v as u64); self.sram[a] = v; self.regs[VC3_SRAM_POINTER] = self.regs[VC3_SRAM_POINTER].wrapping_add(1); } @@ -406,8 +410,12 @@ impl Vc3 { (0, _) => { self.index = (d >> 16) & 0x1F; self.regs[self.index as usize] = d as u16; + crate::pf_instant!(crate::perfetto::events::MGRAS_VC3_WRITE, (self.index as u64) << 16 | (d & 0xFFFF) as u64); + } + (1, _) => { + self.regs[self.index as usize] = d as u16; + crate::pf_instant!(crate::perfetto::events::MGRAS_VC3_WRITE, (self.index as u64) << 16 | (d & 0xFFFF) as u64); } - (1, _) => self.regs[self.index as usize] = d as u16, (3, 4) => { self.sram_write((d >> 16) as u16); self.sram_write(d as u16); } (3, _) => self.sram_write(d as u16), _ => {} @@ -420,6 +428,7 @@ impl Vc3 { (1, _) if self.index as usize == VC3_CURRENT_LINE => { // Keep moving so vertical-blank polls terminate. self.line_counter = (self.line_counter + 1) % 1066; + crate::pf_instant!(crate::perfetto::events::MGRAS_VC3_LINE, self.line_counter); self.line_counter as u32 } (1, _) => self.regs[self.index as usize] as u32, diff --git a/src/dev/mgras/debug.rs b/src/dev/mgras/debug.rs index fe87cc8e..de2f5a45 100644 --- a/src/dev/mgras/debug.rs +++ b/src/dev/mgras/debug.rs @@ -456,8 +456,10 @@ impl Mgras { // SAFETY: read-only peek at the RSS (may tear while drawing). let rss = unsafe { &*self.rss.get() }; let f = self.front(); + let microcode = self.microcode_fingerprints(); let flags = self.all_flags(&f); writeln!(w, "IMPACT {:?} at {:#010x}", self.kind, MGRAS_SLOT_GFX_BASE)?; + writeln!(w, " microcode: {microcode}")?; writeln!(w, " flags {flags:#010x} [{}] interrupt enable {:#010x}", flag_names(flags), f.hq.interrupt_enable)?; writeln!(w, " fifo: hq {} rss {} idle={}", self.hq_fifo.len(), self.rss_fifo.len(), self.idle())?; writeln!(w, " DAC pixmask {:#04x} XMAP DID0 mode {:#x}", f.dcb.dac.pixmask(), f.dcb.xmap.main_mode(0))?; diff --git a/src/dev/mgras/disp.rs b/src/dev/mgras/disp.rs index cb222c3d..b32d1412 100644 --- a/src/dev/mgras/disp.rs +++ b/src/dev/mgras/disp.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use super::dcb::Dcb; use super::rss::{self, Rss}; use super::frame::{self, Frame}; -use super::{plain, Line, Mgras}; +use super::{microcode, plain, Line, Mgras}; /// The window ID a frame for the window at (`x`, `y`), `w` x `h` (screen /// coordinates, top-down) should be painted through: the commonest ID @@ -220,6 +220,12 @@ impl Mgras { let mut shown = (0usize, 0usize); let mut idle_frames = 0u32; let mut shots = 0u32; + // Microcode fingerprints: checked every two seconds, logged once + // the same value is seen twice in a row and differs from the last + // one logged. + let mut microcode_seen = microcode::Fingerprints::default(); + let mut microcode_logged = microcode::Fingerprints::default(); + let mut frames = 0u32; let mut last_rec_flush = std::time::Instant::now(); let mut snap = plain::boxed_zeroed::(); const PERSISTENT: u64 = crate::dev::ng1::rex3::Rex3::HB_LED_RED | crate::dev::ng1::rex3::Rex3::HB_LED_GREEN; @@ -247,16 +253,14 @@ impl Mgras { last_rec_flush = std::time::Instant::now(); } self.trace_flush(false); - self.begin_frame(); - // Video timing chip display control bit 0: retrace interrupts. - let retrace = self.front_view().dcb.vc3.regs[0x1E] & 1 != 0; - // Vertical retrace: a pulse per frame. The handler acknowledges - // nothing on the board, so the line must drop again by itself. - if retrace { - crate::pf_instant!(crate::perfetto::events::MGRAS_RETRACE); - self.set_line(Line::Retrace, true); - std::thread::sleep(std::time::Duration::from_micros(500)); - self.set_line(Line::Retrace, false); + frames = frames.wrapping_add(1); + if frames % 120 == 0 { + let fp = self.microcode_fingerprints(); + if fp.hq != 0 && fp == microcode_seen && fp != microcode_logged { + eprintln!("impact: microcode {fp}"); + microcode_logged = fp; + } + microcode_seen = fp; } let dirty = self.dirty.swap(false, Ordering::AcqRel); let shot = self.screenshot_pending.swap(false, Ordering::Relaxed); @@ -266,13 +270,16 @@ impl Mgras { let mut screen = self.screen.lock(); if dirty || shot { let screen = &mut *screen; - // The snapshot follows the retrace pulse, so the guest's - // retrace-time updates (window-ID tables, swaps) are in. + // The snapshot comes before this frame's retrace pulse: + // the last retrace's updates (window-ID tables, swaps) + // are in, and nothing draws into the front buffer now. // SAFETY: a read-only view of the framebuffer; tearing // tolerated. let rss = unsafe { &*self.rss.get() }; + crate::pf_begin!(crate::perfetto::events::MGRAS_CAPTURE, self.rss_fifo.len()); snap.snapshot(rss, &self.front_view().dcb); snap.compose(&mut screen.fb_rgb); + crate::pf_end!(crate::perfetto::events::MGRAS_CAPTURE); // The frame is already final RGB, so keep `rgba` (what CI // screenshots read) current without a renderer readback, // as GR2 does. This also makes screenshots work headless. @@ -292,7 +299,9 @@ impl Mgras { shown = (screen.width, screen.height); r.resize(shown.0, shown.1); } + crate::pf_begin!(crate::perfetto::events::MGRAS_PRESENT); r.present(&mut screen, &mut overlay, &mut status_bar, &mut sbtex, &stats, shot, None, None); + crate::pf_end!(crate::perfetto::events::MGRAS_PRESENT); } // The screenshot hotkey (Right Ctrl + Print Screen), as REX3 // and GR2 do: the frame just composed, written off-thread. @@ -306,6 +315,25 @@ impl Mgras { }); } } + // Vertical retrace, after the frame is taken: scanout reads the + // front buffer during the frame, and the kernel's retrace + // handler flips buffers (XMAP buffer select) during the blank. + // Taken after the pulse, the snapshot raced the flip: the + // handler flips and the client starts its next frame in the + // buffer just shown, while the snapshot was still copying it + // (octahedra tore). Now a flip at this retrace shows from the + // next frame, as on the board. + self.begin_frame(); + // Video timing chip display control bit 0: retrace interrupts. + let retrace = self.front_view().dcb.vc3.regs[0x1E] & 1 != 0; + // A pulse per frame. The handler acknowledges nothing on the + // board, so the line must drop again by itself. + if retrace { + crate::pf_instant!(crate::perfetto::events::MGRAS_RETRACE); + self.set_line(Line::Retrace, true); + std::thread::sleep(std::time::Duration::from_micros(500)); + self.set_line(Line::Retrace, false); + } if let Some(rest) = frame.checked_sub(start.elapsed()) { std::thread::sleep(rest); } diff --git a/src/dev/mgras/ge11.rs b/src/dev/mgras/ge11.rs index ee6e6337..18a60013 100644 --- a/src/dev/mgras/ge11.rs +++ b/src/dev/mgras/ge11.rs @@ -17,6 +17,8 @@ struct GeDiag { /// Which 32-bit word of the current 72-bit microcode line comes next. word: usize, ucode: [[u32; 3]; UCODE_LINES], + /// The downloaded program's fingerprint, kept as it is written. + hash: super::microcode::Running, } impl GeDiag { @@ -69,6 +71,11 @@ impl Ge11 { self.ge[ge & 1].ucode.get(i).copied() } + /// A fingerprint of engine `ge`'s downloaded program (0 if none). + pub fn fingerprint(&self, ge: usize) -> u64 { + self.ge[ge & 1].hash.get() + } + /// Whether `off` is one of the engines' diagnostic data or address ports. pub fn is_diag_port(off: u32) -> bool { host::GE_DIAG.iter().any(|&(d, a)| off == d || off == a) @@ -97,9 +104,11 @@ impl Ge11 { return false; } if ge.addr >= GeDiag::UCODE_BASE { - if let Some(line) = ge.ucode.get_mut((ge.addr - GeDiag::UCODE_BASE) as usize) { + let l = (ge.addr - GeDiag::UCODE_BASE) as usize; + if let Some(line) = ge.ucode.get_mut(l) { line[ge.word] = val; } + ge.hash.word(l * 3 + ge.word, val); ge.word += 1; if ge.word == 3 { ge.word = 0; diff --git a/src/dev/mgras/gl.rs b/src/dev/mgras/gl.rs index f81aba81..ada0096c 100644 --- a/src/dev/mgras/gl.rs +++ b/src/dev/mgras/gl.rs @@ -55,7 +55,7 @@ use crate::dev::gl::vertex::{self, Clip, Viewport, MAX_POLY}; type Wv = vertex::Wv<4, 1>; /// GL tokens the GE handles (numbers as in `hq3::token_name`). -mod tok { +pub(super) mod tok { pub const VERTEX4F: u32 = 0x000; /// Context set-up for an RGB visual, or a colour-index one (one word: /// 8 or 12, presumably the index bits). @@ -275,6 +275,20 @@ mod tok { pub const CLEAR_COLOR: u32 = 0x0BA; pub const CLEAR_DEPTH: u32 = 0x0BC; pub const CLEAR_STENCIL: u32 = 0x0BE; + /// Selection (glRenderMode(GL_SELECT)), as libGLcore drives the GE: + /// enter with SELECT_MODE (1 word), the name stack operations, leave + /// with RENDER_MODE, then read the hit records back word by word + /// (__MGR_RETURN_MODE with 0xFFFFFFFF as its second word). SELECT_NEXT + /// turns the reads to the second GE's records on a two-GE board, + /// SELECT_DONE ends a read and empties the records. + pub const INIT_NAMES: u32 = 0x0C4; + pub const LOAD_NAME: u32 = 0x0C5; + pub const POP_NAME: u32 = 0x0C6; + pub const PUSH_NAME: u32 = 0x0C7; + pub const RENDER_MODE: u32 = 0x0C9; + pub const SELECT_MODE: u32 = 0x0CB; + pub const SELECT_NEXT: u32 = 0x061; + pub const SELECT_DONE: u32 = 0x062; } /// Primitive kinds, BEGIN_* minus BEGIN_POINTS (GL_POINTS .. GL_POLYGON). @@ -390,6 +404,36 @@ pub struct Window { pub pp1winmode: u32, } +/// The GE's name stack, at most this deep (GL_MAX_NAME_STACK_DEPTH; +/// libGLcore caps its own count there). +const NAME_STACK: usize = 64; +/// Hit-record words kept. libGLcore reads the records back before its own +/// estimate of their size reaches about 2000 words; the context's slot +/// has room for 1536 beside the rest of `Gl`, so a burst of more than +/// about 300 one-name hits between reads loses the rest. +const SELECT_WORDS: usize = 1536; + +/// Selection state (GL_SELECT). Hit records are [name-stack epoch, name +/// count, z min, z max, names...], the depths floats in [0, 1] (libGLcore +/// scales them to 32 bits); the epoch orders and pairs records when two +/// GEs' lists are merged. Plain data, valid zeroed. +#[derive(Clone, Copy)] +#[repr(C)] +struct Select { + on: u32, + depth: u32, + names: [u32; NAME_STACK], + /// A primitive hit since the last record, and its depth range. + hit: u32, + zmin: f32, + zmax: f32, + epoch: u32, + len: u32, + /// Reads are of the second GE's records (empty: one list here). + second: u32, + buf: [u32; SELECT_WORDS], +} + /// GL state held by the GE. Plain data, valid zeroed (`ensure_init` sets /// GL's initial values on first use). #[derive(Clone, Copy)] @@ -572,6 +616,8 @@ pub struct Gl { point_smooth: u32, /// A texture read transfer is armed (READ_TEXTURE .. RESTORE_RSS). tex_reading: u32, + /// glRenderMode(GL_SELECT): the name stack and the hit records. + sel: Select, /// SAVE_RSS's fourth word: the planes the pixel operation transfers /// (traced, glReadPixels: 0 colour, 0x1C00000 depth, 0x1800000 stencil). pixel_planes: u32, @@ -789,6 +835,10 @@ impl Gl { // than its software state. Placeholder zeros erase write masks, // viewports and scissor boxes when applications restore them. 0x29 => Some(self.matrix_mode.wrapping_sub(0x1701)), + // GL_NAME_STACK_DEPTH, and the hit-record words held (for the + // GE whose records the reads are of). + 0xC59 => Some(self.sel.depth), + 0xC60 => Some(if self.sel.second != 0 { 0 } else { self.sel.len }), 0x83 => Some(0x899 + self.mv.top * 16), 0x896 => Some(0xA99 + self.proj.top * 16), 0x897 => Some(0xAB9 + self.tex.top * 16), @@ -866,6 +916,43 @@ impl Gl { let w0 = d.first().copied().unwrap_or(0); let on = (w0 != 0) as u32; match cmd { + tok::SELECT_MODE => { + self.sel.on = 1; + self.sel.depth = 0; + self.sel.hit = 0; + self.sel.len = 0; + self.sel.second = 0; + } + tok::RENDER_MODE => { + self.select_record(); + self.sel.on = 0; + } + tok::INIT_NAMES => { + self.select_record(); + self.sel.depth = 0; + } + tok::LOAD_NAME => { + self.select_record(); + if self.sel.depth > 0 { + self.sel.names[self.sel.depth as usize - 1] = w0; + } + } + tok::PUSH_NAME => { + self.select_record(); + if (self.sel.depth as usize) < NAME_STACK { + self.sel.names[self.sel.depth as usize] = w0; + self.sel.depth += 1; + } + } + tok::POP_NAME => { + self.select_record(); + self.sel.depth = self.sel.depth.saturating_sub(1); + } + tok::SELECT_NEXT => self.sel.second = 1, + tok::SELECT_DONE => { + self.sel.len = 0; + self.sel.second = 0; + } tok::MATRIX_MODE => self.matrix_mode = if d.is_empty() { 0x1700 } else { w0 }, tok::LOAD_IDENTITY => self.matrix_op(|s| s.load(math::IDENT)), tok::PUSH_MATRIX => self.matrix_op(|s| { s.push(); }), @@ -1363,6 +1450,50 @@ impl Gl { tc } + /// In GL_SELECT a primitive that survives clipping draws nothing and + /// makes a hit over its clipped vertices' window depths. + fn select_hit(&mut self, vs: &[Wv]) -> bool { + if self.sel.on == 0 { + return false; + } + for v in vs { + let z = (v.z / ZMAX).clamp(0.0, 1.0); + if self.sel.hit == 0 { + (self.sel.zmin, self.sel.zmax) = (z, z); + self.sel.hit = 1; + } else { + self.sel.zmin = self.sel.zmin.min(z); + self.sel.zmax = self.sel.zmax.max(z); + } + } + true + } + + /// The name stack is about to change (or selection ends): a hit since + /// the last change becomes a record, if it fits. + fn select_record(&mut self) { + let s = &mut self.sel; + if s.hit != 0 { + let n = s.depth as usize; + let at = s.len as usize; + if at + 4 + n <= SELECT_WORDS { + s.buf[at..at + 4].copy_from_slice(&[s.epoch, n as u32, s.zmin.to_bits(), s.zmax.to_bits()]); + s.buf[at + 4..at + 4 + n].copy_from_slice(&s.names[..n]); + s.len += 4 + n as u32; + } + s.hit = 0; + } + s.epoch = s.epoch.wrapping_add(1); + } + + /// Hit-record word `i` (__MGR_RETURN_MODE's select-buffer reads). + pub fn select_word(&self, i: u32) -> u32 { + if self.sel.second != 0 || i >= self.sel.len { + return 0; + } + self.sel.buf[i as usize] + } + fn vertex(&mut self, obj: [f32; 4], sink: &mut dyn Hq3Sink) { if self.prim == prim::NONE { return; @@ -1477,6 +1608,9 @@ impl Gl { if n < 3 || v[..n].iter().any(|w| w.ok == 0) { return; } + if self.select_hit(&v[..n]) { + return; + } // Window-space signed area: > 0 counter-clockwise (y up). let area: f32 = (0..n).map(|i| { let (a, b) = (&v[i], &v[(i + 1) % n]); @@ -1592,6 +1726,9 @@ impl Gl { if a.ok == 0 || b.ok == 0 { return; } + if self.select_hit(&[a, b]) { + return; + } // Flat lines take the segment's second vertex colour. if self.smooth == 0 { a.c = b.c; @@ -1668,6 +1805,9 @@ impl Gl { if v.oc != 0 || v.ok == 0 { return; } + if self.select_hit(&[v]) { + return; + } self.point_window(v, sink); } @@ -2285,6 +2425,7 @@ impl Gl { /// GE does not see, and every other frame then lands in the shown /// buffer. pub fn set_draw_bank(&mut self, bank: u32, sink: &mut dyn Hq3Sink) { + crate::pf_instant!(crate::perfetto::events::MGRAS_DRAW_BANK, bank); self.end_raster(sink); self.swapped = (bank & 1 == 0) as u32; self.banks_known = 1; diff --git a/src/dev/mgras/hq3.rs b/src/dev/mgras/hq3.rs index 9f7708e1..786be837 100644 --- a/src/dev/mgras/hq3.rs +++ b/src/dev/mgras/hq3.rs @@ -330,6 +330,12 @@ pub trait Hq3Board: Send + Sync { /// Whether the raster command `cmd` is dropped instead of executed; the /// reason is reported. fn drop_raster_write(&self, cmd: u32) -> Option; + /// A pixel-state address (PIXEL_STATE, INIT_PIXEL_STATE_ERAM, and the + /// module state addresses in the pipeline program) in the Indigo2 + /// microcode's layout, which the pixel HLE is written for. + fn pixel_state_address(&self, addr: u32) -> u32 { + addr + } } /// CPU-side HQ3 registers (the flags are atomic, in `Mgras`). Plain data, @@ -339,6 +345,8 @@ pub struct Hq3Regs { /// Registers not modelled otherwise, read back as written. pub regs: RegMap<256>, pub ucode: [u32; ((host::UCODE_END - host::UCODE) / 4) as usize], + /// The microcode's fingerprint, kept as it is written. + pub ucode_hash: super::microcode::Running, pub flag_enable: u32, pub interrupt_enable: u32, pub ge_readback: [u32; 2], @@ -367,6 +375,8 @@ pub trait Hq3Sink { /// A GL batch starts (true) or ends: the RSS saves / restores the raster /// registers the batch loads. fn gl_bracket(&mut self, _enter: bool) {} + /// Wait until the raster engines have drawn everything queued so far. + fn finish_raster(&mut self) {} /// System memory for DMA, if connected. fn sys_mem(&mut self) -> Option>; /// Something not modelled yet (reported once). @@ -872,7 +882,10 @@ impl Hq3Engine { let p = std::mem::take(&mut self.payload); match t { 0x089 => self.lut.load_table(&p), - _ => self.gl.pixel_state_block(&p), + _ => { + let p = self.canonical_pixel_state(&p); + self.gl.pixel_state_block(&p) + } } return; } @@ -1010,9 +1023,18 @@ impl Hq3Engine { } } } else if cmd == cmd::CP_SCHEDULE_SWAP { - // No retrace wait: the swap is reported done at once. The GE's - // front and back trade places for the drawing that follows. + // No retrace wait: the swap is reported as soon as the frame + // before it is drawn. The GE's front and back trade places for + // the drawing that follows. self.gl.swap_buffers(sink); + // The Octane kernel flips at the next retrace once this flag is + // up, without checking that the board is idle (it flips on a + // STATUS of HQ idle, board busy): behind the HQ the board's + // pipe is shallow. Our raster queue is deep, so the frame must + // be finished first or the flip shows it half drawn. The + // Indigo2 kernel drains the FIFO before the swap, so this + // costs it nothing. + sink.finish_raster(); self.stats.swaps += 1; self.stats.cp_tokens[cmd as usize] += 1; sink.set_flags(host::FLAG_CP0); @@ -1041,7 +1063,9 @@ impl Hq3Engine { // Addresses the GE HLE knows answer from its state; others read // 0 and are reported once each, so the layout can be mapped. let addr = data.first().copied().unwrap_or(0); - let v = sink.readback_probe(addr).or_else(|| self.ge_state(addr)).unwrap_or_else(|| { + let select = data.get(1) == Some(&0xFFFF_FFFF); + let v = if select { Some(self.gl.select_word(addr)) } else { sink.readback_probe(addr) }; + let v = v.or_else(|| self.ge_state(addr)).unwrap_or_else(|| { sink.note(format!("GE state readback at {addr:#x} ({:x?}) answered 0", data.get(1..).unwrap_or(&[]))); 0 }); @@ -1056,6 +1080,9 @@ impl Hq3Engine { sink.trace(format!(" converted ({conv:#x}): {:?}", floats.iter().map(|w| f32::from_bits(*w)).collect::>())); } self.gl.token(cmd, &floats, sink) + } else if matches!(cmd, super::gl::tok::PIXEL_STATE | super::gl::tok::INIT_PIXEL_STATE_ERAM) { + let data = self.canonical_pixel_state(data); + self.gl.token(cmd, &data, sink) } else { self.gl.token(cmd, data, sink) } @@ -1075,6 +1102,31 @@ impl Hq3Engine { } } + /// A pixel-state block (`Gl::pixel_state_block`'s words: 3 the + /// address, 6 the count, then the values) with its address, and the + /// module state addresses of a pipeline program, in the Indigo2 layout. + fn canonical_pixel_state<'a>(&self, d: &'a [u32]) -> std::borrow::Cow<'a, [u32]> { + let Some(b) = self.board.as_ref() else { return d.into() }; + if d.len() < 7 { + return d.into(); + } + let mut d = d.to_vec(); + let addr = d[3]; + d[3] = b.pixel_state_address(addr); + // The program's pairs are (entry, state address); the first pair's + // state is 0. + if addr <= super::pixpipe::PROGRAM { + let first = (super::pixpipe::PROGRAM - addr) as usize; + let n = (d[6] as usize).min(d.len() - 7); + for k in (first + 1..n).step_by(2) { + if d[7 + k] != 0 { + d[7 + k] = b.pixel_state_address(d[7 + k]); + } + } + } + d.into() + } + /// The GE state word a readback at `addr` answers, in the layout of the /// board's microcode. fn ge_state(&mut self, addr: u32) -> Option { diff --git a/src/dev/mgras/hq4.rs b/src/dev/mgras/hq4.rs index 1b404eb2..1dbccaa5 100644 --- a/src/dev/mgras/hq4.rs +++ b/src/dev/mgras/hq4.rs @@ -97,9 +97,26 @@ pub struct ImpactBoard { pub record_revision: &'static str, } -/// Solid IMPACT, the one board the raster model behind this front is built -/// as. Its values are inferred from an SI with texture, with the TRAM -/// taken away: no textureless board's values are on record. +/// How IRIX reads the two version bytes on the Octane's boards (the IP30 +/// kernel's board probe, and the PROM's names for what it finds): +/// +/// - `version1`: GE11 count in bits 1:0, board revision in 4:2, product in +/// 6:5. The Indigo2's products are 0 (High and Maximum) and 1 (Solid, +/// MG10); the Octane's are its own pair, GM10 and GM20. The SI's 0x45 +/// makes GM10 product 2; GM20 as 3 is an inference (the kernel treats +/// the two alike). +/// - `version0`: TRAMs on raster board A in bits 1:0 and on B in 3:2, each +/// 3 for none fitted and anything else for four. On these products the +/// kernel takes as many REs as GEs. +/// +/// The PROM names 1 GE without TRAMs "SI", with them "SI with texture +/// option", 2 GEs without "SSI", with them "MXI", each prefixed "E" when +/// the GE11 revision is above 2. The serial-number record names the base +/// board: GM10 for one RE (SI, plus a texture mezzanine for SI with +/// texture), GM20 for two (SSI, plus two mezzanines for MXI). +/// +/// Solid IMPACT. Its values are inferred from an SI with texture, with the +/// TRAM taken away: no textureless board's values are on record. pub const SOLID_IMPACT: ImpactBoard = ImpactBoard { name: "SI", version1: 0x45, @@ -114,6 +131,40 @@ pub const SOLID_IMPACT: ImpactBoard = ImpactBoard { record_revision: "U", }; +/// SI with texture option: the GM10 base board with a texture mezzanine, +/// four TRAMs on raster board A (`version0` 0x0C). The raster model is the +/// Indigo2's High IMPACT (one GE, one RE, a TE with TRAM). +pub const SI_TEXTURE: ImpactBoard = ImpactBoard { + name: "SI with texture option", + version0: 0x0C, + ..SOLID_IMPACT +}; + +/// MXI: the GM20 base board (two GEs, two REs) with two texture +/// mezzanines, four TRAMs behind each RE (`version0` 0x00). The raster +/// model is the Indigo2's Maximum IMPACT. The record's part number is not +/// on record: this one is made up and marked so (all zeros). +pub const MXI: ImpactBoard = ImpactBoard { + name: "MXI", + version1: 0x3 << 5 | 1 << 2 | 2, + version0: 0x00, + record_name: "GM20", + record_part: "030-0000-000", + ..SOLID_IMPACT +}; + +impl ImpactBoard { + /// The Octane board for a configured board kind, if there is one. + pub fn for_kind(kind: GraphicsBoard) -> Option { + match kind { + GraphicsBoard::SolidImpact => Some(SOLID_IMPACT), + GraphicsBoard::HighImpact => Some(SI_TEXTURE), + GraphicsBoard::MaxImpact => Some(MXI), + _ => None, + } + } +} + /// The serial-number chip's ROM serial and the record's barcode: made up /// and marked so (letter G, instance 0), so nothing mistakes them for a /// real board's. @@ -317,12 +368,18 @@ impl Hq3Board for Hq4Quirks { /// stack index, all below 0x84, do not move. Where between 0x84 and /// 0x1FA the shift starts is not known; this shifts every Indigo2 /// address from 0x84 up. An approximation, inferred from the two - /// layouts' known addresses. Pixel pipeline words answer at the address - /// they were loaded at. + /// layouts' known addresses. Pixel pipeline words answer where they + /// were loaded. The colour matrix (`0x1B9-0x1C8`) does not move + /// either: the 6.5.22 Octane libGLcore reads it where the Indigo2's + /// does (glstate `pixel/COLOR_MATRIX_SGI`), and no Indigo2 state sits + /// 0x8C below it. fn ge_state(&self, gl: &mut Gl, addr: u32) -> Option { const SHIFT: u32 = 0x8C; - let hq3 = if addr >= 0x84 + SHIFT { addr - SHIFT } else { addr }; - let v = gl.state_word_in(hq3, addr)?; + const COLOR_MATRIX: std::ops::RangeInclusive = 0x1B9..=0x1C8; + let hq3 = if addr >= 0x84 + SHIFT && !COLOR_MATRIX.contains(&addr) { addr - SHIFT } else { addr }; + // Pixel pipeline words are kept in the Indigo2 layout (see + // `pixel_state_address`). + let v = gl.state_word_in(hq3, self.pixel_state_address(addr))?; // The matrix-stack depths answer with an address in the GE's stack, // which moved with the rest. Some(if matches!(hq3, 0x83 | 0x896 | 0x897) { v + SHIFT } else { v }) @@ -337,6 +394,19 @@ impl Hq3Board for Hq4Quirks { fn drop_raster_write(&self, cmd: u32) -> Option { (cmd & 0x200 != 0).then(|| format!("raster command {cmd:#x} past register 0x1FF dropped")) } + + /// The pixel pipeline's ERAM blocks moved up by the same 0x8C: libGLcore + /// on the 6.5.22 Octane writes the expand state, the transfer size and + /// the texture-load blocks at 0xD91, 0xE34 and 0xE43 where the Indigo2's + /// uses 0xD05, 0xDA8 and 0xDB7 (traced, pixtest). GE memory below the + /// ERAM (the program at 0x13B, the module states at 0x187-0x1EF, the + /// zoom at 0x191) did not move. Without this the HLE missed the host + /// pixel shape, and glDrawPixels' rows reached the raster engine as + /// RGBA when the host sent RGB. + fn pixel_state_address(&self, addr: u32) -> u32 { + const SHIFT: u32 = 0x8C; + if addr >= super::pixpipe::ERAM_BASE + SHIFT { addr - SHIFT } else { addr } + } } /// The HQ4 state this front keeps. Plain values under one lock, which is @@ -394,6 +464,7 @@ impl Hq4 { } else if widget != WIDGET_HEART { "its destination is not HEART" } else { + crate::pf_instant!(crate::perfetto::events::MGRAS_HEART_PACKET, (vector as u64) << 1 | set as u64); out.push((vector, set)); return; }; @@ -452,11 +523,16 @@ struct GeUpload { read_odd: [bool; 2], load_mode: [bool; 2], active: usize, + /// Each slot's download fingerprint, kept as it is written. + hash: [super::microcode::Running; 2], } impl GeUpload { - fn control(&mut self, slot: usize, v: u32) { + /// A control write. When it ends a download, the fingerprint of the + /// words loaded. + fn control(&mut self, slot: usize, v: u32) -> Option { self.active = slot; + let finished = self.load_mode[slot].then(|| self.hash[slot].get()); if v & !1 == ge::LOAD { self.write_ptr[slot] = 0; self.read_ptr[slot] = 0; @@ -468,6 +544,7 @@ impl GeUpload { } self.load_mode[slot] = false; } + finished } /// False when the slot is full. @@ -481,6 +558,7 @@ impl GeUpload { } let w = &mut self.words[slot]; if p < w.len() { w[p] = v } else { w.push(v) } + self.hash[slot].word(p, v); self.write_ptr[slot] = p + 1; true } @@ -580,18 +658,31 @@ enum Phase { } impl ImpactXio { - /// A Solid IMPACT board at `widget` (one of the four graphics ports): + /// A Solid IMPACT board (see `with_board`). + pub fn new( + widget: u8, + irq: Arc, + mem: Arc, + heartbeat: Arc, + fasttick: Arc, + ) -> Arc { + Self::with_board(widget, GraphicsBoard::SolidImpact, irq, mem, heartbeat, fasttick) + } + + /// An IMPACT board of kind `kind` (Solid, High for SI with texture, + /// Maximum for MXI) at `widget` (one of the four graphics ports): /// its interrupts are vectors on `irq` (HEART), its DMA reaches `mem` /// (the bus masters' view of memory). `heartbeat` and `fasttick` feed /// the status bar, as for the Indigo2 board. `Device::start` starts it. - pub fn new( + pub fn with_board( widget: u8, + kind: GraphicsBoard, irq: Arc, mem: Arc, heartbeat: Arc, fasttick: Arc, ) -> Arc { - let board = SOLID_IMPACT; + let board = ImpactBoard::for_kind(kind).expect("an IMPACT board kind"); let shared = Arc::new(Shared { hq4: Mutex::new(Hq4::default()), nic: Mutex::new(None), @@ -601,7 +692,7 @@ impl ImpactXio { }); let lines: Arc = Arc::new(Lines(shared.clone())); let mgras = Mgras::new( - GraphicsBoard::SolidImpact, + kind, [IrqLine::new(lines.clone(), 0), IrqLine::new(lines.clone(), 1), IrqLine::new(lines, 2)], heartbeat, fasttick, @@ -831,7 +922,11 @@ impl ImpactXio { let slot = (off == ge::DATA_B || off == ge::CTRL_B) as usize; let mut s = self.shared.hq4.lock(); if off == ge::CTRL_A || off == ge::CTRL_B { - s.ge.control(slot, v); + if let Some(fp) = s.ge.control(slot, v) { + drop(s); + // GE 0 loads through slot B, GE 1 through slot A. + self.mgras.set_ge_upload_fingerprint(1 - slot, fp); + } } else if !s.ge.data(slot, v) { drop(s); self.shared.note("GE program download past the end of its slot".into()); @@ -860,6 +955,7 @@ impl ImpactXio { } hq4::PENDING => s.status_extra = v, hq4::ACK => { + crate::pf_instant!(crate::perfetto::events::MGRAS_HQ4_ACK, (off as u64) << 32 | v as u64); s.pending &= !v; if v & pending::FLAGS != 0 { s.first_ack = true; @@ -916,7 +1012,9 @@ impl ImpactXio { let level = w1::slot::slot(bus, pulse, sample); s.nic_control = v & NIC_WRITABLE | NIC_DONE | level as u32; } - hq4::RETRACE_ACK => {} + hq4::RETRACE_ACK => { + crate::pf_instant!(crate::perfetto::events::MGRAS_HQ4_ACK, (off as u64) << 32 | v as u64); + } hq4::RE_STATUS => board.push((hq3::RASTER + (hq4::RE_STATUS - hq4::RASTER), v)), o if is_plain(o) => { s.plain.insert(o, v); @@ -1669,6 +1767,17 @@ mod tests { } } + /// The pixel pipeline's ERAM blocks sit 0x8C higher on the Octane; GE + /// memory below the ERAM and the program do not move. + #[test] + fn pixel_state_maps_to_the_indigo2_layout() { + let q = Hq4Quirks; + assert_eq!(q.pixel_state_address(0xE34), 0xDA8, "transfer size"); + assert_eq!(q.pixel_state_address(0xD91), 0xD05, "a module state"); + assert_eq!(q.pixel_state_address(0x1D6), 0x1D6, "GE memory"); + assert_eq!(q.pixel_state_address(0x13B), 0x13B, "the program"); + } + #[test] fn retrace_is_a_message_a_frame() { let r = rig(); diff --git a/src/dev/mgras/mgras_tests.rs b/src/dev/mgras/mgras_tests.rs index 6ee16d91..027fe63e 100644 --- a/src/dev/mgras/mgras_tests.rs +++ b/src/dev/mgras/mgras_tests.rs @@ -3261,6 +3261,25 @@ fn gl_12_12_12_keeps_twelve_bits() { m.stop_engines(); } +/// The RGBA 12,12,12 visual has no alpha planes, so glReadPixels' alpha +/// is full scale (GL's rule), in both component orders. Planes 31:24 +/// hold red's and green's low nibbles, which used to come back as alpha +/// (a Solid IMPACT's deepest visual: pixtest failed most of its cases). +#[test] +fn gl_12_12_12_reads_alpha_as_full_scale() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0xE4, &[0, 0x11, 0, 0, 0, 0, 0, 0, 0, 399, 299, 0x240 | 0x140 << 10, 0, 0, 0]); + fifo_token(&m, 0x9A, &[0x2300, 0x2300]); + fifo_token(&m, 0x49, &[1, 1, 0]); + gl_color4(&m, [1.0, 0.0, 0.0, 1.0]); + gl_full_quad(&m); + let rgba = gl_read_one(&m, 50, 50, 0, 0x0041_0080, 4); + assert_eq!(rgba, [0xFF, 0, 0, 0xFF], "RGBA order"); + let abgr = gl_read_one(&m, 50, 50, 0, 0x0001_0080, 4); + assert_eq!(abgr, [0xFF, 0, 0, 0xFF], "ABGR order"); + m.stop_engines(); +} + /// The bank comes from the kernel (VALIDATE_BANKS), not from counting /// swaps: a swap the kernel did not pair with a bank (or one the GE never /// saw) leaves the drawing where the kernel last said. @@ -3373,3 +3392,39 @@ fn a_new_busy_stretch_is_timed_from_its_first_store() { assert!(!m.busy_overdue(false), "the new stretch starts at its store"); m.stop_engines(); } + +/// glRenderMode(GL_SELECT) as libGLcore drives the GE (traced, glstate): +/// SELECT_MODE, the name stack tokens, primitives (which draw nothing), +/// RENDER_MODE, then the hit records read back with __MGR_RETURN_MODE's +/// second word 0xFFFFFFFF. GL_NAME_STACK_DEPTH reads GE word 0xC59 and the +/// records' length 0xC60. A record is [epoch, names, z min, z max, names]. +#[test] +fn gl_select_mode_keeps_a_name_stack_and_records_hits() { + let m = gl_board([0.0, 0.0, 0.0]); + let ret = |a: u32, b: u32| { + write(&m, 32, 0x7000C, 1 << 17); + fifo_token(&m, 0xA2, &[a, b, 1]); + wait_flag(&m, 1 << 17); + read(&m, 32, 0x70014) as u32 + }; + fifo_token(&m, 0xCB, &[0]); + fifo_token(&m, 0xC4, &[]); + fifo_token(&m, 0xC7, &[3]); + fifo_token(&m, 0xC7, &[4]); + assert_eq!(ret(0xC59, 1), 2, "name stack depth"); + // Inside the window: a hit. Outside it: none. + gl_tri(&m, [1.0, 0.0, 0.0], [[100.0, 50.0, 0.0], [300.0, 50.0, 0.0], [200.0, 250.0, 0.0]]); + fifo_token(&m, 0xC6, &[]); + gl_tri(&m, [1.0, 0.0, 0.0], [[-900.0, -900.0, 0.0], [-800.0, -900.0, 0.0], [-850.0, -800.0, 0.0]]); + fifo_token(&m, 0xC9, &[]); + assert_eq!(gl_px(&m, 200, 100), 0, "selection draws nothing"); + let len = ret(0xC60, 1); + assert_eq!(len, 6, "one record of two names"); + let rec: Vec = (0..len).map(|i| ret(i, 0xFFFF_FFFF)).collect(); + assert_eq!(rec[1], 2); + assert!(f32::from_bits(rec[2]) <= f32::from_bits(rec[3])); + assert_eq!(rec[4..], [3, 4]); + fifo_token(&m, 0x62, &[]); + assert_eq!(ret(0xC60, 1), 0, "read out and emptied"); + m.stop_engines(); +} diff --git a/src/dev/mgras/microcode.rs b/src/dev/mgras/microcode.rs new file mode 100644 index 00000000..03d62ebe --- /dev/null +++ b/src/dev/mgras/microcode.rs @@ -0,0 +1,87 @@ +//! Which microcode the kernel loaded: fingerprints of the HQ microcode RAM +//! and of each GE11's program. +//! +//! The GE keeps GL state at addresses its microcode chooses, and libGLcore +//! reads it back at those addresses. Microcode and libGLcore ship together +//! in an IRIX release, so the layout follows the release and the host +//! interface (HQ3 or HQ4), not the board. IRIX 6.5's base release reads +//! viewport, scissor, clear values and raster state from other addresses +//! than 6.5.22 does. A fingerprint names the release a guest runs, so a +//! readback layout can be chosen by it, and a log line says which +//! microcode a run saw. The hashes are kept as the words are written, so +//! reading one costs nothing. + +use std::fmt; + +const FNV_OFFSET: u64 = 0xCBF2_9CE4_8422_2325; + +/// A running FNV-1a hash of a download, updated as each word is written +/// (each as four big-endian bytes). Plain data, valid zeroed: 0 is "not +/// loaded". The kernel writes a download in order from its first word, so +/// a write there starts the hash again; a reload of the same microcode +/// gives the same value. +#[derive(Clone, Copy, Debug, Default)] +#[repr(C)] +pub struct Running(u64); + +impl Running { + /// Word `index` of the download is `w`. + pub fn word(&mut self, index: usize, w: u32) { + let mut h = if index == 0 || self.0 == 0 { FNV_OFFSET } else { self.0 }; + for b in w.to_be_bytes() { + h = (h ^ b as u64).wrapping_mul(0x0000_0100_0000_01B3); + } + self.0 = h; + } + + pub fn get(&self) -> u64 { + self.0 + } +} + +/// The HQ microcode and the two GE11 programs; 0 is "not loaded". +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Fingerprints { + pub hq: u64, + pub ge: [u64; 2], +} + +/// Releases whose HQ microcode has been seen, by fingerprint. +/// The GE11 program is the same on both 6.5.22 boards +/// (`5467b0da1d106495`); only the HQ microcode differs. +const KNOWN: &[(u64, &str)] = &[ + (0x718f_3f8a_d7ff_4561, "IRIX 6.5.22, Octane HQ4"), + (0x972a_2a44_4f1a_6158, "IRIX 6.5.22, Indigo2 HQ3"), +]; + +impl Fingerprints { + /// The release this HQ microcode came with, if it is a known one. + pub fn release(&self) -> Option<&'static str> { + KNOWN.iter().find(|(h, _)| *h == self.hq).map(|(_, r)| *r) + } +} + +impl fmt::Display for Fingerprints { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "HQ {:016x} GE11 {:016x} {:016x} ({})", self.hq, self.ge[0], self.ge[1], + self.release().unwrap_or("not a release on record")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_reload_from_the_first_word_gives_the_same_fingerprint() { + let load = |r: &mut Running, ws: &[u32]| ws.iter().enumerate().for_each(|(i, &w)| r.word(i, w)); + let (mut a, mut b) = (Running::default(), Running::default()); + load(&mut a, &[1, 2, 3]); + load(&mut b, &[9, 9]); + load(&mut b, &[1, 2, 3]); + assert_eq!(a.get(), b.get()); + load(&mut b, &[1, 2, 4]); + assert_ne!(a.get(), b.get()); + assert_eq!(Running::default().get(), 0); + } +} diff --git a/src/dev/mgras/mod.rs b/src/dev/mgras/mod.rs index eef88c91..f12e0cb7 100644 --- a/src/dev/mgras/mod.rs +++ b/src/dev/mgras/mod.rs @@ -43,6 +43,7 @@ mod ge11; mod gl; mod hq3; pub mod hq4; +pub mod microcode; mod pixmem; mod pixpipe; mod plain; @@ -210,7 +211,9 @@ impl Front { fn write(&mut self, off: u32, bits: u32, val: u64, flags: &AtomicU32) -> bool { match off { host::UCODE..=0x45FFF => { - self.hq.ucode[((off - host::UCODE) / 4) as usize] = val as u32 & 0xFF_FFFF; + let i = ((off - host::UCODE) / 4) as usize; + self.hq.ucode[i] = val as u32 & 0xFF_FFFF; + self.hq.ucode_hash.word(i, val as u32 & 0xFF_FFFF); false } 0x60000..=0x67FFF => { @@ -267,6 +270,10 @@ pub enum Line { /// IP30 kernel's waits on its idle bits. const BUSY_STATUS_BOUND: std::time::Duration = std::time::Duration::from_millis(500); +/// How long a STATUS or DMABUSY read waits for a busy board to go idle +/// before it answers (`settle`). +const POLL_SETTLE: std::time::Duration = std::time::Duration::from_micros(100); + pub struct Mgras { kind: GraphicsBoard, /// Perfetto edge trackers (zero-valid, so `new`'s zeroed allocation @@ -292,6 +299,10 @@ pub struct Mgras { /// in ns since `epoch`: STATUS reads vertical blank for 1 ms after it. epoch: std::time::Instant, frame_start: AtomicU64, + /// The GE11 programs' fingerprints as a front-end that keeps its own + /// copy of the download (the Octane's HQ4) reports them; 0 when it + /// does not, and the GE11 diagnostic ports' copy is used. + ge_upload_fingerprint: [AtomicU64; 2], /// CPU-side state and its lock. The display thread reads it unlocked /// (plain data, tearing tolerated). front_lock: SpinLock<()>, @@ -411,7 +422,14 @@ impl Hq3Sink for HqSink<'_> { fn gl_bracket(&mut self, enter: bool) { self.0.rss_push(if enter { tag::GL_ENTER } else { tag::GL_LEAVE }, 0); } + fn finish_raster(&mut self) { + if !self.0.rss_idle() { + crate::pf_scope!(crate::perfetto::events::MGRAS_SWAP_WAIT, self.0.rss_fifo.len()); + self.0.drain_rss(); + } + } fn set_flags(&mut self, bits: u32) { + crate::pf_instant!(crate::perfetto::events::MGRAS_FLAG_SET, (self.0.rss_fifo.len() as u64) << 32 | bits as u64); self.0.flags.fetch_or(bits, Ordering::AcqRel); self.0.refresh_general(); } @@ -681,6 +699,27 @@ impl Mgras { } } + /// A guest polling STATUS or DMABUSY for an idle board: give a busy + /// board up to `POLL_SETTLE` to finish first. The drivers' idle waits + /// count polls (a microsecond delay loop between them), not time, and + /// the model draws far slower than the board: a context switch's wait + /// (DMABUSY, 6 x 15000 polls 10 us apart, about 0.9 s) timed out + /// behind long drawing and the kernel killed the X server ("hq idle + /// wait timeout"). Under jitv2 the delay loop also runs about five + /// times short (clanker/irix/delay-loop-calibrated-cold.md). Waiting + /// here turns each poll into board time, so the budget is counted in + /// work done; an idle board answers at once. + fn settle(&self) { + if self.idle() { + return; + } + let t = std::time::Instant::now(); + let backoff = crossbeam_utils::Backoff::new(); + while !self.idle() && t.elapsed() < POLL_SETTLE { + backoff.snooze(); + } + } + /// HQ3 thread: wait until the engines have run everything queued. fn drain_rss(&self) { let backoff = crossbeam_utils::Backoff::new(); @@ -926,6 +965,7 @@ impl Mgras { Line::General => 1, Line::Retrace => 2, }; + crate::pf_instant!(crate::perfetto::events::MGRAS_IRQ_LINE, n << 1 | active as usize); self.irqs[n].set(active); } @@ -1043,6 +1083,7 @@ impl Mgras { if Self::cfifo_low(cfg, level) { v |= host::STATUS_CFIFO_LOW; } + crate::pf_instant!(crate::perfetto::events::MGRAS_STATUS_READ, v); v } host::FIFOSTATUS => level | (cfg & 0x7F) << host::FIFOSTATUS_HIGH_SHIFT, @@ -1063,6 +1104,26 @@ impl Mgras { } } + /// Fingerprints of the microcode the kernel loaded (see `microcode`), + /// read without the front lock: a download in progress gives a value + /// that does not repeat. + pub fn microcode_fingerprints(&self) -> microcode::Fingerprints { + let f = self.front_view(); + let up = |ge: usize| self.ge_upload_fingerprint[ge].load(Ordering::Relaxed); + microcode::Fingerprints { + hq: f.hq.ucode_hash.get(), + ge: [0, 1].map(|ge| match up(ge) { + 0 => f.ge.fingerprint(ge), + fp => fp, + }), + } + } + + /// A front-end's own copy of GE11 program `ge` has this fingerprint. + pub fn set_ge_upload_fingerprint(&self, ge: usize, fp: u64) { + self.ge_upload_fingerprint[ge & 1].store(fp, Ordering::Relaxed); + } + /// STATUS's vertical blank bit: the first 1 ms of the refresh thread's /// frame, the time of the retrace pulse. fn vblank(&self) -> u32 { @@ -1215,9 +1276,11 @@ impl Mgras { self.wake_hq(); } host::SET_FLAGS | host::SET_FLAGS_PRIVILEGED => { + crate::pf_instant!(crate::perfetto::events::MGRAS_FLAG_SET, (self.rss_fifo.len() as u64) << 32 | lo as u64); self.flags.fetch_or(lo, Ordering::AcqRel); } host::CLEAR_FLAGS | host::CLEAR_FLAGS_PRIVILEGED => { + crate::pf_instant!(crate::perfetto::events::MGRAS_FLAG_CLEAR, lo); self.flags.fetch_and(!lo, Ordering::AcqRel); } 0x80000..=0xFFFFF => self.note(format!("fast-path command window write at {off:#x}")), @@ -1246,6 +1309,9 @@ impl Mgras { fn do_read(&self, addr: u64, bits: u32) -> Option { let Some(off) = self.offset(addr) else { return Some(0) }; + if matches!(off, host::STATUS | host::DMABUSY) { + self.settle(); + } let sub = self.submit.lock(); let v = self.read_locked(off, bits); crate::pf_level!(self.pf_wait, crate::perfetto::events::MGRAS_WAIT, v.is_none()); diff --git a/src/dev/mgras/rss.rs b/src/dev/mgras/rss.rs index 7d2acfae..472ddd44 100644 --- a/src/dev/mgras/rss.rs +++ b/src/dev/mgras/rss.rs @@ -2611,13 +2611,16 @@ impl Rss { /// The read pixel's components at (fx, fy) widened to 16 bits: a /// 12:12:12 visual's twelve bits, else the 8-bit framebuffer value. + /// A 12:12:12 pixel has no alpha planes (planes 31:24 hold red's and + /// green's low nibbles), so its alpha reads as full scale, GL's value + /// for a visual without alpha. fn comps16(&self, fx: i32, fy: i32) -> [u16; 4] { - let px = self.get(fx, fy); if rgb36(self.reg(reg::PP1FILLMODE)) && (0..WIDTH as i32).contains(&fx) && (0..HEIGHT as i32).contains(&fy) { let u = unpack36(self.mem.get(&self.source(), fx as u32, fy as u32)); let w = |c: u32| (c << 4 | c >> 8) as u16; - return [w(u[0]), w(u[1]), w(u[2]), comps16(px)[3]]; + return [w(u[0]), w(u[1]), w(u[2]), 0xFFFF]; } + let px = self.get(fx, fy); comps16(px) } @@ -2649,8 +2652,15 @@ impl Rss { return out.get(i as usize).copied().unwrap_or(0); } // Depth: the ZST word as it is, never a 12-bit pair's half. - let px = if x.format == DEPTH_FORMAT { self.get_word(fx, fy) } else { self.get(fx, fy) }; let pp1 = self.reg(reg::PP1FILLMODE); + let px = if x.format == DEPTH_FORMAT { + self.get_word(fx, fy) + } else if rgb36(pp1) && (3..=8).contains(&x.format.0) { + // No alpha planes: alpha reads full scale (see `comps16`). + self.get(fx, fy) | 0xFF00_0000 + } else { + self.get(fx, fy) + }; let v = match x.format { // 16-bit components from a 12:12:12 pixel: all twelve bits. (7, 1) | (8, 1) if rgb36(pp1) && (0..WIDTH as i32).contains(&fx) && (0..HEIGHT as i32).contains(&fy) => { diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 760dbabf..0dada67d 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -121,7 +121,11 @@ impl OctaneMachine { return Err(format!("Octane PROM {prom_path} is {} bytes; the flash holds {FLASH_SIZE}", prom.len())); } let banks = heart_banks(cfg); - let board = cfg.octane.graphics_board()?; + let board = match cfg.graphics_board() { + crate::config::GraphicsBoard::None => 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())), + }; let (event_tx, event_rx) = std::sync::mpsc::sync_channel(4); let timer_manager = Arc::new(TimerManager::new()); @@ -181,7 +185,7 @@ impl OctaneMachine { // shared with the graphics board before the CPU exists. let heartbeat = Arc::new(AtomicU64::new(0)); let fasttick = Arc::new(AtomicU64::new(0)); - let gfx = board.map(|_| ImpactXio::new(WIDGET_GFX, heart.clone(), mem.clone(), heartbeat, fasttick.clone())); + let gfx = board.map(|b| ImpactXio::with_board(WIDGET_GFX, b, heart.clone(), mem.clone(), heartbeat, fasttick.clone())); // The bus over them, then the table. let phys = Ip30Bus::new(ram.clone()); @@ -304,6 +308,11 @@ impl OctaneMachine { } // `ps2 type` and friends: the keyboard and mouse from the monitor. monitor.register_device(ioc3.ps2()); + #[cfg(feature = "perfetto")] + { + crate::perfetto::set_guest_cycles(Some(cpu.cycles_ptr())); + monitor.register_device(Arc::new(crate::perfetto::PerfettoMonitor)); + } heart.register_locks(); xbow.register_locks(); bridge.register_locks(); @@ -510,6 +519,9 @@ impl OctaneMachine { for d in self.devices().iter().rev() { d.stop(); } + // The guest clock points into this machine's CPU core. + #[cfg(feature = "perfetto")] + crate::perfetto::set_guest_cycles(None); if self.lifecycle != crate::machine_state::GuestState::Idle { self.lifecycle = crate::machine_state::GuestState::Halted; } diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index 8dfade86..54649507 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -414,6 +414,11 @@ impl O2Machine { monitor.register_device(ctl.clone()); } monitor.register_device(Arc::new(O2Control::new(bus.clone()))); + #[cfg(feature = "perfetto")] + { + crate::perfetto::set_guest_cycles(Some(cpu.cycles_ptr())); + monitor.register_device(Arc::new(crate::perfetto::PerfettoMonitor)); + } let monitor_port = cfg.monitor_port.unwrap_or(crate::monitor::DEFAULT_PORT); let (events, event_rx) = std::sync::mpsc::sync_channel(4); @@ -644,6 +649,9 @@ impl O2Machine { pub fn stop(&mut self) { self.cpu.clone().as_device().stop(); self.paused.store(true, Ordering::SeqCst); + // The guest clock points into this machine's CPU core. + #[cfg(feature = "perfetto")] + crate::perfetto::set_guest_cycles(None); // After the CPU, so nothing queues more; each finishes the command // it is running, so a snapshot finds none half done. for ctl in self.bus.scsi.lock().unwrap().iter() { diff --git a/src/machine.rs b/src/machine.rs index 5cd75a8b..b9d90680 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -311,7 +311,7 @@ impl Machine { #[cfg(feature = "jitv2")] let jitv2_threads = cfg.jitv2.threads; let display_resolution = cfg.graphics.resolution; - let newport_active = !cfg.headless && cfg.graphics.board == GraphicsBoard::Newport; + let newport_active = !cfg.headless && cfg.graphics_board() == GraphicsBoard::Newport; let monitor_port = cfg.monitor_port.unwrap_or(crate::monitor::DEFAULT_PORT); let clock_fixed_mhz = cfg.clock.fixed_mhz; let cfg_cpu_model = cfg.machine.cpu; @@ -564,7 +564,7 @@ impl Machine { let nvram_provenance = cfg.nvram.clone(); // REX3 Graphics — Newport only; skipped in headless mode or when non-Newport board selected. - let rex3: Option> = if cfg.headless || cfg.graphics.board != crate::config::GraphicsBoard::Newport { + let rex3: Option> = if cfg.headless || cfg.graphics_board() != crate::config::GraphicsBoard::Newport { None } else { let r = Arc::new(Rex3::new(heartbeat.clone(), fasttick_count.clone(), decoded_count.clone(), Arc::clone(&l1i_hit_count), Arc::clone(&l1i_fetch_count), Arc::clone(&uncached_fetch_count))); @@ -586,10 +586,10 @@ impl Machine { // GR2 (XZ / Extreme) in the GIO gfx slot. Created even when headless: // selecting the board is explicit, and the PROM needs it to probe. - let gr2: Option> = match cfg.graphics.board { + let gr2: Option> = match cfg.graphics_board() { crate::config::GraphicsBoard::Xz | crate::config::GraphicsBoard::Extreme => { use crate::dev::gr2::{Gr2, Gr2Stats, Gr2Variant}; - let variant = if cfg.graphics.board == crate::config::GraphicsBoard::Extreme { Gr2Variant::Extreme } else { Gr2Variant::Xz }; + let variant = if cfg.graphics_board() == crate::config::GraphicsBoard::Extreme { Gr2Variant::Extreme } else { Gr2Variant::Xz }; let g = Gr2::new(variant, Gr2Stats { heartbeat: heartbeat.clone(), fasttick: fasttick_count.clone() }); // GR2 retrace is GIO interrupt 2 (LIO_GIO_2): on Indigo2 through // the EXTIO SG_RETRACE fan-out, on Indy the direct line. It is a @@ -612,11 +612,11 @@ impl Machine { }; // IMPACT graphics in the GIO graphics slot. - let mgras: Option> = match cfg.graphics.board { + let mgras: Option> = match cfg.graphics_board() { crate::config::GraphicsBoard::SolidImpact | crate::config::GraphicsBoard::HighImpact | crate::config::GraphicsBoard::MaxImpact => { - Some(crate::dev::mgras::Mgras::new(cfg.graphics.board, { + Some(crate::dev::mgras::Mgras::new(cfg.graphics_board(), { use crate::dev::ioc::IocInterrupt::{GioSgFifo, GioSgGraphics, GioSgRetrace}; [ioc.irq_line(GioSgFifo), ioc.irq_line(GioSgGraphics), ioc.irq_line(GioSgRetrace)] }, heartbeat.clone(), fasttick_count.clone())) diff --git a/src/perfetto/events.rs b/src/perfetto/events.rs index 2ccef5bf..2923db6d 100644 --- a/src/perfetto/events.rs +++ b/src/perfetto/events.rs @@ -169,7 +169,40 @@ pub static MGRAS_DMA_TO_HOST: EventDef = EventDef::new("host dma -> host", cat:: pub static MGRAS_SWAP: EventDef = EventDef::new("schedule swap", cat::GFX).on_track("IMPACT display").in_group(GFX_GROUP); /// The kernel's BUF_SELECT write: the flip itself (arg: value). pub static MGRAS_BUF_SELECT: EventDef = EventDef::new("buf select", cat::GFX).on_track("IMPACT display").in_group(GFX_GROUP).with_arg("val"); +/// SCHEDULE_SWAP waiting for the raster engines to finish the frame +/// before the swap is reported (arg: raster entries queued). +pub static MGRAS_SWAP_WAIT: EventDef = EventDef::new("swap waits raster", cat::GFX).on_track("IMPACT swap wait").in_group(GFX_GROUP).with_arg("rss_queued"); pub static MGRAS_RETRACE: EventDef = EventDef::new("retrace", cat::GFX).on_track("IMPACT display").in_group(GFX_GROUP); +/// The refresh thread copying the shown frame out of the framebuffer +/// (snapshot and compose), and handing it to the window. +pub static MGRAS_CAPTURE: EventDef = EventDef::new("frame capture", cat::GFX).on_track("IMPACT capture").in_group(GFX_GROUP); +pub static MGRAS_PRESENT: EventDef = EventDef::new("present", cat::GFX).on_track("IMPACT present").in_group(GFX_GROUP); +/// An XMAP display mode written: the mode of one window ID, main or +/// overlay (arg: register << 40 | index << 32 | value). +pub static MGRAS_XMAP_MODE: EventDef = EventDef::new("xmap mode write", cat::GFX).on_track("IMPACT display").in_group(GFX_GROUP).with_arg("reg<<40|index<<32|val"); +/// A VC3 register write (arg: index << 16 | value) and a VC3 SRAM write, +/// the window-ID tables among others (arg: address << 16 | value). +pub static MGRAS_VC3_WRITE: EventDef = EventDef::new("vc3 write", cat::GFX).on_track("IMPACT vc3").in_group(GFX_GROUP).with_arg("index<<16|val"); +pub static MGRAS_VC3_SRAM: EventDef = EventDef::new("vc3 sram write", cat::GFX).on_track("IMPACT vc3").in_group(GFX_GROUP).with_arg("addr<<16|val"); +/// The VC3 current-line register read (arg: the line answered). +pub static MGRAS_VC3_LINE: EventDef = EventDef::new("vc3 line read", cat::GFX).on_track("IMPACT vc3").in_group(GFX_GROUP).with_arg("line"); +/// A STATUS read (arg: the value; bit 2 vertical blank). +pub static MGRAS_STATUS_READ: EventDef = EventDef::new("status read", cat::GFX).on_track("IMPACT status").in_group(GFX_GROUP).with_arg("val"); +/// Flags raised by the engines or a host SET_FLAGS write (arg: raster +/// entries still queued << 32 | bits), and a host CLEAR_FLAGS write (arg: +/// bits). +pub static MGRAS_FLAG_SET: EventDef = EventDef::new("flags set", cat::GFX).on_track("IMPACT flags").in_group(GFX_GROUP).with_arg("rss_queued<<32|bits"); +pub static MGRAS_FLAG_CLEAR: EventDef = EventDef::new("flags cleared", cat::GFX).on_track("IMPACT flags").in_group(GFX_GROUP).with_arg("bits"); +/// The bank GL draws into, from VALIDATE_BANKS or a context image (arg: +/// bank, 1 = B). +pub static MGRAS_DRAW_BANK: EventDef = EventDef::new("draw bank", cat::GFX).on_track("IMPACT display").in_group(GFX_GROUP).with_arg("bank"); +/// One of the board's interrupt lines changed (arg: line << 1 | level; 0 +/// FIFO, 1 general, 2 retrace). +pub static MGRAS_IRQ_LINE: EventDef = EventDef::new("irq line", cat::GFX).on_track("IMPACT irq").in_group(GFX_GROUP).with_arg("line<<1|level"); +/// Octane: an interrupt packet to HEART (arg: vector << 1 | set), and the +/// kernel's acknowledgements (arg: offset << 32 | bits written). +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"); // ── jitv2 ──────────────────────────────────────────────────────────────────── diff --git a/src/platform_profile_tests.rs b/src/platform_profile_tests.rs index ddc97939..51e9a1d8 100644 --- a/src/platform_profile_tests.rs +++ b/src/platform_profile_tests.rs @@ -43,7 +43,7 @@ mod tests { fn impact_solid_on_indigo2_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::Indigo2Ip22; - cfg.graphics.board = GraphicsBoard::SolidImpact; + cfg.graphics.board = Some(GraphicsBoard::SolidImpact); cfg.validate().expect("Solid IMPACT on Indigo2"); } @@ -51,7 +51,7 @@ mod tests { fn impact_on_indy_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::IndyIp24; - cfg.graphics.board = GraphicsBoard::SolidImpact; + cfg.graphics.board = Some(GraphicsBoard::SolidImpact); cfg.validate().expect("Solid IMPACT on Indy"); } @@ -59,7 +59,7 @@ mod tests { fn newport_on_ip28_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::Indigo2Ip28; - cfg.graphics.board = GraphicsBoard::Newport; + cfg.graphics.board = Some(GraphicsBoard::Newport); cfg.validate().expect("Newport on IP28"); } @@ -67,7 +67,7 @@ mod tests { fn xz_on_ip28_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::Indigo2Ip28; - cfg.graphics.board = GraphicsBoard::Xz; + cfg.graphics.board = Some(GraphicsBoard::Xz); cfg.validate().expect("XZ on IP28"); } @@ -86,18 +86,18 @@ mod tests { use clap::Parser; let cli = Cli::try_parse_from(["iris", "--graphics", "impact:high"]).unwrap(); let cfg = cli.apply(MachineConfig::default()); - assert_eq!(cfg.graphics.board, GraphicsBoard::HighImpact); + assert_eq!(cfg.graphics.board, Some(GraphicsBoard::HighImpact)); let cli = Cli::try_parse_from(["iris", "--board", "solidimpact"]).unwrap(); let cfg = cli.apply(MachineConfig::default()); - assert_eq!(cfg.graphics.board, GraphicsBoard::SolidImpact); + assert_eq!(cfg.graphics.board, Some(GraphicsBoard::SolidImpact)); } #[test] fn xz_on_indy_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::IndyIp24; - cfg.graphics.board = GraphicsBoard::Xz; + cfg.graphics.board = Some(GraphicsBoard::Xz); cfg.graphics.heads = 1; cfg.validate().expect("XZ board on Indy"); } @@ -106,7 +106,7 @@ mod tests { fn xz_on_indigo2_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::Indigo2Ip22; - cfg.graphics.board = GraphicsBoard::Xz; + cfg.graphics.board = Some(GraphicsBoard::Xz); cfg.validate().expect("XZ board on Indigo2"); } @@ -114,7 +114,7 @@ mod tests { fn extreme_on_indy_rejected() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::IndyIp24; - cfg.graphics.board = GraphicsBoard::Extreme; + cfg.graphics.board = Some(GraphicsBoard::Extreme); let err = cfg.validate().unwrap_err(); assert!(err.contains("indigo2_ip22"), "got: {err}"); } diff --git a/test/gltest/irix.sh b/test/gltest/irix.sh index 6826efea..cb6c9ebb 100755 --- a/test/gltest/irix.sh +++ b/test/gltest/irix.sh @@ -1,9 +1,9 @@ #!/bin/sh # Build the GL tests on IRIX with MIPSpro cc (stock IRIX has no bash). -# Usage: sh irix.sh [target...] targets: gltest glprim pixtest blendtest glstate xgetpix xresize xclick cicopy gepeek toktest (default: all) +# Usage: sh irix.sh [target...] targets: gltest glprim pixtest blendtest glstate seltest xgetpix xresize xclick cicopy gepeek toktest (default: all) targets="$*" -[ -z "$targets" ] && targets="gltest glprim pixtest blendtest glstate xgetpix xresize xclick cicopy gepeek toktest" +[ -z "$targets" ] && targets="gltest glprim pixtest blendtest glstate seltest xgetpix xresize xclick cicopy gepeek toktest" status=0 for t in $targets; do @@ -13,6 +13,7 @@ for t in $targets; do pixtest) src=pixtest.c; libs="-lX11 -lGL -lm" ;; blendtest) src=blendtest.c; libs="-lX11 -lGL" ;; glstate) src=glstate.c; libs="-lX11 -lGL -lm" ;; + seltest) src=seltest.c; libs="-lX11 -lGL" ;; xgetpix) src=xgetpix.c; libs="-lX11" ;; xresize) src=xresize.c; libs="-lX11" ;; xclick) src=xclick.c; libs="-lXtst -lXext -lX11" ;; diff --git a/test/gltest/seltest.c b/test/gltest/seltest.c new file mode 100644 index 00000000..261440f3 --- /dev/null +++ b/test/gltest/seltest.c @@ -0,0 +1,118 @@ +/* + * seltest: OpenGL selection (glRenderMode(GL_SELECT)) against known hits. + * Board-neutral (GL, GLX, Xlib). Prints each hit record and PASS/FAIL per + * check; the exit status is the number of failures. + * + * The scene, under glOrtho(0, 100, 0, 100, -1, 1) (window z = (1 - z) / 2): + * name 1: a quad at z = 0.5 inside the view -> hit, z 0.25 + * name 2: a quad outside the view (x 200..300) -> no hit + * name 3: a quad at z = -0.5 and one at z = 0 under + * the same name -> hit, z 0.5..0.75 + * names 4, 5 pushed: a line inside the view -> hit, stack [4 5] + */ +#include +#include +#include +#include + +static int fails; + +static void check(int ok, const char *what) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (!ok) + fails++; +} + +static void draw_quad(float x, float z) +{ + glBegin(GL_QUADS); + glVertex3f(x, 10, z); + glVertex3f(x + 20, 10, z); + glVertex3f(x + 20, 30, z); + glVertex3f(x, 30, z); + glEnd(); +} + +/* Window z from a hit record's 32-bit depth. */ +static double zf(GLuint v) +{ + return v / 4294967295.0; +} + +static int near(double a, double b) +{ + return a > b - 0.01 && a < b + 0.01; +} + +int main(void) +{ + static int attr[] = { GLX_RGBA, None }; + Display *dpy = XOpenDisplay(0); + XVisualInfo *vi; + GLXContext cx; + XSetWindowAttributes swa; + Window win; + GLuint buf[64], *p; + GLint depth; + int hits, i, j; + + if (!dpy || !(vi = glXChooseVisual(dpy, DefaultScreen(dpy), attr))) { + fprintf(stderr, "seltest: no display or RGBA visual\n"); + return 255; + } + swa.colormap = XCreateColormap(dpy, RootWindow(dpy, vi->screen), vi->visual, AllocNone); + swa.border_pixel = 0; + win = XCreateWindow(dpy, RootWindow(dpy, vi->screen), 0, 0, 100, 100, 0, vi->depth, + InputOutput, vi->visual, CWColormap | CWBorderPixel, &swa); + XMapWindow(dpy, win); + cx = glXCreateContext(dpy, vi, 0, True); + glXMakeCurrent(dpy, win, cx); + glViewport(0, 0, 100, 100); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0, 100, 0, 100, -1, 1); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + for (i = 0; i < 64; i++) + buf[i] = 0xdeadbeef; + glSelectBuffer(64, buf); + glRenderMode(GL_SELECT); + glInitNames(); + glPushName(1); + draw_quad(10, 0.5f); + glLoadName(2); + draw_quad(200, 0.0f); + glLoadName(3); + draw_quad(40, -0.5f); + draw_quad(60, 0.0f); + glLoadName(4); + glPushName(5); + glGetIntegerv(GL_NAME_STACK_DEPTH, &depth); + check(depth == 2, "GL_NAME_STACK_DEPTH 2"); + glBegin(GL_LINES); + glVertex3f(10, 50, 0); + glVertex3f(90, 50, 0); + glEnd(); + hits = glRenderMode(GL_RENDER); + + printf("hits %d\n", hits); + for (i = 0, p = buf; i < hits && p < buf + 64; i++) { + GLuint n = p[0]; + printf(" names %u z %.4f..%.4f :", n, zf(p[1]), zf(p[2])); + for (j = 0; j < (int)n; j++) + printf(" %u", p[3 + j]); + printf("\n"); + p += 3 + n; + } + check(hits == 3, "three hits (name 2 is outside the view)"); + p = buf; + check(p[0] == 1 && p[3] == 1 && near(zf(p[1]), 0.25) && near(zf(p[2]), 0.25), "name 1 at z 0.25"); + p += 3 + p[0]; + check(p[0] == 1 && p[3] == 3 && near(zf(p[1]), 0.5) && near(zf(p[2]), 0.75), "name 3 over z 0.5..0.75"); + p += 3 + p[0]; + check(p[0] == 2 && p[3] == 4 && p[4] == 5 && near(zf(p[1]), 0.5), "names 4 5 at z 0.5"); + printf("seltest: %d failed\n", fails); + return fails; +}