diff --git a/.claude/skills/gr2-gl-loop/SKILL.md b/.claude/skills/gr2-gl-loop/SKILL.md new file mode 100644 index 00000000..da672908 --- /dev/null +++ b/.claude/skills/gr2-gl-loop/SKILL.md @@ -0,0 +1,202 @@ +--- +name: gr2-gl-loop +description: Autonomous GR2 (XZ/Extreme) OpenGL bring-up loop on IRIX 6.5.22 - boot IRIS, build and run test/gltest/glprim in the guest over telnet, capture gr2 traces around each run, and verify rendered pixels. Use when iterating GL pipeline configurations (primitives, shading, vertex/colour forms, depth, scissor, viewport) against the GR2 HLE. +--- + +# GR2 OpenGL test loop + +Drive one GL configuration at a time through the real IRIX GL stack +(libglcore EXPRESS -> HQ2 FIFO -> our HLE in `src/dev/gr2/{hq2,gl}.rs`) and +check the pixels. Each `glprim` run draws exactly one primitive, so its FIFO +trace is short and every option flips one piece of state or one address bit. + +## Pieces + +| What | Where | +|---|---| +| Test app | `test/gltest/glprim.c` (`--help` lists every option), built by `test/gltest/irix.sh` with MIPSpro `cc` | +| Guest shell | `tools/irix-sh.py` (telnet via host port 2323, root, no password; single-shot) | +| File exchange | built-in NFS: host `./shared` = guest `/shared` (mount with `tools/irix-sh.py mount-shared`). **noexec**: copy to `/tmp` before building/running | +| Monitor | iris MCP (`mcp__iris__run_command`), i.e. the monitor on 127.0.0.1:8888 | +| Token map | `ignore/gr2/HQ2.h` "OpenGL token addressing" (source of truth; add findings there) | + +## Boot + +1. `cargo build --release --features developer` (developer: monitor tooling; + do not combine with `lightning`). +2. Start it with the Bash tool's `run_in_background: true`, as a command of + its own (`target/release/iris > /iris-run.log 2>&1`). A + `nohup ... &` inside a chained command does not survive the command, and + starting it right after killing the old process can fail: check + `pgrep -a iris` before going on. (config: `iris.toml`, Indy R5000 + XZ, + disk `scsi1.raw`.) +3. Start the CPU through the MCP: `run_command("cpu start")` (check with + `get_status` if unsure). +4. `tools/irix-sh.py wait --timeout 900` returns once telnet answers with a + login prompt (network and inetd are up; X/xdm come up around then too). +5. `tools/irix-sh.py mount-shared`. +6. GL needs a logged-in desktop (xdm's clogin holds the display otherwise). + root autologin is set up: `/etc/autologin` = `root`, and the ONE-SHOT flag + `/etc/autologin.on`, which clogin deletes when it logs in. Re-create it + (`touch /etc/autologin.on`) every boot before shutdown. The session takes + ~1 min after telnet comes up: wait until `ps -ef | grep 4Dwm` shows it. +7. The screen saver (`haven` + `/usr/sbin/bongo`, octahedra) takes over the + whole screen after 10 min idle and every probe reads black. The desktop + re-applies its saver settings after `/root/.sgisession`, so run + `/usr/bin/X11/xset s off` once the session is up, and check + `xset q | grep timeout:` = 0. +8. Stopping iris: kill it by its exact pid (`ps -eo pid,args | grep + '[t]arget/release/iris$'`). `pgrep -f target/release/iris` also matches + the shell running the command and kills it. + +`iris.toml` currently writes the disk image directly (no overlay): never +kill iris while IRIX is running (see Shutdown). + +## Build glprim in the guest + +``` +mkdir -p shared/gltest && cp test/gltest/{glprim.c,main.c,irix.sh} shared/gltest/ +tools/irix-sh.py run 'mkdir -p /tmp/gltest && cp /shared/gltest/* /tmp/gltest/' \ + 'cd /tmp/gltest && sh irix.sh glprim 2>&1' +``` + +Every session gets `DISPLAY=:0.0`, `/bin/sh`, cwd `/tmp`. The exit status of +`irix-sh.py run` is the last command's status (124 = timeout/connection). + +## One test case + +Preferred: `tools/glprim-case.py CASE [--probe gx,gy[=rrggbb]]... [--notrace] -- ` +does all of the below (trace, run, window origin, probes in GL window +coordinates, fbdump, crop of the window to `$GLPRIM_OUT/CASE.png`, trace off, +kill glprim) and exits with the number of failed probes. It talks to the +monitor on 127.0.0.1:8888 directly (works alongside the MCP). Stage the +sources first (`cp test/gltest/* shared/gltest/`, then copy to /tmp in the +guest and `sh irix.sh glprim`). + +By hand: + +``` +run_command("gr2 trace /tmp/gl-.log all") # MCP +tools/irix-sh.py run 'cd /tmp/gltest && ./glprim --hold 4000' +run_command("gr2 fbdump /tmp/gl-") # during the hold, or +run_command("gr2 pix ") # spot checks (display coords) +run_command("gr2 trace off") +``` + +Run the capture commands while `--hold` keeps the window up (launch the +guest command in the background with `&` if you need to sample during the +hold). `glprim` prints its configuration and its **real window origin** +(`window origin OX,OY size WxH`; the window manager moves windows). A GL +window pixel (gx, gy) is display pixel `(OX + gx, OY + H - 1 - gy)`. + +Default geometry (unit square scaled to the viewport, glOrtho 0..w, 0..h): +triangle (0.2w,0.2h) red, (0.8w,0.2h) green, (0.5w,0.8h) blue. Flat shading +uses the provoking vertex (last vertex for triangles/strips/fans/quads, the +FIRST vertex for GL_POLYGON). Clear default is pink (1, 0.5, 0.75) = +`rgb=ff80bf`. + +## Verify + +- `gr2 pix x y` prints the VRAM word, DID, XMAP mode and composed rgb. +- `gr2 fbdump DIR` writes VRAM planes and the composed screen as PNGs; view + them with the Read tool. +- Expected values: compute from the GL spec (pixel centres at +0.5, fill rule + top-left style; flat = provoking vertex colour; smooth = barycentric + interpolation, allow +-2 per channel). Check interior, each edge's first + in/out pixel, and a pixel just outside the window (must be untouched). +- In the trace, `HQ exec` lines show the HLE's view: `VERTEX (...) -> window + (...)`, `GL triangle ...`, `GL_CLEAR ... rect`. Unknown tokens appear as + `tokNNN` / `(N words, not implemented)` with their modifier bits decoded + (`V3|USEV:VERTEX`, `C3:COLOR`, `LOADV:...`). + +## Iterate + +`--scene depth|stencil|alphatest|blend` draws small multi-primitive scenes +with known images (see the comment above `draw_scene` in glprim.c); verified +probes are in the history of this skill's cases (depth: (170,150) red, +(230,150) blue; stencil: (200,120) blue, (20,20) pink; blend: (200,120) +800080, (300,120) 8040df; alphatest: (200,200) ~1c1cc6, (90,62) pink). + +Change one option at a time. Suggested order: clear only (`-p none`), flat +triangle, `--smooth`, `--mono`, every `-p` primitive, `--vtx 2f/4f/2i/3i/2s/3d`, +`--color 4f/3ub/4ub/once`, `--viewport`, `--scissor`, `--cull`/`--cw`, +`--polymode`, `--persp`, `--depth`, `--db`. When a new token or format shows +up, decode it against `ignore/gr2/libglcore/decomp/EXPRESS` (struct offset +X = FIFO index (X - 0x2000)/4), record it in `HQ2.h` with the emitter as +citation, implement in `src/dev/gr2/gl.rs`, add a replay test in +`src/dev/gr2/hq2_tests.rs` built from the trace (see `glprim_triangle`), and +re-run the case. + +## Offline replay of real streams + +A guest restart costs ~6 minutes, so capture once and iterate offline: +extract the HQ2 FIFO writes of a case (from the window setup `0x1e5` to the +`GL_FINISH` after the draw, 2D command groups 0x12C..0x1EA dropped) into +`src/dev/gr2/testdata/glprim_.trace` ("index value" hex per line) and +replay it in a unit test with `replay_trace()` (hq2_tests.rs); the window +origin is the first two words after `1e5`. See the lighting/fog tests. + +## Demos + +`/usr/demos/General_Demos//` (atlantis, ideas, buttonfly, flight, +powerflip, ...). GL state is swapped per context on GE_HQMSAV; still, test demos one at a +time. + +Demos that wait for input need it injected: `ideas` sits on its title +screen until clicked (and needs another click to restart after its +animation), so a run without a click only ever draws the title. + +## Driving input (mouse, keyboard) + +Two ways, both scriptable: + +- **Monitor, `ps2`** (any PS/2 machine: Indy, Indigo2/IP28): + `ps2 mouse [buttons]` sends one relative mouse packet (buttons: + 1 left, 2 right, 4 middle; send a packet with the button, then one with 0 + to release), `ps2 type ` types a string, `ps2 enter` presses + Enter, `ps2 status` shows the controller state. Moves are relative and go + through X's pointer acceleration: to reach a spot, first push the pointer + into the top-left corner with a few large negative moves, then step to the + target in small moves. + ``` + run_command("ps2 mouse -500 -500") # corner (repeat to be sure) + run_command("ps2 mouse 40 30") # small steps to the target + run_command("ps2 mouse 0 0 1") # press left + run_command("ps2 mouse 0 0 0") # release + ``` +- **In the guest, XTest**: `test/gltest/xclick.c` (`xclick NAME [BUTTON [X Y]]` + or `xclick NAME key KEYSYM`) clicks or presses a key at exact window + coordinates in the first window whose name starts with NAME; + `test/gltest/xresize.c` (`xresize NAME W1 H1 W2 H2 [COUNT] [DELAY_MS]`) + resizes a window repeatedly. Build with `sh irix.sh xclick xresize`. + Precise, no acceleration, but needs a telnet session. + +Demo sources (ideas, atlantis, ...) are published on opengl.org: read them +before guessing what a demo sends. + +## Shutdown + +``` +tools/irix-sh.py run 'touch /etc/autologin.on; sync; shutdown -p -g0 -y' +``` +Then wait about a minute (the developer build does not exit on power-off), +check `/tmp/iris-run.log` / the monitor for the power-off, and only then kill +the iris process. + +## Gotchas + +- The host shell's `cd` may be aliased; use `builtin cd` or absolute paths. +- `/shared` is noexec: always build and run from `/tmp`. +- Stock IRIX has no bash (scripts must be `/bin/sh`), no `pkill`, and + `chmod` wants `a+x` rather than `+x`. +- GL state is per context: GE_HQMSAV (0x1F0, kernel Gr2PcxSwap) swaps the + HLE's GlState by context id; state 0 = new context = defaults. A trace + that starts mid-session has no switch for the running client: replays keep + the live state for it. +- MIPSpro is C89-ish: declarations at block start, no ``. +- A GL client that waits forever on `version` bit 0 (FIN3) is waiting for a + Finish (`0xA3`) we did not answer; a stuck client on bit 1 is FIN2 (kernel + context switch). +- Window clipping arrives as 0x1E5 only on the switch into a GL context after + its clip changed (see rules/gr2/gl-window-clipping.md). To trace clipping, + start the trace before moving the covering window. diff --git a/CLAUDE.md b/CLAUDE.md index 05b78221..1fe53b5a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,7 @@ system (shell, networking, X11). It is **not** cycle-accurate `rules/irix/irix-install.md`. Check here before re-deriving a gotcha; when you confirm a non-obvious fix, write it up here as a short markdown note so the next session doesn't relearn it. +- `ignore/rules` - local unpublished info ## Build & run diff --git a/HELP.md b/HELP.md index 9926610f..1ba4bb18 100644 --- a/HELP.md +++ b/HELP.md @@ -436,15 +436,10 @@ profile = "indy_ip24" # or "indigo2_ip22" (--ip22) cpu = "r4400" # or "r5000" (--cpu) [graphics] -board = "newport" # "xz" = Indy XZ/Elan register stub (preview) +board = "newport" # "newport", "xz", "extreme", "solidimpact" ("impact:solid"), "highimpact" ("impact:high"), "maximpact" ("impact:max") heads = 1 # 2 = dual-head Newport (second REX3 in GIO slot 1) resolution = "guest" # or "1024x768", "1280x960", "1280x1024" -# [impact] # Indigo2 IMPACT preview stub -# gfx = "none" # "none" | "solid" | "high" | "max" -# exp0 = "none" -# exp1 = "none" - [clock] # fixed_mhz = 33 # CP0 Count rate in MHz (default 33, IRIX shows 66 MHz) diff --git a/README.md b/README.md index 9d804e3c..6c874ef2 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,7 @@ Boots IRIX 6.5 and 5.3. Has networking. Has a framebuffer. maturing (use `console=d` + serial for debugging; see [docs/indigo2-ip22.md](docs/indigo2-ip22.md)). -Pre-built binaries and the Mac App Store GUI are available at -[danifunker/iris releases](https://github.com/danifunker/iris/releases) (upstream packaging). -For latest code, build from source from upstream [techomancer/iris](https://github.com/techomancer/iris). Report bugs and issues in the upstream repository. - +Prebuilt releases are available in Github releases. ## Q&A diff --git a/docs/impact-mgras-research.md b/docs/impact-mgras-research.md deleted file mode 100644 index 8fa34297..00000000 --- a/docs/impact-mgras-research.md +++ /dev/null @@ -1,80 +0,0 @@ -# IMPACT / MGRAS graphics — research notes - -IRIS emulates **Newport (REX3)** on Indy and Indigo2 XL paths. IMPACT is a separate -post-1995 architecture (on-board geometry + TRAM). A **preview register stub** lives in -`src/dev/mgras.rs` (Wave 5). - -## Implementation status (IRIS) - -| Component | Status | -|-----------|--------| -| `src/dev/mgras.rs` | Preview — per-slot register file, GIO mapping for gfx/exp0/exp1 | -| `config::ImpactSection` | `[impact]` TOML: `gfx`, `exp0`, `exp1` → `none` / `solid` / `high` / `max` | -| `physical.rs` | Maps MGRAS stub across populated GIO slots when `profile = indigo2_ip22` | -| Command processing / TRAM / GL | **Not implemented** | -| IRIX `impact` driver attach | **Not expected** to reach a working console | - -Monitor commands: `mgras` (slot summary), `impact` (hinv-style inventory preview). - -Example (preview only — Indigo2 profile): - -```toml -[machine] -profile = "indigo2_ip22" - -[impact] -gfx = "solid" -# exp0 = "high" # second board for High IMPACT -# exp1 = "max" # third board for Maximum IMPACT -``` - -## Hardware families - -| Option | GIO64 slots | Geometry | Texture RAM | IRIX driver class | -|--------|-------------|----------|-------------|-------------------| -| Newport XL | 1 | CPU (FPU) | system RAM Z-buffer | `gfx` / REX3 | -| Solid IMPACT | 1 | on-board GE | TRAM | `impact` / MGRAS | -| High IMPACT | 2 | on-board GE | TRAM | `impact` | -| Maximum IMPACT | 3 | dual GE | TRAM | `impact` | - -Valid dual-head combos with Newport: Solid+Solid, Solid+High, Solid+Max — not High+High. - -## Reference sources - -- MAME: `impact.c`, `newport.cpp`, `ip22.cpp` — register maps and IRQ fan-out -- Linux MIPS: `arch/mips/include/asm/sgi/ip22.h`, `drivers/video/impact` -- IRIX: `impact` kernel module, `libGL` IMPACT path, `hinv -c graphics` -- Hardware overview: [Wikipedia SGI Indigo2](https://en.wikipedia.org/wiki/SGI_Indigo2), [sgistuff.net Newport](http://www.sgistuff.net/hardware/graphics/newport.html) - -## Estimated emulation scope (if pursued) - -1. GIO multi-slot board model + IMPACT PSU/riser constraints -2. MGRAS geometry engine + raster engine + TRAM ASICs -3. IRIX PROM revision checks for IMPACT-ready systems -4. Dual-head policy separate from dual Newport - -**Recommendation:** Complete Indigo2 Newport boot + dual-head before pursuing full IMPACT. -Orders of magnitude more work than REX3. - -## Stub register map (`src/dev/mgras.rs`) - -Each populated GIO slot exposes an 8 KB window at `slot_base + 0x0F0000` (same offset -pattern as Newport/XZ until a verified MGRAS map is available): - -| Offset | Name | Notes | -|--------|------|-------| -| `0x0000` | `BOARD_ID` | ASCII-tagged placeholder per slot kind | -| `0x0004` | `REVISION` | class revision placeholder | -| `0x0008` | `STATUS` | idle / FIFO-empty defaults | -| `0x000C` | `INTR_STATUS` | write-1-to-clear storage | -| `0x0010` | `INTR_ENABLE` | stored | -| `0x0018` | `FIFO_WRITE` | accepted, not executed | -| `0x0024` | `SLOT_ROLE` | slot index (0=gfx, 1=exp0, 2=exp1) | - -GIO physical bases (IP22): - -| Slot | Range | -|------|-------| -| gfx | `0x1F000000`–`0x1F3FFFFF` | -| exp0 | `0x1F400000`–`0x1F5FFFFF` | -| exp1 | `0x1F600000`–`0x1F9FFFFF` | diff --git a/docs/indigo2-ip22.md b/docs/indigo2-ip22.md index 7c273373..f7ea6969 100644 --- a/docs/indigo2-ip22.md +++ b/docs/indigo2-ip22.md @@ -76,7 +76,7 @@ Shared GIO interrupt lines (FIFO full, graphics, retrace) are latched in the IOC ## Not implemented -- IMPACT / MGRAS preview stub (`src/dev/mgras.rs`, `[impact]` config) — see `docs/impact-mgras-research.md` +- IMPACT / MGRAS preview stub (`src/dev/mgras.rs`, `[graphics]` board config) — see `docs/impact-mgras-research.md` - Full EXTIO bus-error and EISA interrupt paths See also [`docs/interrupt_map.md`](interrupt_map.md) and [`rules/gui/machine-profile-vs-guest-ip22.md`](../rules/gui/machine-profile-vs-guest-ip22.md). diff --git a/iris-gui-README.md b/iris-gui-README.md index dbae980c..0f748a92 100644 --- a/iris-gui-README.md +++ b/iris-gui-README.md @@ -144,7 +144,7 @@ IRIX with a media-change Unit Attention, no restart needed. | Tab | What's there | | --- | --- | -| **General** | Platform (Indy / Indigo2 / Indigo2 IMPACT with `ip28`), CPU (R4400 / R5000 / R10000 with `ip28`), graphics board (Newport, GR2 XZ/Extreme, or IMPACT Solid/High/Max — one dropdown covers both `[graphics]` and `[impact]`, since they share the GIO gfx slot), Newport heads, display resolution, PROM, NVRAM, ttyd1 serial log | +| **General** | Platform (Indy / Indigo2 / Indigo2 IMPACT with `ip28`), CPU (R4400 / R5000 / R10000 with `ip28`), graphics board (Newport, GR2 XZ/Extreme, or IMPACT Solid/High/Max via `[graphics] board`), Newport heads, display resolution, PROM, NVRAM, ttyd1 serial log | | **Disks** | SCSI devices: image paths, CD-ROM discs, COW overlay, scratch volume, DaynaPort, controller (Indigo2) | | **Networking** | NAT subnet (applied live, with conflict checks against host interfaces), port forwards (added/removed live), NFS share, PCAP interface, **Check networking** diagnostics | | **Memory** | RAM banks and the resulting total | diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 9b3a1200..e5a1e9b5 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -2,8 +2,8 @@ use egui::{Color32, ComboBox, DragValue, Grid, RichText, ScrollArea, TextEdit, U use iris::build_features; use std::path::Path; use iris::config::{ - format_unix_utc, CpuModel, ForwardBind, ForwardProto, GraphicsBoard, ImpactSection, - ImpactSlot, MachineConfig, MachineProfile, NetMode, NfsConfig, PortForwardConfig, RtcOffset, + format_unix_utc, CpuModel, ForwardBind, ForwardProto, GraphicsBoard, + MachineConfig, MachineProfile, NetMode, NfsConfig, PortForwardConfig, RtcOffset, ScsiDeviceConfig, ScsiKind, VinoSource, VinoStandard, VALID_BANK_SIZES, }; use iris::net::nfsudp::NfsVersion; @@ -223,14 +223,6 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) if !cfg.graphics.board.supports(cfg.machine.profile) { cfg.graphics.board = GraphicsBoard::Xz; } - // [impact] (IMPACT/MGRAS graphics) is Indigo2-only, either profile; - // moving to Indy falls back to Newport rather than leaving a config - // `validate()` would reject at Start. - if cfg.impact.any_enabled() - && !matches!(cfg.machine.profile, MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28) - { - cfg.impact = Default::default(); - } // 256 MB banks are the IP28 MC's granule only; moving away from it would // leave a config `validate()` rejects at Start. Compared against the // profile *variant*, not `.ip28()` (which also requires the `ip28` build @@ -364,7 +356,7 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig, mem_ctx: MemoryUiContext) ui.separator(); show_board_picker(ui, cfg, mem_ctx.running); - if cfg.graphics.board == GraphicsBoard::Newport && !cfg.impact.any_enabled() { + if cfg.graphics.board == GraphicsBoard::Newport { ui.horizontal(|ui| { ui.label("Newport heads"); ui.add(egui::DragValue::new(&mut cfg.graphics.heads).range(1..=2).speed(0.1)); @@ -520,84 +512,8 @@ fn show_rtc_offset(ui: &mut Ui, off: &mut RtcOffset, running: bool) { ); } -/// The graphics board picker unifies two independent config fields -/// (`graphics.board` and `[impact]`) into one dropdown — they claim the same -/// GIO gfx slot and `validate()` refuses a config that sets both. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum GfxChoice { - Newport, - Xz, - Extreme, - ImpactSolid, - ImpactHigh, - ImpactMax, -} - -impl GfxChoice { - const ALL: [Self; 6] = [ - Self::Newport, Self::Xz, Self::Extreme, - Self::ImpactSolid, Self::ImpactHigh, Self::ImpactMax, - ]; - - fn from_cfg(cfg: &MachineConfig) -> Self { - match cfg.impact.gfx { - ImpactSlot::Solid => Self::ImpactSolid, - ImpactSlot::High => Self::ImpactHigh, - ImpactSlot::Max => Self::ImpactMax, - ImpactSlot::None => match cfg.graphics.board { - GraphicsBoard::Newport => Self::Newport, - GraphicsBoard::Xz => Self::Xz, - GraphicsBoard::Extreme => Self::Extreme, - }, - } - } - - fn label(self) -> &'static str { - match self { - Self::Newport => "Newport (XL)", - Self::Xz => "GR2 XZ", - Self::Extreme => "GR2 Extreme", - Self::ImpactSolid => "IMPACT Solid", - Self::ImpactHigh => "IMPACT High", - Self::ImpactMax => "IMPACT Maximum", - } - } - - /// Whether `validate()` accepts this choice on `profile`. - fn supports(self, profile: MachineProfile) -> bool { - match self { - Self::Newport | Self::Xz => true, - Self::Extreme => profile == MachineProfile::Indigo2Ip22, - Self::ImpactSolid | Self::ImpactHigh | Self::ImpactMax => { - matches!(profile, MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28) - } - } - } - - fn is_impact(self) -> bool { - matches!(self, Self::ImpactSolid | Self::ImpactHigh | Self::ImpactMax) - } - - fn apply(self, cfg: &mut MachineConfig) { - cfg.graphics.board = match self { - Self::Xz => GraphicsBoard::Xz, - Self::Extreme => GraphicsBoard::Extreme, - Self::Newport | Self::ImpactSolid | Self::ImpactHigh | Self::ImpactMax => GraphicsBoard::Newport, - }; - cfg.impact = ImpactSection { - gfx: match self { - Self::ImpactSolid => ImpactSlot::Solid, - Self::ImpactHigh => ImpactSlot::High, - Self::ImpactMax => ImpactSlot::Max, - Self::Newport | Self::Xz | Self::Extreme => ImpactSlot::None, - }, - ..Default::default() - }; - } -} - fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { - let before = GfxChoice::from_cfg(cfg); + let before = cfg.graphics.board; let mut choice = before; ui.horizontal(|ui| { ui.label("Graphics board"); @@ -605,7 +521,7 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { ComboBox::from_id_salt("graphics_board") .selected_text(choice.label()) .show_ui(ui, |ui| { - for c in GfxChoice::ALL { + for c in GraphicsBoard::ALL { ui.add_enabled_ui(c.supports(cfg.machine.profile), |ui| { ui.selectable_value(&mut choice, c, c.label()) .on_disabled_hover_text("Indigo2 only"); @@ -614,12 +530,11 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { }); }); }); - // Every non-Newport choice is single-head with no VC2 presets, and GR2 / - // IMPACT share the gfx slot; clear those so the config still passes - // `validate()`. + // Every non-Newport choice is single-head with no VC2 presets; + // clear those so the config still passes `validate()`. if choice != before { - choice.apply(cfg); - if choice != GfxChoice::Newport { + cfg.graphics.board = choice; + if choice != GraphicsBoard::Newport { cfg.graphics.heads = 1; cfg.graphics.resolution = NewportResolution::Guest; } @@ -631,7 +546,7 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { .small(), ); } - if matches!(choice, GfxChoice::Xz | GfxChoice::Extreme) { + if matches!(choice, GraphicsBoard::Xz | GraphicsBoard::Extreme) { ui.label( RichText::new( "GR2 is newer than Newport: PROM, textport, X and GL work, with gaps. \ @@ -661,7 +576,7 @@ fn show_board_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { } fn show_resolution_picker(ui: &mut Ui, cfg: &mut MachineConfig, running: bool) { - let newport = cfg.graphics.board == GraphicsBoard::Newport && !cfg.headless && !cfg.impact.any_enabled(); + let newport = cfg.graphics.board == GraphicsBoard::Newport && !cfg.headless; ui.horizontal(|ui| { ui.label("Display resolution"); if !newport { diff --git a/iris-gui/src/dialogs/new_machine.rs b/iris-gui/src/dialogs/new_machine.rs index 1b392d24..3eb40987 100644 --- a/iris-gui/src/dialogs/new_machine.rs +++ b/iris-gui/src/dialogs/new_machine.rs @@ -336,10 +336,9 @@ impl NewMachineDialog { }; // The IP28 IRIX kernel carries no Newport driver at all — // default straight to IMPACT so a fresh machine has a working - // display instead of a blank Newport head. See config_ui.rs's - // GfxChoice picker, which the user can still change afterwards. + // display instead of a blank Newport head. if self.profile == MachineProfile::Indigo2Ip28 { - cfg.impact.gfx = iris::config::ImpactSlot::Solid; + cfg.graphics.board = iris::config::GraphicsBoard::SolidImpact; } // SCSI defaults: drop the built-in entries unless the // user explicitly opted in. diff --git a/iris-gui/src/framebuffer.rs b/iris-gui/src/framebuffer.rs index 750ccb81..616a431b 100644 --- a/iris-gui/src/framebuffer.rs +++ b/iris-gui/src/framebuffer.rs @@ -96,7 +96,7 @@ impl Renderer for CaptureRenderer { return; } - let needed = 2048 * 1024; + let needed = iris::disp::FB_STRIDE * iris::disp::FB_MAX_H; if self.last_pixels.len() < needed { self.last_pixels.resize(needed, 0); } @@ -160,7 +160,7 @@ impl Renderer for GlCaptureRenderer { return; } - let needed = 2048 * 1024; + let needed = iris::disp::FB_STRIDE * iris::disp::FB_MAX_H; if self.last_pixels.len() < needed { self.last_pixels.resize(needed, 0); } diff --git a/iris.toml b/iris.toml index 60755f19..3058140e 100644 --- a/iris.toml +++ b/iris.toml @@ -219,6 +219,7 @@ 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" # heads 1 or 2; resolution "guest" (IRIX/PROM programs VC2), "1024x768", # "1280x960" or "1280x1024". #[graphics] diff --git a/rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md b/rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md index 529cf918..6c7fe692 100644 --- a/rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md +++ b/rules/gui/gfxdisplay-is-generic-new-boards-need-no-gui-wiring.md @@ -13,11 +13,9 @@ So when a new board lands in core and implements `GfxDisplay` (`src/gfx_display.rs`), the GUI's framebuffer path picks it up automatically. What *does* need GUI-side work for a new board: -- A way to select it that doesn't collide with the existing board field(s). - IMPACT lives in a separate `[impact]` config section from `[graphics].board` - (they share the GIO gfx slot and `validate()` refuses both at once), so - `iris-gui/src/config_ui.rs` unifies them into one `GfxChoice` picker rather - than adding a second dropdown — see `show_board_picker`. +- A way to select it in `[graphics] board`. + `iris-gui/src/config_ui.rs` iterates `GraphicsBoard::ALL` with `label()` and + `supports(profile)` in `show_board_picker`. - Hiding controls that don't apply (Newport heads, VC2 resolution presets) when the new board is active. - Any config field the new board doesn't share with existing ones (bank diff --git a/rules/mgras/DESIGN.md b/rules/mgras/DESIGN.md new file mode 100644 index 00000000..3d055a17 --- /dev/null +++ b/rules/mgras/DESIGN.md @@ -0,0 +1,192 @@ +MGRAS (IMPACT / GR4) design + +References: ignore/gr4/*.h (hardware headers), decompiled kernel modules in +ignore/gr4/mgras_kernel/decomp, libGLcore and the X DDX under ignore/gr4. +The headers were partly generated from this emulator's own early code, and +none of the binaries carry type DWARF, so struct field names in the +decompiles are the headers' guesses. Check raw offsets, not names (see +dcb-device-numbers.md). Hardware findings go back into the headers. + +Same shape as GR2 (rules/gr2/DESIGN.md): CPU thread, a frontend thread, a +backend thread and a display thread, with two FIFOs. + +blocks (src/dev/mgras): + mod.rs Mgras: bus decode, shared state, threads, sync rules + hq3.rs HQ3 engine, frontend thread: CFIFO word parser, command + processor tokens (HLE), host DMA (HAG), formatter, context + switch. GE11 HLE goes here later, as GE7 lives in hq2.rs. + ge11.rs GE11 diagnostic port and microcode storage (inert) + rss.rs raster subsystem, backend thread: RE4 register file and + primitives (block, line, stipple, transfers; later triangles), + PP1 pixel ops, the framebuffer planes. TE1 joins it later. + dcb.rs display control bus decode; vc3/xmap/cmap/dac chips (inert, + CPU thread) + disp.rs display thread: retrace, then a frame snapshot + compose, present + frame.rs display composition, Newport-style: snapshot after vblank + decodes planes, per-pixel window IDs (VC3 runs), per-DID + descriptors (XMAP), cmap, 8-bit gamma and cursor into flat + buffers; one pass per pixel (cursor, overlay, main, gamma) + makes the frame. Shader-ready: per-pixel work reads only its + own position in the buffers plus small tables. + debug.rs monitor commands and annotated trace + record.rs replayable recording + replay test (regression goldens) + +data path: + CPU -> DCB (0x60000), dcbctrl (0x68000), HQ3 ucode RAM, privileged flag + and enable registers, GE diag port: direct, CPU thread + CPU -> CFIFO (user 0x70080, privileged 0x50080, 0x80000 window) + -> hq_fifo -> HQ3 thread -> rss_fifo + CPU -> RSS registers (0x7C000, +0x1000 = execute) -> hq_fifo too: the + HQ3 forwards them, so they stay in program order with CFIFO + raster commands (the old model ran both synchronously) + RSS thread owns the framebuffer; the display thread reads it unlocked + (tearing tolerated, as REX3 and GR2) + +sync rules: + - every producer pushes while holding `submit` (the CPU's bus accesses, + the display thread's frame tick). Holding `submit` with the board idle + means nothing can change engine state: that is how reads of drawing + state, checkpoints, composites and the monitor touch the engines. The + HQ3 thread never takes `submit` (its holder may be waiting for idle); + DMA recording goes through `mem_rec`. + - idle = hq_fifo empty, HQ3 not busy, then rss_fifo empty, RSS not busy, + checked in that order (whatever the HQ3 forwarded before going idle is + already in rss_fifo). + - RSS register reads, the PIO read pair and anything whose answer depends + on drawing wait (bus busy) until both FIFOs are idle. A PIO high read + then queues an advance op, like GR2's READ_ADVANCE. + - flags are atomic: the HQ3 thread raises them (done, swap, context + loaded), the CPU sets and clears them. Whoever changes them recomputes + the general interrupt level. + - host DMA runs on the HQ3 thread. Host to board: memory is read there + and pixel lines go to the RSS as payload entries. Board to host: drain + the RSS, read the framebuffer, write memory, then raise the flag. + - ordering markers go through hq_fifo: context switch, PIO read advance. + Host GL composite waits for idle under `submit` and draws directly. + (Frame ticks are gone: block type 0 turned out to be a plain fill, so + nothing waits for "char data that may follow" any more.) + - status, FIFO status, DMA busy, DMA/RE-interface context and raster reads + wait for idle; flag reads do not (drivers poll flags until the HQ3 + raises them, as on hardware). + A threaded desktop boot polls the flags ~640k times (single-threaded: + a few hundred) at no cost in boot time. If it ever matters, stall the + flag read while the HQ3 has work, as GR2 does for FIN2. + +state: RSS, HQ3 and DCB state are plain data (no Box/Vec/HashMap), built +in place with Arc::new_zeroed, C-style structs in case of a JIT later. + +regression: tools/mgras-capture.py records a session from power-on +(IRIS_MGRAS_REC) with hash checkpoints (scripted marks plus one every 250k +records); record.rs replays it through the bus interface and checks every +checkpoint. Goldens live in ignore/mgras-golden (too big for git): +desktop.rec and dense.rec were recorded by the single-threaded model. +Current golden: desktop2.rec (after the points / skip-last / block type 0 +fixes). desktop, dense, threaded and flat predate them. +Deliberate behaviour changes (2D fixes) make old goldens fail from the +first checkpoint after the changed drawing: check the replayed screen +against the capture's PNGs (diff the changed pixels), then re-capture. +Every refactor step must replay them bit-exact: + MGRAS_REPLAY=ignore/mgras-golden/dense.rec cargo test --release mgras_replay_file -- --nocapture + +phases: + - goldens from the single-threaded model + - split into the files above, no threads, goldens pass + - HQ3 + RSS + display threads, goldens pass, live boot to desktop + - flatten state to plain data, goldens pass + - monitor commands and annotated trace (gr2 parity): `mgras help`, + `mgras stats` (command/primitive counters), `mgras trace + [hq,rss,cpu|all]` (IRIS_MGRAS_TRACE from power-on), `rss regs|pix` + - trace-replay test architecture: + rss.rs tests RSS semantics on a bare `Rss` (no threads) + mgras_tests.rs the live board through the bus: FIFO/direct + ordering, PIO reads, DMA both ways (fake + memory + page table), flags, context switch, + GE11 readback, text-recording replay + testdata/*.txt small captures in the recording's text form + (mgras rec .txt, or tools/mgras-rec.py + text to slice a binary one); replayed with + record::tests::replay_recs, no checkpoints + (they start mid-session), then asserted + ignore/mgras-golden power-on goldens, every checkpoint checked + Not yet: snapshots (save_state is empty). With a non-COW disk a restore + would leave the filesystem behind RAM, and the framebuffer belongs in + the machine's chunk manifest like REX3's; do both together. + - 2D: tools/xwd-compare.py checks XGetImage (xwd -root, fetched over the + built-in NFS share) against `mgras fbdump`; 8-bit dumps match exactly, + and xwud (XPutImage) redisplays them exactly apart from the cursor + - framebuffer 2048x2048 (for 1600x1200 and 1920x1035), indexed by the + hardware's (x, y) with the screen bottom-left. state_bytes hashes the + old 1280x1024 region, plus the rest only when it is non-zero, so older + goldens keep their hashes. Shared display buffers/textures: + disp::FB_MAX_H rows (Newport's input textures stay 1024). Big state is + built in place (Arc/Box::new_zeroed), never by value: never derive + Clone/Default on Rss/Frame or build them as struct literals. + - OpenGL (2026-10-04): GE11 HLE in the HQ thread (gl.rs) on the shared + GL core (src/dev/gl: matrices/stacks, clipping, viewport, lighting, + fog; copied from gr2, gr2 moves onto it next). It sets up triangles + through the RE4 area registers like SGI's diag _mg0_FillTriangle; + rss.rs rasterizes them (GL half-open rule, colour planes rebuilt from + start/dx/de). Area IR opcodes 0/1 are ours (real numbers unknown). + The GL window (origin, masks, DRB pointers) comes from the kernel: + token 0xE4 or the 63-word context image with word 1 bit 29 set (layout + in ignore/gr4/HQ3.h). Each GL batch loads it inside a gl_enter / + gl_leave bracket that the RSS uses to save and restore the raster + registers the X server set (GL_BRACKET in rss.rs). + - contexts (HQ3.h "CONTEXT MANAGEMENT"): real hardware keeps each + context's GE state on the board, in an ERAM slot whose address is image + word 0, and the HQ saves its host-interface (parser) state with it. So + do we: Hq3Engine::eram (2 x 64K words; only GE0's is used) holds, per + slot, the Gl as raw words and the user-port parser. Saved at the switch + request, loaded from the image's slot (fresh on word 1 bit 31). The GE + pass-through tokens (0xFA-0xFD) move ERAM to and from host memory by + DMA or FIFO pixel data, which is how the kernel evicts slots when more + than about ten contexts are open (tested: ideas + atlantis + 8 gltests, + thousands of evictions, state intact). Gl must stay plain numbers: + ERAM is guest-visible, and loads clamp what indexes arrays. + - raster state across switches: the hardware saves none. The load writes + the incoming window registers (plus, for GL, the X server's + screen-wide registers from image words 18-39); the GE regenerates GL + raster state; the X server sets its mode registers per operation. We + apply only the window from the image: each GL batch loads its full + raster state from Gl (begin_raster), and the RSS's gl_enter / gl_leave + bracket keeps the X server's registers as X left them, so X never sees + GL's values. The bracket covers every raster register except the TE's + (the GE keeps those loaded across batches) and the model-private ones: + X sets the IR, colours and modes only when they change, so anything GL + leaves behind gets used. A hand-picked list missed the IR; after a + switch back from gltest, X's line executes redrew GL's last 800x600 + triangle over the desktop (gl_batch_leaves_x_instruction_alone). + glFinish = __MGR_RETURN_MODE round trip (answered 0, address logged). + glprim flat and smooth triangles render live on 6.5.22. + - then 3D + +pixel memory (Octane technical report section 4.4.14, Table 4.2; our +reading): RDRAM bytes are 9 bits, and the framebuffer is built from 36-bit +buffers and one 9-bit buffer a pixel. The 9-bit one is only an 8-bit CI +overlay plus 1 spare bit. Each 36-bit buffer is one of 12/12/12 RGB, +8888 RGBA, 5551 or 4444 double buffered, 12-bit CI double buffered, or +ZST (24-bit Z + 8-bit stencil). Spare bits (not 12/12/12) are per-pixel +tags: the displayed half of independently swapped double-buffered windows, +fast clears. Count by resolution (High/Maximum): 1024x768 3/6, +1280x1024 2/4, 1600x1200 and 1920x1035 1/2. 12MB x 9 bits fits +1280x1024 x (2x36 + 9) exactly. Confirmed from the IDE diags' +_xy_to_rdram and the PROM timing tables (full layout in ignore/gr4/PP1.h): +there is no linear framebuffer; a buffer is a run of 2KB RDRAM pages +(448 at 1280x1024, 288 at 1024x768, tiles of 192x16 pixels), and +DRBpointers / the XMAP DIB pointer hold page numbers. Off-screen memory +(back buffer, pbuffer) is therefore another buffer's pages on the same +(x, y) grid, not rows beyond the screen. Pixels are still stored as the +old u32 values in a word's low bits; double buffering, Z and 12-bit +colour need the 36-bit word formats per buffer use. + +pixel memory model (pixmem.rs, 2026-10-03): 1024 pages x 3072 u64 words, +a page = one tile; a 36-bit buffer reads a page as 192x16 pixels, the +overlay as 768x16, four 9-bit pixels a word (shift 9 * (x & 3)). Drawing +goes to the page in DRBpointers[9:0], DrawBuffer picking overlay vs wide; +scanout to the XMAP DIB pointers ([9:0] main, [31:20] overlay; PROM value +until written); DRBsize gives tiles per row (PROM 1280x1024 layout until +written). state_bytes rebuilds the old 1280x1024 fb/overlay views through +the scanout pointers plus a hash of any other set bit, so desktop2.rec +still replays 35/35. Not yet: the second pointer field ([19:10]), double +buffering, Z/stencil, the raw RDRAM window (device space is still a +register map), two raster engines. diff --git a/rules/mgras/dcb-device-numbers.md b/rules/mgras/dcb-device-numbers.md new file mode 100644 index 00000000..872ed6ee --- /dev/null +++ b/rules/mgras/dcb-device-numbers.md @@ -0,0 +1,20 @@ +DCB device numbers: trust the kernel's dcbctrl writes, not header names + +The display control bus maps device N at 0x60000 + N*0x400, with its +protocol timing register at 0x68000 + N*4. MgrasInitBoardGfx +(mgras_init.c ~line 650) writes the timing words: + + slot 3,4,5 0x108C -> CMAP broadcast, CMAP0, CMAP1 (identical timing) + slot 6 0x1084 -> DAC + slot 7 0x318C -> XMAP (in the PP1s; slowest CS hold) + slot 8 0x210A -> VC3 (the only async-ack device) + slot 9 0x1090 -> board version + slot 0 never written + +An RE pass renumbered the devices from MGRAS.h (XMAP 0, DAC 4, CMAPs 5/6/7) +and broke the display. MGRAS.h's struct and the decompile's field names +(dcbctrl_cmap1 at 0x6801C, ...) are wrong: the binaries have no type DWARF, +so those names came from the generated header itself. + +General rule: in ignore/gr4 decompiles, check raw offsets and values; a +field name is only the header's guess. diff --git a/rules/mgras/header-errata.md b/rules/mgras/header-errata.md new file mode 100644 index 00000000..4b32bfb8 --- /dev/null +++ b/rules/mgras/header-errata.md @@ -0,0 +1,30 @@ +ignore/gr4 headers: known errata (fold back into the headers) + +The headers were partly generated from this emulator's early code and from +guesses, and no binary carries type DWARF, so they are hints until checked +against raw offsets in the decompiles or against what boots. Found so far: + +- DCB device numbers (MGRAS.h struct, decompile field names): were wrong. Real: + CMAP all/0/1 = 3/4/5, DAC = 6, XMAP = 7, VC3 = 8, BDVERS = 9. See + dcb-device-numbers.md. +- Command FIFO ports (HQ3.h HQ3_CFIFO_USER 0x700F0, HQ3_CFIFO_PRIV + 0x500F0): the drivers write 0x70080/0x70084 (user) and 0x50080/0x50084 + (privileged). +- Context switch request (HQ3.h HQ3_CTXSW 0x500F8): the kernel writes + 0x50050. +- Raster commands through the FIFO (HQ3.h HQ_CMD_RSS_WRITE(reg) = + 0x1000 | reg << 2): the command word is (0x1000 | reg | 0x400 execute) + << 8 | byte count. +- IR opcodes (RE4.h had TRIANGLE 1, LINE 2, BLOCK 3, CHAR 4, SPAN 5): 4 is a + point (at xline_xystarti), 5 the X line (IR_OP_X_LINE, with Setup), 8 the + block (IR_OP_BLOCK). Fixed in RE4.h. +- Block type 0 is a fill, not "wait for char data"; char stipples are block + type 1 with CharStipEn (fill mode bit 3). Fill mode bit 10 skips a line's + last pixel. Fixed in RE4.h. +- The mgras_hw struct had the DCB devices and the FIFO ports in the 1994 + diagnostic order/addresses. Fixed in MGRAS.h (offsets checked with + offsetof); HQ3.h's FIFO section rewritten (word formats, command space, + CP_* and GL token tables). + +What the headers get right and the code relies on: the rss_single register +file layout (names and offsets of every register the model uses). diff --git a/src/compositor.rs b/src/compositor.rs index 1122ba67..85b5c808 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -74,7 +74,7 @@ pub trait Compositor: Send { /// Pure-CPU compositor. Runs the per-pixel loop into a `Vec`, uploads to /// a GL texture via `glTexSubImage2D`, and returns that texture. pub struct SwCompositor { - /// CPU pixel buffer: stride-2048 array, exactly 2048×1024 words. + /// CPU pixel buffer: stride 2048, `FB_MAX_H` rows. buf: Vec, /// GL texture that holds the last uploaded frame. tex: Option, @@ -83,7 +83,7 @@ pub struct SwCompositor { impl SwCompositor { pub fn new() -> Self { Self { - buf: vec![0u32; 2048 * 1024], + buf: vec![0u32; crate::disp::FB_STRIDE * crate::disp::FB_MAX_H], tex: None, } } @@ -101,7 +101,7 @@ impl SwCompositor { gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32); gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32); gl.tex_image_2d(glow::TEXTURE_2D, 0, glow::RGBA as i32, - 2048, 1024, 0, glow::RGBA, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice(None)); + 2048, crate::disp::FB_MAX_H as i32, 0, glow::RGBA, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice(None)); t }; self.tex = Some(t); diff --git a/src/config.rs b/src/config.rs index 442fe483..f46cb3e3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -424,72 +424,112 @@ impl MachineProfile { } /// Indy / Indigo2 graphics board in the GIO gfx slot. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)] pub enum GraphicsBoard { /// Newport (REX3) — fully emulated. Default. #[default] + #[clap(name = "newport", alias = "xl")] Newport, /// GR2 XZ (2 GE7) — Indy XZ or Indigo2 XZ (`src/dev/gr2`). In bring-up. + #[clap(name = "xz", alias = "gr2", alias = "gr2_xz", alias = "gr2-xz")] Xz, /// GR2 Extreme (8 GE7) — Indigo2 only (`src/dev/gr2`). In bring-up. + #[clap(name = "extreme", alias = "gr2_extreme", alias = "gr2-extreme")] Extreme, + /// Solid IMPACT (1 GE11, 1 RE4). + #[clap(name = "solidimpact", alias = "impact:solid", alias = "solid_impact")] + SolidImpact, + /// High IMPACT (2 GE11, 1 RE4). + #[clap(name = "highimpact", alias = "impact:high", alias = "high_impact")] + HighImpact, + /// Maximum IMPACT (2 GE11, 2 RE4). + #[clap(name = "maximpact", alias = "impact:max", alias = "max_impact")] + MaxImpact, } impl GraphicsBoard { - pub const ALL: [Self; 3] = [Self::Newport, Self::Xz, Self::Extreme]; + pub const ALL: [Self; 6] = [ + Self::Newport, + Self::Xz, + Self::Extreme, + Self::SolidImpact, + Self::HighImpact, + Self::MaxImpact, + ]; + pub fn label(self) -> &'static str { match self { Self::Newport => "Newport (XL)", Self::Xz => "GR2 XZ", Self::Extreme => "GR2 Extreme", + Self::SolidImpact => "IMPACT Solid", + Self::HighImpact => "IMPACT High", + Self::MaxImpact => "IMPACT Maximum", } } + + pub fn as_str(self) -> &'static str { + match self { + Self::Newport => "newport", + Self::Xz => "xz", + Self::Extreme => "extreme", + Self::SolidImpact => "solidimpact", + Self::HighImpact => "highimpact", + Self::MaxImpact => "maximpact", + } + } + + pub fn is_impact(self) -> bool { + matches!(self, Self::SolidImpact | Self::HighImpact | Self::MaxImpact) + } + /// Whether `validate()` accepts this board on `profile`. pub fn supports(self, profile: MachineProfile) -> bool { self != Self::Extreme || profile == MachineProfile::Indigo2Ip22 } } -/// IMPACT board occupying one GIO64 slot (Indigo2 preview scaffold). -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum ImpactSlot { - #[default] - None, - Solid, - High, - Max, +impl std::str::FromStr for GraphicsBoard { + type Err = String; + + fn from_str(s: &str) -> Result { + let lower = s.trim().to_ascii_lowercase(); + match lower.as_str() { + "newport" | "xl" => Ok(Self::Newport), + "xz" | "gr2" | "gr2_xz" | "gr2-xz" => Ok(Self::Xz), + "extreme" | "gr2_extreme" | "gr2-extreme" => Ok(Self::Extreme), + "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), + _ => Err(format!( + "invalid graphics board \"{s}\" (valid: newport, xz, extreme, solidimpact, highimpact, maximpact, impact:solid, impact:high, impact:max)" + )), + } + } } -/// `[impact]` section — IMPACT/MGRAS slot population (Indigo2 IP22 preview). -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(deny_unknown_fields)] -pub struct ImpactSection { - /// GIO gfx slot (`0x1F000000`). Solid IMPACT anchors here. - #[serde(default)] - pub gfx: ImpactSlot, - /// GIO expansion slot 0 (`0x1F400000`). Second board for High / Max configs. - #[serde(default)] - pub exp0: ImpactSlot, - /// GIO expansion slot 1 (`0x1F600000`). Third board for Maximum IMPACT. - #[serde(default)] - pub exp1: ImpactSlot, +impl Serialize for GraphicsBoard { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } } -impl ImpactSection { - pub fn any_enabled(&self) -> bool { - self.gfx != ImpactSlot::None - || self.exp0 != ImpactSlot::None - || self.exp1 != ImpactSlot::None +impl<'de> Deserialize<'de> for GraphicsBoard { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse::().map_err(serde::de::Error::custom) } +} - /// One IMPACT board, in the graphics slot; a second head is not modelled yet. - pub fn validate(&self) -> Result<(), String> { - if self.exp0 != ImpactSlot::None || self.exp1 != ImpactSlot::None { - return Err("[impact] only the graphics slot (gfx) is supported so far".into()); - } - Ok(()) +impl std::fmt::Display for GraphicsBoard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) } } @@ -1092,10 +1132,6 @@ pub struct MachineConfig { #[serde(default)] pub graphics: GraphicsSection, - /// IMPACT/MGRAS slot population (`[impact]` section, Indigo2 preview). - #[serde(default)] - pub impact: ImpactSection, - /// Misc debug/capture runtime tuning (`[debug]` section). #[serde(default)] pub debug: DebugConfig, @@ -1224,7 +1260,6 @@ impl Default for MachineConfig { audio: AudioConfig::default(), machine: MachineSection::default(), graphics: GraphicsSection::default(), - impact: ImpactSection::default(), debug: DebugConfig::default(), jitv2: Jitv2Config::default(), perf: PerfConfig::default(), @@ -1284,7 +1319,7 @@ impl MachineConfig { )); } if self.graphics.board != GraphicsBoard::Newport { - let name = if self.graphics.board == GraphicsBoard::Xz { "xz" } else { "extreme" }; + let name = self.graphics.board.as_str(); if self.graphics.board == GraphicsBoard::Extreme && self.machine.profile != MachineProfile::Indigo2Ip22 { return Err( "graphics.board \"extreme\" is only valid on Indigo2 (machine.profile = indigo2_ip22)".into(), @@ -1296,18 +1331,7 @@ impl MachineConfig { if !self.graphics.resolution.is_guest() { return Err("graphics.resolution presets require Newport (graphics.board = newport)".into()); } - if self.impact.any_enabled() { - return Err(format!("graphics.board \"{name}\" and [impact] both claim the GIO gfx slot")); - } - } - if self.impact.any_enabled() - && !matches!(self.machine.profile, MachineProfile::Indigo2Ip22 | MachineProfile::Indigo2Ip28) - { - return Err( - "[impact] slots need an Indigo2 (machine.profile = indigo2_ip22 or indigo2_ip28)".into(), - ); } - self.impact.validate()?; for (&id, dev) in &self.scsi { if dev.controller != 0 && self.machine.profile != MachineProfile::Indigo2Ip22 { return Err(format!( @@ -1568,6 +1592,10 @@ pub struct Cli { #[arg(long = "cpu", value_name = "MODEL")] pub cpu: Option, + /// Graphics board: `newport` (default), `xz`, `extreme`, `solidimpact`, `highimpact`, `maximpact` (or `impact:solid`, `impact:high`, `impact:max`). + #[arg(long = "graphics", visible_alias = "board", visible_alias = "graphics-board", value_name = "BOARD")] + pub graphics: Option, + /// Enable GDB stub on the given TCP port (e.g. --gdb-port 1234). /// Connect with: target remote localhost: #[arg(long = "gdb-port", value_name = "PORT")] @@ -1674,6 +1702,7 @@ impl Cli { if let Some(p) = self.scsi7.clone() { apply_scsi(&mut cfg.scsi, 7, p, false, vec![]); } if let Some(cpu) = self.cpu { cfg.machine.cpu = cpu; } + if let Some(b) = self.graphics { cfg.graphics.board = b; } if self.scale2x { cfg.scale = 2; } if self.headless { cfg.headless = true; } if self.no_audio { cfg.no_audio = true; } diff --git a/src/debug_overlay.rs b/src/debug_overlay.rs index 520657a1..5aa7916d 100644 --- a/src/debug_overlay.rs +++ b/src/debug_overlay.rs @@ -58,7 +58,7 @@ pub struct DebugOverlay { impl DebugOverlay { pub fn new() -> Self { Self { - buf: vec![0u32; 2048 * 1024], + buf: vec![0u32; crate::disp::FB_STRIDE * crate::disp::FB_MAX_H], tex: None, font: crate::vga_font::VGA_8X16.to_vec(), show_cmap: false, @@ -418,7 +418,7 @@ impl DebugOverlay { gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32); gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32); gl.tex_image_2d(glow::TEXTURE_2D, 0, glow::RGBA as i32, - 2048, 1024, 0, glow::RGBA, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice(None)); + 2048, crate::disp::FB_MAX_H as i32, 0, glow::RGBA, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice(None)); t }; self.tex = Some(t); diff --git a/src/dev/gl/light.rs b/src/dev/gl/light.rs new file mode 100644 index 00000000..56122850 --- /dev/null +++ b/src/dev/gl/light.rs @@ -0,0 +1,312 @@ +//! The OpenGL 1.1 lighting equation and fog, per vertex. Copied from GR2's +//! GE7 HLE (`src/dev/gr2/gl_light.rs`), without its FIFO token loader: a +//! board fills `Lighting` from its own protocol. +//! +//! Two things vary between boards' libglcore and are parameters here: +//! - specular and spot exponents arrive either as numbers (`shininess`, +//! `spot_exp`) or as uploaded pow() tables the GE interpolates (GR2: +//! `spec_table` / `spot_table` set, x mapped to (x - start) * scale); +//! - GR2's libglcore folds the lights' ambient terms into the scene ambient +//! when it can (`ambient_folded`); otherwise each light adds its own. + +pub const MAX_LIGHTS: usize = 8; +pub const SPEC_N: usize = 128; +pub const SPOT_N: usize = 32; + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Material { + pub emission: [f32; 3], + pub ambient: [f32; 3], + pub diffuse: [f32; 4], + pub specular: [f32; 3], + pub shininess: f32, + /// Non-zero: the specular factor comes from `spec` (see module docs). + pub spec_table: u32, + pub spec_scale: f32, + pub spec_start: f32, + pub spec: [f32; SPEC_N], +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Light { + pub ambient: [f32; 3], + pub diffuse: [f32; 3], + pub specular: [f32; 3], + /// Eye space; w = 0: direction to the light. + pub pos: [f32; 4], + pub spot_dir: [f32; 3], + /// linear, quadratic, constant + pub atten: [f32; 3], + pub spot_on: u32, + pub spot_exp: f32, + /// cos(GL_SPOT_CUTOFF). + pub spot_cos_cutoff: f32, + pub spot_table: u32, + pub spot_scale: f32, + pub spot_start: f32, + pub spot: [f32; SPOT_N], + /// Next enabled light (-1 = end), from `Lighting::head`. + pub next: i32, +} + +/// Lighting and fog state. Plain data, valid when zeroed (see `init`). +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Lighting { + pub on: u32, + pub two_sided: u32, + pub local_viewer: u32, + /// Scene ambient (with the lights' ambient folded in when + /// `ambient_folded`). + pub amb_sum: [f32; 3], + pub ambient_folded: u32, + pub mat: [Material; 2], + pub lights: [Light; MAX_LIGHTS], + pub head: i32, + pub count: u32, + /// Colour material: param 1 emission 2 ambient 3 diffuse 4 specular 5 + /// ambient+diffuse; face 0 front 1 back 2 both. + pub cmat_param: u32, + pub cmat_face: u32, + pub cmat_on: u32, + pub normal_matrix: [f32; 9], + pub normalize: u32, + pub fog_on: u32, + /// Fog: mode (0 linear, 1 EXP, 2 EXP2), then linear: end, 1 / (end - + /// start); EXP/EXP2: density; then the colour RGB. + pub fog_mode: u32, + pub fog_a: f32, + pub fog_b: f32, + pub fog_color: [f32; 3], +} + +fn dot(a: [f32; 3], b: [f32; 3]) -> f32 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +fn norm(a: [f32; 3]) -> [f32; 3] { + let l = dot(a, a).sqrt(); + if l > 1e-20 { [a[0] / l, a[1] / l, a[2] / l] } else { a } +} + +/// Table lookup as the GE7 does it: x mapped to (x - start) * scale, +/// linearly interpolated, 0 below the table. +fn table(t: &[f32], start: f32, scale: f32, x: f32) -> f32 { + let p = (x - start) * scale; + if !p.is_finite() || p < 0.0 { + return 0.0; + } + let last = (t.len() - 1) as f32; + if p >= last { + return t[t.len() - 1]; + } + let i = p.floor() as usize; + let fr = p - i as f32; + t[i] + (t[i + 1] - t[i]) * fr +} + +impl Lighting { + /// OpenGL's initial state (no lights enabled). + pub fn init(&mut self) { + self.head = -1; + for l in self.lights.iter_mut() { + l.next = -1; + l.atten = [0.0, 0.0, 1.0]; + l.pos = [0.0, 0.0, 1.0, 0.0]; + l.spot_dir = [0.0, 0.0, -1.0]; + l.spot_cos_cutoff = -1.0; + } + self.lights[0].diffuse = [1.0; 3]; + self.lights[0].specular = [1.0; 3]; + self.amb_sum = [0.2; 3]; + self.normal_matrix = [1., 0., 0., 0., 1., 0., 0., 0., 1.]; + for m in self.mat.iter_mut() { + m.ambient = [0.2; 3]; + m.diffuse = [0.8, 0.8, 0.8, 1.0]; + m.spec_scale = 1.0; + } + self.fog_a = 1.0; + self.fog_b = 1.0; + } + + /// Enabled lights, in the order of the `head` / `next` chain. + pub fn enabled(&self) -> impl Iterator { + let mut i = self.head; + let mut left = self.count.min(MAX_LIGHTS as u32); + std::iter::from_fn(move || { + if left == 0 || !(0..MAX_LIGHTS as i32).contains(&i) { + return None; + } + left -= 1; + let l = &self.lights[i as usize]; + i = l.next; + Some(l) + }) + } + + /// Colour material: while tracking is on, a colour command writes the + /// colour into the tracked properties of the active material, and the + /// change lasts until the material is loaded again (GL and IRIS GL + /// lmcolor alike). + pub fn track_color(&mut self, color: [f32; 4]) { + if self.cmat_on == 0 { + return; + } + let c3 = [color[0], color[1], color[2]]; + for face in 0..2 { + if self.cmat_face != 2 && self.cmat_face as usize != face { + continue; + } + let m = &mut self.mat[face]; + match self.cmat_param { + 1 => m.emission = c3, + 2 => m.ambient = c3, + 3 => m.diffuse = color, + 4 => m.specular = c3, + 5 => { + m.ambient = c3; + m.diffuse = color; + } + _ => {} + } + } + } + + /// Lit colours (front, back) of a vertex: `eye` = eye-space position, + /// `n_obj` = current normal, `color` = current colour. + pub fn light_vertex(&self, eye: [f32; 4], n_obj: [f32; 3], color: [f32; 4]) -> ([f32; 4], [f32; 4]) { + let nm = &self.normal_matrix; + let mut n = [ + nm[0] * n_obj[0] + nm[3] * n_obj[1] + nm[6] * n_obj[2], + nm[1] * n_obj[0] + nm[4] * n_obj[1] + nm[7] * n_obj[2], + nm[2] * n_obj[0] + nm[5] * n_obj[1] + nm[8] * n_obj[2], + ]; + if self.normalize != 0 { + n = norm(n); + } + let p = if eye[3] != 0.0 { [eye[0] / eye[3], eye[1] / eye[3], eye[2] / eye[3]] } else { [eye[0], eye[1], eye[2]] }; + let front = self.shade(0, n, p, color); + let back = if self.two_sided != 0 { self.shade(1, [-n[0], -n[1], -n[2]], p, color) } else { front }; + (front, back) + } + + fn shade(&self, face: usize, n: [f32; 3], p: [f32; 3], _color: [f32; 4]) -> [f32; 4] { + let m = &self.mat[face]; + let per_light_amb = self.ambient_folded == 0; + let mut c = [0.0f32; 3]; + for k in 0..3 { + c[k] = m.emission[k] + m.ambient[k] * self.amb_sum[k]; + } + let v = if self.local_viewer != 0 { norm([-p[0], -p[1], -p[2]]) } else { [0.0, 0.0, 1.0] }; + for l in self.enabled() { + let (lv, mut att) = if l.pos[3] == 0.0 { + (norm([l.pos[0], l.pos[1], l.pos[2]]), 1.0) + } else { + let d = [l.pos[0] - p[0], l.pos[1] - p[1], l.pos[2] - p[2]]; + let dist = dot(d, d).sqrt(); + let k = l.atten[2] + l.atten[0] * dist + l.atten[1] * dist * dist; + (norm(d), if k > 0.0 { 1.0 / k } else { 1.0 }) + }; + if l.spot_on != 0 { + let cos = -dot(lv, l.spot_dir); + att *= if l.spot_table != 0 { + table(&l.spot, l.spot_start, l.spot_scale, cos) + } else if cos < l.spot_cos_cutoff { + 0.0 + } else { + cos.max(0.0).powf(l.spot_exp) + }; + } + if att == 0.0 { + continue; + } + let ndl = dot(n, lv); + for k in 0..3 { + let mut t = 0.0; + if per_light_amb { + t += l.ambient[k] * m.ambient[k]; + } + if ndl > 0.0 { + t += ndl * l.diffuse[k] * m.diffuse[k]; + } + c[k] += att * t; + } + if ndl > 0.0 { + let h = norm([lv[0] + v[0], lv[1] + v[1], lv[2] + v[2]]); + let ndh = dot(n, h); + let s = if m.spec_table != 0 { + table(&m.spec, m.spec_start, m.spec_scale, ndh) + } else { + ndh.max(0.0).powf(m.shininess) + }; + for k in 0..3 { + c[k] += att * s * l.specular[k] * m.specular[k]; + } + } + } + [c[0].clamp(0.0, 1.0), c[1].clamp(0.0, 1.0), c[2].clamp(0.0, 1.0), m.diffuse[3].clamp(0.0, 1.0)] + } + + /// Fog factor for eye-space distance `z` (>= 0): 1 = no fog. + pub fn fog_factor(&self, z: f32) -> f32 { + let f = match self.fog_mode { + 0 => (self.fog_a - z) * self.fog_b, + 1 => (-self.fog_a * z).exp(), + _ => (-(self.fog_a * z) * (self.fog_a * z)).exp(), + }; + f.clamp(0.0, 1.0) + } + + /// Apply fog to a colour for eye-space position `eye`. + pub fn fog_color(&self, eye: [f32; 4], c: [f32; 4]) -> [f32; 4] { + if self.fog_on == 0 { + return c; + } + let z = if eye[3] != 0.0 { (eye[2] / eye[3]).abs() } else { eye[2].abs() }; + let fct = self.fog_factor(z); + let fc = self.fog_color; + [ + fct * c[0] + (1.0 - fct) * fc[0], + fct * c[1] + (1.0 - fct) * fc[1], + fct * c[2] + (1.0 - fct) * fc[2], + c[3], + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lit() -> Lighting { + let mut lt: Lighting = unsafe { std::mem::zeroed() }; + lt.init(); + lt.on = 1; + lt.head = 0; + lt.count = 1; + lt + } + + /// The default light 0 (white, from +z) on a face looking at it: the + /// material's diffuse plus its ambient times the scene and light + /// ambient (OpenGL 1.1 initial state: 0.2 * 0.2 + 0.8). + #[test] + fn default_light_on_a_facing_normal() { + let lt = lit(); + let (c, _) = lt.light_vertex([0.0, 0.0, -5.0, 1.0], [0.0, 0.0, 1.0], [1.0; 4]); + for k in 0..3 { + assert!((c[k] - 0.84).abs() < 1e-5, "{c:?}"); + } + } + + #[test] + fn linear_fog_half_way() { + let mut lt = lit(); + lt.fog_on = 1; + (lt.fog_mode, lt.fog_a, lt.fog_b, lt.fog_color) = (0, 10.0, 0.1, [1.0, 1.0, 1.0]); + let c = lt.fog_color([0.0, 0.0, -5.0, 1.0], [0.0, 0.0, 0.0, 1.0]); + assert!((c[0] - 0.5).abs() < 1e-5); + } +} diff --git a/src/dev/gl/math.rs b/src/dev/gl/math.rs new file mode 100644 index 00000000..ffccb05d --- /dev/null +++ b/src/dev/gl/math.rs @@ -0,0 +1,246 @@ +//! 4x4 matrices, column-major (element (row r, column c) at `c * 4 + r`, the +//! order glLoadMatrix takes), and the matrix stacks. + +pub type Mat4 = [f32; 16]; + +pub const IDENT: Mat4 = [1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]; + +/// a * b. +pub fn mul(a: &Mat4, b: &Mat4) -> Mat4 { + let mut m = [0.0f32; 16]; + for c in 0..4 { + for r in 0..4 { + m[c * 4 + r] = (0..4).map(|k| a[k * 4 + r] * b[c * 4 + k]).sum(); + } + } + m +} + +/// m * v. +pub fn xform(m: &Mat4, v: [f32; 4]) -> [f32; 4] { + let row = |r: usize| m[r] * v[0] + m[4 + r] * v[1] + m[8 + r] * v[2] + m[12 + r] * v[3]; + [row(0), row(1), row(2), row(3)] +} + +/// glOrtho. +pub fn ortho(l: f32, r: f32, b: f32, t: f32, n: f32, f: f32) -> Mat4 { + let mut m = IDENT; + m[0] = 2.0 / (r - l); + m[5] = 2.0 / (t - b); + m[10] = -2.0 / (f - n); + m[12] = -(r + l) / (r - l); + m[13] = -(t + b) / (t - b); + m[14] = -(f + n) / (f - n); + m +} + +/// glFrustum. +pub fn frustum(l: f32, r: f32, b: f32, t: f32, n: f32, f: f32) -> Mat4 { + let mut m = [0.0f32; 16]; + m[0] = 2.0 * n / (r - l); + m[5] = 2.0 * n / (t - b); + m[8] = (r + l) / (r - l); + m[9] = (t + b) / (t - b); + m[10] = -(f + n) / (f - n); + m[11] = -1.0; + m[14] = -2.0 * f * n / (f - n); + m +} + +/// glTranslate. +pub fn translate(x: f32, y: f32, z: f32) -> Mat4 { + let mut m = IDENT; + m[12] = x; + m[13] = y; + m[14] = z; + m +} + +/// glScale. +pub fn scale(x: f32, y: f32, z: f32) -> Mat4 { + let mut m = IDENT; + m[0] = x; + m[5] = y; + m[10] = z; + m +} + +/// glRotate: `deg` degrees about (x, y, z). +pub fn rotate(deg: f32, x: f32, y: f32, z: f32) -> Mat4 { + let len = (x * x + y * y + z * z).sqrt(); + if len < 1e-20 { + return IDENT; + } + let (x, y, z) = (x / len, y / len, z / len); + let (s, c) = deg.to_radians().sin_cos(); + let t = 1.0 - c; + [ + x * x * t + c, y * x * t + z * s, x * z * t - y * s, 0.0, + x * y * t - z * s, y * y * t + c, y * z * t + x * s, 0.0, + x * z * t + y * s, y * z * t - x * s, z * z * t + c, 0.0, + 0.0, 0.0, 0.0, 1.0, + ] +} + +/// Upper-left 3x3 of the inverse transpose of `m` (column-major), what +/// normals transform by. Identity if `m` is singular. +pub fn normal_matrix(m: &Mat4) -> [f32; 9] { + let a = |r: usize, c: usize| m[c * 4 + r]; + let cof = |r0: usize, r1: usize, c0: usize, c1: usize| a(r0, c0) * a(r1, c1) - a(r0, c1) * a(r1, c0); + // Cofactor matrix of the 3x3: (inverse transpose) = cofactors / det. + let c = [ + cof(1, 2, 1, 2), -cof(1, 2, 0, 2), cof(1, 2, 0, 1), + -cof(0, 2, 1, 2), cof(0, 2, 0, 2), -cof(0, 2, 0, 1), + cof(0, 1, 1, 2), -cof(0, 1, 0, 2), cof(0, 1, 0, 1), + ]; + let det = a(0, 0) * c[0] + a(0, 1) * c[1] + a(0, 2) * c[2]; + if det.abs() < 1e-30 { + return [1., 0., 0., 0., 1., 0., 0., 0., 1.]; + } + // c is row-major cofactors C[r][c]; column-major result element (r, c) + // = C[r][c] / det at c * 3 + r. + let mut out = [0.0f32; 9]; + for r in 0..3 { + for col in 0..3 { + out[col * 3 + r] = c[r * 3 + col] / det; + } + } + out +} + +/// Inverse of `m` (column-major), by cofactors. None if singular. +pub fn invert(m: &Mat4) -> Option { + let a = |r: usize, c: usize| m[c * 4 + r] as f64; + let mut inv = [0.0f64; 16]; + // inv[c * 4 + r] = cofactor(r, c) of the transpose = cofactor(c, r) / det. + let minor = |skip_r: usize, skip_c: usize| -> f64 { + let rows: Vec = (0..4).filter(|&r| r != skip_r).collect(); + let cols: Vec = (0..4).filter(|&c| c != skip_c).collect(); + let e = |i: usize, j: usize| a(rows[i], cols[j]); + e(0, 0) * (e(1, 1) * e(2, 2) - e(1, 2) * e(2, 1)) - e(0, 1) * (e(1, 0) * e(2, 2) - e(1, 2) * e(2, 0)) + + e(0, 2) * (e(1, 0) * e(2, 1) - e(1, 1) * e(2, 0)) + }; + let mut det = 0.0; + for c in 0..4 { + det += a(0, c) * minor(0, c) * if c % 2 == 0 { 1.0 } else { -1.0 }; + } + if det.abs() < 1e-30 { + return None; + } + for r in 0..4 { + for c in 0..4 { + let sign = if (r + c) % 2 == 0 { 1.0 } else { -1.0 }; + // (row r, column c) of the inverse = cofactor(c, r) / det. + inv[c * 4 + r] = sign * minor(c, r) / det; + } + } + let mut out = [0.0f32; 16]; + for (o, v) in out.iter_mut().zip(inv) { + *o = v as f32; + } + Some(out) +} + +/// A GL matrix stack. Plain data, valid zeroed after `init`. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Stack { + pub m: [Mat4; N], + pub top: u32, +} + +impl Stack { + pub fn init(&mut self) { + self.top = 0; + self.m[0] = IDENT; + } + + /// The top entry's index. State saved in guest-visible memory (a GE's + /// context RAM) can come back as anything: clamp rather than panic. + fn t(&self) -> usize { + (self.top as usize).min(N - 1) + } + + pub fn get(&self) -> &Mat4 { + &self.m[self.t()] + } + + pub fn load(&mut self, m: Mat4) { + let t = self.t(); + self.m[t] = m; + } + + /// glMultMatrix: top = top * m. + pub fn mult(&mut self, m: &Mat4) { + let t = self.t(); + self.m[t] = mul(&self.m[t], m); + } + + /// False on overflow (GL_STACK_OVERFLOW; the stack is unchanged). + pub fn push(&mut self) -> bool { + let t = self.t(); + if t + 1 >= N { + return false; + } + self.m[t + 1] = self.m[t]; + self.top += 1; + true + } + + /// False on underflow. + pub fn pop(&mut self) -> bool { + if self.top == 0 { + return false; + } + self.top -= 1; + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn close(a: [f32; 4], b: [f32; 4]) -> bool { + a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-5) + } + + #[test] + fn ortho_maps_the_box_to_the_unit_cube() { + let m = ortho(0.0, 400.0, 0.0, 300.0, -1.0, 1.0); + assert!(close(xform(&m, [0.0, 0.0, 0.0, 1.0]), [-1.0, -1.0, 0.0, 1.0])); + assert!(close(xform(&m, [400.0, 300.0, 0.0, 1.0]), [1.0, 1.0, 0.0, 1.0])); + } + + #[test] + fn rotate_turns_x_into_y_about_z() { + let m = rotate(90.0, 0.0, 0.0, 1.0); + assert!(close(xform(&m, [1.0, 0.0, 0.0, 1.0]), [0.0, 1.0, 0.0, 1.0])); + } + + #[test] + fn stack_push_mult_pop() { + let mut s: Stack<4> = unsafe { std::mem::zeroed() }; + s.init(); + assert!(s.push()); + s.mult(&translate(1.0, 2.0, 3.0)); + assert!(close(xform(s.get(), [0.0, 0.0, 0.0, 1.0]), [1.0, 2.0, 3.0, 1.0])); + assert!(s.pop()); + assert_eq!(*s.get(), IDENT); + assert!(!s.pop()); + } + + #[test] + fn invert_undoes_a_transform() { + let m = mul(&translate(1.0, 2.0, 3.0), &mul(&rotate(30.0, 1.0, 2.0, 3.0), &scale(2.0, 3.0, 4.0))); + let i = invert(&m).unwrap(); + let p = xform(&i, xform(&m, [0.5, -1.0, 2.0, 1.0])); + assert!(close(p, [0.5, -1.0, 2.0, 1.0]), "{p:?}"); + } + + #[test] + fn normal_matrix_of_a_scale_is_its_inverse() { + let n = normal_matrix(&scale(2.0, 4.0, 1.0)); + assert_eq!([n[0], n[4], n[8]], [0.5, 0.25, 1.0]); + } +} diff --git a/src/dev/gl/mod.rs b/src/dev/gl/mod.rs new file mode 100644 index 00000000..08a54ca2 --- /dev/null +++ b/src/dev/gl/mod.rs @@ -0,0 +1,17 @@ +//! Board-independent OpenGL core for the high-level geometry engines: what +//! the GE microcode of every SGI board does the same way. GR4 (IMPACT's +//! GE11) builds on it; GR2 (GE7, `src/dev/gr2/gl.rs`) is where it was copied +//! from and moves onto it next. +//! +//! - `math`: 4x4 matrices (column-major, glLoadMatrix order), the GL matrix +//! builders and matrix stacks. +//! - `vertex`: the transformed vertex, view-volume and user clip planes, +//! polygon clipping, the viewport mapping. +//! - `light`: the OpenGL 1.1 lighting equation and fog. +//! +//! Board specifics stay with the boards: how state arrives (FIFO token +//! encodings), the raster engine's input formats, window clipping. + +pub mod light; +pub mod math; +pub mod vertex; diff --git a/src/dev/gl/vertex.rs b/src/dev/gl/vertex.rs new file mode 100644 index 00000000..64c3f7da --- /dev/null +++ b/src/dev/gl/vertex.rs @@ -0,0 +1,225 @@ +//! The transformed vertex, clipping and the viewport mapping. + +/// A vertex after transform: window coordinates (GL: y up), colour 0..1. +#[derive(Clone, Copy, Default, Debug)] +#[repr(C)] +pub struct Wv { + pub x: f32, + pub y: f32, + pub z: f32, + pub c: [f32; 4], + /// Back-face colour (two-sided lighting; = c otherwise). + pub cb: [f32; 4], + /// 0 when the vertex is behind the eye (w <= 0): it has no window + /// position and its primitive must be clipped. + pub ok: u32, + /// Clip planes the vertex is outside of (bit i = plane i), computed once + /// at transform; 0 for vertices made by clipping or in window space. + pub oc: u32, + /// Clip-space position (frustum clipping) and eye-space position (user + /// clip planes). Both are linear in the object position, so clipping + /// interpolates them along an edge exactly. + pub h: [f32; 4], + pub e: [f32; 4], + /// Texture coordinates (s, t, r, q), after the texture matrix. + pub t: [f32; 4], + /// Fog factor (1 = no fog), when fog is applied per fragment. + pub f: f32, +} + +/// Clip planes: 0..5 the view volume -w <= x, y, z <= w, 6..11 the user +/// planes (eye space). +pub const CLIP_PLANES: usize = 12; + +/// Largest polygon `clip_polygon` handles: the input plus one vertex per +/// plane. +pub const MAX_POLY: usize = 32 + CLIP_PLANES; + +/// Point on a-b at the crossing of a plane where the signed distances are +/// da and db: positions and colours interpolated, window position still to +/// be computed (`Viewport::project`). +pub fn clip_cross(a: &Wv, b: &Wv, da: f32, db: f32) -> Wv { + let t = da / (da - db); + let l = |x: f32, y: f32| x + (y - x) * t; + let mut v = *a; + v.oc = 0; + for k in 0..4 { + v.h[k] = l(a.h[k], b.h[k]); + v.e[k] = l(a.e[k], b.e[k]); + v.c[k] = l(a.c[k], b.c[k]); + v.cb[k] = l(a.cb[k], b.cb[k]); + v.t[k] = l(a.t[k], b.t[k]); + } + v.f = l(a.f, b.f); + v +} + +/// The clip planes in use. Plain data, valid zeroed. +#[derive(Clone, Copy, Default)] +#[repr(C)] +pub struct Clip { + /// User clip planes (eye space) and their enable bits. + pub user: [[f32; 4]; 6], + pub user_on: u32, +} + +impl Clip { + /// Signed distance of `v` to clip plane `i` (>= 0 inside). + pub fn dist(&self, v: &Wv, i: usize) -> f32 { + let h = &v.h; + match i { + 0 => h[3] + h[0], + 1 => h[3] - h[0], + 2 => h[3] + h[1], + 3 => h[3] - h[1], + 4 => h[3] + h[2], + 5 => h[3] - h[2], + _ => { + let p = &self.user[i - 6]; + p[0] * v.e[0] + p[1] * v.e[1] + p[2] * v.e[2] + p[3] * v.e[3] + } + } + } + + /// Planes in use: the view volume plus the enabled user planes. + pub fn mask(&self) -> u32 { + 0x3f | ((self.user_on & 0x3f) << 6) + } + + /// Bit i set when `v` is outside plane i. + pub fn outcode(&self, v: &Wv) -> u32 { + let mask = self.mask(); + (0..CLIP_PLANES).filter(|&i| mask & (1 << i) != 0 && self.dist(v, i) < 0.0) + .fold(0, |o, i| o | (1 << i)) + } + + /// Clip the polygon `v[..n]` against every plane its vertices are + /// outside of, in place (Sutherland-Hodgman). Returns the new vertex + /// count (0: nothing left). New vertices still need projecting. + pub fn clip_polygon(&self, v: &mut [Wv; MAX_POLY], n: usize) -> usize { + let any = v[..n].iter().fold(0, |o, w| o | w.oc); + if any == 0 { + return n; + } + let mut n = n; + let mut tmp = [Wv::default(); MAX_POLY]; + for plane in 0..CLIP_PLANES { + if any & (1 << plane) == 0 || n == 0 { + continue; + } + let mut m = 0; + for i in 0..n { + let (a, b) = (&v[i], &v[(i + 1) % n]); + let (da, db) = (self.dist(a, plane), self.dist(b, plane)); + if da >= 0.0 && m < MAX_POLY { + tmp[m] = *a; + m += 1; + } + if (da >= 0.0) != (db >= 0.0) && m < MAX_POLY { + tmp[m] = clip_cross(a, b, da, db); + m += 1; + } + } + v[..m].copy_from_slice(&tmp[..m]); + n = m; + } + n + } + + /// Clip the segment a-b; None when it is entirely outside. + pub fn clip_line(&self, mut a: Wv, mut b: Wv) -> Option<(Wv, Wv)> { + let any = a.oc | b.oc; + for plane in 0..CLIP_PLANES { + if any & (1 << plane) == 0 { + continue; + } + let (da, db) = (self.dist(&a, plane), self.dist(&b, plane)); + match (da >= 0.0, db >= 0.0) { + (true, true) => {} + (false, false) => return None, + (true, false) => b = clip_cross(&a, &b, da, db), + (false, true) => a = clip_cross(&a, &b, da, db), + } + } + Some((a, b)) + } +} + +/// The viewport (window relative): x0, x1, y0, y1 (inclusive), z scale and +/// centre. Plain data, valid zeroed. +#[derive(Clone, Copy, Default)] +#[repr(C)] +pub struct Viewport { + pub x0: f32, + pub x1: f32, + pub y0: f32, + pub y1: f32, + pub zscale: f32, + pub zcenter: f32, +} + +impl Viewport { + /// glViewport(x, y, w, h) with glDepthRange(n, f) mapped onto + /// 0..`zmax`. + pub fn set(&mut self, x: f32, y: f32, w: f32, h: f32, n: f32, f: f32, zmax: f32) { + (self.x0, self.x1, self.y0, self.y1) = (x, x + w - 1.0, y, y + h - 1.0); + self.zscale = (f - n) * 0.5 * zmax; + self.zcenter = (f + n) * 0.5 * zmax; + } + + /// Window position of `v` from its clip position (`ok` = 0 if w <= 0), + /// offset by `origin` and snapped to 1/`subpixel` of a pixel (the + /// raster engine's fixed-point precision; removes float noise such as + /// 319.99998 on edges). + pub fn project(&self, mut out: Wv, origin: [f32; 2], subpixel: f32) -> Wv { + let [cx, cy, cz, cw] = out.h; + out.ok = 0; + if cw <= 1e-6 { + return out; + } + let (nx, ny, nz) = (cx / cw, cy / cw, cz / cw); + let snap = |v: f32| (v * subpixel).round() / subpixel; + out.x = snap(origin[0] + self.x0 + (nx + 1.0) * 0.5 * (self.x1 - self.x0 + 1.0)); + out.y = snap(origin[1] + self.y0 + (ny + 1.0) * 0.5 * (self.y1 - self.y0 + 1.0)); + out.z = self.zcenter + nz * self.zscale; + out.ok = 1; + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn at(x: f32, y: f32) -> Wv { + let mut v = Wv { h: [x, y, 0.0, 1.0], ..Default::default() }; + v.oc = Clip::default().outcode(&v); + v + } + + #[test] + fn viewport_maps_ndc_corners_to_pixel_edges() { + let mut vp = Viewport::default(); + vp.set(0.0, 0.0, 400.0, 300.0, 0.0, 1.0, 1.0); + let a = vp.project(at(-1.0, -1.0), [0.0, 0.0], 256.0); + let b = vp.project(at(1.0, 1.0), [0.0, 0.0], 256.0); + assert_eq!((a.x, a.y, b.x, b.y), (0.0, 0.0, 400.0, 300.0)); + } + + #[test] + fn polygon_straddling_the_right_plane_is_cut_at_x_equals_w() { + let clip = Clip::default(); + let mut v = [Wv::default(); MAX_POLY]; + v[0] = at(0.0, 0.0); + v[1] = at(2.0, 0.0); + v[2] = at(0.0, 0.5); + let n = clip.clip_polygon(&mut v, 3); + assert_eq!(n, 4); + assert!(v[..n].iter().all(|w| w.h[0] <= 1.0 + 1e-6)); + } + + #[test] + fn line_outside_one_plane_is_dropped() { + assert!(Clip::default().clip_line(at(2.0, 0.0), at(3.0, 0.5)).is_none()); + } +} diff --git a/src/dev/mgras/dcb.rs b/src/dev/mgras/dcb.rs index 42b88470..b9f5ecce 100644 --- a/src/dev/mgras/dcb.rs +++ b/src/dev/mgras/dcb.rs @@ -8,7 +8,7 @@ //! the most significant bytes of a 32-bit bus access, so a one-byte register //! read with a word load comes back in bits 31:24. -use std::collections::HashMap; +use super::plain::RegMap; /// Device numbers on the bus. pub const DEV_CMAP_ALL: u32 = 3; @@ -16,6 +16,7 @@ pub const DEV_CMAP0: u32 = 4; pub const DEV_CMAP1: u32 = 5; pub const DEV_DAC: u32 = 6; pub const DEV_XMAP: u32 = 7; +pub const DEV_PP1_XMAP: u32 = DEV_XMAP; pub const DEV_VC3: u32 = 8; pub const DEV_BDVERS: u32 = 9; pub const DEV_I2C: u32 = 11; @@ -59,18 +60,15 @@ impl Txn { } /// A colormap chip: 8192 entries of 24-bit RGB. +#[repr(C)] pub struct Cmap { - pub pal: Vec, + pub pal: [u32; 8192], addr: u32, rev: u32, cmd: u32, } impl Cmap { - fn new(rev: u32) -> Self { - Cmap { pal: vec![0; 8192], addr: 0, rev, cmd: 0 } - } - fn write(&mut self, t: Txn, d: u32) { match (t.crs, t.width) { // Address: one byte at a time (low, then high), or 16 bits sent low @@ -103,10 +101,11 @@ impl Cmap { /// The RAMDAC: an address register selecting internal registers, and a /// 256-entry, 10-bit gamma table written as red, green, blue in turn. +#[repr(C)] pub struct Dac { addr: u32, - regs: HashMap, - pub gamma: Vec<[u16; 3]>, + regs: RegMap<256>, + pub gamma: [[u16; 3]; 256], gamma_comp: usize, mode: u32, } @@ -115,9 +114,12 @@ pub struct Dac { pub const DAC_PIXMASK: u32 = 4; impl Dac { - fn new() -> Self { - let gamma = (0..256).map(|i| { let v = (i << 2) as u16; [v, v, v] }).collect(); - Dac { addr: 0, regs: HashMap::new(), gamma, gamma_comp: 0, mode: 0 } + /// Power-on: an identity gamma ramp. + fn init(&mut self) { + for (i, g) in self.gamma.iter_mut().enumerate() { + let v = (i << 2) as u16; + *g = [v, v, v]; + } } fn write(&mut self, t: Txn, d: u32) { @@ -138,7 +140,7 @@ impl Dac { self.addr = self.addr.wrapping_add(1); } } - 2 => { self.regs.insert(self.addr, d & 0xFF); } + 2 => self.regs.insert(self.addr, d & 0xFF), 3 => self.mode = d, _ => {} } @@ -147,23 +149,40 @@ impl Dac { fn read(&self, t: Txn) -> u32 { match t.crs { 0 => self.addr, - 2 => self.regs.get(&self.addr).copied().unwrap_or(0), + 2 => self.regs.get(self.addr), 3 => self.mode, _ => 0, } } + /// Registers written so far, (address, value). + pub fn regs(&self) -> impl Iterator + '_ { + self.regs.iter() + } + pub fn pixmask(&self) -> u32 { - self.regs.get(&DAC_PIXMASK).copied().unwrap_or(0xFF) + self.regs.lookup(DAC_PIXMASK).unwrap_or(0xFF) } } /// The XMAP: the pixel processors' display side (display modes per window ID, /// buffer selects, scanout pointers), reached as an index register (`INDEX`) /// plus register files on the selects after it. +#[repr(C)] pub struct Xmap { index: u32, - regs: HashMap<(u32, u32), u32>, + /// The config register (CONFIG select, indices 0-3): one 32-bit + /// register whose bytes a one-byte transfer at index n reaches, byte 0 + /// being bits 31:24. Bit 19 makes the index auto-increment; bits 10:0 + /// hold the cursor colormap base / 4. + config: u32, + /// Register files, keyed by `key(select, index)`. + regs: RegMap<1024>, +} + +/// An XMAP register file key: the select in the top byte, the index below. +fn key(crs: u32, index: u32) -> u32 { + crs << 24 | (index & 0xFF_FFFF) } /// XMAP registers, by select (named as in OpenBSD's impact(4)). @@ -176,36 +195,40 @@ mod xmap { /// Display mode per window ID, at index `did * 4`. pub const MAIN_MODE: u32 = 4; pub const OVERLAY_MODE: u32 = 5; + /// Display interface buffer registers (scanout), index 0 the pointers. + pub const DIB: u32 = 6; /// The raster engine to pixel processor link. pub const RE_RAC: u32 = 7; } /// `CONFIG` index of the byte whose bit 3 makes the index auto-increment. -const XMAP_CONFIG_BYTE: u32 = 1; -const XMAP_AUTOINC: u32 = 0x08; +const XMAP_AUTOINC: u32 = 1 << 19; /// `CONFIG` index 4: the pixel processor revision. const XMAP_REV_INDEX: u32 = 4; /// `CONFIG` index read before each display-mode write. const XMAP_MODE_ROOM_INDEX: u32 = 8; impl Xmap { - fn new() -> Self { - Xmap { index: 0, regs: HashMap::new() } - } - fn autoinc(&mut self, t: Txn) { - let cfg = self.regs.get(&(xmap::CONFIG, XMAP_CONFIG_BYTE)).copied().unwrap_or(0); - if t.crs >= xmap::BUF_SELECT && cfg & XMAP_AUTOINC != 0 { + if t.crs >= xmap::BUF_SELECT && self.config & XMAP_AUTOINC != 0 { self.index = self.index.wrapping_add(t.width); } } fn write(&mut self, t: Txn, d: u32) { match t.crs { - xmap::PP1SELECT => { self.regs.insert((xmap::PP1SELECT, 0), d); } + xmap::PP1SELECT => self.regs.insert(key(xmap::PP1SELECT, 0), d), xmap::INDEX => self.index = d, + xmap::CONFIG if self.index < 4 => { + // Bytes index..index+width of the register, most significant + // first. + let n = t.width.min(4 - self.index); + let shift = 8 * (4 - self.index - n); + let mask = (if n == 4 { u32::MAX } else { (1 << (8 * n)) - 1 }) << shift; + self.config = (self.config & !mask) | ((d << shift) & mask); + } crs => { - self.regs.insert((crs, self.index), d); + self.regs.insert(key(crs, self.index), d); self.autoinc(t); } } @@ -217,11 +240,16 @@ impl Xmap { // The raster engine to pixel processor link reports its sync // signature here; 1 in each nibble lane means "in sync". xmap::RE_RAC => 0x0001_0101, + xmap::CONFIG if self.index < 4 => { + let n = t.width.min(4 - self.index); + let v = self.config >> (8 * (4 - self.index - n)); + if n == 4 { v } else { v & ((1 << (8 * n)) - 1) } + } xmap::CONFIG if self.index == XMAP_REV_INDEX => 1, // Polled nonzero before every display-mode write: room for a // mode update. xmap::CONFIG if self.index == XMAP_MODE_ROOM_INDEX => 0x10, - crs => self.regs.get(&(crs, self.index)).copied().unwrap_or(0), + crs => self.regs.get(key(crs, self.index)), }; if t.crs >= xmap::CONFIG { self.autoinc(t); } v @@ -230,18 +258,40 @@ impl Xmap { /// Display mode of overlay window ID `did` (`OVERLAY_MODE`, index `did * 4`); /// 0 is "overlay off". pub fn overlay_mode(&self, did: u32) -> u32 { - self.regs.get(&(xmap::OVERLAY_MODE, (did & 0x1F) << 2)).copied().unwrap_or(0) + self.regs.get(key(xmap::OVERLAY_MODE, (did & 0x1F) << 2)) } /// Colormap address of cursor colour 0: the config register (`CONFIG`, /// index 0) holds it divided by four. pub fn cursor_cmap_base(&self) -> usize { - (self.regs.get(&(xmap::CONFIG, 0)).copied().unwrap_or(0) as usize & 0x7FF) << 2 + (self.config as usize & 0x7FF) << 2 + } + + /// Register file entries written so far, (select << 24 | index, value). + pub fn regs(&self) -> impl Iterator + '_ { + self.regs.iter() + } + + /// Displayed buffer per window ID (`BUF_SELECT`, index 0): bit d set = + /// window ID d shows buffer B (the DIB pointer's second field) instead + /// of A. The kernel's retrace handler flips the bits for swapbuffers. + pub fn buf_select(&self) -> u32 { + self.regs.get(key(xmap::BUF_SELECT, 0)) + } + + /// Scanout page pointers (`DIB`, index 0): bits 9:0 the main buffer + /// (A), 19:10 the second buffer (B, double buffering), 31:20 the + /// overlay. Until written, the PROM's 1280x1024 value. + pub fn dib_pointers(&self) -> u32 { + match self.regs.get(key(xmap::DIB, 0)) { + 0 => 0x1C0C_8240, + v => v, + } } /// Display mode of window ID `did` (`MAIN_MODE`, index `did * 4`). pub fn main_mode(&self, did: u32) -> u32 { - self.regs.get(&(xmap::MAIN_MODE, did << 2)).copied().unwrap_or(0) + self.regs.get(key(xmap::MAIN_MODE, did << 2)) } } @@ -257,10 +307,11 @@ const CURSOR_CONTROL_REG: usize = 0x1D; const MAIN_WID_FRAME_REG: usize = 4; const OVERLAY_WID_FRAME_REG: usize = 5; +#[repr(C)] pub struct Vc3 { index: u32, pub regs: [u16; 32], - pub sram: Vec, + pub sram: [u16; 0x8000], line_counter: u16, } @@ -268,10 +319,6 @@ pub const VC3_SRAM_POINTER: usize = 7; const VC3_CURRENT_LINE: usize = 0xB; impl Vc3 { - fn new() -> Self { - Vc3 { index: 0, regs: [0; 32], sram: vec![0; 0x8000], line_counter: 0 } - } - /// The hardware cursor, when shown: the screen position of its top-left /// corner, its size (32 or 64), and the SRAM address of its glyph. The /// glyph is two bit planes, one after the other, a row at a time, most @@ -291,6 +338,22 @@ impl Vc3 { )) } + /// Visible display size from the video timing tables (register 0 points + /// at them; the layout is the VC2's, so Newport's decoder reads them). + /// None while no timing is loaded. + pub fn timing_size(&self) -> Option<(usize, usize)> { + let (w, h, _) = crate::disp::decode_vc2_timings(&self.regs, &self.sram); + (w > 0 && h > 0).then_some((w, h)) + } + + /// Scanlines in the main window-ID frame table: one entry per visible + /// line, then an 0xFFFF end-of-frame entry. + pub fn did_lines(&self) -> usize { + let frame = self.regs[MAIN_WID_FRAME_REG] as usize; + // No end marker (the table not loaded yet): 0. + (0..2048).find(|&y| self.sram[(frame + y) & 0x7FFF] == 0xFFFF).unwrap_or(0) + } + /// Window-ID runs of scanline `y` (0 is the top) in the main planes, as /// `(first x, did)`. Register 4 points at the frame table, one line-table /// address per scanline; a line table lists `(x << 5) | did` entries, @@ -364,7 +427,9 @@ impl Vc3 { } } -/// The whole bus: all devices of one board. +/// The whole bus: all devices of one board. Plain data, valid zeroed; call +/// `init` once. +#[repr(C)] pub struct Dcb { pub cmap: [Cmap; 2], pub dac: Dac, @@ -372,21 +437,32 @@ pub struct Dcb { pub vc3: Vc3, bdvers: [u32; 2], bc1: u32, - other: HashMap<(u32, u32), u32>, + pub dcbctrl: [u32; 16], + /// Devices not modelled, by (device, select). + other: [[u32; 8]; 32], } impl Dcb { /// `bdvers` are the two board-version bytes; `cmap_rev` the colormaps' /// revision registers. - pub fn new(bdvers: [u32; 2], cmap_rev: [u32; 2]) -> Self { - Dcb { - cmap: [Cmap::new(cmap_rev[0]), Cmap::new(cmap_rev[1])], - dac: Dac::new(), - xmap: Xmap::new(), - vc3: Vc3::new(), - bdvers, - bc1: 0, - other: HashMap::new(), + pub fn init(&mut self, bdvers: [u32; 2], cmap_rev: [u32; 2]) { + self.cmap[0].rev = cmap_rev[0]; + self.cmap[1].rev = cmap_rev[1]; + self.dac.init(); + self.bdvers = bdvers; + } + + pub fn write_dcbctrl(&mut self, dev: usize, val: u32) { + if dev < 16 { + self.dcbctrl[dev] = val; + } + } + + pub fn read_dcbctrl(&self, dev: usize) -> u32 { + if dev < 16 { + self.dcbctrl[dev] + } else { + 0 } } @@ -402,7 +478,7 @@ impl Dcb { if t.crs == 1 { self.bc1 = d; } } DEV_I2C => {} - dev => { self.other.insert((dev, t.crs), d); } + dev => self.other[dev as usize & 31][t.crs as usize] = d, } } @@ -416,7 +492,7 @@ impl Dcb { DEV_BDVERS => self.bdvers.get(t.crs as usize).copied().unwrap_or(0), // No flat panel: the I2C controller reads back nothing. DEV_I2C => 0, - dev => self.other.get(&(dev, t.crs)).copied().unwrap_or(0), + dev => self.other[dev as usize & 31][t.crs as usize], } } } diff --git a/src/dev/mgras/debug.rs b/src/dev/mgras/debug.rs new file mode 100644 index 00000000..14d1d06e --- /dev/null +++ b/src/dev/mgras/debug.rs @@ -0,0 +1,681 @@ +//! IMPACT introspection: monitor commands (`mgras ...`, `rss ...`) and the +//! annotated trace (`mgras trace [hq,rss,cpu,tex|all]`). +//! +//! Trace lines look like +//! ` 12.345678 HQ #000123 u fillmode = 0x100000` +//! ` 12.345690 HQ #000124 u block_xyendi = 0x1f003ff +exec` +//! ` 12.345702 RSS #004410 hq block_xyendi = 0x1f003ff (496, 1023) -> BLOCK (0, 0)-(496, 1023) fast fill ...` +//! ` 12.345705 CPU #000071 rd flag_set = 0x00010000 (x5123)` +//! with the time in seconds since the trace started. HQ lines are command +//! FIFO commands as the HQ3 dispatches them; RSS lines are raster register +//! writes and ops as the RSS applies them (`hq` from the command FIFO, `cpu` +//! from the direct register window); CPU lines are every other board access, +//! with repeats of one register and runs through a memory collapsed. Written +//! from the CPU, HQ3 and RSS threads, so tracing slows the pipeline. +//! +//! `IRIS_MGRAS_TRACE=` starts a full trace at power-on. + +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::sync::atomic::Ordering; +use std::time::Instant; + +use super::hq3::host; +use super::rss::{self, Rss, REG_NAMES}; +use super::{dcb, disp, tag, Mgras, MGRAS_SLOT_GFX_BASE}; + +pub const TRACE_HQ: u32 = 1; +pub const TRACE_RSS: u32 = 2; +/// CPU accesses outside the command FIFO and the direct raster window. +pub const TRACE_CPU: u32 = 4; +/// One line per texture load (cheap enough for a running game). +pub const TRACE_TEX: u32 = 8; +/// The categories `IRIS_MGRAS_TRACE` starts with. +pub const TRACE_ALL: u32 = TRACE_HQ | TRACE_RSS | TRACE_CPU; + +/// Pending run of similar CPU accesses, collapsed into one line. +struct CpuRun { + write: bool, + /// Region kind; bulk regions merge any offsets, registers merge only + /// identical (offset, value) repeats. + kind: &'static str, + bulk: bool, + first: u32, + last: u32, + val: u64, + count: u64, +} + +/// Trace sink state; lives in `Mgras::trace` behind a mutex. +pub struct MgrasTrace { + out: Option>, + path: String, + start: Option, + hq_seq: u64, + rss_seq: u64, + cpu_seq: u64, + run: Option, + last_flush: Option, +} + +impl MgrasTrace { + pub const fn new() -> Self { + Self { out: None, path: String::new(), start: None, hq_seq: 0, rss_seq: 0, cpu_seq: 0, run: None, last_flush: None } + } + + /// Emit the pending collapsed CPU run, if any. + fn close_run(&mut self) { + let Some(r) = self.run.take() else { return }; + let dir = if r.write { "wr" } else { "rd" }; + let seq = self.cpu_seq; + if r.count == 1 { + self.line(format_args!("CPU #{seq:06} {dir} {} = {:#010x}", region_detail(r.first), r.val)); + } else if r.bulk { + self.line(format_args!("CPU #{seq:06} {dir} {} x{} ({} .. {}) last={:#010x}", + r.kind, r.count, region_detail(r.first), region_detail(r.last), r.val)); + } else { + self.line(format_args!("CPU #{seq:06} {dir} {} = {:#010x} (x{})", region_detail(r.first), r.val, r.count)); + } + } + + fn line(&mut self, text: std::fmt::Arguments) { + let t = self.start.map(|s| s.elapsed().as_secs_f64()).unwrap_or(0.0); + if let Some(f) = self.out.as_mut() { + let _ = writeln!(f, "{t:12.6} {text}"); + } + } +} + +impl Mgras { + #[inline] + pub(super) fn tracing(&self, what: u32) -> bool { + self.trace_mask.load(Ordering::Relaxed) & what != 0 + } + + /// One CPU access outside the FIFOs (board offset, value read/written). + pub(super) fn trace_cpu(&self, write: bool, off: u32, val: u64) { + let (kind, bulk) = region_kind(off); + let mut t = self.trace.lock(); + if let Some(r) = t.run.as_mut() { + let same = r.write == write && r.kind == kind && if bulk { true } else { r.first == off && r.val == val }; + if same { + r.count += 1; + r.last = off; + r.val = val; + return; + } + } + t.close_run(); + t.cpu_seq += 1; + t.run = Some(CpuRun { write, kind, bulk, first: off, last: off, val, count: 1 }); + } + + /// One command the HQ3 dispatched (or another frontend event). + pub(super) fn trace_hq(&self, desc: &str) { + let mut t = self.trace.lock(); + t.close_run(); + t.hq_seq += 1; + let seq = t.hq_seq; + t.line(format_args!("HQ #{seq:06} {desc}")); + } + + /// One `rss_fifo` entry, after the RSS applied it (so an executed + /// primitive decodes with the state it ran with). + pub(super) fn trace_rss(&self, entry: u32, val: u64, rss: &Rss, changed: bool) { + let text = match entry { + tag::PIO_ADVANCE => "op PIO read advance".to_string(), + tag::DMA_LINE => format!("op DMA line {} ({} bytes)", val as u32, val >> 32), + e if e < 0x1000 => { + let src = if e & super::RSS_SRC_CPU != 0 { "cpu" } else { "hq " }; + let r = (e & 0x7FF) >> 1; + let v = val as u32; + let exec = e & 1 != 0; + let name = match REG_NAMES.get(r as usize).copied().unwrap_or("") { + "" => format!("rss[{r:#x}]"), + n => n.to_string(), + }; + let extra = match r { + 0x40 | 0x41 | 0x46 | 0x47 | 0x48 => format!(" ({}, {})", v >> 16 & 0xFFFF, v & 0xFFFF), + // The window origin and screen masks hold y (or min) in + // the high half. + 0x115 => format!(" (x {}, y {})", v & 0xFFFF, v >> 16), + 0x147..=0x14E => format!(" ({}..{})", v >> 16, v & 0xFFFF), + _ => String::new(), + }; + let run = if exec { format!(" -> {}", rss.describe_ir()) } else { String::new() }; + format!("{src} {name} = {v:#x}{extra}{run}") + } + e => format!("op {e:#x} = {val:#x}"), + }; + let mut t = self.trace.lock(); + t.close_run(); + t.rss_seq += 1; + let seq = t.rss_seq; + t.line(format_args!("RSS #{seq:06} {text}")); + } + + /// Flush the trace file (called every frame from the display thread; it + /// writes out at most twice a second unless forced). + pub(super) fn trace_flush(&self, force: bool) { + if self.trace_mask.load(Ordering::Relaxed) == 0 && !force { + return; + } + let mut t = self.trace.lock(); + let due = t.last_flush.map_or(true, |l| l.elapsed().as_millis() >= 500); + if force || due { + t.close_run(); + if let Some(f) = t.out.as_mut() { + let _ = f.flush(); + } + t.last_flush = Some(Instant::now()); + } + } + + /// Start the annotated trace into `path` for the categories in `mask`. + pub(super) fn start_trace(&self, path: &str, mask: u32) -> std::io::Result<()> { + let f = File::create(path)?; + let mut t = self.trace.lock(); + t.out = Some(BufWriter::with_capacity(1 << 20, f)); + t.path = path.to_string(); + t.start = Some(Instant::now()); + t.hq_seq = 0; + t.rss_seq = 0; + t.cpu_seq = 0; + t.run = None; + t.line(format_args!("IMPACT {:?} trace started (mask {mask:#x})", self.kind)); + drop(t); + self.trace_mask.store(mask, Ordering::SeqCst); + Ok(()) + } + + fn stop_trace(&self) -> Option { + self.trace_mask.store(0, Ordering::SeqCst); + let mut t = self.trace.lock(); + t.close_run(); + let mut f = t.out.take()?; + let _ = f.flush(); + Some(format!("{} (hq {} / rss {} / cpu {} entries)", t.path, t.hq_seq, t.rss_seq, t.cpu_seq)) + } + + // ── monitor commands ───────────────────────────────────────────────────── + + pub(super) fn cmd_mgras(&self, args: &[&str], w: &mut dyn Write) -> Result<(), String> { + let e = |r: std::io::Result<()>| r.map_err(|e| e.to_string()); + let num = |i: usize, d: u32| -> u32 { + args.get(i).and_then(|s| match s.strip_prefix("0x") { + Some(h) => u32::from_str_radix(h, 16).ok(), + None => s.parse().ok(), + }).unwrap_or(d) + }; + match args { + [] | ["status"] => return e(self.mgras_status(w)), + ["rec", "off"] => { + let what = self.submit.lock().as_ref().map(|r| { let r = r.lock(); format!("{} ({} records)", r.path, r.records) }); + self.set_recording(None).map_err(|e| format!("mgras rec: {e}"))?; + return e(writeln!(w, "recording stopped: {}", what.unwrap_or_else(|| "none running".into()))); + } + ["rec", "mark"] => { + let Some((h, n)) = self.record_mark() else { return Err("mgras rec: no recording running".into()) }; + return e(writeln!(w, "checkpoint {} at record {n}", blake3::Hash::from(h).to_hex())); + } + ["rec", path] => { + self.set_recording(Some(path)).map_err(|e| format!("mgras rec: {e}"))?; + return e(writeln!(w, "recording to {path}")); + } + ["trace", "flush"] => { + self.trace_flush(true); + return e(writeln!(w, "trace flushed")); + } + ["trace"] | ["trace", "off"] => { + return e(match self.stop_trace() { + Some(s) => writeln!(w, "trace closed: {s}"), + None => writeln!(w, "trace not active"), + }); + } + ["trace", path, rest @ ..] => { + let mask = trace_mask(rest.first().copied().unwrap_or("all")); + self.start_trace(path, mask).map_err(|e| format!("mgras trace: cannot create {path}: {e}"))?; + return e(writeln!(w, "tracing to {path} (mask {mask:#x})")); + } + ["dump", path] => { + let bytes = { + let _sub = self.submit.lock(); + self.wait_idle(); + self.state_bytes() + }; + std::fs::write(path, bytes).map_err(|e| format!("mgras dump: {e}"))?; + return e(writeln!(w, "dumped {path}")); + } + ["shot", path] => { + self.save_shot(path).map_err(|e| format!("mgras shot: {e}"))?; + return e(writeln!(w, "saved {path}")); + } + ["fbdump", rest @ ..] => { + let dir = std::path::PathBuf::from(rest.first().copied().unwrap_or("mgrasdump")); + return e(match self.dump_framebuffer(&dir) { + Ok(()) => writeln!(w, "framebuffer dumped to {}/ (screen rgb ci overlay .png, fb.bin overlay.bin)", dir.display()), + Err(err) => writeln!(w, "fbdump failed: {err}"), + }); + } + ["stats"] => return e(self.stats(w)), + _ => {} + } + let f = self.front(); + match args[0] { + "hq" => { + let flags = self.all_flags(&f); + e(writeln!(w, "HQ3: flags {flags:#010x} [{}]", flag_names(flags)))?; + e(writeln!(w, " flag enable {:#010x} interrupt enable {:#010x} GE readback {:#x} {:#x}", + f.hq.flag_enable, f.hq.interrupt_enable, f.hq.ge_readback[0], f.hq.ge_readback[1]))?; + let regs: Vec = sorted(f.hq.regs.iter()).iter().map(|(k, v)| format!("{}={v:#x}", region_detail(*k))).collect(); + e(writeln!(w, " other registers: {}", regs.join(" ")))?; + drop(f); + // SAFETY: read-only peek at the HQ3 thread's state (may tear). + let eng = unsafe { &*self.eng.get() }; + e(writeln!(w, " engine: {}", eng.summary()))?; + let dma: Vec = eng.dma_regs.iter().enumerate().filter(|(_, v)| **v != 0).map(|(i, v)| format!("{i:#x}={v:#x}")).collect(); + e(writeln!(w, " DMA registers: {}", dma.join(" ")))?; + let rif: Vec = eng.raster_if_regs.iter().enumerate().filter(|(_, v)| **v != 0).map(|(i, v)| format!("{i:#x}={v:#x}")).collect(); + e(writeln!(w, " raster interface: {} formatter {:#x}", rif.join(" "), eng.formatter))?; + e(writeln!(w, " fifo: hq {} (busy {}) rss {} (busy {}) idle={}", self.hq_fifo.len(), + self.hq_busy.load(Ordering::Relaxed), self.rss_fifo.len(), self.rss_busy.load(Ordering::Relaxed), self.idle())) + } + "gl" => { + drop(f); + // SAFETY: read-only peek at the HQ3 thread's state (may tear). + let eng = unsafe { &*self.eng.get() }; + e(writeln!(w, "{}", eng.gl_summary())) + } + "ge" => { + let ge = num(1, 0) as usize & 1; + let start = num(2, 0) as usize; + let n = num(3, 16) as usize; + e(writeln!(w, "GE11 #{ge} diagnostic address {:#x}; microcode lines (72 bits as byte2:word1:word0):", f.ge.diag_addr(ge)))?; + for i in start..start + n { + let Some(l) = f.ge.ucode_line(ge, i) else { break }; + e(writeln!(w, " {i:#07x}: {:02x}:{:08x}:{:08x}", l[2] & 0xFF, l[1], l[0]))?; + } + Ok(()) + } + "ucode" => { + let start = (num(1, 0) as usize).min(f.hq.ucode.len()); + let n = (num(2, 32) as usize).min(f.hq.ucode.len() - start); + e(hexdump(w, start, &f.hq.ucode[start..start + n])) + } + "vc3" => { + let v = &f.dcb.vc3; + if args.get(1) == Some(&"sram") { + let start = num(2, 0) as usize & 0x7FFF; + let n = (num(3, 64) as usize).min(0x8000 - start); + let words: Vec = v.sram[start..start + n].iter().map(|&x| x as u32).collect(); + return e(hexdump16(w, start, &words)); + } + e(writeln!(w, "VC3 registers:"))?; + for (i, chunk) in v.regs.chunks(8).enumerate() { + let line: Vec = chunk.iter().map(|r| format!("{r:04x}")).collect(); + e(writeln!(w, " {:#04x}: {}", i * 8, line.join(" ")))?; + } + e(writeln!(w, " cursor: {:?} (x, y, size, glyph)", v.cursor()))?; + e(writeln!(w, " display: timing tables {:?}, main DID frame table {} lines", v.timing_size(), v.did_lines()))?; + let mut runs = Vec::new(); + for y in [0usize, 512, 1023] { + v.main_did_runs(y, &mut runs); + e(writeln!(w, " line {y} main DID runs: {runs:?}"))?; + v.overlay_did_runs(y, &mut runs); + e(writeln!(w, " line {y} overlay DID runs: {runs:?}"))?; + } + Ok(()) + } + "xmap" => { + for did in 0..32u32 { + let (m, o) = (f.dcb.xmap.main_mode(did), f.dcb.xmap.overlay_mode(did)); + if m != 0 || o != 0 || did == 0 { + e(writeln!(w, " DID {did:2}: main {m:#010x} ({}) overlay {o:#010x}", decode_mode(m)))?; + } + } + e(writeln!(w, " cursor colormap base {:#x}", f.dcb.xmap.cursor_cmap_base()))?; + let regs: Vec = sorted(f.dcb.xmap.regs()).iter().map(|(k, v)| format!("sel{}[{:#x}]={v:#x}", k >> 24, k & 0xFF_FFFF)).collect(); + e(writeln!(w, " register files: {}", regs.join(" "))) + } + "cmap" => { + let start = num(1, 0) as usize & 0x1FFF; + let n = (num(2, 16) as usize).min(0x2000 - start); + let which = num(3, 0) as usize & 1; + for i in start..start + n { + let c = f.dcb.cmap[which].pal[i]; + e(writeln!(w, " cmap{which}[{i:#06x}] r={:3} g={:3} b={:3}", (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF))?; + } + Ok(()) + } + "dac" => { + let d = &f.dcb.dac; + let ident = d.gamma.iter().enumerate().all(|(i, g)| g.iter().all(|&v| v as usize == i << 2)); + e(writeln!(w, "DAC: pixmask {:#04x} gamma {}", d.pixmask(), if ident { "identity" } else { "custom" }))?; + let regs: Vec = sorted(d.regs()).iter().map(|(k, v)| format!("[{k:#x}]={v:#x}")).collect(); + e(writeln!(w, " registers: {}", regs.join(" ")))?; + e(writeln!(w, " gamma[0..8]: {:x?}", &d.gamma[..8])) + } + "pix" => { + // mgras pix x y (display coordinates: y = 0 is the top row) + let (x, y) = (num(1, 0) as usize, num(2, 0) as usize); + drop(f); + let fr = self.snapshot_frame(); + if x >= fr.width || y >= fr.height { + return e(writeln!(w, "out of range ({}x{} displayed)", fr.width, fr.height)); + } + let i = y * rss::WIDTH + x; + let (did, odid) = (fr.did_main[i] as usize & 31, fr.did_overlay[i] as usize & 31); + let px = fr.pixel(x, y); + e(writeln!(w, "({x}, {y}) [fb row {}]: main={:#010x} overlay={:#x} DID main {did} {:?} overlay {odid} {:?} -> screen r={} g={} b={}", + fr.height - 1 - y, fr.main[i], fr.overlay[i], fr.main_mode[did], fr.overlay_mode[odid], + px & 0xFF, (px >> 8) & 0xFF, (px >> 16) & 0xFF)) + } + _ => e(help(w)), + } + } + + pub(super) fn cmd_rss(&self, args: &[&str], w: &mut dyn Write) -> Result<(), String> { + let e = |r: std::io::Result<()>| r.map_err(|e| e.to_string()); + // SAFETY: read-only peek at the RSS thread's state (may tear). + let rss = unsafe { &*self.rss.get() }; + let num = |i: usize, d: u32| args.get(i).and_then(|s| s.parse::().ok()).unwrap_or(d); + match args.first().copied().unwrap_or("regs") { + "regs" => { + e(writeln!(w, "RSS: fifo {} pending, busy={}", self.rss_fifo.len(), self.rss_busy.load(Ordering::Relaxed)))?; + for r in 0..0x400u32 { + let v = rss.reg(r); + let name = REG_NAMES.get(r as usize).copied().unwrap_or(""); + if v != 0 { + e(writeln!(w, " {r:#05x} {name:<16} {v:#010x}"))?; + } + } + e(writeln!(w, " IR decodes as: {}", rss.describe_ir())) + } + "pix" => { + // rss pix x y [w] [h] framebuffer coordinates: y = 0 is the bottom row + let (x0, y0) = (num(1, 0) as usize, num(2, 0) as usize); + let (wd, ht) = (num(3, 8) as usize, num(4, 1) as usize); + let b = rss.target(); + e(writeln!(w, " drawing buffer: page {} {:?}", b.ptr, b.kind))?; + for y in (y0..(y0 + ht).min(rss::HEIGHT)).rev() { + let row: Vec = (x0..(x0 + wd).min(rss::WIDTH)) + .map(|x| format!("{:08x}", rss.mem.get(&b, x as u32, y as u32))).collect(); + e(writeln!(w, " y={y:4} x={x0:4}: {}", row.join(" ")))?; + } + Ok(()) + } + _ => e(writeln!(w, "rss [regs] | rss pix [w] [h] (the buffer being drawn; y = 0 is the bottom row)")), + } + } + + fn mgras_status(&self, w: &mut dyn Write) -> std::io::Result<()> { + // SAFETY: read-only peek at the RSS (may tear while drawing). + let rss = unsafe { &*self.rss.get() }; + let f = self.front(); + let flags = self.all_flags(&f); + writeln!(w, "IMPACT {:?} at {:#010x}", self.kind, MGRAS_SLOT_GFX_BASE)?; + 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))?; + match f.dcb.vc3.cursor() { + Some((x, y, size, _)) => writeln!(w, " cursor at ({x}, {y}) size {size}")?, + None => writeln!(w, " cursor hidden")?, + } + writeln!(w, " fill modes seen: {:x?}", rss.fillmodes_seen())?; + let mut hist: HashMap = HashMap::new(); + let (main, _) = super::frame::scanout_buffers(rss, &f.dcb); + for y in 0..1024 { + for x in 0..1280 { + *hist.entry(rss.mem.get(&main, x, y) as u32).or_default() += 1; + } + } + let mut top: Vec<_> = hist.into_iter().collect(); + top.sort_by(|a, b| b.1.cmp(&a.1)); + writeln!(w, " main buffer (page {}) values over 1280x1024 (value, pixels): {:x?}", main.ptr, &top[..top.len().min(8)])?; + let pal = &f.dcb.cmap[0].pal; + let blocks: Vec = (0..pal.len() / 256).filter(|k| pal[k * 256..(k + 1) * 256].iter().any(|c| *c != 0)).collect(); + writeln!(w, " colormap blocks in use: {blocks:?}")?; + drop(f); + if let Some(r) = self.submit.lock().as_ref() { + let r = r.lock(); + writeln!(w, " recording: {} ({} records)", r.path, r.records)?; + } + let t = self.trace.lock(); + let mask = self.trace_mask.load(Ordering::Relaxed); + if mask != 0 { + writeln!(w, " trace: {} (mask {mask:#x}) hq={} rss={} cpu={} entries", t.path, t.hq_seq, t.rss_seq, t.cpu_seq)?; + } + drop(t); + for u in self.unhandled.lock().iter() { + writeln!(w, " not modelled: {u}")?; + } + Ok(()) + } + + fn stats(&self, w: &mut dyn Write) -> std::io::Result<()> { + // SAFETY: read-only peeks at the engines' counters (may tear). + let h = unsafe { &*self.eng.get() }.stats; + let r = unsafe { &*self.rss.get() }.stats; + writeln!(w, "HQ3: {} words; raster writes {} (exec {}), done flags {}, swaps {}, context switches {}", + h.words, h.raster_writes, h.raster_execs, h.set_done, h.swaps, h.context_switches)?; + writeln!(w, " DMA: register writes {}, writes {}, reads {}, {} bytes; formatter {}, raster interface {}, pixel commands {}, other {}, display lists {} ({} segments)", + h.dma_reg_writes, h.dma_writes, h.dma_reads, h.dma_bytes, h.formatter_writes, h.raster_if_writes, h.pixel_cmds, h.other_cmds, h.dl_calls, h.dl_segments)?; + let cp: Vec = h.cp_tokens.iter().enumerate().filter(|(_, n)| **n != 0).map(|(t, n)| format!("{t:#x}:{n}")).collect(); + writeln!(w, " command-processor tokens (token:count): {}", if cp.is_empty() { "none".into() } else { cp.join(" ") })?; + let prims: Vec = r.prims.iter().enumerate().filter(|(_, n)| **n != 0).map(|(o, n)| format!("{o:#x}:{n}")).collect(); + writeln!(w, "RSS: primitives by IR opcode {}", prims.join(" "))?; + let blocks: Vec = r.blocks.iter().enumerate().filter(|(_, n)| **n != 0).map(|(k, n)| format!("{k}:{n}")).collect(); + writeln!(w, " blocks by type {} fast fills {}", blocks.join(" "), r.fast_fills)?; + writeln!(w, " stipple chunks {} PIO dw written {} read {} DMA lines in {}", + r.stipple_chunks, r.pio_write_dw, r.pio_read_dw, r.dma_lines_in) + } + + /// `mgras fbdump`: the raw page memory (pixmem.bin, big-endian u64s), + /// the displayed main and overlay buffers as big-endian u32s (fb.bin, + /// overlay.bin: stride 2048, display-height rows, row 0 at the bottom), + /// and PNG views of them at the displayed size, top-down. + fn dump_framebuffer(&self, dir: &std::path::Path) -> std::io::Result<()> { + use rss::WIDTH; + std::fs::create_dir_all(dir)?; + // SAFETY: read-only views; tearing tolerated. + let rss = unsafe { &*self.rss.get() }; + let f = self.front(); + let (dw, dh) = super::frame::display_size(&f.dcb); + let (main, overlay) = super::frame::scanout_buffers(rss, &f.dcb); + drop(f); + std::fs::write(dir.join("pixmem.bin"), rss.mem.words.iter().flat_map(|w| w.to_be_bytes()).collect::>())?; + let view = |b: Option| { + let mut out = vec![0u32; WIDTH * dh]; + if let Some(b) = b { + for y in 0..dh { + rss.mem.read_row(&b, y as u32, &mut out[y * WIDTH..(y + 1) * WIDTH]); + } + } + out + }; + let (fb, ov_plane) = (view(Some(main)), view(overlay)); + let be = |words: &[u32]| words.iter().flat_map(|w| w.to_be_bytes()).collect::>(); + std::fs::write(dir.join("fb.bin"), be(&fb))?; + std::fs::write(dir.join("overlay.bin"), be(&ov_plane))?; + let (mut rgb, mut ci, mut ov) = (Vec::new(), Vec::new(), Vec::new()); + for y in (0..dh).rev() { + for &p in &fb[y * WIDTH..y * WIDTH + dw] { + rgb.extend_from_slice(&[p as u8, (p >> 8) as u8, (p >> 16) as u8]); + ci.push(p as u8); + } + ov.extend(ov_plane[y * WIDTH..y * WIDTH + dw].iter().map(|&p| p as u8)); + } + write_png(&dir.join("rgb.png"), dw, dh, &rgb, png::ColorType::Rgb)?; + write_png(&dir.join("ci.png"), dw, dh, &ci, png::ColorType::Grayscale)?; + write_png(&dir.join("overlay.png"), dw, dh, &ov, png::ColorType::Grayscale)?; + self.save_shot(dir.join("screen.png").to_str().unwrap_or("screen.png")).map_err(std::io::Error::other) + } +} + +fn trace_mask(spec: &str) -> u32 { + spec.split(',').fold(0, |m, c| m | match c { + "hq" => TRACE_HQ, + "rss" => TRACE_RSS, + "cpu" => TRACE_CPU, + "tex" => TRACE_TEX, + _ => TRACE_ALL, + }) +} + +fn help(w: &mut dyn Write) -> std::io::Result<()> { + for l in [ + "mgras [status] board summary", + "mgras hq HQ3 flags, enables, engine, DMA/RE-interface registers, FIFOs", + "mgras ge [n] [start] [count] GE11 microcode lines", + "mgras ucode [start] [count] HQ3 microcode RAM", + "mgras vc3 | vc3 sram [n] VC3 registers, cursor, DID runs / SRAM words", + "mgras xmap XMAP display modes per DID and register files", + "mgras cmap [start] [n] [0|1] colormap entries", + "mgras dac DAC registers and gamma", + "mgras pix one pixel through the compositor (y = 0 is the top)", + "mgras stats command, primitive and DMA counters", + "mgras fbdump [dir] planes and the screen as PNG + raw files", + "mgras shot | dump ", + "mgras trace [hq,rss,cpu,tex|all] | trace flush | trace off", + "mgras rec | rec mark | rec off replayable recording (see record.rs)", + "rss [regs] | rss pix [w] [h]", + ] { + writeln!(w, "{l}")?; + } + Ok(()) +} + +fn sorted(it: impl Iterator) -> Vec<(u32, u32)> { + let mut v: Vec<_> = it.collect(); + v.sort(); + v +} + +/// Names of the flag bits this model knows. +fn flag_names(flags: u32) -> String { + const NAMES: [(u32, &str); 6] = [ + (host::FLAG_CONTEXT_LOADED, "ctx_loaded"), (host::FLAG_CP0, "cp0_swap"), (host::FLAG_DONE, "done"), + (host::FLAG_GE_DATA, "ge_data"), (host::FLAG_GE_DIAG, "ge_diag"), (host::FLAG_CONTEXT_SAVED, "ctx_saved"), + ]; + let mut out: Vec = NAMES.iter().filter(|(b, _)| flags & b != 0).map(|(_, n)| n.to_string()).collect(); + let rest = flags & !NAMES.iter().fold(0, |m, (b, _)| m | b); + if rest != 0 { + out.push(format!("{rest:#x}")); + } + out.join(" ") +} + +/// An XMAP display mode as this model reads it (pixel format in bits 4:0, +/// colormap block in 9:5). +fn decode_mode(m: u32) -> String { + let fmt = m & 0x1F; + if fmt >= 4 { format!("RGB fmt {fmt}") } else { format!("CI fmt {fmt} cmap block {}", (m >> 5) & 0x1F) } +} + +fn hexdump(w: &mut dyn Write, start: usize, words: &[u32]) -> std::io::Result<()> { + for (i, chunk) in words.chunks(8).enumerate() { + let line: Vec = chunk.iter().map(|v| format!("{v:08x}")).collect(); + writeln!(w, " {:#06x}: {}", start + i * 8, line.join(" "))?; + } + Ok(()) +} + +fn hexdump16(w: &mut dyn Write, start: usize, words: &[u32]) -> std::io::Result<()> { + for (i, chunk) in words.chunks(8).enumerate() { + let line: Vec = chunk.iter().map(|v| format!("{v:04x}")).collect(); + writeln!(w, " {:#06x}: {}", start + i * 8, line.join(" "))?; + } + Ok(()) +} + +fn write_png(path: &std::path::Path, width: usize, height: usize, data: &[u8], color: png::ColorType) -> std::io::Result<()> { + let file = File::create(path)?; + let mut enc = png::Encoder::new(BufWriter::new(file), width as u32, height as u32); + enc.set_color(color); + enc.set_depth(png::BitDepth::Eight); + let mut w = enc.write_header().map_err(std::io::Error::other)?; + w.write_image_data(data).map_err(std::io::Error::other) +} + +/// Display control bus device names, and each device's register selects. +fn dcb_names(dev: u32) -> (&'static str, [&'static str; 8]) { + match dev { + dcb::DEV_CMAP_ALL | dcb::DEV_CMAP0 | dcb::DEV_CMAP1 => ( + ["cmap_all", "cmap0", "cmap1"][(dev - dcb::DEV_CMAP_ALL) as usize], + ["addr", "addrhi", "pal", "cmd", "status", "sel5", "rev", "reserved"], + ), + dcb::DEV_DAC => ("dac", ["addr", "pal", "reg", "mode", "sel4", "sel5", "sel6", "sel7"]), + dcb::DEV_XMAP => ("xmap", ["pp1select", "index", "config", "buf_select", "main_mode", "overlay_mode", "dib", "re_rac"]), + dcb::DEV_VC3 => ("vc3", ["index", "data", "sel2", "ram", "sel4", "sel5", "sel6", "sel7"]), + dcb::DEV_BDVERS => ("bdvers", ["bdvers0", "bdvers1", "sel2", "sel3", "sel4", "sel5", "sel6", "sel7"]), + dcb::DEV_I2C => ("i2c", ["sel0", "sel1", "sel2", "sel3", "sel4", "sel5", "sel6", "sel7"]), + _ => ("dcb?", ["sel0", "sel1", "sel2", "sel3", "sel4", "sel5", "sel6", "sel7"]), + } +} + +/// Region of a board offset for CPU tracing: (kind, bulk). Bulk regions are +/// memories (colormap and gamma tables, VC3 SRAM, microcode) whose runs of +/// accesses collapse into one line; any other repeat collapses only while +/// it hits the same register with the same value. +fn region_kind(off: u32) -> (&'static str, bool) { + match off { + host::UCODE..host::UCODE_END => ("hq.ucode", true), + 0x60000..0x68000 => { + let t = dcb::Txn::decode(off); + match (t.dev, t.crs) { + (dcb::DEV_CMAP_ALL..=dcb::DEV_CMAP1, 2) => ("cmap.pal", true), + (dcb::DEV_DAC, 1) => ("dac.pal", true), + (dcb::DEV_VC3, 3) => ("vc3.ram", true), + (dcb::DEV_XMAP, 6) => ("xmap.dib", true), + _ => ("dcb", false), + } + } + 0x50200..0x50500 => ("hq.context", true), + _ => ("reg", false), + } +} + +/// Human-readable name for a board offset (register names where known). +fn region_detail(off: u32) -> String { + const HOST: [(u32, &str); 40] = [ + (0x00000, "gio_id"), (0x46000, "hqpc"), (0x47000, "cp_data"), + (0x50000, "hq_config"), (0x50004, "flag_ctx"), (0x50008, "flag_set_priv"), (0x5000C, "flag_clr_priv"), + (0x50010, "flag_enab_set"), (0x50014, "flag_enab_clr"), (0x50018, "intr_enab_set"), (0x5001C, "intr_enab_clr"), + (0x50020, "cfifo_hw"), (0x50024, "cfifo_lw"), (0x50028, "cfifo_delay"), (0x5002C, "dfifo_hw"), + (0x50030, "dfifo_lw"), (0x50034, "dfifo_delay"), (0x50040, "ge0_diag_d"), (0x50044, "ge0_diag_a"), + (0x50048, "ge1_diag_d"), (0x5004C, "ge1_diag_a"), (0x50050, "context_switch"), + (0x50108, "bfifo_hw"), (0x5010C, "bfifo_lw"), (0x50110, "bfifo_delay"), (0x50114, "gio_config"), + (0x5022C, "ge_diag_read"), (0x50230, "ge_diag_read_pad"), (0x50880, "tlb_curr_addr"), (0x50900, "tlb_valids"), + (0x70000, "status"), (0x70004, "fifo_status"), (0x70008, "flag_set"), (0x7000C, "flag_clr"), + (0x70010, "ge_readback_hi"), (0x70014, "ge_readback_lo"), (0x70100, "gio_status"), (0x70104, "dma_busy"), + (host::PIO_READ_HI, "pio_read_hi"), (host::PIO_READ_LO, "pio_read_lo"), + ]; + if let Some((_, n)) = HOST.iter().find(|(a, _)| *a == off) { + return n.to_string(); + } + match off { + host::UCODE..host::UCODE_END => format!("hq.ucode[{:#06x}]", (off - host::UCODE) >> 2), + 0x4C000..0x50000 => format!("rss_diag[{:#x}]", (off - 0x4C000) >> 2), + 0x50200..0x50300 => format!("reif_ctx[{:#x}]", (off - 0x50200) >> 2), + 0x50300..0x50500 => format!("hag_ctx[{:#x}]", (off - 0x50300) >> 2), + 0x50800..0x50880 => format!("tlb[{}]", (off - 0x50800) >> 2), + 0x60000..0x68000 => { + let t = dcb::Txn::decode(off); + let (dev, sel) = dcb_names(t.dev); + format!("{dev}.{} (w{})", sel[t.crs as usize], t.width) + } + 0x68000..0x68040 => { + let dev = (off - 0x68000) >> 2; + format!("dcbctrl[{dev}] ({})", dcb_names(dev).0) + } + host::RASTER..host::RASTER_END => { + let r = (off & 0xFFC) >> 2; + match REG_NAMES.get(r as usize).copied().unwrap_or("") { + "" => format!("rss[{r:#x}]"), + n => format!("rss.{n}"), + } + } + _ => format!("[{off:#07x}]"), + } +} diff --git a/src/dev/mgras/disp.rs b/src/dev/mgras/disp.rs new file mode 100644 index 00000000..9c874d96 --- /dev/null +++ b/src/dev/mgras/disp.rs @@ -0,0 +1,227 @@ +//! Display side: the refresh thread (vertical retrace, then a frame +//! snapshot and composition, see `frame`), host GL frame compositing, and +//! PNG output. + +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use super::dcb::Dcb; +use super::rss::{self, Rss}; +use super::frame::{self, Frame}; +use super::{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 +/// over a grid of samples inside it that has an RGB display mode. None +/// if no sampled pixel is in an RGB window. +pub fn window_did(rss: &Rss, dcb: &Dcb, x: i32, y: i32, w: usize, h: usize) -> Option { + let mut counts = [0u32; 32]; + let mut runs = Vec::new(); + let (screen_w, screen_h) = frame::display_size(dcb); + for sy in 0..16 { + let py = y + (h as i32 * (2 * sy + 1)) / 32; + if !(0..screen_h as i32).contains(&py) { + continue; + } + dcb.vc3.main_did_runs(py as usize, &mut runs); + for sx in 0..16 { + let px = x + (w as i32 * (2 * sx + 1)) / 32; + if !(0..screen_w as i32).contains(&px) { + continue; + } + let did = runs.iter().rev().find(|r| r.0 as i32 <= px).map_or(0, |r| r.1); + if dcb.xmap.main_mode(did as u32) & 0x1F >= 4 { + counts[did as usize & 31] += 1; + } + } + } + let (did, n) = counts.iter().enumerate().max_by_key(|&(_, n)| *n)?; + (*n > 0).then_some(did as u8) +} + +/// Paint a host GL frame (`bgra`: `h` rows, top first, `stride` bytes +/// each) into the framebuffer at screen position (`x`, `y`), wherever the +/// pixel belongs to the frame's window ID, so windows over it stay over it. +/// The pixels become part of the framebuffer, as GL's would on the board, +/// for anything that reads them back. False when no window ID fits. +pub fn composite(rss: &mut Rss, dcb: &Dcb, x: i32, y: i32, bgra: &[u8], stride: usize, w: usize, h: usize) -> bool { + let Some(target) = window_did(rss, dcb, x, y, w, h) else { return false }; + let mut runs = Vec::new(); + let (screen_w, screen_h) = frame::display_size(dcb); + let (main, _) = frame::scanout_buffers(rss, dcb); + for row in 0..h { + let sy = y + row as i32; + if !(0..screen_h as i32).contains(&sy) { + continue; + } + dcb.vc3.main_did_runs(sy as usize, &mut runs); + if runs.is_empty() || runs[0].0 != 0 { + runs.insert(0, (0, 0)); + } + let fb_y = (screen_h - 1 - sy as usize) as u32; + for (k, &(x0, did)) in runs.iter().enumerate() { + if did != target { + continue; + } + let x1 = runs.get(k + 1).map_or(screen_w as i32, |r| r.0 as i32); + let lo = (x0 as i32).max(x).max(0); + let hi = x1.min(x + w as i32).min(screen_w as i32); + for sx in lo..hi { + let i = row * stride + (sx - x) as usize * 4; + let Some(p) = bgra.get(i..i + 4) else { break }; + let v = p[2] as u32 | (p[1] as u32) << 8 | (p[0] as u32) << 16; + rss.mem.put(&main, sx as u32, fb_y, v as u64); + } + } + } + true +} + +/// Write a scanned-out frame (`0xFF_BB_GG_RR`, stride `OUT_STRIDE`) as a PNG. +pub fn save_png(path: &str, frame: &[u32], width: usize, height: usize) -> Result<(), String> { + let file = std::fs::File::create(path).map_err(|e| e.to_string())?; + let mut enc = png::Encoder::new(std::io::BufWriter::new(file), width as u32, height as u32); + enc.set_color(png::ColorType::Rgb); + enc.set_depth(png::BitDepth::Eight); + let mut out = enc.write_header().map_err(|e| e.to_string())?; + let mut rows = Vec::with_capacity(width * height * 3); + for y in 0..height { + for px in &frame[y * frame::OUT_STRIDE..y * frame::OUT_STRIDE + width] { + rows.extend_from_slice(&[*px as u8, (px >> 8) as u8, (px >> 16) as u8]); + } + } + out.write_image_data(&rows).map_err(|e| e.to_string()) +} + +impl Mgras { + pub(super) fn refresh_loop(self: &Arc) { + let frame = std::time::Duration::from_micros(16_667); + { + let mut screen = self.screen.lock(); + (screen.width, screen.height) = frame::DEFAULT_SIZE; + screen.fb_rgb.fill(0xFF00_0000); + screen.prebuilt = true; + } + let mut overlay = crate::debug_overlay::DebugOverlay::new(); + let mut status_bar = crate::disp::StatusBar::new(); + let mut sbtex = crate::disp::StatusBarTexture::new(); + // Size the renderer was last given; the guest can change modes. + let mut shown = (0usize, 0usize); + let mut idle_frames = 0u32; + let mut shots = 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; + + while self.running.load(Ordering::Relaxed) { + let start = std::time::Instant::now(); + let stats = crate::disp::BarStats { + now: start, + hb: self.heartbeat.fetch_and(PERSISTENT, Ordering::Relaxed), + cycles: self.cycles.lock().get(), + fasttick: self.fasttick.load(Ordering::Relaxed), + decoded_delta: 0, + l1i_hits: 0, + l1i_fetches: 0, + uncached: 0, + count_hz: 0, + gfifo_pending: 0, + }; + if last_rec_flush.elapsed() >= std::time::Duration::from_millis(500) { + if let Some(rec) = self.mem_rec.lock().clone() { + rec.lock().flush(); + } + last_rec_flush = std::time::Instant::now(); + } + self.trace_flush(false); + // 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 { + self.set_line(Line::Retrace, true); + std::thread::sleep(std::time::Duration::from_micros(500)); + self.set_line(Line::Retrace, false); + } + let dirty = self.dirty.swap(false, Ordering::AcqRel); + let shot = self.screenshot_pending.swap(false, Ordering::Relaxed); + idle_frames += 1; + if dirty || shot || idle_frames >= 6 { + idle_frames = 0; + 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. + // SAFETY: a read-only view of the framebuffer; tearing + // tolerated. + let rss = unsafe { &*self.rss.get() }; + snap.snapshot(rss, &self.front_view().dcb); + snap.compose(&mut screen.fb_rgb); + // 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. + let (w, h) = (snap.width, snap.height); + for y in 0..h { + let row = y * 2048; + screen.rgba[row..row + w].copy_from_slice(&screen.fb_rgb[row..row + w]); + } + screen.width = w; + screen.height = h; + screen.status_bar_only = false; + } else { + screen.status_bar_only = true; + } + if let Some(r) = self.renderer.lock().as_mut() { + if shown != (screen.width, screen.height) { + shown = (screen.width, screen.height); + r.resize(shown.0, shown.1); + } + r.present(&mut screen, &mut overlay, &mut status_bar, &mut sbtex, &stats, shot, None, None); + } + // The screenshot hotkey (Right Ctrl + Print Screen), as REX3 + // and GR2 do: the frame just composed, written off-thread. + if shot { + let (pixels, w, h) = (screen.fb_rgb.clone(), screen.width, screen.height); + let path = format!("screenshot_{shots:04}.png"); + shots += 1; + std::thread::spawn(move || match save_png(&path, &pixels, w, h) { + Ok(()) => println!("iris: screenshot saved to {path}"), + Err(e) => println!("iris: screenshot failed: {e}"), + }); + } + } + if let Some(rest) = frame.checked_sub(start.elapsed()) { + std::thread::sleep(rest); + } + } + // The renderer's GL state belongs to this thread (its context is + // current here), so it is torn down here and nowhere else. + if let Some(r) = self.renderer.lock().as_mut() { + r.stop(); + } + } + + /// Start the engines and the display refresh thread. + pub fn start_display(self: &Arc) { + self.start_engines(); + if self.running.swap(true, Ordering::AcqRel) { + return; + } + let me = Arc::clone(self); + *self.refresh.lock() = Some( + std::thread::Builder::new() + .name("MGRAS-Refresh".into()) + .spawn(move || me.refresh_loop()) + .expect("spawn MGRAS refresh thread"), + ); + } + + pub fn stop_display(&self) { + self.running.store(false, Ordering::Release); + if let Some(h) = self.refresh.lock().take() { + let _ = h.join(); + } + self.stop_engines(); + } +} diff --git a/src/dev/mgras/frame.rs b/src/dev/mgras/frame.rs new file mode 100644 index 00000000..e06895fd --- /dev/null +++ b/src/dev/mgras/frame.rs @@ -0,0 +1,286 @@ +//! Display composition in two steps, the way Newport's compositor works: +//! +//! 1. `Frame::snapshot`, taken by the display thread just after the vertical +//! blank (so the guest's retrace-time updates are in): copies the planes +//! and decodes every piece of board state into flat buffers and small +//! tables, in display order. All board-specific decisions are made here: +//! the VC3 window-ID runs become a window ID per pixel (main and overlay), +//! the XMAP mode words become a descriptor per window ID, the DAC gamma +//! becomes an 8-bit table, the cursor glyph a 2-bit-per-pixel image. +//! 2. `Frame::compose`: one pass over the pixels, cursor first, then the +//! overlay, then the main planes through their window ID's descriptor, +//! then gamma. Each pixel depends only on the buffers at its own position +//! and on the small tables, so the same pass can run as a fragment shader +//! with the buffers uploaded as textures. +//! +//! The displayed size comes from the VC3 (see `display_size`): IRIX runs +//! the same board from 1024x768 to 1600x1200, with the screen's top row at +//! framebuffer row height - 1 (the window origin X uses). W x H is the +//! whole framebuffer, of which the screen is the bottom-left corner. +//! +//! Output: stride `OUT_STRIDE`, `0xFFBBGGRR` (red in the low byte), what the +//! renderers take as a prebuilt frame. + +use super::dcb::Dcb; +use super::pixmem::{Buffer, Kind}; +use super::rss::{self, Rss}; + +pub const W: usize = rss::WIDTH; +pub const H: usize = rss::HEIGHT; +pub const OUT_STRIDE: usize = crate::disp::FB_STRIDE; +/// What the board displays before IRIX loads its VC3 tables. +pub const DEFAULT_SIZE: (usize, usize) = (1280, 1024); +/// Largest cursor glyph (64x64). +pub const CURSOR_MAX: usize = 64; + +/// How a window ID's main planes display. `rgb`: the pixel is `0x00BBGGRR`; +/// otherwise its low 12 bits index the colormap from `cmap_base`. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct MainMode { + pub rgb: bool, + pub cmap_base: u16, +} + +/// How a window ID's overlay planes display: off, or a non-zero 8-bit value +/// indexing the colormap from `cmap_base` (0 shows the main planes). +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct OverlayMode { + pub on: bool, + pub cmap_base: u16, +} + +/// One displayed frame's worth of decoded board state. Plain data, valid +/// zeroed; build with `plain::boxed_zeroed` and fill with `snapshot`. +#[repr(C)] +pub struct Frame { + /// Displayed size (at most W x H). + pub width: usize, + pub height: usize, + /// Main planes, display order (row 0 at the top). + pub main: [u32; W * H], + /// Overlay planes (8 bits used), display order. + pub overlay: [u8; W * H], + /// Window ID of each pixel, main and overlay planes, display order. + pub did_main: [u8; W * H], + pub did_overlay: [u8; W * H], + pub main_mode: [MainMode; 32], + pub overlay_mode: [OverlayMode; 32], + /// Colormap 0, `0x00RRGGBB`. + pub cmap: [u32; 8192], + /// DAC gamma reduced to 8 bits, per input level: [red, green, blue]. + pub gamma: [[u8; 3]; 256], + /// DAC pixel mask is zero: the screen is blanked. + pub blank: bool, + pub cursor_on: bool, + /// Top-left corner on screen, and the glyph's size (32 or 64). + pub cursor_x: i32, + pub cursor_y: i32, + pub cursor_size: usize, + /// Glyph, 2 bits a pixel (0 transparent), CURSOR_MAX x CURSOR_MAX. + pub cursor: [u8; CURSOR_MAX * CURSOR_MAX], + /// Colours of cursor values 1..3 (index 0 unused), `0x00RRGGBB`. + pub cursor_rgb: [u32; 4], +} + +impl Frame { + /// Take the frame: copy the planes and decode the display state. + pub fn snapshot(&mut self, rss: &Rss, dcb: &Dcb) { + (self.width, self.height) = display_size(dcb); + let (w, h) = (self.width, self.height); + let (main, overlay) = scanout_buffers(rss, dcb); + // Double buffering: window IDs with their BUF_SELECT bit show + // buffer B. The window-ID runs are decoded below; this pass needs + // them first, so decode them before the planes. + let select = dcb.xmap.buf_select(); + let back = Buffer::new(dcb.xmap.dib_pointers() >> 10, Kind::Wide, rss.reg(rss::reg::DRBSIZE)); + let mut runs = Vec::with_capacity(64); + for y in 0..h { + dcb.vc3.main_did_runs(y, &mut runs); + fill_did_row(&mut self.did_main[y * W..(y + 1) * W], &mut runs); + } + let mut row = [0u32; W]; + let mut row_b = [0u32; W]; + for y in 0..h { + let dst = y * W; + let fb_y = (h - 1 - y) as u32; + rss.mem.read_row(&main, fb_y, &mut self.main[dst..dst + w]); + if select != 0 { + rss.mem.read_row(&back, fb_y, &mut row_b[..w]); + for x in 0..w { + if select >> (self.did_main[dst + x] & 31) & 1 != 0 { + self.main[dst + x] = row_b[x]; + } + } + } + match overlay { + Some(o) => { + rss.mem.read_row(&o, fb_y, &mut row[..w]); + for (d, s) in self.overlay[dst..dst + w].iter_mut().zip(&row[..w]) { + *d = *s as u8; + } + } + None => self.overlay[dst..dst + w].fill(0), + } + } + for y in 0..h { + dcb.vc3.overlay_did_runs(y, &mut runs); + fill_did_row(&mut self.did_overlay[y * W..(y + 1) * W], &mut runs); + } + for did in 0..32u32 { + let m = dcb.xmap.main_mode(did); + self.main_mode[did as usize] = MainMode { rgb: m & 0x1F >= 4, cmap_base: (((m >> 5) & 0x1F) * 256) as u16 }; + let o = dcb.xmap.overlay_mode(did); + self.overlay_mode[did as usize] = OverlayMode { on: o != 0, cmap_base: (((o >> 3) & 0x1F) * 256) as u16 }; + } + self.cmap.copy_from_slice(&dcb.cmap[0].pal); + for (g, d) in self.gamma.iter_mut().zip(dcb.dac.gamma.iter()) { + *g = [(d[0] >> 2) as u8, (d[1] >> 2) as u8, (d[2] >> 2) as u8]; + } + self.blank = dcb.dac.pixmask() == 0; + self.cursor_on = false; + if let Some((cx, cy, size, glyph)) = dcb.vc3.cursor() { + self.cursor_on = true; + (self.cursor_x, self.cursor_y, self.cursor_size) = (cx, cy, size); + let sram = &dcb.vc3.sram; + let words_per_row = size / 16; + let plane_words = size * words_per_row; + let bit = |plane: usize, row: usize, col: usize| -> u8 { + let w = sram[(glyph + plane * plane_words + row * words_per_row + col / 16) & 0x7FFF]; + (w >> (15 - col % 16)) as u8 & 1 + }; + for row in 0..size { + for col in 0..size { + self.cursor[row * CURSOR_MAX + col] = bit(0, row, col) | bit(1, row, col) << 1; + } + } + // Cursor colours sit in the colormap at the XMAP's cursor base; + // past its end they read as white. + let base = dcb.xmap.cursor_cmap_base(); + for c in 1..4 { + self.cursor_rgb[c] = self.cmap.get(base + c).copied().unwrap_or(0xFF_FFFF); + } + } + } + + /// One pixel, `0xFFBBGGRR`. `x`, `y` in display coordinates. + #[inline] + pub fn pixel(&self, x: usize, y: usize) -> u32 { + if self.blank { + return 0xFF00_0000; + } + let i = y * W + x; + let rgb = self.cursor_value(x, y).map(|c| self.cursor_rgb[c as usize]) + .or_else(|| { + let o = self.overlay_mode[self.did_overlay[i] as usize & 31]; + let v = self.overlay[i]; + (o.on && v != 0).then(|| self.cmap[(o.cmap_base as usize + v as usize) & 0x1FFF]) + }); + match rgb { + Some(c) => self.gamma_rgb(c >> 16, c >> 8, c), + None => { + let m = self.main_mode[self.did_main[i] as usize & 31]; + let v = self.main[i]; + if m.rgb { + self.gamma_rgb(v, v >> 8, v >> 16) + } else { + let c = self.cmap[(m.cmap_base as usize + (v & 0xFFF) as usize) & 0x1FFF]; + self.gamma_rgb(c >> 16, c >> 8, c) + } + } + } + } + + /// The cursor's 2-bit value at a screen position, if the cursor covers + /// it with a non-transparent pixel. + #[inline] + fn cursor_value(&self, x: usize, y: usize) -> Option { + if !self.cursor_on { + return None; + } + let (cx, cy) = (x as i32 - self.cursor_x, y as i32 - self.cursor_y); + let size = self.cursor_size as i32; + if !(0..size).contains(&cx) || !(0..size).contains(&cy) { + return None; + } + let c = self.cursor[cy as usize * CURSOR_MAX + cx as usize]; + (c != 0).then_some(c) + } + + #[inline] + fn gamma_rgb(&self, r: u32, g: u32, b: u32) -> u32 { + let lvl = |v: u32, comp: usize| self.gamma[(v & 0xFF) as usize][comp] as u32; + 0xFF00_0000 | lvl(b, 2) << 16 | lvl(g, 1) << 8 | lvl(r, 0) + } + + /// The whole frame into `out` (stride `OUT_STRIDE`, `height` rows of + /// `width` pixels). + pub fn compose(&self, out: &mut [u32]) { + for y in 0..self.height { + let row = &mut out[y * OUT_STRIDE..y * OUT_STRIDE + self.width]; + for (x, o) in row.iter_mut().enumerate() { + *o = self.pixel(x, y); + } + } + } +} + +/// The buffers scanout reads: the XMAP DIB pointers' main buffer and, when +/// its pointer differs, the overlay. +pub fn scanout_buffers(rss: &Rss, dcb: &Dcb) -> (Buffer, Option) { + let dib = dcb.xmap.dib_pointers(); + let drbsize = rss.reg(rss::reg::DRBSIZE); + let main = Buffer::new(dib, Kind::Wide, drbsize); + let overlay = Buffer::new(dib >> 20, Kind::Overlay, drbsize); + (main, (overlay.ptr != main.ptr).then_some(overlay)) +} + +/// The displayed size the VC3 is programmed for. Height: the scanlines in +/// the main window-ID frame table (one entry per visible line). Width: by +/// height, every IMPACT video format having its own (1280x492 stereo, +/// 1280x960 and 1920x1035 HDTV included). Only for a height not in the list, +/// the timing tables decoded with the VC2 layout, snapped to a standard +/// width; that decode is loose on the VC3 (978 for 1024, 1260 or 1680 for +/// 1280), its blanking bits not being pinned down. Before IRIX loads its +/// tables: `DEFAULT_SIZE`. +pub fn display_size(dcb: &Dcb) -> (usize, usize) { + const WIDTHS: [usize; 7] = [640, 800, 1024, 1152, 1280, 1600, 1920]; + let lines = dcb.vc3.did_lines(); + let Some(h) = (1..=H.min(crate::disp::FB_MAX_H)).contains(&lines).then_some(lines) else { + return DEFAULT_SIZE; + }; + let w = match h { + 480 => 640, + 492 | 960 | 1024 => 1280, + 576 => 768, + 600 => 800, + 768 => 1024, + 864 => 1152, + 1035 => 1920, + 1200 => 1600, + _ => match dcb.vc3.timing_size() { + Some((tw, _)) => *WIDTHS.iter().min_by_key(|&&s| s.abs_diff(tw)).unwrap(), + None => DEFAULT_SIZE.0, + }, + }; + (w.min(W), h) +} + +/// Window IDs of one display row from its VC3 runs `(first x, did)`. A row +/// with no run at x = 0 starts in window ID 0; each run lasts until the +/// next one's x (clamped to the screen); runs out of order overwrite the +/// pixels earlier ones set. +fn fill_did_row(row: &mut [u8], runs: &mut Vec<(u16, u8)>) { + row.fill(0); + if runs.is_empty() || runs[0].0 != 0 { + runs.insert(0, (0, 0)); + } + for (k, &(x0, did)) in runs.iter().enumerate() { + let x0 = (x0 as usize).min(W); + let x1 = runs.get(k + 1).map_or(W, |r| (r.0 as usize).min(W)); + if x1 > x0 { + row[x0..x1].fill(did); + } + } +} diff --git a/src/dev/mgras/ge11.rs b/src/dev/mgras/ge11.rs new file mode 100644 index 00000000..ee6e6337 --- /dev/null +++ b/src/dev/mgras/ge11.rs @@ -0,0 +1,113 @@ +//! GE11 geometry engines as their diagnostic ports see them. The engines do +//! not run; they only hold downloaded microcode, read it back, and answer +//! "started". + +use super::hq3::host; +use super::plain::Ring; + +/// Microcode lines an engine holds (ge11.bin is about 79K lines). +const UCODE_LINES: usize = 0x2_0000; + +/// One engine's diagnostic state. +#[repr(C)] +struct GeDiag { + /// Current diagnostic address: a microcode line (from `UCODE_BASE`) or an + /// internal register. + addr: u32, + /// Which 32-bit word of the current 72-bit microcode line comes next. + word: usize, + ucode: [[u32; 3]; UCODE_LINES], +} + +impl GeDiag { + const UCODE_BASE: u32 = 0x20_0000; + /// Internal register: execution control; bit 0 starts the engine. + const EXEC_CONTROL: u32 = 0x4_0000; + + /// Microcode line at diagnostic address `addr`, if it is one. + fn line(&self, addr: u32) -> Option<&[u32; 3]> { + self.ucode.get(addr.wrapping_sub(Self::UCODE_BASE) as usize) + } + + /// Words a readback starting at `addr` delivers, in order: for each pair + /// of lines, word 0 and the top byte of the first, then word 1 of the + /// second. A readback starting on an odd line is preceded by two words + /// that carry nothing. (The driver's verifier reads exactly this; the + /// order is inferred from it.) + fn readback(&self, lines: u32) -> Vec { + let w = |l: u32, i: usize| self.line(l).map(|x| x[i]).unwrap_or(0); + let mut out = Vec::new(); + let mut l = self.addr; + if l.wrapping_sub(Self::UCODE_BASE) & 1 == 1 { + out.extend([0, 0]); + } + for _ in 0..lines / 2 { + out.extend([w(l, 0), w(l, 2) & 0xFF, w(l + 1, 1)]); + l += 2; + } + out + } +} + +/// Both engines' diagnostic ports and their shared readback queue. Plain +/// data, valid zeroed. +#[repr(C)] +pub struct Ge11 { + ge: [GeDiag; 2], + /// Pending diagnostic readback words, oldest first. + pub out: Ring<{ 1 << 18 }>, +} + +impl Ge11 { + /// Engine `ge`'s current diagnostic address. + pub fn diag_addr(&self, ge: usize) -> u32 { + self.ge[ge & 1].addr + } + + /// Engine `ge`'s microcode line `i` (from the start of microcode). + pub fn ucode_line(&self, ge: usize, i: usize) -> Option<[u32; 3]> { + self.ge[ge & 1].ucode.get(i).copied() + } + + /// 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) + } + + /// A write to a diagnostic data or address port. True when an engine was + /// started: its version program's answer (revision 1) is then waiting in + /// the readback registers. + pub fn write(&mut self, off: u32, val: u32) -> bool { + let n = host::GE_DIAG.iter().position(|&(d, a)| off == d || off == a).unwrap(); + let (data_port, _) = host::GE_DIAG[n]; + let ge = &mut self.ge[n]; + if off != data_port { + if val & 0x8000_0000 != 0 { + // Read request for `val & 0x7FFF_FFFF` lines from `addr`; it + // replaces anything a previous request left unread. + let words = ge.readback(val & 0x7FFF_FFFF); + self.out.clear(); + for w in words { + self.out.push(w); + } + } else { + ge.addr = val; + ge.word = 0; + } + return false; + } + if ge.addr >= GeDiag::UCODE_BASE { + if let Some(line) = ge.ucode.get_mut((ge.addr - GeDiag::UCODE_BASE) as usize) { + line[ge.word] = val; + } + ge.word += 1; + if ge.word == 3 { + ge.word = 0; + ge.addr += 1; + } + false + } else { + ge.addr == GeDiag::EXEC_CONTROL && val & 1 != 0 + } + } +} diff --git a/src/dev/mgras/gl.rs b/src/dev/mgras/gl.rs new file mode 100644 index 00000000..312f3f0a --- /dev/null +++ b/src/dev/mgras/gl.rs @@ -0,0 +1,2297 @@ +//! OpenGL on IMPACT: high-level emulation of the GE11 geometry microcode. +//! +//! libGLcore writes GL commands into the HQ3 command FIFO as command- +//! processor tokens (`hq3::token_name`). On the board the HQ3 hands them to +//! the GE11s, whose microcode keeps the GL state, transforms, lights and +//! clips, and sets up primitives in the RE4. Here the HQ thread does the +//! same with the shared GL core (`crate::dev::gl`): triangles go through the +//! RE4's area registers the way SGI's diagnostic triangle code sets them up +//! (`rss::Rss::triangle`), lines through the GL line registers +//! (`rss::Rss::gl_line`), and the per-fragment state (alpha, stencil, depth, +//! blend, logic op, write masks) into the PP1 registers (layouts in +//! `rss.rs`, provisional where only microcode writes them). +//! +//! The GL context's window (origin, screen masks, buffer pointers) reaches +//! the GE from the kernel (`Hq3Engine::apply_window`). The RE4 also serves +//! the X server, which keeps its own origin, masks, Y-flip and fill modes +//! in the same registers; the RSS saves them when a GL primitive batch +//! starts and puts them back when it ends (`rss::Rss::gl_enter`), the +//! effect of the RE4's separate register contexts. +//! +//! Token data formats, from traces of libGLcore (IRIX 6.5.22): compare +//! functions arrive as indices 0..7 (NEVER..ALWAYS), blend factors and +//! polygon modes as GL enums, logic ops as indices (GL order = X11 order), +//! stencil ops as indices (KEEP ZERO REPLACE INCR DECR INVERT), the alpha +//! reference as ref * 4096, the depth clear value as a 24-bit integer, the +//! colour mask as RGBA bits 0..3, the line stipple as (repeat - 1, pattern +//! bit-reversed: first bit in bit 15). +//! +//! Lighting (tokens per glLight / glMaterial / glLightModel parameter, light +//! positions and clip planes in object space, transformed here by the +//! modelview current when they arrive, as OpenGL specifies), fog (per +//! vertex) and user clip planes use the shared GL core. +//! +//! Texture (2D): texture coordinates go through the texture matrix and are +//! clipped with the vertex; triangles get S/W, T/W and Q/W planes in the +//! TE's iterator registers (`te1.rs`). The context's TE registers arrive in +//! libGLcore's register lists; the GE keeps a shadow of them and loads the +//! TE when a batch starts, so contexts do not see each other's textures. +//! Texture images arrive as SEND_PIXELS with routine 0x511A and go to the +//! texture loader (`tex_load`). +//! +//! Not yet: point size, textured lines and points, texgen, 1D/3D textures. + +use super::hq3::Hq3Sink; +use super::te1::{self, reg as te_reg}; +use crate::dev::gl::light::{Lighting, MAX_LIGHTS}; +use crate::dev::gl::math::{self, Mat4, Stack}; +use crate::dev::gl::vertex::{Clip, Viewport, Wv, MAX_POLY}; + +/// GL tokens the GE handles (numbers as in `hq3::token_name`). +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). + pub const INIT_RGB: u32 = 0x009; + pub const INIT_CI: u32 = 0x00A; + /// glClearIndex. + pub const CLEAR_INDEX: u32 = 0x0BD; + /// glRasterPos (object x, y [, z [, w]]). + pub const RASTER_POS4F: u32 = 0x038; + /// glRasterPos / glBitmap raster position update (floats dx, dy). + pub const UPDATE_RASTER_POS: u32 = 0x0D6; + /// Latch raster position attributes to current attributes. + pub const LOAD_RASTER_POS_INFO: u32 = 0x0D7; + /// glIndexMask. + pub const INDEX_MASK: u32 = 0x03D; + /// glTexCoord (s [, t [, r [, q]]]). + pub const TEX_COORD: u32 = 0x008; + /// Texturing on / off for the primitives that follow (libGLcore + /// mgrTM_SendTexmode1: on when the bound texture is complete). + pub const TEX_ON: u32 = 0x00B; + pub const TEX_OFF: u32 = 0x00C; + /// GE pixel state (ERAM) writes: [?, ?, bytes, address, 0, 2, count, + /// values]; the layout PIXEL_STATE also has. + pub const INIT_PIXEL_STATE_ERAM: u32 = 0x0CD; + /// One register written through an indexed GE shadow table: [index + /// shadow, table shadow, register, value] (TXMIPMAP, TXBORDER and + /// DETAILSCALE, indexed by TXADDR). + pub const RSS_REG_SHADOW_IDX: u32 = 0x0EC; + /// glTexGen: [coordinate 0x2000 + i (S T R Q), GL_TEXTURE_GEN_MODE, + /// mode]; a plane's header [coordinate, GL_OBJECT_PLANE / EYE_PLANE], + /// then its data (4 floats). + pub const TEX_GENIV: u32 = 0x05A; + pub const TEX_GEN_PLANE_HEADER: u32 = 0x05B; + pub const TEX_GEN_PLANE_DATA: u32 = 0x05C; + /// glEnable / glDisable of a capability the GE keeps (GL enum; seen: + /// GL_TEXTURE_GEN_S..Q). + pub const ENABLE_CAP: u32 = 0x075; + /// glGetTexImage: [width, height, s and t offsets (floats, 0.5: texel + /// centres), transfer mode, the raster interface start word]; the host + /// starts the read itself (raster interface register 5). + pub const READ_TEXTURE: u32 = 0x0F0; + /// The texture manager's save/restore of TRAM pages (mgrTM_SR_TexRead + /// / mgrTM_SR_TexLoad): a raw read of the sampler's level [rows, + /// xfrmode, 0x400D], and the GE set up for texels by host DMA [?, ?, + /// ?, GE routine]. + pub const READ_TEXTURE_RAW: u32 = 0x0E5; + pub const WRITE_DMAGESETUP: u32 = 0x09D; + /// glPointSize (float). + pub const POINT_SIZE: u32 = 0x04D; + pub const DISABLE_CAP: u32 = 0x076; + /// glBitmap: format, row length in bits, rows, x / y origin and x / y + /// move (floats), data words; the rows follow as FIFO pixel data. + pub const BITMAP: u32 = 0x094; + /// glDrawPixels through the GE's pixel path (libGLcore + /// __glMgrSendPixels), 8 words: data words per row, two skips, the + /// pixel-state counts, rows (provisional), the GE routine (0x49D0 for + /// host to raster), flags; the rows follow as FIFO pixel data. + pub const SEND_PIXELS: u32 = 0x08D; + /// A write of GE pixel-path state (libGLcore): words 3 the GE address, + /// 6 the count, then the values. Address 0x192 holds 1 / pixel zoom + /// (1.0 in mandel, 1/6 in snoop at zoom 6). + pub const PIXEL_STATE: u32 = 0x080; + /// glReadPixels into host memory (libGLcore GetPixels*): 1 word; the + /// read block comes from RSS_REG_SHADOW, the host DMA follows. + pub const GET_PIXELS: u32 = 0x0DB; + /// glReadBuffer: words, the third the GL enum (0x404 front, 0x405 back). + pub const READ_BUFFER: u32 = 0x044; + /// End of a pixel operation: the raster registers saved by SAVE_RSS + /// come back. + pub const RESTORE_RSS: u32 = 0x0D3; + /// Start of a pixel operation (libGLcore __glMgrSaveRSS, 5 words). + pub const SAVE_RSS: u32 = 0x0D2; + /// glNormal3f (other forms through the HQ's conversion field). + pub const NORMAL: u32 = 0x007; + pub const LIGHTING_ON: u32 = 0x00D; + pub const LIGHTING_OFF: u32 = 0x00E; + /// glMaterial: face (GL_FRONT/BACK/FRONT_AND_BACK), pname, 4 values. + pub const MATERIAL: u32 = 0x011; + /// glLight: light index, values. Position and spot direction are in + /// object space. + pub const LIGHT_AMBIENT: u32 = 0x012; + pub const LIGHT_DIFFUSE: u32 = 0x013; + pub const LIGHT_SPECULAR: u32 = 0x014; + pub const LIGHT_POSITION: u32 = 0x04F; + pub const LIGHT_SPOT_DIRECTION: u32 = 0x050; + pub const LIGHT_SPOT_EXPONENT: u32 = 0x051; + pub const LIGHT_SPOT_CUTOFF: u32 = 0x052; + pub const LIGHT_CONSTANT_ATTENUATION: u32 = 0x053; + pub const LIGHT_LINEAR_ATTENUATION: u32 = 0x054; + pub const LIGHT_QUADRATIC_ATTENUATION: u32 = 0x055; + pub const LIGHT_MODEL_AMBIENT: u32 = 0x057; + pub const LIGHT_MODEL_LOCAL_VIEWER: u32 = 0x058; + pub const LIGHT_MODEL_TWO_SIDE: u32 = 0x059; + /// glEnable / glDisable(GL_LIGHTi): the light index. + pub const LIGHT_ON: u32 = 0x071; + pub const LIGHT_OFF: u32 = 0x0A0; + /// glColorMaterial(face, mode): front, back, front and back. + pub const COLOR_MATERIAL_FRONT: u32 = 0x0BF; + pub const COLOR_MATERIAL_BACK: u32 = 0x0C0; + pub const COLOR_MATERIAL_BOTH: u32 = 0x0C1; + pub const COLOR_MATERIAL_ON: u32 = 0x0C2; + pub const COLOR_MATERIAL_OFF: u32 = 0x0C3; + /// glEnable / glDisable(GL_NORMALIZE): 1 / 0. + pub const NORMALIZE: u32 = 0x0A3; + /// glClipPlane: 0x3000 + plane, a, b, c, d (object space); enable / + /// disable: 0x3000 + plane. + pub const CLIP_PLANE: u32 = 0x0A5; + pub const CLIP_PLANE_ON: u32 = 0x0A6; + pub const CLIP_PLANE_OFF: u32 = 0x0A7; + /// Fog: enable (1 / 0), mode (GL enum), colour, start, end, density. + pub const FOG: u32 = 0x068; + pub const FOG_MODE: u32 = 0x0B9; + pub const FOG_COLOR: u32 = 0x0B4; + pub const FOG_START: u32 = 0x0B5; + pub const FOG_END: u32 = 0x0B6; + pub const FOG_DENSITY: u32 = 0x0B7; + /// glDrawBuffer: buffer bits (1 front left, 2 front right, 4 back + /// left, 8 back right; 0 none), then 2 words not decoded (1, 0 seen). + pub const DRAW_BUFFER: u32 = 0x049; + /// IRIS GL through IGLOO (libGLcore mgras_igloo.c): swaptmesh, + /// lmcolor(LMC_COLOR) on / off, n3f (a normal, 3 floats). + pub const SWAPTMESH: u32 = 0x0DC; + pub const LMC_COLOR_ON: u32 = 0x0DD; + pub const LMC_COLOR_OFF: u32 = 0x0DE; + pub const IRIS_NORMAL: u32 = 0x0DF; + pub const COLOR4F: u32 = 0x002; + pub const CLEAR: u32 = 0x015; + pub const BEGIN_POINTS: u32 = 0x016; + pub const BEGIN_POLYGON: u32 = 0x01F; + pub const END_POINTS: u32 = 0x020; + pub const END_POLYGON: u32 = 0x029; + pub const MATRIX_MODE: u32 = 0x02A; + pub const LOAD_MATRIXD: u32 = 0x02B; + pub const LOAD_IDENTITY: u32 = 0x02C; + pub const MULT_MATRIXD: u32 = 0x02D; + pub const POP_MATRIX: u32 = 0x02E; + pub const PUSH_MATRIX: u32 = 0x02F; + pub const ROTATED: u32 = 0x030; + pub const SCALED: u32 = 0x031; + pub const TRANSLATED: u32 = 0x032; + pub const VIEWPORT: u32 = 0x033; + pub const FRUSTUM: u32 = 0x034; + pub const ORTHO: u32 = 0x035; + pub const FLUSH: u32 = 0x036; + pub const SCISSOR: u32 = 0x039; + pub const STENCIL_MASK: u32 = 0x03A; + pub const COLOR_MASK: u32 = 0x03B; + pub const DEPTH_MASK: u32 = 0x03C; + pub const ALPHA_FUNC: u32 = 0x03E; + pub const BLEND_FUNC: u32 = 0x03F; + pub const LOGIC_OP: u32 = 0x040; + pub const STENCIL_FUNC: u32 = 0x041; + pub const STENCIL_OP: u32 = 0x042; + pub const DEPTH_FUNC: u32 = 0x043; + pub const LINE_WIDTH: u32 = 0x045; + pub const LINE_STIPPLE: u32 = 0x046; + pub const SHADE_MODEL: u32 = 0x047; + pub const DEPTH_RANGE: u32 = 0x048; + /// glClear's depth / stencil parts (sent only when the visual has the + /// buffer). + pub const CLEAR_DEPTH_BUFFER: u32 = 0x04A; + pub const CLEAR_STENCIL_BUFFER: u32 = 0x04C; + pub const LOAD_POLYGON_STIPPLE: u32 = 0x04E; + pub const FRONT_FACE: u32 = 0x056; + /// glEnable / glDisable, 1 / 0 (3 / 0 for dither). + pub const ALPHA_TEST: u32 = 0x064; + pub const BLEND: u32 = 0x065; + pub const DEPTH_TEST: u32 = 0x066; + pub const DITHER: u32 = 0x067; + pub const LINE_STIPPLE_ON: u32 = 0x06A; + pub const LOGIC_OP_ON: u32 = 0x06B; + pub const POLYGON_STIPPLE_ON: u32 = 0x06D; + pub const SCISSOR_TEST: u32 = 0x06E; + pub const STENCIL_TEST: u32 = 0x06F; + /// glEnable / glDisable(GL_CULL_FACE), no data. + pub const CULL_ON: u32 = 0x072; + pub const CULL_OFF: u32 = 0x073; + pub const CULL_FACE: u32 = 0x074; + pub const POLYGON_MODE: u32 = 0x0A8; + pub const CLEAR_COLOR: u32 = 0x0BA; + pub const CLEAR_DEPTH: u32 = 0x0BC; + pub const CLEAR_STENCIL: u32 = 0x0BE; +} + +/// Primitive kinds, BEGIN_* minus BEGIN_POINTS (GL_POINTS .. GL_POLYGON). +mod prim { + pub const POINTS: u32 = 0; + pub const LINES: u32 = 1; + pub const LSTRIP: u32 = 2; + pub const LLOOP: u32 = 3; + pub const TRIANGLES: u32 = 4; + pub const TSTRIP: u32 = 5; + pub const TFAN: u32 = 6; + pub const QUADS: u32 = 7; + pub const QSTRIP: u32 = 8; + pub const POLYGON: u32 = 9; + /// No primitive open. + pub const NONE: u32 = 0xFF; +} + +/// Raster registers the GE writes. +mod re { + pub const TRI_X0: u32 = 0x000; + pub const TRI_X2: u32 = 0x002; + pub const TRI_YMID: u32 = 0x004; + pub const TRI_DXDY0: u32 = 0x006; + pub const TRI_DXDY1: u32 = 0x008; + pub const TRI_DXDY2: u32 = 0x00A; + pub const GLINE_XSTARTF: u32 = 0x00C; + pub const IR: u32 = 0x013; + pub const IR_ALIAS: u32 = 0x045; + pub const BLOCK_XYSTARTI: u32 = 0x046; + pub const BLOCK_XYENDI: u32 = 0x047; + pub const XFRSIZE: u32 = 0x153; + pub const XFRCONTROL: u32 = 0x102; + pub const XFRMODE: u32 = 0x159; + pub const RED: u32 = 0x05C; + pub const DRE: u32 = 0x060; + pub const Z: u32 = 0x068; + pub const FILLMODE: u32 = 0x110; + pub const XYWIN: u32 = 0x115; + pub const GLINECONFIG: u32 = 0x146; + pub const SCRMSK1X: u32 = 0x147; + pub const WINMODE: u32 = 0x14F; + pub const LSPAT: u32 = 0x15A; + pub const LSCRL: u32 = 0x15B; + pub const DEVICE_ADDR: u32 = 0x15C; + pub const DEVICE_DATA: u32 = 0x15D; + pub const PP1FILLMODE: u32 = 0x161; + pub const COLORMASKMSBS: u32 = 0x162; + pub const COLORMASKLSBSA: u32 = 0x163; + pub const BLENDFACTOR: u32 = 0x165; + pub const STENCILMODE: u32 = 0x166; + pub const STENCILMASK: u32 = 0x167; + pub const ZMODE: u32 = 0x168; + pub const AFUNCMODE: u32 = 0x169; + pub const DRBPOINTERS: u32 = 0x16D; + pub const FILL_COLOR_R: u32 = 0x176; +} + +/// Fill mode: fast fill from the fill colour registers, block type 0. +const FILL_FAST: u32 = 1 << 20; +/// Fill mode: block type 5, a DMA write transfer (the X server's value). +const FILL_DMA_WRITE: u32 = 0x1140_0000; +/// Fill mode: block type 4, a DMA read transfer (SAVE_RSS's value). +const FILL_DMA_READ: u32 = 0x0100_0000; +/// Fill mode: polygon stipple (provisional bit, see rss.rs) / line stipple. +const FILL_POLY_STIPPLE: u32 = 1 << 2; +const FILL_LINE_STIPPLE: u32 = 1 << 5; +/// PP1 fill mode for a 24-bit RGB window drawing buffer A (the 6.5.22 X +/// server's TrueColor value: pixel type 2, logic op copy enabled, draw +/// buffer 0x01); bits 29:26 hold the logic op. +const PP1_RGB24_BUFFER_A: u32 = 0x0C00_6204; +/// Area / GL line IR opcodes (provisional numbers, see rss.rs). +const IR_AREA_LTOR: u32 = super::rss::OP_AREA_LTOR; +const IR_AREA_RTOL: u32 = super::rss::OP_AREA_RTOL; +const IR_GL_LINE: u32 = super::rss::OP_GL_LINE; +/// SEND_PIXELS' routine for a texture image (glDrawPixels' is 0x49D0). +const SEND_PIXELS_TEXTURE: u32 = 0x511A; +/// XFRCONTROL: start a DMA transfer to the texture side (te1.rs, rss.rs). +const TE_LOAD_START: u32 = 0x5; +/// XFRCONTROL: a DMA read from the texture side (glGetTexImage starts it +/// as 0x400D through the raster interface). +const TE_READ_START: u32 = 0xD; +/// IR for a block with Setup. +const IR_BLOCK_SETUP: u32 = 0x18; +/// The RE4 takes positions as floats of value + this bias: in +/// [32768, 65536) a float's mantissa is fixed point with 8 fractional bits. +const POS_BIAS: f32 = 49151.5; +/// Largest window Z (24-bit depth buffer). +const ZMAX: f32 = 16_777_215.0; + +/// The GL window's raster state, from the kernel. +#[derive(Clone, Copy, Default)] +#[repr(C)] +pub struct Window { + pub valid: u32, + /// x | y << 16, y bottom-up (window's lower left in framebuffer rows). + pub origin: u32, + pub mode: u32, + /// Screen masks 1-4: (x, y) ranges `min << 16 | max`. + pub masks: [[u32; 2]; 4], + pub drb: u32, +} + +/// GL state held by the GE. Plain data, valid zeroed (`ensure_init` sets +/// GL's initial values on first use). +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Gl { + inited: u32, + pub window: Window, + matrix_mode: u32, + mv: Stack<32>, + proj: Stack<4>, + tex: Stack<4>, + mvp: Mat4, + mvp_dirty: u32, + vp: Viewport, + depth_range: [f32; 2], + clip: Clip, + color: [f32; 4], + normal: [f32; 3], + /// Lighting and fog (shared core); enabled lights as a bit mask; the + /// normal matrix is stale; fog start, end, density. + lt: Lighting, + lights_on: u32, + nm_dirty: u32, + fog_start: f32, + fog_end: f32, + fog_density: f32, + /// IRIS GL lmcolor(LMC_COLOR): on, and whether a colour has come after + /// the last normal (what follows is then drawn unlit; IRIS GL + /// Programming Guide 9.6.4, as GR2's GE7 HLE models it). + lmc_color: u32, + unlit: u32, + /// Triangle strip: winding toggles per triangle and per swaptmesh. + tflip: u32, + /// glDrawBuffer bits, and whether the buffers have swapped an odd + /// number of times (front = B, back = A). + draw_bits: u32, + swapped: u32, + smooth: u32, + clear_color: [f32; 4], + /// Depth: test on, function index, write mask on, clear value (24-bit). + depth_test: u32, + depth_func: u32, + depth_mask: u32, + clear_depth: u32, + /// Culling: on, mode (GL_FRONT / BACK / FRONT_AND_BACK), front face + /// counter-clockwise; polygon mode front / back (GL_POINT/LINE/FILL). + cull: u32, + cull_mode: u32, + front_ccw: u32, + polymode: [u32; 2], + dither: u32, + scissor_test: u32, + /// Scissor, window coordinates, inclusive: x0, x1, y0, y1. + scissor: [i32; 4], + /// Blend: on, PP1 factor codes (see rss.rs). + blend: u32, + blend_src: u32, + blend_dst: u32, + /// Alpha test: on, function index, reference * 4096. + alpha_test: u32, + alpha_func: u32, + alpha_ref: u32, + /// Colour logic op: on, op index. + logic: u32, + logic_op: u32, + /// Stencil: on, function, ref, compare mask, ops (fail, zfail, zpass), + /// write mask, clear value. + stencil: u32, + st_func: u32, + st_ref: u32, + st_cmask: u32, + st_ops: [u32; 3], + st_wmask: u32, + clear_stencil: u32, + /// glColorMask: RGBA in bits 0..3. + color_mask: u32, + /// Lines: width, stipple on, repeat - 1, pattern (first bit 15). + line_width: f32, + line_stipple: u32, + line_repeat: u32, + line_pattern: u32, + /// Polygon stipple: on, rows (window y mod 32; MSB = window x mod 32 0). + poly_stipple: u32, + stipple_rows: [u32; 32], + /// The current raster position: window x, y, z, its colour, and + /// whether it is valid (inside the view volume). + raster: [f32; 3], + raster_color: [f32; 4], + raster_valid: u32, + /// Colour-index context (INIT_CI): colours are indices (red / 4095 in + /// `color`), drawn as 12-bit colour index pixels; and the clear index. + ci: u32, + clear_index: u32, + index_mask: u32, + /// A glBitmap header waiting for its rows (pixel data). + bitmap: [u32; 8], + bitmap_pending: u32, + /// A SEND_PIXELS header waiting for its rows, and the raster engine's + /// transfer mode as the driver last set it (host pixel format). + send_pixels: [u32; 8], + send_pixels_pending: u32, + pub xfrmode: u32, + /// 1 / the pixel zoom (GE pixel state 0x192); 0 when never set. + zoom_inv: f32, + /// The pixel block (block_xystarti / endi, window coordinates) the + /// driver last set through RSS_REG_SHADOW / SET, and glReadBuffer back. + pub pixel_block: [u32; 2], + read_back: u32, + /// Between SAVE_RSS and RESTORE_RSS: a GL pixel operation, whose + /// register lists belong inside the raster bracket. + pub pixel_op: u32, + /// Primitive assembly: the open primitive (`prim`), vertices since its + /// BEGIN, and the buffered vertices (strips and fans keep the first + /// and the last two; quads and polygons keep them all). + prim: u32, + nv: u32, + vb: [Wv; MAX_POLY], + /// A GL batch's raster state is loaded (`begin_raster`), and the fill + /// mode last written in it. + raster_loaded: u32, + fill_loaded: u32, + /// Texturing: the current texture coordinates; on (TEX_ON / TEX_OFF); + /// the GE's shadow of the context's TE registers (`te1::CONTEXT_REGS` + /// order), TXADDR and the tables it indexes; the TE must be loaded from + /// the shadow at the next batch. + texcoord: [f32; 4], + tex_on: u32, + te: [u32; te1::CONTEXT_REGS.len()], + te_index: u32, + te_tables: [[u32; te1::TABLE_LEN]; 3], + pub te_dirty: u32, + /// The texture loader's GE pixel state: the sub-image (ERAM 0xDA8: s + /// and t origin, width, height, transfer size) and where it goes + /// (0xDB1: level width, ?, level, page | bank << 8, border page, ?). + tl_rect: [u32; 5], + tl_dest: [u32; 6], + /// The host pixel format's scale to 0..1 (ERAM 0xD0A: 1/255 for 8-bit + /// components, 1/65535 for 16-bit, 1.0 for floats). + tl_scale: u32, + /// The host element the GE unpacks (pixel state 0xDBB): 1 bytes, 3 16-bit + /// (components, or packed 4-4-4-4 / 5-5-5-1), 4 32-bit (packed 8-8-8-8, + /// 10-10-10-2, or components). + tl_elem: u32, + /// Texture coordinate generation, per coordinate S T R Q: on (bits), + /// mode (0 object linear, 1 eye linear, 2 sphere map), object and eye + /// planes (eye planes in eye space), and the plane the next + /// TEX_GEN_PLANE_DATA is for (coordinate | eye << 2). + texgen_on: u32, + texgen_mode: [u32; 4], + texgen_obj: [[f32; 4]; 4], + texgen_eye: [[f32; 4]; 4], + texgen_plane: u32, + /// glPointSize. + point_size: f32, + /// A texture read transfer is armed (READ_TEXTURE .. RESTORE_RSS). + tex_reading: u32, + pub stats_triangles: u64, +} + +impl Gl { + /// The state that places drawing (window, buffers, viewport, scissor), + /// for the monitor. + pub fn describe(&self) -> String { + let w = &self.window; + format!( + " window: valid {} origin {:#x} mode {:#x} DRBpointers {:#x} masks {:x?}\n \ + draw buffer bits {:#x}, swapped {}, colour mask {:#x}\n \ + viewport x {}..{} y {}..{}, scissor {} {:?}\n \ + colour {:?}, clear colour {:?}, lighting {} (lights {:#x}), depth test {}", + w.valid, w.origin, w.mode, w.drb, w.masks, self.draw_bits, self.swapped, self.color_mask, + self.vp.x0, self.vp.x1, self.vp.y0, self.vp.y1, self.scissor_test, self.scissor, + self.color, self.clear_color, self.lt.on, self.lights_on, self.depth_test) + } + + fn ensure_init(&mut self) { + if self.inited != 0 { + return; + } + self.inited = 1; + self.mv.init(); + self.proj.init(); + self.tex.init(); + self.mvp = math::IDENT; + self.depth_range = [0.0, 1.0]; + self.vp.set(0.0, 0.0, 1.0, 1.0, 0.0, 1.0, ZMAX); + self.color = [1.0; 4]; + self.normal = [0.0, 0.0, 1.0]; + self.lt.init(); + self.nm_dirty = 1; + self.fog_end = 1.0; + self.fog_density = 1.0; + self.lt.fog_mode = 1; + self.update_fog(); + self.smooth = 1; + self.draw_bits = 1; + self.depth_func = 1; + self.depth_mask = 1; + self.clear_depth = ZMAX as u32; + self.cull_mode = 0x405; + self.front_ccw = 1; + self.polymode = [0x1B02; 2]; + self.dither = 1; + self.blend_dst = 0; + self.blend_src = 1; + self.alpha_func = 7; + self.logic_op = 3; + self.st_func = 7; + self.st_cmask = 0xFF; + self.st_wmask = 0xFF; + self.color_mask = 0xF; + self.index_mask = 0xFFF; + self.scissor = [0, 1279, 0, 1023]; + self.line_width = 1.0; + self.line_pattern = 0xFFFF; + self.prim = prim::NONE; + self.matrix_mode = 0x1700; + } + + fn stack(&mut self) -> &mut dyn MatrixStack { + match self.matrix_mode { + 0x1701 => &mut self.proj, + 0x1702 => &mut self.tex, + _ => &mut self.mv, + } + } + + fn matrix_op(&mut self, f: impl FnOnce(&mut dyn MatrixStack)) { + f(self.stack()); + self.mvp_dirty = 1; + self.nm_dirty = 1; + } + + /// Rebuild the enabled-light chain from `lights_on`. + fn update_lights(&mut self) { + let mut prev: i32 = -1; + self.lt.head = -1; + self.lt.count = 0; + for i in 0..MAX_LIGHTS { + if self.lights_on & (1 << i) == 0 { + continue; + } + if prev < 0 { + self.lt.head = i as i32; + } else { + self.lt.lights[prev as usize].next = i as i32; + } + self.lt.lights[i].next = -1; + prev = i as i32; + self.lt.count += 1; + } + } + + /// The fog parameters in the shared core's form. + fn update_fog(&mut self) { + if self.lt.fog_mode == 0 { + let d = self.fog_end - self.fog_start; + (self.lt.fog_a, self.lt.fog_b) = (self.fog_end, if d != 0.0 { 1.0 / d } else { 0.0 }); + } else { + self.lt.fog_a = self.fog_density; + } + } + + /// A light parameter token: light index, then values. + fn light_param(&mut self, cmd: u32, d: &[u32]) { + let Some(&i) = d.first() else { return }; + if i as usize >= MAX_LIGHTS { + return; + } + let v: Vec = d[1..].iter().map(|&w| f32::from_bits(w)).collect(); + let g = |k: usize| v.get(k).copied().unwrap_or(0.0); + let mv = *self.mv.get(); + let l = &mut self.lt.lights[i as usize]; + match cmd { + tok::LIGHT_AMBIENT => l.ambient = [g(0), g(1), g(2)], + tok::LIGHT_DIFFUSE => l.diffuse = [g(0), g(1), g(2)], + tok::LIGHT_SPECULAR => l.specular = [g(0), g(1), g(2)], + tok::LIGHT_POSITION => l.pos = math::xform(&mv, [g(0), g(1), g(2), g(3)]), + tok::LIGHT_SPOT_DIRECTION => { + let e = math::xform(&mv, [g(0), g(1), g(2), 0.0]); + let n = (e[0] * e[0] + e[1] * e[1] + e[2] * e[2]).sqrt(); + l.spot_dir = if n > 1e-20 { [e[0] / n, e[1] / n, e[2] / n] } else { [0.0, 0.0, -1.0] }; + } + tok::LIGHT_SPOT_EXPONENT => l.spot_exp = g(0), + tok::LIGHT_SPOT_CUTOFF => { + l.spot_on = (g(0) != 180.0) as u32; + l.spot_cos_cutoff = g(0).to_radians().cos(); + } + tok::LIGHT_CONSTANT_ATTENUATION => l.atten[2] = g(0), + tok::LIGHT_LINEAR_ATTENUATION => l.atten[0] = g(0), + _ => l.atten[1] = g(0), + } + } + + /// glMaterial: face, pname, values. + fn material(&mut self, d: &[u32]) { + if d.len() < 3 { + return; + } + let (face, pname) = (d[0], d[1]); + let v: Vec = d[2..].iter().map(|&w| f32::from_bits(w)).collect(); + let g = |k: usize| v.get(k).copied().unwrap_or(0.0); + let faces: &[usize] = match face { + 0x404 => &[0], + 0x405 => &[1], + _ => &[0, 1], + }; + for &f in faces { + let m = &mut self.lt.mat[f]; + match pname { + 0x1200 => m.ambient = [g(0), g(1), g(2)], + 0x1201 => m.diffuse = [g(0), g(1), g(2), g(3)], + 0x1202 => m.specular = [g(0), g(1), g(2)], + 0x1600 => m.emission = [g(0), g(1), g(2)], + 0x1601 => m.shininess = g(0), + 0x1602 => { + m.ambient = [g(0), g(1), g(2)]; + m.diffuse = [g(0), g(1), g(2), g(3)]; + } + _ => {} + } + } + } + + fn mvp(&mut self) -> Mat4 { + if self.mvp_dirty != 0 { + self.mvp_dirty = 0; + self.mvp = math::mul(self.proj.get(), self.mv.get()); + } + self.mvp + } + + /// A GE state word for a readback (`__MGR_RETURN_MODE` address), where + /// the HLE knows the layout: 0x1FC the dither enable (glFinish asks for + /// it); 0x3A-0x49 the current matrix mode's top matrix, 16 floats in + /// OpenGL's memory order (IRIS GL getmatrix under IGLOO reads them). + pub fn state_word(&mut self, addr: u32) -> Option { + self.ensure_init(); + match addr { + 0x1FC => Some(self.dither), + // Scissor box 0 (window coordinates inclusive: xmin, xmax, ymin, ymax): + // 0x200 returns (xmax << 16) | xmin, 0x202 returns (ymax << 16) | ymin. + // Read by libGLcore __glMgrim_DrawPixels for scissor box clipping. + 0x200 => Some(((self.scissor[1] as u32) << 16) | (self.scissor[0] as u32 & 0xFFFF)), + 0x202 => Some(((self.scissor[3] as u32) << 16) | (self.scissor[2] as u32 & 0xFFFF)), + // The current raster position, window x, y, z (floats), and + // whether it is valid: read by IRIS GL getcpos (gr_osview + // measures its text with it before and after charstr) and + // OpenGL glGetIntegerv(GL_CURRENT_RASTER_POSITION) / glDrawPixels. + // X and Y are biased by RE_COORD_BIAS (49151.5), which libGLcore + // subtracts when converting GE coordinates back to window space. + 0x2CD..=0x2CF => { + let v = match addr { + 0x2CD => self.raster[0] + 49151.5, + 0x2CE => self.raster[1] + 49151.5, + _ => self.raster[2] / ZMAX, + }; + Some(v.to_bits()) + } + 0x2C4 => Some(1.0f32.to_bits()), + // Bit 2 (0x4) is the valid flag tested by libGLcore (__glMgrDoGet); + // bit 0 (0x1) is tested by other callers. Return 0x7 when valid. + 0x2D0 => Some(if self.raster_valid != 0 { 0x7 } else { 0 }), + 0x2BF => Some(0.0f32.to_bits()), + 0x2C5..=0x2C8 => Some(self.raster_color[(addr - 0x2C5) as usize].to_bits()), + 0x3A..=0x49 => { + let m = match self.matrix_mode { + 0x1701 => self.proj.get(), + 0x1702 => self.tex.get(), + _ => self.mv.get(), + }; + Some(m[(addr - 0x3A) as usize].to_bits()) + } + _ => None, + } + } + + /// One GL token. False when the GE does not handle it (yet). + pub fn token(&mut self, cmd: u32, d: &[u32], sink: &mut dyn Hq3Sink) -> bool { + self.ensure_init(); + let w0 = d.first().copied().unwrap_or(0); + let on = (w0 != 0) as u32; + match cmd { + 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(); }), + tok::POP_MATRIX => self.matrix_op(|s| { s.pop(); }), + tok::LOAD_MATRIXD | tok::MULT_MATRIXD => { + let a = args(d, 16); + let mut m = [0.0f32; 16]; + m.copy_from_slice(&a[..16]); + if cmd == tok::LOAD_MATRIXD { + self.matrix_op(|s| s.load(m)); + } else { + self.matrix_op(|s| s.mult(&m)); + } + } + tok::ROTATED => { + let a = args(d, 4); + self.matrix_op(|s| s.mult(&math::rotate(a[0], a[1], a[2], a[3]))); + } + tok::TRANSLATED => { + let a = args(d, 3); + self.matrix_op(|s| s.mult(&math::translate(a[0], a[1], a[2]))); + } + tok::SCALED => { + let a = args(d, 3); + self.matrix_op(|s| s.mult(&math::scale(a[0], a[1], a[2]))); + } + tok::ORTHO | tok::FRUSTUM => { + let a = args(d, 6); + let m = if cmd == tok::ORTHO { + math::ortho(a[0], a[1], a[2], a[3], a[4], a[5]) + } else { + math::frustum(a[0], a[1], a[2], a[3], a[4], a[5]) + }; + self.matrix_op(|s| s.mult(&m)); + } + tok::VIEWPORT if d.len() >= 4 => { + let v: Vec = d[..4].iter().map(|&w| w as i32 as f32).collect(); + let [n, f] = self.depth_range; + self.vp.set(v[0], v[1], v[2], v[3], n, f, ZMAX); + } + tok::DEPTH_RANGE => { + let a = args(d, 2); + self.depth_range = [a[0], a[1]]; + let vp = self.vp; + self.vp.set(vp.x0, vp.y0, vp.x1 - vp.x0 + 1.0, vp.y1 - vp.y0 + 1.0, a[0], a[1], ZMAX); + } + tok::SHADE_MODEL => self.smooth = (w0 != 0x1D00) as u32, + tok::COLOR4F => { + self.color = args_f32(d); + if self.ci != 0 { + // An index: red carries it to the raster engine, which + // writes red * 4095 in colour-index pixels. + self.color = [self.color[0] / 4095.0, 0.0, 0.0, 1.0]; + } + // The raster colour follows the current colour: IRIS GL + // programs (gr_osview's colour-keyed legend, through IGLOO) + // set a colour between bitmaps without a new raster + // position, and libGLcore sends no colour for glBitmap. + // Our reading; OpenGL's latch at glRasterPos would then be + // libGLcore's business (UPDATE_RASTER_POS and friends). + self.raster_color = self.color; + self.lt.track_color(self.color); + if self.lmc_color != 0 && self.lt.cmat_on == 0 { + self.unlit = 1; + } + } + tok::NORMAL | tok::IRIS_NORMAL => { + let a = args_f32(d); + self.normal = [a[0], a[1], a[2]]; + self.unlit = 0; + } + tok::LMC_COLOR_ON => self.lmc_color = 1, + tok::LMC_COLOR_OFF => { + self.lmc_color = 0; + self.unlit = 0; + } + tok::SWAPTMESH => { + self.vb.swap(0, 1); + self.tflip ^= 1; + } + tok::LIGHTING_ON => self.lt.on = 1, + tok::LIGHTING_OFF => self.lt.on = 0, + tok::MATERIAL => self.material(d), + tok::LIGHT_AMBIENT | tok::LIGHT_DIFFUSE | tok::LIGHT_SPECULAR + | tok::LIGHT_POSITION..=tok::LIGHT_QUADRATIC_ATTENUATION => self.light_param(cmd, d), + tok::LIGHT_MODEL_AMBIENT => { + let a = args_f32(d); + self.lt.amb_sum = [a[0], a[1], a[2]]; + } + tok::LIGHT_MODEL_LOCAL_VIEWER => self.lt.local_viewer = on, + tok::LIGHT_MODEL_TWO_SIDE => self.lt.two_sided = on, + tok::LIGHT_ON | tok::LIGHT_OFF if (w0 as usize) < MAX_LIGHTS => { + if cmd == tok::LIGHT_ON { self.lights_on |= 1 << w0 } else { self.lights_on &= !(1 << w0) } + self.update_lights(); + } + tok::COLOR_MATERIAL_FRONT | tok::COLOR_MATERIAL_BACK | tok::COLOR_MATERIAL_BOTH => { + self.lt.cmat_face = cmd - tok::COLOR_MATERIAL_FRONT; + self.lt.cmat_param = match w0 { + 0x1600 => 1, + 0x1200 => 2, + 0x1201 => 3, + 0x1202 => 4, + _ => 5, + }; + } + tok::COLOR_MATERIAL_ON => { + self.lt.cmat_on = 1; + self.lt.track_color(self.color); + } + tok::COLOR_MATERIAL_OFF => self.lt.cmat_on = 0, + tok::NORMALIZE => self.lt.normalize = on, + tok::CLIP_PLANE if d.len() >= 5 && (w0.wrapping_sub(0x3000) as usize) < 6 => { + // Object space to eye space: the plane times the inverse of + // the current modelview. + let p = [f32::from_bits(d[1]), f32::from_bits(d[2]), f32::from_bits(d[3]), f32::from_bits(d[4])]; + let inv = math::invert(self.mv.get()).unwrap_or(math::IDENT); + let col = |c: usize| (0..4).map(|r| p[r] * inv[c * 4 + r]).sum::(); + self.clip.user[(w0 - 0x3000) as usize] = [col(0), col(1), col(2), col(3)]; + } + tok::CLIP_PLANE_ON | tok::CLIP_PLANE_OFF if (w0.wrapping_sub(0x3000) as usize) < 6 => { + let bit = 1 << (w0 - 0x3000); + if cmd == tok::CLIP_PLANE_ON { self.clip.user_on |= bit } else { self.clip.user_on &= !bit } + } + tok::FOG => self.lt.fog_on = on, + tok::FOG_MODE => { + self.lt.fog_mode = match w0 { 0x2601 => 0, 0x801 => 2, _ => 1 }; + self.update_fog(); + } + tok::FOG_COLOR => { + let a = args_f32(d); + self.lt.fog_color = [a[0], a[1], a[2]]; + } + tok::FOG_START | tok::FOG_END | tok::FOG_DENSITY => { + let v = f32::from_bits(w0); + match cmd { + tok::FOG_START => self.fog_start = v, + tok::FOG_END => self.fog_end = v, + _ => self.fog_density = v, + } + self.update_fog(); + } + // IRIS GL clear() clears to the current colour: IGLOO sends GE + // state addresses instead of floats (4, 5, 6, 7: the current + // colour's words; as floats these would be denormals). + tok::CLEAR_COLOR if d.len() >= 4 && d[..4].iter().all(|&w| w < 0x100) => { + let mut c = [0.0f32; 4]; + for (k, &a) in d[..4].iter().enumerate() { + c[k] = match a { + 4..=7 => self.color[(a - 4) as usize], + _ => 0.0, + }; + } + self.clear_color = c; + } + tok::CLEAR_COLOR => self.clear_color = args_f32(d), + tok::CLEAR_DEPTH => self.clear_depth = w0 & 0xFF_FFFF, + tok::CLEAR_STENCIL => self.clear_stencil = w0 & 0xFF, + tok::CLEAR => self.clear_color_buffer(sink), + tok::CLEAR_DEPTH_BUFFER => self.clear_depth_stencil(true, sink), + tok::CLEAR_STENCIL_BUFFER => self.clear_depth_stencil(false, sink), + tok::DEPTH_TEST => self.set_state(|g| g.depth_test = on, sink), + tok::DEPTH_FUNC => self.set_state(|g| g.depth_func = w0 & 7, sink), + tok::DEPTH_MASK => self.set_state(|g| g.depth_mask = on, sink), + tok::ALPHA_TEST => self.set_state(|g| g.alpha_test = on, sink), + tok::ALPHA_FUNC if d.len() >= 2 => self.set_state(|g| (g.alpha_func, g.alpha_ref) = (w0 & 7, d[1].min(0xFFF)), sink), + tok::BLEND => self.set_state(|g| g.blend = on, sink), + tok::BLEND_FUNC if d.len() >= 2 => self.set_state(|g| (g.blend_src, g.blend_dst) = (blend_code(w0), blend_code(d[1])), sink), + tok::LOGIC_OP_ON => self.set_state(|g| g.logic = on, sink), + tok::LOGIC_OP => self.set_state(|g| g.logic_op = w0 & 0xF, sink), + tok::STENCIL_TEST => self.set_state(|g| g.stencil = on, sink), + tok::STENCIL_FUNC if d.len() >= 3 => self.set_state(|g| (g.st_func, g.st_ref, g.st_cmask) = (w0 & 7, d[1] & 0xFF, d[2] & 0xFF), sink), + tok::STENCIL_OP if d.len() >= 3 => self.set_state(|g| g.st_ops = [w0 & 7, d[1] & 7, d[2] & 7], sink), + tok::STENCIL_MASK => self.set_state(|g| g.st_wmask = w0 & 0xFF, sink), + tok::COLOR_MASK => self.set_state(|g| g.color_mask = w0 & 0xF, sink), + tok::DITHER => self.dither = on, + tok::SCISSOR_TEST => self.scissor_test = on, + tok::SCISSOR if d.len() >= 4 => self.scissor = [d[0] as i32, d[1] as i32, d[2] as i32, d[3] as i32], + tok::CULL_ON => self.cull = 1, + tok::CULL_OFF => self.cull = 0, + tok::CULL_FACE => self.cull_mode = if d.is_empty() { 0x405 } else { w0 }, + tok::FRONT_FACE => self.front_ccw = (w0 != 0x900) as u32, + tok::POLYGON_MODE if d.len() >= 2 => { + let m = d[1]; + match w0 { + 0x404 => self.polymode[0] = m, + 0x405 => self.polymode[1] = m, + _ => self.polymode = [m; 2], + } + } + tok::LINE_WIDTH => self.line_width = f32::from_bits(w0).max(1.0), + tok::LINE_STIPPLE_ON => self.set_state(|g| g.line_stipple = on, sink), + tok::LINE_STIPPLE if d.len() >= 2 => self.set_state(|g| (g.line_repeat, g.line_pattern) = (w0 & 0xFF, d[1] & 0xFFFF), sink), + tok::POLYGON_STIPPLE_ON => self.set_state(|g| g.poly_stipple = on, sink), + tok::LOAD_POLYGON_STIPPLE if d.len() >= 32 => { + let mut rows = [0u32; 32]; + rows.copy_from_slice(&d[..32]); + self.set_state(|g| g.stipple_rows = rows, sink); + } + tok::FLUSH => self.end_raster(sink), + tok::DRAW_BUFFER => { + self.end_raster(sink); + self.draw_bits = w0; + } + tok::BEGIN_POINTS..=tok::BEGIN_POLYGON => { + self.prim = cmd - tok::BEGIN_POINTS; + self.nv = 0; + } + tok::END_POINTS..=tok::END_POLYGON => { + self.end_primitive(sink); + self.prim = prim::NONE; + self.end_raster(sink); + } + tok::VERTEX4F => self.vertex(args_f32(d), sink), + tok::INIT_RGB => self.ci = 0, + tok::INIT_CI => self.ci = 1, + tok::CLEAR_INDEX => self.clear_index = if w0 >> 16 == 0 { w0 } else { f32::from_bits(w0) as u32 }, + tok::RASTER_POS4F => { + let w = self.transform(args_f32(d)); + let h = w.h; + let inside = (0..3).all(|i| h[i].abs() <= h[3]); + self.raster = [w.x, w.y, w.z]; + self.raster_color = w.c; + self.raster_valid = (w.ok != 0 && inside) as u32; + } + tok::UPDATE_RASTER_POS => { + let a = args_f32(d); + self.raster[0] += a[0]; + self.raster[1] += a[1]; + } + tok::LOAD_RASTER_POS_INFO => { + self.color = self.raster_color; + } + tok::INDEX_MASK => self.set_state(|g| g.index_mask = w0 & 0xFFF, sink), + tok::READ_BUFFER => self.read_back = (d.get(2) == Some(&0x405)) as u32, + tok::SAVE_RSS => self.pixel_op = 1, + tok::READ_TEXTURE if d.len() >= 5 => { + self.begin_raster(sink); + sink.rss_write(re::XFRMODE, d[4], false); + sink.rss_write(re::XFRSIZE, d[1] << 16 | (d[0] & 0xFFFF), false); + sink.rss_write(re::XFRCONTROL, TE_READ_START, false); + self.tex_reading = 1; + } + // TRAM saved raw: the level the sampler points at (64 texels of + // 32 bits a row from its page), byte for byte. + tok::READ_TEXTURE_RAW if d.len() >= 3 => { + self.begin_raster(sink); + let size = self.te[te1::CONTEXT_REGS.iter().position(|&r| r == te_reg::TXSIZE).unwrap_or(0)]; + sink.rss_write(re::XFRMODE, d[1], false); + sink.rss_write(re::XFRSIZE, d[0] << 16 | 1 << (size & 0xF), false); + sink.rss_write(super::rss::reg::TE_RAW, 1, false); + sink.rss_write(re::XFRCONTROL, TE_READ_START, false); + self.tex_reading = 1; + } + // TRAM restored: the texels of the 0xDA8 rectangle come by host + // DMA as they were saved. + tok::WRITE_DMAGESETUP if d.len() >= 4 && d[3] == SEND_PIXELS_TEXTURE => { + // Rows of the transfer mode's texels (RGB 3 bytes, RGBA 4). + let (w, h) = (self.tl_rect[2], self.tl_rect[3]); + let words = (w * te1::Te1::read_texel_bytes(self.xfrmode)).div_ceil(4); + self.send_pixels = [words, 0, 0, h, 0, 1, SEND_PIXELS_TEXTURE, 0]; + self.send_pixels_pending = 1; + } + tok::RESTORE_RSS => { + if self.tex_reading != 0 { + self.tex_reading = 0; + sink.rss_write(re::XFRCONTROL, 0, false); + sink.rss_write(super::rss::reg::TE_RAW, 0, false); + } + self.pixel_op = 0; + self.end_raster(sink); + } + tok::GET_PIXELS => self.get_pixels(sink), + tok::PIXEL_STATE | tok::INIT_PIXEL_STATE_ERAM if d.len() >= 7 => { + let (addr, n) = (d[3], d[6] as usize); + for (k, &v) in d[7..].iter().take(n).enumerate() { + self.pixel_state(addr + k as u32, v); + } + } + tok::TEX_COORD => self.texcoord = args_f32(d), + tok::POINT_SIZE => self.point_size = f32::from_bits(w0), + tok::TEX_GENIV if d.len() >= 3 && d[0].wrapping_sub(0x2000) < 4 && d[1] == 0x2500 => { + self.texgen_mode[(d[0] - 0x2000) as usize] = match d[2] { + 0x2401 => 0, + 0x2402 => 2, + _ => 1, + }; + } + tok::TEX_GEN_PLANE_HEADER if d.len() >= 2 && d[0].wrapping_sub(0x2000) < 4 => { + self.texgen_plane = (d[0] - 0x2000) | ((d[1] == 0x2502) as u32) << 2; + } + tok::TEX_GEN_PLANE_DATA if d.len() >= 4 => { + let p = args_f32(d); + let i = (self.texgen_plane & 3) as usize; + if self.texgen_plane & 4 != 0 { + // Eye planes are kept in eye space: times the inverse + // of the modelview current now (OpenGL), as clip planes. + let inv = math::invert(self.mv.get()).unwrap_or(math::IDENT); + let col = |c: usize| (0..4).map(|r| p[r] * inv[c * 4 + r]).sum::(); + self.texgen_eye[i] = [col(0), col(1), col(2), col(3)]; + } else { + self.texgen_obj[i] = p; + } + } + tok::ENABLE_CAP | tok::DISABLE_CAP if w0.wrapping_sub(0xC60) < 4 => { + let bit = 1 << (w0 - 0xC60); + if cmd == tok::ENABLE_CAP { self.texgen_on |= bit } else { self.texgen_on &= !bit } + } + tok::TEX_ON | tok::TEX_OFF => { + self.tex_on = (cmd == tok::TEX_ON) as u32; + if self.raster_loaded != 0 { + sink.rss_write(te_reg::TEXMODE1, self.texmode1(), false); + } + } + tok::RSS_REG_SHADOW_IDX if d.len() >= 4 => { + self.te_write(d[2] & 0x3FF, d[3], sink); + } + tok::SEND_PIXELS if d.len() >= 8 => { + self.send_pixels.copy_from_slice(&d[..8]); + self.send_pixels_pending = 1; + } + tok::BITMAP if d.len() >= 8 => { + self.bitmap.copy_from_slice(&d[..8]); + self.bitmap_pending = 1; + } + _ => return false, + } + true + } + + /// Object coordinates to a vertex with clip, eye and window positions + /// (window relative, y up). + fn transform(&mut self, v: [f32; 4]) -> Wv { + let mvp = self.mvp(); + let h = math::xform(&mvp, v); + let e = math::xform(self.mv.get(), v); + let (mut c, mut cb) = (self.color, self.color); + if self.lt.on != 0 && self.unlit == 0 { + if self.nm_dirty != 0 { + self.nm_dirty = 0; + self.lt.normal_matrix = math::normal_matrix(self.mv.get()); + } + (c, cb) = self.lt.light_vertex(e, self.normal, self.color); + } + // With texturing, fog comes after the texture environment: the + // fog factor goes with the vertex instead (`fog_after_texture`). + let mut f = 1.0; + if self.lt.fog_on != 0 { + if self.fog_after_texture() { + let z = if e[3] != 0.0 { (e[2] / e[3]).abs() } else { e[2].abs() }; + f = self.lt.fog_factor(z); + } else { + c = self.lt.fog_color(e, c); + cb = self.lt.fog_color(e, cb); + } + } + let mut w = Wv { c, cb, h, e, f, ..Default::default() }; + if self.tex_on != 0 { + let tc = self.texgen(v, e); + w.t = math::xform(self.tex.get(), tc); + } + w.oc = self.clip.outcode(&w); + self.vp.project(w, [0.0, 0.0], 256.0) + } + + /// The vertex's texture coordinates before the texture matrix: the + /// current ones, with the generated ones where texgen is on (object + /// or eye linear: the plane times the position; sphere map: from the + /// reflection of the eye direction about the eye-space normal). + fn texgen(&mut self, obj: [f32; 4], eye: [f32; 4]) -> [f32; 4] { + let mut tc = self.texcoord; + if self.texgen_on == 0 { + return tc; + } + let dot = |p: &[f32; 4], v: [f32; 4]| p[0] * v[0] + p[1] * v[1] + p[2] * v[2] + p[3] * v[3]; + let mut sphere: Option<[f32; 2]> = None; + for i in 0..4 { + if self.texgen_on & (1 << i) == 0 { + continue; + } + tc[i] = match self.texgen_mode[i] { + 0 => dot(&self.texgen_obj[i], obj), + 2 if i < 2 => { + let st = *sphere.get_or_insert_with(|| { + if self.nm_dirty != 0 { + self.nm_dirty = 0; + self.lt.normal_matrix = math::normal_matrix(self.mv.get()); + } + let nm = &self.lt.normal_matrix; + let n0 = self.normal; + let n = [ + nm[0] * n0[0] + nm[3] * n0[1] + nm[6] * n0[2], + nm[1] * n0[0] + nm[4] * n0[1] + nm[7] * n0[2], + nm[2] * n0[0] + nm[5] * n0[1] + nm[8] * n0[2], + ]; + let len = |v: [f32; 3]| (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-20); + let ln = len(n); + let n = [n[0] / ln, n[1] / ln, n[2] / ln]; + let w = if eye[3] != 0.0 { eye[3] } else { 1.0 }; + let u = [eye[0] / w, eye[1] / w, eye[2] / w]; + let lu = len(u); + let u = [u[0] / lu, u[1] / lu, u[2] / lu]; + let nu = n[0] * u[0] + n[1] * u[1] + n[2] * u[2]; + let r = [u[0] - 2.0 * n[0] * nu, u[1] - 2.0 * n[1] * nu, u[2] - 2.0 * n[2] * nu]; + let m = 2.0 * (r[0] * r[0] + r[1] * r[1] + (r[2] + 1.0) * (r[2] + 1.0)).sqrt().max(1e-20); + [r[0] / m + 0.5, r[1] / m + 0.5] + }); + st[i] + } + 2 => tc[i], + _ => dot(&self.texgen_eye[i], eye), + }; + } + tc + } + + fn vertex(&mut self, obj: [f32; 4], sink: &mut dyn Hq3Sink) { + if self.prim == prim::NONE { + return; + } + let v = self.transform(obj); + let n = self.nv as usize; + self.nv += 1; + let mut poly = [Wv::default(); MAX_POLY]; + match self.prim { + prim::POINTS => self.point(v, sink), + prim::LINES => { + self.vb[n % 2] = v; + if n % 2 == 1 { + self.line(self.vb[0], v, true, sink); + } + } + prim::LSTRIP | prim::LLOOP => { + if n == 0 { + self.vb[0] = v; + } else { + self.line(self.vb[1 + (n - 1) % 2], v, n == 1, sink); + } + self.vb[1 + n % 2] = v; + } + prim::TRIANGLES => { + self.vb[n % 3] = v; + if n % 3 == 2 { + poly[..3].copy_from_slice(&self.vb[..3]); + self.polygon(&mut poly, 3, v, sink); + } + } + prim::TSTRIP => { + // vb[0] older, vb[1] newer. Every other triangle is + // reversed to keep the strip's winding; swaptmesh swaps the + // pair and flips it too. The newest vertex provokes. + if n == 0 { + self.tflip = 0; + } + if n >= 2 { + let (a, b) = (self.vb[0], self.vb[1]); + poly[..3].copy_from_slice(&if self.tflip == 0 { [a, b, v] } else { [b, a, v] }); + self.tflip ^= 1; + self.polygon(&mut poly, 3, v, sink); + } + self.vb[0] = self.vb[1]; + self.vb[1] = v; + } + prim::TFAN => { + if n == 0 { + self.vb[0] = v; + } else { + if n >= 2 { + poly[..3].copy_from_slice(&[self.vb[0], self.vb[1], v]); + self.polygon(&mut poly, 3, v, sink); + } + self.vb[1] = v; + } + } + prim::QUADS => { + self.vb[n % 4] = v; + if n % 4 == 3 { + poly[..4].copy_from_slice(&self.vb[..4]); + self.polygon(&mut poly, 4, v, sink); + } + } + prim::QSTRIP => { + self.vb[n % 4] = v; + if n >= 3 && n % 2 == 1 { + let q = |k: usize| self.vb[(n - k) % 4]; + poly[..4].copy_from_slice(&[q(3), q(2), q(0), q(1)]); + self.polygon(&mut poly, 4, v, sink); + } + } + prim::POLYGON => { + if n < MAX_POLY - 12 { + self.vb[n] = v; + } + } + _ => {} + } + } + + /// END: close line loops, draw polygons. + fn end_primitive(&mut self, sink: &mut dyn Hq3Sink) { + let n = self.nv as usize; + match self.prim { + prim::LLOOP if n >= 2 => { + let (last, first) = (self.vb[1 + (n - 1) % 2], self.vb[0]); + self.line(last, first, false, sink); + } + prim::POLYGON if n >= 3 => { + let n = n.min(MAX_POLY - 12); + let mut v = [Wv::default(); MAX_POLY]; + v[..n].copy_from_slice(&self.vb[..n]); + let prov = v[0]; + self.polygon(&mut v, n, prov, sink); + } + _ => {} + } + } + + /// Clip a convex polygon, cull it or draw it in its polygon mode (as + /// a fan of triangles, its edges, or its vertices). `prov` carries the + /// flat-shading colour. + fn polygon(&mut self, v: &mut [Wv; MAX_POLY], n: usize, prov: Wv, sink: &mut dyn Hq3Sink) { + let n = self.clip.clip_polygon(v, n); + // Vertices made by clipping carry a parent's window position: + // project them all again from their clip positions. + for w in v[..n].iter_mut() { + *w = self.vp.project(*w, [0.0, 0.0], 256.0); + } + if n < 3 || v[..n].iter().any(|w| w.ok == 0) { + 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]); + a.x * b.y - b.x * a.y + }).sum(); + let front = (area > 0.0) == (self.front_ccw != 0); + if self.cull != 0 { + let culled = match self.cull_mode { + 0x404 => front, + 0x408 => true, + _ => !front, + }; + if culled || area == 0.0 { + return; + } + } + // Two-sided lighting: back faces show the back colours. + let mut prov = prov; + if self.lt.on != 0 && self.lt.two_sided != 0 && !front { + for w in v[..n].iter_mut() { + w.c = w.cb; + } + prov.c = prov.cb; + } + let flat = (self.smooth == 0).then_some(prov.c); + if flat.is_some() { + for w in v[..n].iter_mut() { + w.c = prov.c; + } + } + match self.polymode[!front as usize] { + 0x1B00 => { + for w in v[..n].to_vec() { + self.point_window(w, sink); + } + return; + } + 0x1B01 => { + for i in 0..n { + self.line_window(v[i], v[(i + 1) % n], i == 0, sink); + } + return; + } + _ => {} + } + let n = self.scissor_polygon(v, n); + if n < 3 { + return; + } + self.begin_raster(sink); + self.fill_mode(if self.poly_stipple != 0 { FILL_POLY_STIPPLE } else { 0 }, sink); + for k in 1..n - 1 { + self.triangle(&v[0], &v[k], &v[k + 1], flat, sink); + } + } + + /// The scissor box as window-space half-planes, if the test is on: + /// x0, x1 + 1, y0, y1 + 1. + fn scissor_box(&self) -> Option<[f32; 4]> { + let [x0, x1, y0, y1] = self.scissor_rect(); + (self.scissor_test != 0).then_some([x0 as f32, (x1 + 1) as f32, y0 as f32, (y1 + 1) as f32]) + } + + /// The scissor box, inclusive window coordinates [x0, x1, y0, y1]. The + /// token carries [x, width - 1, y, height - 1] (traced: glScissor(30, + /// 40, 100, 50) sends 30, 99, 40, 49), kept as sent for the readback. + fn scissor_rect(&self) -> [i32; 4] { + let [x, w1, y, h1] = self.scissor; + [x, x + w1, y, y + h1] + } + + /// Clip a window-space polygon to the scissor box (colours and depth + /// interpolated linearly in window space). + fn scissor_polygon(&self, v: &mut [Wv; MAX_POLY], n: usize) -> usize { + let Some(b) = self.scissor_box() else { return n }; + let mut n = n; + let mut tmp = [Wv::default(); MAX_POLY]; + for edge in 0..4 { + let dist = |w: &Wv| match edge { + 0 => w.x - b[0], + 1 => b[1] - w.x, + 2 => w.y - b[2], + _ => b[3] - w.y, + }; + let mut m = 0; + for i in 0..n { + let (a, c) = (v[i], v[(i + 1) % n]); + let (da, dc) = (dist(&a), dist(&c)); + if da >= 0.0 && m < MAX_POLY { + tmp[m] = a; + m += 1; + } + if (da >= 0.0) != (dc >= 0.0) && m < MAX_POLY { + tmp[m] = lerp_window(&a, &c, da / (da - dc)); + m += 1; + } + } + v[..m].copy_from_slice(&tmp[..m]); + n = m; + if n == 0 { + break; + } + } + n + } + + /// A line from object-space vertices already transformed: clip, then + /// draw. `restart` restarts the stipple pattern (each GL_LINES segment, + /// the first segment of a strip or loop). + fn line(&mut self, a: Wv, b: Wv, restart: bool, sink: &mut dyn Hq3Sink) { + let Some((a, b)) = self.clip.clip_line(a, b) else { return }; + let (mut a, b) = (self.vp.project(a, [0.0, 0.0], 256.0), self.vp.project(b, [0.0, 0.0], 256.0)); + if a.ok == 0 || b.ok == 0 { + return; + } + // Flat lines take the segment's second vertex colour. + if self.smooth == 0 { + a.c = b.c; + } + self.line_window(a, b, restart, sink); + } + + /// A window-space line: scissor, then the RE4 GL line registers. + fn line_window(&mut self, a: Wv, b: Wv, restart: bool, sink: &mut dyn Hq3Sink) { + let (mut a, mut b) = (a, b); + if let Some(bx) = self.scissor_box() { + // Liang-Barsky against the box. + let (dx, dy) = (b.x - a.x, b.y - a.y); + let (mut t0, mut t1) = (0.0f32, 1.0f32); + for (p, q) in [(-dx, a.x - bx[0]), (dx, bx[1] - a.x), (-dy, a.y - bx[2]), (dy, bx[3] - a.y)] { + if p == 0.0 { + if q < 0.0 { + return; + } + } else { + let r = q / p; + if p < 0.0 { t0 = t0.max(r) } else { t1 = t1.min(r) } + } + } + if t0 > t1 { + return; + } + let (oa, ob) = (a, b); + a = lerp_window(&oa, &ob, t0); + b = lerp_window(&oa, &ob, t1); + } + self.begin_raster(sink); + self.fill_mode(if self.line_stipple != 0 { FILL_LINE_STIPPLE } else { 0 }, sink); + if restart { + sink.rss_write(re::LSCRL, self.line_repeat, false); + } + let len = (b.x - a.x).abs().max((b.y - a.y).abs()); + if len == 0.0 { + return; + } + let pos = |f: f32| (f + POS_BIAS).to_bits(); + sink.rss_write(re::GLINE_XSTARTF, pos(a.x), false); + sink.rss_write(re::GLINE_XSTARTF + 1, pos(a.y), false); + sink.rss_write(re::GLINE_XSTARTF + 2, pos(b.x), false); + sink.rss_write(re::GLINE_XSTARTF + 3, pos(b.y), false); + sink.rss_write(re::GLINECONFIG, (self.line_width.round() as u32).max(1) - 1, false); + let fixc = |c: f32| (c as f64 * 0xFF_F000 as f64) as i32 as u32; + for k in 0..4 { + sink.rss_write(re::RED + k as u32, fixc(a.c[k]), false); + sink.rss_write(re::DRE + 2 * k as u32, fixc((b.c[k] - a.c[k]) / len), false); + } + let z64 = |v: f64| (v * 4096.0) as i64 as u64; + let (z, dz) = (z64(a.z as f64), z64((b.z as f64 - a.z as f64) / len as f64)); + sink.rss_write(re::Z, (z >> 32) as u32, false); + sink.rss_write(re::Z + 1, z as u32, false); + sink.rss_write(re::Z + 4, (dz >> 32) as u32, false); + sink.rss_write(re::Z + 5, dz as u32, false); + if self.texturing() { + // S/W, T/W, Q/W at the start and their steps per pixel along + // the major axis (in the edge step registers). + let [qa, qb] = normalised_q([tex_q(&a), tex_q(&b)]); + for k in 0..3 { + let (r, dr) = [(te_reg::SW, te_reg::DSWE), (te_reg::TW, te_reg::DTWE), (te_reg::WI, te_reg::DWIE)][k]; + put64(sink, r, qa[k] * te1::ITER_ONE); + put64(sink, dr, (qb[k] - qa[k]) / len as f64 * te1::ITER_ONE); + } + } + self.fog_plane(a.f as f64, (b.f as f64 - a.f as f64) / len as f64, 0.0, sink); + sink.rss_write(re::IR, IR_GL_LINE, true); + } + + /// A point (size 1): the pixel containing it, as a one-pixel GL line. + fn point(&mut self, v: Wv, sink: &mut dyn Hq3Sink) { + if v.oc != 0 || v.ok == 0 { + return; + } + self.point_window(v, sink); + } + + /// Whether a glBitmap header waits for its rows. + pub fn wants_bitmap_rows(&self) -> bool { + self.bitmap_pending != 0 || self.send_pixels_pending != 0 + } + + /// Words a pending SEND_PIXELS image needs before it can be drawn or + /// loaded (words per row times rows, when the header says; 0 for + /// glBitmap, drawn as it comes). + pub fn pixel_words_needed(&self) -> usize { + if self.send_pixels_pending == 0 { + return 0; + } + let rows = (self.send_pixels[3] + 3 * self.send_pixels[4]) as usize; + (self.send_pixels[0] as usize * rows).min(1 << 22) + } + + /// glReadPixels: a DMA read block over the pixel block, in the read + /// buffer (the depth buffer for the depth format, transfer mode format + /// 2 / type 3, our reading); the host starts the transfer itself + /// (raster interface register 5, see hq3.rs). The raster state stays + /// loaded until RESTORE_RSS. + fn get_pixels(&mut self, sink: &mut dyn Hq3Sink) { + self.begin_raster(sink); + let depth = (self.xfrmode >> 4) & 0xF == 2 && self.xfrmode & 0xF == 3; + let drb = if depth { + super::rss::ZST_PAGE + } else { + let w = self.window.drb; + let (a, b) = (w & 0x3FF, (w >> 10) & 0x3FF); + (w & !0x3FF) | if (self.read_back != 0) != (self.swapped != 0) { b } else { a } + }; + sink.rss_write(re::DRBPOINTERS, drb, false); + self.fill_mode(FILL_DMA_READ, sink); + sink.rss_write(re::XFRMODE, self.xfrmode, false); + sink.rss_write(re::IR_ALIAS, IR_BLOCK_SETUP, false); + sink.rss_write(re::BLOCK_XYSTARTI, self.pixel_block[0], false); + sink.rss_write(re::BLOCK_XYENDI, self.pixel_block[1], true); + } + + /// glDrawPixels' rows (FIFO pixel data, `send_pixels[0]` words a row, + /// bottom row first) at the raster position, as the GE streams them: + /// a raster-engine DMA write block the size of the image, the host + /// format from the transfer mode (the driver sets `xfrmode` itself), so + /// the pixels go through the raster engine's format conversion, plane + /// masks and clipping. Pixel transfer stages (index shift/offset, maps, + /// zoom) are taken as identity. + fn draw_pixels(&mut self, words: &[u32], sink: &mut dyn Hq3Sink) { + self.send_pixels_pending = 0; + let wpr = self.send_pixels[0] as usize; + if self.raster_valid == 0 || wpr == 0 || words.len() < wpr { + return; + } + // Rows: header words 3 + 3 * 4 (mandel 1 + 0, snoop 2 + 3 * 18 = + // 56), else what came. + let hdr_rows = (self.send_pixels[3] + 3 * self.send_pixels[4]) as usize; + let rows = if hdr_rows > 0 { hdr_rows.min(words.len() / wpr) } else { words.len() / wpr }; + let bpp = super::rss::bytes_per_pixel(self.xfrmode) as usize; + // The host's pixels may carry fewer components than the raster + // engine's format: glCopyPixels draws RGB16 back in RGBA16 (the GE's + // pixel pipeline adds alpha). The image's size (pixel state 0xDA8's + // transfer size, when its rows match) gives the host's pixel size. + let xs = self.tl_rect[4]; + let host_bpp = match (xs & 0xFFFF) as usize { + w if w > 0 && (xs >> 16) as usize == rows && wpr * 4 % w == 0 => wpr * 4 / w, + _ => bpp, + }; + let comp = if self.xfrmode & 0xF == 1 { 2 } else { 1 }; + let words: std::borrow::Cow<[u32]> = if host_bpp != bpp && host_bpp % comp == 0 && host_bpp < bpp { + // Widen each pixel: its components, then ones (alpha). + let bytes: Vec = words.iter().flat_map(|w| w.to_be_bytes()).collect(); + let mut out: Vec = Vec::with_capacity(bytes.len() / host_bpp * bpp + 8); + for row in bytes.chunks(wpr * 4).take(rows) { + let mut line: Vec = Vec::new(); + for px in row.chunks_exact(host_bpp) { + line.extend_from_slice(px); + line.resize(line.len() + bpp - host_bpp, 0xFF); + } + line.resize(line.len().div_ceil(4) * 4, 0); + out.extend(line); + } + let wpr2 = out.len() / rows.max(1) / 4; + let ws: Vec = out.chunks(4).map(|c| u32::from_be_bytes([c[0], c[1], c[2], c[3]])).collect(); + return self.draw_pixels_words(&ws, wpr2, rows, bpp, sink); + } else { + std::borrow::Cow::Borrowed(words) + }; + self.draw_pixels_words(&words, wpr, rows, bpp, sink); + } + + /// glDrawPixels' rows, `wpr` words each, in the raster engine's format + /// (`bpp` bytes a pixel). + fn draw_pixels_words(&mut self, words: &[u32], wpr: usize, rows: usize, bpp: usize, sink: &mut dyn Hq3Sink) { + // Pixel zoom (same both ways, any factor): output pixel i takes + // source pixel floor(i / zoom), as OpenGL's zoomed rectangles do + // (the desks overview shrinks its miniatures, snoop magnifies 6x). + let zoom = if self.zoom_inv > 0.0 { (1.0 / self.zoom_inv).clamp(1.0 / 64.0, 64.0) } else { 1.0 }; + // A large image comes in chunks (glCopyPixels draws back 80 pixels + // wide): pixel state 0xDA8 holds the chunk's place in the image + // (x0, y0, x1, y1, transfer size), relative to the raster position. + let xs = self.tl_rect[4]; + let (ox, oy) = if xs != 0 && (xs >> 16) as usize == rows && (xs & 0xFFFF) as usize == wpr * 4 / bpp.max(1) { + (self.tl_rect[0] as i32 as f32 * zoom, self.tl_rect[1] as i32 as f32 * zoom) + } else { + (0.0, 0.0) + }; + let (x0, y0) = ((self.raster[0] + ox).floor() as i32, (self.raster[1] + oy).floor() as i32); + let src_w = wpr * 4 / bpp; + let out_w = ((src_w as f32 * zoom).round() as i32).max(1); + let out_h = ((rows as f32 * zoom).round() as i32).max(1); + // The scissor box (window coordinates, inclusive) cuts the + // rectangle, as it does every fragment. + let (mut cx0, mut cx1, mut cy0, mut cy1) = (x0, x0 + out_w - 1, y0, y0 + out_h - 1); + if self.scissor_test != 0 { + let [sx0, sx1, sy0, sy1] = self.scissor_rect(); + (cx0, cx1, cy0, cy1) = (cx0.max(sx0), cx1.min(sx1), cy0.max(sy0), cy1.min(sy1)); + } + if cx0 > cx1 || cy0 > cy1 { + return; + } + let (w, h) = (cx1 - cx0 + 1, cy1 - cy0 + 1); + self.begin_raster(sink); + if (self.xfrmode >> 4) & 0xF == 2 && self.xfrmode & 0xF == 3 { + // Depth pixels (glCopyPixels of depth draws them back). + sink.rss_write(re::DRBPOINTERS, super::rss::ZST_PAGE, false); + } + self.fill_mode(FILL_DMA_WRITE, sink); + sink.rss_write(re::XFRMODE, self.xfrmode, false); + sink.rss_write(re::XFRSIZE, (h as u32) << 16 | w as u32, false); + sink.rss_write(re::IR_ALIAS, IR_BLOCK_SETUP, false); + sink.rss_write(re::BLOCK_XYSTARTI, xy(cx0, cy0), false); + sink.rss_write(re::BLOCK_XYENDI, xy(cx1, cy1), true); + let src_px = |r: usize, c: usize| -> [u8; 8] { + let mut px = [0u8; 8]; + for (k, b) in px.iter_mut().enumerate().take(bpp) { + let i = r * wpr * 4 + c * bpp + k; + *b = words.get(i / 4).map_or(0, |w| (w >> (24 - 8 * (i % 4))) as u8); + } + px + }; + for (line, oy) in (cy0 - y0..=cy1 - y0).enumerate() { + let sr = ((oy as f32 / zoom) as usize).min(rows - 1); + let mut bytes = Vec::with_capacity(w as usize * bpp); + for ox in cx0 - x0..=cx1 - x0 { + let sc = ((ox as f32 / zoom) as usize).min(src_w - 1); + bytes.extend_from_slice(&src_px(sr, sc)[..bpp]); + } + sink.dma_write_line(line as u32, &bytes); + } + self.end_raster(sink); + } + + /// glBitmap's rows (FIFO pixel data words, big-endian bytes): rows of + /// the header's length in bits, padded to bytes, bottom row first, most + /// significant bit leftmost. Set bits are fragments of the raster + /// colour and depth at the raster position less the origin; the raster + /// position then moves. Each run of set bits goes through the GL + /// fragment path as a one-pixel-high rectangle. + pub fn bitmap_rows(&mut self, words: &[u32], sink: &mut dyn Hq3Sink) { + self.ensure_init(); + if self.send_pixels_pending != 0 { + if self.send_pixels[6] == SEND_PIXELS_TEXTURE { + self.tex_load(words, sink); + } else { + self.draw_pixels(words, sink); + } + return; + } + self.bitmap_pending = 0; + let h = self.bitmap; + let (bits, rows) = (h[1] as usize, h[2] as usize); + let f = |i: usize| f32::from_bits(h[i]); + let (xorig, yorig, xmove, ymove) = (f(3), f(4), f(5), f(6)); + if self.raster_valid == 0 { + return; + } + let x0 = (self.raster[0] - xorig).floor(); + let y0 = (self.raster[1] - yorig).floor(); + let stride = bits.div_ceil(8); + let byte = |i: usize| words.get(i / 4).map_or(0, |w| (w >> (24 - 8 * (i % 4))) as u8); + let mut v = Wv { z: self.raster[2], c: self.raster_color, cb: self.raster_color, ok: 1, ..Default::default() }; + for r in 0..rows { + let set = |c: usize| byte(r * stride + c / 8) >> (7 - c % 8) & 1 != 0; + let mut c = 0; + while c < bits { + if !set(c) { + c += 1; + continue; + } + let start = c; + while c < bits && set(c) { + c += 1; + } + let (xa, xb, ya) = (x0 + start as f32, x0 + c as f32, y0 + r as f32); + let mut quad = [Wv::default(); MAX_POLY]; + for (k, (x, y)) in [(xa, ya), (xb, ya), (xb, ya + 1.0), (xa, ya + 1.0)].into_iter().enumerate() { + v.x = x; + v.y = y; + quad[k] = v; + } + let n = self.scissor_polygon(&mut quad, 4); + if n < 3 { + continue; + } + self.begin_raster(sink); + self.fill_mode(0, sink); + for k in 1..n - 1 { + self.triangle(&quad[0], &quad[k], &quad[k + 1], Some(self.raster_color), sink); + } + } + } + self.raster[0] += xmove; + self.raster[1] += ymove; + } + + fn point_window(&mut self, v: Wv, sink: &mut dyn Hq3Sink) { + let size = self.point_size.round().max(1.0); + let (mut a, mut b) = (v, v); + if size <= 1.0 { + let x = v.x.floor(); + a.x = x + 0.25; + b.x = x + 0.75; + self.line_window(a, b, true, sink); + return; + } + // A square of size x size pixels, centred on the nearest pixel + // corner (even sizes) or on the pixel's centre (odd), as OpenGL + // places non-antialiased points: a horizontal line over those + // pixels, that wide. + let even = size as i32 % 2 == 0; + let (cx, cy) = if even { ((v.x + 0.5).floor(), (v.y + 0.5).floor()) } else { (v.x.floor() + 0.5, v.y.floor() + 0.5) }; + let left = cx - size / 2.0; + a.x = left + 0.25; + b.x = left + size - 0.25; + a.y = cy; + b.y = cy; + let width = std::mem::replace(&mut self.line_width, size); + self.line_window(a, b, true, sink); + self.line_width = width; + } + + /// The PP1 per-fragment registers for the current state (layouts in + /// rss.rs, provisional). + fn load_fragment_state(&mut self, sink: &mut dyn Hq3Sink) { + let zmask = if self.depth_mask != 0 { 0xFF_FFFF << 4 } else { 0 }; + sink.rss_write(re::ZMODE, if self.depth_test != 0 { zmask | 1 << 3 | self.depth_func } else { 0 }, false); + sink.rss_write(re::AFUNCMODE, if self.alpha_test != 0 { self.alpha_ref << 4 | 1 << 3 | self.alpha_func } else { 7 }, false); + let st = if self.stencil != 0 { + self.st_ref << 16 | self.st_ops[2] << 12 | self.st_ops[1] << 8 | self.st_ops[0] << 4 | 1 << 3 | self.st_func + } else { + 7 + }; + sink.rss_write(re::STENCILMODE, st, false); + sink.rss_write(re::STENCILMASK, self.st_wmask << 8 | self.st_cmask, false); + sink.rss_write(re::BLENDFACTOR, if self.blend != 0 { 1 << 8 | self.blend_dst << 4 | self.blend_src } else { 0 }, false); + let op = if self.logic != 0 { self.logic_op } else { 3 }; + sink.rss_write(re::PP1FILLMODE, (self.pp1_base() & !(0xF << 26)) | op << 26, false); + let cm = self.color_mask; + let rgb = if self.ci != 0 { self.index_mask & 0xFFF } else { (cm & 1) * 0xFF | (cm >> 1 & 1) * 0xFF00 | (cm >> 2 & 1) * 0xFF_0000 }; + sink.rss_write(re::COLORMASKLSBSA, rgb, false); + sink.rss_write(re::COLORMASKMSBS, (cm >> 3 & 1) * 0xFF, false); + sink.rss_write(re::LSPAT, self.line_pattern, false); + if self.poly_stipple != 0 { + for (i, row) in self.stipple_rows.iter().enumerate() { + sink.rss_write(re::DEVICE_ADDR, super::rss::POLY_STIPPLE_RAM + i as u32, false); + sink.rss_write(re::DEVICE_DATA, *row, false); + } + } + } + + /// Fragment state changed: a batch already loaded picks it up now. + fn set_state(&mut self, f: impl FnOnce(&mut Self), sink: &mut dyn Hq3Sink) { + f(self); + if self.raster_loaded != 0 { + self.load_fragment_state(sink); + } + } + + /// The fill mode for the primitives that follow, written when it + /// changes within a batch. + fn fill_mode(&mut self, fm: u32, sink: &mut dyn Hq3Sink) { + if self.fill_loaded != fm { + self.fill_loaded = fm; + sink.rss_write(re::FILLMODE, fm, false); + } + } + + /// SCHEDULE_SWAP reached the GE: front and back trade places for what + /// follows (the display flips at the next retrace, by the kernel's + /// BUF_SELECT write). + pub fn swap_buffers(&mut self, sink: &mut dyn Hq3Sink) { + self.ensure_init(); + self.end_raster(sink); + self.swapped ^= 1; + } + + /// The DRBpointers value for the colour buffer drawn into: the window's + /// pointers with bits 9:0 set to the front or back buffer's page (A in + /// bits 9:0, B in 19:10 of the window's value). Front and back both: + /// the back buffer (not drawn twice yet). + fn draw_pointers(&self) -> u32 { + let drb = self.window.drb; + let (a, b) = (drb & 0x3FF, (drb >> 10) & 0x3FF); + let back = self.draw_bits & 0xC != 0; + let page = if back != (self.swapped != 0) { b } else { a }; + (drb & !0x3FF) | page + } + + /// Load the GL window's raster state for a batch of GL primitives (the + /// RSS saves the X server's first and turns Y-flip off). + pub fn begin_raster(&mut self, sink: &mut dyn Hq3Sink) { + if self.raster_loaded != 0 { + return; + } + self.raster_loaded = 1; + sink.gl_bracket(true); + let w = self.window; + if w.valid != 0 { + sink.rss_write(re::XYWIN, w.origin, false); + for (n, [x, y]) in w.masks.iter().enumerate() { + sink.rss_write(re::SCRMSK1X + 2 * n as u32, *x, false); + sink.rss_write(re::SCRMSK1X + 2 * n as u32 + 1, *y, false); + } + sink.rss_write(re::WINMODE, w.mode, false); + sink.rss_write(re::DRBPOINTERS, self.draw_pointers(), false); + } + self.fill_loaded = 0; + sink.rss_write(re::FILLMODE, 0, false); + self.load_fragment_state(sink); + self.load_te(sink); + } + + /// Put the X server's raster state back after a GL batch. + pub fn end_raster(&mut self, sink: &mut dyn Hq3Sink) { + if self.raster_loaded == 0 { + return; + } + self.raster_loaded = 0; + sink.gl_bracket(false); + } + + /// A fast-fill block over the window's first screen mask, cut to the + /// scissor box (window coordinates), in `color` (12-bit components: + /// red, green, blue, alpha) through plane masks `lsb` / `msb`, into + /// buffer page `drb`. The fragment tests are off for clears. + fn clear_block(&mut self, color: [u32; 4], lsb: u32, msb: u32, drb: u32, sink: &mut dyn Hq3Sink) { + let w = self.window; + if w.valid == 0 { + sink.note("GL clear before the kernel gave the window".into()); + return; + } + let (ox, oy) = ((w.origin & 0xFFFF) as i32, (w.origin >> 16) as i32); + let [mx, my] = w.masks[0]; + let (mut x0, mut x1) = ((mx >> 16) as i32 - ox, (mx & 0xFFFF) as i32 - ox); + let (mut y0, mut y1) = ((my >> 16) as i32 - oy, (my & 0xFFFF) as i32 - oy); + if self.scissor_test != 0 { + let [sx0, sx1, sy0, sy1] = self.scissor_rect(); + (x0, x1, y0, y1) = (x0.max(sx0), x1.min(sx1), y0.max(sy0), y1.min(sy1)); + if x0 > x1 || y0 > y1 { + return; + } + } + self.begin_raster(sink); + sink.rss_write(re::ZMODE, 0, false); + sink.rss_write(re::AFUNCMODE, 7, false); + sink.rss_write(re::STENCILMODE, 7, false); + sink.rss_write(re::BLENDFACTOR, 0, false); + sink.rss_write(re::PP1FILLMODE, self.pp1_base(), false); + sink.rss_write(re::DRBPOINTERS, drb, false); + sink.rss_write(re::COLORMASKLSBSA, lsb, false); + sink.rss_write(re::COLORMASKMSBS, msb, false); + self.fill_mode(FILL_FAST, sink); + for (k, c) in color.iter().enumerate() { + sink.rss_write(re::FILL_COLOR_R + k as u32, *c, false); + } + sink.rss_write(re::IR_ALIAS, IR_BLOCK_SETUP, false); + sink.rss_write(re::BLOCK_XYSTARTI, xy(x0, y0), false); + sink.rss_write(re::BLOCK_XYENDI, xy(x1, y1), true); + sink.rss_write(re::DRBPOINTERS, self.draw_pointers(), false); + self.load_fragment_state(sink); + self.end_raster(sink); + } + + /// The PP1 fill mode GL drawing uses: 24-bit RGB, or 12-bit colour + /// index (pixel type 6) in a colour-index context. + fn pp1_base(&self) -> u32 { + if self.ci != 0 { (PP1_RGB24_BUFFER_A & !0x700) | 0x600 } else { PP1_RGB24_BUFFER_A } + } + + /// glClear's colour part: the clear colour through the colour mask. + fn clear_color_buffer(&mut self, sink: &mut dyn Hq3Sink) { + if self.ci != 0 { + let drb = self.draw_pointers(); + self.clear_block([self.clear_index & 0xFFF, 0, 0, 0], self.index_mask & 0xFFF, 0, drb, sink); + return; + } + let q = |c: f32| ((c.clamp(0.0, 1.0) * 255.0).round() as u32) << 4; + let c = self.clear_color; + let cm = self.color_mask; + let lsb = (cm & 1) * 0xFF | (cm >> 1 & 1) * 0xFF00 | (cm >> 2 & 1) * 0xFF_0000; + let drb = self.draw_pointers(); + self.clear_block([q(c[0]), q(c[1]), q(c[2]), q(c[3])], lsb, (cm >> 3 & 1) * 0xFF, drb, sink); + } + + /// glClear's depth (24-bit Z, planes 23:0) or stencil (planes 31:24, + /// through the stencil write mask) part, in the ZST buffer. + fn clear_depth_stencil(&mut self, depth: bool, sink: &mut dyn Hq3Sink) { + let zst = super::rss::ZST_PAGE; + if depth { + if self.depth_mask == 0 { + return; + } + let z = self.clear_depth; + let c = [(z & 0xFF) << 4, ((z >> 8) & 0xFF) << 4, ((z >> 16) & 0xFF) << 4, 0]; + self.clear_block(c, 0xFF_FFFF, 0, zst, sink); + } else { + let s = self.clear_stencil << 4; + let m = self.st_wmask; + self.clear_block([0, 0, 0, s], 0, m, zst, sink); + } + } + + fn triangle(&mut self, p: &Wv, q: &Wv, r: &Wv, flat: Option<[f32; 4]>, sink: &mut dyn Hq3Sink) { + let mut v = [p, q, r]; + v.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal)); + let (cw, bw, aw) = (v[0], v[1], v[2]); + let major_y = cw.y - aw.y; + if major_y <= 0.0 { + return; + } + self.stats_triangles += 1; + let major_x = aw.x - cw.x; + let minor_y = cw.y - bw.y; + let dxdy0 = major_x / major_y; + let perc = minor_y / major_y; + let dxdy1 = if minor_y != 0.0 { (bw.x - cw.x) / minor_y } else { 0.0 }; + let dxdy2 = if bw.y - aw.y != 0.0 { (aw.x - bw.x) / (bw.y - aw.y) } else { 0.0 }; + let (ymax, ymid) = (cw.y.floor(), bw.y.floor()); + let xmid = cw.x + perc * major_x; + let ltor = xmid <= bw.x; + let span_x = bw.x - xmid; + let dy = cw.y - ymax; + let x0 = cw.x + dxdy0 * dy; + let x1 = cw.x + dxdy1 * dy; + let x2 = bw.x + dxdy2 * (bw.y - ymid); + let pos = |f: f32| (f + POS_BIAS).to_bits(); + sink.rss_write(re::TRI_YMID, pos(bw.y), false); + sink.rss_write(re::TRI_YMID + 1, pos(aw.y), false); + for (reg, s) in [(re::TRI_DXDY0, dxdy0), (re::TRI_DXDY1, dxdy1), (re::TRI_DXDY2, dxdy2)] { + let f = (s as f64 * (1u64 << 24) as f64) as i64 as u64; + sink.rss_write(reg, (f >> 32) as u32, false); + sink.rss_write(reg + 1, f as u32, false); + } + sink.rss_write(re::TRI_X0, pos(x0), false); + sink.rss_write(re::TRI_X0 + 1, pos(x1), false); + sink.rss_write(re::TRI_X2, pos(x2), false); + sink.rss_write(re::TRI_X2 + 1, pos(cw.y), false); + // Colour: start value at the first pixel, steps along the span and + // down the major edge. + let dx_x = (if ltor { x0.ceil() } else { x0.floor() }) - cw.x; + let n1 = dxdy0.trunc(); + let fixc = |c: f32| (c as f64 * 0xFF_F000 as f64) as i32 as u32; + for k in 0..4 { + let (start, dre, drx) = match flat { + Some(c) => (c[k], 0.0, 0.0), + None => { + let (a, b, c) = (aw.c[k], bw.c[k], cw.c[k]); + let mid = c + perc * (a - c); + let drx = if span_x != 0.0 { (b - mid) / span_x } else { 0.0 }; + let drdy = (a - c - drx * major_x) / major_y; + let start = c + drdy * dy + drx * dx_x; + let mut dre = drdy + n1 * drx; + let drx_out = if ltor { drx } else { -drx }; + if ltor ^ (dxdy0 >= 0.0) { + dre -= drx_out; + } + (start, dre, drx_out) + } + }; + sink.rss_write(re::RED + k as u32, fixc(start), false); + sink.rss_write(re::DRE + 2 * k as u32, fixc(dre), false); + sink.rss_write(re::DRE + 2 * k as u32 + 1, fixc(drx), false); + } + if self.depth_test != 0 { + // Z: the same plane setup, 64-bit, value * 0x1000. + let (a, b, c) = (aw.z as f64, bw.z as f64, cw.z as f64); + let (major_x, major_y) = (major_x as f64, major_y as f64); + let mid = c + perc as f64 * (a - c); + let dzx = if span_x != 0.0 { (b - mid) / span_x as f64 } else { 0.0 }; + let dzdy = (a - c - dzx * major_x) / major_y; + let z = c + dzdy * dy as f64 + dzx * dx_x as f64; + let mut dze = dzdy + n1 as f64 * dzx; + let dzx_out = if ltor { dzx } else { -dzx }; + if ltor ^ (dxdy0 >= 0.0) { + dze -= dzx_out; + } + for (k, val) in [z, dzx_out, dze].into_iter().enumerate() { + let f = (val * 4096.0) as i64 as u64; + sink.rss_write(re::Z + 2 * k as u32, (f >> 32) as u32, false); + sink.rss_write(re::Z + 2 * k as u32 + 1, f as u32, false); + } + } + if self.fog_after_texture() { + let (a, b, c) = (aw.f as f64, bw.f as f64, cw.f as f64); + let mid = c + perc as f64 * (a - c); + let dx = if span_x != 0.0 { (b - mid) / span_x as f64 } else { 0.0 }; + let ddown = (a - c - dx * major_x as f64) / major_y as f64; + let start = c + ddown * dy as f64 + dx * dx_x as f64; + let mut de = ddown + n1 as f64 * dx; + let dx_out = if ltor { dx } else { -dx }; + if ltor ^ (dxdy0 >= 0.0) { + de -= dx_out; + } + self.fog_plane(start, dx_out, de, sink); + } else { + self.fog_plane(1.0, 0.0, 0.0, sink); + } + if self.texturing() { + // S/W, T/W, Q/W: perspective-correct texture coordinates are + // linear in window space divided by clip w. Each gets the same + // plane setup as Z, at 2^32 (te1::ITER_ONE), plus d/dy. + let [qa, qb, qc] = normalised_q([tex_q(aw), tex_q(bw), tex_q(cw)]); + let (major_x, major_y) = (major_x as f64, major_y as f64); + let mut planes = [[0.0f64; 4]; 3]; + for (k, pl) in planes.iter_mut().enumerate() { + let (a, b, c) = (qa[k], qb[k], qc[k]); + let mid = c + perc as f64 * (a - c); + let dx = if span_x != 0.0 { (b - mid) / span_x as f64 } else { 0.0 }; + let ddown = (a - c - dx * major_x) / major_y; + let start = c + ddown * dy as f64 + dx * dx_x as f64; + let mut de = ddown + n1 as f64 * dx; + let dx_out = if ltor { dx } else { -dx }; + if ltor ^ (dxdy0 >= 0.0) { + de -= dx_out; + } + *pl = [start, dx_out, de, -ddown]; + } + let put = |r: u32, v: f64, sink: &mut dyn Hq3Sink| put64(sink, r, v * te1::ITER_ONE); + let [s, t, w] = planes; + for (r, v) in [ + (te_reg::SW, s[0]), (te_reg::TW, t[0]), (te_reg::WI, w[0]), + (te_reg::DWIE, w[2]), (te_reg::DWIX, w[1]), (te_reg::DWIY, w[3]), + (te_reg::DSWE, s[2]), (te_reg::DTWE, t[2]), (te_reg::DSWX, s[1]), + (te_reg::DTWX, t[1]), (te_reg::DSWY, s[3]), (te_reg::DTWY, t[3]), + ] { + put(r, v, sink); + } + } + sink.rss_write(re::IR, if ltor { IR_AREA_LTOR } else { IR_AREA_RTOL }, true); + } + + /// Texturing is on for the primitives that follow (GE on, TE enabled). + fn texturing(&self) -> bool { + self.tex_on != 0 && self.texmode1() & te1::TEXMODE1_ENABLE != 0 + } + + /// Fog is applied per fragment, after the texture environment (OpenGL's + /// order), rather than to vertex colours. + fn fog_after_texture(&self) -> bool { + self.lt.fog_on != 0 && self.texturing() + } + + /// The per-fragment fog of the next primitive (model-private registers, + /// `rss::reg::FOG_*`): the fog colour, the fog factor's plane (start, + /// step along the span, step down the edge; along a line, its step per + /// pixel), or off. + fn fog_plane(&mut self, start: f64, dx: f64, de: f64, sink: &mut dyn Hq3Sink) { + use super::rss::reg as rr; + if !self.fog_after_texture() { + sink.rss_write(rr::FOG_ON, 0, false); + return; + } + let c = self.lt.fog_color.map(|v| (v.clamp(0.0, 1.0) * 4095.0).round() as u32); + sink.rss_write(rr::FOG_RG, c[1] << 12 | c[0], false); + sink.rss_write(rr::FOG_B, c[2], false); + put64(sink, rr::FOG_F, start * te1::ITER_ONE); + put64(sink, rr::FOG_F + 2, dx * te1::ITER_ONE); + put64(sink, rr::FOG_F + 4, de * te1::ITER_ONE); + sink.rss_write(rr::FOG_ON, 1, false); + } + + /// A GE pixel state word (PIXEL_STATE / INIT_PIXEL_STATE_ERAM) at + /// `addr`: the pixel zoom and the texture loader's blocks are kept. + fn pixel_state(&mut self, addr: u32, v: u32) { + match addr { + 0x192 => self.zoom_inv = f32::from_bits(v), + 0xDA8..=0xDAC => self.tl_rect[(addr - 0xDA8) as usize] = v, + 0xDB1..=0xDB6 => self.tl_dest[(addr - 0xDB1) as usize] = v, + 0xD0A => self.tl_scale = v, + 0xDBB => self.tl_elem = v, + _ => {} + } + } + + /// TEXMODE1 as the TE gets it: the shadow's, with texturing off while + /// the GE has it off. + fn texmode1(&self) -> u32 { + let m = self.te[0]; + if self.tex_on != 0 { m } else { m & !te1::TEXMODE1_ENABLE } + } + + /// A TE register from the driver (register lists, RSS_REG_SHADOW_IDX): + /// into the context's shadow, and through to the TE while a batch has + /// it loaded. False for other registers. + pub fn te_write(&mut self, r: u32, v: u32, sink: &mut dyn Hq3Sink) -> bool { + self.ensure_init(); + if r == te_reg::TXADDR { + self.te_index = v; + } else if let Some(i) = te1::CONTEXT_REGS.iter().position(|&x| x == r) { + self.te[i] = v; + } else if let Some(k) = te1::TABLE_REGS.iter().position(|&x| x == r) { + self.te_tables[k][self.te_index as usize % te1::TABLE_LEN] = v; + self.te_index = self.te_index.wrapping_add(1); + } else { + return false; + } + if self.raster_loaded != 0 { + sink.rss_write(r, if r == te_reg::TEXMODE1 { self.texmode1() } else { v }, false); + } else { + self.te_dirty = 1; + } + true + } + + /// Load the TE from the context's shadow (all of it when it changed or + /// another context may have used the TE, else TEXMODE1 alone). + fn load_te(&mut self, sink: &mut dyn Hq3Sink) { + if self.te_dirty == 0 { + sink.rss_write(te_reg::TEXMODE1, self.texmode1(), false); + return; + } + self.te_dirty = 0; + for (i, &r) in te1::CONTEXT_REGS.iter().enumerate() { + sink.rss_write(r, if i == 0 { self.texmode1() } else { self.te[i] }, false); + } + for (k, &r) in te1::TABLE_REGS.iter().enumerate() { + sink.rss_write(te_reg::TXADDR, 0, false); + for &v in &self.te_tables[k] { + sink.rss_write(r, v, false); + } + } + sink.rss_write(te_reg::TXADDR, self.te_index, false); + } + + /// A texture image (SEND_PIXELS routine 0x511A): the texels to the + /// texture loader. The GE pixel state says which sub-image (0xDA8: s + /// and t origin, signed, -1 for a border; width, height) and which TRAM + /// page (0xDB1); the context's TL_MODE / TL_SPEC (in the TE once the + /// batch is loaded) say level size, components and depth. Large images + /// come as 64-texel-wide strips (a TRAM page's width). Rows are + /// SEND_PIXELS' words-per-row apart when it sends a row per texel row, + /// else packed; host components are 8 or 16 bits (the pixel format's + /// scale, 0xD0A) or floats, and become the internal format's + /// (luminance replicated, alpha picked out, missing alpha 1). + fn tex_load(&mut self, words: &[u32], sink: &mut dyn Hq3Sink) { + self.send_pixels_pending = 0; + let [s0, t0, w, h, _] = self.tl_rect; + let (w, h) = (w as usize, h as usize); + if w == 0 || h == 0 || w > 4096 || h > 4096 { + return; + } + let bytes: Vec = words.iter().flat_map(|w| w.to_be_bytes()).collect(); + let wpr = self.send_pixels[0] as usize * 4; + let hdr_rows = (self.send_pixels[3] + 3 * self.send_pixels[4]) as usize; + let (pitch, bpt) = if hdr_rows == h && wpr >= w { (wpr, wpr / w) } else { (0, bytes.len() / (w * h)) }; + let pitch = if pitch == 0 { w * bpt } else { pitch }; + let scale = f32::from_bits(self.tl_scale); + // GL_UNSIGNED_SHORT_4_4_4_4_EXT: 16-bit elements with an 8-bit + // scale. Widen each nibble to a byte (RGBA, R in the top nibble). + // A restore of saved TRAM (no destination level: the loader + // registers are the driver's own) is copied as it comes. + let raw = self.tl_dest[0] == 0; + let packed4 = !raw && self.tl_elem == 3 && bpt == 2 && scale > 1.0 / 1000.0 && scale < 0.5; + let (bytes, pitch, bpt) = if packed4 { + let mut out = Vec::with_capacity(w * h * 4); + for row in 0..h { + for col in 0..w { + let i = row * pitch + col * 2; + let v = bytes.get(i..i + 2).map_or(0, |b| u16::from_be_bytes([b[0], b[1]])); + out.extend((0..4).map(|k| ((v >> (12 - 4 * k)) & 0xF) as u8 * 0x11)); + } + } + (out, w * 4, 4) + } else { + (bytes, pitch, bpt) + }; + let (comp, float) = if raw { + (1, false) + } else if scale >= 0.5 { + (4, true) + } else if scale > 0.0 && scale < 1.0 / 1000.0 { + (if scale < 1.0 / 100_000.0 { 4 } else { 2 }, false) + } else { + (1, false) + }; + let n_ext = (bpt / comp).clamp(1, 4); + let tl_mode = self.te[te1::CONTEXT_REGS.iter().position(|&r| r == te_reg::TL_MODE).unwrap_or(0)]; + let nc = ((tl_mode >> 1) & 3) as usize + 1; + let alpha_class = !raw && (self.te[0] >> 9) & 7 == 1; + if sink.tracing_tex() { + let spec = self.te[te1::CONTEXT_REGS.iter().position(|&r| r == te_reg::TL_SPEC).unwrap_or(0)]; + sink.trace(format!( + "TEX load rect {:?} dest {:?} scale {scale} elem {} hdr {:x?} bytes {} pitch {pitch} bpt {bpt} n_ext {n_ext} \ + tl_mode {tl_mode:#x} tl_spec {spec:#x} texmode1 {:#x} tl_mipmap {:#x}", + self.tl_rect.map(|v| v as i32), self.tl_dest, self.tl_elem, &self.send_pixels[..6], bytes.len(), self.te[0], + self.te[te1::CONTEXT_REGS.iter().position(|&r| r == te_reg::TL_MIPMAP).unwrap_or(0)] + )); + } + self.begin_raster(sink); + if !raw { + sink.rss_write(te_reg::TL_MIPMAP, self.tl_dest[3], false); + sink.rss_write(te_reg::TL_BORDER, self.tl_dest[4], false); + } + sink.rss_write(te_reg::TL_ADDR, 0, false); + sink.rss_write(te_reg::TL_S_SIZE, w as u32, false); + sink.rss_write(te_reg::TL_T_SIZE, h as u32, false); + sink.rss_write(te_reg::TL_S_LEFT, s0, false); + sink.rss_write(te_reg::TL_T_BOTTOM, t0, false); + sink.rss_write(re::XFRSIZE, (h as u32) << 16 | w as u32, false); + sink.rss_write(re::XFRCONTROL, TE_LOAD_START, false); + for row in 0..h { + let mut line = Vec::with_capacity(w * 4); + for col in 0..w { + let i = row * pitch + col * bpt; + // Component k's top 8 bits (big-endian), or a float's. + let src = |k: usize| { + let at = i + k * comp; + if float { + let b = bytes.get(at..at + 4).map_or(0, |b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])); + (f32::from_bits(b).clamp(0.0, 1.0) * 255.0).round() as u8 + } else { + bytes.get(at).copied().unwrap_or(0) + } + }; + let (lum, alpha) = (src(0), if n_ext == 2 || n_ext == 4 { src(n_ext - 1) } else { 0xFF }); + let px = match (n_ext, nc) { + _ if raw => [src(0), src(1), src(2), src(3)], + _ if alpha_class => [alpha, 0, 0, 0], + (n, c) if n == c => [src(0), src(1), src(2), src(3)], + (1 | 2, 4) => [lum, lum, lum, alpha], + (1 | 2, 3) => [lum, lum, lum, 0], + (_, 2) => [lum, alpha, 0, 0], + (3, 4) => [src(0), src(1), src(2), 0xFF], + _ => [src(0), src(1), src(2), src(3)], + }; + line.extend_from_slice(&px); + } + sink.dma_write_line(row as u32, &line); + } + sink.rss_write(re::XFRCONTROL, 0, false); + } +} + +/// A 64-bit register pair (high word first), value rounded toward zero. +fn put64(sink: &mut dyn Hq3Sink, r: u32, v: f64) { + let f = v as i64 as u64; + sink.rss_write(r, (f >> 32) as u32, false); + sink.rss_write(r + 1, f as u32, false); +} + +/// A vertex's S/W, T/W, Q/W (texture coordinates over clip w, linear in +/// window space). +fn tex_q(v: &Wv) -> [f64; 3] { + let wi = if v.h[3] != 0.0 { 1.0 / v.h[3] as f64 } else { 0.0 }; + [v.t[0] as f64 * wi, v.t[1] as f64 * wi, v.t[3] as f64 * wi] +} + +/// A primitive's S/W, T/W, Q/W scaled by one factor so the largest Q/W is +/// 1. The TE divides by Q/W, so a common scale changes neither the +/// coordinates nor their derivatives; what it changes is precision in the +/// fixed-point planes. Far geometry has tiny 1/W (blast's nebula at W ~ +/// 5e5: 1/W ~ 2e-6, its steps per pixel one or two LSBs at 2^32), and +/// each triangle of a clipped polygon then mapped the texture differently. +/// The IDE's planes (1/W = 1.0 at 2^20) suggest the GE normalises too. +fn normalised_q(q: [[f64; 3]; N]) -> [[f64; 3]; N] { + let m = q.iter().map(|v| v[2].abs()).fold(0.0, f64::max); + if m == 0.0 || !m.is_finite() { + return q; + } + q.map(|v| v.map(|c| c / m)) +} + +/// x | y << 16 for the RE4's integer xy registers. +fn xy(x: i32, y: i32) -> u32 { + (x as u32 & 0xFFFF) << 16 | (y as u32 & 0xFFFF) +} + +/// The point at `t` along window-space a-b (position, depth, colours). +fn lerp_window(a: &Wv, b: &Wv, t: f32) -> Wv { + let l = |p: f32, q: f32| p + (q - p) * t; + let mut v = *a; + v.x = l(a.x, b.x); + v.y = l(a.y, b.y); + v.z = l(a.z, b.z); + for k in 0..4 { + v.c[k] = l(a.c[k], b.c[k]); + v.cb[k] = l(a.cb[k], b.cb[k]); + v.t[k] = l(a.t[k], b.t[k]); + } + v.f = l(a.f, b.f); + v +} + +/// A GL blend factor enum as the PP1 code (see rss.rs). +fn blend_code(e: u32) -> u32 { + match e { + 0 => 0, + 1 => 1, + 0x300..=0x308 => e - 0x300 + 2, + _ => 1, + } +} + +/// A token's float arguments: missing ones read as GL's defaults for +/// vertices and colours (0, 0, 0, 1). +fn args_f32(d: &[u32]) -> [f32; 4] { + let mut v = [0.0, 0.0, 0.0, 1.0]; + for (o, w) in v.iter_mut().zip(d) { + *o = f32::from_bits(*w); + } + v +} + +/// `n` arguments of a *D token: doubles (high word first) when there are +/// two words each, floats otherwise. +fn args(d: &[u32], n: usize) -> Vec { + let mut v: Vec = if d.len() >= 2 * n { + d.chunks(2).take(n).map(|p| f64::from_bits((p[0] as u64) << 32 | p[1] as u64) as f32).collect() + } else { + d.iter().take(n).map(|&w| f32::from_bits(w)).collect() + }; + v.resize(n.max(v.len()), 0.0); + v +} + +/// The matrix stacks behind one interface (they differ only in depth). +trait MatrixStack { + fn load(&mut self, m: Mat4); + fn mult(&mut self, m: &Mat4); + fn push(&mut self) -> bool; + fn pop(&mut self) -> bool; +} + +impl MatrixStack for Stack { + fn load(&mut self, m: Mat4) { + Stack::load(self, m) + } + fn mult(&mut self, m: &Mat4) { + Stack::mult(self, m) + } + fn push(&mut self) -> bool { + Stack::push(self) + } + fn pop(&mut self) -> bool { + Stack::pop(self) + } +} diff --git a/src/dev/mgras/hq3.rs b/src/dev/mgras/hq3.rs new file mode 100644 index 00000000..cd3e14be --- /dev/null +++ b/src/dev/mgras/hq3.rs @@ -0,0 +1,1449 @@ +//! HQ3: the host interface and command processor. +//! +//! `Hq3Regs` is the CPU-side register state (microcode RAM, flags and +//! interrupt enables). `Hq3Engine` is the frontend: it parses the command +//! FIFO word stream, runs the command-processor tokens this model handles, +//! the host DMA engine and the formatter, and turns raster commands into +//! raster register writes through an `Hq3Sink`. +//! +//! Drawing arrives through the command FIFO as (command, data) pairs. +//! Commands at `0x1000` and up write raster registers directly, with bit +//! `0x400` meaning "execute"; lower numbers go to the command processor's +//! microcode, which this model does not run (the PROM, the kernel's console +//! and the X server's 2D paths never need it). + +use std::sync::Arc; + +use super::plain::RegMap; + +use crate::traits::BusDevice; + +/// Host interface register offsets. +pub mod host { + pub const UCODE: u32 = 0x40000; + pub const UCODE_END: u32 = 0x46000; + pub const SET_FLAGS_PRIVILEGED: u32 = 0x50008; + pub const CLEAR_FLAGS_PRIVILEGED: u32 = 0x5000C; + pub const CFIFO_PRIVILEGED: u32 = 0x50080; + pub const STATUS: u32 = 0x70000; + pub const FIFOSTATUS: u32 = 0x70004; + pub const SET_FLAGS: u32 = 0x70008; + pub const CLEAR_FLAGS: u32 = 0x7000C; + pub const GE_READBACK_HI: u32 = 0x70010; + pub const GE_READBACK_LO: u32 = 0x70014; + pub const CFIFO: u32 = 0x70080; + /// libGLcore's immediate-mode path stores command words here (HQ3.h + /// HQ3_CFIFO_GL); taken as another user FIFO port. + pub const CFIFO_GL: u32 = 0x74080; + pub const GIOSTATUS: u32 = 0x70100; + pub const DMABUSY: u32 = 0x70104; + pub const RASTER: u32 = 0x7C000; + pub const RASTER_END: u32 = 0x80000; + + /// Status: raster idle and host idle, command and data FIFOs at or below + /// their low-water marks. + pub const STATUS_IDLE: u32 = 0x01 | 0x02 | 0x10 | 0x40; + pub const STATUS_VBLANK: u32 = 0x04; + + /// Flag set by the "set flag" command (0xE04): DMA/sync completion. + pub const FLAG_DONE: u32 = 1 << 16; + /// Flag set when the geometry engine has data waiting to be read back. + pub const FLAG_GE_DATA: u32 = 1 << 17; + /// Flag set while a geometry engine diagnostic readback has data waiting. + pub const FLAG_GE_DIAG: u32 = 1 << 18; + /// Context switch: outgoing context saved (phase 1) and incoming context + /// loaded (phase 2). + pub const FLAG_CONTEXT_SAVED: u32 = 1 << 19; + pub const FLAG_CONTEXT_LOADED: u32 = 1 << 6; + /// Command-processor flag 0: a scheduled buffer swap has happened. + pub const FLAG_CP0: u32 = 1 << 10; + /// Flags that can raise the general interrupt. + pub const INTR_CAUSES: u32 = 0x7F_FFFF; + + /// Flag and interrupt enables: read at the first address, write-1-to-set + /// there, write-1-to-clear at the second. + pub const FLAG_ENABLE_SET: u32 = 0x50010; + pub const FLAG_ENABLE_CLEAR: u32 = 0x50014; + pub const INTERRUPT_ENABLE_SET: u32 = 0x50018; + pub const INTERRUPT_ENABLE_CLEAR: u32 = 0x5001C; + /// Context switch request: starts the switch routine at the written + /// microcode address. + pub const CONTEXT_SWITCH: u32 = 0x50050; + /// Words of incoming context the host pushes after the save phase. + pub const CONTEXT_SWITCH_WORDS: u32 = 63; + + /// Geometry engine diagnostic ports, per engine: data, then address. + pub const GE_DIAG: [(u32, u32); 2] = [(0x50040, 0x50044), (0x50048, 0x5004C)]; + /// Diagnostic readback words: a discarded word, then the data word. + pub const GE_DIAG_READ_PAD: u32 = 0x50230; + pub const GE_DIAG_READ: u32 = 0x5022C; + + /// Host DMA engine and raster-interface context, read back per register. + pub const DMA_CONTEXT: u32 = 0x50300; + pub const RASTER_IF_CONTEXT: u32 = 0x50200; + /// PIO pixel reads: the raster char registers, high word at the execute + /// alias (which takes the next doubleword), low word at the plain one. + pub const PIO_READ_HI: u32 = 0x7D1C0; + pub const PIO_READ_LO: u32 = 0x7C1C4; +} + +/// Context image word 1: the window state in words 2-13 is new. +const CTX_WINDOW_CHANGED: u32 = 1 << 29; +/// Context image word 1: the context's first load since it was created. +const CTX_FIRST_LOAD: u32 = 1 << 31; + +/// Words of external RAM (ERAM) behind each GE11. +pub const ERAM_WORDS: usize = 0x10000; +/// The external RAM of the two GE11s. Plain data, valid zeroed. +#[repr(C)] +pub struct Eram { + pub words: [[u32; ERAM_WORDS]; 2], +} + +/// A context's saved state in its ERAM slot (image word 0), our layout: the +/// GE HLE's `Gl` as raw words, then the user-port parser. The real +/// microcode's layout is unknown; only the kernel's view matters (a slot +/// is an opaque block of words it may copy to host memory and back, maybe +/// into another slot). +const GL_WORDS: usize = std::mem::size_of::().div_ceil(4); +const PARSER_WORDS: usize = 6 + CFIFO_KEEP; +/// The host DMA engine's and raster interface's registers are context +/// state too (the HAG and REIF context RAMs): the kernel installs a page +/// table for one process's transfer, and another process's switch-in must +/// not leave it in place when the first comes back (snoop's pixel +/// transfers ran through a parked table and scribbled on low memory). +const IF_WORDS: usize = 0x80 + 0x10; +const SLOT_RECORD_WORDS: usize = GL_WORDS + PARSER_WORDS + IF_WORDS; +/// IRIX 6.5.22's slot size (MgrasGlContextAlloc; slots traced 0x17C7 words +/// apart): our record must fit. +const KERNEL_SLOT_WORDS: usize = 0x17C7; +const _: () = assert!(SLOT_RECORD_WORDS <= KERNEL_SLOT_WORDS); +/// Control word data conversion (bits 29:23), as libGLcore's vertex, +/// colour, normal and index entry points use it (glColor4ub = 0x29, +/// glVertex3s = 0x72, ...): bit 5 converts to float, bit 4 signed, bit 3 +/// normalise (OpenGL: unsigned c / (2^b - 1), signed (2c + 1) / (2^b - 1)), +/// bits 2:0 the source size (0 32-bit, 1 byte, 2 short, 4 double); bit 6 +/// appends a fourth component of 1.0 (the three-component forms, floats +/// included). +const CONV_ON: u32 = 1 << 5; +const CONV_PAD: u32 = 1 << 6; + +/// A command's data words converted to floats (bit patterns), per `conv`. +fn convert(conv: u32, bytes: u32, data: &[u32]) -> Vec { + let mut out: Vec = Vec::new(); + if conv & CONV_ON == 0 { + out.extend(data.iter().map(|w| f32::from_bits(*w))); + } else { + let signed = conv & 0x10 != 0; + let norm = conv & 0x08 != 0; + let scale = |v: f64, bits: u32| -> f32 { + let max = ((1u64 << bits) - 1) as f64; + (if !norm { v } else if signed { (2.0 * v + 1.0) / max } else { v / max }) as f32 + }; + match conv & 7 { + 4 => { + for p in data.chunks(2).filter(|p| p.len() == 2) { + out.push(f64::from_bits((p[0] as u64) << 32 | p[1] as u64) as f32); + } + } + 1 => { + for i in 0..bytes as usize { + let b = (data.get(i / 4).copied().unwrap_or(0) >> (24 - 8 * (i % 4))) & 0xFF; + out.push(scale(if signed { b as u8 as i8 as f64 } else { b as f64 }, 8)); + } + } + 2 => { + for i in 0..bytes as usize / 2 { + let h = (data.get(i / 2).copied().unwrap_or(0) >> (16 - 16 * (i % 2))) & 0xFFFF; + out.push(scale(if signed { h as u16 as i16 as f64 } else { h as f64 }, 16)); + } + } + _ => { + for w in data.iter().take(bytes as usize / 4) { + out.push(scale(if signed { *w as i32 as f64 } else { *w as f64 }, 32)); + } + } + } + } + if conv & CONV_PAD != 0 { + out.push(1.0); + } + out.into_iter().map(f32::to_bits).collect() +} + +/// Command FIFO command numbers. +mod cmd { + /// Command-processor token: schedule a buffer swap for the next retrace. + pub const CP_SCHEDULE_SWAP: u32 = 0x37; + /// Command-processor token: return the state word at a GE address + /// (`__Mgr_ReturnMode(addr, 1, 1)`: glGet*, glIsEnabled, and glFinish, + /// which is a glGetBooleanv(GL_DITHER) round trip). The host clears + /// FLAG_GE_DATA, sends this, polls for the flag, then reads the answer + /// at GE_READBACK_LO. + pub const CP_RETURN_MODE: u32 = 0xA2; + /// Command-processor token: wait for the pipeline, then return the + /// argument word (libGLcore spin_and_return; same protocol as + /// CP_RETURN_MODE). IRIS GL programs (powerflip) block on it. That the + /// answer is the argument echoed is our reading of the name; its callers + /// are not in the decompiled libraries. + pub const CP_SPIN_AND_RETURN: u32 = 0xA1; + /// Kernel token (MgrasValidateClip, current context): a GL window's + /// raster state, 15 words: origin, buffer select bits, window mode, + /// screen masks 4..1 as (x, y) pairs, DRB pointers, 0, then the same + /// origin and pointers for the second buffer set. + /// The kernel's and X server's way into GE11 ERAM (MgrasEramRead / + /// MgrasEramWrite; SGI's diagnostic names CP_PASS_THROUGH_GE_*). Write: + /// ERAM_WRITE (word count, rounded up to even), the words (FIFO pixel + /// data, or a host to board DMA), ERAM_WRITE_AT (address, count), and + /// ERAM_WRITE_END after the last chunk. Read: a board to host DMA armed + /// first, then ERAM_READ (address, count) feeds it. + pub const CP_ERAM_WRITE: u32 = 0x0FD; + pub const CP_ERAM_WRITE_AT: u32 = 0x0FB; + pub const CP_ERAM_WRITE_END: u32 = 0x0FA; + pub const CP_ERAM_READ: u32 = 0x0FC; + /// Kernel, switching to its own context: skip the next GE save. + pub const CP_GE_NOSAVE_NEXT_SWITCH: u32 = 0x0F4; + pub const CP_WINDOW: u32 = 0xE4; + pub const SET_DONE_FLAG: u32 = 0xE04; + pub const RASTER_BASE: u32 = 0x1000; + pub const RASTER_EXECUTE: u32 = 0x400; + pub const DMA_BASE: u32 = 0x800; + pub const RASTER_IF_BASE: u32 = 0xA00; + pub const FORMATTER: u32 = 0xC00; + /// Below this, commands are command-processor microcode tokens. + pub const CP_LIMIT: u32 = 0x200; +} + +/// Host DMA engine registers. +mod dma { + pub const PAGE_LIST: usize = 0x00; + pub const STRIDE: usize = 0x04; + pub const ROW_OFFSET: usize = 0x05; + pub const ROW_START: usize = 0x06; + pub const LINES: usize = 0x07; + pub const LINE_BYTES: usize = 0x08; + /// In list mode, the bytes taken from the last page. + pub const LAST_BYTES: usize = 0x09; + /// The start word: bit 0 run, bits 2:1 pool, bit 3 board to host. + pub const START: usize = 0x0B; + /// Display-list branches: eight bytes, the list's offset in the + /// display-list space (high word) and its length in words (low word). + /// NO_WAIT branches at once, WAIT once the pipe has drained, PUSH is a + /// call (the FIFO resumes after the list in every case here). + pub const DL_JUMP_NO_WAIT: usize = 0x0C; + pub const DL_JUMP_WAIT: usize = 0x0E; + pub const DL_PUSH: usize = 0x37; + pub const DL_RETURN: usize = 0x39; + /// Page-table base of pool `p`: eight bytes at `TABLE_BASE + 2p`, the + /// address in the low word. + pub const TABLE_BASE: usize = 0x20; + /// The display-list space's page table (eight bytes, address in the low + /// word) and its last valid offset. + pub const DL_TABLE_BASE: usize = 0x28; + pub const MAX_DL_PTR_OFFSET: usize = 0x34; +} + +/// The parser slot that display-list words run through. +const DL_PORT: usize = 2; +/// Display lists calling display lists deeper than this are cut off (GL's +/// own limit, GL_MAX_LIST_NESTING, is 64), and so is a list running more +/// segments than this (a loop). +const DL_MAX_DEPTH: usize = 64; +const DL_MAX_SEGMENTS: u32 = 1 << 20; + +/// A display-list segment's words from host memory: `offset` in the +/// display-list space, through `table`. +fn fetch_dl_segment(mem: &dyn BusDevice, table: u32, offset: u32, words: u32) -> Option> { + let mut out = Vec::with_capacity(words as usize); + let mut cached: Option<(u32, u32)> = None; + for i in 0..words { + let l = offset.wrapping_add(4 * i); + let page = l >> 12; + let frame = match cached { + Some((p, f)) if p == page => f, + _ => { + let r = mem.read32(table.wrapping_add(4 * page)); + if !r.is_ok() { + return None; + } + cached = Some((page, r.data << 12)); + r.data << 12 + } + }; + let w = mem.read32(frame | (l & 0xFFF)); + out.push(if w.is_ok() { w.data } else { 0 }); + } + Some(out) +} + +/// Raster instruction: the transfer control register, and the value that +/// starts a board-to-host transfer. +const RSS_XFRCONTROL: u32 = 0x102; +const XFRCONTROL_READ_START: u32 = 9; + +/// CPU-side HQ3 registers (the flags are atomic, in `Mgras`). Plain data, +/// valid zeroed. +#[repr(C)] +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], + pub flag_enable: u32, + pub interrupt_enable: u32, + pub ge_readback: [u32; 2], +} + +/// What the frontend drives: the raster subsystem, the flags, system memory. +pub trait Hq3Sink { + /// Write raster register `r`; `exec` runs the IR afterwards. + fn rss_write(&mut self, r: u32, val: u32, exec: bool); + /// The armed raster transfer, once everything queued ahead has run: + /// `Some(read)`, and its (lines, bytes per line). + fn transfer(&mut self) -> Option<(bool, (u32, u32))>; + /// DMA into the armed transfer: one line of pixel bytes. + fn dma_write_line(&mut self, line: u32, bytes: &[u8]); + /// DMA out of the armed transfer: one line of pixel bytes. + fn dma_read_line(&mut self, line: u32) -> Vec; + fn set_flags(&mut self, bits: u32); + /// A GL batch starts (true) or ends: the RSS saves / restores the raster + /// registers the batch loads. + fn gl_bracket(&mut self, _enter: bool) {} + /// System memory for DMA, if connected. + fn sys_mem(&mut self) -> Option>; + /// Something not modelled yet (reported once). + fn note(&mut self, what: String); + /// Whether the annotated trace wants HQ3 lines. + fn tracing(&self) -> bool { + false + } + /// Whether the annotated trace wants texture-load lines. + fn tracing_tex(&self) -> bool { + false + } + /// One annotated trace line. + fn trace(&mut self, _line: String) {} +} + +/// What the frontend has done, for `mgras stats`. +#[derive(Clone, Copy)] +pub struct Hq3Stats { + pub words: u64, + pub raster_writes: u64, + pub raster_execs: u64, + /// Command-processor tokens by number (below `CP_LIMIT`). + pub cp_tokens: [u64; cmd::CP_LIMIT as usize], + pub set_done: u64, + pub swaps: u64, + pub dma_reg_writes: u64, + pub dma_writes: u64, + pub dma_reads: u64, + pub dma_bytes: u64, + pub formatter_writes: u64, + pub raster_if_writes: u64, + pub pixel_cmds: u64, + pub other_cmds: u64, + pub context_switches: u64, + /// Display lists run from the FIFO, and their segments. + pub dl_calls: u64, + pub dl_segments: u64, +} + +impl Hq3Stats { + const ZERO: Hq3Stats = Hq3Stats { + words: 0, raster_writes: 0, raster_execs: 0, cp_tokens: [0; cmd::CP_LIMIT as usize], set_done: 0, swaps: 0, + dma_reg_writes: 0, dma_writes: 0, dma_reads: 0, dma_bytes: 0, formatter_writes: 0, raster_if_writes: 0, + pixel_cmds: 0, other_cmds: 0, context_switches: 0, dl_calls: 0, dl_segments: 0, + }; +} + +/// Names of the host DMA engine registers this model uses. +fn dma_reg_name(n: usize) -> &'static str { + match n { + dma::PAGE_LIST => "page_list", + dma::STRIDE => "stride", + dma::ROW_OFFSET => "row_offset", + dma::ROW_START => "row_start", + dma::LINES => "lines", + dma::LINE_BYTES => "line_bytes", + dma::START => "start", + dma::DL_JUMP_NO_WAIT => "dl_jump", + dma::DL_JUMP_WAIT => "dl_jump_wait", + dma::DL_PUSH => "dl_push", + dma::DL_RETURN => "dl_return", + dma::DL_TABLE_BASE => "dl_table_base", + dma::MAX_DL_PTR_OFFSET => "max_dl_offset", + n if (dma::TABLE_BASE..dma::TABLE_BASE + 8).contains(&n) => "table_base", + _ => "", + } +} + +/// Data words a command keeps (the rest of a longer one are counted, not kept). +const CFIFO_KEEP: usize = 64; + +/// The command FIFO's word-stream parser: a command word, then the data words +/// its byte count announces. +#[derive(Clone, Copy)] +struct Cfifo { + cmd: u32, + /// The control word's data conversion (bits 29:23) and byte count. + conv: u32, + bytes: u32, + pixel: bool, + need: u32, + data: [u32; CFIFO_KEEP], + len: usize, +} + +impl Cfifo { + const EMPTY: Cfifo = Cfifo { cmd: 0, conv: 0, bytes: 0, pixel: false, need: 0, data: [0; CFIFO_KEEP], len: 0 }; + + fn data(&self) -> &[u32] { + &self.data[..self.len] + } + + /// Into a context's ERAM record, `PARSER_WORDS` words. + fn store(&self, out: &mut [u32]) { + out[..6].copy_from_slice(&[self.cmd, self.conv, self.bytes, self.pixel as u32, self.need, self.len as u32]); + out[6..PARSER_WORDS].copy_from_slice(&self.data); + } + + /// From a context's ERAM record (any words: clamped to sense). + fn load(w: &[u32]) -> Self { + let mut f = Cfifo { + cmd: w[0], conv: w[1], bytes: w[2], pixel: w[3] != 0, need: w[4], + len: (w[5] as usize).min(CFIFO_KEEP), data: [0; CFIFO_KEEP], + }; + f.data.copy_from_slice(&w[6..PARSER_WORDS]); + f + } +} + +/// The words of a context's record in ERAM, if the slot holds it. +fn slot_range(slot: u32) -> Option> { + let s = slot as usize; + (s + SLOT_RECORD_WORDS <= ERAM_WORDS).then(|| s..s + SLOT_RECORD_WORDS) +} + +/// The frontend: everything behind the command FIFO. +pub struct Hq3Engine { + /// Context-switch packet words still to swallow from the FIFO. + context_words: u32, + /// The incoming context image being loaded (`CONTEXT_SWITCH_WORDS`). + context_image: [u32; host::CONTEXT_SWITCH_WORDS as usize], + /// One parser per FIFO port (user, privileged): the two may interleave. + /// The third runs display lists. + cfifo: [Cfifo; 3], + /// Display-list branches logged so far (bring-up). + dl_logged: u32, + /// Host DMA engine registers as 32-bit words; an eight-byte register + /// takes two, high word first. + pub dma_regs: [u32; 0x80], + pub raster_if_regs: [u32; 0x10], + pub formatter: u32, + /// A board-to-host DMA started on the host side, waiting for the raster + /// engine to be started (xfrcontrol = 9). + dma_read_pending: Option, + /// DMA transfers logged so far (bring-up). + dma_logged: u32, + /// The GE11s' OpenGL state (HLE) for the current context, the context + /// it belongs to (image word 0, constant per context), and the other + /// contexts' state, parked until they are switched back in (the real + /// GE saves and restores its state with the context). + pub gl: super::gl::Gl, + /// The loaded context's ERAM slot (image word 0); none before the + /// first context load. + slot: Option, + /// CP_GE_NOSAVE_NEXT_SWITCH seen: the next save skips the GE state. + nosave_next: bool, + /// An ERAM write in progress (CP_PASS_THROUGH_GE_WRAM): the words + /// that came for it so far, by FIFO pixel data or host DMA, until + /// CP_PASS_THROUGH_GE_FIFO_3 says where they go. + eram_in: Option>, + /// glBitmap / glDrawPixels rows arriving as FIFO pixel data, and those + /// of switched-out contexts, by slot (a switch can land halfway + /// through a client's pixel data; with two clients drawing text, their + /// rows mixed into broken glyphs). + bitmap_in: Vec, + bitmap_parked: Vec<(u32, Vec)>, + pub eram: Box, + /// The geometry engine's last answer to a state readback + /// (`__MGR_RETURN_MODE`), read at the GE readback registers once set. + pub ge_return: Option<[u32; 2]>, + last_token: Option, + pub stats: Hq3Stats, +} + +impl Hq3Engine { + pub fn new() -> Self { + Hq3Engine { + context_words: 0, + context_image: [0; host::CONTEXT_SWITCH_WORDS as usize], + cfifo: [Cfifo::EMPTY; 3], + dl_logged: 0, + dma_regs: [0; 0x80], + raster_if_regs: [0; 0x10], + formatter: 0, + dma_read_pending: None, + dma_logged: 0, + ge_return: None, + last_token: None, + // SAFETY: plain data, valid zeroed (see `Gl`). + gl: unsafe { std::mem::zeroed() }, + slot: None, + nosave_next: false, + eram_in: None, + bitmap_in: Vec::new(), + bitmap_parked: Vec::new(), + eram: super::plain::boxed_zeroed(), + stats: Hq3Stats::ZERO, + } + } + + /// The host requested a context switch: the save phase completes at once + /// (the caller raises the flag). The outgoing context's GE state and + /// user-port parser go to its ERAM slot, as the HQ microcode saves them + /// on the board: a switch can preempt a process halfway through a + /// command, whose remaining words come when it runs again (otherwise + /// the next context's first words, such as the kernel's SCHEDULE_SWAP, + /// were swallowed as the old command's data). The incoming context + /// follows through the FIFO as raw words (see `push`). + pub fn begin_context_switch(&mut self, sink: &mut dyn Hq3Sink) { + self.gl.end_raster(sink); + if let Some(slot) = self.slot { + self.bitmap_parked.retain(|(s, _)| *s != slot); + if !self.bitmap_in.is_empty() { + self.bitmap_parked.push((slot, std::mem::take(&mut self.bitmap_in))); + } + } + self.bitmap_in.clear(); + let parser = std::mem::replace(&mut self.cfifo[0], Cfifo::EMPTY); + match self.slot.and_then(slot_range) { + Some(r) => { + let rec = &mut self.eram.words[0][r]; + if !std::mem::take(&mut self.nosave_next) { + // SAFETY: `Gl` is plain numbers (no padding-sensitive + // or invalid-bit-pattern fields) and fits the words. + unsafe { + std::ptr::copy_nonoverlapping( + &self.gl as *const super::gl::Gl as *const u8, + rec.as_mut_ptr() as *mut u8, + std::mem::size_of::(), + ); + } + } + parser.store(&mut rec[GL_WORDS..]); + let ifr = &mut rec[GL_WORDS + PARSER_WORDS..]; + ifr[..0x80].copy_from_slice(&self.dma_regs); + ifr[0x80..IF_WORDS].copy_from_slice(&self.raster_if_regs); + } + None => sink.note(format!("context switch from slot {:x?}: nowhere to save", self.slot)), + } + self.context_words = host::CONTEXT_SWITCH_WORDS; + self.stats.context_switches += 1; + if sink.tracing() { + sink.trace(format!("context switch: saved slot {:x?}; loading {} words", self.slot, host::CONTEXT_SWITCH_WORDS)); + } + } + + /// Load the context in ERAM slot `slot`: its GE state and user-port + /// parser, or OpenGL's initial state on its first load (the kernel + /// recycles slots, so a fresh context finds a dead one's state there). + fn load_context(&mut self, slot: u32, first: bool, sink: &mut dyn Hq3Sink) { + self.slot = Some(slot); + self.bitmap_in = match self.bitmap_parked.iter().position(|(s, _)| *s == slot) { + Some(i) if !first => self.bitmap_parked.remove(i).1, + _ => Vec::new(), + }; + // SAFETY (both): plain numbers, valid zeroed and for any bit pattern. + self.gl = unsafe { std::mem::zeroed() }; + self.cfifo[0] = Cfifo::EMPTY; + // Another context may have used the TE meanwhile. + self.gl.te_dirty = 1; + if first { + return; + } + let Some(r) = slot_range(slot) else { + sink.note(format!("context load from slot {slot:#x}: outside ERAM")); + return; + }; + let rec = &self.eram.words[0][r]; + unsafe { + std::ptr::copy_nonoverlapping( + rec.as_ptr() as *const u8, + &mut self.gl as *mut super::gl::Gl as *mut u8, + std::mem::size_of::(), + ); + } + self.cfifo[0] = Cfifo::load(&rec[GL_WORDS..]); + let ifr = &rec[GL_WORDS + PARSER_WORDS..]; + self.dma_regs.copy_from_slice(&ifr[..0x80]); + self.raster_if_regs.copy_from_slice(&ifr[0x80..IF_WORDS]); + self.gl.te_dirty = 1; + } + + /// Load a GL window's raster state (from a context image or + /// CP_WINDOW): origin (x | y << 16, y bottom-up), window mode (masks + /// enabled, bits 0-3, and kept inside, 4-7), screen masks 1-4 as (x, y) + /// ranges `min << 16 | max`, and the buffer page pointers. + fn apply_window(&mut self, origin: u32, mode: u32, masks: [[u32; 2]; 4], drb: u32, sink: &mut dyn Hq3Sink) { + if sink.tracing() { + sink.trace(format!("GL window: origin {:#x} mode {mode:#x} masks {masks:x?} DRBpointers {drb:#x}", origin)); + } + self.gl.window = super::gl::Window { valid: 1, origin, mode, masks, drb }; + } + + /// The current context's GL state and the parked contexts, for the + /// monitor. + pub fn gl_summary(&self) -> String { + format!("context in ERAM slot {:x?} (record {SLOT_RECORD_WORDS} words, GE state {GL_WORDS})\n{}", self.slot, self.gl.describe()) + } + + /// Words still owed to a context switch, and the parsers' state, for the + /// monitor. + pub fn summary(&self) -> String { + let p = |f: &Cfifo| if f.need == 0 { "idle".to_string() } else { format!("cmd {:#x} needs {} more", f.cmd, f.need) }; + format!("context words owed {}, user parser {}, privileged parser {}, DMA read pending {:?}", + self.context_words, p(&self.cfifo[0]), p(&self.cfifo[1]), self.dma_read_pending) + } + + /// One 32-bit word from command FIFO `port` (0 user, 1 privileged). + pub fn push(&mut self, port: usize, w: u32, sink: &mut dyn Hq3Sink) { + self.stats.words += 1; + // A context switch's incoming state follows the save phase as raw + // words; the command processor would load it. Consume it here, and + // report the load done after the last word. + if self.context_words > 0 { + let i = (host::CONTEXT_SWITCH_WORDS - self.context_words) as usize; + self.context_image[i] = w; + self.context_words -= 1; + if self.context_words == 0 { + let img = self.context_image; + if sink.tracing() { + sink.trace(format!("context switch: loaded {:x?}", &img[..])); + } + // Word 0 is the context's ERAM slot; a first load (word 1 + // bit 31, cleared by the kernel afterwards) starts from + // OpenGL's initial state. Every image also carries its + // context's window (words 2-13); the kernel + // (MgrasValidateClip) rewrites it and sets bit 29 of word 1 + // when the window changed. + self.load_context(img[0], img[1] & CTX_FIRST_LOAD != 0, sink); + let masks = [[img[11], img[12]], [img[9], img[10]], [img[7], img[8]], [img[5], img[6]]]; + if sink.tracing() && img[1] & CTX_WINDOW_CHANGED != 0 { + sink.trace(format!("context {:#x}: window changed", img[0])); + } + self.apply_window(img[2], img[3], masks, img[13], sink); + sink.set_flags(host::FLAG_CONTEXT_LOADED); + } + return; + } + let f = &mut self.cfifo[port]; + if f.need == 0 { + if w & 0x8000_0000 != 0 { + // Pixel data: byte count in bits 19:0, sent as doublewords. + f.pixel = true; + f.cmd = w; + f.need = (((w & 0xF_FFFF) + 7) / 8) * 2; + } else { + f.pixel = false; + f.cmd = (w >> 8) & 0x1FFF; + f.conv = (w >> 23) & 0x7F; + f.bytes = w & 0xFF; + f.need = ((w & 0xFF) + 3) / 4; + } + f.len = 0; + if f.need == 0 { + self.dispatch(port, sink); + } + return; + } + f.need -= 1; + if f.pixel { + if let Some(buf) = self.eram_in.as_mut() { + buf.push(w); + } else if self.gl.wants_bitmap_rows() { + self.bitmap_in.push(w); + } + } + let f = &mut self.cfifo[port]; + if f.len < CFIFO_KEEP { + f.data[f.len] = w; + f.len += 1; + } + if f.need == 0 { + self.dispatch(port, sink); + } + } + + fn dispatch(&mut self, port: usize, sink: &mut dyn Hq3Sink) { + let f = self.cfifo[port]; + let (cmd, data) = (f.cmd, f.data()); + if sink.tracing() { + sink.trace(describe_cmd(port, f.pixel, cmd, data)); + } + if f.pixel { + self.stats.pixel_cmds += 1; + if self.eram_in.is_none() && self.gl.wants_bitmap_rows() { + // An image bigger than one pixel command (255 bytes) comes + // in several: wait for all of it. + if self.bitmap_in.len() >= self.gl.pixel_words_needed() { + let rows = std::mem::take(&mut self.bitmap_in); + self.gl.bitmap_rows(&rows, sink); + } + } else if self.eram_in.is_none() { + // libGLcore __glMgrFlushImageRegs: RSS_REG_SET (0x07F) + // carries (raster register, value) pairs, RSS_REG_SHADOW + // (0x07C, __glMgrRSSRegShadow) (register, value, GE shadow + // address) triples, padded to whole doublewords. + let group = match self.last_token { + Some(0x07F) => 2, + Some(0x07C) => 3, + _ => 0, + }; + if group != 0 { + // Inside a GL pixel operation (SAVE_RSS .. RESTORE_RSS) + // the registers are written, within the GL raster + // bracket. Outside one they are the GE's shadow of a GL + // context's raster state (libGLcore sends 33 triples at + // context creation, CONFIG = 0x4 among them): kept for + // the pixel path, not written over the X server's state. + let live = self.gl.pixel_op != 0; + if live { + self.gl.begin_raster(sink); + } + for g in data.chunks_exact(group) { + // Texture registers belong to the context in any + // case: the GE keeps them and loads the TE. + if self.gl.te_write(g[0] & 0x3FF, g[1], sink) { + continue; + } + match g[0] & 0x3FF { + 0x159 => self.gl.xfrmode = g[1], + 0x046 => self.gl.pixel_block[0] = g[1], + 0x047 => self.gl.pixel_block[1] = g[1], + _ => {} + } + if live { + sink.rss_write(g[0] & 0x3FF, g[1], false); + } + } + } else if !matches!(self.last_token, Some(0x082)) { + sink.note(format!("pixel data command {cmd:#010x}")); + } + } + return; + } + self.last_token = Some(cmd); + if cmd >= cmd::RASTER_BASE { + let r = cmd & 0x3FF; + let exec = cmd & cmd::RASTER_EXECUTE != 0; + self.stats.raster_writes += 1; + self.stats.raster_execs += exec as u64; + match data { + [] => sink.rss_write(r, 0, exec), + [d] => sink.rss_write(r, *d, exec), + [hi, lo, ..] => { + sink.rss_write(r, *hi, false); + sink.rss_write(r + 1, *lo, exec); + } + } + if r == RSS_XFRCONTROL && data.first() == Some(&XFRCONTROL_READ_START) { + self.start_read_dma(sink); + } + } else if cmd == cmd::SET_DONE_FLAG { + self.stats.set_done += 1; + sink.set_flags(host::FLAG_DONE); + } else if cmd >= cmd::DMA_BASE { + let n = (cmd & 0x1FF) as usize; + let v = data.first().copied().unwrap_or(0); + if cmd >= cmd::FORMATTER { + self.stats.formatter_writes += 1; + self.formatter = v; + } else if cmd >= cmd::RASTER_IF_BASE { + self.stats.raster_if_writes += 1; + self.raster_if_regs[n & 0xF] = v; + // Register 5 starts the raster engine's side of a read (the + // host DMA was armed first): libGLcore's glReadPixels + // writes 0x4009, glGetTexImage 0x400D, low bits the + // xfrcontrol read start (bit 2: the texture side). + if n & 0xF == 5 && v & XFRCONTROL_READ_START == XFRCONTROL_READ_START { + self.start_read_dma(sink); + } + } else { + self.stats.dma_reg_writes += 1; + // Eight-byte registers arrive as a high word, then a low word, + // and fill two register slots. + let n = n & 0x7F; + // The page list is four words (eight page indices). + let words = if n == dma::PAGE_LIST { 4 } else { 2 }; + for (i, d) in data.iter().take(words).enumerate() { + self.dma_regs[(n + i) & 0x7F] = *d; + } + if data.is_empty() { + self.dma_regs[n] = 0; + } + match n { + dma::START => self.dma_start(v, sink), + // From the FIFO, a branch starts a display list; inside + // one, the links are the list engine's business. + dma::DL_JUMP_NO_WAIT | dma::DL_JUMP_WAIT if port != DL_PORT => { + let (offset, words) = (self.dma_regs[n], self.dma_regs[n + 1]); + self.run_display_list(offset, words, sink); + } + _ => {} + } + } + } 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. + self.gl.swap_buffers(sink); + self.stats.swaps += 1; + self.stats.cp_tokens[cmd as usize] += 1; + sink.set_flags(host::FLAG_CP0); + } else if cmd == cmd::CP_WINDOW && data.len() >= 12 { + self.stats.cp_tokens[cmd as usize] += 1; + let d = data; + let masks = [[d[9], d[10]], [d[7], d[8]], [d[5], d[6]], [d[3], d[4]]]; + self.apply_window(d[0], d[2], masks, d[11], sink); + } else if matches!(cmd, cmd::CP_ERAM_WRITE | cmd::CP_ERAM_WRITE_AT | cmd::CP_ERAM_WRITE_END | cmd::CP_ERAM_READ) { + self.stats.cp_tokens[cmd as usize] += 1; + self.eram_pass_through(cmd, data, sink); + } else if cmd == cmd::CP_GE_NOSAVE_NEXT_SWITCH { + // The kernel, on switching to its own context: it changes no GE + // state, so the next switch away need not save it. + self.stats.cp_tokens[cmd as usize] += 1; + self.nosave_next = true; + } else if cmd == cmd::CP_SPIN_AND_RETURN { + self.stats.cp_tokens[cmd as usize] += 1; + self.gl.end_raster(sink); + self.ge_return = Some([0, data.first().copied().unwrap_or(0)]); + sink.set_flags(host::FLAG_GE_DATA); + } else if cmd == cmd::CP_RETURN_MODE { + self.stats.cp_tokens[cmd as usize] += 1; + // 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 = self.gl.state_word(addr).unwrap_or_else(|| { + sink.note(format!("GE state readback at {addr:#x} ({:x?}) answered 0", data.get(1..).unwrap_or(&[]))); + 0 + }); + self.gl.end_raster(sink); + self.ge_return = Some([0, v]); + sink.set_flags(host::FLAG_GE_DATA); + } else if cmd < cmd::CP_LIMIT && { + let conv = f.conv; + if conv & (CONV_ON | CONV_PAD) != 0 && f.bytes > 0 { + let floats = convert(conv, f.bytes, data); + if sink.tracing() { + sink.trace(format!(" converted ({conv:#x}): {:?}", floats.iter().map(|w| f32::from_bits(*w)).collect::>())); + } + self.gl.token(cmd, &floats, sink) + } else { + self.gl.token(cmd, data, sink) + } + } { + self.stats.cp_tokens[cmd as usize] += 1; + } else if cmd < cmd::CP_LIMIT { + self.stats.cp_tokens[cmd as usize] += 1; + // The kernel's GE11 plumbing (diagnostic CP_* tokens, the HQ DMA + // setup, its own tokens) changes nothing this model draws; the + // rest is the OpenGL protocol, for the GE11s. + if !is_ge_plumbing(cmd) { + sink.note(format!("command-processor token {cmd:#x} {} ({} data words)", token_name(cmd).unwrap_or("?"), data.len())); + } + } else { + self.stats.other_cmds += 1; + sink.note(format!("command {cmd:#x}")); + } + } + + /// The host DMA engine's start word. Host to board runs now: the raster + /// engine was armed first. Board to host waits for the raster engine. + fn dma_start(&mut self, word: u32, sink: &mut dyn Hq3Sink) { + if word & 1 == 0 { + return; + } + if word & 8 != 0 { + self.dma_read_pending = Some(word); + return; + } + if self.eram_in.is_some() { + // An ERAM write's data: host to board, into the pending words. + let mut buf = self.eram_in.take().unwrap_or_default(); + self.dma(word, DmaPeer::EramIn(&mut buf), sink); + self.eram_in = Some(buf); + return; + } + if self.gl.wants_bitmap_rows() { + // SEND_PIXELS whose rows come by host DMA (snoop's zoomed + // image, distort's textures) rather than as FIFO pixel data; + // possibly in several DMAs. + let mut buf = std::mem::take(&mut self.bitmap_in); + self.dma(word, DmaPeer::EramIn(&mut buf), sink); + if buf.len() >= self.gl.pixel_words_needed() { + self.gl.bitmap_rows(&buf, sink); + } else { + self.bitmap_in = buf; + } + return; + } + match sink.transfer() { + Some((false, _)) => self.dma(word, DmaPeer::Raster, sink), + _ => sink.note(format!("host DMA start {word:#x} with no write transfer armed")), + } + } + + /// The ERAM pass-through commands (see `cmd::CP_ERAM_*`), on GE0's + /// ERAM (a board with two GE11s gets the same transfer once per GE). + fn eram_pass_through(&mut self, cmd: u32, data: &[u32], sink: &mut dyn Hq3Sink) { + let arg = |i: usize| data.get(i).copied().unwrap_or(0) as usize; + match cmd { + cmd::CP_ERAM_WRITE => self.eram_in = Some(Vec::with_capacity(arg(0))), + cmd::CP_ERAM_WRITE_AT => { + let (addr, n) = (arg(0), arg(1)); + let buf = self.eram_in.replace(Vec::new()).unwrap_or_default(); + let n = n.min(buf.len()).min(ERAM_WORDS.saturating_sub(addr)); + self.eram.words[0][addr..addr + n].copy_from_slice(&buf[..n]); + if sink.tracing() { + sink.trace(format!("ERAM write {n} words at {addr:#x} ({} came)", buf.len())); + } + } + cmd::CP_ERAM_WRITE_END => self.eram_in = None, + _ => { + let (addr, n) = (arg(0).min(ERAM_WORDS), arg(1)); + let n = n.min(ERAM_WORDS - addr); + let Some(word) = self.dma_read_pending.take() else { + sink.note(format!("ERAM read at {addr:#x} with no host DMA armed")); + return; + }; + if sink.tracing() { + sink.trace(format!("ERAM read {n} words at {addr:#x}")); + } + let words: Vec = self.eram.words[0][addr..addr + n].to_vec(); + self.dma(word, DmaPeer::EramOut(&words, 0), sink); + } + } + } + + fn start_read_dma(&mut self, sink: &mut dyn Hq3Sink) { + let Some(word) = self.dma_read_pending.take() else { + sink.note("raster DMA read started with no host DMA pending".into()); + return; + }; + match sink.transfer() { + Some((true, _)) => self.dma(word, DmaPeer::Raster, sink), + _ => sink.note(format!("host DMA read {word:#x} with no read transfer armed")), + } + } + + /// Run a display list starting with the segment of `words` words at + /// byte `offset` in the display-list space, fetched from host memory + /// through the display-list page table (one 32-bit frame number per + /// 4 KB page, as for images). libGLcore compiles a list into segments + /// whose first commands are the segment's link, the body following: + /// the engine runs the body, then the link. Links (host DMA engine + /// writes): DL_JUMP to the next segment, DL_PUSH (a return segment) + /// followed by DL_JUMP to a called list, DL_RETURN to the last pushed + /// segment, or back to the FIFO when none is left. ideas keeps its + /// materials in lists; long lists chain segments of about 1 K words. + fn run_display_list(&mut self, offset: u32, words: u32, sink: &mut dyn Hq3Sink) { + let Some(mem) = sink.sys_mem() else { return }; + let table = self.dma_regs[dma::DL_TABLE_BASE + 1] & !3; + let saved = std::mem::replace(&mut self.cfifo[DL_PORT], Cfifo::EMPTY); + let mut stack: Vec<(u32, u32)> = Vec::new(); + let mut cur = Some((offset, words)); + let mut segments = 0u32; + while let Some((offset, words)) = cur.take() { + segments += 1; + if segments > DL_MAX_SEGMENTS { + sink.note(format!("display list runs past {DL_MAX_SEGMENTS} segments")); + break; + } + self.stats.dl_segments += 1; + let Some(seg) = fetch_dl_segment(&*mem, table, offset, words) else { + sink.note(format!("display list segment at {offset:#x} unreadable")); + break; + }; + // The link: the DL engine commands at the head of the segment. + let mut links: Vec<(usize, u32, u32)> = Vec::new(); + let mut at = 0; + while at < seg.len() { + let w = seg[at]; + let c = (w >> 8) & 0x1FFF; + let n = ((w & 0xFF) as usize).div_ceil(4); + let r = (c & 0x7F) as usize; + if w & 0x8000_0000 != 0 || !(cmd::DMA_BASE..cmd::RASTER_IF_BASE).contains(&c) + || !matches!(r, dma::DL_JUMP_NO_WAIT | dma::DL_JUMP_WAIT | dma::DL_PUSH | dma::DL_RETURN) + { + break; + } + let arg = |k: usize| seg.get(at + 1 + k).copied().unwrap_or(0); + links.push((r, arg(0), arg(1))); + at += 1 + n; + } + if sink.tracing() { + sink.trace(format!("display list segment {offset:#x}: {words} words, links {links:x?}, stack depth {}", stack.len())); + } + for w in &seg[at.min(seg.len())..] { + self.push(DL_PORT, *w, sink); + } + for (r, a, b) in links { + match r { + dma::DL_PUSH => { + if stack.len() >= DL_MAX_DEPTH { + sink.note("display list calls nest too deep".into()); + break; + } + stack.push((a, b)); + } + dma::DL_RETURN => cur = stack.pop(), + _ => cur = Some((a, b)), + } + } + } + if self.dl_logged < 2 { + self.dl_logged += 1; + eprintln!("mgras: display list at {offset:#x} ({words} words) ran {segments} segments"); + } + self.stats.dl_calls += 1; + self.cfifo[DL_PORT] = saved; + } + + /// Run a DMA between host memory and the armed raster transfer, a line at + /// a time. Host addresses are logical within the pool and translate + /// through its page table: one 32-bit frame number per 4 KB page. + fn dma(&mut self, word: u32, mut peer: DmaPeer, sink: &mut dyn Hq3Sink) { + let Some(mem) = sink.sys_mem() else { return }; + let read = word & 8 != 0; + let pool = ((word >> 1) & 3) as usize; + let table = self.dma_regs[dma::TABLE_BASE + 2 * pool + 1] & !3; + // Between transfers the kernel parks a pool's table at a dummy + // value (0x1, 0x5, 0x9: no page table). A DMA through it would take + // frame numbers from low memory and scribble over the kernel. + if table < 0x1000 { + sink.note(format!("host DMA {word:#x} through pool {pool} with no page table ({table:#x}): dropped")); + return; + } + // List mode (the texture manager's restores): the page list holds + // up to eight 16-bit indices into the pool's table, bit 15 on the + // last; one run from ROW_OFFSET in the first page, LINE_BYTES of it + // there, whole pages between, LAST_BYTES of the last. Otherwise + // (the list just [0 | last]) the pool's pages are consecutive and + // the transfer is LINES lines of LINE_BYTES, STRIDE apart. + let mut list: Vec = Vec::new(); + for w in &self.dma_regs[dma::PAGE_LIST..dma::PAGE_LIST + 4] { + for h in [w >> 16, w & 0xFFFF] { + if list.last().is_some_and(|l| l & 0x8000 != 0) { + break; + } + list.push(h); + } + } + if list.last().is_none_or(|l| l & 0x8000 == 0) || list == [0x8000] { + list.clear(); + } + let (base, stride, lines, len) = if list.is_empty() { + ( + self.dma_regs[dma::ROW_START].wrapping_add(self.dma_regs[dma::ROW_OFFSET]), + self.dma_regs[dma::STRIDE], + self.dma_regs[dma::LINES], + self.dma_regs[dma::LINE_BYTES], + ) + } else { + let (first, last) = (self.dma_regs[dma::LINE_BYTES], self.dma_regs[dma::LAST_BYTES]); + let n = list.len() as u32; + let len = if n == 1 { first } else { first + 0x1000 * (n - 2) + last }; + (self.dma_regs[dma::ROW_OFFSET] & 0xFFF, 0, 1, len) + }; + let list: Vec = list.iter().map(|h| h & 0x7FFF).collect(); + if read { self.stats.dma_reads += 1 } else { self.stats.dma_writes += 1 } + self.stats.dma_bytes += lines as u64 * len as u64; + let desc = || format!( + "DMA {} pool {pool} table {table:#x} base {base:#x} stride {stride} lines {lines} bytes {len} pglist {:#x} with {}", + if read { "read" } else { "write" }, + self.dma_regs[dma::PAGE_LIST], + peer.name() + ); + if sink.tracing() { + sink.trace(desc()); + } + if self.dma_logged < 16 { + self.dma_logged += 1; + eprintln!("mgras: {}", desc()); + } + let frame_of = |page: u32| -> Option { + let page = if list.is_empty() { page } else { *list.get(page as usize)? }; + let r = mem.read32(table.wrapping_add(4 * page)); + r.is_ok().then_some(r.data << 12) + }; + let mut cached: Option<(u32, u32)> = None; + let mut phys = |l: u32| -> Option { + let page = l >> 12; + let f = match cached { + Some((p, f)) if p == page => f, + _ => { + let f = frame_of(page)?; + cached = Some((page, f)); + f + } + }; + Some(f | (l & 0xFFF)) + }; + // A read from the raster engine is a byte stream across its lines: + // the host may take it a row per DMA line, or all rows in one + // (glGetTexImage of a small texture reads 8 rows as one line). + let raster_lines = if read && matches!(peer, DmaPeer::Raster) { sink.transfer().map_or(0, |(_, (n, _))| n) } else { 0 }; + let (mut stream, mut next_line) = (std::collections::VecDeque::::new(), 0u32); + for i in 0..lines { + let a = base.wrapping_add(i.wrapping_mul(stride)); + if read { + let mut bytes = match &mut peer { + DmaPeer::EramOut(words, pos) => { + let mut v = Vec::with_capacity(len as usize); + for _ in 0..len { + let w = words.get(*pos / 4).copied().unwrap_or(0); + v.push((w >> (24 - 8 * (*pos % 4))) as u8); + *pos += 1; + } + v + } + _ => { + while stream.len() < len as usize && next_line < raster_lines.max(i + 1) { + stream.extend(sink.dma_read_line(next_line)); + next_line += 1; + } + let n = (len as usize).min(stream.len()); + stream.drain(..n).collect() + } + }; + format_pixels(self.formatter, &mut bytes); + for (k, b) in bytes.iter().take(len as usize).enumerate() { + let Some(pa) = phys(a + k as u32) else { return }; + mem.write8(pa, *b); + } + } else { + let mut bytes = Vec::with_capacity(len as usize); + for k in 0..len { + let Some(pa) = phys(a + k) else { return }; + let r = mem.read8(pa); + bytes.push(if r.is_ok() { r.data } else { 0 }); + } + if i == 0 && sink.tracing() { + sink.trace(format!(" DMA line 0 from {a:#x}: {:02x?}", &bytes[..bytes.len().min(32)])); + } + format_pixels(self.formatter, &mut bytes); + match &mut peer { + DmaPeer::EramIn(buf) => buf.extend(bytes.chunks(4).map(|c| { + c.iter().enumerate().fold(0u32, |w, (j, b)| w | (*b as u32) << (24 - 8 * j)) + })), + _ => sink.dma_write_line(i, &bytes), + } + } + } + } +} + +/// The formatter's work on DMA'd pixel bytes (mode register 0xC00, see +/// HQ3.h): within each pixel of N components of 1, 2 or 4 bytes, swap the +/// bytes of each component (bit 6) and reverse the components (bit 7, +/// "swizzle": libGLcore loads GL_ABGR_EXT textures with 0xE8D, byte +/// components, and 0xE8E, shorts). The same reordering undoes itself, so +/// reads use it too. Nibble components and the endian modes are not done. +pub(super) fn format_pixels(mode: u32, bytes: &mut [u8]) { + const SWAP_BYTES: u32 = 1 << 6; + const SWIZZLE: u32 = 1 << 7; + if mode & (SWAP_BYTES | SWIZZLE) == 0 { + return; + } + let size = match mode & 3 { + 1 => 1, + 2 => 2, + 3 => 4, + _ => return, + }; + let n = ((mode >> 2) & 3) as usize + 1; + for px in bytes.chunks_exact_mut(size * n) { + if mode & SWAP_BYTES != 0 { + for c in px.chunks_exact_mut(size) { + c.reverse(); + } + } + if mode & SWIZZLE != 0 { + px.reverse(); + if size > 1 { + // Reversing the pixel reversed each component's bytes too. + for c in px.chunks_exact_mut(size) { + c.reverse(); + } + } + } + } +} + +/// What a host DMA moves data to or from: the raster engine's armed +/// transfer, or GE11 ERAM (words big-endian in host memory; the read keeps +/// its byte position across lines). +enum DmaPeer<'a> { + Raster, + EramIn(&'a mut Vec), + EramOut(&'a [u32], usize), +} + +impl DmaPeer<'_> { + fn name(&self) -> &'static str { + match self { + DmaPeer::Raster => "raster", + DmaPeer::EramIn(_) => "ERAM write", + DmaPeer::EramOut(..) => "ERAM read", + } + } +} + +/// Tokens the kernel sends to set up and save the GE11s (and the HQ DMA +/// setup token), which nothing in this model needs. +fn is_ge_plumbing(t: u32) -> bool { + matches!(t, 0x006 | 0x07C | 0x07F | 0x082 | 0x08C | 0x09A | 0x0EC | 0x0ED | 0x0F2 | 0x0F3..=0x0FF) +} + +/// One command FIFO command, decoded for the trace. +fn describe_cmd(port: usize, pixel: bool, cmd: u32, data: &[u32]) -> String { + use super::rss::REG_NAMES; + let p = ["u", "p", "dl"][port.min(2)]; + let words = |d: &[u32]| d.iter().take(16).map(|v| format!("{v:#x}")).collect::>().join(" "); + if pixel { + return format!("{p} pixel data: {} bytes, addr {}, align {}{}{} ({} words) {}", + cmd & 0xF_FFFF, (cmd >> 20) & 3, (cmd >> 22) & 7, + if cmd & 1 << 25 != 0 { ", packed" } else { "" }, if cmd & 1 << 26 != 0 { ", last" } else { "" }, data.len(), words(data)); + } + let name = |r: u32| match REG_NAMES.get(r as usize).copied().unwrap_or("") { + "" => format!("rss[{r:#x}]"), + n => n.to_string(), + }; + if cmd >= cmd::RASTER_BASE { + let r = cmd & 0x3FF; + let exec = if cmd & cmd::RASTER_EXECUTE != 0 { " +exec" } else { "" }; + return match data { + [] => format!("{p} {} (no data){exec}", name(r)), + [v] => format!("{p} {} = {v:#x}{exec}", name(r)), + [hi, lo, ..] => format!("{p} {} = {hi:#x}, {} = {lo:#x}{exec}", name(r), name(r + 1)), + }; + } + let v = data.first().copied().unwrap_or(0); + match cmd { + cmd::SET_DONE_FLAG => format!("{p} SET_DONE_FLAG"), + c if c >= cmd::FORMATTER => format!("{p} formatter = {v:#x}"), + c if c >= cmd::RASTER_IF_BASE => format!("{p} raster_if[{:#x}] = {v:#x}", c & 0xF), + c if c >= cmd::DMA_BASE => { + let n = (c & 0x7F) as usize; + format!("{p} dma[{n:#x}] {} = {}", dma_reg_name(n), words(data)) + } + cmd::CP_SCHEDULE_SWAP => format!("{p} CP SCHEDULE_SWAP {}", words(data)), + c if c < cmd::CP_LIMIT => format!("{p} CP {} [{c:#x}] ({} words) {}", token_name(c).unwrap_or("token"), data.len(), words(data)), + c => format!("{p} command {c:#x} ({} words) {}", data.len(), words(data)), + } +} + +/// Command-processor token names (ignore/gr4/HQ3.h): the OpenGL protocol as +/// libGLcore sends it, SGI's diagnostic CP_* tokens, and kernel tokens. +pub fn token_name(t: u32) -> Option<&'static str> { + Some(match t { + 0x000 => "VERTEX4F", + 0x002 => "COLOR4F", + 0x005 => "SIM_OPEN_GRAPHICS", + 0x006 => "MAKE_CURRENT_READ", + 0x008 => "TEX_COORD4F", + 0x009 => "INIT_RGB", + 0x00D => "ENABLE_LIGHTING", + 0x00E => "DISABLE_LIGHTING", + 0x00F => "EDGE_FLAG_ENABLE", + 0x010 => "EDGE_FLAG", + 0x011 => "MATERIAL", + 0x012 => "LIGHT_AMBIENT", + 0x015 => "CLEAR", + 0x016 => "BEGIN_POINTS", + 0x017 => "BEGIN_LINES", + 0x018 => "BEGIN_LSTRIP", + 0x019 => "BEGIN_LLOOP", + 0x01A => "BEGIN_TRIANGLES", + 0x01B => "BEGIN_TSTRIP", + 0x01C => "BEGIN_TFAN", + 0x01D => "BEGIN_QUADS", + 0x01E => "BEGIN_QSTRIP", + 0x01F => "BEGIN_POLYGON", + 0x020 => "END_POINTS", + 0x021 => "END_LINES", + 0x022 => "END_LSTRIP", + 0x023 => "END_LLOOP", + 0x024 => "END_TRIANGLES", + 0x025 => "END_TSTRIP", + 0x026 => "END_TFAN", + 0x027 => "END_QUADS", + 0x028 => "END_QSTRIP", + 0x029 => "END_POLYGON", + 0x02A => "MATRIX_MODE", + 0x02B => "LOAD_MATRIXD", + 0x02C => "LOAD_IDENTITY", + 0x02D => "MULT_MATRIXD", + 0x02E => "POP_MATRIX", + 0x02F => "PUSH_MATRIX", + 0x030 => "ROTATED", + 0x031 => "SCALED", + 0x032 => "TRANSLATED", + 0x033 => "VIEWPORT", + 0x034 => "FRUSTUM", + 0x035 => "ORTHO", + 0x036 => "FLUSH", + 0x037 => "SCHEDULE_SWAP", + 0x038 => "RASTER_POS4F", + 0x039 => "SCISSOR", + 0x03A => "STENCIL_MASK", + 0x03B => "COLOR_MASK", + 0x03C => "DEPTH_MASK", + 0x03D => "INDEX_MASK", + 0x03E => "ALPHA_FUNC", + 0x03F => "BLEND_FUNC", + 0x040 => "LOGIC_OP", + 0x041 => "STENCIL_FUNC", + 0x042 => "STENCIL_OP", + 0x043 => "DEPTH_FUNC", + 0x044 => "READ_BUFFER", + 0x045 => "LINE_WIDTH", + 0x046 => "LINE_STIPPLE", + 0x047 => "SHADE_MODEL", + 0x048 => "DEPTH_RANGE", + 0x049 => "DRAW_BUFFER", + 0x04A => "CLEAR_DEPTH_BUFFER", + 0x04C => "CLEAR_STENCIL_BUFFER", + 0x04D => "POINT_SIZE", + 0x04E => "LOAD_POLYGON_STIPPLE_RAM", + 0x055 => "LIGHT_QUADRATIC_ATTENUATION", + 0x056 => "FRONT_FACE", + 0x057 => "LIGHT_MODEL_AMBIENT", + 0x059 => "LIGHT_MODEL_TWO_SIDE", + 0x05A => "TEX_GENIV", + 0x05B => "TEX_GEN_PLANE_HEADER", + 0x05C => "TEX_GEN_PLANE_DATA", + 0x05D => "PASS_THROUGH", + 0x05F => "DISABLE", + 0x060 => "BLEND_EQUATION_EXT", + 0x061 => "GET_FEEDBACK_DATA", + 0x063 => "GET_ERROR", + 0x066 => "DEPTH_TEST_ENABLE", + 0x067 => "DITHER_ENABLE", + 0x06E => "SOFT_DISABLE", + 0x072 => "CULL_FACE_ENABLE", + 0x073 => "CULL_FACE_DISABLE", + 0x071 => "ENABLE_LIGHT_SOURCE", + 0x074 => "CULL_FACE", + 0x07C => "RSS_REG_SHADOW", + 0x07F => "RSS_REG_SET", + 0x082 => "INIT_TABLES", + 0x083 => "RESET_HISTOGRAM_MEMORY", + 0x085 => "INIT_CONVOLVE2_DSTATE", + 0x086 => "FAST_COPY_PIXELS", + 0x088 => "NEW_INIT_FAST_PATH", + 0x089 => "INIT_COLOR_TABLE", + 0x08B => "__MGR_RETURN_HIST_PIO", + 0x08C => "KERN_8C", + 0x08E => "COPY_TILE", + 0x091 => "GENERAL_BITMAP", + 0x094 => "BITMAP", + 0x096 => "COPY_COLOR_TABLE_SGI", + 0x09A => "INIT_FORMAT_VALUES", + 0x09B => "TL_NO_TABLE", + 0x09D => "_WRITE_DMAGESETUP", + 0x0A0 => "DISABLE_LIGHT_SOURCE", + 0x0A1 => "SPIN_AND_RETURN", + 0x0A2 => "__MGR_RETURN_MODE", + 0x0A5 => "CLIP_PLANE", + 0x0A8 => "POLYGON_MODE", + 0x0A9 => "EVAL_MESH1_DGE11", + 0x0AA => "EVAL_MESH2_DGE11", + 0x0AB => "EVAL_COORD1_DGE11", + 0x0AC => "EVAL_COORD2_DGE11", + 0x0AE => "MAP_GRID1D", + 0x0AF => "MAP_GRID2D", + 0x0B0 => "CLEAN_UP1_D", + 0x0B1 => "CLEAN_UP2_D", + 0x0B2 => "SEND_MAP_DATA1_D", + 0x0B3 => "SEND_MAP_DATA2_D", + 0x0B7 => "FOG_DENSITY", + 0x0B9 => "FOG_MODE", + 0x0BA => "CLEAR_COLOR", + 0x0BB => "CLEAR_ACCUM", + 0x0BC => "CLEAR_DEPTH", + 0x0BD => "CLEAR_INDEX", + 0x0BE => "CLEAR_STENCIL", + 0x0C1 => "COLOR_MATERIAL_BOTH", + 0x0C4 => "INIT_NAMES", + 0x0C5 => "LOAD_NAME", + 0x0C6 => "POP_NAME", + 0x0C7 => "PUSH_NAME", + 0x0C9 => "RENDER_MODE", + 0x0CC => "END_GENERIC", + 0x0D2 => "SAVE_RSS", + 0x0D3 => "RESTORE_RSS", + 0x0D4 => "SEND_HISTOGRAM_FILLS", + 0x0D5 => "SEND_MINMAX_FILLS", + 0x0D6 => "UPDATE_RASTER_POS", + 0x0D7 => "LOAD_RASTER_POS_INFO", + 0x0D9 => "BLEND_COLOR_EXT", + 0x0DB => "GET_PIXELS_BITMAP_PIO", + 0x0DC => "SWAPTMESH", + 0x0E4 => "KERN_WINDOW", + 0x0DE => "LMCOLOR", + 0x0E0 => "DRAW_DUMB_POINT", + 0x0E7 => "POLYGON_OFFSET", + 0x0E9 => "SAVE_TEXMODE", + 0x0EA => "RESTORE_TEXMODE", + 0x0EC => "RSS_REG_SHADOW_IDX", + 0x0ED => "SET_DATA", + 0x0EE => "GET_MATERIALFV", + 0x0EF => "GET_LIGHTFV", + 0x0F2 => "GET_HISTOGRAM_EXT", + 0x0F0 => "READ_TEXTURE", + 0x0F3 => "CP_DUMP_FOREVER", + 0x0F4 => "CP_GE_NOSAVE_NEXT_SWITCH", + 0x0F5 => "CP_GE_FTOF", + 0x0F6 => "CP_GE_DMA_HEADER_SIZE", + 0x0F7 => "CP_DELAY", + 0x0F8 => "CP_SET_CP_FLAGS", + 0x0F9 => "CP_PASS_THROUGH_GE_WRAM_5", + 0x0FA => "CP_PASS_THROUGH_GE_WRAM_4", + 0x0FB => "CP_PASS_THROUGH_GE_FIFO_3", + 0x0FC => "CP_PASS_THROUGH_GE_WRAM_DELAYED", + 0x0FD => "CP_PASS_THROUGH_GE_WRAM", + 0x0FE => "CP_PASS_THROUGH_GE_FIFO", + 0x0FF => "CP_GE_SAVE_IDLE_HQ_STATE", + 0x100 => "COPY_PIXELS", + 0x007 => "NORMAL3F", + 0x013 => "LIGHT_DIFFUSE", + 0x014 => "LIGHT_SPECULAR", + 0x04F => "LIGHT_POSITION", + 0x050 => "LIGHT_SPOT_DIRECTION", + 0x051 => "LIGHT_SPOT_EXPONENT", + 0x052 => "LIGHT_SPOT_CUTOFF", + 0x053 => "LIGHT_CONSTANT_ATTENUATION", + 0x054 => "LIGHT_LINEAR_ATTENUATION", + 0x058 => "LIGHT_MODEL_LOCAL_VIEWER", + 0x064 => "ALPHA_TEST_ENABLE", + 0x065 => "BLEND_ENABLE", + 0x068 => "FOG_ENABLE", + 0x06A => "LINE_STIPPLE_ENABLE", + 0x06B => "LOGIC_OP_ENABLE", + 0x06D => "POLYGON_STIPPLE_ENABLE", + 0x06F => "STENCIL_TEST_ENABLE", + 0x0A3 => "NORMALIZE_ENABLE", + 0x0A6 => "CLIP_PLANE_ENABLE", + 0x0A7 => "CLIP_PLANE_DISABLE", + 0x0B4 => "FOG_COLOR", + 0x0B5 => "FOG_START", + 0x0B6 => "FOG_END", + 0x0B8 => "FOG_INDEX", + 0x0BF => "COLOR_MATERIAL_FRONT", + 0x0C0 => "COLOR_MATERIAL_BACK", + 0x0C2 => "COLOR_MATERIAL_ENABLE", + 0x0C3 => "COLOR_MATERIAL_DISABLE", + _ => return None, + }) +} diff --git a/src/dev/mgras/mgras_tests.rs b/src/dev/mgras/mgras_tests.rs new file mode 100644 index 00000000..1cadc929 --- /dev/null +++ b/src/dev/mgras/mgras_tests.rs @@ -0,0 +1,2160 @@ +//! Whole-board tests through the bus, with the HQ3 and RSS threads running: +//! command FIFO and direct-window drawing and their ordering, PIO reads, +//! host DMA both ways, flags, the context switch, the GE11 diagnostic port, +//! and replay of text recordings (`testdata/*.txt`). + +use std::collections::HashMap; +use std::sync::Arc; + +use parking_lot::Mutex; + +use super::record::tests::{live_board, read, replay_recs, write}; +use super::record::load_text; +use super::Mgras; +use crate::traits::{BusDevice, BusRead32, BusRead8, BUS_OK}; + +const CFIFO: u32 = 0x70080; +const RASTER: u32 = 0x7C000; +const EXEC: u32 = 0x1000; +const FLAGS: u32 = 0x70008; + +// Raster registers (rss.rs `reg`). +const IR_ALIAS: u32 = 0x045; +const BLOCKXYSTARTI: u32 = 0x046; +const BLOCKXYENDI: u32 = 0x047; +const XFRCONTROL: u32 = 0x102; +const FILLMODE: u32 = 0x110; +const CONFIG: u32 = 0x112; +const XYWIN: u32 = 0x115; +const DRBPOINTERS: u32 = 0x16D; +const DRBSIZE: u32 = 0x16E; +const XFRSIZE: u32 = 0x153; +const XFRMODE: u32 = 0x159; +const PP1FILLMODE: u32 = 0x161; +const FILL_COLOR_R: u32 = 0x176; +const FILL_FAST: u32 = 1 << 20; + +/// A raster register write through the command FIFO. +fn fifo_rss(m: &Mgras, r: u32, val: u32, exec: bool) { + let cmd = 0x1000 | r | if exec { 0x400 } else { 0 }; + write(m, 32, CFIFO, ((cmd << 8) | 4) as u64); + write(m, 32, CFIFO, val as u64); +} + +/// A raster register write through the direct window. +fn direct_rss(m: &Mgras, r: u32, val: u32, exec: bool) { + write(m, 32, RASTER + 4 * r + if exec { EXEC } else { 0 }, val as u64); +} + +/// A host DMA engine register write through the command FIFO. +fn fifo_dma(m: &Mgras, n: u32, val: u32) { + write(m, 32, CFIFO, (((0x800 | n) << 8) | 4) as u64); + write(m, 32, CFIFO, val as u64); +} + +/// The X server's raster setup: origin at the top row, Y flipped, colour +/// index drawing. +fn x_server(m: &Mgras) { + direct_rss(m, CONFIG, 0xCAC, false); + direct_rss(m, XYWIN, 1023 << 16, false); + // The PROM's 1280x1024 layout: draw into the main buffer at page 0x240. + direct_rss(m, DRBSIZE, 0x31E, false); + direct_rss(m, DRBPOINTERS, 0x240, false); + // All planes writable, as the X server sets them. + direct_rss(m, 0x163, u32::MAX, false); + direct_rss(m, PP1FILLMODE, 0x0C00_4504, false); +} + +fn block(m: &Mgras, x0: u32, y0: u32, x1: u32, y1: u32) { + direct_rss(m, IR_ALIAS, 0x18, false); + direct_rss(m, BLOCKXYSTARTI, x0 << 16 | y0, false); + direct_rss(m, BLOCKXYENDI, x1 << 16 | y1, true); +} + +/// Flags, polled until `bit` is set (the HQ3 raises them asynchronously). +fn wait_flag(m: &Mgras, bit: u32) -> u32 { + for _ in 0..1_000_000 { + let f = read(m, 32, FLAGS) as u32; + if f & bit != 0 { + return f; + } + std::thread::yield_now(); + } + panic!("flag {bit:#x} never set"); +} + +#[test] +fn fifo_fast_fill_lands_top_down() { + let m = live_board(); + x_server(&m); + fifo_rss(&m, FILLMODE, FILL_FAST, false); + fifo_rss(&m, FILL_COLOR_R, 0x13, false); + fifo_rss(&m, IR_ALIAS, 0x18, false); + fifo_rss(&m, BLOCKXYSTARTI, 10 << 16 | 20, false); + fifo_rss(&m, BLOCKXYENDI, 12 << 16 | 21, true); + assert_eq!(m.fb_pixel(10, 20), 0x13); + assert_eq!(m.fb_pixel(12, 21), 0x13); + assert_eq!(m.fb_pixel(13, 21), 0); + m.stop_engines(); +} + +#[test] +fn fifo_and_direct_writes_keep_program_order() { + let m = live_board(); + x_server(&m); + direct_rss(&m, FILLMODE, FILL_FAST, false); + // FIFO colour, then a direct colour, then a FIFO-executed block: the + // direct write came last, so it wins. + fifo_rss(&m, FILL_COLOR_R, 3, false); + direct_rss(&m, FILL_COLOR_R, 5, false); + fifo_rss(&m, IR_ALIAS, 0x18, false); + fifo_rss(&m, BLOCKXYSTARTI, 1 << 16 | 1, false); + fifo_rss(&m, BLOCKXYENDI, 1 << 16 | 1, true); + // And the other way round, executed through the direct window. + direct_rss(&m, FILL_COLOR_R, 7, false); + fifo_rss(&m, FILL_COLOR_R, 9, false); + block(&m, 2, 1, 2, 1); + assert_eq!(m.fb_pixel(1, 1), 5); + assert_eq!(m.fb_pixel(2, 1), 9); + m.stop_engines(); +} + +#[test] +fn pio_read_through_the_bus() { + let m = live_board(); + x_server(&m); + direct_rss(&m, FILLMODE, FILL_FAST, false); + for (k, v) in [1u32, 2, 3, 4, 5, 6].into_iter().enumerate() { + let (x, y) = (100 + k as u32 % 3, 50 + k as u32 / 3); + direct_rss(&m, FILL_COLOR_R, v, false); + block(&m, x, y, x, y); + } + // Three 1-byte pixels a line, two lines, begin skip 2 (rss.rs + // pio_read_frames_each_line_in_a_fresh_doubleword). + direct_rss(&m, FILLMODE, 2 << 22, false); + direct_rss(&m, XFRMODE, 2 << 8, false); + direct_rss(&m, XFRSIZE, 2 << 16 | 3, false); + block(&m, 100, 50, 102, 51); + let mut dw = || (read(&m, 32, 0x7D1C0) << 32) | read(&m, 32, 0x7C1C4); + assert_eq!(dw(), 0x0000_0102_0300_0000); + assert_eq!(dw(), 0x0000_0000_0004_0506); + assert_eq!(dw(), 0); + m.stop_engines(); +} + +/// System memory for DMA tests: page-table words and pixel bytes. +#[derive(Default)] +struct TestMem { + words: Mutex>, + bytes: Mutex>, +} + +impl BusDevice for TestMem { + fn read8(&self, addr: u32) -> BusRead8 { BusRead8::ok(self.bytes.lock().get(&addr).copied().unwrap_or(0)) } + fn write8(&self, addr: u32, val: u8) -> u32 { self.bytes.lock().insert(addr, val); BUS_OK } + fn read32(&self, addr: u32) -> BusRead32 { BusRead32::ok(self.words.lock().get(&addr).copied().unwrap_or(0)) } +} + +/// Memory with pool 0's page table at 0x1000 mapping logical page 0 to +/// frame 2 (0x2000), and the board pointed at it: two lines of three bytes, +/// four bytes apart, from logical 0. +fn dma_setup(m: &Mgras) -> Arc { + let mem = Arc::new(TestMem::default()); + mem.words.lock().insert(0x1000, 2); + m.set_phys(mem.clone()); + fifo_dma(m, 0x21, 0x1000); // pool 0 table base (low word) + fifo_dma(m, 0x06, 0); // row start + fifo_dma(m, 0x05, 0); // row offset + fifo_dma(m, 0x04, 4); // stride + fifo_dma(m, 0x07, 2); // lines + fifo_dma(m, 0x08, 3); // bytes a line + mem +} + +#[test] +fn dma_write_from_host_memory() { + let m = live_board(); + x_server(&m); + let mem = dma_setup(&m); + for (a, b) in [(0x2000, 1u8), (0x2001, 2), (0x2002, 3), (0x2004, 4), (0x2005, 5), (0x2006, 6)] { + mem.bytes.lock().insert(a, b); + } + direct_rss(&m, FILLMODE, 5 << 22, false); + direct_rss(&m, XFRMODE, 0, false); + direct_rss(&m, XFRSIZE, 2 << 16 | 3, false); + block(&m, 10, 10, 12, 11); + fifo_dma(&m, 0x0B, 1); // start: run, pool 0, host to board + let got: Vec = [(10, 10), (11, 10), (12, 10), (10, 11), (11, 11), (12, 11)].iter().map(|&(x, y)| m.fb_pixel(x, y)).collect(); + assert_eq!(got, [1, 2, 3, 4, 5, 6]); + m.stop_engines(); +} + +#[test] +fn dma_read_to_host_memory() { + let m = live_board(); + x_server(&m); + direct_rss(&m, FILLMODE, FILL_FAST, false); + for (k, v) in [7u32, 8, 9, 10, 11, 12].into_iter().enumerate() { + let (x, y) = (20 + k as u32 % 3, 30 + k as u32 / 3); + direct_rss(&m, FILL_COLOR_R, v, false); + block(&m, x, y, x, y); + } + let mem = dma_setup(&m); + direct_rss(&m, FILLMODE, 4 << 22, false); + direct_rss(&m, XFRMODE, 0, false); + direct_rss(&m, XFRSIZE, 2 << 16 | 3, false); + block(&m, 20, 30, 22, 31); + // The host side starts first (board to host), then the raster engine. + fifo_dma(&m, 0x0B, 0x9); + fifo_rss(&m, XFRCONTROL, 9, false); + m.state_hash(); // wait for the board + let b = mem.bytes.lock(); + let got: Vec = [0x2000, 0x2001, 0x2002, 0x2004, 0x2005, 0x2006].iter().map(|a| b.get(a).copied().unwrap_or(0)).collect(); + assert_eq!(got, [7, 8, 9, 10, 11, 12]); + drop(b); + m.stop_engines(); +} + +#[test] +fn set_done_command_raises_the_done_flag() { + let m = live_board(); + write(&m, 32, CFIFO, (0xE04 << 8) as u64); + assert_ne!(wait_flag(&m, 1 << 16) & 1 << 16, 0); + // The host clears it. + write(&m, 32, 0x7000C, 1 << 16); + assert_eq!(read(&m, 32, FLAGS) as u32 & 1 << 16, 0); + m.stop_engines(); +} + +#[test] +fn context_switch_swallows_the_incoming_context() { + let m = live_board(); + x_server(&m); + write(&m, 32, 0x50050, 0x100); + assert_ne!(read(&m, 32, FLAGS) as u32 & 1 << 19, 0, "saved at once"); + // 63 words of context that would parse as commands if not swallowed: + // each a raster write header announcing four data bytes. + for _ in 0..63 { + write(&m, 32, CFIFO, ((0x1000 | FILL_COLOR_R) << 8 | 4) as u64); + } + wait_flag(&m, 1 << 6); + fifo_rss(&m, FILLMODE, FILL_FAST, false); + fifo_rss(&m, FILL_COLOR_R, 0x2A, false); + block(&m, 5, 5, 5, 5); + assert_eq!(m.fb_pixel(5, 5), 0x2A); + m.stop_engines(); +} + +#[test] +fn ge11_microcode_reads_back_as_the_driver_verifies_it() { + let m = live_board(); + let (data, addr) = (0x50040, 0x50044); + write(&m, 32, addr, 0x20_0000); + for w in [0x1111_1111u32, 0x2222_2222, 0x0000_0033, 0x4444_4444, 0x5555_5555, 0x0000_0066] { + write(&m, 32, data, w as u64); + } + write(&m, 32, addr, 0x20_0000); + write(&m, 32, addr, 0x8000_0002); // read two lines + assert_ne!(read(&m, 32, FLAGS) as u32 & 1 << 18, 0, "readback waiting"); + let words: Vec = (0..3).map(|_| read(&m, 32, 0x5022C) as u32).collect(); + assert_eq!(words, [0x1111_1111, 0x33, 0x5555_5555]); + assert_eq!(read(&m, 32, FLAGS) as u32 & 1 << 18, 0, "drained"); + m.stop_engines(); +} + +/// A text recording replays through the same engine as the goldens. +#[test] +fn text_recording_replays() { + let recs = load_text( + "# X-style setup, then a fast fill of (300, 400)-(301, 400) in index 0x44 + W 32 7c448 cac + W 32 7c454 3ff0000 + W 32 7c584 c004504 + W 32 7c5b8 31e + W 32 7c5b4 240 + W 32 7c58c ffffffff + W 32 7c440 100000 + W 32 7c5d8 44 + W 32 7c114 18 + W 32 7c118 12c0190 + W 32 7d11c 12d0190 + T", + ) + .unwrap(); + let m = live_board(); + let rep = replay_recs(&m, &recs, false).unwrap(); + assert!(!rep.diverged()); + assert_eq!((m.fb_pixel(300, 400), m.fb_pixel(301, 400), m.fb_pixel(302, 400)), (0x44, 0x44, 0)); + m.stop_engines(); +} + +/// A real capture (testdata/xsetroot_red.txt) replayed on a fresh board: +/// the exposed root takes colour index 1 (red in the IRIS default "4sight" +/// colormap, loaded long before the capture), and the Console window, not +/// exposed, keeps what it had. +#[test] +fn xsetroot_red_trace() { + let recs = load_text(include_str!("testdata/xsetroot_red.txt")).unwrap(); + let m = live_board(); + let rep = replay_recs(&m, &recs, false).unwrap(); + assert!(!rep.diverged(), "{rep:?}"); + let root = m.fb_pixel(640, 900); + assert_eq!(root, 1); + for (x, y) in [(100, 500), (1000, 200), (500, 50), (1270, 1010), (150, 250)] { + assert_eq!(m.fb_pixel(x, y), root, "root at ({x}, {y})"); + } + assert_eq!(m.fb_pixel(500, 500), 0, "Console interior not exposed"); + m.stop_engines(); +} + +/// Window-ID runs: no run at x = 0 starts in ID 0; each run lasts to the +/// next one; the overlay ID picks the overlay mode. +#[test] +fn frame_composes_runs_overlay_and_cursor() { + use super::frame::{Frame, MainMode, OverlayMode, H, OUT_STRIDE, W}; + let mut f = super::plain::boxed_zeroed::(); + (f.width, f.height) = (W, H); + for (i, g) in f.gamma.iter_mut().enumerate() { + *g = [i as u8; 3]; + } + f.cmap[0x100 + 5] = 0x11_2233; + f.cmap[0x200 + 7] = 0x44_5566; + f.main_mode[3] = MainMode { rgb: false, cmap_base: 0x100 }; + f.main_mode[4] = MainMode { rgb: true, cmap_base: 0 }; + f.overlay_mode[2] = OverlayMode { on: true, cmap_base: 0x200 }; + f.did_main[10] = 3; + f.main[10] = 5; + f.did_main[11] = 4; + f.main[11] = 0x00_8040_20; + f.did_overlay[12] = 2; + f.overlay[12] = 7; + f.did_main[12] = 4; + f.main[12] = 0x00FF_FFFF; + let mut out = vec![0u32; OUT_STRIDE * H]; + f.compose(&mut out); + assert_eq!(out[10], 0xFF33_2211, "CI through cmap block, red in the low byte"); + assert_eq!(out[11], 0xFF80_4020 & 0xFFFF_FFFF, "RGB as stored"); + assert_eq!(out[12], 0xFF66_5544, "overlay over the main planes"); +} + +/// Probe: the display size a recording's final state programs into the VC3. +/// MGRAS_SIZE_PROBE= cargo test --release mgras_size_probe -- --nocapture +#[test] +fn mgras_size_probe() { + let Ok(path) = std::env::var("MGRAS_SIZE_PROBE") else { return }; + let recs = super::record::load(&path).unwrap(); + let m = live_board(); + replay_recs(&m, &recs, false).unwrap(); + let (t, d) = m.vc3_sizes(); + eprintln!("{path}: timing tables {t:?}, DID frame table {d} lines"); + m.stop_engines(); +} + +/// A command-processor token and its data words through the command FIFO. +fn fifo_token(m: &Mgras, token: u32, data: &[u32]) { + write(m, 32, CFIFO, ((token << 8) | (4 * data.len() as u32)) as u64); + for d in data { + write(m, 32, CFIFO, *d as u64); + } +} + +fn f(v: f32) -> u32 { + v.to_bits() +} + +/// The glprim triangle (window at the screen's lower left), shade model +/// `shade` (GL_FLAT 0x1D00, GL_SMOOTH 0x1D01). +fn gl_glprim_triangle(m: &Mgras, shade: u32) { + x_server(m); + let mut win = vec![0u32; 15]; + win[2] = 0x11; + (win[9], win[10]) = (399, 299); + win[11] = 0x240; + fifo_token(m, 0xE4, &win); + fifo_token(m, 0x33, &[0, 0, 400, 300]); + fifo_token(m, 0x2A, &[0x1701]); + fifo_token(m, 0x2C, &[]); + fifo_token(m, 0x35, &[f(0.0), f(400.0), f(0.0), f(300.0), f(-1.0), f(1.0)]); + fifo_token(m, 0x2A, &[0x1700]); + fifo_token(m, 0x2C, &[]); + fifo_token(m, 0x47, &[shade]); + fifo_token(m, 0xBA, &[f(1.0), f(0.5), f(0.75), f(1.0)]); + fifo_token(m, 0x15, &[]); + fifo_token(m, 0x1A, &[]); + for (c, v) in [([1.0, 0.0, 0.0], [80.0, 60.0]), ([0.0, 1.0, 0.0], [320.0, 60.0]), ([0.0, 0.0, 1.0], [200.0, 240.0])] { + fifo_token(m, 0x02, &[f(c[0]), f(c[1]), f(c[2])]); + fifo_token(m, 0x00, &[f(v[0]), f(v[1]), f(0.0)]); + } + fifo_token(m, 0x24, &[]); +} + +/// Smooth shading interpolates the vertex colours across the triangle: +/// near each vertex its colour dominates; the centroid is an even mix. +#[test] +fn gl_smooth_triangle_interpolates() { + let m = live_board(); + gl_glprim_triangle(&m, 0x1D01); + let at = |x: usize, y_gl: usize| m.fb_pixel(x, 1023 - y_gl); + let rgb = |v: u32| [v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF]; + let near_red = rgb(at(82, 60)); + let near_green = rgb(at(317, 60)); + let near_blue = rgb(at(200, 237)); + assert!(near_red[0] > 245 && near_red[1] < 10, "{near_red:?}"); + assert!(near_green[1] > 245 && near_green[0] < 10, "{near_green:?}"); + assert!(near_blue[2] > 240, "{near_blue:?}"); + let mid = rgb(at(200, 120)); + for c in mid { + assert!((75..=95).contains(&c), "centroid {mid:?}"); + } + m.stop_engines(); +} + +/// glprim's default frame as libGLcore sends it (IRIX 6.5.22, traced): a +/// 400x300 window, ortho projection, pink clear, then one flat-shaded +/// triangle (80,60) red, (320,60) green, (200,240) blue. Flat shading takes +/// the last vertex's colour; the window is placed by the kernel's window +/// token (here at the screen's lower left, drawing buffer page 0x240). +#[test] +fn gl_flat_triangle_and_clear() { + let m = live_board(); + x_server(&m); + // Window: origin (0, 0) bottom-up, mask 1 = the window, kept inside. + let mut win = vec![0u32; 15]; + win[0] = 0; + win[2] = 0x11; + (win[9], win[10]) = (399, 299); + win[11] = 0x240; + fifo_token(&m, 0xE4, &win); + fifo_token(&m, 0x33, &[0, 0, 400, 300]); + fifo_token(&m, 0x2A, &[0x1701]); + fifo_token(&m, 0x2C, &[]); + fifo_token(&m, 0x35, &[f(0.0), f(400.0), f(0.0), f(300.0), f(-1.0), f(1.0)]); + fifo_token(&m, 0x2A, &[0x1700]); + fifo_token(&m, 0x2C, &[]); + fifo_token(&m, 0x47, &[0x1D00]); + fifo_token(&m, 0xBA, &[f(1.0), f(0.5), f(0.75), f(1.0)]); + fifo_token(&m, 0x15, &[]); + fifo_token(&m, 0x1A, &[]); + for (c, v) in [([1.0, 0.0, 0.0], [80.0, 60.0]), ([0.0, 1.0, 0.0], [320.0, 60.0]), ([0.0, 0.0, 1.0], [200.0, 240.0])] { + fifo_token(&m, 0x02, &[f(c[0]), f(c[1]), f(c[2])]); + fifo_token(&m, 0x00, &[f(v[0]), f(v[1]), f(0.0)]); + } + fifo_token(&m, 0x24, &[]); + // Display rows count down from the top of the 1024-line screen. + let at = |x: usize, y_gl: usize| m.fb_pixel(x, 1023 - y_gl); + let pink = 0xBF_80FF; + assert_eq!(at(5, 5), pink, "clear inside the window"); + assert_eq!(at(399, 299), pink); + assert_eq!(at(400, 5), 0, "nothing outside the window"); + assert_eq!(at(200, 100), 0xFF_0000, "flat triangle in the last vertex's colour (blue)"); + assert_eq!(at(81, 60), 0xFF_0000, "bottom edge row is in"); + assert_eq!(at(79, 60), pink, "left of the triangle"); + assert_eq!(at(200, 240), pink, "the apex row is out (half-open)"); + m.stop_engines(); +} + +/// Switch to a context whose image carries `id` (word 0) and a GL window +/// at (`x`, `y`) bottom-up, `w` x `h`, mask 1 = the window, kept inside. +/// GL context ERAM slots (image word 0) as IRIX 6.5.22 hands them out. +const SLOT_A: u32 = 0xA9B; +const SLOT_B: u32 = 0xA9B + 0x17C7; +const SLOT_C: u32 = 0xA9B + 2 * 0x17C7; +const SLOT_D: u32 = 0xA9B + 3 * 0x17C7; + +fn switch_to_gl_context(m: &Mgras, id: u32, x: u32, y: u32, w: u32, h: u32) { + write(m, 32, 0x50050, 0x4FC); + let mut img = [0u32; 63]; + img[0] = id; + img[2] = x | y << 16; + img[3] = 0x11; + img[11] = x << 16 | (x + w - 1); + img[12] = y << 16 | (y + h - 1); + img[13] = 0x240; + for wd in img { + write(m, 32, CFIFO, wd as u64); + } + wait_flag(m, 1 << 6); +} + +fn gl_setup_window(m: &Mgras, w: u32, h: u32, clear: [f32; 3]) { + fifo_token(m, 0x33, &[0, 0, w, h]); + fifo_token(m, 0xBA, &[f(clear[0]), f(clear[1]), f(clear[2]), f(1.0)]); +} + +/// Two GL contexts, each with its own window and clear colour, switched in +/// and out: each clear lands in its own window in its own colour (window +/// and GL state belong to the context, not the board), and the X server's +/// raster registers, even ones it wrote through the direct window, are +/// back afterwards. +#[test] +fn gl_state_and_window_follow_the_context() { + let m = live_board(); + x_server(&m); + switch_to_gl_context(&m, SLOT_A, 0, 0, 100, 100); + gl_setup_window(&m, 100, 100, [1.0, 0.0, 0.0]); + switch_to_gl_context(&m, SLOT_B, 200, 0, 100, 100); + gl_setup_window(&m, 100, 100, [0.0, 0.0, 1.0]); + switch_to_gl_context(&m, SLOT_A, 0, 0, 100, 100); + fifo_token(&m, 0x15, &[]); + switch_to_gl_context(&m, SLOT_B, 200, 0, 100, 100); + fifo_token(&m, 0x15, &[]); + let at = |x: usize, y_gl: usize| m.fb_pixel(x, 1023 - y_gl); + assert_eq!(at(50, 50), 0x00_00FF, "context A clears its window red"); + assert_eq!(at(250, 50), 0xFF_0000, "context B clears its window blue"); + assert_eq!(at(150, 50), 0, "between the windows"); + // The X server's state survives, including CONFIG written directly. + direct_rss(&m, CONFIG, 0xCAC, false); + fifo_token(&m, 0x15, &[]); + fifo_rss(&m, FILLMODE, FILL_FAST, false); + fifo_rss(&m, FILL_COLOR_R, 0x2A, false); + block(&m, 5, 5, 5, 5); + assert_eq!(m.fb_pixel(5, 5), 0x2A, "X draws top-down at its own origin again"); + m.stop_engines(); +} + +/// A GL context in a 400x300 window at the screen's lower left with an +/// ortho projection (z = -1 near .. 1 far after glOrtho's flip). +fn gl_window_400x300(m: &Mgras) { + switch_to_gl_context(m, 0xC, 0, 0, 400, 300); + gl_ortho_400x300(m); +} + +/// The current context's 400x300 viewport, ortho projection, flat shading. +fn gl_ortho_400x300(m: &Mgras) { + fifo_token(m, 0x33, &[0, 0, 400, 300]); + fifo_token(m, 0x2A, &[0x1701]); + fifo_token(m, 0x2C, &[]); + fifo_token(m, 0x35, &[f(0.0), f(400.0), f(0.0), f(300.0), f(-1.0), f(1.0)]); + fifo_token(m, 0x2A, &[0x1700]); + fifo_token(m, 0x2C, &[]); + fifo_token(m, 0x47, &[0x1D00]); +} + +fn gl_tri(m: &Mgras, c: [f32; 3], v: [[f32; 3]; 3]) { + fifo_token(m, 0x1A, &[]); + fifo_token(m, 0x02, &[f(c[0]), f(c[1]), f(c[2])]); + for p in v { + fifo_token(m, 0x00, &[f(p[0]), f(p[1]), f(p[2])]); + } + fifo_token(m, 0x24, &[]); +} + +/// Depth test (GL_LESS): a near triangle drawn first hides the far one +/// drawn over it; with the depth buffer cleared to 1.0 the far one still +/// shows where it is alone. +#[test] +fn gl_depth_test_hides_the_farther_triangle() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + fifo_token(&m, 0x66, &[1]); + fifo_token(&m, 0xBA, &[f(0.0), f(0.0), f(0.0), f(1.0)]); + fifo_token(&m, 0x15, &[]); + fifo_token(&m, 0x4A, &[]); + // glOrtho(-1, 1) maps object z to window z = (1 - z) / 2: z = 0.5 near. + gl_tri(&m, [0.0, 0.0, 1.0], [[50.0, 50.0, 0.5], [250.0, 50.0, 0.5], [150.0, 250.0, 0.5]]); + gl_tri(&m, [1.0, 0.0, 0.0], [[100.0, 40.0, -0.5], [350.0, 40.0, -0.5], [225.0, 260.0, -0.5]]); + let at = |x: usize, y_gl: usize| m.fb_pixel(x, 1023 - y_gl); + assert_eq!(at(150, 100), 0xFF_0000, "near blue wins where they overlap"); + assert_eq!(at(320, 60), 0x00_00FF, "far red where it is alone"); + m.stop_engines(); +} + +/// Back-face culling: a clockwise triangle (back facing with the default +/// front face, counter-clockwise) is dropped; with culling off it draws. +#[test] +fn gl_back_face_culling() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + let cw = [[80.0, 60.0, 0.0], [200.0, 240.0, 0.0], [320.0, 60.0, 0.0]]; + fifo_token(&m, 0x72, &[]); + fifo_token(&m, 0x74, &[0x405]); + gl_tri(&m, [0.0, 1.0, 0.0], cw); + let at = |x: usize, y_gl: usize| m.fb_pixel(x, 1023 - y_gl); + assert_eq!(at(200, 100), 0, "culled"); + fifo_token(&m, 0x56, &[0x900]); + gl_tri(&m, [0.0, 1.0, 0.0], cw); + assert_eq!(at(200, 100), 0x00_FF00, "front face clockwise: drawn"); + m.stop_engines(); +} + +/// gltest's setup: glFrustum(-4/3, 4/3, -1, 1, 1, 100), the object +/// translated to z = -5, depth test on, a quad facing the viewer. +#[test] +fn gl_frustum_quad_with_depth() { + let m = live_board(); + x_server(&m); + switch_to_gl_context(&m, SLOT_C, 0, 0, 800, 600); + fifo_token(&m, 0x33, &[0, 0, 800, 600]); + fifo_token(&m, 0x2A, &[0x1701]); + fifo_token(&m, 0x2C, &[]); + fifo_token(&m, 0x34, &[f(-4.0 / 3.0), f(4.0 / 3.0), f(-1.0), f(1.0), f(1.0), f(100.0)]); + fifo_token(&m, 0x2A, &[0x1700]); + fifo_token(&m, 0x66, &[1]); + fifo_token(&m, 0xBA, &[f(0.4), f(0.1), f(0.6), f(1.0)]); + fifo_token(&m, 0x4A, &[]); + fifo_token(&m, 0x15, &[]); + fifo_token(&m, 0x2C, &[]); + fifo_token(&m, 0x32, &[f(0.0), f(0.0), f(-5.0)]); + fifo_token(&m, 0x1D, &[]); + fifo_token(&m, 0x02, &[f(1.0), f(1.0), f(0.0)]); + for p in [[-1.0, -1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, 1.0], [-1.0, 1.0, 1.0]] { + fifo_token(&m, 0x00, &[f(p[0]), f(p[1]), f(p[2])]); + } + fifo_token(&m, 0x27, &[]); + let at = |x: usize, y_gl: usize| m.fb_pixel(x, 1023 - y_gl); + assert_eq!(at(400, 300), 0x00_FFFF, "the quad covers the centre"); + assert_eq!(at(10, 10), 0x99_1A66, "clear colour outside it"); + m.stop_engines(); +} + +// ---- GL raster state, one feature per test -------------------------------- + +/// A 400x300 GL window (ortho), cleared to `clear`, flat shading. +fn gl_board(clear: [f32; 3]) -> std::sync::Arc { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + fifo_token(&m, 0xBA, &[f(clear[0]), f(clear[1]), f(clear[2]), f(1.0)]); + fifo_token(&m, 0x15, &[]); + m +} + +/// Window pixel (x, y), GL y up, of the 400x300 window at the screen's +/// lower left. +fn gl_px(m: &Mgras, x: usize, y: usize) -> u32 { + m.fb_pixel(x, 1023 - y) & 0xFF_FFFF +} + +fn gl_color4(m: &Mgras, c: [f32; 4]) { + fifo_token(m, 0x02, &[f(c[0]), f(c[1]), f(c[2]), f(c[3])]); +} + +/// A full-window quad in the current colour. +fn gl_full_quad(m: &Mgras) { + fifo_token(m, 0x1D, &[]); + for p in [[0.0, 0.0], [400.0, 0.0], [400.0, 300.0], [0.0, 300.0]] { + fifo_token(m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(m, 0x27, &[]); +} + +fn gl_line_tokens(m: &Mgras, a: [f32; 2], b: [f32; 2]) { + fifo_token(m, 0x17, &[]); + fifo_token(m, 0x00, &[f(a[0]), f(a[1]), f(0.0)]); + fifo_token(m, 0x00, &[f(b[0]), f(b[1]), f(0.0)]); + fifo_token(m, 0x21, &[]); +} + +#[test] +fn gl_blend_src_alpha_over_the_clear_colour() { + let m = gl_board([1.0, 0.0, 0.0]); + fifo_token(&m, 0x65, &[1]); + fifo_token(&m, 0x3F, &[0x302, 0x303]); + gl_color4(&m, [0.0, 0.0, 1.0, 0.5]); + gl_full_quad(&m); + let p = gl_px(&m, 200, 150); + assert_eq!((p & 0xFF, (p >> 8) & 0xFF, p >> 16), (0x80, 0, 0x80), "{p:#x}"); + // ONE, ONE adds. + fifo_token(&m, 0x3F, &[1, 1]); + gl_color4(&m, [0.0, 1.0, 0.0, 1.0]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 150), 0x80_FF80); + m.stop_engines(); +} + +#[test] +fn gl_alpha_test_greater() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x64, &[1]); + fifo_token(&m, 0x3E, &[4, 0x800]); + gl_color4(&m, [1.0, 1.0, 1.0, 0.25]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 150), 0, "alpha 0.25 fails GREATER 0.5"); + gl_color4(&m, [1.0, 1.0, 1.0, 0.75]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 150), 0xFF_FFFF, "alpha 0.75 passes"); + m.stop_engines(); +} + +#[test] +fn gl_logic_op_xor_and_colour_mask() { + let m = gl_board([1.0, 0.0, 0.0]); + fifo_token(&m, 0x6B, &[1]); + fifo_token(&m, 0x40, &[6]); + gl_color4(&m, [1.0, 1.0, 0.0, 1.0]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 150), 0x00_FF00, "red XOR yellow = green"); + fifo_token(&m, 0x6B, &[0]); + // Colour mask: red and blue only. + fifo_token(&m, 0x3B, &[0x5]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 150), 0xFF_FFFF & (0xFF_00FF | 0x00_FF00), "green kept, red and blue written"); + m.stop_engines(); +} + +/// The classic stencil mask: a triangle writes stencil 1 with colour +/// writes off, then a full quad drawn where stencil == 1. +#[test] +fn gl_stencil_masks_a_later_draw() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0xBE, &[0]); + fifo_token(&m, 0x4C, &[]); + fifo_token(&m, 0x6F, &[1]); + fifo_token(&m, 0x41, &[7, 1, 0xFF]); + fifo_token(&m, 0x42, &[0, 0, 2]); + fifo_token(&m, 0x3B, &[0]); + gl_tri(&m, [1.0, 1.0, 1.0], [[100.0, 50.0, 0.0], [300.0, 50.0, 0.0], [200.0, 250.0, 0.0]]); + fifo_token(&m, 0x3B, &[0xF]); + fifo_token(&m, 0x41, &[2, 1, 0xFF]); + fifo_token(&m, 0x42, &[0, 0, 0]); + gl_color4(&m, [0.0, 0.0, 1.0, 1.0]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 100), 0xFF_0000, "inside the stencilled triangle"); + assert_eq!(gl_px(&m, 20, 20), 0, "outside it"); + m.stop_engines(); +} + +/// Depth GREATER with depth writes off: a far quad passes against a +/// cleared-to-0 depth buffer, and leaves it at 0 for the next one. +#[test] +fn gl_depth_func_greater_without_depth_writes() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x66, &[1]); + fifo_token(&m, 0xBC, &[0]); + fifo_token(&m, 0x4A, &[]); + fifo_token(&m, 0x43, &[4]); + fifo_token(&m, 0x3C, &[0]); + gl_tri(&m, [1.0, 0.0, 0.0], [[0.0, 0.0, -0.5], [400.0, 0.0, -0.5], [200.0, 300.0, -0.5]]); + assert_eq!(gl_px(&m, 200, 100), 0x00_00FF); + gl_tri(&m, [0.0, 1.0, 0.0], [[0.0, 0.0, 0.9], [400.0, 0.0, 0.9], [200.0, 300.0, 0.9]]); + assert_eq!(gl_px(&m, 200, 100), 0x00_FF00, "depth buffer untouched: still 0"); + m.stop_engines(); +} + +#[test] +fn gl_lines_half_open_width_and_stipple() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_line_tokens(&m, [10.0, 10.5], [100.0, 10.5]); + assert_eq!(gl_px(&m, 10, 10), 0xFF_FFFF, "first pixel in"); + assert_eq!(gl_px(&m, 99, 10), 0xFF_FFFF); + assert_eq!(gl_px(&m, 100, 10), 0, "last pixel out"); + assert_eq!(gl_px(&m, 50, 11), 0, "one pixel wide"); + // Width 3: rows 19..21 around y = 20.5. + fifo_token(&m, 0x45, &[f(3.0)]); + gl_line_tokens(&m, [10.0, 20.5], [100.0, 20.5]); + assert_eq!([gl_px(&m, 50, 19), gl_px(&m, 50, 20), gl_px(&m, 50, 21), gl_px(&m, 50, 22)], [0xFF_FFFF, 0xFF_FFFF, 0xFF_FFFF, 0]); + fifo_token(&m, 0x45, &[f(1.0)]); + // Stipple 0x0F0F as libGLcore sends it: bit-reversed (0xF0F0), repeat + // 1: four pixels on, four off. + fifo_token(&m, 0x6A, &[1]); + fifo_token(&m, 0x46, &[0, 0xF0F0]); + gl_line_tokens(&m, [0.0, 40.5], [16.0, 40.5]); + let row: Vec = (0..16).map(|x| (gl_px(&m, x, 40) != 0) as u32).collect(); + assert_eq!(row, [1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0]); + m.stop_engines(); +} + +#[test] +fn gl_polygon_mode_line_and_points() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0xA8, &[0x408, 0x1B01]); + gl_tri(&m, [0.0, 1.0, 0.0], [[100.5, 50.5, 0.0], [300.5, 50.5, 0.0], [200.5, 250.5, 0.0]]); + assert_eq!(gl_px(&m, 200, 120), 0, "interior not filled"); + assert_eq!(gl_px(&m, 200, 50), 0x00_FF00, "bottom edge drawn"); + fifo_token(&m, 0xA8, &[0x408, 0x1B02]); + fifo_token(&m, 0x16, &[]); + fifo_token(&m, 0x00, &[f(5.5), f(7.5), f(0.0)]); + fifo_token(&m, 0x20, &[]); + assert_eq!(gl_px(&m, 5, 7), 0x00_FF00, "a point lights its pixel"); + assert_eq!(gl_px(&m, 6, 7), 0); + m.stop_engines(); +} + +#[test] +fn gl_polygon_stipple_and_scissor() { + let m = gl_board([0.0, 0.0, 0.0]); + let rows: Vec = (0..32).map(|r| if r & 1 == 1 { 0x5555_5555 } else { 0xAAAA_AAAA }).collect(); + fifo_token(&m, 0x4E, &rows); + fifo_token(&m, 0x6D, &[1]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_full_quad(&m); + // Row 0 = 0xAAAAAAAA: MSB (x mod 32 = 0) set. + assert_eq!([gl_px(&m, 0, 0), gl_px(&m, 1, 0), gl_px(&m, 0, 1), gl_px(&m, 1, 1)], [0xFF_FFFF, 0, 0, 0xFF_FFFF]); + fifo_token(&m, 0x6D, &[0]); + fifo_token(&m, 0x6E, &[1]); + // glScissor(30, 40, 100, 50) as libGLcore sends it (traced): x, width + // - 1, y, height - 1. + fifo_token(&m, 0x39, &[30, 99, 40, 49]); + gl_color4(&m, [1.0, 0.0, 0.0, 1.0]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 30, 40), 0x00_00FF); + assert_eq!(gl_px(&m, 129, 89), 0x00_00FF); + assert_ne!(gl_px(&m, 130, 89), 0x00_00FF, "right of the scissor box"); + assert_ne!(gl_px(&m, 129, 90), 0x00_00FF, "above it"); + assert_ne!(gl_px(&m, 29, 60), 0x00_00FF); + m.stop_engines(); +} + +/// Context slots are recycled: a context loaded for the first time (image +/// word 1 bit 31) starts from OpenGL's defaults, not the state the slot's +/// previous owner left (an XOR logic op here). +#[test] +fn gl_new_context_in_a_recycled_slot_starts_fresh() { + let m = live_board(); + x_server(&m); + switch_to_gl_context(&m, SLOT_D, 0, 0, 400, 300); + fifo_token(&m, 0x6B, &[1]); + fifo_token(&m, 0x40, &[6]); + // Same slot (word 0), first load again: a new process. + write(&m, 32, 0x50050, 0x4FC); + let mut img = [0u32; 63]; + (img[0], img[1], img[2], img[3]) = (SLOT_D, 1 << 31, 0, 0x11); + (img[11], img[12], img[13]) = (399, 299, 0x240); + for wd in img { + write(&m, 32, CFIFO, wd as u64); + } + wait_flag(&m, 1 << 6); + gl_setup_window(&m, 400, 300, [1.0, 0.0, 0.0]); + fifo_token(&m, 0x15, &[]); + gl_color4(&m, [1.0, 1.0, 0.0, 1.0]); + fifo_token(&m, 0x2A, &[0x1701]); + fifo_token(&m, 0x35, &[f(0.0), f(400.0), f(0.0), f(300.0), f(-1.0), f(1.0)]); + fifo_token(&m, 0x2A, &[0x1700]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 150), 0x00_FFFF, "yellow copied, not XORed onto red"); + m.stop_engines(); +} + +// ---- GL lighting, fog, clip planes ----------------------------------------- + +/// A quad over the window's middle (100..300 x 75..225) at depth z. +fn gl_mid_quad(m: &Mgras, z: [f32; 4]) { + fifo_token(m, 0x1D, &[]); + for (k, p) in [[100.0, 75.0], [300.0, 75.0], [300.0, 225.0], [100.0, 225.0]].iter().enumerate() { + fifo_token(m, 0x00, &[f(p[0]), f(p[1]), f(z[k])]); + } + fifo_token(m, 0x27, &[]); +} + +fn grey(v: u32) -> [u32; 3] { + [v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF] +} + +/// OpenGL's default light 0 (white, from +z) and default material on a +/// quad facing it: 0.2 * 0.2 (model ambient) + 0.8 (diffuse) = 0.84. +#[test] +fn gl_default_light_and_material() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x0D, &[]); + fifo_token(&m, 0x71, &[0]); + fifo_token(&m, 0x07, &[f(0.0), f(0.0), f(1.0)]); + gl_mid_quad(&m, [0.0; 4]); + assert_eq!(grey(gl_px(&m, 200, 150)), [214, 214, 214]); + // A red diffuse material through glMaterial (front, GL_DIFFUSE). + fifo_token(&m, 0x11, &[0x404, 0x1201, f(1.0), f(0.0), f(0.0), f(1.0)]); + gl_mid_quad(&m, [0.0; 4]); + assert_eq!(grey(gl_px(&m, 200, 150)), [10 + 245, 10, 10], "0.04 ambient + 1.0 red diffuse"); + m.stop_engines(); +} + +/// Colour material (front, ambient and diffuse) follows glColor. +#[test] +fn gl_color_material_tracks_the_colour() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x0D, &[]); + fifo_token(&m, 0x71, &[0]); + fifo_token(&m, 0xBF, &[0x1602]); + fifo_token(&m, 0xC2, &[]); + fifo_token(&m, 0x07, &[f(0.0), f(0.0), f(1.0)]); + gl_color4(&m, [0.0, 1.0, 0.0, 1.0]); + gl_mid_quad(&m, [0.0; 4]); + assert_eq!(grey(gl_px(&m, 200, 150)), [0, 255, 0], "green: 0.2 * 0.2 ambient + 1.0 diffuse, clamped"); + m.stop_engines(); +} + +/// Two-sided lighting: a clockwise (back-facing) quad shows the back +/// material, lit with the flipped normal. +#[test] +fn gl_two_sided_lighting_uses_the_back_material() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x0D, &[]); + fifo_token(&m, 0x71, &[0]); + fifo_token(&m, 0x59, &[1]); + fifo_token(&m, 0x11, &[0x405, 0x1201, f(0.0), f(0.0), f(1.0), f(1.0)]); + // Normal -z: facing the light once flipped for the back face. + fifo_token(&m, 0x07, &[f(0.0), f(0.0), f(-1.0)]); + fifo_token(&m, 0x1D, &[]); + for p in [[100.0, 75.0], [100.0, 225.0], [300.0, 225.0], [300.0, 75.0]] { + fifo_token(&m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(&m, 0x27, &[]); + assert_eq!(grey(gl_px(&m, 200, 150)), [10, 10, 255]); + m.stop_engines(); +} + +/// User clip plane 0 keeps x <= 200 (plane (-1, 0, 0, 200)). +#[test] +fn gl_user_clip_plane() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0xA5, &[0x3000, f(-1.0), f(0.0), f(0.0), f(200.0)]); + fifo_token(&m, 0xA6, &[0x3000]); + gl_color4(&m, [1.0, 1.0, 0.0, 1.0]); + gl_mid_quad(&m, [0.0; 4]); + assert_eq!(gl_px(&m, 150, 150), 0x00_FFFF); + assert_eq!(gl_px(&m, 250, 150), 0, "clipped away"); + fifo_token(&m, 0xA7, &[0x3000]); + gl_mid_quad(&m, [0.0; 4]); + assert_eq!(gl_px(&m, 250, 150), 0x00_FFFF, "plane disabled"); + m.stop_engines(); +} + +/// Linear fog from start 0 to end 1 (eye z = -object z under glOrtho): +/// white at the near edge, the fog colour at the far edge. +#[test] +fn gl_linear_fog() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x68, &[1]); + fifo_token(&m, 0xB9, &[0x2601]); + fifo_token(&m, 0xB5, &[f(0.0)]); + fifo_token(&m, 0xB6, &[f(1.0)]); + fifo_token(&m, 0xB4, &[f(0.0), f(1.0), f(0.0), f(1.0)]); + fifo_token(&m, 0x47, &[0x1D01]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_mid_quad(&m, [0.0, -1.0, -1.0, 0.0]); + let near = grey(gl_px(&m, 101, 150)); + let far = grey(gl_px(&m, 298, 150)); + assert!(near[0] > 245 && near[1] > 245, "near: {near:?}"); + assert!(far[0] < 10 && far[1] > 245 && far[2] < 10, "far: {far:?}"); + m.stop_engines(); +} + +/// A token with a control-word data conversion (bits 29:23). +fn fifo_token_conv(m: &Mgras, token: u32, conv: u32, bytes: u32, data: &[u32]) { + write(m, 32, CFIFO, (conv << 23 | token << 8 | bytes) as u64); + for d in data { + write(m, 32, CFIFO, *d as u64); + } +} + +/// The HQ converts integer forms to floats before the GE: glColor4ub +/// (0x29, bytes R G B A in one word, normalised) and glVertex3s (0x72, +/// shorts, not normalised, w padded to 1). +#[test] +fn gl_hq_converts_ubyte_colours_and_short_vertices() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token_conv(&m, 0x02, 0x29, 4, &[0xFF80_00FF]); + fifo_token(&m, 0x1D, &[]); + for (x, y) in [(100u32, 75u32), (300, 75), (300, 225), (100, 225)] { + fifo_token_conv(&m, 0x00, 0x72, 6, &[x << 16 | y, 0]); + } + fifo_token(&m, 0x27, &[]); + assert_eq!(gl_px(&m, 200, 150), 0x00_80FF, "orange from (255, 128, 0)"); + assert_eq!(gl_px(&m, 50, 150), 0, "outside the quad"); + m.stop_engines(); +} + +/// Triangle strips keep one winding: with back faces culled, both +/// triangles of a counter-clockwise strip draw. IRIS GL's swaptmesh +/// (0xDC) swaps the two remembered vertices (the new vertex replaces the +/// older one) and keeps the winding: v0 v1 swap v2 swap v3 is the IRIS GL +/// fan idiom, triangles (v0 v1 v2) and (v0 v2 v3). +#[test] +fn gl_tstrip_winding_and_swaptmesh() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x72, &[]); + fifo_token(&m, 0x74, &[0x405]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + let strip = |m: &Mgras, verts: &[[f32; 2]], swap_from: Option| { + fifo_token(m, 0x1B, &[]); + for (k, p) in verts.iter().enumerate() { + if swap_from.is_some_and(|s| k >= s) { + fifo_token(m, 0xDC, &[]); + } + fifo_token(m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(m, 0x25, &[]); + }; + // A strip along the bottom: (0,0) (0,100) (100,0) (100,100). + strip(&m, &[[0.0, 100.0], [0.0, 0.0], [100.0, 100.0], [100.0, 0.0]], None); + assert_eq!(gl_px(&m, 20, 50), 0xFF_FFFF, "first triangle"); + assert_eq!(gl_px(&m, 80, 50), 0xFF_FFFF, "second triangle, reversed to keep the winding"); + // A fan made with swaptmesh around (250, 50): v0 = centre. + strip(&m, &[[250.0, 50.0], [300.0, 50.0], [250.0, 100.0], [200.0, 50.0]], Some(2)); + assert_eq!(gl_px(&m, 265, 60), 0xFF_FFFF, "(c, right, top)"); + assert_eq!(gl_px(&m, 235, 60), 0xFF_FFFF, "(c, top, left) after the swap"); + m.stop_engines(); +} + +/// A context switch can preempt a process halfway through a command: the +/// rest of its words come when it runs again. The parser state belongs to +/// the context, so the next context's commands parse from a clean start +/// (the kernel's SCHEDULE_SWAP was swallowed as the old command's data, a +/// swap timeout that killed the X server when a GL window was resized). +#[test] +fn gl_partial_command_survives_a_context_switch() { + let m = live_board(); + x_server(&m); + switch_to_gl_context(&m, SLOT_A, 0, 0, 100, 100); + gl_setup_window(&m, 100, 100, [0.0, 0.0, 0.0]); + fifo_token(&m, 0x2A, &[0x1701]); + fifo_token(&m, 0x35, &[f(0.0), f(100.0), f(0.0), f(100.0), f(-1.0), f(1.0)]); + fifo_token(&m, 0x2A, &[0x1700]); + // A: COLOR4F header and its first word, then preempted. + write(&m, 32, CFIFO, (0x02 << 8 | 16) as u64); + write(&m, 32, CFIFO, f(1.0) as u64); + switch_to_gl_context(&m, SLOT_B, 200, 0, 100, 100); + gl_setup_window(&m, 100, 100, [0.0, 0.0, 1.0]); + fifo_token(&m, 0x15, &[]); + assert_eq!(gl_px(&m, 250, 50), 0xFF_0000, "context B's clear ran"); + // A resumes: the colour's last three words, then a quad in it. + switch_to_gl_context(&m, SLOT_A, 0, 0, 100, 100); + for w in [f(1.0), f(0.0), f(1.0)] { + write(&m, 32, CFIFO, w as u64); + } + fifo_token(&m, 0x1D, &[]); + for p in [[10.0, 10.0], [90.0, 10.0], [90.0, 90.0], [10.0, 90.0]] { + fifo_token(&m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(&m, 0x27, &[]); + assert_eq!(gl_px(&m, 50, 50), 0x00_FFFF, "A's colour (1, 1, 0) arrived whole"); + m.stop_engines(); +} + +/// A display-list branch runs the list's commands from host memory, through +/// the display-list page table, then the FIFO carries on (ideas keeps its +/// materials in lists). +#[test] +fn gl_display_list_runs_from_host_memory() { + let m = gl_board([0.0, 0.0, 0.0]); + let mem = Arc::new(TestMem::default()); + // The table's low bits carry flags (the kernel writes 0x...d); logical + // page 0 is frame 2. The list, at offset 0x80: the colour red. + mem.words.lock().insert(0x1000, 2); + let list = [0x02 << 8 | 16, f(1.0), f(0.0), f(0.0), f(1.0)]; + for (i, w) in list.iter().enumerate() { + mem.words.lock().insert(0x2080 + 4 * i as u32, *w); + } + m.set_phys(mem.clone()); + fifo_token(&m, 0x800 | 0x28, &[0, 0x1001]); + fifo_token(&m, 0x800 | 0x0C, &[0x80, list.len() as u32]); + gl_full_quad(&m); + assert_eq!(gl_px(&m, 200, 150), 0x0000FF); + m.stop_engines(); +} + +/// Pool 0's page table at 0x1000, logical pages 0..n at frames 2.., and a +/// one-line host DMA of `bytes` bytes from logical 0. +fn eram_dma_setup(m: &Mgras, bytes: u32) -> Arc { + let mem = Arc::new(TestMem::default()); + for p in 0..8 { + mem.words.lock().insert(0x1000 + 4 * p, 2 + p); + } + m.set_phys(mem.clone()); + fifo_dma(m, 0x21, 0x1000); + fifo_dma(m, 0x06, 0); + fifo_dma(m, 0x05, 0); + fifo_dma(m, 0x04, 0); + fifo_dma(m, 0x07, 1); + fifo_dma(m, 0x08, bytes); + mem +} + +/// The X server's ERAM write at start-up (traced): a word count, the words +/// as FIFO pixel data, then where they go. Read back the kernel's way: a +/// board to host DMA armed first, then the ERAM read command feeds it. +#[test] +fn eram_write_by_pio_and_read_by_dma() { + let m = live_board(); + x_server(&m); + let mem = eram_dma_setup(&m, 8); + fifo_token(&m, 0xFD, &[2]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x1234_5678); + write(&m, 32, CFIFO, 0x9ABC_DEF0); + fifo_token(&m, 0xFB, &[0x40, 2]); + fifo_token(&m, 0xFA, &[]); + fifo_dma(&m, 0x0B, 0x9); // run, pool 0, board to host + fifo_token(&m, 0xFC, &[0x40, 2]); + m.state_hash(); + let b = mem.bytes.lock(); + let got: Vec = (0..8).map(|i| b.get(&(0x2000 + i)).copied().unwrap_or(0)).collect(); + assert_eq!(got, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]); + drop(b); + m.stop_engines(); +} + +/// Slot eviction as IRIX does it: the kernel copies a context's ERAM slot +/// to host memory and later back into whatever slot is free. The context +/// loaded from its new slot still has its GL state (here its clear colour +/// and viewport). +#[test] +fn gl_context_survives_eviction_to_another_slot() { + let m = live_board(); + x_server(&m); + switch_to_gl_context(&m, SLOT_A, 0, 0, 100, 100); + gl_setup_window(&m, 100, 100, [1.0, 0.0, 0.0]); + switch_to_gl_context(&m, SLOT_B, 200, 0, 100, 100); + // Evict A: slot to host, then host into slot C. + let mem = eram_dma_setup(&m, 0x17C7 * 4); + fifo_dma(&m, 0x0B, 0x9); + fifo_token(&m, 0xFC, &[SLOT_A, 0x17C8]); + fifo_token(&m, 0xFD, &[0x17C8]); + fifo_dma(&m, 0x0B, 0x1); // run, pool 0, host to board + fifo_token(&m, 0xFB, &[SLOT_C, 0x17C7]); + fifo_token(&m, 0xFA, &[]); + m.state_hash(); + assert!(mem.bytes.lock().len() > 1000, "the slot reached host memory"); + switch_to_gl_context(&m, SLOT_C, 400, 0, 100, 100); + fifo_token(&m, 0x15, &[]); + assert_eq!(m.fb_pixel(450, 1023 - 50), 0x00_00FF, "A's clear colour, from its new slot"); + m.stop_engines(); +} + +/// Display lists as libGLcore lays them out (traced): each segment starts +/// with its link, the body following, and the engine runs the body before +/// the link. Here: a segment sets red and calls list 1 (DL_PUSH of the +/// return segment, DL_JUMP to the list), list 1 draws the full window and +/// returns, the return segment sets green and returns to the FIFO; then a +/// quad split over two chained segments (DL_JUMP) in the current colour. +#[test] +fn gl_display_list_calls_and_chains_segments() { + let m = gl_board([0.0, 0.0, 0.0]); + let mem = Arc::new(TestMem::default()); + mem.words.lock().insert(0x1000, 2); // logical page 0 = frame 2 (0x2000) + let put = |at: u32, ws: &[u32]| { + for (i, w) in ws.iter().enumerate() { + mem.words.lock().insert(0x2000 + at + 4 * i as u32, *w); + } + }; + let (push, jump, ret) = (0x83708, 0x80C08, 0x83904); + let quad = |x0: f32, y0: f32, x1: f32, y1: f32| -> Vec { + let mut v = vec![0x1D00]; + for (x, y) in [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] { + v.extend([0x0C, f(x), f(y), f(0.0)]); + } + v.push(0x2700); + v + }; + // List 1 at 0: return; the full window. + let mut list1 = vec![ret, 0]; + list1.extend(quad(0.0, 0.0, 400.0, 300.0)); + put(0x000, &list1); + // Return segment at 0x200: return; green. + let back = [ret, 0, 0x0210, f(0.0), f(1.0), f(0.0), f(1.0)]; + put(0x200, &back); + // Start segment at 0x100: push 0x200, jump to list 1; red. + let start = [push, 0x200, back.len() as u32, jump, 0, list1.len() as u32, 0x0210, f(1.0), f(0.0), f(0.0), f(1.0)]; + put(0x100, &start); + // A small quad over two chained segments at 0x400 and 0x500. + let q = quad(10.0, 10.0, 50.0, 50.0); + let mut tail = vec![ret, 0]; + tail.extend(&q[9..]); + let mut head = vec![jump, 0x500, tail.len() as u32]; + head.extend(&q[..9]); + put(0x400, &head); + put(0x500, &tail); + m.set_phys(mem.clone()); + fifo_token(&m, 0x800 | 0x28, &[0, 0x1001]); + fifo_token(&m, 0x800 | 0x0C, &[0x100, start.len() as u32]); + assert_eq!(gl_px(&m, 200, 150), 0x0000FF, "list 1 ran in red"); + fifo_token(&m, 0x800 | 0x0C, &[0x400, head.len() as u32]); + assert_eq!(gl_px(&m, 30, 30), 0x00FF00, "the chained quad, green from the return segment"); + assert_eq!(gl_px(&m, 200, 150), 0x0000FF, "outside it, still red"); + m.stop_engines(); +} + +/// glBitmap as libGLcore sends it (traced, glXUseXFont text): the raster +/// position, then BITMAP's header and its rows as FIFO pixel data, bottom +/// row first, most significant bit leftmost. Set bits draw in the raster +/// colour; the raster position moves on for the next glyph. +#[test] +fn gl_bitmap_draws_set_bits_at_the_raster_position() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + fifo_token(&m, 0x38, &[f(10.0), f(10.0), f(0.0)]); + let glyph = |m: &Mgras| { + fifo_token(m, 0x94, &[0x1800F, 16, 2, f(0.0), f(0.0), f(9.0), f(0.0), 1]); + write(m, 32, CFIFO, 0x8000_0004); + write(m, 32, CFIFO, 0xC000_8000); // row 0: bits 0, 1; row 1: bit 0 + write(m, 32, CFIFO, 0); + }; + glyph(&m); + glyph(&m); + let px = |x, y| gl_px(&m, x, y); + assert_eq!([px(10, 10), px(11, 10), px(12, 10)], [0xFF_FFFF, 0xFF_FFFF, 0], "row 0"); + assert_eq!([px(10, 11), px(11, 11)], [0xFF_FFFF, 0], "row 1, above"); + assert_eq!([px(19, 10), px(20, 10), px(19, 11)], [0xFF_FFFF, 0xFF_FFFF, 0xFF_FFFF], "next glyph, 9 to the right"); + assert_eq!(px(15, 10), 0, "between the glyphs"); + m.stop_engines(); +} + +/// A colour-index GL context (INIT_CI, as gr_osview's IRIS GL window +/// sends): colours are indices and land in the pixels as 12-bit indices, +/// for the colour-index visual's colormap; the clear uses the clear index. +#[test] +fn gl_colour_index_context_draws_indices() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + fifo_token(&m, 0x0A, &[12]); + fifo_token(&m, 0xBD, &[2]); + fifo_token(&m, 0x15, &[]); + fifo_token(&m, 0x02, &[f(5.0)]); + fifo_token(&m, 0x1D, &[]); + for p in [[0.0, 0.0], [200.0, 0.0], [200.0, 300.0], [0.0, 300.0]] { + fifo_token(&m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(&m, 0x27, &[]); + assert_eq!(m.fb_pixel(100, 1023 - 150) & 0xFFF, 5, "the quad, index 5"); + assert_eq!(m.fb_pixel(300, 1023 - 150) & 0xFFF, 2, "the clear index"); + m.stop_engines(); +} + + +/// Query GE state via __MGR_RETURN_MODE (token 0x0A2) and wait for the answer. +fn gl_return_mode(m: &Mgras, addr: u32) -> u32 { + write(m, 32, 0x7000C, 1 << 17); // clear FLAG_GE_DATA + fifo_token(m, 0x0A2, &[addr, 1, 1]); + wait_flag(m, 1 << 17); + read(m, 32, 0x70014) as u32 +} + +#[test] +fn gl_raster_position_queries_valid_bias_and_update() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + + // Set a valid raster position at (50.0, 60.0, 0.0) + fifo_token(&m, 0x02, &[f(0.5), f(0.25), f(0.75), f(1.0)]); // latch color + fifo_token(&m, 0x38, &[f(50.0), f(60.0), f(0.0)]); + + // 0x2D0 should report valid with bit 2 (0x4) and bit 0 (0x1) set + let valid = gl_return_mode(&m, 0x2D0); + assert_ne!(valid & 0x4, 0, "bit 2 must be set for libGLcore"); + assert_ne!(valid & 0x1, 0, "bit 0 must be set"); + + // 0x2CD (X) and 0x2CE (Y) should be biased by 49151.5 + let rx = f32::from_bits(gl_return_mode(&m, 0x2CD)); + let ry = f32::from_bits(gl_return_mode(&m, 0x2CE)); + let rz = f32::from_bits(gl_return_mode(&m, 0x2CF)); + let rw = f32::from_bits(gl_return_mode(&m, 0x2C4)); + let dist = f32::from_bits(gl_return_mode(&m, 0x2BF)); + assert_eq!(rx, 50.0 + 49151.5); + assert_eq!(ry, 60.0 + 49151.5); + assert_eq!(rz, 0.5); // glOrtho maps z=0 to window z=0.5 + assert_eq!(rw, 1.0); + assert_eq!(dist, 0.0); + + // Raster color readback at 0x2C5..=0x2C8 + let cr = f32::from_bits(gl_return_mode(&m, 0x2C5)); + let cg = f32::from_bits(gl_return_mode(&m, 0x2C6)); + assert_eq!(cr, 0.5); + assert_eq!(cg, 0.25); + + // UPDATE_RASTER_POS (0x0D6) advances raster by (dx, dy) + fifo_token(&m, 0x0D6, &[f(15.0), f(25.0)]); + let rx2 = f32::from_bits(gl_return_mode(&m, 0x2CD)); + let ry2 = f32::from_bits(gl_return_mode(&m, 0x2CE)); + assert_eq!(rx2, 65.0 + 49151.5); + assert_eq!(ry2, 85.0 + 49151.5); + + // LOAD_RASTER_POS_INFO (0x0D7) restores current color to raster_color + fifo_token(&m, 0x02, &[f(0.0), f(0.0), f(1.0), f(1.0)]); // blue + fifo_token(&m, 0x0D7, &[]); // restore to raster color (red/green) + + // Invalid raster position (outside clip volume) + fifo_token(&m, 0x38, &[f(500.0), f(60.0), f(0.0)]); + let valid_out = gl_return_mode(&m, 0x2D0); + assert_eq!(valid_out, 0, "clipped raster position must be invalid"); + + // Scissor readback at 0x200 and 0x202 + fifo_token(&m, 0x39, &[10, 100, 20, 200]); + let sc_x = gl_return_mode(&m, 0x200); + let sc_y = gl_return_mode(&m, 0x202); + assert_eq!(sc_x, (100 << 16) | 10); + assert_eq!(sc_y, (200 << 16) | 20); + + m.stop_engines(); +} + +#[test] +fn gl_colour_index_mask_applies_to_clear() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + fifo_token(&m, 0x0A, &[12]); + fifo_token(&m, 0xBD, &[0x0AA]); + fifo_token(&m, 0x15, &[]); // clear to 0x0AA + assert_eq!(m.fb_pixel(100, 1023 - 100) & 0xFFF, 0x0AA); + + // Apply glIndexMask(0x0F0) and clear with 0x055: only the high nibble changes + fifo_token(&m, 0x3D, &[0x0F0]); + fifo_token(&m, 0xBD, &[0x055]); + fifo_token(&m, 0x15, &[]); + // High nibble was 0xA, becomes 0x5; low nibble remains 0xA: 0x05A + assert_eq!(m.fb_pixel(100, 1023 - 100) & 0xFFF, 0x05A); + m.stop_engines(); +} + +/// glDrawPixels as mandel sends it (traced, colour index, GL_UNSIGNED_SHORT): +/// the transfer mode through RSS_REG_SET (0x07F, register/value pairs as +/// pixel data), the raster position, then SEND_PIXELS (0x08D) with the rows +/// as pixel data. The pixels land at the raster position, left to right. +#[test] +fn gl_draw_pixels_colour_index_shorts() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + fifo_token(&m, 0x0A, &[12]); + fifo_token(&m, 0xBD, &[0]); + fifo_token(&m, 0x15, &[]); + fifo_token(&m, 0x7F, &[1, 2]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x159); + write(&m, 32, CFIFO, 0x00C0_0001); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_token(&m, 0x8D, &[2, 0, 0, 1, 0, 1, 0x49D0, 0x18]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x0005_0006); + write(&m, 32, CFIFO, 0x0007_0008); + let px = |x: usize| m.fb_pixel(x, 1023 - 20) & 0xFFF; + assert_eq!([px(10), px(11), px(12), px(13), px(14)], [5, 6, 7, 8, 0]); + m.stop_engines(); +} + +/// Pixel zoom (snoop's magnifier): the GE pixel state at 0x192 holds +/// 1 / zoom (token 0x080, traced: 1/6 at zoom 6); each pixel and row of a +/// SEND_PIXELS image is repeated. +#[test] +fn gl_draw_pixels_zoomed() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + fifo_token(&m, 0x0A, &[12]); + fifo_token(&m, 0xBD, &[0]); + fifo_token(&m, 0x15, &[]); + fifo_token(&m, 0x7F, &[1, 2]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x159); + write(&m, 32, CFIFO, 0x00C0_0001); + fifo_token(&m, 0x80, &[0, 2, 8, 0x191, 0, 2, 2, 0, f(0.5)]); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_token(&m, 0x8D, &[1, 0, 0, 1, 0, 1, 0x49D0, 0x18]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x0005_0006); + write(&m, 32, CFIFO, 0); + let px = |x: usize, y: usize| m.fb_pixel(x, 1023 - y) & 0xFFF; + assert_eq!([px(10, 20), px(11, 20), px(12, 20), px(13, 20), px(14, 20)], [5, 5, 6, 6, 0]); + assert_eq!([px(10, 21), px(13, 21), px(10, 22)], [5, 6, 0]); + m.stop_engines(); +} + +/// glReadPixels as snoop sends it (traced, one pixel): the read block and +/// transfer size through RSS_REG_SHADOW (0x07C triples), the transfer +/// counters and mode through RSS_REG_SET, GET_PIXELS (0x0DB), then the +/// host arms a board-to-host DMA and starts the raster side through raster +/// interface register 5 (0x4009). +#[test] +fn gl_read_pixels_to_host_memory() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_color4(&m, [1.0, 0.5, 0.25, 1.0]); + gl_full_quad(&m); + let px = gl_px(&m, 30, 40); + assert_ne!(px, 0); + fifo_token(&m, 0xD2, &[0x11E2_3929, 0x100_0000, 0xFF, 0, 2]); + fifo_token(&m, 0x7C, &[1, 4]); + write(&m, 32, CFIFO, 0x8000_0010); + for w in [0x153, 0x0001_0001, 0x226, 0x47] { + write(&m, 32, CFIFO, w); + } + fifo_token(&m, 0x7C, &[2, 6]); + write(&m, 32, CFIFO, 0x8000_0018); + for w in [0x46, 30 << 16 | 40, 0x28E, 0x47, 30 << 16 | 40, 0x290] { + write(&m, 32, CFIFO, w); + } + fifo_token(&m, 0x7F, &[2, 4]); + write(&m, 32, CFIFO, 0x8000_0010); + for w in [0x158, 0x0001_0001, 0x159, 0x0088_0080] { + write(&m, 32, CFIFO, w); + } + fifo_token(&m, 0xDB, &[0x4009]); + let mem = eram_dma_setup(&m, 4); + fifo_dma(&m, 0x0B, 0x9); + fifo_token(&m, 0xA05, &[0x4009]); + fifo_token(&m, 0xD3, &[]); + m.state_hash(); + let b = mem.bytes.lock(); + let got: Vec = (0..4).map(|i| b.get(&(0x2000 + i)).copied().unwrap_or(0)).collect(); + assert_ne!(got, [0, 0, 0, 0], "the pixel reached host memory (window pixel {px:#x})"); + drop(b); + m.stop_engines(); +} + +/// SEND_PIXELS honours the scissor box (the desks overview draws each +/// miniature under its own) and shrinking zooms (nearest source pixel). +#[test] +fn gl_draw_pixels_scissored_and_shrunk() { + let m = live_board(); + x_server(&m); + gl_window_400x300(&m); + fifo_token(&m, 0x0A, &[12]); + fifo_token(&m, 0xBD, &[0]); + fifo_token(&m, 0x15, &[]); + fifo_token(&m, 0x7F, &[1, 2]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x159); + write(&m, 32, CFIFO, 0x00C0_0001); + // Zoom 1/2: four pixels 1, 2, 3, 4 become two, 1 and 3. + fifo_token(&m, 0x80, &[0, 2, 8, 0x191, 0, 2, 2, 0, f(2.0)]); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_token(&m, 0x8D, &[2, 0, 0, 1, 0, 1, 0x49D0, 0x18]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x0001_0002); + write(&m, 32, CFIFO, 0x0003_0004); + let px = |x: usize, y: usize| m.fb_pixel(x, 1023 - y) & 0xFFF; + assert_eq!([px(10, 20), px(11, 20), px(12, 20)], [1, 3, 0], "shrunk by 2"); + // Zoom 1, scissor x 31..32: only the middle two pixels of four. + fifo_token(&m, 0x80, &[0, 2, 8, 0x191, 0, 2, 2, 0, f(1.0)]); + fifo_token(&m, 0x39, &[31, 1, 0, 299]); + fifo_token(&m, 0x6E, &[1]); + fifo_token(&m, 0x38, &[f(30.0), f(20.0), f(0.0)]); + fifo_token(&m, 0x8D, &[2, 0, 0, 1, 0, 1, 0x49D0, 0x18]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, 0x0001_0002); + write(&m, 32, CFIFO, 0x0003_0004); + assert_eq!([px(30, 20), px(31, 20), px(32, 20), px(33, 20)], [0, 2, 3, 0], "scissored"); + m.stop_engines(); +} + +/// A GL client's pixel operation sets raster registers through RSS_REG_SET +/// (CONFIG among them, Y-flip off); the X server's must come back after +/// RESTORE_RSS. They leaked: 4Dwm drew snoop's frame with Y-flip off. +#[test] +fn gl_pixel_op_registers_do_not_leak_to_x() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0xD2, &[0x1E0_3929, 0x140_0000, 0, 0, 3]); + fifo_token(&m, 0x7F, &[1, 2]); + write(&m, 32, CFIFO, 0x8000_0008); + write(&m, 32, CFIFO, CONFIG as u64); + write(&m, 32, CFIFO, 0x4); + fifo_token(&m, 0xD3, &[]); + m.state_hash(); + direct_rss(&m, FILLMODE, FILL_FAST, false); + direct_rss(&m, FILL_COLOR_R, 0x123, false); + block(&m, 5, 5, 5, 5); + m.state_hash(); + assert_eq!(m.fb_pixel(5, 5) & 0xFFF, 0x123, "the X server's fill lands top-down, Y-flip back"); + m.stop_engines(); +} + +/// The X server sets the IR only when its operation changes: back from a +/// GL batch it executes with the IR as it left it (traced: 4Dwm's lines +/// after a context switch from gltest). GL's triangle opcode leaked into +/// it, and each X line redrew GL's last triangle in X's window. +#[test] +fn gl_batch_leaves_x_instruction_alone() { + let m = gl_board([0.0, 0.0, 0.0]); + direct_rss(&m, FILLMODE, FILL_FAST, false); + direct_rss(&m, FILL_COLOR_R, 0x123, false); + direct_rss(&m, IR_ALIAS, 0x18, false); + gl_color4(&m, [0.0, 1.0, 0.0, 1.0]); + gl_full_quad(&m); + fifo_token(&m, 0x36, &[]); + m.state_hash(); + direct_rss(&m, BLOCKXYSTARTI, 5 << 16 | 5, false); + direct_rss(&m, BLOCKXYENDI, 5 << 16 | 5, true); + m.state_hash(); + assert_eq!(m.fb_pixel(5, 5) & 0xFFF, 0x123, "X's block, with X's instruction"); + assert_eq!(m.fb_pixel(300, 5) & 0xFF_FFFF, 0, "no GL quad in X's window"); + m.stop_engines(); +} + +// ---- texture (TE1) --------------------------------------------------------- + +/// FIFO pixel data: one pixel command carrying `words`, padded to whole +/// doublewords as the HQ takes them. +fn fifo_pixel_data(m: &Mgras, words: &[u32]) { + write(m, 32, CFIFO, 0x8000_0000 | 4 * words.len() as u64); + for w in words { + write(m, 32, CFIFO, *w as u64); + } + if words.len() % 2 == 1 { + write(m, 32, CFIFO, 0); + } +} + +/// An RSS_REG_SHADOW list (0x07C): (register, value) with a GE shadow +/// address each, padded to whole doublewords, as libGLcore sends them. +fn rss_shadow_list(m: &Mgras, regs: &[(u32, u32)]) { + let mut words: Vec = regs.iter().flat_map(|&(r, v)| [r, v, 0x200 + r]).collect(); + if words.len() % 2 == 1 { + words.push(0x189); + } + fifo_token(m, 0x7C, &[regs.len() as u32, words.len() as u32]); + fifo_pixel_data(m, &words); +} + +/// glTexImage2D of one level of a square RGBA8 texture as libGLcore sends +/// it (traced, IRIX 6.5.22, 4 TRAMs): loader registers, SAVE_RSS, the +/// sub-image (0xDA8) and destination (0xDB1: width, ?, level, page | bank +/// << 8) pixel state, SEND_PIXELS routine 0x511A and the texels. +fn gl_tex_level(m: &Mgras, level: u32, n: u32, dest: u32, texels: &[u32]) { + gl_tex_level_as(m, level, n, dest, texels, 1, 0.0, 0x34E26); +} + +/// `gl_tex_level` for host elements `elem` (pixel state 0xDBB: 1 bytes, 3 +/// 16-bit, 4 32-bit) with component scale `scale` (0xD0A; 0 leaves it). +fn gl_tex_level_as(m: &Mgras, level: u32, n: u32, dest: u32, texels: &[u32], elem: u32, scale: f32, tl_mode: u32) { + let log = n.trailing_zeros(); + fifo_token(m, 0x0C, &[]); + rss_shadow_list(m, &[(0x1A0, 0), (0x18B, 0), (0x111, 0x40858)]); + rss_shadow_list(m, &[(0x1A1, tl_mode)]); + rss_shadow_list(m, &[(0x1A2, log << 8 | log << 4 | level)]); + fifo_token(m, 0xD2, &[0xFFFF_FFFF, 0x0140_0000, 0, 0, 3]); + fifo_token(m, 0xCD, &[1, 1, 0x14, 0xDA8, 0, 2, 5, 0, 0, n, n, n << 16 | n, 0]); + fifo_token(m, 0xCD, &[1, 2, 0x18, 0xDB1, 0, 2, 6, n, 0, level, dest, 0, 0, 0]); + fifo_token(m, 0xCD, &[1, 2, 0x18, 0xDB7, 0, 2, 6, 0, 0, 0, 0, elem, 0, 0]); + if scale != 0.0 { + fifo_token(m, 0xCD, &[1, 1, 4, 0xD0A, 0, 2, 1, f(scale), 0]); + } + fifo_token(m, 0x8D, &[texels.len() as u32, 0, 0, 1, 0, 1, 0x511A, 0x98]); + fifo_pixel_data(m, texels); + fifo_token(m, 0xD3, &[]); +} + +/// A 4x4 RGBA8 texture at page 0: texel (s, t) = (s * 0x40, t * 0x40, +/// 0xA5, 0xFF). +fn gl_tex_image_4x4(m: &Mgras) { + let texels: Vec = (0..16).map(|i| (i % 4) * 0x40 << 24 | (i / 4) * 0x40 << 16 | 0xA5FF).collect(); + gl_tex_level(m, 0, 4, 0, &texels); +} + +/// Texturing on for what follows, as for a draw (traced): TEX_ON, the +/// sampler registers, level 0 at page 0 through RSS_REG_SHADOW_IDX. +fn gl_tex_bind_4x4(m: &Mgras, texmode2: u32, texmode1: u32) { + fifo_token(m, 0x0B, &[]); + rss_shadow_list(m, &[(0x180, texmode2)]); + rss_shadow_list(m, &[(0x111, texmode1)]); + rss_shadow_list(m, &[(0x184, 0), (0x185, 0), (0x190, 0)]); + fifo_token(m, 0xEC, &[0xBC6, 0xBC7, 0x191, 0]); + rss_shadow_list(m, &[(0x181, 0x2222), (0x183, 1), (0x18C, 0x8004)]); +} + +/// A quad over window (100..300, 100..200) with texture coordinates 0..1. +fn gl_tex_quad(m: &Mgras) { + fifo_token(m, 0x1D, &[]); + for (t, p) in [([0.0, 0.0], [100.0, 100.0]), ([1.0, 0.0], [300.0, 100.0]), ([1.0, 1.0], [300.0, 200.0]), ([0.0, 1.0], [100.0, 200.0])] { + fifo_token(m, 0x08, &[f(t[0]), f(t[1])]); + fifo_token(m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(m, 0x27, &[]); +} + +/// A texture loaded through the texture loader and drawn nearest with +/// modulate (libGLcore's GL_REPLACE) on a white quad: each texel covers a +/// 50 x 25 pixel block, in GL's orientation (s right, t up). +#[test] +fn gl_texture_nearest_quad() { + let m = gl_board([1.0, 0.5, 0.75]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + for (s, t) in [(0, 0), (3, 0), (0, 3), (2, 1)] { + let (x, y) = (100 + 50 * s + 25, 100 + 25 * t + 12); + assert_eq!(gl_px(&m, x, y), 0xA5_0000 | (t as u32 * 0x40) << 8 | s as u32 * 0x40, "texel ({s}, {t}) at ({x}, {y})"); + } + assert_eq!(gl_px(&m, 50, 50), 0xBF_80FF, "clear colour outside"); + m.stop_engines(); +} + +/// A tilted textured quad, larger than the view so the clipper cuts it +/// into a fan, at `scale` times the distance and size: the same picture +/// at any scale. Returns the window's pixels. +fn far_quad_pixels(scale: f32) -> Vec { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + fifo_token(&m, 0x2A, &[0x1701]); + fifo_token(&m, 0x2C, &[]); + fifo_token(&m, 0x34, &[f(-1.0), f(1.0), f(-0.75), f(0.75), f(1.0), f(1.0e8)]); + fifo_token(&m, 0x2A, &[0x1700]); + fifo_token(&m, 0x2C, &[]); + // Far enough that only the sides clip it (the near plane does not scale). + fifo_token(&m, 0x32, &[f(0.0), f(0.0), f(-4.0 * scale)]); + fifo_token(&m, 0x30, &[f(-60.0), f(1.0), f(0.0), f(0.0)]); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + fifo_token(&m, 0x1F, &[]); + for (t, p) in [([0.0, 0.0], [-3.0, -3.0]), ([3.0, 0.0], [3.0, -3.0]), ([3.0, 3.0], [3.0, 3.0]), ([0.0, 3.0], [-3.0, 3.0])] { + fifo_token(&m, 0x08, &[f(t[0]), f(t[1])]); + fifo_token(&m, 0x00, &[f(p[0] * scale), f(p[1] * scale), f(0.0)]); + } + fifo_token(&m, 0x29, &[]); + m.state_hash(); + let px = (0..300).flat_map(|y| (0..400).map(move |x| (x, y))).map(|(x, y)| gl_px(&m, x, y)).collect(); + m.stop_engines(); + px +} + +/// Far geometry (blast's nebula, W ~ 5e5) keeps its texture mapping: the +/// fixed-point S/W, T/W, Q/W planes are normalised per primitive. Without +/// that, 1/W's steps were one or two LSBs and each triangle of the clipped +/// quad mapped the texture differently. +#[test] +fn gl_texture_far_quad_maps_as_near() { + let (near, far) = (far_quad_pixels(1.0), far_quad_pixels(1.0e6)); + let textured = near.iter().filter(|&&p| p != 0).count(); + // Texel edges may move by a pixel: a far pixel is wrong when no near + // pixel within one matches it. + let at = |v: &[u32], x: i32, y: i32| v.get((y.clamp(0, 299) * 400 + x.clamp(0, 399)) as usize).copied(); + let differ = (0..300 * 400) + .filter(|&i| { + let (x, y) = (i % 400, i / 400); + !(-1..=1).any(|dy| (-1..=1).any(|dx| at(&near, x + dx, y + dy) == Some(far[i as usize]))) + }) + .count(); + assert!(textured > 60_000, "the quad covers most of the window ({textured})"); + assert!(differ < textured / 200, "{differ} of {textured} pixels differ"); +} + +/// SGIS_texture_select (traced, TyrQuake's lightmaps): one-component +/// textures share cells, each in the slot TL_MODE bits 4:3 give at load +/// and TEXMODE1 bits 8:7 when drawn. Two luminance textures on the same +/// page keep their own texels. +#[test] +fn gl_texture_select_slots_share_a_page() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_level_as(&m, 0, 4, 0, &[0x4040_4040; 4], 1, 0.0, 0x34020); + gl_tex_level_as(&m, 0, 4, 0, &[0xC0C0_C0C0; 4], 1, 0.0, 0x34028); + for (slot, want) in [(0u32, 0x40_4040), (1, 0xC0_C0C0)] { + // Luminance class (2), one component, the slot in bits 8:7. + gl_tex_bind_4x4(&m, 0x10004, 0x40441 | slot << 7); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 125, 112), want, "slot {slot}"); + } + m.stop_engines(); +} + +/// The select bits mean nothing for textures that fill the cell: TyrQuake +/// draws RGBA textures with TEXMODE1 0x409D9 (bits 8:7 = 3). +#[test] +fn gl_texture_select_ignored_for_rgba() { + let m = gl_board([1.0, 0.5, 0.75]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859 | 3 << 7); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 100 + 50 * 2 + 25, 100 + 25 + 12), 0xA5_0000 | 0x40 << 8 | 2 * 0x40); + m.stop_engines(); +} + +/// GL_UNSIGNED_SHORT_4_4_4_4_EXT texels (SGI quake's lightmaps): 16-bit +/// elements at an 8-bit scale, each nibble a component, R on top. +#[test] +fn gl_texture_packed_4444() { + let m = gl_board([1.0, 0.5, 0.75]); + let t16 = |i: u32| (i % 4) * 4 << 12 | (i / 4) * 4 << 8 | 0xAF; + let words: Vec = (0..8).map(|k| t16(2 * k) << 16 | t16(2 * k + 1)).collect(); + gl_tex_level_as(&m, 0, 4, 0, &words, 3, 1.0 / 255.0, 0x34E26); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + for (s, t) in [(0, 0), (3, 0), (0, 3), (2, 1)] { + let (x, y) = (100 + 50 * s + 25, 100 + 25 * t + 12); + assert_eq!(gl_px(&m, x, y), 0xAA_0000 | (t as u32 * 0x44) << 8 | s as u32 * 0x44, "texel ({s}, {t})"); + } + m.stop_engines(); +} + +/// Modulate with a coloured fragment, and texturing off again: TEX_OFF +/// draws plain colour even with the texture still bound. +#[test] +fn gl_texture_modulate_and_off() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + gl_color4(&m, [1.0, 0.0, 1.0, 1.0]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 100 + 150 + 25, 100 + 75 + 12), 0xA5_00C0, "texel (3, 3) times magenta"); + fifo_token(&m, 0x0C, &[]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 100 + 150 + 25, 100 + 75 + 12), 0xFF_00FF, "untextured"); + m.stop_engines(); +} + +/// Bilinear magnification (TEXMODE2 bit 7): halfway between two texel +/// centres is their average; at a centre, the texel. +#[test] +fn gl_texture_bilinear_magnification() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10084, 0x40859); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + // Texel centres at x = 125, 175 ... and y = 112.5, 137.5 ...; at x = + // 200, between texels (1, 1) and (2, 1), the red ramp is halfway: 0x60. + let mid = gl_px(&m, 200, 137); + assert!(((mid & 0xFF) as i32 - 0x60).abs() <= 3, "{mid:#x}"); + let centre = gl_px(&m, 175, 137); + assert!(((centre & 0xFF) as i32 - 0x40).abs() <= 3, "{centre:#x}"); + m.stop_engines(); +} + +/// A second context with no texture state draws untextured, and the first +/// gets its texture back when it returns: the TE is loaded from each +/// context's own shadow, TRAM is shared. +#[test] +fn gl_texture_state_is_per_context() { + let m = live_board(); + x_server(&m); + switch_to_gl_context(&m, SLOT_A, 0, 0, 400, 300); + gl_ortho_400x300(&m); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + switch_to_gl_context(&m, SLOT_B, 0, 0, 400, 300); + gl_ortho_400x300(&m); + fifo_token(&m, 0x0B, &[]); + gl_color4(&m, [0.0, 1.0, 0.0, 1.0]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 125, 112), 0x00_FF00, "plain green in context B"); + switch_to_gl_context(&m, SLOT_A, 0, 0, 400, 300); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 125, 112), 0xA5_0000, "texel (0, 0) in context A"); + assert_eq!(gl_px(&m, 275, 187), 0xA5_C0C0, "texel (3, 3) in context A"); + m.stop_engines(); +} + +/// Mipmaps: level 0 (4x4) at page 0, level 1 (2x2, flat grey) at page 1 +/// bank 1, each placed by its own destination. Magnified the quad shows +/// level 0; drawn at 2x2 pixels (two texels a pixel, lambda 1) level 1. +#[test] +fn gl_texture_mipmap_levels_from_their_pages() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + gl_tex_level(&m, 1, 2, 0x101, &[0x8080_80FF; 4]); + fifo_token(&m, 0x0B, &[]); + rss_shadow_list(&m, &[(0x180, 0x90024), (0x111, 0x40859), (0x190, 0)]); + fifo_token(&m, 0xEC, &[0xBC6, 0xBC7, 0x191, 0]); + fifo_token(&m, 0xEC, &[0xBC6, 0xBC7, 0x191, 0x101]); + rss_shadow_list(&m, &[(0x181, 0x2222), (0x183, 1)]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 275, 187), 0xA5_C0C0, "level 0, texel (3, 3)"); + fifo_token(&m, 0x1D, &[]); + for (t, p) in [([0.0, 0.0], [10.0, 10.0]), ([1.0, 0.0], [12.0, 10.0]), ([1.0, 1.0], [12.0, 12.0]), ([0.0, 1.0], [10.0, 12.0])] { + fifo_token(&m, 0x08, &[f(t[0]), f(t[1])]); + fifo_token(&m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(&m, 0x27, &[]); + assert_eq!(gl_px(&m, 11, 11), 0x80_8080, "level 1"); + m.stop_engines(); +} + +/// distort's texture load (traced): 16-bit components (pixel format scale +/// 1/65535 at ERAM 0xD0A), a one-texel border, the sub-image starting at +/// (-1, -1), rows SEND_PIXELS' words-per-row apart. The interior lands in +/// the level, the border is dropped, the top byte of each component kept. +#[test] +fn gl_texture_rgba16_with_border() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x0C, &[]); + rss_shadow_list(&m, &[(0x1A0, 0), (0x18B, 0), (0x111, 0x40858)]); + rss_shadow_list(&m, &[(0x1A1, 0x35A26)]); + rss_shadow_list(&m, &[(0x1A2, 0x220)]); + fifo_token(&m, 0xD2, &[0xFFFF_FFFF, 0x0140_0000, 0, 0, 3]); + fifo_token(&m, 0xCD, &[1, 2, 0x18, 0xDB1, 0, 2, 6, 4, 0, 0, 0, 0, 0, 0]); + fifo_token(&m, 0xCD, &[1, 2, 0x18, 0xD05, 0, 2, 6, 0x20C, 0x4064, 0x50C, 0xA, 0x14, 0x3780_0080, 0]); + fifo_token(&m, 0xCD, &[1, 1, 0x14, 0xDA8, 0, 2, 5, 0xFFFF_FFFF, 0xFFFF_FFFF, 6, 6, 0x60006, 0]); + // 6 rows of 6 texels, 8 bytes each: 12 words a row. + let mut words = Vec::new(); + for t in -1i32..5 { + for s in -1i32..5 { + let border = !(0..4).contains(&s) || !(0..4).contains(&t); + let (r, g, b) = if border { (0xFFFF, 0xFFFF, 0xFFFF) } else { (s as u32 * 0x4000 + 0xFF, t as u32 * 0x4000, 0xA5A5) }; + words.push(r << 16 | g); + words.push(b << 16 | 0xFFFF); + } + } + fifo_token(&m, 0x8D, &[12, 0, 0, 0, 2, 1, 0x511A, 0x98]); + for row in words.chunks(12) { + fifo_pixel_data(&m, row); + } + fifo_token(&m, 0xD3, &[]); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + assert_eq!(gl_px(&m, 125, 112), 0xA5_0000, "texel (0, 0)"); + assert_eq!(gl_px(&m, 275, 187), 0xA5_C0C0, "texel (3, 3)"); + assert_eq!(gl_px(&m, 175, 137), 0xA5_4040, "texel (1, 1)"); + m.stop_engines(); +} + +/// The HQ formatter's swizzle (traced: GL_ABGR_EXT texture loads set the +/// mode to 0xE8D for bytes, 0xE8E for shorts, around the DMA): component +/// order reversed within each pixel, each component's bytes kept; the +/// canonical mode 0xE00 leaves data alone. +#[test] +fn formatter_swizzle_reverses_components() { + let mut b = [0xFF, 0xA5, 0x00, 0x20, 1, 2, 3, 4]; + super::hq3::format_pixels(0xE8D, &mut b); + assert_eq!(b, [0x20, 0x00, 0xA5, 0xFF, 4, 3, 2, 1]); + let mut s = [0x00, 0x00, 0x0F, 0x0F, 0x1B, 0x1B, 0x22, 0x21]; + super::hq3::format_pixels(0xE8E, &mut s); + assert_eq!(s, [0x22, 0x21, 0x1B, 0x1B, 0x0F, 0x0F, 0x00, 0x00]); + let mut c = [1, 2, 3, 4]; + super::hq3::format_pixels(0xE00, &mut c); + assert_eq!(c, [1, 2, 3, 4]); +} + +/// A quad over window (100..300, 100..200) with texture coordinates s0..s1 +/// and t0..t1. +fn gl_tex_quad_st(m: &Mgras, s: [f32; 2], t: [f32; 2]) { + fifo_token(m, 0x1D, &[]); + for (tc, p) in [([s[0], t[0]], [100.0, 100.0]), ([s[1], t[0]], [300.0, 100.0]), ([s[1], t[1]], [300.0, 200.0]), ([s[0], t[1]], [100.0, 200.0])] { + fifo_token(m, 0x08, &[f(tc[0]), f(tc[1])]); + fifo_token(m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(m, 0x27, &[]); +} + +fn near(v: u32, want: [i32; 3]) -> bool { + [v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF].iter().zip(want).all(|(&c, w)| (c as i32 - w).abs() <= 3) +} + +/// GL_CLAMP with linear filtering and no border (TEXMODE2 0x15984, +/// traced): left of s = 0 the filter blends texel 0 half and half with the +/// border colour (TXBCOLOR, green here). +#[test] +fn gl_texture_clamp_blends_the_border_colour() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x15984, 0x40859); + rss_shadow_list(&m, &[(0x184, 0xFFF000), (0x185, 0xFFF000)]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad_st(&m, [-1.0, 2.0], [0.0, 1.0]); + // y = 137: t = 0.375, exactly texel row 1 (green 0x40). + let v = gl_px(&m, 105, 137); + assert!(near(v, [0, (255 + 0x40) / 2, 0xA5 / 2]), "{v:#x}"); + m.stop_engines(); +} + +/// A texture with a one-texel border (magenta), loaded from (-1, -1), its +/// border texels on their own page (0xDB1's border page): with GL_CLAMP +/// and linear filtering the edge blends with the border texels. +#[test] +fn gl_texture_border_texels() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x0C, &[]); + rss_shadow_list(&m, &[(0x1A0, 0), (0x18B, 0), (0x111, 0x40858)]); + rss_shadow_list(&m, &[(0x1A1, 0x35A26)]); + rss_shadow_list(&m, &[(0x1A2, 0x220)]); + fifo_token(&m, 0xD2, &[0xFFFF_FFFF, 0x0140_0000, 0, 0, 3]); + fifo_token(&m, 0xCD, &[1, 2, 0x18, 0xDB1, 0, 2, 6, 4, 0, 0, 0, 1, 0, 0]); + fifo_token(&m, 0xCD, &[1, 1, 0x14, 0xDA8, 0, 2, 5, 0xFFFF_FFFF, 0xFFFF_FFFF, 6, 6, 0x60006, 0]); + let mut texels = Vec::new(); + for t in -1i32..5 { + for s in -1i32..5 { + let border = !(0..4).contains(&s) || !(0..4).contains(&t); + texels.push(if border { 0xFF00_FFFF } else { (s as u32 * 0x40) << 24 | (t as u32 * 0x40) << 16 | 0xA5FF }); + } + } + fifo_token(&m, 0x8D, &[18, 0, 0, 2, 0, 1, 0x511A, 0x98]); + for part in texels.chunks(18) { + fifo_pixel_data(&m, part); + } + fifo_token(&m, 0xD3, &[]); + gl_tex_bind_4x4(&m, 0x5984, 0x40859); + rss_shadow_list(&m, &[(0x190, 0)]); + fifo_token(&m, 0xEC, &[0xBC6, 0xBC7, 0x193, 1]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad_st(&m, [-1.0, 2.0], [0.0, 1.0]); + let v = gl_px(&m, 105, 137); + assert!(near(v, [0xFF / 2, 0x40 / 2, (0xFF + 0xA5) / 2]), "{v:#x}"); + // s = 0.375 at x = 191.5: texel (1, 1)'s centre. + let v = gl_px(&m, 191, 137); + assert!(near(v, [0x40, 0x40, 0xA5]), "interior texel (1, 1) intact: {v:#x}"); + m.stop_engines(); +} + +/// Object-linear texgen (traced: TEX_GENIV, plane header and data, enable +/// token 0x075 with GL_TEXTURE_GEN_S / T): planes map the quad to 0..1, the +/// vertices' own coordinates (0) are ignored. +#[test] +fn gl_texgen_object_linear() { + let m = gl_board([1.0, 0.5, 0.75]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + for (c, plane) in [(0x2000, [1.0 / 200.0, 0.0, 0.0, -0.5]), (0x2001, [0.0, 1.0 / 100.0, 0.0, -1.0])] { + fifo_token(&m, 0x5A, &[c, 0x2500, 0x2401]); + fifo_token(&m, 0x5B, &[c, 0x2501]); + fifo_token(&m, 0x5C, &plane.map(f)); + fifo_token(&m, 0x75, &[c - 0x2000 + 0xC60]); + } + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad_st(&m, [0.0, 0.0], [0.0, 0.0]); + for (s, t) in [(0, 0), (3, 0), (0, 3), (2, 1)] { + let (x, y) = (100 + 50 * s + 25, 100 + 25 * t + 12); + assert_eq!(gl_px(&m, x, y), 0xA5_0000 | (t as u32 * 0x40) << 8 | s as u32 * 0x40, "texel ({s}, {t})"); + } + m.stop_engines(); +} + +/// glPointSize 4 (token 0x04D): a 4x4 square around the point. +#[test] +fn gl_point_size_draws_a_square() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x4D, &[f(4.0)]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + fifo_token(&m, 0x16, &[]); + fifo_token(&m, 0x00, &[f(50.5), f(50.5), f(0.0)]); + fifo_token(&m, 0x20, &[]); + for (x, y, on) in [(49, 49, true), (52, 52, true), (49, 52, true), (48, 50, false), (53, 50, false), (50, 48, false), (50, 53, false)] { + assert_eq!(gl_px(&m, x, y) != 0, on, "({x}, {y})"); + } + // Size 3 at (100.5, 100.5): pixels 99..101 each way. + fifo_token(&m, 0x4D, &[f(3.0)]); + fifo_token(&m, 0x16, &[]); + fifo_token(&m, 0x00, &[f(100.5), f(100.5), f(0.0)]); + fifo_token(&m, 0x20, &[]); + for (x, y, on) in [(99, 99, true), (101, 101, true), (98, 100, false), (102, 100, false), (100, 98, false), (100, 102, false)] { + assert_eq!(gl_px(&m, x, y) != 0, on, "size 3: ({x}, {y})"); + } + m.stop_engines(); +} + +/// A textured line: texture coordinates along it, texel by texel. +#[test] +fn gl_textured_line() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + fifo_token(&m, 0x17, &[]); + for (s, x) in [(0.0, 100.0), (1.0, 300.0)] { + fifo_token(&m, 0x08, &[f(s), f(0.375)]); + fifo_token(&m, 0x00, &[f(x), f(50.5), f(0.0)]); + } + fifo_token(&m, 0x21, &[]); + for k in 0..4u32 { + assert_eq!(gl_px(&m, 125 + 50 * k as usize, 50), 0xA5_4000 | k * 0x40, "texel ({k}, 1)"); + } + m.stop_engines(); +} + +/// Fog with texturing is applied after the texture environment: linear +/// fog 0..1, grey, the quad at eye depth 0.5 (factor 0.5) blends texel +/// (0, 0) half and half with the fog colour. +#[test] +fn gl_fog_after_texture() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + fifo_token(&m, 0xB9, &[0x2601]); + fifo_token(&m, 0xB5, &[f(0.0)]); + fifo_token(&m, 0xB6, &[f(1.0)]); + fifo_token(&m, 0xB4, &[f(0.5), f(0.5), f(0.5), f(1.0)]); + fifo_token(&m, 0x68, &[1]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + fifo_token(&m, 0x1D, &[]); + for (tc, p) in [([0.0, 0.0], [100.0, 100.0]), ([1.0, 0.0], [300.0, 100.0]), ([1.0, 1.0], [300.0, 200.0]), ([0.0, 1.0], [100.0, 200.0])] { + fifo_token(&m, 0x08, &[f(tc[0]), f(tc[1])]); + fifo_token(&m, 0x00, &[f(p[0]), f(p[1]), f(-0.5)]); + } + fifo_token(&m, 0x27, &[]); + let v = gl_px(&m, 125, 112); + assert!(near(v, [0x40, 0x40, (0xA5 + 0x80) / 2]), "{v:#x}"); + m.stop_engines(); +} + +/// Texgen beyond the texture with GL_REPEAT (glprim --texgen obj's +/// triangle, s 1.29..2.29 across it): the texture repeats along s; nearest +/// sampling does not stop at the last texel. +#[test] +fn gl_texgen_repeats_past_one() { + let m = gl_board([1.0, 0.5, 0.75]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x10004, 0x40859); + for (c, plane) in [(0x2000u32, [1.0f32 / 140.0, 0.0, 0.0, -0.2857]), (0x2001, [0.0, 1.0 / 180.0, 0.0, -0.3333])] { + fifo_token(&m, 0x5A, &[c, 0x2500, 0x2401]); + fifo_token(&m, 0x5B, &[c, 0x2501]); + fifo_token(&m, 0x5C, &plane.map(f)); + fifo_token(&m, 0x75, &[c - 0x2000 + 0xC60]); + } + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + fifo_token(&m, 0x1A, &[]); + for (tc, p) in [([-0.5f32, -0.5f32], [220.0f32, 60.0f32]), ([1.5, -0.5], [360.0, 60.0]), ([0.5, 1.5], [290.0, 240.0])] { + fifo_token(&m, 0x08, &[f(tc[0]), f(tc[1])]); + fifo_token(&m, 0x00, &[f(p[0]), f(p[1]), f(0.0)]); + } + fifo_token(&m, 0x24, &[]); + // y = 70: t row 0. x = 240.5: s = 1.43, texel 1; x = 300.5: s = 1.86, + // texel 3; x = 330.5: s = 2.08, texel 0 again. + for (x, red) in [(240, 0x40), (300, 0xC0), (330, 0x00)] { + assert_eq!(gl_px(&m, x, 70) & 0xFF, red, "x {x}"); + } + m.stop_engines(); +} + +/// glGetTexImage as libGLcore sends it (traced): SAVE_RSS, the sampler at +/// the level (TXSIZE, TXMIPMAP), READ_TEXTURE (0x0F0), the host DMA armed, +/// then raster interface register 5 = 0x400D starts the read. The texels +/// reach host memory as RGBA bytes, row by row. +#[test] +fn gl_texture_readback_to_host_memory() { + texture_readback(4, 16); + // libGLcore reads a small texture as one DMA line (8x8: 256 bytes). + texture_readback(1, 64); +} + +/// The texture manager moving a texture (traced, SGI quake): it saves +/// TRAM pages raw (0xE5 with the sampler on the page, 64 texels of 32 bits +/// a row) and restores them elsewhere through the loader (0x9D, TL_MODE +/// 0x4026, TL_SPEC 64 x 8192, no destination level) from a list-mode +/// host DMA. The texture then samples the same from its new page. +#[test] +fn gl_texture_save_and_restore_pages() { + texture_save_and_restore(4); +} + +/// The same for RGB8: three components of 8 bits a texel in TRAM, a 32-bit +/// cell each in the page view. +#[test] +fn gl_texture_save_and_restore_rgb8() { + texture_save_and_restore(3); +} + +fn texture_save_and_restore(nc: u32) { + let m = gl_board([0.0, 0.0, 0.0]); + let tl_mode = (nc - 1) << 1 | 1 << 5; + let texels: Vec = (0..16).map(|i| (i % 4) * 0x40 << 24 | (i / 4) * 0x40 << 16 | 0xA5FF).collect(); + gl_tex_level_as(&m, 0, 4, 0, &texels, 1, 0.0, 0x34E00 | tl_mode); + // Save page 0 (the texture's components and depth): 64 rows of 256 + // bytes into host pages 2..5. + rss_shadow_list(&m, &[(0x181, 0xD6D6), (0x183, 0), (0x111, (nc - 1) << 3), (0x180, 0x11804), (0x190, 0)]); + fifo_token(&m, 0xEC, &[0xBC6, 0xBC7, 0x191, 0]); + fifo_token(&m, 0xD2, &[0xFFFF_FFFF, 0x0100_1000, 0x1F_C0FF, 0, 0]); + // The transfer mode's texels: RGB three bytes, RGBA four (traced). + let xfrmode = if nc == 3 { 0x40_0070 } else { 0x40_0080 }; + let bytes = 64 * 64 * nc; + fifo_token(&m, 0xE5, &[64, xfrmode, 0x400D]); + let mem = eram_dma_setup(&m, bytes); + fifo_dma(&m, 0x0B, 0x9); + fifo_token(&m, 0xA05, &[0x400D]); + fifo_token(&m, 0xD3, &[]); + // Page 0 gets another texture. + gl_tex_level(&m, 0, 4, 0, &[0; 16]); + // Restore to page 3 from the four host pages, listed. + rss_shadow_list(&m, &[(0x1A1, 0x4000 | tl_mode), (0x1A2, 0xD60), (0x1A3, 0), (0x1A4, 3)]); + fifo_token(&m, 0xD2, &[0xFFFF_FFFF, 0x0140_0000, 0, 0, 3]); + fifo_token(&m, 0x7F, &[1, 2]); + fifo_pixel_data(&m, &[0x159, xfrmode]); + fifo_token(&m, 0xCD, &[1, 1, 0x14, 0xDA8, 0, 2, 5, 0, 0, 64, 64, 64 << 16 | 64, 0]); + fifo_token(&m, 0xCD, &[0, 2, 8, 0xDB1, 0, 2, 2, 0, 0]); + fifo_token(&m, 0x9D, &[0, 8, 0, 0x511A]); + write(&m, 32, CFIFO, ((0x800 << 8) | 16) as u64); + let list = if nc == 3 { [0x0000_0001u32, 0x8002_0000] } else { [0x0000_0001, 0x0002_8003] }; + for w in [list[0], list[1], 0, 0] { + write(&m, 32, CFIFO, w as u64); + } + fifo_dma(&m, 0x05, 0); + fifo_dma(&m, 0x08, 0x1000); + fifo_dma(&m, 0x09, 0x1000); + fifo_dma(&m, 0x07, 1); + fifo_dma(&m, 0x0B, 0x1); + fifo_token(&m, 0xD3, &[]); + // Draw from page 3. + gl_tex_bind_4x4(&m, 0x10004, 0x40841 | (nc - 1) << 3); + rss_shadow_list(&m, &[(0x190, 0)]); + fifo_token(&m, 0xEC, &[0xBC6, 0xBC7, 0x191, 3]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad(&m); + for (s, t) in [(0, 0), (3, 0), (0, 3), (2, 1)] { + let (x, y) = (100 + 50 * s + 25, 100 + 25 * t + 12); + assert_eq!(gl_px(&m, x, y), 0xA5_0000 | (t as u32 * 0x40) << 8 | s as u32 * 0x40, "texel ({s}, {t}), {nc} components"); + } + drop(mem); + m.stop_engines(); +} + +fn texture_readback(lines: u32, bytes: u32) { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + rss_shadow_list(&m, &[(0x183, 0), (0x18C, 0x40008), (0x1A1, 0), (0x181, 0x2222), (0x111, 0x58), (0x180, 0x10004)]); + fifo_token(&m, 0xD2, &[0xFFFF_FFFF, 0x0100_1000, 0x1F_C0FF, 0, 0]); + rss_shadow_list(&m, &[(0x190, 0)]); + fifo_token(&m, 0xEC, &[0xBC6, 0xBC7, 0x191, 0]); + fifo_token(&m, 0xF0, &[4, 4, f(0.5), f(0.5), 0x40_0080, 0x400D]); + let mem = eram_dma_setup(&m, bytes); + fifo_dma(&m, 0x07, lines); + fifo_dma(&m, 0x04, bytes); + fifo_dma(&m, 0x0B, 0x9); + fifo_token(&m, 0xA05, &[0x400D]); + fifo_token(&m, 0xD3, &[]); + m.state_hash(); + let b = mem.bytes.lock(); + for (s, t) in [(0u32, 0u32), (3, 0), (1, 2), (3, 3)] { + let at = 0x2000 + t * 16 + s * 4; + let got: Vec = (0..4).map(|i| b.get(&(at + i)).copied().unwrap_or(0)).collect(); + assert_eq!(got, [(s * 0x40) as u8, (t * 0x40) as u8, 0xA5, 0xFF], "texel ({s}, {t}), {lines} DMA lines"); + } + drop(b); + m.stop_engines(); +} + +/// glCopyPixels' write-back (traced): the host holds RGB with 16-bit +/// components (6 bytes a pixel), the transfer mode is RGBA16 (0xC00081): +/// the GE widens each pixel (alpha 1) before the raster engine takes it. +#[test] +fn gl_draw_pixels_rgb16_into_rgba16() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x7F, &[1, 2]); + fifo_pixel_data(&m, &[0x159, 0x00C0_0081]); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_token(&m, 0xCD, &[1, 2, 0x18, 0xDA8, 0, 2, 6, 0, 0, 1, 0, 0x1_0002, 1]); + fifo_token(&m, 0x8D, &[3, 0, 0, 1, 0, 1, 0x49D0, 0x98]); + fifo_pixel_data(&m, &[0xFFFF_8080, 0xBFBF_0000, 0xFFFF_0000]); + m.state_hash(); + assert_eq!(gl_px(&m, 10, 20), 0xBF_80FF, "pink"); + assert_eq!(gl_px(&m, 11, 20), 0x00_FF00, "green"); + m.stop_engines(); +} + +/// A chunk of a larger image (glCopyPixels draws back 80 pixels at a time, +/// traced): pixel state 0xDA8 places it within the image, so it lands that +/// far from the raster position. +#[test] +fn gl_draw_pixels_chunk_offset() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x7F, &[1, 2]); + fifo_pixel_data(&m, &[0x159, 0x00C0_0081]); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_token(&m, 0xCD, &[1, 2, 0x18, 0xDA8, 0, 2, 6, 0x50, 0, 0x51, 0, 0x1_0002, 1]); + fifo_token(&m, 0x8D, &[3, 0, 0, 1, 0, 1, 0x49D0, 0x98]); + fifo_pixel_data(&m, &[0xFFFF_8080, 0xBFBF_0000, 0xFFFF_0000]); + m.state_hash(); + assert_eq!(gl_px(&m, 90, 20), 0xBF_80FF, "at the raster position + 80"); + assert_eq!(gl_px(&m, 10, 20), 0, "not at the raster position"); + m.stop_engines(); +} + +/// GL_CLAMP_TO_BORDER_SGIS (TEXMODE2 clamp bits without bit 14, libGLcore's +/// mapping): past the edge the samples are all border colour, where +/// GL_CLAMP gives half border, half edge texel. +#[test] +fn gl_texture_clamp_to_border() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_tex_image_4x4(&m); + gl_tex_bind_4x4(&m, 0x11984, 0x40859); + rss_shadow_list(&m, &[(0x184, 0xFFF000), (0x185, 0xFFF000)]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + gl_tex_quad_st(&m, [-1.0, 2.0], [0.0, 1.0]); + assert_eq!(gl_px(&m, 105, 137), 0x00_FF00, "border colour only"); + m.stop_engines(); +} diff --git a/src/dev/mgras/mod.rs b/src/dev/mgras/mod.rs index 73bb82a5..153b77b9 100644 --- a/src/dev/mgras/mod.rs +++ b/src/dev/mgras/mod.rs @@ -12,7 +12,7 @@ //! | `0x60000-0x67FFF` | display control bus devices (see `dcb`) | //! | `0x68000-0x6FFFF` | per-device bus protocol registers | //! | `0x70000-0x7BFFF` | user registers: status, flags, command FIFO | -//! | `0x7C000-0x7FFFF` | raster registers, direct access (see `raster`); | +//! | `0x7C000-0x7FFFF` | raster registers, direct access (see `rss`); | //! | | the alias at `+0x1000` also executes the IR | //! //! Drawing arrives through the command FIFO as (command, data) pairs. Commands @@ -27,18 +27,48 @@ //! //! `IRIS_MGRAS_TRACE=` logs every access to the board. +//! +//! Threads (rules/mgras/DESIGN.md): the CPU thread owns the host-side +//! registers and the display control bus (`Front`). Command FIFO words and +//! direct raster writes go into `hq_fifo`; the HQ3 thread runs the frontend +//! and feeds raster writes into `rss_fifo`; the RSS thread draws. The display +//! thread scans the framebuffer out unlocked. + mod dcb; -mod raster; +mod debug; +mod disp; +mod frame; +mod ge11; +mod gl; +mod hq3; +mod pixmem; +mod plain; +mod record; +mod rss; +mod te1; + +#[cfg(test)] +#[path = "mgras_tests.rs"] +mod mgras_tests; use parking_lot::Mutex; -use std::collections::{HashMap, HashSet}; +use std::cell::UnsafeCell; +use std::collections::HashSet; use std::io::Write as IoWrite; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; +use std::mem::MaybeUninit; +use std::ptr::addr_of_mut; use std::sync::Arc; +use std::thread; -use crate::config::{ImpactSection, ImpactSlot}; +use crate::config::GraphicsBoard; use crate::dev::ng1::rex3::Renderer; -use crate::traits::{BusDevice, BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, Device, Saveable}; +use crate::gfifo::GFifo; +use crate::traits::{BusDevice, BusRead8, BusRead16, BusRead32, BusRead64, BUS_BUSY, BUS_OK, Device, Saveable}; + +use ge11::Ge11; +use hq3::{host, Hq3Engine, Hq3Regs, Hq3Sink}; +use rss::Rss; /// The graphics slot the board decodes. pub const MGRAS_SLOT_GFX_BASE: u32 = 0x1F00_0000; @@ -49,463 +79,84 @@ const MAP_SIZE: u32 = 0x10_0000; /// GIO ID: product 0x10, 32-bit ID, revision 1, GIO64, no ROM, manufacturer 1. pub const GIO_ID: u32 = 0x0005_0190; -/// Host interface register offsets. -mod host { - pub const UCODE: u32 = 0x40000; - pub const UCODE_END: u32 = 0x46000; - pub const SET_FLAGS_PRIVILEGED: u32 = 0x50008; - pub const CLEAR_FLAGS_PRIVILEGED: u32 = 0x5000C; - pub const CFIFO_PRIVILEGED: u32 = 0x50080; - pub const STATUS: u32 = 0x70000; - pub const FIFOSTATUS: u32 = 0x70004; - pub const SET_FLAGS: u32 = 0x70008; - pub const CLEAR_FLAGS: u32 = 0x7000C; - pub const GE_READBACK_HI: u32 = 0x70010; - pub const GE_READBACK_LO: u32 = 0x70014; - pub const CFIFO: u32 = 0x70080; - pub const GIOSTATUS: u32 = 0x70100; - pub const DMABUSY: u32 = 0x70104; - pub const RASTER: u32 = 0x7C000; - pub const RASTER_END: u32 = 0x80000; - - /// Status: raster idle and host idle, command and data FIFOs at or below - /// their low-water marks. - pub const STATUS_IDLE: u32 = 0x01 | 0x02 | 0x10 | 0x40; - pub const STATUS_VBLANK: u32 = 0x04; - - /// Flag set by the "set flag" command (0xE04): DMA/sync completion. - pub const FLAG_DONE: u32 = 1 << 16; - /// Flag set when the geometry engine has data waiting to be read back. - pub const FLAG_GE_DATA: u32 = 1 << 17; - /// Flag set while a geometry engine diagnostic readback has data waiting. - pub const FLAG_GE_DIAG: u32 = 1 << 18; - /// Context switch: outgoing context saved (phase 1) and incoming context - /// loaded (phase 2). - pub const FLAG_CONTEXT_SAVED: u32 = 1 << 19; - pub const FLAG_CONTEXT_LOADED: u32 = 1 << 6; - /// Command-processor flag 0: a scheduled buffer swap has happened. - pub const FLAG_CP0: u32 = 1 << 10; - /// Flags that can raise the general interrupt. - pub const INTR_CAUSES: u32 = 0x7F_FFFF; - - /// Flag and interrupt enables: read at the first address, write-1-to-set - /// there, write-1-to-clear at the second. - pub const FLAG_ENABLE_SET: u32 = 0x50010; - pub const FLAG_ENABLE_CLEAR: u32 = 0x50014; - pub const INTERRUPT_ENABLE_SET: u32 = 0x50018; - pub const INTERRUPT_ENABLE_CLEAR: u32 = 0x5001C; - /// Context switch request: starts the switch routine at the written - /// microcode address. - pub const CONTEXT_SWITCH: u32 = 0x50050; - /// Words of incoming context the host pushes after the save phase. - pub const CONTEXT_SWITCH_WORDS: u32 = 63; - - /// Geometry engine diagnostic ports, per engine: data, then address. - pub const GE_DIAG: [(u32, u32); 2] = [(0x50040, 0x50044), (0x50048, 0x5004C)]; - /// Diagnostic readback words: a discarded word, then the data word. - pub const GE_DIAG_READ_PAD: u32 = 0x50230; - pub const GE_DIAG_READ: u32 = 0x5022C; - - /// Host DMA engine and raster-interface context, read back per register. - pub const DMA_CONTEXT: u32 = 0x50300; - pub const RASTER_IF_CONTEXT: u32 = 0x50200; - /// PIO pixel reads: the raster char registers, high word at the execute - /// alias (which takes the next doubleword), low word at the plain one. - pub const PIO_READ_HI: u32 = 0x7D1C0; - pub const PIO_READ_LO: u32 = 0x7C1C4; +const HQ_FIFO_DEPTH: usize = 65536; +const RSS_FIFO_DEPTH: usize = 65536; + +/// FIFO entry tags. `hq_fifo`: command FIFO words carry their port (0 user, +/// 1 privileged); `RSS | entry` is a direct raster write for the HQ3 to +/// forward in order. `rss_fifo`: a raster write is `entry` itself (register +/// << 1 | execute), below `0x800`. Ops share numbers in both. +mod tag { + pub const PORT_USER: u32 = 0; + pub const PORT_PRIVILEGED: u32 = 1; + pub const RSS: u32 = 0x0100_0000; + /// Context switch requested: the incoming context follows as FIFO words. + pub const CONTEXT_SWITCH: u32 = 0x8000_0001; + /// The host read the high half of a PIO read doubleword: next one. + pub const PIO_ADVANCE: u32 = 0x8000_0004; + /// DMA pixel line into the armed transfer: value is line | bytes << 32, + /// the bytes follow as payload, eight to an entry, first byte highest. + pub const DMA_LINE: u32 = 0x8000_0010; + /// A GL batch starts / ends: the RSS stashes the raster registers it + /// loads, and puts them back after (see `rss::Rss::gl_enter`). + pub const GL_ENTER: u32 = 0x8000_0020; + pub const GL_LEAVE: u32 = 0x8000_0021; + pub const EXIT: u32 = 0x8000_00FF; } -/// A geometry engine as its diagnostic port sees it. The engine itself does -/// not run; it only has to hold downloaded microcode, read it back, and -/// answer "started". -#[derive(Default)] -struct GeDiag { - /// Current diagnostic address: a microcode line (from `UCODE_BASE`) or an - /// internal register. - addr: u32, - /// Which 32-bit word of the current 72-bit microcode line comes next. - word: usize, - ucode: HashMap, -} +/// `rss_fifo` raster write from the CPU's direct register window (for the +/// trace; the RSS treats both sources alike). +const RSS_SRC_CPU: u32 = 0x800; -impl GeDiag { - const UCODE_BASE: u32 = 0x20_0000; - /// Internal register: execution control; bit 0 starts the engine. - const EXEC_CONTROL: u32 = 0x4_0000; - - /// Words a readback starting at `addr` delivers, in order: for each pair - /// of lines, word 0 and the top byte of the first, then word 1 of the - /// second. A readback starting on an odd line is preceded by two words - /// that carry nothing. (The driver's verifier reads exactly this; the - /// order is inferred from it.) - fn readback(&self, lines: u32) -> Vec { - let w = |l: u32, i: usize| self.ucode.get(&l).map(|x| x[i]).unwrap_or(0); - let mut out = Vec::new(); - let mut l = self.addr; - if l.wrapping_sub(Self::UCODE_BASE) & 1 == 1 { - out.extend([0, 0]); - } - for _ in 0..lines / 2 { - out.extend([w(l, 0), w(l, 2) & 0xFF, w(l + 1, 1)]); - l += 2; - } - out - } +/// A raster write as `rss_fifo` carries it. +fn rss_entry(r: u32, exec: bool) -> u32 { + (r & 0x3FF) << 1 | exec as u32 } -/// Command FIFO command numbers. -mod cmd { - /// Command-processor token: schedule a buffer swap for the next retrace. - pub const CP_SCHEDULE_SWAP: u32 = 0x37; - pub const SET_DONE_FLAG: u32 = 0xE04; - pub const RASTER_BASE: u32 = 0x1000; - pub const RASTER_EXECUTE: u32 = 0x400; - pub const DMA_BASE: u32 = 0x800; - pub const RASTER_IF_BASE: u32 = 0xA00; - pub const FORMATTER: u32 = 0xC00; - /// Below this, commands are command-processor microcode tokens. - pub const CP_LIMIT: u32 = 0x200; +/// CPU-side board state: host interface registers, the geometry engines' +/// diagnostic ports, the display control bus. Plain data, valid zeroed. +#[repr(C)] +struct Front { + hq: Hq3Regs, + ge: Ge11, + dcb: dcb::Dcb, } -/// The command FIFO's word-stream parser: a command word, then the data words -/// its byte count announces. -#[derive(Default)] -struct Cfifo { - cmd: u32, - pixel: bool, - need: u32, - data: Vec, +/// `Front` borrowed under its lock. +struct FrontGuard<'a> { + _lock: parking_lot::MutexGuard<'a, ()>, + front: &'a mut Front, } -/// Host DMA engine registers. -mod dma { - pub const PAGE_LIST: usize = 0x00; - pub const STRIDE: usize = 0x04; - pub const ROW_OFFSET: usize = 0x05; - pub const ROW_START: usize = 0x06; - pub const LINES: usize = 0x07; - pub const LINE_BYTES: usize = 0x08; - /// The start word: bit 0 run, bits 2:1 pool, bit 3 board to host. - pub const START: usize = 0x0B; - /// Page-table base of pool `p`: eight bytes at `TABLE_BASE + 2p`, the - /// address in the low word. - pub const TABLE_BASE: usize = 0x20; +impl std::ops::Deref for FrontGuard<'_> { + type Target = Front; + fn deref(&self) -> &Front { self.front } } -/// Board state behind one lock. -struct Board { - regs: HashMap, - ucode: Vec, - flags: u32, - flag_enable: u32, - interrupt_enable: u32, - /// Context-switch packet words still to swallow from the FIFO. - context_words: u32, - ge_readback: [u32; 2], - ge: [GeDiag; 2], - /// Pending diagnostic readback words, oldest first. - ge_out: std::collections::VecDeque, - /// One parser per FIFO port (user, privileged): the two may interleave. - cfifo: [Cfifo; 2], - /// Host DMA engine registers as 32-bit words; an eight-byte register - /// takes two, high word first. - dma_regs: [u32; 0x80], - raster_if_regs: [u32; 0x10], - formatter: u32, - /// A board-to-host DMA started on the host side, waiting for the raster - /// engine to be started (xfrcontrol = 9). - dma_read_pending: Option, - /// System memory, for DMA. - mem: Option>, - /// DMA transfers logged so far (bring-up). - dma_logged: u32, - dcb: dcb::Dcb, - raster: raster::Raster, - /// Commands and registers not modelled yet, reported once each. - unhandled: HashSet, +impl std::ops::DerefMut for FrontGuard<'_> { + fn deref_mut(&mut self) -> &mut Front { self.front } } -impl Board { - fn new(kind: ImpactSlot) -> Self { - // Board version bytes: [RA/RB boards + TRAMs, product + GE count]. +impl Front { + fn init(&mut self, kind: GraphicsBoard) { + // Board version bytes: [RB revision (6:4, 7 = no RB board), RB TRAMs + // (3:2), RA TRAMs (1:0), as log2 of the TRAM count; product (6:5) + + // GE count (1:0)]. High and Max Impact get 4 TRAMs (4 MB) per board. let bdvers = match kind { - ImpactSlot::Solid => [0x70, 0x21], - ImpactSlot::High => [0x70, 0x01], - ImpactSlot::Max => [0x00, 0x02], - ImpactSlot::None => [0, 0], - }; - Board { - regs: HashMap::new(), - ucode: vec![0; ((host::UCODE_END - host::UCODE) / 4) as usize], - flags: 0, - flag_enable: 0, - interrupt_enable: 0, - context_words: 0, - ge_readback: [0; 2], - ge: [GeDiag::default(), GeDiag::default()], - ge_out: std::collections::VecDeque::new(), - cfifo: [Cfifo::default(), Cfifo::default()], - dma_regs: [0; 0x80], - raster_if_regs: [0; 0x10], - formatter: 0, - dma_read_pending: None, - mem: None, - dma_logged: 0, - dcb: dcb::Dcb::new(bdvers, [0xFB, 0xFB]), - raster: raster::Raster::default(), - unhandled: HashSet::new(), - } - } - - fn note(&mut self, what: String) { - if self.unhandled.len() < 256 && self.unhandled.insert(what.clone()) { - eprintln!("mgras: not modelled yet: {what}"); - } - } - - /// Push one 32-bit word into the command FIFO. Returns true when the - /// framebuffer changed. - fn cfifo_word(&mut self, port: usize, w: u32) -> bool { - // A context switch's incoming state follows the save phase as raw - // words; the command processor would load it. Consume it here, and - // report the load done after the last word. - if self.context_words > 0 { - self.context_words -= 1; - if self.context_words == 0 { - self.flags |= host::FLAG_CONTEXT_LOADED; - } - return false; - } - let f = &mut self.cfifo[port]; - if f.need == 0 { - if w & 0x8000_0000 != 0 { - // Pixel data: byte count in bits 19:0, sent as doublewords. - f.pixel = true; - f.cmd = w; - f.need = (((w & 0xF_FFFF) + 7) / 8) * 2; - } else { - f.pixel = false; - f.cmd = (w >> 8) & 0x1FFF; - f.need = ((w & 0xFF) + 3) / 4; - } - f.data.clear(); - if f.need == 0 { - return self.dispatch(port); - } - return false; - } - f.need -= 1; - if f.data.len() < 64 { - f.data.push(w); - } - if f.need == 0 { self.dispatch(port) } else { false } - } - - fn dispatch(&mut self, port: usize) -> bool { - let cmd = self.cfifo[port].cmd; - let data = std::mem::take(&mut self.cfifo[port].data); - if self.cfifo[port].pixel { - self.note(format!("pixel data command {cmd:#010x}")); - return false; - } - let changed = if cmd >= cmd::RASTER_BASE { - let r = cmd & 0x3FF; - let exec = cmd & cmd::RASTER_EXECUTE != 0; - let changed = match data.as_slice() { - [] => self.raster.write(r, 0, exec), - [d] => self.raster.write(r, *d, exec), - [hi, lo, ..] => { - self.raster.write(r, *hi, false); - self.raster.write(r + 1, *lo, exec) - } - }; - if r == raster::reg::XFRCONTROL && data.first() == Some(&9) { - changed | self.start_read_dma() - } else { - changed - } - } else if cmd == cmd::SET_DONE_FLAG { - self.flags |= host::FLAG_DONE; - false - } else if cmd >= cmd::DMA_BASE { - let n = (cmd & 0x1FF) as usize; - let v = data.first().copied().unwrap_or(0); - if cmd >= cmd::FORMATTER { - self.formatter = v; - false - } else if cmd >= cmd::RASTER_IF_BASE { - self.raster_if_regs[n & 0xF] = v; - false - } else { - // Eight-byte registers arrive as a high word, then a low word, - // and fill two register slots. - let n = n & 0x7F; - for (i, d) in data.iter().take(2).enumerate() { - self.dma_regs[(n + i) & 0x7F] = *d; - } - if data.is_empty() { - self.dma_regs[n] = 0; - } - if n == dma::START { self.dma_start(v) } else { false } - } - } else if cmd == cmd::CP_SCHEDULE_SWAP { - // No retrace wait: the swap is reported done at once. - self.flags |= host::FLAG_CP0; - false - } else if cmd < cmd::CP_LIMIT { - self.note(format!("command-processor token {cmd:#x} ({} data words)", data.len())); - false - } else { - self.note(format!("command {cmd:#x}")); - false - }; - self.cfifo[port].data = data; - changed - } - - /// The host DMA engine's start word. Host to board runs now: the raster - /// engine was armed first. Board to host waits for the raster engine. - fn dma_start(&mut self, word: u32) -> bool { - if word & 1 == 0 { - return false; - } - if word & 8 != 0 { - self.dma_read_pending = Some(word); - return false; - } - match self.raster.transfer_armed() { - Some(false) => self.dma(word, false), - _ => { - self.note(format!("host DMA start {word:#x} with no write transfer armed")); - false - } - } - } - - fn start_read_dma(&mut self) -> bool { - let Some(word) = self.dma_read_pending.take() else { - self.note("raster DMA read started with no host DMA pending".into()); - return false; - }; - if self.raster.transfer_armed() != Some(true) { - self.note(format!("host DMA read {word:#x} with no read transfer armed")); - return false; - } - self.dma(word, true) - } - - /// Run a DMA between host memory and the armed raster transfer, a line at - /// a time. Host addresses are logical within the pool and translate - /// through its page table: one 32-bit frame number per 4 KB page. - fn dma(&mut self, word: u32, read: bool) -> bool { - let Some(mem) = self.mem.clone() else { return false }; - let pool = ((word >> 1) & 3) as usize; - let table = self.dma_regs[dma::TABLE_BASE + 2 * pool + 1] & !3; - let base = self.dma_regs[dma::ROW_START].wrapping_add(self.dma_regs[dma::ROW_OFFSET]); - let stride = self.dma_regs[dma::STRIDE]; - let lines = self.dma_regs[dma::LINES]; - let len = self.dma_regs[dma::LINE_BYTES]; - if self.dma_logged < 16 { - self.dma_logged += 1; - eprintln!( - "mgras: DMA {} pool {pool} table {table:#x} base {base:#x} stride {stride} lines {lines} bytes {len} pglist {:#x} shape {:?}", - if read { "read" } else { "write" }, - self.dma_regs[dma::PAGE_LIST], - self.raster.transfer_shape() - ); - } - let frame_of = |page: u32| -> Option { - let r = mem.read32(table.wrapping_add(4 * page)); - r.is_ok().then_some(r.data << 12) + GraphicsBoard::SolidImpact => [0x70, 0x21], + GraphicsBoard::HighImpact => [0x72, 0x01], + GraphicsBoard::MaxImpact => [0x0A, 0x02], + _ => [0, 0], }; - let mut cached: Option<(u32, u32)> = None; - let mut phys = |l: u32| -> Option { - let page = l >> 12; - let f = match cached { - Some((p, f)) if p == page => f, - _ => { - let f = frame_of(page)?; - cached = Some((page, f)); - f - } - }; - Some(f | (l & 0xFFF)) - }; - let mut changed = false; - for i in 0..lines { - let a = base.wrapping_add(i.wrapping_mul(stride)); - if read { - let bytes = self.raster.dma_read_line(i); - for (k, b) in bytes.iter().take(len as usize).enumerate() { - let Some(pa) = phys(a + k as u32) else { return changed }; - mem.write8(pa, *b); - } - } else { - let mut bytes = Vec::with_capacity(len as usize); - for k in 0..len { - let Some(pa) = phys(a + k) else { return changed }; - let r = mem.read8(pa); - bytes.push(if r.is_ok() { r.data } else { 0 }); - } - self.raster.dma_write_line(i, &bytes); - changed = true; - } - } - changed - } - - /// Flags as the host reads them, including those derived from state. - fn all_flags(&self) -> u32 { - self.flags | if self.ge_out.is_empty() { 0 } else { host::FLAG_GE_DIAG } - } - - /// Whether the general interrupt (GIO line 1) is asserted: some enabled - /// flag is set. It is a level, held until the handler clears the flag or - /// its enable. - fn general_irq(&self) -> bool { - self.all_flags() & self.interrupt_enable & host::INTR_CAUSES != 0 + self.dcb.init(bdvers, [0xFB, 0xFB]); } - /// Video timing chip display control bit 0: vertical retrace interrupts - /// enabled. - fn retrace_enabled(&self) -> bool { - self.dcb.vc3.regs[0x1E] & 1 != 0 + /// Flags derived from state, on top of the stored ones. + fn derived_flags(&self) -> u32 { + if self.ge.out.is_empty() { 0 } else { host::FLAG_GE_DIAG } } - /// A write to a geometry engine's diagnostic data or address port. - fn ge_diag_write(&mut self, off: u32, val: u32) { - let n = host::GE_DIAG.iter().position(|&(d, a)| off == d || off == a).unwrap(); - let (data_port, _) = host::GE_DIAG[n]; - let ge = &mut self.ge[n]; - if off != data_port { - if val & 0x8000_0000 != 0 { - // Read request for `val & 0x7FFF_FFFF` lines from `addr`; it - // replaces anything a previous request left unread. - let words = ge.readback(val & 0x7FFF_FFFF); - self.ge_out.clear(); - self.ge_out.extend(words); - } else { - ge.addr = val; - ge.word = 0; - } - return; - } - if ge.addr >= GeDiag::UCODE_BASE { - let line = ge.ucode.entry(ge.addr).or_insert([0; 3]); - line[ge.word] = val; - ge.word += 1; - if ge.word == 3 { - ge.word = 0; - ge.addr += 1; - } - } else if ge.addr == GeDiag::EXEC_CONTROL && val & 1 != 0 { - // Started: the version program's answer is waiting (revision 1). - self.ge_readback = [0, 1]; - self.flags |= host::FLAG_GE_DATA; - } - } - - fn status(&self) -> u32 { + fn status() -> u32 { // A vertical blank of about 1 ms in every 60 Hz frame. let us = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -517,41 +168,34 @@ impl Board { fn read(&mut self, off: u32, bits: u32) -> u64 { match off { 0 => GIO_ID as u64, - host::UCODE..=0x45FFF => self.ucode[((off - host::UCODE) / 4) as usize] as u64, + host::UCODE..=0x45FFF => self.hq.ucode[((off - host::UCODE) / 4) as usize] as u64, 0x60000..=0x67FFF => { let t = dcb::Txn::decode(off); let d = self.dcb.read(t); t.load_value(bits, d) } - host::STATUS => self.status() as u64, - host::FIFOSTATUS | host::GIOSTATUS | host::DMABUSY => 0, - host::SET_FLAGS | host::CLEAR_FLAGS | host::SET_FLAGS_PRIVILEGED | host::CLEAR_FLAGS_PRIVILEGED => self.all_flags() as u64, - host::FLAG_ENABLE_SET | host::FLAG_ENABLE_CLEAR => self.flag_enable as u64, - host::INTERRUPT_ENABLE_SET | host::INTERRUPT_ENABLE_CLEAR => self.interrupt_enable as u64, - host::GE_DIAG_READ => self.ge_out.pop_front().unwrap_or(0) as u64, - host::RASTER_IF_CONTEXT..=0x50228 => self.raster_if_regs[((off - host::RASTER_IF_CONTEXT) / 4) as usize] as u64, - host::DMA_CONTEXT..=0x504FF => self.dma_regs[((off - host::DMA_CONTEXT) / 4) as usize] as u64, - host::PIO_READ_HI => self.raster.pio_read_hi() as u64, - host::PIO_READ_LO => self.raster.pio_read_lo() as u64, - host::GE_READBACK_HI => self.ge_readback[0] as u64, - host::GE_READBACK_LO => self.ge_readback[1] as u64, - host::RASTER..=0x7FFFF => { - let r = (off & 0xFFC) >> 2; + 0x68000..=0x6803F => { + let dev = ((off - 0x68000) >> 2) as usize; if bits == 64 { - ((self.raster.read(r) as u64) << 32) | self.raster.read(r + 1) as u64 + ((self.dcb.read_dcbctrl(dev) as u64) << 32) | (self.dcb.read_dcbctrl(dev + 1) as u64) } else { - self.raster.read(r) as u64 + self.dcb.read_dcbctrl(dev) as u64 } } - _ => self.regs.get(&off).copied().unwrap_or(0) as u64, + host::FLAG_ENABLE_SET | host::FLAG_ENABLE_CLEAR => self.hq.flag_enable as u64, + host::INTERRUPT_ENABLE_SET | host::INTERRUPT_ENABLE_CLEAR => self.hq.interrupt_enable as u64, + host::GE_DIAG_READ => self.ge.out.pop().unwrap_or(0) as u64, + host::GE_READBACK_HI => self.hq.ge_readback[0] as u64, + host::GE_READBACK_LO => self.hq.ge_readback[1] as u64, + _ => self.hq.regs.get(off) as u64, } } - /// Returns true when the framebuffer changed. - fn write(&mut self, off: u32, bits: u32, val: u64) -> bool { + /// Returns true when what is displayed changed. + fn write(&mut self, off: u32, bits: u32, val: u64, flags: &AtomicU32) -> bool { match off { host::UCODE..=0x45FFF => { - self.ucode[((off - host::UCODE) / 4) as usize] = val as u32 & 0xFF_FFFF; + self.hq.ucode[((off - host::UCODE) / 4) as usize] = val as u32 & 0xFF_FFFF; false } 0x60000..=0x67FFF => { @@ -562,378 +206,429 @@ impl Board { // writes (select 1 and below) change nothing by themselves. t.crs >= 2 || t.dev == dcb::DEV_VC3 } - off if host::GE_DIAG.iter().any(|&(d, a)| off == d || off == a) => { - self.ge_diag_write(off, val as u32); - false - } - host::FLAG_ENABLE_SET => { self.flag_enable |= val as u32; false } - host::FLAG_ENABLE_CLEAR => { self.flag_enable &= !(val as u32); false } - host::INTERRUPT_ENABLE_SET => { self.interrupt_enable |= val as u32; false } - host::INTERRUPT_ENABLE_CLEAR => { self.interrupt_enable &= !(val as u32); false } - host::CONTEXT_SWITCH => { - // The save phase completes at once; the incoming context - // follows through the FIFO (see `cfifo_word`). - self.flags |= host::FLAG_CONTEXT_SAVED; - self.context_words = host::CONTEXT_SWITCH_WORDS; - false - } - host::SET_FLAGS | host::SET_FLAGS_PRIVILEGED => { self.flags |= val as u32; false } - host::CLEAR_FLAGS | host::CLEAR_FLAGS_PRIVILEGED => { self.flags &= !(val as u32); false } - host::CFIFO | 0x70084 | host::CFIFO_PRIVILEGED | 0x50084 => { - let port = if off >= host::STATUS { 0 } else { 1 }; + 0x68000..=0x6803F => { + let dev = ((off - 0x68000) >> 2) as usize; if bits == 64 { - let a = self.cfifo_word(port, (val >> 32) as u32); - let b = self.cfifo_word(port, val as u32); - a | b + self.dcb.write_dcbctrl(dev, (val >> 32) as u32); + self.dcb.write_dcbctrl(dev + 1, val as u32); } else { - self.cfifo_word(port, val as u32) + self.dcb.write_dcbctrl(dev, val as u32); } + false } - host::RASTER..=0x7FFFF => { - let r = (off & 0xFFC) >> 2; - // Registers at 0x7C000 + 4r; the same register at +0x1000 - // also executes the primitive in the IR after the write. - let exec = off & 0x1000 != 0; - if bits == 64 { - self.raster.write(r, (val >> 32) as u32, false); - self.raster.write(r + 1, val as u32, exec) - } else { - self.raster.write(r, val as u32, exec) + off if Ge11::is_diag_port(off) => { + if self.ge.write(off, val as u32) { + // Started: the version program's answer is waiting + // (revision 1). + self.hq.ge_readback = [0, 1]; + flags.fetch_or(host::FLAG_GE_DATA, Ordering::AcqRel); } - } - 0x80000..=0xFFFFF => { - self.note(format!("fast-path command window write at {off:#x}")); false } + host::FLAG_ENABLE_SET => { self.hq.flag_enable |= val as u32; false } + host::FLAG_ENABLE_CLEAR => { self.hq.flag_enable &= !(val as u32); false } + host::INTERRUPT_ENABLE_SET => { self.hq.interrupt_enable |= val as u32; false } + host::INTERRUPT_ENABLE_CLEAR => { self.hq.interrupt_enable &= !(val as u32); false } _ => { - self.regs.insert(off, val as u32); + self.hq.regs.insert(off, val as u32); false } } } +} - /// 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 - /// over a grid of samples inside it that has an RGB display mode. None - /// if no sampled pixel is in an RGB window. - fn window_did(&self, x: i32, y: i32, w: usize, h: usize) -> Option { - let mut counts = [0u32; 32]; - let mut runs = Vec::new(); - for sy in 0..16 { - let py = y + (h as i32 * (2 * sy + 1)) / 32; - if !(0..raster::HEIGHT as i32).contains(&py) { - continue; - } - self.dcb.vc3.main_did_runs(py as usize, &mut runs); - for sx in 0..16 { - let px = x + (w as i32 * (2 * sx + 1)) / 32; - if !(0..raster::WIDTH as i32).contains(&px) { - continue; - } - let did = runs.iter().rev().find(|r| r.0 as i32 <= px).map_or(0, |r| r.1); - if self.dcb.xmap.main_mode(did as u32) & 0x1F >= 4 { - counts[did as usize & 31] += 1; - } - } - } - let (did, n) = counts.iter().enumerate().max_by_key(|&(_, n)| *n)?; - (*n > 0).then_some(did as u8) +/// The board's three interrupt lines to the GIO slot. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Line { + /// GIO interrupt 0: command FIFO high/low water. + Fifo, + /// GIO interrupt 1: the general graphics interrupt. + General, + /// GIO interrupt 2: vertical retrace. + Retrace, +} + +pub struct Mgras { + kind: GraphicsBoard, + ioc: crate::dev::ioc::Ioc, + /// Current level of the general interrupt line. + general: AtomicBool, + /// CPU-side state and its lock. The display thread reads it unlocked + /// (plain data, tearing tolerated). + front_lock: Mutex<()>, + front: UnsafeCell, + /// Commands and registers not modelled yet, reported once each. + unhandled: Mutex>, + /// HQ3 flags: the HQ3 thread raises them, the CPU sets and clears them. + flags: AtomicU32, + /// Owned by the HQ3 thread (others only while the board is idle). + eng: UnsafeCell, + /// Owned by the RSS thread (others only while the board is idle; the + /// display thread reads the framebuffer unlocked). + rss: UnsafeCell, + /// System memory, for the board's DMA (it is a GIO bus master). + mem: Mutex>>, + hq_fifo: GFifo, + rss_fifo: GFifo, + hq_busy: AtomicBool, + rss_busy: AtomicBool, + /// Held by every producer while it pushes, and by anyone who needs the + /// engines to stay idle (reads of drawing state, checkpoints, monitor). + /// Holds the recording, so records come out in FIFO order. + submit: Mutex>, + /// The recording again, for DMA memory accesses on the HQ3 thread (which + /// must not wait on `submit`: its holder may be waiting for idle). + mem_rec: Mutex>, + engines: AtomicBool, + threads: Mutex>>, + hq_thread: Mutex>, + rss_thread: Mutex>, + dirty: AtomicBool, + running: AtomicBool, + refresh: Mutex>>, + renderer: Mutex>>, + /// The finished frame, handed to the renderer as a prebuilt picture + /// (`Rex3Screen::prebuilt`), as GR2 does. + screen: Mutex, + screenshot_pending: AtomicBool, + heartbeat: Arc, + fasttick: Arc, + cycles: Mutex, + /// Annotated trace (`mgras trace`); `trace_mask` gates the hot paths. + trace: Mutex, + trace_mask: AtomicU32, +} + +// SAFETY: `eng` is touched only by the HQ3 thread, `rss` only by the RSS +// thread, except by holders of `submit` while the board is idle (no producer +// can push, no engine is running). The display thread reads `rss`'s +// framebuffer unlocked; tearing is tolerated, as with REX3 and GR2. +unsafe impl Sync for Mgras {} +unsafe impl Send for Mgras {} + +/// The HQ3 thread's view of the rest of the board. +struct HqSink<'a>(&'a Mgras); + +impl Hq3Sink for HqSink<'_> { + fn rss_write(&mut self, r: u32, val: u32, exec: bool) { + self.0.rss_push(rss_entry(r, exec), val as u64); + } + fn transfer(&mut self) -> Option<(bool, (u32, u32))> { + self.0.drain_rss(); + // SAFETY: the RSS is drained and idle; only the HQ3 feeds it now. + let rss = unsafe { &*self.0.rss.get() }; + Some((rss.transfer_armed()?, rss.transfer_shape()?)) + } + fn dma_write_line(&mut self, line: u32, bytes: &[u8]) { + let vals: Vec = bytes + .chunks(8) + .map(|c| c.iter().enumerate().fold(0u64, |a, (i, &b)| a | (b as u64) << (56 - 8 * i))) + .collect(); + self.0.rss_fifo.push_batch(tag::DMA_LINE, line as u64 | (bytes.len() as u64) << 32, &vals); + Mgras::wake(&self.0.rss_thread); + } + fn dma_read_line(&mut self, line: u32) -> Vec { + self.0.drain_rss(); + // SAFETY: as in `transfer`. + unsafe { &*self.0.rss.get() }.dma_read_line(line) + } + fn gl_bracket(&mut self, enter: bool) { + self.0.rss_push(if enter { tag::GL_ENTER } else { tag::GL_LEAVE }, 0); + } + fn set_flags(&mut self, bits: u32) { + self.0.flags.fetch_or(bits, Ordering::AcqRel); + self.0.refresh_general(); + } + fn sys_mem(&mut self) -> Option> { + let mem = self.0.mem.lock().clone()?; + Some(match self.0.mem_rec.lock().clone() { + Some(rec) => Arc::new(record::RecordingMem { inner: mem, rec }), + None => mem, + }) + } + fn note(&mut self, what: String) { + self.0.note(what); } + fn tracing(&self) -> bool { + self.0.tracing(debug::TRACE_HQ) + } + fn tracing_tex(&self) -> bool { + self.0.tracing(debug::TRACE_TEX) + } + fn trace(&mut self, line: String) { + self.0.trace_hq(&line); + } +} - /// Paint a host GL frame (`bgra`: `h` rows, top first, `stride` bytes - /// each) into the framebuffer at screen position (`x`, `y`), wherever the - /// pixel belongs to the frame's window ID, so windows over it stay over it. - /// The pixels become part of the framebuffer, as GL's would on the board, - /// for anything that reads them back. False when no window ID fits. - fn composite(&mut self, x: i32, y: i32, bgra: &[u8], stride: usize, w: usize, h: usize) -> bool { - let Some(target) = self.window_did(x, y, w, h) else { return false }; - let mut runs = Vec::new(); - for row in 0..h { - let sy = y + row as i32; - if !(0..raster::HEIGHT as i32).contains(&sy) { - continue; - } - self.dcb.vc3.main_did_runs(sy as usize, &mut runs); - if runs.is_empty() || runs[0].0 != 0 { - runs.insert(0, (0, 0)); +impl Mgras { + /// Build the board in place on the heap: its multi-megabyte state is + /// zero-initialised without touching the stack, and only the fields that + /// are not valid zeroed are written. + pub fn new(board: GraphicsBoard, ioc: crate::dev::ioc::Ioc, heartbeat: Arc, fasttick: Arc) -> Arc { + let mut a: Arc> = Arc::new_zeroed(); + let p = Arc::get_mut(&mut a).unwrap().as_mut_ptr(); + // SAFETY: `p` points at zeroed, exclusively owned memory. Every field + // that is not valid zeroed is written below, before assume_init. + let m = unsafe { + addr_of_mut!((*p).kind).write(board); + addr_of_mut!((*p).ioc).write(ioc); + addr_of_mut!((*p).general).write(AtomicBool::new(false)); + addr_of_mut!((*p).front_lock).write(Mutex::new(())); + (*(*p).front.get()).init(board); + addr_of_mut!((*p).unhandled).write(Mutex::new(HashSet::new())); + addr_of_mut!((*p).flags).write(AtomicU32::new(0)); + addr_of_mut!((*p).eng).write(UnsafeCell::new(Hq3Engine::new())); + Rss::init((*p).rss.get()); + addr_of_mut!((*p).mem).write(Mutex::new(None)); + addr_of_mut!((*p).submit).write(Mutex::new(None)); + addr_of_mut!((*p).mem_rec).write(Mutex::new(None)); + addr_of_mut!((*p).threads).write(Mutex::new(Vec::new())); + addr_of_mut!((*p).hq_thread).write(Mutex::new(None)); + addr_of_mut!((*p).rss_thread).write(Mutex::new(None)); + addr_of_mut!((*p).dirty).write(AtomicBool::new(true)); + addr_of_mut!((*p).refresh).write(Mutex::new(None)); + addr_of_mut!((*p).renderer).write(Mutex::new(None)); + addr_of_mut!((*p).screen).write(Mutex::new(crate::disp::Rex3Screen::new())); + addr_of_mut!((*p).heartbeat).write(heartbeat); + addr_of_mut!((*p).fasttick).write(fasttick); + addr_of_mut!((*p).cycles).write(Mutex::new(crate::cpu::mips_core::CyclesPtr::dangling())); + addr_of_mut!((*p).trace).write(Mutex::new(debug::MgrasTrace::new())); + a.assume_init() + }; + if let Some(path) = std::env::var_os("IRIS_MGRAS_REC") { + let path = path.to_string_lossy(); + match m.set_recording(Some(&path)) { + Ok(()) => eprintln!("mgras: recording to {path}"), + Err(e) => eprintln!("mgras: cannot record to {path}: {e}"), } - let fb_row = (raster::HEIGHT - 1 - sy as usize) * raster::WIDTH; - for (k, &(x0, did)) in runs.iter().enumerate() { - if did != target { - continue; - } - let x1 = runs.get(k + 1).map_or(raster::WIDTH as i32, |r| r.0 as i32); - let lo = (x0 as i32).max(x).max(0); - let hi = x1.min(x + w as i32).min(raster::WIDTH as i32); - for sx in lo..hi { - let i = row * stride + (sx - x) as usize * 4; - let Some(p) = bgra.get(i..i + 4) else { break }; - self.raster.fb[fb_row + sx as usize] = p[2] as u32 | (p[1] as u32) << 8 | (p[0] as u32) << 16; - } + } + if let Some(path) = std::env::var_os("IRIS_MGRAS_TRACE") { + let path = path.to_string_lossy(); + match m.start_trace(&path, debug::TRACE_ALL) { + Ok(()) => eprintln!("mgras: tracing to {path}"), + Err(e) => eprintln!("mgras: cannot trace to {path}: {e}"), } } - true + m } - /// Scan the framebuffer out to `0xFF_BB_GG_RR` (the compositor's order, - /// red in the low byte), stride 2048, top row first. Each pixel's window - /// ID (from the video timing chip's tables) picks its display mode. Modes - /// with a pixel format (bits 4:0) of 4 and up are RGB; the others are - /// colour index, into the colormap block that bits 9:5 choose. Both go - /// through the DAC gamma. - fn scanout(&self, out: &mut [u32]) { - if self.dcb.dac.pixmask() == 0 { - out.iter_mut().for_each(|p| *p = 0xFF00_0000); - return; + /// The CPU-side state, locked. + fn front(&self) -> FrontGuard<'_> { + let lock = self.front_lock.lock(); + // SAFETY: mutable access only under `front_lock`; the display + // thread's unlocked reads are of plain data (see `front_view`). + FrontGuard { _lock: lock, front: unsafe { &mut *self.front.get() } } + } + + /// The CPU-side state without the lock, for the display thread: plain + /// data, so a concurrent write can only tear a value. + fn front_view(&self) -> &Front { + // SAFETY: see above. + unsafe { &*self.front.get() } + } + + fn note(&self, what: String) { + let mut u = self.unhandled.lock(); + if u.len() < 256 && u.insert(what.clone()) { + eprintln!("mgras: not modelled yet: {what}"); } - let pal = &self.dcb.cmap[0].pal; - let gamma = &self.dcb.dac.gamma; - let gamma_rgb = |r: u32, g: u32, b: u32| -> u32 { - let g1 = |v: u32, comp: usize| (gamma[(v & 0xFF) as usize][comp] >> 2) as u32; - 0xFF00_0000 | (g1(b, 2) << 16) | (g1(g, 1) << 8) | g1(r, 0) - }; - // Per window ID: None for an RGB mode, else the colormap block's - // entries through the gamma tables. - let luts: Vec>> = (0..32u32) - .map(|did| { - let mode = self.dcb.xmap.main_mode(did); - if mode & 0x1F >= 4 { - return None; - } - let base = ((mode >> 5) & 0x1F) as usize * 256; - Some( - (0..4096usize) - .map(|i| { - let c = pal.get((base + i) % pal.len().max(1)).copied().unwrap_or(0); - gamma_rgb(c >> 16, c >> 8, c) - }) - .collect(), - ) - }) - .collect(); - let mut runs = Vec::new(); - for row in 0..raster::HEIGHT { - let y = raster::HEIGHT - 1 - row; - let src = &self.raster.fb[y * raster::WIDTH..(y + 1) * raster::WIDTH]; - let dst = &mut out[row * 2048..row * 2048 + raster::WIDTH]; - self.dcb.vc3.main_did_runs(row, &mut runs); - if runs.is_empty() || runs[0].0 != 0 { - runs.insert(0, (0, 0)); - } - for (k, &(x0, did)) in runs.iter().enumerate() { - let x0 = (x0 as usize).min(raster::WIDTH); - let x1 = runs.get(k + 1).map(|r| (r.0 as usize).min(raster::WIDTH)).unwrap_or(raster::WIDTH); - if x1 <= x0 { - continue; - } - match &luts[did as usize & 31] { - Some(lut) => { - for x in x0..x1 { - dst[x] = lut[(src[x] & 0xFFF) as usize]; - } - } - None => { - for x in x0..x1 { - let v = src[x]; - dst[x] = gamma_rgb(v, v >> 8, v >> 16); - } - } - } - } + } + + fn wake(slot: &Mutex>) { + if let Some(t) = slot.lock().as_ref() { + t.unpark(); } - self.draw_overlay(out, &gamma_rgb); - self.draw_cursor(out, &gamma_rgb); } - /// Overlay planes over the main scanout: a nonzero pixel is a colour - /// index into the block its overlay mode names (bits 7:3); zero, or a - /// window ID whose overlay is off, shows the main planes. - fn draw_overlay(&self, out: &mut [u32], gamma_rgb: &dyn Fn(u32, u32, u32) -> u32) { - let pal = &self.dcb.cmap[0].pal; - let bases: Vec> = (0..32u32) - .map(|did| { - let mode = self.dcb.xmap.overlay_mode(did); - (mode != 0).then(|| ((mode >> 3) & 0x1F) as usize * 256) - }) - .collect(); - if bases.iter().all(Option::is_none) { - return; + /// True when neither FIFO holds work and neither engine is mid-entry. + /// The HQ3 is checked first: whatever it forwarded before going idle is + /// already in `rss_fifo` when that is checked. + fn idle(&self) -> bool { + self.hq_fifo.is_empty() + && !self.hq_busy.load(Ordering::Acquire) + && self.rss_fifo.is_empty() + && !self.rss_busy.load(Ordering::Acquire) + } + + /// Wait until the board is idle. Holding `submit` keeps it idle. + fn wait_idle(&self) { + let backoff = crossbeam_utils::Backoff::new(); + while !self.idle() { + backoff.snooze(); } - let mut runs = Vec::new(); - for row in 0..raster::HEIGHT { - let y = raster::HEIGHT - 1 - row; - let src = &self.raster.overlay[y * raster::WIDTH..(y + 1) * raster::WIDTH]; - let dst = &mut out[row * 2048..row * 2048 + raster::WIDTH]; - self.dcb.vc3.overlay_did_runs(row, &mut runs); - if runs.is_empty() || runs[0].0 != 0 { - runs.insert(0, (0, 0)); - } - for (k, &(x0, did)) in runs.iter().enumerate() { - let Some(base) = bases[did as usize & 31] else { continue }; - let x0 = (x0 as usize).min(raster::WIDTH); - let x1 = runs.get(k + 1).map(|r| (r.0 as usize).min(raster::WIDTH)).unwrap_or(raster::WIDTH); - for x in x0..x1 { - let v = src[x] & 0xFF; - if v != 0 { - let c = pal.get((base + v as usize) % pal.len().max(1)).copied().unwrap_or(0); - dst[x] = gamma_rgb(c >> 16, c >> 8, c); - } - } - } + } + + /// HQ3 thread: wait until the RSS has run everything queued to it. + fn drain_rss(&self) { + let backoff = crossbeam_utils::Backoff::new(); + while !self.rss_fifo.is_empty() || self.rss_busy.load(Ordering::Acquire) { + backoff.snooze(); } } - /// Overlay the hardware cursor, its colours from the cursor colormap. - fn draw_cursor(&self, out: &mut [u32], gamma_rgb: &dyn Fn(u32, u32, u32) -> u32) { - let Some((cx, cy, size, glyph)) = self.dcb.vc3.cursor() else { return }; - let sram = &self.dcb.vc3.sram; - let pal = &self.dcb.cmap[0].pal; - let base = self.dcb.xmap.cursor_cmap_base(); - let words_per_row = size / 16; - let plane_words = size * words_per_row; - let bit = |plane: usize, row: usize, col: usize| -> u32 { - let w = sram[(glyph + plane * plane_words + row * words_per_row + col / 16) & 0x7FFF]; - (w >> (15 - col % 16)) as u32 & 1 - }; - for row in 0..size { - let y = cy + row as i32; - if !(0..raster::HEIGHT as i32).contains(&y) { - continue; - } - for col in 0..size { - let x = cx + col as i32; - if !(0..raster::WIDTH as i32).contains(&x) { - continue; + /// HQ3 thread: queue one entry for the RSS. + fn rss_push(&self, addr: u32, val: u64) { + self.rss_fifo.push(addr, val); + Self::wake(&self.rss_thread); + } + + /// Everything a replay must reproduce, as one byte string, little-endian + /// u32s: the displayed main buffer's bottom-left 1280x1024 (row 0 at the + /// bottom), the overlay's, colormap 0, the 32 main XMAP modes, the video + /// timing chip's registers and SRAM, then, only if any other bit of + /// pixel memory is set, the hash of the rest. (This is the byte string + /// the old 1280x1024 plane model hashed, so older recordings keep their + /// hashes.) Call with `submit` held and the board idle. + fn state_bytes(&self) -> Vec { + const RW: u32 = 1280; + const RH: u32 = 1024; + // SAFETY: idle, and the caller holds `submit` (see the Sync impl). + let rss = unsafe { &*self.rss.get() }; + let f = self.front(); + let (main, overlay) = frame::scanout_buffers(rss, &f.dcb); + let mut out = Vec::with_capacity((2 * (RW * RH) as usize + 8192 + 32 + 32 + 0x8000) * 4 + 32); + let mut put = |v: u32| out.extend_from_slice(&v.to_le_bytes()); + for b in [Some(main), overlay] { + for y in 0..RH { + for x in 0..RW { + put(b.map_or(0, |b| rss.mem.get(&b, x, y) as u32)); } - let c = bit(0, row, col) | bit(1, row, col) << 1; - if c != 0 { - let rgb = pal.get(base + c as usize).copied().unwrap_or(0xFF_FFFF); - out[y as usize * 2048 + x as usize] = gamma_rgb(rgb >> 16, rgb >> 8, rgb); + } + } + for v in f.dcb.cmap[0].pal.iter() { + put(*v); + } + for d in 0..32 { + put(f.dcb.xmap.main_mode(d)); + } + for v in f.dcb.vc3.regs.iter().chain(f.dcb.vc3.sram.iter()) { + put(*v as u32); + } + // The rest: pixel memory with the bits hashed above cleared. + let mut rest = plain::boxed_zeroed::(); + rest.words.copy_from_slice(&rss.mem.words); + for b in [Some(main), overlay].into_iter().flatten() { + for y in 0..RH { + for x in 0..RW { + rest.put(&b, x, y, 0); } } } - } -} - -/// Access trace for bring-up: every access to the board, one line each -/// (`R`/`W`, width in bits, physical address, value). Started from -/// `IRIS_MGRAS_TRACE=` or the monitor (`mgras trace `, `mgras -/// trace off`). Off, it costs one relaxed load per access. -mod trace { - use std::io::Write; - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::OnceLock; - use parking_lot::Mutex; - - static ON: AtomicBool = AtomicBool::new(false); - - fn sink() -> &'static Mutex>> { - static SINK: OnceLock>>> = OnceLock::new(); - SINK.get_or_init(|| { - let w = std::env::var_os("IRIS_MGRAS_TRACE").and_then(|p| open(&p).ok()); - ON.store(w.is_some(), Ordering::Relaxed); - Mutex::new(w) - }) + if rest.words.iter().any(|&w| w != 0) { + let mut h = blake3::Hasher::new(); + for w in rest.words.iter() { + h.update(&w.to_le_bytes()); + } + out.extend_from_slice(h.finalize().as_bytes()); + } + out } - fn open(path: &std::ffi::OsStr) -> std::io::Result> { - let f = std::fs::OpenOptions::new().create(true).append(true).open(path)?; - Ok(std::io::BufWriter::new(f)) + /// Hash of `state_bytes`, waiting for the board to be idle. Call with + /// `submit` held. + fn idle_state_hash(&self) -> [u8; 32] { + self.wait_idle(); + *blake3::hash(&self.state_bytes()).as_bytes() } - /// Start tracing to `path`, or stop with `None`. - pub fn set(path: Option<&str>) -> std::io::Result<()> { - let mut s = sink().lock(); - if let Some(mut w) = s.take() { - let _ = w.flush(); + /// Start a recording at `path` (checkpointing the state it starts from), + /// or stop the running one with `None` (checkpointing where it ends). + fn set_recording(&self, path: Option<&str>) -> std::io::Result<()> { + let mut sub = self.submit.lock(); + if let Some(rec) = sub.take() { + *self.mem_rec.lock() = None; + let h = self.idle_state_hash(); + let mut r = rec.lock(); + r.put(record::Rec::Hash(h)); + r.flush(); } if let Some(p) = path { - *s = Some(open(std::ffi::OsStr::new(p))?); + let rec = record::Recorder::create(p)?; + rec.lock().put(record::Rec::Hash(self.idle_state_hash())); + *self.mem_rec.lock() = Some(rec.clone()); + *sub = Some(rec); } - ON.store(s.is_some(), Ordering::Relaxed); Ok(()) } - pub fn init() { - let _ = sink(); + /// Checkpoint the running recording now. Returns the hash and record + /// count, or None when not recording. + fn record_mark(&self) -> Option<([u8; 32], u64)> { + let sub = self.submit.lock(); + let rec = sub.as_ref()?; + let h = self.idle_state_hash(); + let mut r = rec.lock(); + r.put(record::Rec::Hash(h)); + r.flush(); + Some((h, r.records)) } - pub fn note(dir: char, bits: u32, addr: u32, val: u64) { - if !ON.load(Ordering::Relaxed) { - return; - } - if let Some(w) = sink().lock().as_mut() { - let _ = writeln!(w, "{dir}{bits} {addr:08x} {val:x}"); + /// Composite a host GL frame at a screen position (see `disp::composite`). + pub fn composite(&self, x: i32, y: i32, bgra: &[u8], stride: usize, w: usize, h: usize) -> bool { + let sub = self.submit.lock(); + self.wait_idle(); + // SAFETY: idle with `submit` held. + let rss = unsafe { &mut *self.rss.get() }; + let ok = disp::composite(rss, &self.front().dcb, x, y, bgra, stride, w, h); + if ok { + if let Some(rec) = sub.as_ref() { + rec.lock().put(record::Rec::Composite); + } + self.dirty.store(true, Ordering::Release); } + ok } -} -/// The board's three interrupt lines to the GIO slot. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Line { - /// GIO interrupt 0: command FIFO high/low water. - Fifo, - /// GIO interrupt 1: the general graphics interrupt. - General, - /// GIO interrupt 2: vertical retrace. - Retrace, -} -pub struct Mgras { - kind: ImpactSlot, - ioc: crate::dev::ioc::Ioc, - /// System memory, for the board's DMA (it is a GIO bus master). - sys_mem: Mutex>>, - /// Current level of the general interrupt line. - general: AtomicBool, - board: Mutex, - dirty: AtomicBool, - running: AtomicBool, - refresh: Mutex>>, - renderer: Mutex>>, - /// The finished frame, handed to the renderer as a prebuilt picture - /// (`Rex3Screen::prebuilt`), as GR2 does. - screen: Mutex, - screenshot_pending: AtomicBool, - heartbeat: Arc, - fasttick: Arc, - cycles: Mutex, -} + /// Framebuffer pixel (its 24 colour planes) at display (`x`, `y`), y = 0 + /// the top row, once the board has run everything queued (tests). + #[cfg(test)] + pub(crate) fn fb_pixel(&self, x: usize, y: usize) -> u32 { + let _sub = self.submit.lock(); + self.wait_idle(); + // SAFETY: idle with `submit` held. + let dcb = &self.front().dcb; + let h = frame::display_size(dcb).1; + let rss = unsafe { &*self.rss.get() }; + let (main, _) = frame::scanout_buffers(rss, dcb); + // The colour planes (alpha, in the top byte, is not displayed). + rss.mem.get(&main, x as u32, (h - 1 - y) as u32) as u32 & 0xFF_FFFF + } -impl Mgras { - pub fn new(cfg: &ImpactSection, ioc: crate::dev::ioc::Ioc, heartbeat: Arc, fasttick: Arc) -> Self { - trace::init(); - Mgras { - kind: cfg.gfx, - ioc, - sys_mem: Mutex::new(None), - general: AtomicBool::new(false), - board: Mutex::new(Board::new(cfg.gfx)), - dirty: AtomicBool::new(true), - running: AtomicBool::new(false), - refresh: Mutex::new(None), - renderer: Mutex::new(None), - screen: Mutex::new(crate::disp::Rex3Screen::new()), - screenshot_pending: AtomicBool::new(false), - heartbeat, - fasttick, - cycles: Mutex::new(crate::cpu::mips_core::CyclesPtr::dangling()), - } + /// The VC3's display size, from its timing tables and its DID table. + #[cfg(test)] + pub(crate) fn vc3_sizes(&self) -> (Option<(usize, usize)>, usize) { + let f = self.front(); + (f.dcb.vc3.timing_size(), f.dcb.vc3.did_lines()) } - /// Composite a host GL frame at a screen position (see `Board::composite`). - pub fn composite(&self, x: i32, y: i32, bgra: &[u8], stride: usize, w: usize, h: usize) -> bool { - let ok = self.board.lock().composite(x, y, bgra, stride, w, h); - if ok { - self.dirty.store(true, Ordering::Release); - } - ok + /// Hash of the state a replay must reproduce (`state_bytes`). + #[cfg(test)] + pub(crate) fn state_hash(&self) -> [u8; 32] { + let _sub = self.submit.lock(); + self.idle_state_hash() + } + + /// A frame snapshot of what is displayed now (monitor, screenshots). + fn snapshot_frame(&self) -> Box { + let mut f = plain::boxed_zeroed::(); + // SAFETY: a read-only view; tearing tolerated. + let rss = unsafe { &*self.rss.get() }; + f.snapshot(rss, &self.front().dcb); + f + } + + /// Save the displayed frame as a PNG. + pub(crate) fn save_shot(&self, path: &str) -> Result<(), String> { + let mut out = vec![0u32; frame::OUT_STRIDE * frame::H]; + let f = self.snapshot_frame(); + f.compose(&mut out); + disp::save_png(path, &out, f.width, f.height) } /// Give the board its path to system memory, for DMA. pub fn set_phys(&self, mem: Arc) { - self.board.lock().mem = Some(mem.clone()); - *self.sys_mem.lock() = Some(mem); + *self.mem.lock() = Some(mem); } /// Drive one of the board's interrupt lines (graphics slot wiring). @@ -957,146 +652,318 @@ impl Mgras { (off < MAP_SIZE).then_some(off) } - /// Bring the general interrupt line in line with the board's flags. - fn update_general(&self, level: bool) { + /// Flags as the host reads them. + fn all_flags(&self, f: &Front) -> u32 { + self.flags.load(Ordering::Acquire) | f.derived_flags() + } + + /// Bring the general interrupt line (GIO line 1) in line with the flags: + /// asserted while some enabled flag is set, a level held until the + /// handler clears the flag or its enable. Decided and driven under the + /// front lock, so two threads cannot leave a stale level behind. + fn refresh_general(&self) { + let f = self.front(); + let level = self.all_flags(&f) & f.hq.interrupt_enable & host::INTR_CAUSES != 0; if self.general.swap(level, Ordering::AcqRel) != level { self.set_line(Line::General, level); } } - fn do_read(&self, addr: u32, bits: u32) -> u64 { - let v = match self.offset(addr) { - Some(off) => { - let mut b = self.board.lock(); - let v = b.read(off, bits); - let irq = b.general_irq(); - drop(b); - self.update_general(irq); - v + /// Reads whose answer depends on what the engines have done wait for + /// them (bus busy) until the board is idle. + fn read_needs_idle(off: u32) -> bool { + matches!(off, + host::STATUS | host::FIFOSTATUS | host::GIOSTATUS | host::DMABUSY + | host::GE_READBACK_HI | host::GE_READBACK_LO + | host::RASTER_IF_CONTEXT..=0x50228 | host::DMA_CONTEXT..=0x504FF + | host::RASTER..=0x7FFFF) + } + + /// One read, with `submit` held. None means "busy, retry". + fn read_locked(&self, off: u32, bits: u32) -> Option { + if Self::read_needs_idle(off) && !self.idle() { + return None; + } + // SAFETY (for both engines below): idle with `submit` held. + let v = match off { + host::STATUS => Front::status() as u64, + host::FIFOSTATUS | host::GIOSTATUS | host::DMABUSY => 0, + host::SET_FLAGS | host::CLEAR_FLAGS | host::SET_FLAGS_PRIVILEGED | host::CLEAR_FLAGS_PRIVILEGED => { + self.all_flags(&self.front()) as u64 } - None => 0, + host::RASTER_IF_CONTEXT..=0x50228 => { + unsafe { &*self.eng.get() }.raster_if_regs[((off - host::RASTER_IF_CONTEXT) / 4) as usize] as u64 + } + host::DMA_CONTEXT..=0x504FF => { + unsafe { &*self.eng.get() }.dma_regs[((off - host::DMA_CONTEXT) / 4) as usize] as u64 + } + host::PIO_READ_HI => { + // Taking the high half moves the stream on; the RSS does + // that, in order (the FIFO is empty, so this fits). + let v = unsafe { &*self.rss.get() }.pio_peek_hi(); + self.hq_fifo.push(tag::PIO_ADVANCE, 0); + Self::wake(&self.hq_thread); + v as u64 + } + host::PIO_READ_LO => unsafe { &*self.rss.get() }.pio_read_lo() as u64, + // A state readback's answer, once the HQ has given one; before + // that, the diagnostic port's. + host::GE_READBACK_HI | host::GE_READBACK_LO => match unsafe { &*self.eng.get() }.ge_return { + Some(r) => r[(off == host::GE_READBACK_LO) as usize] as u64, + None => self.front().read(off, bits), + }, + host::RASTER..=0x7FFFF => { + let rss = unsafe { &*self.rss.get() }; + let r = (off & 0xFFC) >> 2; + if bits == 64 { + ((rss.read(r) as u64) << 32) | rss.read(r + 1) as u64 + } else { + rss.read(r) as u64 + } + } + _ => self.front().read(off, bits), }; - trace::note('R', bits, addr, v); - v + Some(v) } - fn do_write(&self, addr: u32, bits: u32, val: u64) { - trace::note('W', bits, addr, val); - if let Some(off) = self.offset(addr) { - let mut b = self.board.lock(); - let changed = b.write(off, bits, val); - let irq = b.general_irq(); - drop(b); - if changed { - self.dirty.store(true, Ordering::Release); + /// One write, with `submit` held. False means "busy, retry". + fn write_locked(&self, off: u32, bits: u32, val: u64) -> bool { + let (hi, lo) = ((val >> 32) as u32, val as u32); + match off { + host::CFIFO | 0x70084 | host::CFIFO_GL | host::CFIFO_PRIVILEGED | 0x50084 => { + let port = if off >= host::STATUS { tag::PORT_USER } else { tag::PORT_PRIVILEGED }; + let ok = if bits == 64 { + // Both words or neither: a retried store must not push + // the first twice. + self.hq_fifo.try_push2(port, hi as u64, port, lo as u64) + } else { + self.hq_fifo.try_push(port, lo as u64) + }; + if !ok { + return false; + } + Self::wake(&self.hq_thread); + } + host::RASTER..=0x7FFFF => { + let r = (off & 0xFFC) >> 2; + // Registers at 0x7C000 + 4r; the same register at +0x1000 + // also executes the primitive in the IR after the write. + let exec = off & 0x1000 != 0; + let ok = if bits == 64 { + self.hq_fifo.try_push2(tag::RSS | rss_entry(r, false), hi as u64, tag::RSS | rss_entry(r + 1, exec), lo as u64) + } else { + self.hq_fifo.try_push(tag::RSS | rss_entry(r, exec), lo as u64) + }; + if !ok { + return false; + } + Self::wake(&self.hq_thread); + } + host::CONTEXT_SWITCH => { + // The save phase completes at once; the incoming context + // follows through the FIFO, after this marker. + if !self.hq_fifo.try_push(tag::CONTEXT_SWITCH, 0) { + return false; + } + self.flags.fetch_or(host::FLAG_CONTEXT_SAVED, Ordering::AcqRel); + Self::wake(&self.hq_thread); + } + host::SET_FLAGS | host::SET_FLAGS_PRIVILEGED => { + self.flags.fetch_or(lo, Ordering::AcqRel); + } + host::CLEAR_FLAGS | host::CLEAR_FLAGS_PRIVILEGED => { + self.flags.fetch_and(!lo, Ordering::AcqRel); + } + 0x80000..=0xFFFFF => self.note(format!("fast-path command window write at {off:#x}")), + _ => { + if self.front().write(off, bits, val, &self.flags) { + self.dirty.store(true, Ordering::Release); + } } - self.update_general(irq); } + true } - fn refresh_loop(self: &Arc) { - let frame = std::time::Duration::from_micros(16_667); - { - let mut screen = self.screen.lock(); - screen.width = raster::WIDTH; - screen.height = raster::HEIGHT; - screen.fb_rgb.fill(0xFF00_0000); - screen.prebuilt = true; + /// Writes that go through `hq_fifo` (traced as HQ/RSS lines instead). + fn is_fifo_write(off: u32) -> bool { + matches!(off, host::CFIFO | 0x70084 | host::CFIFO_GL | host::CFIFO_PRIVILEGED | 0x50084 | host::RASTER..=0x7FFFF) + } + + fn do_read(&self, addr: u32, bits: u32) -> Option { + let Some(off) = self.offset(addr) else { return Some(0) }; + let sub = self.submit.lock(); + let v = self.read_locked(off, bits)?; + if let Some(rec) = sub.as_ref() { + rec.lock().put(record::Rec::Read { bits: bits as u8, off, val: v }); + } + drop(sub); + if self.tracing(debug::TRACE_CPU) { + self.trace_cpu(false, off, v); } - let mut overlay = crate::debug_overlay::DebugOverlay::new(); - let mut status_bar = crate::disp::StatusBar::new(); - let mut sbtex = crate::disp::StatusBarTexture::new(); - let mut sized = false; - let mut last_pending = 0u64; - let mut idle_frames = 0u32; - const PERSISTENT: u64 = crate::dev::ng1::rex3::Rex3::HB_LED_RED | crate::dev::ng1::rex3::Rex3::HB_LED_GREEN; - - while self.running.load(Ordering::Relaxed) { - let start = std::time::Instant::now(); - let stats = crate::disp::BarStats { - now: start, - hb: self.heartbeat.fetch_and(PERSISTENT, Ordering::Relaxed), - cycles: self.cycles.lock().get(), - fasttick: self.fasttick.load(Ordering::Relaxed), - decoded_delta: 0, - l1i_hits: 0, - l1i_fetches: 0, - uncached: 0, - count_hz: 0, - gfifo_pending: 0, + self.refresh_general(); + Some(v) + } + + fn do_write(&self, addr: u32, bits: u32, val: u64) -> u32 { + let Some(off) = self.offset(addr) else { return BUS_OK }; + let sub = self.submit.lock(); + if !self.write_locked(off, bits, val) { + return BUS_BUSY; + } + if let Some(rec) = sub.as_ref() { + let due = { + let mut r = rec.lock(); + r.put(record::Rec::Write { bits: bits as u8, off, val }); + r.checkpoint_due() }; - let retrace = { - let mut b = self.board.lock(); - if b.raster.flush_if_stale(&mut last_pending) { - self.dirty.store(true, Ordering::Release); + if due { + let h = self.idle_state_hash(); + rec.lock().put(record::Rec::Hash(h)); + } + } + drop(sub); + if self.tracing(debug::TRACE_CPU) && !Self::is_fifo_write(off) { + self.trace_cpu(true, off, val); + } + self.refresh_general(); + BUS_OK + } + + // ── engine threads ─────────────────────────────────────────────────────── + + fn hq_loop(&self) { + *self.hq_thread.lock() = Some(thread::current()); + let mut sink = HqSink(self); + let backoff = crossbeam_utils::Backoff::new(); + loop { + let Some((t, val)) = self.hq_fifo.peek() else { + self.hq_fifo.flush_head(); + self.hq_busy.store(false, Ordering::Release); + if backoff.is_completed() { + thread::park_timeout(std::time::Duration::from_millis(2)); + } else { + backoff.snooze(); } - b.retrace_enabled() + continue; }; - // Vertical retrace: a pulse per frame. The handler acknowledges - // nothing on the board, so the line must drop again by itself. - if retrace { - self.set_line(Line::Retrace, true); - std::thread::sleep(std::time::Duration::from_micros(500)); - self.set_line(Line::Retrace, false); + self.hq_busy.store(true, Ordering::Release); + // SAFETY: the HQ3 thread owns the engine. + let eng = unsafe { &mut *self.eng.get() }; + match t { + tag::EXIT => break, + tag::PORT_USER | tag::PORT_PRIVILEGED => eng.push(t as usize, val as u32, &mut sink), + tag::CONTEXT_SWITCH => eng.begin_context_switch(&mut sink), + tag::PIO_ADVANCE => self.rss_push(t, 0), + t if t & !0x7FF == tag::RSS => self.rss_push(t & 0x7FF | RSS_SRC_CPU, val), + _ => {} } - let dirty = self.dirty.swap(false, Ordering::AcqRel); - let shot = self.screenshot_pending.swap(false, Ordering::Relaxed); - idle_frames += 1; - if dirty || shot || idle_frames >= 6 { - idle_frames = 0; - let mut screen = self.screen.lock(); - if dirty || shot { - let screen = &mut *screen; - self.board.lock().scanout(&mut screen.fb_rgb); - // 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. - for y in 0..raster::HEIGHT { - let row = y * 2048; - screen.rgba[row..row + raster::WIDTH].copy_from_slice(&screen.fb_rgb[row..row + raster::WIDTH]); - } - screen.status_bar_only = false; + self.hq_fifo.consume(); + backoff.reset(); + } + self.hq_fifo.consume(); + self.hq_fifo.flush_head(); + self.hq_busy.store(false, Ordering::Release); + } + + fn rss_loop(&self) { + *self.rss_thread.lock() = Some(thread::current()); + let backoff = crossbeam_utils::Backoff::new(); + let mut stale_seen = 0u32; + let mut payload: Vec = Vec::new(); + loop { + let Some((t, val)) = self.rss_fifo.peek() else { + self.rss_fifo.flush_head(); + self.rss_busy.store(false, Ordering::Release); + if backoff.is_completed() { + thread::park_timeout(std::time::Duration::from_millis(2)); } else { - screen.status_bar_only = true; + backoff.snooze(); + } + continue; + }; + self.rss_busy.store(true, Ordering::Release); + // SAFETY: the RSS thread owns the raster subsystem. + let rss = unsafe { &mut *self.rss.get() }; + let changed = match t { + tag::EXIT => break, + t if t < 0x1000 => rss.write((t & 0x7FF) >> 1, val as u32, t & 1 != 0), + tag::PIO_ADVANCE => { + rss.pio_read_hi(); + false + } + tag::GL_ENTER => { + rss.gl_enter(); + false } - if let Some(r) = self.renderer.lock().as_mut() { - if !sized { - r.resize(raster::WIDTH, raster::HEIGHT); - sized = true; - } - r.present(&mut screen, &mut overlay, &mut status_bar, &mut sbtex, &stats, shot, None, None); + tag::GL_LEAVE => { + rss.gl_leave(); + false } + tag::DMA_LINE => { + let (line, len) = (val as u32, (val >> 32) as usize); + payload.resize(len.div_ceil(8), 0); + let n = payload.len(); + self.rss_fifo.drain_payload(n, &mut payload[..]); + let bytes: Vec = payload.iter().flat_map(|w| w.to_be_bytes()).take(len).collect(); + rss.dma_write_line(line, &bytes); + true + } + _ => false, + }; + if changed { + self.dirty.store(true, Ordering::Release); } - if let Some(rest) = frame.checked_sub(start.elapsed()) { - std::thread::sleep(rest); + if self.tracing(debug::TRACE_RSS) { + self.trace_rss(t, val, rss, changed); } + if rss.te.stale_events != stale_seen { + stale_seen = rss.te.stale_events; + if self.tracing(debug::TRACE_TEX) { + let [want, have] = rss.te.stale_last; + self.trace_hq(&format!("TEX stale sample #{stale_seen}: want {want:#x}, page holds {have:#x}")); + } + } + self.rss_fifo.consume(); + backoff.reset(); } - // The renderer's GL state belongs to this thread (its context is - // current here), so it is torn down here and nowhere else. - if let Some(r) = self.renderer.lock().as_mut() { - r.stop(); - } + self.rss_fifo.consume(); + self.rss_fifo.flush_head(); + self.rss_busy.store(false, Ordering::Release); } - /// Start the display refresh thread. - pub fn start_display(self: &Arc) { - if self.running.swap(true, Ordering::AcqRel) { + /// Start the HQ3 and RSS threads. + pub fn start_engines(self: &Arc) { + if self.engines.swap(true, Ordering::AcqRel) { return; } + let mut threads = self.threads.lock(); + let me = Arc::clone(self); + threads.push(thread::Builder::new().name("MGRAS-HQ3".into()).spawn(move || me.hq_loop()).expect("spawn MGRAS HQ3 thread")); let me = Arc::clone(self); - *self.refresh.lock() = Some( - std::thread::Builder::new() - .name("MGRAS-Refresh".into()) - .spawn(move || me.refresh_loop()) - .expect("spawn MGRAS refresh thread"), - ); + threads.push(thread::Builder::new().name("MGRAS-RSS".into()).spawn(move || me.rss_loop()).expect("spawn MGRAS RSS thread")); } - pub fn stop_display(&self) { - self.running.store(false, Ordering::Release); - if let Some(h) = self.refresh.lock().take() { + /// Stop the engine threads once they have run everything queued. + pub fn stop_engines(&self) { + if !self.engines.swap(false, Ordering::AcqRel) { + return; + } + { + let _sub = self.submit.lock(); + self.hq_fifo.push(tag::EXIT, 0); + Self::wake(&self.hq_thread); + } + let mut threads = self.threads.lock(); + if let Some(h) = (!threads.is_empty()).then(|| threads.remove(0)) { + let _ = h.join(); + } + self.rss_fifo.push(tag::EXIT, 0); + Self::wake(&self.rss_thread); + for h in threads.drain(..) { let _ = h.join(); } + self.hq_fifo.reset(); + self.rss_fifo.reset(); } } @@ -1115,99 +982,21 @@ impl Device for Mgras { fn get_clock(&self) -> u64 { 0 } fn register_commands(&self) -> Vec<(String, String)> { - vec![("mgras".into(), "IMPACT graphics: mgras (board state) | mgras shot (save the displayed frame) | mgras dump (raw display state) | mgras trace |off".into())] + vec![ + ("mgras".into(), "IMPACT graphics: status|hq|gl|ge|ucode|vc3|xmap|cmap|dac|pix|stats|fbdump|shot|dump|trace|rec (mgras help)".into()), + ("rss".into(), "IMPACT raster subsystem: regs|pix".into()), + ] } fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { - if cmd != "mgras" { - return Err(format!("unknown command: {cmd}")); - } - if let ["trace", what] = args { - let r = if *what == "off" { trace::set(None) } else { trace::set(Some(what)) }; - r.map_err(|e| format!("mgras trace: {e}"))?; - return writeln!(w, "trace {what}").map_err(|e| e.to_string()); - } - if let ["dump", path] = args { - let b = self.board.lock(); - dump_state(path, &b).map_err(|e| format!("mgras dump: {e}"))?; - return writeln!(w, "dumped {path}").map_err(|e| e.to_string()); - } - if let ["shot", path] = args { - let mut frame = vec![0u32; 2048 * raster::HEIGHT]; - self.board.lock().scanout(&mut frame); - save_png(path, &frame).map_err(|e| format!("mgras shot: {e}"))?; - return writeln!(w, "saved {path}").map_err(|e| e.to_string()); - } - let b = self.board.lock(); - let e = |r: std::io::Result<()>| r.map_err(|e| e.to_string()); - e(writeln!(w, "IMPACT {:?} at {:#010x}", self.kind, MGRAS_SLOT_GFX_BASE))?; - e(writeln!(w, " flags {:#010x} DAC pixmask {:#04x} XMAP DID0 mode {:#x}", - b.flags, b.dcb.dac.pixmask(), b.dcb.xmap.main_mode(0)))?; - e(writeln!(w, " fill modes seen: {:x?}", b.raster.fillmodes_seen))?; - let mut hist: HashMap = HashMap::new(); - for v in &b.raster.fb { - *hist.entry(*v).or_default() += 1; + match cmd { + "mgras" => self.cmd_mgras(args, &mut *w), + "rss" => self.cmd_rss(args, &mut *w), + _ => Err(format!("unknown command: {cmd}")), } - let mut top: Vec<_> = hist.into_iter().collect(); - top.sort_by(|a, b| b.1.cmp(&a.1)); - e(writeln!(w, " framebuffer values (value, pixels): {:x?}", &top[..top.len().min(12)]))?; - let pal = &b.dcb.cmap[0].pal; - let blocks: Vec = (0..pal.len() / 256) - .filter(|k| pal[k * 256..(k + 1) * 256].iter().any(|c| *c != 0)) - .collect(); - e(writeln!(w, " colormap blocks in use (of {}): {:?}", pal.len() / 256, blocks))?; - for k in blocks.iter().take(4) { - e(writeln!(w, " block {k}: {:06x?}", &pal[k * 256..k * 256 + 8]))?; - } - let modes: Vec<(u32, u32)> = (0..32).map(|d| (d, b.dcb.xmap.main_mode(d))).filter(|m| m.1 != 0).collect(); - e(writeln!(w, " XMAP main modes (did, mode): {:x?}", modes))?; - let mut runs = Vec::new(); - for y in [0usize, 100, 400, 700, 1023] { - b.dcb.vc3.main_did_runs(y, &mut runs); - e(writeln!(w, " scanline {y} DID runs: {:?}", runs))?; - } - for u in &b.unhandled { - e(writeln!(w, " not modelled: {u}"))?; - } - Ok(()) } } -/// Save the display state for offline study: little-endian u32 sections, in -/// order: framebuffer (`WIDTH * HEIGHT`, row 0 at the bottom), overlay (same -/// size), colormap 0, the 32 main XMAP modes, then the video timing chip's -/// registers and SRAM as u32s. -fn dump_state(path: &str, b: &Board) -> std::io::Result<()> { - let mut f = std::io::BufWriter::new(std::fs::File::create(path)?); - let mut put = |v: u32| f.write_all(&v.to_le_bytes()); - for v in b.raster.fb.iter().chain(b.raster.overlay.iter()).chain(b.dcb.cmap[0].pal.iter()) { - put(*v)?; - } - for d in 0..32 { - put(b.dcb.xmap.main_mode(d))?; - } - for v in b.dcb.vc3.regs.iter().chain(b.dcb.vc3.sram.iter()) { - put(*v as u32)?; - } - Ok(()) -} - -/// Write a scanned-out frame (`0xFF_BB_GG_RR`, stride 2048) as a PNG. -fn save_png(path: &str, frame: &[u32]) -> Result<(), String> { - let file = std::fs::File::create(path).map_err(|e| e.to_string())?; - let mut enc = png::Encoder::new(std::io::BufWriter::new(file), raster::WIDTH as u32, raster::HEIGHT as u32); - enc.set_color(png::ColorType::Rgb); - enc.set_depth(png::BitDepth::Eight); - let mut out = enc.write_header().map_err(|e| e.to_string())?; - let mut rows = Vec::with_capacity(raster::WIDTH * raster::HEIGHT * 3); - for y in 0..raster::HEIGHT { - for px in &frame[y * 2048..y * 2048 + raster::WIDTH] { - rows.extend_from_slice(&[*px as u8, (px >> 8) as u8, (px >> 16) as u8]); - } - } - out.write_image_data(&rows).map_err(|e| e.to_string()) -} - impl Saveable for Mgras { // Bring-up: board state is not snapshotted yet. fn save_state(&self) -> toml::Value { @@ -1219,19 +1008,38 @@ impl Saveable for Mgras { } } +fn read_status(v: Option) -> (u32, u64) { + match v { + Some(v) => (BUS_OK, v), + None => (BUS_BUSY, 0), + } +} + impl BusDevice for Mgras { - fn read32(&self, addr: u32) -> BusRead32 { BusRead32::ok(self.do_read(addr, 32) as u32) } - fn write32(&self, addr: u32, val: u32) -> u32 { self.do_write(addr, 32, val as u64); BUS_OK } - fn read8(&self, addr: u32) -> BusRead8 { BusRead8::ok(self.do_read(addr, 8) as u8) } - fn write8(&self, addr: u32, val: u8) -> u32 { self.do_write(addr, 8, val as u64); BUS_OK } - fn read16(&self, addr: u32) -> BusRead16 { BusRead16::ok(self.do_read(addr, 16) as u16) } - fn write16(&self, addr: u32, val: u16) -> u32 { self.do_write(addr, 16, val as u64); BUS_OK } - fn read64(&self, addr: u32) -> BusRead64 { BusRead64::ok(self.do_read(addr, 64)) } - fn write64(&self, addr: u32, val: u64) -> u32 { self.do_write(addr, 64, val); BUS_OK } + fn read32(&self, addr: u32) -> BusRead32 { + let (status, data) = read_status(self.do_read(addr, 32)); + BusRead32 { status, data: data as u32 } + } + fn write32(&self, addr: u32, val: u32) -> u32 { self.do_write(addr, 32, val as u64) } + fn read8(&self, addr: u32) -> BusRead8 { + let (status, data) = read_status(self.do_read(addr, 8)); + BusRead8 { status, data: data as u8 } + } + fn write8(&self, addr: u32, val: u8) -> u32 { self.do_write(addr, 8, val as u64) } + fn read16(&self, addr: u32) -> BusRead16 { + let (status, data) = read_status(self.do_read(addr, 16)); + BusRead16 { status, data: data as u16 } + } + fn write16(&self, addr: u32, val: u16) -> u32 { self.do_write(addr, 16, val as u64) } + fn read64(&self, addr: u32) -> BusRead64 { + let (status, data) = read_status(self.do_read(addr, 64)); + BusRead64 { status, data } + } + fn write64(&self, addr: u32, val: u64) -> u32 { self.do_write(addr, 64, val) } } /// The board as the display host GL presents into: frames land in the -/// framebuffer under their window (see `Board::composite`). It cannot know the +/// framebuffer under their window (see `disp::composite`). It cannot know the /// guest X server's window ids, so only frames that say where their window is /// are taken; the rest go back to the program to put up itself. #[cfg(feature = "hostgl")] diff --git a/src/dev/mgras/pixmem.rs b/src/dev/mgras/pixmem.rs new file mode 100644 index 00000000..4e6a0838 --- /dev/null +++ b/src/dev/mgras/pixmem.rs @@ -0,0 +1,145 @@ +//! Pixel memory: the RDRAM behind one raster engine, as pages. +//! +//! The board has no linear framebuffer. RDRAM bytes are 9 bits wide, and a +//! page (2KB on each of the six chips) is one tile of the screen: 192 x 16 +//! pixels of a 36-bit buffer, or 768 x 16 pixels of the 9-bit overlay +//! buffer, four to a 36-bit word. A buffer is a run of pages starting at a +//! page pointer (`DRBpointers` for drawing, the XMAP DIB pointer for +//! scanout); pixel (x, y) is in page `ptr + (y / 16) * xtiles + x / tile +//! width`, `xtiles` coming from `DRBsize`. See ignore/gr4/PP1.h for how the +//! real board spreads a page over its chips; only the raw RDRAM window +//! (the RE4's indirect device space) could see that order, so here a page +//! is 3072 words in plain row order and an overlay word holds four +//! neighbouring pixels, lowest x in the low bits. +//! +//! One raster engine only: with two, scanlines alternate between them and +//! each has its own pages. + +/// Pages behind one raster engine (2MB RDRAMs, 2KB pages). +pub const PAGES: usize = 1024; +pub const TILE_W: usize = 192; +pub const TILE_H: usize = 16; +pub const PAGE_WORDS: usize = TILE_W * TILE_H; +/// Overlay tile width: four 9-bit pixels a word. +pub const OVERLAY_TILE_W: usize = 4 * TILE_W; +/// Bits of a word the buffers use (four 9-bit bytes). +pub const WORD_MASK: u64 = 0xF_FFFF_FFFF; + +/// `DRBsize` bits 1:0: overlay tiles per row; bits 5:2: 36-bit tiles per +/// row. +pub fn xtiles(drbsize: u32) -> (u32, u32) { + match ((drbsize >> 2) & 0xF, drbsize & 3) { + // Not programmed yet: the PROM's 1280x1024 layout (7 and 2). + (0, _) => (7, 2), + t => t, + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Kind { + /// A 36-bit buffer: one pixel a word. + Wide, + /// The 9-bit overlay buffer: four pixels a word, 9 bits each. + Overlay, +} + +/// Where a buffer is and how its words hold pixels. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Buffer { + pub ptr: u32, + pub kind: Kind, + /// Tiles per row for this kind. + pub xtiles: u32, +} + +impl Buffer { + pub fn new(ptr: u32, kind: Kind, drbsize: u32) -> Self { + let (x36, x9) = xtiles(drbsize); + Buffer { ptr: ptr & 0x3FF, kind, xtiles: if kind == Kind::Wide { x36 } else { x9 } } + } + + /// Word index, bit shift and value mask of pixel (x, y). + #[inline] + pub fn locate(&self, x: u32, y: u32) -> (usize, u32, u64) { + let (x, y) = (x as usize, y as usize); + let tile_w = if self.kind == Kind::Wide { TILE_W } else { OVERLAY_TILE_W }; + let page = (self.ptr as usize + (y / TILE_H) * self.xtiles as usize + x / tile_w) % PAGES; + let base = page * PAGE_WORDS + (y % TILE_H) * TILE_W; + match self.kind { + Kind::Wide => (base + x % TILE_W, 0, WORD_MASK), + Kind::Overlay => (base + (x % OVERLAY_TILE_W) / 4, 9 * (x as u32 % 4), 0x1FF), + } + } +} + +/// The pages. Plain data, valid zeroed (build in place, never by value). +#[repr(C)] +pub struct PixMem { + pub words: [u64; PAGES * PAGE_WORDS], +} + +impl PixMem { + #[inline] + pub fn get(&self, b: &Buffer, x: u32, y: u32) -> u64 { + let (i, shift, mask) = b.locate(x, y); + (self.words[i] >> shift) & mask + } + + #[inline] + pub fn put(&mut self, b: &Buffer, x: u32, y: u32, v: u64) { + let (i, shift, mask) = b.locate(x, y); + self.words[i] = (self.words[i] & !(mask << shift)) | ((v & mask) << shift); + } + + /// Row `y` of a buffer, pixels `0..out.len()`, as u32s. + pub fn read_row(&self, b: &Buffer, y: u32, out: &mut [u32]) { + match b.kind { + Kind::Wide => { + for (tx, chunk) in out.chunks_mut(TILE_W).enumerate() { + let (i, _, _) = b.locate((tx * TILE_W) as u32, y); + let n = chunk.len(); + for (o, w) in chunk.iter_mut().zip(&self.words[i..i + n]) { + *o = *w as u32; + } + } + } + Kind::Overlay => { + for (x, o) in out.iter_mut().enumerate() { + *o = self.get(b, x as u32, y) as u32; + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wide_pixels_tile_by_192_by_16() { + let b = Buffer::new(576, Kind::Wide, 0x31E); + assert_eq!(b.locate(0, 0), (576 * PAGE_WORDS, 0, WORD_MASK)); + assert_eq!(b.locate(191, 15).0, 576 * PAGE_WORDS + 15 * 192 + 191); + // Next tile across, then the next tile row (7 tiles at 1280). + assert_eq!(b.locate(192, 0).0, 577 * PAGE_WORDS); + assert_eq!(b.locate(0, 16).0, 583 * PAGE_WORDS); + // The last row of a 1280x1024 buffer ends at page 1023. + assert_eq!(b.locate(1279, 1023).0 / PAGE_WORDS, 1023); + } + + #[test] + fn overlay_packs_four_pixels_a_word() { + let b = Buffer::new(448, Kind::Overlay, 0x31E); + assert_eq!(b.locate(0, 0), (448 * PAGE_WORDS, 0, 0x1FF)); + assert_eq!(b.locate(3, 0), (448 * PAGE_WORDS, 27, 0x1FF)); + assert_eq!(b.locate(4, 0).0, 448 * PAGE_WORDS + 1); + assert_eq!(b.locate(768, 0).0, 449 * PAGE_WORDS); + // 1280x1024 overlay: 2 tiles x 64 rows = 128 pages, up to main's 576. + assert_eq!(b.locate(1279, 1023).0 / PAGE_WORDS, 575); + let mut m = crate::dev::mgras::plain::boxed_zeroed::(); + m.put(&b, 1, 5, 0x1AB); + m.put(&b, 2, 5, 0x0FF); + assert_eq!((m.get(&b, 0, 5), m.get(&b, 1, 5), m.get(&b, 2, 5)), (0, 0x1AB, 0xFF)); + } +} diff --git a/src/dev/mgras/plain.rs b/src/dev/mgras/plain.rs new file mode 100644 index 00000000..0d77de02 --- /dev/null +++ b/src/dev/mgras/plain.rs @@ -0,0 +1,141 @@ +//! Plain-data containers for board state: valid when zeroed, no heap, so a +//! whole block can be built in place and read by another thread without +//! a reallocation pulling memory out from under it. + +/// A fixed-capacity u32 -> u32 map (open addressing, linear probing) for +/// sparse register spaces. Absent keys read as 0. When full, new keys are +/// dropped (`overflowed` says so) rather than evicting. +#[repr(C)] +pub struct RegMap { + keys: [u32; N], + vals: [u32; N], + used: [bool; N], + pub len: usize, + pub overflowed: bool, +} + +impl RegMap { + const MASK: usize = { + assert!(N.is_power_of_two(), "RegMap capacity must be a power of two"); + N - 1 + }; + + fn slot(&self, key: u32) -> (usize, bool) { + let mut i = (key.wrapping_mul(0x9E37_79B1) >> 7) as usize & Self::MASK; + for _ in 0..N { + if !self.used[i] { + return (i, false); + } + if self.keys[i] == key { + return (i, true); + } + i = (i + 1) & Self::MASK; + } + (0, false) + } + + pub fn lookup(&self, key: u32) -> Option { + match self.slot(key) { + (i, true) => Some(self.vals[i]), + _ => None, + } + } + + pub fn get(&self, key: u32) -> u32 { + match self.slot(key) { + (i, true) => self.vals[i], + _ => 0, + } + } + + pub fn insert(&mut self, key: u32, val: u32) { + match self.slot(key) { + (i, true) => self.vals[i] = val, + (i, false) if self.len < N && !self.used[i] => { + self.used[i] = true; + self.keys[i] = key; + self.vals[i] = val; + self.len += 1; + } + _ => self.overflowed = true, + } + } + + /// Entries in slot order. + pub fn iter(&self) -> impl Iterator + '_ { + (0..N).filter(|&i| self.used[i]).map(|i| (self.keys[i], self.vals[i])) + } +} + +/// A fixed-capacity FIFO of u32s. Pushing onto a full ring drops the word. +#[repr(C)] +pub struct Ring { + buf: [u32; N], + head: usize, + len: usize, +} + +impl Ring { + pub fn clear(&mut self) { + self.head = 0; + self.len = 0; + } + + pub fn push(&mut self, v: u32) { + if self.len < N { + self.buf[(self.head + self.len) % N] = v; + self.len += 1; + } + } + + pub fn pop(&mut self) -> Option { + if self.len == 0 { + return None; + } + let v = self.buf[self.head]; + self.head = (self.head + 1) % N; + self.len -= 1; + Some(v) + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } +} + +/// Build a `T` in place on the heap, all zero. `T` must be valid zeroed +/// (plain data built from these containers, integers, arrays and bools). +pub fn boxed_zeroed() -> Box { + // SAFETY: callers only use this for plain-data types valid when zeroed. + unsafe { Box::::new_zeroed().assume_init() } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn regmap_insert_get_overwrite_and_overflow() { + let mut m = boxed_zeroed::>(); + assert_eq!(m.get(7), 0); + for k in [0u32, 0xB000_0080, 0x2700_3800, 5] { + m.insert(k, k ^ 1); + } + m.insert(5, 99); + assert_eq!((m.get(0), m.get(0xB000_0080), m.get(0x2700_3800), m.get(5)), (1, 0xB000_0081, 0x2700_3801, 99)); + assert!(!m.overflowed); + m.insert(6, 1); + assert!(m.overflowed); + assert_eq!(m.get(6), 0); + assert_eq!(m.iter().count(), 4); + } + + #[test] + fn ring_is_fifo_and_drops_when_full() { + let mut r = boxed_zeroed::>(); + r.push(1); + r.push(2); + r.push(3); + assert_eq!((r.pop(), r.pop(), r.pop()), (Some(1), Some(2), None)); + } +} diff --git a/src/dev/mgras/raster.rs b/src/dev/mgras/raster.rs deleted file mode 100644 index 4bc00f61..00000000 --- a/src/dev/mgras/raster.rs +++ /dev/null @@ -1,899 +0,0 @@ -//! The raster subsystem: the raster engine's register file, its indirect -//! device space, pixel transfers, and the framebuffer it draws into. -//! -//! Registers are numbered 0..0x3FF. A register write may carry an "execute" -//! flag, which runs the primitive held in the instruction register (IR) once -//! the write has landed. -//! -//! Coordinates: primitives give block corners in window coordinates. The -//! window origin (`xywin`: y in the high half, x in the low) is added, and -//! with Y-flip set in `config` y runs downward from it. The PROM draws with no -//! origin and no flip; the X server sets the origin to the top row and flips, -//! so it draws top-down. The framebuffer has row 0 at the bottom. -//! -//! A block runs one of several ways, chosen by the fill mode's block type: -//! fill it (fast fill uses the fill colour registers, others the red -//! iterator), stipple it with character data, or move pixels in or out of it -//! (by PIO through the character registers, or by DMA). - -use std::collections::{HashMap, VecDeque}; - -pub const WIDTH: usize = 1280; -pub const HEIGHT: usize = 1024; - -/// Raster registers. Those OpenBSD's impact(4) driver also uses carry its -/// names; the rest are named for what they do here. -pub mod reg { - /// The instruction register: the primitive a write with "execute" runs. - pub const IR: u32 = 0x013; - pub const LINE_START: u32 = 0x040; - pub const LINE_END: u32 = 0x041; - pub const IR_ALIAS: u32 = 0x045; - pub const BLOCKXYSTARTI: u32 = 0x046; - pub const BLOCKXYENDI: u32 = 0x047; - /// Packed RGB colour for character and line drawing in RGB modes. - pub const PACKEDCOLOR: u32 = 0x05B; - pub const RED: u32 = 0x05C; - pub const CHAR_H: u32 = 0x070; - pub const CHAR_L: u32 = 0x071; - pub const XFRCONTROL: u32 = 0x102; - pub const FILLMODE: u32 = 0x110; - pub const CONFIG: u32 = 0x112; - pub const XYWIN: u32 = 0x115; - /// Background colour, where an opaque stipple draws its 0 bits: a colour - /// index, or in RGB modes blue (bits 23:12) and green (11:0), 12 bits - /// each, with red in the next register. - pub const BG_COLOR: u32 = 0x140; - pub const BG_COLOR_RED: u32 = 0x141; - /// The clip rectangle: x and y ranges, each `min << 16 | max`, and its - /// control (bit 0 enable, bit 4 keep the inside rather than the outside). - pub const CLIP_X: u32 = 0x147; - pub const CLIP_Y: u32 = 0x148; - pub const CLIP_MODE: u32 = 0x14F; - pub const XFRSIZE: u32 = 0x153; - pub const XFRMODE: u32 = 0x159; - pub const LINE_STIPPLE: u32 = 0x15A; - /// The indirect device space: an address, then its data. - pub const INDIRECT_ADDR: u32 = 0x15C; - pub const INDIRECT_DATA: u32 = 0x15D; - pub const STATUS: u32 = 0x15E; - pub const PP1FILLMODE: u32 = 0x161; - /// Plane write mask (low planes, buffer A). - pub const COLORMASKLSBSA: u32 = 0x163; - pub const DRBPOINTERS: u32 = 0x16D; - /// Fast-fill colour: one 12-bit component each, or the index in R. - pub const FILL_COLOR_R: u32 = 0x176; - pub const FILL_COLOR_G: u32 = 0x177; - pub const FILL_COLOR_B: u32 = 0x178; -} - -/// IR opcodes: a line between two points, and a block (rectangle). -const OP_LINE: u32 = 0x5; -const OP_BLOCK: u32 = 0x8; -/// Fill mode: lines follow the 32-bit line stipple pattern. -const FILL_LINE_STIPPLE: u32 = 1 << 5; -/// Fill mode: the line stipple is opaque, its 0 bits drawn in the background -/// colour instead of left alone. The desktop shades icons this way: a 50% -/// pattern of the foreground and background colours. -const FILL_LINE_STIPPLE_OPAQUE: u32 = 1 << 6; -/// Status: command FIFO empty, engine and pixel processors idle, revision 1. -const STATUS_IDLE: u32 = 0x100 | (1 << 4); -/// Config: Y-flip. -const CONFIG_YFLIP: u32 = 1 << 3; -/// Fill mode: fast fill (solid, from the fill colour registers). -const FILL_FAST: u32 = 1 << 20; -/// Pixel processor fill mode used when drawing window IDs, which live in -/// their own planes, not the colour planes. -const PP1_DRAW_CID: u32 = 0x14_2600; -/// Scanout pointers: the low nine bits say which planes are drawn, and this -/// value means the overlay planes (the main planes read 0x240). -const DRB_PLANES: u32 = 0x1FF; -const DRB_OVERLAY: u32 = 0x1C0; - -/// Block types (fill mode bits 24:22). -mod block { - pub const NORMAL: u32 = 1; - pub const PIO_READ: u32 = 2; - pub const PIO_WRITE: u32 = 3; - pub const DMA_READ: u32 = 4; - pub const DMA_WRITE: u32 = 5; -} - -/// Whether the pixel processors' pixel type (fill mode bits 10:8) is an RGB -/// one; the others are colour index. -fn rgb_pixtype(pp1fillmode: u32) -> bool { - matches!((pp1fillmode >> 8) & 7, 0 | 1 | 4) -} - -/// Pixel processor logic op (fill mode bit 2 enables it; bits 29:26 hold -/// the X11 function number) applied to source `s` and destination `d`. -fn logic_op(op: u32, s: u32, d: u32) -> u32 { - match op & 0xF { - 0x0 => 0, - 0x1 => s & d, - 0x2 => s & !d, - 0x3 => s, - 0x4 => !s & d, - 0x5 => d, - 0x6 => s ^ d, - 0x7 => s | d, - 0x8 => !(s | d), - 0x9 => !(s ^ d), - 0xA => !d, - 0xB => s | !d, - 0xC => !s, - 0xD => !s | d, - 0xE => !(s & d), - _ => !0, - } -} -const PP1_LOGIC_OP_ENABLE: u32 = 1 << 2; - -/// Framebuffer pixels: colour indices as they are, RGB as `0x00BBGGRR` with -/// eight bits per component. -fn pack_rgb(r: u32, g: u32, b: u32) -> u32 { - (r & 0xFF) | (g & 0xFF) << 8 | (b & 0xFF) << 16 -} - -/// A host pixel of transfer format (PixelFormat, CompType) to a framebuffer -/// pixel, and back. Only the RGB formats convert. -fn from_host(format: (u32, u32), v: u32) -> u32 { - let c4 = |s: u32| ((v >> s) & 0xF) * 0x11; - let c5 = |s: u32| ((v >> s) & 0x1F) << 3 | ((v >> s) & 0x1F) >> 2; - match format { - (8, 8) => pack_rgb(c4(0), c4(4), c4(8)), - (8, 10) => pack_rgb(c5(0), c5(5), c5(10)), - (8, 0) => v & 0xFF_FFFF, - (0, 1) => v & 0xFFF, - _ => v, - } -} - -fn to_host(format: (u32, u32), v: u32) -> u32 { - let c = |s: u32| (v >> s) & 0xFF; - match format { - (8, 8) => c(0) / 0x11 | (c(8) / 0x11) << 4 | (c(16) / 0x11) << 8, - (8, 10) => c(0) >> 3 | (c(8) >> 3) << 5 | (c(16) >> 3) << 10, - _ => v, - } -} - -fn signed16(v: u32) -> i32 { - v as u16 as i16 as i32 -} - -/// A block, in window coordinates, with its colour. -#[derive(Clone, Copy)] -struct Block { - xs: i32, - ys: i32, - xe: i32, - ye: i32, - color: u32, -} - -impl Block { - fn dx(&self) -> i32 { - if self.xe < self.xs { -1 } else { 1 } - } - fn dy(&self) -> i32 { - if self.ye < self.ys { -1 } else { 1 } - } - fn rows(&self) -> i32 { - (self.ye - self.ys).abs() + 1 - } - fn cols(&self) -> i32 { - (self.xe - self.xs).abs() + 1 - } -} - -/// A character block being filled with stipple data, one row at a time. -#[derive(Clone, Copy)] -struct Stipple { - block: Block, - col: i32, - row: i32, -} - -/// An armed pixel transfer. -struct Xfer { - block: Block, - read: bool, - /// Pixels per line, bytes per pixel, and (PixelFormat, CompType). - width: u32, - bpp: u32, - format: (u32, u32), - begin_skip: u32, - stride_skip: u32, - /// PIO write stream: the line being assembled, its byte offset within - /// its first doubleword, the bytes collected, and filler still to skip. - line: u32, - line_begin: u32, - pending: Vec, - skip: u32, - /// PIO read: doublewords waiting to be read, and the low half of the one - /// the last high read took. - out: VecDeque, - out_lo: u32, -} - -impl Xfer { - fn line_bytes(&self) -> u32 { - self.width * self.bpp - } - - /// Where the line after one starting at `b` starts within its first - /// doubleword. - fn next_begin(&self, b: u32) -> u32 { - (b + self.line_bytes() + self.stride_skip) & 7 - } -} - -/// Bytes per pixel for an xfrmode (PixelFormat, PixelCompType) pair. -fn bytes_per_pixel(xfrmode: u32) -> u32 { - match ((xfrmode >> 4) & 0xF, xfrmode & 0xF) { - (0, 0) => 1, - (0, 1) | (8, 8) | (8, 10) => 2, - (8, 0) => 4, - (7, 1) => 6, - _ => 1, - } -} - -pub struct Raster { - regs: Vec, - device: HashMap, - /// Colour planes, one value per pixel, row 0 at the bottom. In - /// colour-index modes the low byte is the index. - pub fb: Vec, - /// Overlay planes (kept, not yet displayed). - pub overlay: Vec, - pending: Option, - /// Bumped for every new pending block, so the refresh thread can tell a - /// block that has waited a whole frame (see `flush_if_stale`). - pending_id: u64, - stipple: Option, - xfer: Option, - /// Fill modes seen with a block, for bring-up logging. - pub fillmodes_seen: Vec, -} - -impl Default for Raster { - fn default() -> Self { - Raster { - regs: vec![0; 0x400], - device: HashMap::new(), - fb: vec![0; WIDTH * HEIGHT], - overlay: vec![0; WIDTH * HEIGHT], - pending: None, - pending_id: 0, - stipple: None, - xfer: None, - fillmodes_seen: Vec::new(), - } - } -} - -impl Raster { - pub fn read(&self, r: u32) -> u32 { - match r & 0x3FF { - reg::STATUS => STATUS_IDLE, - reg::INDIRECT_DATA => { - self.device.get(&self.regs[reg::INDIRECT_ADDR as usize]).copied().unwrap_or(0) - } - r => self.regs[r as usize], - } - } - - fn reg(&self, r: u32) -> u32 { - self.regs[r as usize] - } - - /// Write register `r`; `exec` runs the primitive afterwards. Returns true - /// when the framebuffer changed. - pub fn write(&mut self, r: u32, val: u32, exec: bool) -> bool { - let r = r & 0x3FF; - self.regs[r as usize] = val; - match r { - reg::INDIRECT_DATA => { - self.device.insert(self.regs[reg::INDIRECT_ADDR as usize], val); - } - reg::IR_ALIAS => self.regs[reg::IR as usize] = val, - reg::XFRCONTROL if val == 0 => self.xfer = None, - _ => {} - } - if !exec { - return false; - } - let pio_write = self.xfer.as_ref().is_some_and(|x| !x.read); - match r { - reg::CHAR_L if pio_write => { - let dw = ((self.reg(reg::CHAR_H) as u64) << 32) | val as u64; - self.pio_write(dw) - } - reg::CHAR_H if pio_write => self.pio_write((val as u64) << 32), - reg::CHAR_H => self.stipple_bits((val as u64) << 32, 32), - reg::CHAR_L => { - let bits = ((self.reg(reg::CHAR_H) as u64) << 32) | val as u64; - self.stipple_bits(bits, 64) - } - _ => self.execute(), - } - } - - /// Whether pixels are RGB rather than colour indices: an RGB pixel type, - /// or a write mask of exactly the 24 RGB planes (24-bit windows draw - /// with other pixel types; colour-index drawing masks 8 or 12 planes, or - /// all 32 when the PROM and kernel draw). - fn rgb_mode(&self) -> bool { - rgb_pixtype(self.reg(reg::PP1FILLMODE)) || self.reg(reg::COLORMASKLSBSA) == 0xFF_FFFF - } - - /// Window coordinates to framebuffer coordinates. - fn to_fb(&self, x: i32, y: i32) -> (i32, i32) { - let win = self.reg(reg::XYWIN); - let (ox, oy) = (signed16(win), signed16(win >> 16)); - if self.reg(reg::CONFIG) & CONFIG_YFLIP != 0 { - (ox + x, oy - y) - } else { - (ox + x, oy + y) - } - } - - /// Whether a framebuffer pixel may be written: on screen, and inside - /// the clip rectangle when it is enabled (bit 4 chooses inside or outside). - fn visible(&self, x: i32, y: i32) -> bool { - if !(0..WIDTH as i32).contains(&x) || !(0..HEIGHT as i32).contains(&y) { - return false; - } - let clip = self.reg(reg::CLIP_MODE); - if clip & 1 != 0 { - let (mx, my) = (self.reg(reg::CLIP_X), self.reg(reg::CLIP_Y)); - let inside = (signed16(mx >> 16)..=signed16(mx)).contains(&x) - && (signed16(my >> 16)..=signed16(my)).contains(&y); - if inside != (clip & 0x10 != 0) { - return false; - } - } - true - } - - /// Store `v` at framebuffer `(x, y)` in the planes the pixel processors - /// are drawing to. Window-ID drawing is dropped. - fn put(&mut self, x: i32, y: i32, v: u32) { - if self.reg(reg::PP1FILLMODE) == PP1_DRAW_CID || !self.visible(x, y) { - return; - } - let i = y as usize * WIDTH + x as usize; - let pp1 = self.reg(reg::PP1FILLMODE); - // A write through all planes (window moves copy the screen that way, - // 24 bits a pixel) keeps the whole value; so does RGB. - let wide = self.rgb_mode() || self.reg(reg::COLORMASKLSBSA) == 0xFFFF_FFFF; - let plane = if self.reg(reg::DRBPOINTERS) & DRB_PLANES == DRB_OVERLAY { - &mut self.overlay - } else { - &mut self.fb - }; - plane[i] = if pp1 & PP1_LOGIC_OP_ENABLE != 0 { - let width = if wide { 0xFF_FFFF } else { 0xFFF }; - logic_op(pp1 >> 26, v, plane[i]) & width - } else { - v - }; - } - - fn get(&self, x: i32, y: i32) -> u32 { - if !(0..WIDTH as i32).contains(&x) || !(0..HEIGHT as i32).contains(&y) { - return 0; - } - let i = y as usize * WIDTH + x as usize; - if self.reg(reg::DRBPOINTERS) & DRB_PLANES == DRB_OVERLAY { self.overlay[i] } else { self.fb[i] } - } - - /// Block pixel `(col, row)` in framebuffer coordinates. - fn block_px(&self, b: &Block, col: i32, row: i32) -> (i32, i32) { - self.to_fb(b.xs + col * b.dx(), b.ys + row * b.dy()) - } - - fn current_block(&self) -> Block { - let s = self.reg(reg::BLOCKXYSTARTI); - let e = self.reg(reg::BLOCKXYENDI); - // Colour index modes: the index, from the fill colour register for - // fast fills and from the red iterator (12 fraction bits) otherwise. - // RGB modes: three 12-bit components for fast fills, a packed - // 8-8-8 colour otherwise. - let rgb = self.rgb_mode(); - let fast = self.reg(reg::FILLMODE) & FILL_FAST != 0; - let color = match (rgb, fast) { - (false, true) => self.reg(reg::FILL_COLOR_R), - (false, false) => (self.reg(reg::RED) >> 12) & 0xFFF, - (true, true) => pack_rgb( - self.reg(reg::FILL_COLOR_R) >> 4, - self.reg(reg::FILL_COLOR_G) >> 4, - self.reg(reg::FILL_COLOR_B) >> 4, - ), - (true, false) => self.reg(reg::PACKEDCOLOR) & 0xFF_FFFF, - }; - Block { xs: signed16(s >> 16), ys: signed16(s), xe: signed16(e >> 16), ye: signed16(e), color } - } - - /// Run the primitive in the IR. - fn execute(&mut self) -> bool { - let changed = self.flush_pending(); - match self.reg(reg::IR) & 0xF { - OP_BLOCK => {} - OP_LINE => return self.line() | changed, - _ => return changed, - } - let fm = self.reg(reg::FILLMODE); - if !self.fillmodes_seen.contains(&fm) && self.fillmodes_seen.len() < 32 { - self.fillmodes_seen.push(fm); - } - let b = self.current_block(); - // A new block ends whatever the last one was doing; a write transfer - // in particular is never disarmed explicitly. - self.stipple = None; - self.xfer = None; - let kind = (fm >> 22) & 7; - if fm & FILL_FAST != 0 { - self.fill(&b); - return true; - } - match kind { - block::NORMAL => { - // Character block: filled by the stipple data that follows. - self.stipple = Some(Stipple { block: b, col: 0, row: 0 }); - changed - } - block::PIO_READ | block::PIO_WRITE | block::DMA_READ | block::DMA_WRITE => { - let read = kind == block::PIO_READ || kind == block::DMA_READ; - self.arm_transfer(b, read, kind == block::PIO_READ); - changed - } - _ => { - // A block followed by character data is a stipple; anything - // else, a fill. - self.pending_id += 1; - self.pending = Some(b); - changed - } - } - } - - /// A line from the start to the end point, both included, in the current - /// colour; with line stipple on, pixel `k` is drawn only where bit - /// `31 - k % 32` of the pattern is set. - fn line(&mut self) -> bool { - let s = self.reg(reg::LINE_START); - let e = self.reg(reg::LINE_END); - let (mut x, mut y) = (signed16(s >> 16), signed16(s)); - let (x1, y1) = (signed16(e >> 16), signed16(e)); - let color = self.current_block().color; - let fm = self.reg(reg::FILLMODE); - let stipple = (fm & FILL_LINE_STIPPLE != 0).then(|| self.reg(reg::LINE_STIPPLE)); - let background = (stipple.is_some() && fm & FILL_LINE_STIPPLE_OPAQUE != 0).then(|| self.background()); - let (dx, dy) = ((x1 - x).abs(), -(y1 - y).abs()); - let (sx, sy) = (if x < x1 { 1 } else { -1 }, if y < y1 { 1 } else { -1 }); - let mut err = dx + dy; - let mut k = 0u32; - loop { - let lit = stipple.map_or(true, |p| p & (1 << (31 - k % 32)) != 0); - if let Some(c) = if lit { Some(color) } else { background } { - let (fx, fy) = self.to_fb(x, y); - self.put(fx, fy, c); - } - if x == x1 && y == y1 { - break; - } - let e2 = 2 * err; - if e2 >= dy { - err += dy; - x += sx; - } - if e2 <= dx { - err += dx; - y += sy; - } - k += 1; - } - true - } - - /// The background colour, in the same form as the drawing colour: packed - /// RGB in RGB modes (from three 12-bit components, like the fast-fill - /// colour), a colour index otherwise. - fn background(&self) -> u32 { - let bg = self.reg(reg::BG_COLOR); - if self.rgb_mode() { - let red = self.reg(reg::BG_COLOR_RED) & 0xFFF; - pack_rgb(red >> 4, (bg & 0xFFF) >> 4, ((bg >> 12) & 0xFFF) >> 4) - } else { - bg & 0xFFF - } - } - - fn fill(&mut self, b: &Block) { - for row in 0..b.rows() { - for col in 0..b.cols() { - let (x, y) = self.block_px(b, col, row); - self.put(x, y, b.color); - } - } - } - - /// Called once per displayed frame: a block that was already pending at - /// the previous frame never got character data, so draw it as a fill. - pub fn flush_if_stale(&mut self, last_seen: &mut u64) -> bool { - if self.pending.is_none() { - return false; - } - if *last_seen == self.pending_id { - return self.flush_pending(); - } - *last_seen = self.pending_id; - false - } - - /// A pending block that got no character data is a solid fill. - fn flush_pending(&mut self) -> bool { - let Some(b) = self.pending.take() else { return false }; - self.fill(&b); - true - } - - /// Consume `n` stipple bits (most significant first) into the current - /// character block: 1 bits take the colour, 0 bits leave the pixel. A row - /// ends at the block's edge, discarding the rest of the chunk. - fn stipple_bits(&mut self, bits: u64, n: u32) -> bool { - if let Some(b) = self.pending.take() { - self.stipple = Some(Stipple { block: b, col: 0, row: 0 }); - } - let Some(mut s) = self.stipple else { return false }; - if s.row >= s.block.rows() { - return false; - } - for i in 0..n { - if bits & (1u64 << (63 - i)) != 0 { - let (x, y) = self.block_px(&s.block, s.col, s.row); - self.put(x, y, s.block.color); - } - s.col += 1; - if s.col >= s.block.cols() { - s.col = 0; - s.row += 1; - break; - } - } - self.stipple = Some(s); - true - } - - // ---- pixel transfers ---- - - fn arm_transfer(&mut self, block: Block, read: bool, pio_read: bool) { - let mode = self.reg(reg::XFRMODE); - let begin = (mode >> 8) & 7; - let mut x = Xfer { - block, - read, - width: self.reg(reg::XFRSIZE) & 0xFFFF, - bpp: bytes_per_pixel(mode), - format: ((mode >> 4) & 0xF, mode & 0xF), - begin_skip: begin, - stride_skip: (mode >> 14) & 0x1FF, - line: 0, - line_begin: begin, - pending: Vec::new(), - skip: begin, - out: VecDeque::new(), - out_lo: 0, - }; - if pio_read { - x.out = self.pio_read_stream(&x); - } - self.xfer = Some(x); - } - - /// `Some(read)` while a transfer is armed. - pub fn transfer_armed(&self) -> Option { - self.xfer.as_ref().map(|x| x.read) - } - - /// Lines and bytes per line of the armed transfer. - pub fn transfer_shape(&self) -> Option<(u32, u32)> { - self.xfer.as_ref().map(|x| (x.block.rows() as u32, x.line_bytes())) - } - - fn put_line(&mut self, x: &Xfer, line: u32, bytes: &[u8]) { - let bpp = x.bpp as usize; - for (k, px) in bytes.chunks(bpp).enumerate().take(x.width as usize) { - // Big-endian bytes; pixels wider than 32 bits keep their low word. - let v = px.iter().fold(0u64, |a, &b| (a << 8) | b as u64) as u32; - let (fx, fy) = self.block_px(&x.block, k as i32, line as i32); - self.put(fx, fy, from_host(x.format, v)); - } - } - - fn get_line(&self, x: &Xfer, line: u32) -> Vec { - let mut out = Vec::with_capacity(x.line_bytes() as usize); - for k in 0..x.width as i32 { - let (fx, fy) = self.block_px(&x.block, k, line as i32); - let v = to_host(x.format, self.get(fx, fy)) as u64; - for i in (0..x.bpp).rev() { - out.push((v >> (8 * i)) as u8); - } - } - out - } - - /// A PIO write doubleword. Each line starts in a fresh doubleword, after - /// its begin offset; the rest of a line's last doubleword is dropped. - fn pio_write(&mut self, dw: u64) -> bool { - let Some(mut x) = self.xfer.take() else { return false }; - let mut changed = false; - for i in 0..8 { - if x.line >= x.block.rows() as u32 { - break; - } - if x.skip > 0 { - x.skip -= 1; - continue; - } - x.pending.push((dw >> (56 - 8 * i)) as u8); - if x.pending.len() as u32 == x.line_bytes() { - let bytes = std::mem::take(&mut x.pending); - self.put_line(&x, x.line, &bytes); - changed = true; - x.line += 1; - x.line_begin = x.next_begin(x.line_begin); - x.skip = x.line_begin; - break; - } - } - self.xfer = Some(x); - changed - } - - /// The whole PIO read stream: per line, filler up to its begin offset, the - /// pixels, and padding to the doubleword. - fn pio_read_stream(&self, x: &Xfer) -> VecDeque { - let mut out = VecDeque::new(); - let mut b = x.begin_skip; - for line in 0..x.block.rows() as u32 { - let mut bytes = vec![0u8; b as usize]; - bytes.extend(self.get_line(x, line)); - while bytes.len() % 8 != 0 { - bytes.push(0); - } - for c in bytes.chunks(8) { - out.push_back(c.iter().fold(0u64, |a, &v| (a << 8) | v as u64)); - } - b = x.next_begin(b); - } - out - } - - /// PIO read, high half: takes the next doubleword. - pub fn pio_read_hi(&mut self) -> u32 { - let Some(x) = self.xfer.as_mut() else { return 0 }; - let dw = x.out.pop_front().unwrap_or(0); - x.out_lo = dw as u32; - (dw >> 32) as u32 - } - - /// PIO read, low half of the doubleword the last high read took. - pub fn pio_read_lo(&self) -> u32 { - self.xfer.as_ref().map(|x| x.out_lo).unwrap_or(0) - } - - /// DMA into the armed block: line `line`'s pixel bytes. - pub fn dma_write_line(&mut self, line: u32, bytes: &[u8]) { - if let Some(x) = self.xfer.take() { - self.put_line(&x, line, bytes); - self.xfer = Some(x); - } - } - - /// DMA out of the armed block: line `line`'s pixel bytes. - pub fn dma_read_line(&self, line: u32) -> Vec { - self.xfer.as_ref().map(|x| self.get_line(x, line)).unwrap_or_default() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn px(r: &Raster, x: usize, y_top: usize) -> u32 { - r.fb[(HEIGHT - 1 - y_top) * WIDTH + x] - } - - /// The X server's setup: window origin at the top row, Y-flip on, so - /// block coordinates are X's top-down ones. - fn x_server() -> Raster { - let mut r = Raster::default(); - r.write(reg::CONFIG, 0xCAC, false); - r.write(reg::XYWIN, 1023 << 16, false); - r.write(reg::PP1FILLMODE, 0x0C00_4504, false); - r - } - - fn block(r: &mut Raster, x0: u32, y0: u32, x1: u32, y1: u32) { - r.write(reg::IR_ALIAS, 0x18, false); - r.write(reg::BLOCKXYSTARTI, x0 << 16 | y0, false); - r.write(reg::BLOCKXYENDI, x1 << 16 | y1, true); - } - - #[test] - fn fast_fill_lands_top_down_with_yflip() { - let mut r = x_server(); - r.write(reg::FILLMODE, FILL_FAST, false); - r.write(reg::FILL_COLOR_R, 0x13, false); - block(&mut r, 10, 20, 12, 21); - assert_eq!(px(&r, 10, 20), 0x13); - assert_eq!(px(&r, 12, 21), 0x13); - assert_eq!(px(&r, 13, 21), 0); - assert_eq!(px(&r, 10, 22), 0); - } - - #[test] - fn rgb_fast_fill_packs_components() { - let mut r = x_server(); - r.write(reg::PP1FILLMODE, 3 << 26 | 0x104, false); // RGB pixel type, copy - r.write(reg::FILLMODE, FILL_FAST, false); - r.write(reg::FILL_COLOR_R, 0xF00, false); - r.write(reg::FILL_COLOR_G, 0x800, false); - r.write(reg::FILL_COLOR_B, 0x100, false); - block(&mut r, 0, 0, 0, 0); - assert_eq!(px(&r, 0, 0), 0x10_80F0); - } - - #[test] - fn pio_write_frames_each_line_in_a_fresh_doubleword() { - let mut r = x_server(); - // Three 1-byte pixels per line, two lines, begin skip 2: line 0 is - // bytes 2..5 of its doubleword; line 1 starts at (2 + 3) & 7 = 5. - r.write(reg::FILLMODE, 3 << 22, false); - r.write(reg::XFRMODE, 2 << 8, false); - r.write(reg::XFRSIZE, 2 << 16 | 3, false); - block(&mut r, 100, 50, 102, 51); - assert_eq!(r.transfer_armed(), Some(false)); - r.write(reg::CHAR_H, 0x0000_0102, false); - r.write(reg::CHAR_L, 0x03FF_FFFF, true); - r.write(reg::CHAR_H, 0x0000_0000, false); - r.write(reg::CHAR_L, 0x0004_0506, true); - assert_eq!([px(&r, 100, 50), px(&r, 101, 50), px(&r, 102, 50)], [1, 2, 3]); - assert_eq!([px(&r, 100, 51), px(&r, 101, 51), px(&r, 102, 51)], [4, 5, 6]); - } - - #[test] - fn a_new_block_disarms_a_finished_write_transfer() { - let mut r = x_server(); - r.write(reg::FILLMODE, 5 << 22, false); - r.write(reg::XFRSIZE, 1 << 16 | 1, false); - block(&mut r, 0, 0, 0, 0); - assert!(r.transfer_armed().is_some()); - // A glyph block: its char data must stipple, not feed the transfer. - r.write(reg::FILLMODE, 1 << 22, false); - r.write(reg::RED, 0x7 << 12, false); - block(&mut r, 200, 10, 203, 10); - assert_eq!(r.transfer_armed(), None); - r.write(reg::CHAR_H, 0xA000_0000, true); - assert_eq!([px(&r, 200, 10), px(&r, 201, 10), px(&r, 202, 10)], [7, 0, 7]); - } - - #[test] - fn rgb_host_formats_round_trip() { - for (fmt, v) in [((8, 8), 0x0ABCu32), ((8, 10), 0x7FFF), ((8, 0), 0x00C0_FFEE), ((0, 1), 0xFFF)] { - assert_eq!(to_host(fmt, from_host(fmt, v)), v, "{fmt:?}"); - } - assert_eq!(from_host((8, 8), 0x0F0), pack_rgb(0, 0xFF, 0)); - // 8-8-8 host pixels are X pixel values of the visuals, red in 7:0. - assert_eq!(from_host((8, 0), 0x00_00FF), pack_rgb(0xFF, 0, 0)); - } - - #[test] - fn a_24_plane_mask_means_rgb_fills() { - let mut r = x_server(); - r.write(reg::PP1FILLMODE, 0x0C00_6304, false); - r.write(reg::COLORMASKLSBSA, 0xFF_FFFF, false); - r.write(reg::FILLMODE, FILL_FAST, false); - r.write(reg::FILL_COLOR_R, 0x380, false); - r.write(reg::FILL_COLOR_G, 0x8E0, false); - r.write(reg::FILL_COLOR_B, 0x8E0, false); - block(&mut r, 1, 1, 1, 1); - assert_eq!(px(&r, 1, 1), 0x8E_8E38); - } - - #[test] - fn an_all_planes_copy_keeps_24_bit_pixels() { - let mut r = x_server(); - // A window move's write-back: pixel type 2, every plane enabled, - // 4-byte host pixels. - r.write(reg::PP1FILLMODE, 0x0C00_6204, false); - r.write(reg::COLORMASKLSBSA, 0xFFFF_FFFF, false); - r.write(reg::FILLMODE, 5 << 22, false); - r.write(reg::XFRMODE, 0x80, false); - r.write(reg::XFRSIZE, 1 << 16 | 1, false); - block(&mut r, 7, 7, 7, 7); - r.dma_write_line(0, &[0x00, 0x50, 0x50, 0x50]); - assert_eq!(px(&r, 7, 7), 0x50_5050); - } - - #[test] - fn xor_fill_toggles_and_restores() { - let mut r = x_server(); - r.write(reg::FILLMODE, FILL_FAST, false); - r.write(reg::FILL_COLOR_R, 0x13, false); - block(&mut r, 5, 5, 5, 5); - r.write(reg::PP1FILLMODE, 0x0C00_4504 & !(0xF << 26) | 6 << 26, false); - r.write(reg::FILL_COLOR_R, 0x0F, false); - block(&mut r, 5, 5, 5, 5); - assert_eq!(px(&r, 5, 5), 0x13 ^ 0x0F); - block(&mut r, 5, 5, 5, 5); - assert_eq!(px(&r, 5, 5), 0x13); - } - - #[test] - fn stippled_line_skips_zero_bits() { - let mut r = x_server(); - r.write(reg::FILLMODE, FILL_LINE_STIPPLE, false); - r.write(reg::RED, 0x9 << 12, false); - r.write(reg::LINE_STIPPLE, 0xAAAA_AAAA, false); - r.write(reg::IR_ALIAS, 0x15, false); - r.write(reg::LINE_START, 300 << 16 | 40, false); - r.write(reg::LINE_END, 303 << 16 | 40, true); - assert_eq!((300..304).map(|x| px(&r, x, 40)).collect::>(), [9, 0, 9, 0]); - } - - /// What the IRIX desktop sends to shade an icon: fill mode 0x60 (line - /// stipple, opaque), the foreground in RED, the background in BG_COLOR. - /// Both colours land; nothing underneath shows through. - #[test] - fn opaque_stippled_line_draws_zero_bits_in_the_background_colour() { - let mut r = x_server(); - r.write(reg::FILLMODE, FILL_LINE_STIPPLE | FILL_LINE_STIPPLE_OPAQUE, false); - r.write(reg::RED, 0xF << 12, false); - r.write(reg::BG_COLOR, 0x7, false); - r.write(reg::LINE_STIPPLE, 0xAAAA_AAAA, false); - r.write(reg::IR_ALIAS, 0x15, false); - r.write(reg::LINE_START, 300 << 16 | 40, false); - r.write(reg::LINE_END, 303 << 16 | 40, true); - assert_eq!((300..304).map(|x| px(&r, x, 40)).collect::>(), [0xF, 7, 0xF, 7]); - } - - /// In RGB modes the background is three 12-bit components across two - /// registers, as IRIX writes it for a 12-bit window's icon: a 0xf0 grey - /// is 0xf00f00 (blue, green) and 0xf00 (red). These are the values from - /// a trace of the desktop repainting its Icon Catalog. - #[test] - fn opaque_stipple_background_in_rgb_modes() { - let mut r = x_server(); - r.write(reg::PP1FILLMODE, 0x0C00_4004, false); // RGB pixel type - r.write(reg::COLORMASKLSBSA, 0xFFF, false); - r.write(reg::FILLMODE, FILL_LINE_STIPPLE | FILL_LINE_STIPPLE_OPAQUE, false); - r.write(reg::PACKEDCOLOR, 0xA0_A0A0, false); - r.write(reg::BG_COLOR, 0xC00_700, false); - r.write(reg::BG_COLOR_RED, 0x300, false); - r.write(reg::LINE_STIPPLE, 0xAAAA_AAAA, false); - r.write(reg::IR_ALIAS, 0x15, false); - r.write(reg::LINE_START, 300 << 16 | 40, false); - r.write(reg::LINE_END, 301 << 16 | 40, true); - assert_eq!(px(&r, 300, 40), 0xA0_A0A0); - assert_eq!(px(&r, 301, 40), pack_rgb(0x30, 0x70, 0xC0), "red 0x30, green 0x70, blue 0xc0"); - } - - /// Without the opaque bit, BG_COLOR plays no part. - #[test] - fn transparent_stipple_ignores_the_background_colour() { - let mut r = x_server(); - r.write(reg::FILLMODE, FILL_LINE_STIPPLE, false); - r.write(reg::RED, 0xF << 12, false); - r.write(reg::BG_COLOR, 0x7, false); - r.write(reg::LINE_STIPPLE, 0x5555_5555, false); - r.write(reg::IR_ALIAS, 0x15, false); - r.write(reg::LINE_START, 300 << 16 | 40, false); - r.write(reg::LINE_END, 303 << 16 | 40, true); - assert_eq!((300..304).map(|x| px(&r, x, 40)).collect::>(), [0, 0xF, 0, 0xF]); - } -} diff --git a/src/dev/mgras/record.rs b/src/dev/mgras/record.rs new file mode 100644 index 00000000..e36c6ca8 --- /dev/null +++ b/src/dev/mgras/record.rs @@ -0,0 +1,479 @@ +//! Replayable recording of everything that changes the board, for regression +//! tests: every bus access, the system memory the board's DMA reads and +//! writes, and board-internal events that change drawing (a stale block +//! flushed by the display thread). Replaying a recording from power-on through +//! the device's bus interface must reproduce the board state the live run +//! reached, checked against hash checkpoints taken while recording. +//! +//! Started from `IRIS_MGRAS_REC=` (from power-on, which is what replay +//! needs) or the monitor (`mgras rec `, `mgras rec mark`, `mgras rec +//! off`). Replay: `MGRAS_REPLAY= cargo test --release mgras_replay_file +//! -- --nocapture` (`MGRAS_REPLAY_OUT=` saves the final screen, +//! `MGRAS_REPLAY_TRACE=` writes the annotated trace of the replay). +//! +//! File: `MGRASREC` + u32 version, then records, little-endian: +//! +//! | tag | fields | what | +//! |-----|--------------------------------|-------------------------------------| +//! | `W` | bits u8, off u32, val u64 | bus write, `off` within the board | +//! | `R` | bits u8, off u32, val u64 | bus read and the value it returned | +//! | `M` | bits u8, addr u32, val u32 | DMA read of system memory | +//! | `m` | bits u8, addr u32, val u32 | DMA write to system memory | +//! | `F` | | display thread flushed a stale block| +//! | `T` | | display frame tick (stale-block check)| +//! | `H` | blake3 [u8; 32] | checkpoint: hash of `state_bytes` | +//! | `C` | | host GL composite (not replayable) | +//! +//! A path ending in `.txt` records the same records as text, one per line +//! (`W 64 70080 1000400000001`, `M 8 8001000 ab`, `T`, `H `; numbers +//! in hex, `#` starts a comment). Text recordings are for small traces kept +//! in `testdata/`; `load` reads either form. + +use std::io::{BufWriter, Read, Write}; +use std::sync::Arc; + +use parking_lot::Mutex; + +use crate::traits::{BusDevice, BusRead32, BusRead8, BUS_OK}; + +const MAGIC: &[u8; 8] = b"MGRASREC"; +const VERSION: u32 = 1; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Rec { + Write { bits: u8, off: u32, val: u64 }, + Read { bits: u8, off: u32, val: u64 }, + MemRead { bits: u8, addr: u32, val: u32 }, + MemWrite { bits: u8, addr: u32, val: u32 }, + FlushStale, + Tick, + Hash([u8; 32]), + Composite, +} + +pub struct Recorder { + out: BufWriter, + text: bool, + pub path: String, + pub records: u64, + /// Records since the last checkpoint. + since_hash: u64, +} + +/// Records between automatic checkpoints, so a divergence is caught near +/// where it happens, not only at the next scripted mark. +const CHECKPOINT_EVERY: u64 = 250_000; + +/// Shared so the DMA path can record through `RecordingMem` while the board +/// is borrowed. +pub type RecHandle = Arc>; + +impl Recorder { + pub fn create(path: &str) -> std::io::Result { + let mut out = BufWriter::with_capacity(1 << 20, std::fs::File::create(path)?); + let text = path.ends_with(".txt"); + if text { + writeln!(out, "# MGRAS recording (record.rs text form)")?; + } else { + out.write_all(MAGIC)?; + out.write_all(&VERSION.to_le_bytes())?; + } + Ok(Arc::new(Mutex::new(Recorder { out, text, path: path.to_string(), records: 0, since_hash: 0 }))) + } + + pub fn put(&mut self, r: Rec) { + self.records += 1; + self.since_hash = if matches!(r, Rec::Hash(_)) { 0 } else { self.since_hash + 1 }; + let o = &mut self.out; + if self.text { + let _ = writeln!(o, "{}", text_line(r)); + return; + } + let _ = match r { + Rec::Write { bits, off, val } => put_access(o, b'W', bits, off, val), + Rec::Read { bits, off, val } => put_access(o, b'R', bits, off, val), + Rec::MemRead { bits, addr, val } => put_mem(o, b'M', bits, addr, val), + Rec::MemWrite { bits, addr, val } => put_mem(o, b'm', bits, addr, val), + Rec::FlushStale => o.write_all(b"F"), + Rec::Tick => o.write_all(b"T"), + Rec::Hash(h) => o.write_all(b"H").and_then(|_| o.write_all(&h)), + Rec::Composite => o.write_all(b"C"), + }; + } + + /// Time for an automatic checkpoint. + pub fn checkpoint_due(&self) -> bool { + self.since_hash >= CHECKPOINT_EVERY + } + + pub fn flush(&mut self) { + let _ = self.out.flush(); + } +} + +fn put_access(o: &mut impl Write, tag: u8, bits: u8, off: u32, val: u64) -> std::io::Result<()> { + o.write_all(&[tag, bits])?; + o.write_all(&off.to_le_bytes())?; + o.write_all(&val.to_le_bytes()) +} + +fn put_mem(o: &mut impl Write, tag: u8, bits: u8, addr: u32, val: u32) -> std::io::Result<()> { + o.write_all(&[tag, bits])?; + o.write_all(&addr.to_le_bytes())?; + o.write_all(&val.to_le_bytes()) +} + +/// One record in the text form. +fn text_line(r: Rec) -> String { + match r { + Rec::Write { bits, off, val } => format!("W {bits} {off:x} {val:x}"), + Rec::Read { bits, off, val } => format!("R {bits} {off:x} {val:x}"), + Rec::MemRead { bits, addr, val } => format!("M {bits} {addr:x} {val:x}"), + Rec::MemWrite { bits, addr, val } => format!("m {bits} {addr:x} {val:x}"), + Rec::FlushStale => "F".into(), + Rec::Tick => "T".into(), + Rec::Hash(h) => format!("H {}", blake3::Hash::from(h).to_hex()), + Rec::Composite => "C".into(), + } +} + +/// Parse a text recording. +pub fn load_text(text: &str) -> Result, String> { + let mut out = Vec::new(); + for (n, line) in text.lines().enumerate() { + let line = line.split('#').next().unwrap_or("").trim(); + if line.is_empty() { + continue; + } + let f: Vec<&str> = line.split_whitespace().collect(); + let bad = || format!("line {}: cannot parse {line:?}", n + 1); + let hex = |i: usize| f.get(i).and_then(|s| u64::from_str_radix(s, 16).ok()).ok_or_else(bad); + let bits = || f.get(1).and_then(|s| s.parse::().ok()).ok_or_else(bad); + out.push(match f[0] { + "W" => Rec::Write { bits: bits()?, off: hex(2)? as u32, val: hex(3)? }, + "R" => Rec::Read { bits: bits()?, off: hex(2)? as u32, val: hex(3)? }, + "M" => Rec::MemRead { bits: bits()?, addr: hex(2)? as u32, val: hex(3)? as u32 }, + "m" => Rec::MemWrite { bits: bits()?, addr: hex(2)? as u32, val: hex(3)? as u32 }, + "F" => Rec::FlushStale, + "T" => Rec::Tick, + "C" => Rec::Composite, + "H" => { + let h = f.get(1).and_then(|s| blake3::Hash::from_hex(s).ok()).ok_or_else(bad)?; + Rec::Hash(*h.as_bytes()) + } + _ => return Err(bad()), + }); + } + Ok(out) +} + +/// Parse a whole recording, binary or text. +pub fn load(path: &str) -> std::io::Result> { + let mut data = Vec::new(); + std::fs::File::open(path)?.read_to_end(&mut data)?; + let bad = |what: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, what.to_string()); + if data.len() < 12 || &data[..8] != MAGIC { + let text = std::str::from_utf8(&data).map_err(|_| bad("not an MGRAS recording"))?; + return load_text(text).map_err(|e| bad(&e)); + } + if u32::from_le_bytes(data[8..12].try_into().unwrap()) != VERSION { + return Err(bad("unsupported recording version")); + } + let mut out = Vec::with_capacity(data.len() / 14); + let mut i = 12; + let u32_at = |i: usize| u32::from_le_bytes(data[i..i + 4].try_into().unwrap()); + let u64_at = |i: usize| u64::from_le_bytes(data[i..i + 8].try_into().unwrap()); + while i < data.len() { + let need = match data[i] { + b'W' | b'R' => 14, + b'M' | b'm' => 10, + b'H' => 33, + b'F' | b'T' | b'C' => 1, + t => return Err(bad(&format!("bad record tag {t:#x} at byte {i}"))), + }; + if i + need > data.len() { + // A recording cut off mid-record (the emulator was killed): keep + // what is complete. + break; + } + let r = match data[i] { + b'W' => Rec::Write { bits: data[i + 1], off: u32_at(i + 2), val: u64_at(i + 6) }, + b'R' => Rec::Read { bits: data[i + 1], off: u32_at(i + 2), val: u64_at(i + 6) }, + b'M' => Rec::MemRead { bits: data[i + 1], addr: u32_at(i + 2), val: u32_at(i + 6) }, + b'm' => Rec::MemWrite { bits: data[i + 1], addr: u32_at(i + 2), val: u32_at(i + 6) }, + b'H' => Rec::Hash(data[i + 1..i + 33].try_into().unwrap()), + b'F' => Rec::FlushStale, + b'T' => Rec::Tick, + _ => Rec::Composite, + }; + out.push(r); + i += need; + } + Ok(out) +} + +/// System memory as the board's DMA sees it, recording every access. +pub struct RecordingMem { + pub inner: Arc, + pub rec: RecHandle, +} + +impl BusDevice for RecordingMem { + fn read8(&self, addr: u32) -> BusRead8 { + let r = self.inner.read8(addr); + self.rec.lock().put(Rec::MemRead { bits: 8, addr, val: if r.is_ok() { r.data as u32 } else { 0 } }); + if r.is_ok() { r } else { BusRead8::ok(0) } + } + fn read32(&self, addr: u32) -> BusRead32 { + let r = self.inner.read32(addr); + self.rec.lock().put(Rec::MemRead { bits: 32, addr, val: if r.is_ok() { r.data } else { 0 } }); + if r.is_ok() { r } else { BusRead32::ok(0) } + } + fn write8(&self, addr: u32, val: u8) -> u32 { + self.rec.lock().put(Rec::MemWrite { bits: 8, addr, val: val as u32 }); + self.inner.write8(addr, val) + } + fn write32(&self, addr: u32, val: u32) -> u32 { + self.rec.lock().put(Rec::MemWrite { bits: 32, addr, val }); + self.inner.write32(addr, val) + } +} + +/// System memory for replay: answers the board's DMA reads with the recorded +/// values, in order, and checks each access hits the recorded address. +pub struct ReplayMem { + reads: Vec<(u8, u32, u32)>, + next: Mutex, + pub mismatches: Mutex, +} + +impl ReplayMem { + pub fn new(recs: &[Rec]) -> Self { + let reads = recs + .iter() + .filter_map(|r| match *r { + Rec::MemRead { bits, addr, val } => Some((bits, addr, val)), + _ => None, + }) + .collect(); + ReplayMem { reads, next: Mutex::new(0), mismatches: Mutex::new(0) } + } + + fn take(&self, bits: u8, addr: u32) -> u32 { + let mut n = self.next.lock(); + let Some(&(b, a, v)) = self.reads.get(*n) else { + *self.mismatches.lock() += 1; + return 0; + }; + *n += 1; + if (b, a) != (bits, addr) { + *self.mismatches.lock() += 1; + } + v + } + + pub fn consumed(&self) -> (usize, usize) { + (*self.next.lock(), self.reads.len()) + } +} + +impl BusDevice for ReplayMem { + fn read8(&self, addr: u32) -> BusRead8 { BusRead8::ok(self.take(8, addr) as u8) } + fn read32(&self, addr: u32) -> BusRead32 { BusRead32::ok(self.take(32, addr)) } + fn write8(&self, _addr: u32, _val: u8) -> u32 { BUS_OK } + fn write32(&self, _addr: u32, _val: u32) -> u32 { BUS_OK } +} + +#[cfg(test)] +pub(super) mod tests { + use super::*; + use crate::config::GraphicsBoard; + use crate::dev::mgras::{Mgras, MGRAS_SLOT_GFX_BASE}; + use crate::traits::BUS_BUSY; + + /// A High IMPACT board with its engine threads running (no display). + pub fn live_board() -> Arc { + let hb = Arc::new(std::sync::atomic::AtomicU64::new(0)); + let m = Mgras::new(GraphicsBoard::HighImpact, crate::dev::ioc::Ioc::new(false), hb.clone(), hb); + m.start_engines(); + m + } + + /// Store, retrying on bus back-pressure like the CPU does. + pub fn write(m: &Mgras, bits: u8, off: u32, val: u64) { + let a = MGRAS_SLOT_GFX_BASE + off; + loop { + let st = match bits { + 8 => m.write8(a, val as u8), + 16 => m.write16(a, val as u16), + 32 => m.write32(a, val as u32), + _ => m.write64(a, val), + }; + if st != BUS_BUSY { + return; + } + std::hint::spin_loop(); + } + } + + /// Load, retrying while the board answers busy. + pub fn read(m: &Mgras, bits: u8, off: u32) -> u64 { + let a = MGRAS_SLOT_GFX_BASE + off; + loop { + let (st, v) = match bits { + 8 => { let r = m.read8(a); (r.status, r.data as u64) } + 16 => { let r = m.read16(a); (r.status, r.data as u64) } + 32 => { let r = m.read32(a); (r.status, r.data as u64) } + _ => { let r = m.read64(a); (r.status, r.data) } + }; + if st != BUS_BUSY { + return v; + } + std::hint::spin_loop(); + } + } + + /// What a replay saw. + #[derive(Debug, Default)] + pub struct Report { + pub records: usize, + pub checked: usize, + /// Record indices of failed checkpoints. + pub failed: Vec, + pub dma_used: usize, + pub dma_total: usize, + pub dma_mismatched: u64, + pub reads_differ: u64, + pub differ_at: std::collections::BTreeMap, + } + + impl Report { + pub fn diverged(&self) -> bool { + !self.failed.is_empty() || self.dma_mismatched != 0 || self.dma_used != self.dma_total + } + } + + /// Feed `recs` through `m`'s bus interface, serving its DMA reads from + /// the recorded values; checkpoints are checked when `check_hashes` (a + /// recording that starts mid-session cannot match a fresh board's). + /// Returns once the board has run everything. + pub fn replay_recs(m: &Mgras, recs: &[Rec], check_hashes: bool) -> Result { + replay_recs_with(m, recs, check_hashes, &mut |_| {}) + } + + /// `replay_recs`, calling `at_checkpoint` at every checkpoint record + /// (whether or not hashes are checked) with the board idle. + pub fn replay_recs_with(m: &Mgras, recs: &[Rec], check_hashes: bool, at_checkpoint: &mut dyn FnMut(&Mgras)) -> Result { + let mem = Arc::new(ReplayMem::new(recs)); + m.set_phys(mem.clone()); + let mut rep = Report { records: recs.len(), ..Default::default() }; + for (i, r) in recs.iter().enumerate() { + match *r { + Rec::Write { bits, off, val } => write(m, bits, off, val), + Rec::Read { bits, off, val } => { + // Compare at the access width (early recordings kept + // the whole register value for narrow reads). + let mask = if bits >= 64 { u64::MAX } else { (1u64 << bits) - 1 }; + if read(m, bits, off) & mask != val & mask { + rep.reads_differ += 1; + *rep.differ_at.entry(off).or_default() += 1; + } + } + // Recordings from before block type 0 was understood as a + // fill carry these; the board no longer needs them. + Rec::FlushStale | Rec::Tick => {} + Rec::Hash(h) => { + if check_hashes { + rep.checked += 1; + if m.state_hash() != h { + rep.failed.push(i); + } + } + at_checkpoint(m); + } + Rec::Composite => return Err(format!("record {i}: host GL composite cannot be replayed")), + Rec::MemRead { .. } | Rec::MemWrite { .. } => {} + } + } + m.state_hash(); // waits for the board to finish + (rep.dma_used, rep.dma_total) = mem.consumed(); + rep.dma_mismatched = *mem.mismatches.lock(); + Ok(rep) + } + + /// Replay a recording file through a fresh board and check every + /// checkpoint. + pub fn replay(path: &str, png: Option<&str>) -> Result<(), String> { + let recs = load(path).map_err(|e| format!("{path}: {e}"))?; + let m = live_board(); + // MGRAS_REPLAY_TRACE=: the annotated trace of the replay + // (`mgras trace`), all categories. + if let Ok(t) = std::env::var("MGRAS_REPLAY_TRACE") { + m.start_trace(&t, crate::dev::mgras::debug::TRACE_ALL).map_err(|e| format!("{t}: {e}"))?; + } + let rep = replay_recs(&m, &recs, true)?; + m.trace_flush(true); + if let Some(p) = png { + m.save_shot(p)?; + } + m.stop_engines(); + eprintln!( + "{path}: {} records, {} checkpoints, {} failed {:?}, DMA reads {}/{} ({} mismatched), {} bus reads differ", + rep.records, rep.checked, rep.failed.len(), &rep.failed[..rep.failed.len().min(8)], + rep.dma_used, rep.dma_total, rep.dma_mismatched, rep.reads_differ + ); + if !rep.differ_at.is_empty() { + eprintln!(" bus reads that differ, by offset: {:x?}", rep.differ_at); + } + if rep.checked == 0 { + return Err("no checkpoints in recording".into()); + } + if rep.diverged() { + return Err(format!("replay diverged: {} of {} checkpoints failed", rep.failed.len(), rep.checked)); + } + Ok(()) + } + + #[test] + fn mgras_replay_file() { + let Ok(path) = std::env::var("MGRAS_REPLAY") else { return }; + let png = std::env::var("MGRAS_REPLAY_OUT").ok(); + replay(&path, png.as_deref()).unwrap(); + } + + const ALL: [Rec; 8] = [ + Rec::Write { bits: 64, off: 0x70080, val: 0x0010_0004_0000_0001 }, + Rec::Read { bits: 32, off: 0x70000, val: 0x53 }, + Rec::MemRead { bits: 8, addr: 0x0800_1000, val: 0xAB }, + Rec::MemWrite { bits: 32, addr: 0x0800_2000, val: 0xDEAD_BEEF }, + Rec::FlushStale, + Rec::Tick, + Rec::Hash([7; 32]), + Rec::Composite, + ]; + + fn round_trip(ext: &str) { + let path = std::env::temp_dir().join(format!("mgras_rec_{}{ext}", std::process::id())); + let path = path.to_str().unwrap().to_string(); + { + let h = Recorder::create(&path).unwrap(); + let mut r = h.lock(); + for x in ALL { + r.put(x); + } + r.flush(); + } + assert_eq!(load(&path).unwrap(), ALL); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn record_round_trip_binary() { + round_trip(".rec"); + } + + #[test] + fn record_round_trip_text() { + round_trip(".txt"); + } +} diff --git a/src/dev/mgras/rss.rs b/src/dev/mgras/rss.rs new file mode 100644 index 00000000..cedfd33c --- /dev/null +++ b/src/dev/mgras/rss.rs @@ -0,0 +1,1990 @@ +//! The raster subsystem: the raster engine's register file, its indirect +//! device space, pixel transfers, and the framebuffer it draws into. +//! +//! Registers are numbered 0..0x3FF. A register write may carry an "execute" +//! flag, which runs the primitive held in the instruction register (IR) once +//! the write has landed. +//! +//! Coordinates: primitives give block corners in window coordinates. The +//! window origin (`xywin`: y in the high half, x in the low) is added, and +//! with Y-flip set in `config` y runs downward from it. The PROM draws with no +//! origin and no flip; the X server sets the origin to the top row and flips, +//! so it draws top-down. Framebuffer row 0 is at the bottom. +//! +//! Pixels live in page memory (`pixmem`): drawing goes to the buffer whose +//! page pointer is in `DRBpointers` bits 9:0, and the PP1 fill mode's +//! draw-buffer field says what kind it is (overlay or 36-bit). Both X +//! servers switch the pointer to the overlay's pages before overlay +//! drawing (6.5.0: 0x101C0 / 0x90240, 6.5.22: 0x38120 / 0xB81C0). +//! +//! A block runs one of several ways, chosen by the fill mode's block type: +//! fill it (fast fill uses the fill colour registers, others the red +//! iterator), stipple it with character data, or move pixels in or out of it +//! (by PIO through the character registers, or by DMA). + +use super::pixmem::{Buffer, Kind, PixMem}; +use super::plain::RegMap; +use super::te1::{reg as te_reg, tex_env, Sampler, Te1, ITER_ONE, TEXMODE1_ENABLE}; + +/// Framebuffer coordinates the model accepts (x and y, 0..2048); where a +/// pixel lands in memory depends on its buffer (see `pixmem`). +pub const WIDTH: usize = 2048; +pub const HEIGHT: usize = 2048; + +/// Rss registers. Those OpenBSD's impact(4) driver also uses carry its +/// names; the rest are named for what they do here. +pub mod reg { + /// The instruction register: the primitive a write with "execute" runs. + pub const IR: u32 = 0x013; + pub const LINE_START: u32 = 0x040; + pub const LINE_END: u32 = 0x041; + pub const IR_ALIAS: u32 = 0x045; + pub const BLOCKXYSTARTI: u32 = 0x046; + pub const BLOCKXYENDI: u32 = 0x047; + /// Packed RGB colour for character and line drawing in RGB modes. + pub const PACKEDCOLOR: u32 = 0x05B; + pub const RED: u32 = 0x05C; + pub const CHAR_H: u32 = 0x070; + pub const CHAR_L: u32 = 0x071; + pub const XFRCONTROL: u32 = 0x102; + /// Fog colour: red 11:0, green 23:12; blue 11:0 (12 bits each). + pub const FOG_RG: u32 = 0x144; + pub const FOG_B: u32 = 0x145; + /// Model-private (our GE HLE and this RSS agree on them; the GE11's + /// own way of fogging after texturing is not visible on the bus): the + /// fog factor's plane, 64-bit pairs at 2^32 (start, step along the span + /// or line, step down the edge), and whether it applies. + pub const FOG_F: u32 = 0x3F0; + pub const FOG_ON: u32 = 0x3F6; + /// Model-private: a TE read returns TRAM's bytes as they lie (the + /// texture manager's save of pages), not texels. + pub const TE_RAW: u32 = 0x3F7; + pub const FILLMODE: u32 = 0x110; + pub const CONFIG: u32 = 0x112; + pub const XYWIN: u32 = 0x115; + /// Background colour, where an opaque stipple draws its 0 bits: a colour + /// index, or in RGB modes blue (bits 23:12) and green (11:0), 12 bits + /// each, with red in the next register. + pub const BG_COLOR: u32 = 0x140; + pub const BG_COLOR_RED: u32 = 0x141; + /// The clip rectangle: x and y ranges, each `min << 16 | max`, and its + /// control (bit 0 enable, bit 4 keep the inside rather than the outside). + pub const CLIP_X: u32 = 0x147; + pub const CLIP_Y: u32 = 0x148; + pub const CLIP_MODE: u32 = 0x14F; + pub const XFRSIZE: u32 = 0x153; + pub const XFRMODE: u32 = 0x159; + pub const LINE_STIPPLE: u32 = 0x15A; + /// The indirect device space: an address, then its data. + pub const INDIRECT_ADDR: u32 = 0x15C; + pub const INDIRECT_DATA: u32 = 0x15D; + pub const STATUS: u32 = 0x15E; + pub const PP1FILLMODE: u32 = 0x161; + /// Plane write mask (low planes, buffer A). + pub const COLORMASKLSBSA: u32 = 0x163; + /// Page pointer of the buffer drawn into (bits 9:0). + pub const DRBPOINTERS: u32 = 0x16D; + /// Tiles per row: bits 5:2 for 36-bit buffers, 1:0 for the overlay. + pub const DRBSIZE: u32 = 0x16E; + pub const ZMODE: u32 = 0x168; + pub const BLENDFACTOR: u32 = 0x165; + pub const STENCILMODE: u32 = 0x166; + pub const STENCILMASK: u32 = 0x167; + pub const AFUNCMODE: u32 = 0x169; + pub const COLORMASKMSBS: u32 = 0x162; + pub const GLINECONFIG: u32 = 0x146; + pub const LSCRL: u32 = 0x15B; + /// Fast-fill colour: one 12-bit component each, or the index in R. + pub const FILL_COLOR_R: u32 = 0x176; + pub const FILL_COLOR_G: u32 = 0x177; + pub const FILL_COLOR_B: u32 = 0x178; +} + +/// Raster register names (MGRAS.h `rss_single`; TE registers 0x80-0xCB in +/// their non-warp meaning). Unnamed registers are empty. +pub const REG_NAMES: [&str; 0x200] = { + let mut n = [""; 0x200]; + n[0x000] = "tri_x0"; + n[0x001] = "tri_x1"; + n[0x002] = "tri_x2"; + n[0x003] = "tri_ymax"; + n[0x004] = "tri_ymid"; + n[0x005] = "tri_ymin"; + n[0x006] = "tri_dxdy0"; + n[0x007] = "tri_dxdy0_low"; + n[0x008] = "tri_dxdy1_hi"; + n[0x009] = "tri_dxdy1_low"; + n[0x00A] = "tri_dxdy2_hi"; + n[0x00B] = "tri_dxdy2_low"; + n[0x00C] = "gline_xstartf"; + n[0x00D] = "gline_ystartf"; + n[0x00E] = "gline_xendf"; + n[0x00F] = "gline_yendf"; + n[0x010] = "gline_dx"; + n[0x011] = "gline_dy"; + n[0x012] = "gline_adjust"; + n[0x013] = "ir"; + n[0x040] = "xline_xystarti"; + n[0x041] = "xline_xyendi"; + n[0x042] = "xline_inc1"; + n[0x043] = "xline_inc2"; + n[0x044] = "xline_error_oct"; + n[0x045] = "ir_alias"; + n[0x046] = "block_xystarti"; + n[0x047] = "block_xyendi"; + n[0x048] = "block_xymove"; + n[0x049] = "block_xsaveoct"; + n[0x05B] = "packed_color"; + n[0x05C] = "red"; + n[0x05D] = "green"; + n[0x05E] = "blue"; + n[0x05F] = "alpha"; + n[0x060] = "dre"; + n[0x061] = "drx"; + n[0x062] = "dge"; + n[0x063] = "dgx"; + n[0x064] = "dbe"; + n[0x065] = "dbx"; + n[0x066] = "dae"; + n[0x067] = "dax"; + n[0x068] = "z_hi"; + n[0x069] = "z_low"; + n[0x06A] = "dzx_hi"; + n[0x06B] = "dzx_low"; + n[0x06C] = "dze_hi"; + n[0x06D] = "dze_low"; + n[0x070] = "char_h"; + n[0x071] = "char_l"; + n[0x080] = "sw_u"; + n[0x081] = "sw_l"; + n[0x082] = "tw_dtx_u"; + n[0x083] = "tw_dtx_l"; + n[0x084] = "wi_u"; + n[0x085] = "wi_l"; + n[0x086] = "dwie_u"; + n[0x087] = "dwie_l"; + n[0x088] = "dwix_u"; + n[0x089] = "dwix_l"; + n[0x08A] = "dwiy_u"; + n[0x08B] = "dwiy_l"; + n[0x08C] = "txscale"; + n[0x0C0] = "dswe_u"; + n[0x0C1] = "dswe_l"; + n[0x0C2] = "dtwe_u"; + n[0x0C3] = "dtwe_l"; + n[0x0C4] = "dswx_u"; + n[0x0C5] = "dswx_l"; + n[0x0C6] = "dtwx_u"; + n[0x0C7] = "dtwx_l"; + n[0x0C8] = "dswy_u"; + n[0x0C9] = "dswy_l"; + n[0x0CA] = "dtwy_u"; + n[0x0CB] = "dtwy_l"; + n[0x0CC] = "dsex_u"; + n[0x0CD] = "dsex_l"; + n[0x0CE] = "dtex_u"; + n[0x0CF] = "dtex_l"; + n[0x100] = "xfrabort"; + n[0x102] = "xfrcontrol"; + n[0x110] = "fillmode"; + n[0x111] = "texmode1"; + n[0x112] = "config"; + n[0x113] = "scissorx"; + n[0x114] = "scissory"; + n[0x115] = "xywin"; + n[0x140] = "bkgrd_rg"; + n[0x141] = "bkgrd_ba"; + n[0x142] = "txenv_rg"; + n[0x143] = "txenv_b"; + n[0x144] = "fog_rg"; + n[0x145] = "fog_b"; + n[0x146] = "glineconfig"; + n[0x147] = "scrmsk1x"; + n[0x148] = "scrmsk1y"; + n[0x149] = "scrmsk2x"; + n[0x14A] = "scrmsk2y"; + n[0x14B] = "scrmsk3x"; + n[0x14C] = "scrmsk3y"; + n[0x14D] = "scrmsk4x"; + n[0x14E] = "scrmsk4y"; + n[0x14F] = "winmode"; + n[0x153] = "xfrsize"; + n[0x154] = "xfrinitfactor"; + n[0x155] = "xfrfactor"; + n[0x156] = "xfrmasklow"; + n[0x157] = "xfrmaskhigh"; + n[0x158] = "xfrcounters"; + n[0x159] = "xfrmode"; + n[0x15A] = "lspat"; + n[0x15B] = "lscrl"; + n[0x15C] = "device_addr"; + n[0x15D] = "device_data"; + n[0x15E] = "status"; + n[0x15F] = "re_togglecntx"; + n[0x160] = "PixCmd"; + n[0x161] = "pp1fillmode"; + n[0x162] = "ColorMaskMSBs"; + n[0x163] = "ColorMaskLSBsA"; + n[0x164] = "ColorMaskLSBsB"; + n[0x165] = "BlendFactor"; + n[0x166] = "Stencilmode"; + n[0x167] = "Stencilmask"; + n[0x168] = "Zmode"; + n[0x169] = "Afuncmode"; + n[0x16A] = "Accmode"; + n[0x16B] = "BlendcolorRG"; + n[0x16C] = "BlendcolorBA"; + n[0x16D] = "DRBpointers"; + n[0x16E] = "DRBsize"; + n[0x16F] = "TAGmode"; + n[0x170] = "TAGdata_R"; + n[0x171] = "TAGdata_G"; + n[0x172] = "TAGdata_B"; + n[0x173] = "TAGdata_A"; + n[0x174] = "TAGdata_Z"; + n[0x175] = "PixCoordinate"; + n[0x176] = "PIXcolorR"; + n[0x177] = "PIXcolorG"; + n[0x178] = "PIXcolorB"; + n[0x179] = "PIXcolorA"; + n[0x17A] = "PIXcolorAcc"; + n[0x17B] = "pp1winmode"; + n[0x17E] = "MainBufSel"; + n[0x17F] = "OlayBufSel"; + n[0x180] = "texmode2"; + n[0x181] = "txsize"; + n[0x182] = "txtile"; + n[0x183] = "txlod"; + n[0x184] = "txbcolor_rg"; + n[0x185] = "txbcolor_ba"; + n[0x186] = "txbase"; + n[0x187] = "tram_cntrl"; + n[0x188] = "max_pixel_rg"; + n[0x189] = "max_pixel_ba"; + n[0x18A] = "mddma_cntrl"; + n[0x18B] = "texrbuffer"; + n[0x18C] = "txclampsize"; + n[0x190] = "txaddr"; + n[0x191] = "txmipmap"; + n[0x192] = "txdetail"; + n[0x193] = "txborder"; + n[0x194] = "detailscale"; + n[0x195] = "te_togglecntx"; + n[0x196] = "teversion"; + n[0x1A0] = "tl_wbuffer"; + n[0x1A1] = "tl_mode"; + n[0x1A2] = "tl_spec"; + n[0x1A3] = "tl_addr"; + n[0x1A4] = "tl_mipmap"; + n[0x1A5] = "tl_border"; + n[0x1A6] = "tl_vidcntrl"; + n[0x1A7] = "tl_base"; + n[0x1A8] = "tl_vidabort"; + n[0x1A9] = "tl_s_size"; + n[0x1AA] = "tl_t_size"; + n[0x1AB] = "tl_s_left"; + n[0x1AC] = "tl_t_bottom"; + n[0x1AF] = "reserved"; + n +}; + +/// IR opcodes: a line between two points, and a block (rectangle). +const OP_LINE: u32 = 0x5; +const OP_BLOCK: u32 = 0x8; +/// IR opcode: a point at xline_xystarti, drawn by each execute of it (X +/// PolyPoint). +const OP_POINT: u32 = 0x4; +/// IR opcodes: a triangle (area) whose spans run left to right (the major +/// edge on the left) or right to left. SGI's names are IR_OP_AREA_LTOR / +/// IR_OP_AREA_RTOL; their numbers are not known (only GE11 microcode writes +/// them), these are provisional and used by our GE11 HLE alone. +pub const OP_AREA_LTOR: u32 = 0x0; +pub const OP_AREA_RTOL: u32 = 0x1; +/// IR opcode: an OpenGL line (IR_OP_GL_LINE; number provisional, ours). +pub const OP_GL_LINE: u32 = 0x2; + +/// The OpenGL per-fragment state our GE11 HLE programs into the PP1 +/// registers. The real layouts are not known (only GE11 microcode writes +/// them); these follow the shapes SGI's diagnostics' reset values suggest +/// (Afuncmode 0x7, Stencilmode 0x7, Stencilmask 0xFFFF, Zmode 0x0FFFFFF1: +/// compare function in bits 2:0, masks) and are provisional: +/// Afuncmode (0x169) bits 2:0 compare, bit 3 enable, bits 15:4 ref * 4096 +/// Stencilmode (0x166) bits 2:0 compare, bit 3 enable, ops (KEEP ZERO +/// REPLACE INCR DECR INVERT) fail 6:4, zfail 10:8, +/// zpass 14:12, ref 23:16 +/// Stencilmask (0x167) compare mask 7:0, write mask 15:8 +/// BlendFactor (0x165) source 3:0, destination 7:4 (ZERO ONE SRC_COLOR +/// ONE_MINUS_SRC_COLOR SRC_ALPHA ONE_MINUS_SRC_ALPHA +/// DST_ALPHA ONE_MINUS_DST_ALPHA DST_COLOR +/// ONE_MINUS_DST_COLOR SRC_ALPHA_SATURATE), bit 8 enable +/// ColorMaskLSBsA / MSBs RGB / alpha plane write masks +/// fill mode bit 2 polygon stipple, rows at device address POLY_STIPPLE_RAM +/// Compare functions in OpenGL order: NEVER LESS EQUAL LEQUAL GREATER +/// NOTEQUAL GEQUAL ALWAYS, "incoming OP stored". +const TEST_ENABLE: u32 = 1 << 3; +const BLEND_ENABLE: u32 = 1 << 8; +const FILL_POLY_STIPPLE: u32 = 1 << 2; +/// Polygon stipple RAM in the indirect device space (SGI's POLYSTIP_RAM). +pub const POLY_STIPPLE_RAM: u32 = 0xA000_0000; + +/// `a` OP `b` for an OpenGL compare function (0 NEVER .. 7 ALWAYS). +fn compare(func: u32, a: f64, b: f64) -> bool { + match func & 7 { + 0 => false, + 1 => a < b, + 2 => a == b, + 3 => a <= b, + 4 => a > b, + 5 => a != b, + 6 => a >= b, + _ => true, + } +} +/// Fill mode: a line leaves out its last pixel (X's CapNotLast). +const FILL_LINE_SKIP_LAST: u32 = 1 << 10; +/// Fill mode: lines follow the 32-bit line stipple pattern. +const FILL_LINE_STIPPLE: u32 = 1 << 5; +/// Fill mode: the line stipple is opaque, its 0 bits drawn in the background +/// colour instead of left alone. The desktop shades icons this way: a 50% +/// pattern of the foreground and background colours. +const FILL_LINE_STIPPLE_OPAQUE: u32 = 1 << 6; +/// Status: command FIFO empty, engine and pixel processors idle, revision 1. +const STATUS_IDLE: u32 = 0x100 | (1 << 4); +/// Config: Y-flip. +const CONFIG_YFLIP: u32 = 1 << 3; +/// Fill mode: fast fill (solid, from the fill colour registers). +const FILL_FAST: u32 = 1 << 20; +/// Fill mode: char data stipples the block (with block type 1), and the +/// stipple is opaque, its 0 bits drawn in the background colour. +const FILL_CHAR_STIPPLE: u32 = 1 << 3; +const FILL_CHAR_STIPPLE_OPAQUE: u32 = 1 << 4; +/// Pixel processor fill mode used when drawing window IDs, which live in +/// their own planes, not the colour planes. +/// PP1 fill mode draw-buffer field (bits 20:14), as the drivers use it: +/// 0x01 the main colour buffer (A), 0x02 the second (B), 0x03 both (see +/// `Rss::target`), 0x4F the overlay planes (4Dwm menus, overlay +/// clears), 0x50 the window-ID planes (not modelled: their drawing is +/// dropped). The 6.5.22 TrueColor server keeps `DRBpointers` at 0xB81C0 +/// (A 0x1C0, B 0x2E0) for all drawing and picks the buffer here. +fn draw_buffer(pp1fillmode: u32) -> u32 { + (pp1fillmode >> 14) & 0x7F +} +const DRAW_B: u32 = 0x02; +const DRAW_A_AND_B: u32 = 0x03; +const DRAW_OVERLAY: u32 = 0x4F; +const DRAW_CID: u32 = 0x50; + +/// PP1 fill mode read-buffer field (bits 25:21): 0 for every read of the +/// main buffer traced; 4 when the X server reads the overlay back (with the +/// draw field 0 and DRBpointers at the overlay's pages: it copies popup +/// menu pixels through host memory). +fn read_buffer(pp1fillmode: u32) -> u32 { + (pp1fillmode >> 21) & 0x1F +} +const READ_OVERLAY: u32 = 4; + +/// Block types (fill mode bits 24:22). +mod block { + pub const NORMAL: u32 = 1; + pub const PIO_READ: u32 = 2; + pub const PIO_WRITE: u32 = 3; + pub const DMA_READ: u32 = 4; + pub const DMA_WRITE: u32 = 5; +} + +/// Whether the pixel processors' pixel type (fill mode bits 10:8) is an RGB +/// one; the others are colour index. +fn rgb_pixtype(pp1fillmode: u32) -> bool { + matches!((pp1fillmode >> 8) & 7, 0 | 1 | 4) +} + +/// Pixel processor logic op (fill mode bit 2 enables it; bits 29:26 hold +/// the X11 function number) applied to source `s` and destination `d`. +fn logic_op(op: u32, s: u32, d: u32) -> u32 { + match op & 0xF { + 0x0 => 0, + 0x1 => s & d, + 0x2 => s & !d, + 0x3 => s, + 0x4 => !s & d, + 0x5 => d, + 0x6 => s ^ d, + 0x7 => s | d, + 0x8 => !(s | d), + 0x9 => !(s ^ d), + 0xA => !d, + 0xB => s | !d, + 0xC => !s, + 0xD => !s | d, + 0xE => !(s & d), + _ => !0, + } +} +const PP1_LOGIC_OP_ENABLE: u32 = 1 << 2; + +/// Framebuffer pixels: colour indices as they are, RGB as `0x00BBGGRR` with +/// eight bits per component. +fn pack_rgb(r: u32, g: u32, b: u32) -> u32 { + (r & 0xFF) | (g & 0xFF) << 8 | (b & 0xFF) << 16 +} + +/// A host pixel of transfer format (PixelFormat, CompType) to a framebuffer +/// pixel, and back. Only the RGB formats convert. +/// Transfer format (PixelFormat, CompType) of depth pixels. +const DEPTH_FORMAT: (u32, u32) = (2, 3); + +fn from_host(format: (u32, u32), v: u64) -> u32 { + // RGB with 16-bit components (glCopyPixels' round trip through the + // host, 6 bytes a pixel, red first): the top 8 bits of each. + if format == (7, 1) { + return pack_rgb((v >> 40) as u32, (v >> 24) as u32, (v >> 8) as u32); + } + // RGBA with 16-bit components (glCopyPixels draws back so). + if format == (8, 1) { + return pack_rgb((v >> 56) as u32, (v >> 40) as u32, (v >> 24) as u32); + } + // RGB, a byte each, red first (glCopyTexImage reads the screen so). + if format == (7, 0) { + return pack_rgb((v >> 16) as u32, (v >> 8) as u32, v as u32); + } + // Depth (glReadPixels / glCopyPixels of GL_DEPTH_COMPONENT): 32 bits + // full scale on the host (libGLcore's scale is 1 / 2^31), 24 here. + if format == DEPTH_FORMAT { + return (v as u32 >> 8) & 0xFF_FFFF; + } + let v = v as u32; + let c4 = |s: u32| ((v >> s) & 0xF) * 0x11; + let c5 = |s: u32| ((v >> s) & 0x1F) << 3 | ((v >> s) & 0x1F) >> 2; + match format { + (8, 8) => pack_rgb(c4(0), c4(4), c4(8)), + (8, 10) => pack_rgb(c5(0), c5(5), c5(10)), + (8, 0) => v & 0xFF_FFFF, + (0, 1) => v & 0xFFF, + _ => v, + } +} + +fn to_host(format: (u32, u32), v: u32) -> u64 { + let c = |s: u32| (v >> s) & 0xFF; + if format == (7, 1) { + let w = |s: u32| c(s) as u64 * 0x101; + return w(0) << 32 | w(8) << 16 | w(16); + } + if format == (7, 0) { + return (c(0) << 16 | c(8) << 8 | c(16)) as u64; + } + if format == (8, 1) { + let w = |s: u32| c(s) as u64 * 0x101; + return w(0) << 48 | w(8) << 32 | w(16) << 16 | 0xFFFF; + } + if format == DEPTH_FORMAT { + let z = v & 0xFF_FFFF; + return (z << 8 | z >> 16) as u64; + } + (match format { + (8, 8) => c(0) / 0x11 | (c(8) / 0x11) << 4 | (c(16) / 0x11) << 8, + (8, 10) => c(0) >> 3 | (c(8) >> 3) << 5 | (c(16) >> 3) << 10, + _ => v, + }) as u64 +} + +fn signed16(v: u32) -> i32 { + v as u16 as i16 as i32 +} + +/// A block, in window coordinates, with its colour. +#[derive(Clone, Copy)] +struct Block { + xs: i32, + ys: i32, + xe: i32, + ye: i32, + color: u32, +} + +impl Block { + fn dx(&self) -> i32 { + if self.xe < self.xs { -1 } else { 1 } + } + fn dy(&self) -> i32 { + if self.ye < self.ys { -1 } else { 1 } + } + fn rows(&self) -> i32 { + (self.ye - self.ys).abs() + 1 + } + fn cols(&self) -> i32 { + (self.xe - self.xs).abs() + 1 + } +} + +/// A character block being filled with stipple data, one row at a time. +#[derive(Clone, Copy)] +struct Stipple { + block: Block, + col: i32, + row: i32, + opaque: bool, +} + +/// An armed pixel transfer. +#[derive(Clone, Copy)] +struct Xfer { + block: Block, + read: bool, + /// Armed by a PIO read block: the host reads it through the char + /// registers. + pio_read: bool, + /// Pixels per line, bytes per pixel, and (PixelFormat, CompType). + width: u32, + bpp: u32, + format: (u32, u32), + begin_skip: u32, + stride_skip: u32, + /// PIO write stream: the line being assembled, its byte offset within + /// its first doubleword, the bytes collected (in `Rss::line_buf`), and + /// filler still to skip. + line: u32, + line_begin: u32, + pending: u32, + skip: u32, + /// PIO read stream, produced as it is read: per line, filler up to its + /// begin offset, the pixels, padding to the doubleword. The line, the + /// byte within it, its begin offset, and the low half of the doubleword + /// the last high read took. + rd_line: u32, + rd_pos: u32, + rd_begin: u32, + out_lo: u32, +} + +impl Xfer { + fn line_bytes(&self) -> u32 { + self.width * self.bpp + } + + /// Where the line after one starting at `b` starts within its first + /// doubleword. + fn next_begin(&self, b: u32) -> u32 { + (b + self.line_bytes() + self.stride_skip) & 7 + } + + /// PIO read: bytes the current line takes in the stream (filler, pixels, + /// padding to the doubleword). + fn rd_line_len(&self) -> u32 { + (self.rd_begin + self.line_bytes() + 7) & !7 + } + + /// PIO read: move past finished (or empty) lines. + fn rd_settle(&mut self) { + while self.rd_line < self.block.rows() as u32 && self.rd_pos >= self.rd_line_len() { + self.rd_pos = 0; + self.rd_line += 1; + self.rd_begin = self.next_begin(self.rd_begin); + } + } +} + +/// Bytes per pixel for an xfrmode (PixelFormat, PixelCompType) pair. +pub(crate) fn bytes_per_pixel(xfrmode: u32) -> u32 { + match ((xfrmode >> 4) & 0xF, xfrmode & 0xF) { + (0, 0) => 1, + (0, 1) | (8, 8) | (8, 10) => 2, + (8, 0) | (2, 3) => 4, + (7, 0) => 3, + (8, 1) => 8, + (7, 1) => 6, + _ => 1, + } +} + +/// What the raster subsystem has done, for `mgras stats`. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RssStats { + /// Executed primitives by IR opcode (bits 3:0). + pub prims: [u64; 16], + /// Blocks by kind (fill mode bits 24:22), fast fills counted apart. + pub blocks: [u64; 8], + pub fast_fills: u64, + pub stipple_chunks: u64, + pub pio_write_dw: u64, + pub pio_read_dw: u64, + pub dma_lines_in: u64, +} + +/// Registers a GL batch leaves as it set them when it ends (`gl_leave` +/// puts every other register back): the texture engine's, which the GE +/// keeps loaded across batches (it reloads them only when another context +/// may have used the TE), and the model-private ones from 0x3F0. +fn gl_keeps(r: usize) -> bool { + r >= 0x3F0 || r == te_reg::TXADDR as usize || crate::dev::mgras::te1::CONTEXT_REGS.contains(&(r as u32)) +} + +/// PP1 Zmode, as our GE11 HLE uses it (the real layout is not known; SGI's +/// diagnostics write it as value | 0x0FFFFFF0, which fits bits 27:4 being +/// a 24-bit Z write mask): bits 2:0 the compare function in OpenGL's order +/// (NEVER, LESS, EQUAL, LEQUAL, GREATER, NOTEQUAL, GEQUAL, ALWAYS), bit 3 +/// test enable (provisional), bits 27:4 the Z write mask. +const ZMODE_TEST: u32 = 1 << 3; +/// Page pointer of the depth (ZST: Z 23:0, stencil 31:24) buffer: the +/// buffer the PROM's 1280x1024 layout calls "aux" (page 0; unused by the +/// X server at 1024x768). Provisional: how the PP1 is told is not known. +pub const ZST_PAGE: u32 = 0; + +/// Longest PIO line, in bytes (2048 pixels of 6 bytes, with room to spare). +const MAX_LINE_BYTES: usize = 16384; + +/// The raster subsystem's state. Plain data apart from the `Option`s, which +/// `init` writes; build with `Rss::init` on zeroed memory or `new_boxed`. +#[repr(C)] +pub struct Rss { + regs: [u32; 0x400], + device: RegMap<256>, + /// Pixel memory. In colour-index modes a pixel's low byte is the index. + pub mem: PixMem, + /// The PIO write line being assembled. + line_buf: [u8; MAX_LINE_BYTES], + stipple: Option, + xfer: Option, + pub stats: RssStats, + /// Pixels drawn since the GL line stipple was last restarted (lscrl). + gl_line_stipple_pos: u32, + /// The raster registers as `gl_enter` found them. + gl_stash: [u32; 0x400], + /// Fill modes seen with a block, for bring-up logging. + fillmodes: [u32; 32], + fillmodes_n: usize, + /// The texture engine and TRAM. + pub te: Te1, + /// A texture load transfer is armed (XFRCONTROL to the texture side): + /// DMA lines go to the texture loader. + te_load: u32, + /// A texture read transfer is armed: armed, texels a line, lines. + te_read: [u32; 3], +} + +/// XFRCONTROL (provisional layout, from the IDE's TRAM load writing 5): +/// start by DMA or PIO (bits 0, 1), device (bit 2: 0 PP1, 1 texture), +/// direction (bit 3: read). +const XFR_START: u32 = 3; +const XFR_DEVICE_TE: u32 = 1 << 2; +const XFR_READ: u32 = 1 << 3; + +impl Rss { + /// Make zeroed memory a valid, reset `Rss`. + /// + /// # Safety + /// `p` points at a zeroed, exclusively owned `Rss`. + pub unsafe fn init(p: *mut Rss) { + use std::ptr::addr_of_mut; + addr_of_mut!((*p).stipple).write(None); + addr_of_mut!((*p).xfer).write(None); + } + + /// A GL batch begins (our GE11 HLE): save the raster registers and + /// turn Y-flip off (GL draws y up). The X server keeps its origin, + /// masks, flip, fill modes, colours and instruction in the same + /// registers and sets them only when they change: after a GL batch it + /// executes with what it left (a line with the IR as GL left it drew + /// GL's last triangle in X's window, traced with gltest). The real RE4 + /// has a second register context for this (re_togglecntx), which is not + /// modelled, so `gl_leave` puts the registers back. + pub fn gl_enter(&mut self) { + self.gl_stash = self.regs; + self.regs[reg::CONFIG as usize] &= !CONFIG_YFLIP; + } + + /// A GL batch ends: restore what `gl_enter` saved, but for what the GE + /// keeps loaded (`gl_keeps`). + pub fn gl_leave(&mut self) { + for (r, s) in self.gl_stash.iter().enumerate() { + if !gl_keeps(r) { + self.regs[r] = *s; + } + } + } + + /// A reset `Rss` on the heap (tests). + #[cfg(test)] + pub fn new_boxed() -> Box { + let mut b = Box::::new_zeroed(); + // SAFETY: zeroed and exclusively owned; `init` writes the fields that + // are not valid zeroed. + unsafe { + Rss::init(b.as_mut_ptr()); + b.assume_init() + } + } + + pub fn fillmodes_seen(&self) -> &[u32] { + &self.fillmodes[..self.fillmodes_n] + } +} + +impl Rss { + pub fn read(&self, r: u32) -> u32 { + match r & 0x3FF { + reg::STATUS => STATUS_IDLE, + reg::INDIRECT_DATA => { + self.device.get(self.regs[reg::INDIRECT_ADDR as usize]) + } + r => self.regs[r as usize], + } + } + + pub fn reg(&self, r: u32) -> u32 { + self.regs[r as usize] + } + + /// Write register `r`; `exec` runs the primitive afterwards. Returns true + /// when the framebuffer changed. + pub fn write(&mut self, r: u32, val: u32, exec: bool) -> bool { + let r = r & 0x3FF; + self.regs[r as usize] = val; + match r { + reg::INDIRECT_DATA => { + self.device.insert(self.regs[reg::INDIRECT_ADDR as usize], val); + if self.device.overflowed { + self.device.overflowed = false; + eprintln!("mgras: raster device space full, write to {:#x} dropped", self.regs[reg::INDIRECT_ADDR as usize]); + } + } + reg::IR_ALIAS => self.regs[reg::IR as usize] = val, + reg::LSCRL => self.gl_line_stipple_pos = 0, + reg::XFRCONTROL if val == 0 => { + self.xfer = None; + self.te_load = 0; + self.te_read[0] = 0; + } + reg::XFRCONTROL if val & XFR_DEVICE_TE != 0 && val & XFR_START != 0 => { + self.xfer = None; + let read = val & XFR_READ != 0; + self.te_load = (!read) as u32; + let size = self.regs[reg::XFRSIZE as usize]; + self.te_read = [read as u32, size & 0xFFFF, size >> 16]; + } + te_reg::TXMIPMAP | te_reg::TXBORDER | te_reg::DETAILSCALE => { + let i = &mut self.regs[te_reg::TXADDR as usize]; + self.te.table_write(r, *i, val); + *i = i.wrapping_add(1); + } + _ => {} + } + if !exec { + return false; + } + let pio_write = self.xfer.as_ref().is_some_and(|x| !x.read); + match r { + reg::CHAR_L if pio_write => { + let dw = ((self.reg(reg::CHAR_H) as u64) << 32) | val as u64; + self.pio_write(dw) + } + reg::CHAR_H if pio_write => self.pio_write((val as u64) << 32), + reg::CHAR_H => self.stipple_bits((val as u64) << 32, 32), + reg::CHAR_L => { + let bits = ((self.reg(reg::CHAR_H) as u64) << 32) | val as u64; + self.stipple_bits(bits, 64) + } + _ => self.execute(), + } + } + + /// Whether pixels are RGB rather than colour indices: an RGB pixel type, + /// or a write mask of exactly the 24 RGB planes (24-bit windows draw + /// with other pixel types; colour-index drawing masks 8 or 12 planes, or + /// all 32 when the PROM and kernel draw). + fn rgb_mode(&self) -> bool { + rgb_pixtype(self.reg(reg::PP1FILLMODE)) || self.reg(reg::COLORMASKLSBSA) == 0xFF_FFFF + } + + /// Window coordinates to framebuffer coordinates. + fn to_fb(&self, x: i32, y: i32) -> (i32, i32) { + let win = self.reg(reg::XYWIN); + let (ox, oy) = (signed16(win), signed16(win >> 16)); + if self.reg(reg::CONFIG) & CONFIG_YFLIP != 0 { + (ox + x, oy - y) + } else { + (ox + x, oy + y) + } + } + + /// Whether a framebuffer pixel may be written: on screen, and passing + /// every enabled screen mask. Window mode bit `n - 1` enables mask `n` + /// (1..4, `scrmsk{n}x` / `scrmsk{n}y`, each `min << 16 | max`), and bit + /// `n + 3` keeps the pixels inside it rather than outside. + fn visible(&self, x: i32, y: i32) -> bool { + if !(0..WIDTH as i32).contains(&x) || !(0..HEIGHT as i32).contains(&y) { + return false; + } + let mode = self.reg(reg::CLIP_MODE); + for n in 0..4 { + if mode & (1 << n) == 0 { + continue; + } + let (mx, my) = (self.reg(reg::CLIP_X + 2 * n), self.reg(reg::CLIP_Y + 2 * n)); + let inside = (signed16(mx >> 16)..=signed16(mx)).contains(&x) + && (signed16(my >> 16)..=signed16(my)).contains(&y); + if inside != (mode & (0x10 << n) != 0) { + return false; + } + } + true + } + + /// The buffer drawing and pixel reads go to: the page pointer in + /// `DRBpointers`, its kind from the draw-buffer field. + /// + /// Main-buffer drawing names its buffer absolutely in the draw-buffer + /// field: 1 the page in DRBpointers bits 9:0 (A), 2 the page in bits + /// 19:10 (B), 3 both. The X server keeps DRBpointers at A | B << 10 + /// and tracks which one a double-buffered window shows itself (traced: + /// the Icon Catalog draws with 2 before the kernel's swap shows B, 1 + /// before the swap back). 8/8/8/8 double buffering is two 36-bit + /// buffers (Octane report 4.4.14). + pub fn target(&self) -> Buffer { + let kind = if self.draws_overlay() { Kind::Overlay } else { Kind::Wide }; + let drb = self.reg(reg::DRBPOINTERS); + let ptr = match self.second_buffer() { + Some(b) if kind == Kind::Wide && draw_buffer(self.reg(reg::PP1FILLMODE)) == DRAW_B => b, + _ => drb, + }; + Buffer::new(ptr, kind, self.reg(reg::DRBSIZE)) + } + + /// DRBpointers' second buffer page (bits 19:10), if any. + fn second_buffer(&self) -> Option { + let b = (self.reg(reg::DRBPOINTERS) >> 10) & 0x3FF; + (b != 0).then_some(b) + } + + /// Store `v` at framebuffer `(x, y)` in the buffer (or both buffers) + /// the pixel processors are drawing to. Window-ID drawing is dropped. + fn put(&mut self, x: i32, y: i32, v: u32) { + let field = draw_buffer(self.reg(reg::PP1FILLMODE)); + if field == DRAW_CID || !self.visible(x, y) { + return; + } + let b = self.target(); + self.put_in(b, x, y, v); + if field == DRAW_A_AND_B { + if let Some(p) = self.second_buffer() { + let b2 = Buffer::new(p, Kind::Wide, self.reg(reg::DRBSIZE)); + self.put_in(b2, x, y, v); + } + } + } + + fn put_in(&mut self, b: Buffer, x: i32, y: i32, v: u32) { + let pp1 = self.reg(reg::PP1FILLMODE); + // A write through all planes (window moves copy the screen that way, + // 24 bits a pixel) keeps the whole value; so does RGB. + let wide = self.rgb_mode() || self.reg(reg::COLORMASKLSBSA) == 0xFFFF_FFFF; + let old = self.mem.get(&b, x as u32, y as u32) as u32; + let v = if pp1 & PP1_LOGIC_OP_ENABLE != 0 { + let width = if wide { 0xFF_FFFF } else { 0xFFF }; + logic_op(pp1 >> 26, v, old) & width + } else { + v + }; + // Plane write masks where the pixel format matches our storage + // (pixel type 2, RGBA8888: R G B in the low 24 planes, alpha in the + // top 8): ColorMaskLSBsA the low 24 planes (all 32 when it is all + // ones), ColorMaskMSBs the top 8. Overlay writes: the X server + // sends ColorMaskLSBsA 0 and ColorMaskMSBs 0xF0 (overlay visuals, + // values 0-15) or 0x70 (popup menus, values 0-3, colormap entries + // 0-3); read as the high nibble masking overlay planes 3:0 (our + // reading: the stored values stay as the X server wrote them). + // Other formats (colour index, 12-bit RGB) keep whole values: their + // masks describe a storage layout this model does not keep. + let v = if b.kind == Kind::Overlay { + let mask = (self.reg(reg::COLORMASKMSBS) >> 4) & 0xF; + (old & !mask) | (v & mask) + } else if b.kind == Kind::Wide && (pp1 >> 8) & 7 == 2 { + let lsb = self.reg(reg::COLORMASKLSBSA); + let mask = if lsb == u32::MAX { lsb } else { lsb & 0xFF_FFFF | (self.reg(reg::COLORMASKMSBS) & 0xFF) << 24 }; + (old & !mask) | (v & mask) + } else if b.kind == Kind::Wide && (pp1 >> 8) & 7 == 6 { + let mask = self.reg(reg::COLORMASKLSBSA) & 0xFFF; + (old & !mask) | (v & mask) + } else { + v + }; + self.mem.put(&b, x as u32, y as u32, v as u64); + } + + /// Drawing goes to the overlay planes. + fn draws_overlay(&self) -> bool { + draw_buffer(self.reg(reg::PP1FILLMODE)) == DRAW_OVERLAY + } + + /// The buffer pixel reads (transfers to the host) come from: the + /// overlay when the read-buffer field says so, else the drawing target. + fn source(&self) -> Buffer { + if read_buffer(self.reg(reg::PP1FILLMODE)) == READ_OVERLAY { + Buffer::new(self.reg(reg::DRBPOINTERS), Kind::Overlay, self.reg(reg::DRBSIZE)) + } else { + self.target() + } + } + + fn get(&self, x: i32, y: i32) -> u32 { + if !(0..WIDTH as i32).contains(&x) || !(0..HEIGHT as i32).contains(&y) { + return 0; + } + self.mem.get(&self.source(), x as u32, y as u32) as u32 + } + + /// Block pixel `(col, row)` in framebuffer coordinates. + fn block_px(&self, b: &Block, col: i32, row: i32) -> (i32, i32) { + self.to_fb(b.xs + col * b.dx(), b.ys + row * b.dy()) + } + + fn current_block(&self) -> Block { + let s = self.reg(reg::BLOCKXYSTARTI); + let e = self.reg(reg::BLOCKXYENDI); + // Colour index modes: the index, from the fill colour register for + // fast fills and from the red iterator (12 fraction bits) otherwise. + // RGB modes: three 12-bit components for fast fills, a packed + // 8-8-8 colour otherwise. + let rgb = self.rgb_mode(); + let fast = self.reg(reg::FILLMODE) & FILL_FAST != 0; + let color = match (rgb, fast) { + (false, true) => self.reg(reg::FILL_COLOR_R), + (false, false) => (self.reg(reg::RED) >> 12) & 0xFFF, + (true, true) => pack_rgb( + self.reg(reg::FILL_COLOR_R) >> 4, + self.reg(reg::FILL_COLOR_G) >> 4, + self.reg(reg::FILL_COLOR_B) >> 4, + ) | ((self.reg(reg::FILL_COLOR_B + 1) >> 4) & 0xFF) << 24, + (true, false) => self.reg(reg::PACKEDCOLOR) & 0xFF_FFFF, + }; + Block { xs: signed16(s >> 16), ys: signed16(s), xe: signed16(e >> 16), ye: signed16(e), color } + } + + /// Run the primitive in the IR. + fn execute(&mut self) -> bool { + self.stats.prims[(self.reg(reg::IR) & 0xF) as usize] += 1; + match self.reg(reg::IR) & 0xF { + OP_BLOCK => {} + OP_LINE => return self.line(), + OP_POINT => return self.point(), + OP_AREA_LTOR => return self.triangle(true), + OP_AREA_RTOL => return self.triangle(false), + OP_GL_LINE => return self.gl_line(), + _ => return false, + } + let fm = self.reg(reg::FILLMODE); + if !self.fillmodes_seen().contains(&fm) && self.fillmodes_n < 32 { + self.fillmodes[self.fillmodes_n] = fm; + self.fillmodes_n += 1; + } + let b = self.current_block(); + // A new block ends whatever the last one was doing; a write transfer + // in particular is never disarmed explicitly. + self.stipple = None; + self.xfer = None; + let kind = (fm >> 22) & 7; + if fm & FILL_FAST != 0 { + self.stats.fast_fills += 1; + self.fill(&b); + return true; + } + self.stats.blocks[kind as usize] += 1; + match kind { + block::NORMAL if fm & FILL_CHAR_STIPPLE != 0 => { + // Character block: the char data that follows stipples it. + let opaque = fm & FILL_CHAR_STIPPLE_OPAQUE != 0; + self.stipple = Some(Stipple { block: b, col: 0, row: 0, opaque }); + false + } + block::PIO_READ | block::PIO_WRITE | block::DMA_READ | block::DMA_WRITE => { + let read = kind == block::PIO_READ || kind == block::DMA_READ; + self.arm_transfer(b, read, kind == block::PIO_READ); + false + } + _ => { + // Block type 0 (and 1 without char stipple): a fill in the + // iterated colour. The kernel's boot gradient draws a block + // per line this way; char data never follows one. + self.fill(&b); + true + } + } + } + + /// The primitive in the IR as the registers describe it, for the trace. + pub fn describe_ir(&self) -> String { + let ir = self.reg(reg::IR); + let fm = self.reg(reg::FILLMODE); + let pp1 = self.reg(reg::PP1FILLMODE); + let xy = |v: u32| format!("({}, {})", signed16(v >> 16), signed16(v)); + let target = format!( + "{}{} mask={:#x} pp1={pp1:#x}{} win=(x {}, y {}){}", + if self.rgb_mode() { "RGB" } else { "CI" }, + if self.draws_overlay() { " overlay" } else { "" }, + self.reg(reg::COLORMASKLSBSA), + if pp1 & PP1_LOGIC_OP_ENABLE != 0 { format!(" lop={:#x}", (pp1 >> 26) & 0xF) } else { String::new() }, + signed16(self.reg(reg::XYWIN)), + signed16(self.reg(reg::XYWIN) >> 16), + if self.reg(reg::CONFIG) & CONFIG_YFLIP != 0 { " yflip" } else { "" }, + ); + match ir & 0xF { + OP_BLOCK => { + let b = self.current_block(); + let kind = if fm & FILL_FAST != 0 { + "fast fill".to_string() + } else { + match (fm >> 22) & 7 { + block::NORMAL if fm & FILL_CHAR_STIPPLE != 0 => "char block".into(), + block::PIO_READ => format!("PIO read {}", xfer_desc(self)), + block::PIO_WRITE => format!("PIO write {}", xfer_desc(self)), + block::DMA_READ => format!("DMA read {}", xfer_desc(self)), + block::DMA_WRITE => format!("DMA write {}", xfer_desc(self)), + k => format!("fill (block type {k})"), + } + }; + format!("BLOCK ({}, {})-({}, {}) {kind} color={:#x} fm={fm:#x} {target}", b.xs, b.ys, b.xe, b.ye, b.color) + } + OP_LINE => format!( + "LINE {}-{} color={:#x} fm={fm:#x}{}{} {target}", + xy(self.reg(reg::LINE_START)), + xy(self.reg(reg::LINE_END)), + self.current_block().color, + if fm & FILL_LINE_STIPPLE != 0 { format!(" stipple={:#010x}", self.reg(reg::LINE_STIPPLE)) } else { String::new() }, + if fm & FILL_LINE_SKIP_LAST != 0 { " skip-last" } else { "" }, + ), + OP_POINT => format!("POINT {} color={:#x} {target}", xy(self.reg(reg::LINE_START)), self.current_block().color), + op @ (OP_AREA_LTOR | OP_AREA_RTOL) => { + let pos = |n: u32| f32::from_bits(self.reg(n)) - 49151.5; + format!("TRIANGLE {} ymax {} ymid {} ymin {} x0 {} {target}", + if op == OP_AREA_LTOR { "left to right" } else { "right to left" }, + pos(0x003), pos(0x004), pos(0x005), pos(0x000)) + } + op => format!("IR {ir:#x} (opcode {op:#x} not modelled) fm={fm:#x} {target}"), + } + } + + /// A point at the line start register, in the current colour. + fn point(&mut self) -> bool { + let s = self.reg(reg::LINE_START); + let color = self.current_block().color; + let (fx, fy) = self.to_fb(signed16(s >> 16), signed16(s)); + self.put(fx, fy, color); + true + } + + /// A line from the start to the end point, both included unless the fill + /// mode skips the last, in the current colour; with line stipple on, + /// pixel `k` is drawn only where bit `31 - k % 32` of the pattern is set. + fn line(&mut self) -> bool { + let s = self.reg(reg::LINE_START); + let e = self.reg(reg::LINE_END); + let (mut x, mut y) = (signed16(s >> 16), signed16(s)); + let (x1, y1) = (signed16(e >> 16), signed16(e)); + let color = self.current_block().color; + let fm = self.reg(reg::FILLMODE); + let stipple = (fm & FILL_LINE_STIPPLE != 0).then(|| self.reg(reg::LINE_STIPPLE)); + let background = (stipple.is_some() && fm & FILL_LINE_STIPPLE_OPAQUE != 0).then(|| self.background()); + let (dx, dy) = ((x1 - x).abs(), -(y1 - y).abs()); + let (sx, sy) = (if x < x1 { 1 } else { -1 }, if y < y1 { 1 } else { -1 }); + let mut err = dx + dy; + let mut k = 0u32; + let skip_last = fm & FILL_LINE_SKIP_LAST != 0; + loop { + if skip_last && x == x1 && y == y1 { + break; + } + let lit = stipple.map_or(true, |p| p & (1 << (31 - k % 32)) != 0); + if let Some(c) = if lit { Some(color) } else { background } { + let (fx, fy) = self.to_fb(x, y); + self.put(fx, fy, c); + } + if x == x1 && y == y1 { + break; + } + let e2 = 2 * err; + if e2 >= dy { + err += dy; + x += sx; + } + if e2 <= dx { + err += dx; + y += sy; + } + k += 1; + } + true + } + + /// The background colour, in the same form as the drawing colour: packed + /// RGB in RGB modes (from three 12-bit components, like the fast-fill + /// colour), a colour index otherwise. + fn background(&self) -> u32 { + let bg = self.reg(reg::BG_COLOR); + if self.rgb_mode() { + let red = self.reg(reg::BG_COLOR_RED) & 0xFFF; + pack_rgb(red >> 4, (bg & 0xFFF) >> 4, ((bg >> 12) & 0xFFF) >> 4) + } else { + bg & 0xFFF + } + } + + /// A triangle from the area registers, as SGI's diagnostic triangle + /// setup (_mg0_FillTriangle) loads them: vertices sorted top (max y), + /// middle, bottom (min y); positions as floats biased by 49151.5 + /// (window coordinates, y up); edge slopes dx per unit of y going down, + /// s.24 fixed point in hi/lo pairs: + /// tri_x0/x1 major / upper minor edge x at y = floor(ymax) + /// tri_x2 lower minor edge x at y = floor(ymid) + /// tri_ymax, tri_ymid, tri_ymin + /// tri_dxdy0/1/2 major, upper minor, lower minor slopes + /// Colours: 12.12 fixed (1.0 = 0xFFF000), the value at the first pixel + /// of the first span (x = ceil(x0) left to right, floor(x0) right to + /// left; y = floor(ymax)), dx per pixel along the span (negated right to + /// left) and de per scanline down the major edge, corrected for the + /// edge's integer step (de = dc/dy + trunc(dxdy0) * dx, less dx when the + /// span direction and the slope's sign disagree). + /// + /// Pixels are sampled at their centres with OpenGL's half-open rule + /// (bottom and left edges in). The real RE4's edge and tie rules are + /// not known; this is the GL rule. + fn triangle(&mut self, ltor: bool) -> bool { + let pos = |r: &Self, n: u32| f32::from_bits(r.reg(n)) as f64 - 49151.5; + let slope = |r: &Self, n: u32| ((r.reg(n) as u64) << 32 | r.reg(n + 1) as u64) as i64 as f64 / (1u64 << 24) as f64; + let (x0, x1, x2) = (pos(self, 0x000), pos(self, 0x001), pos(self, 0x002)); + let (ymax, ymid, ymin) = (pos(self, 0x003), pos(self, 0x004), pos(self, 0x005)); + let (s0, s1, s2) = (slope(self, 0x006), slope(self, 0x008), slope(self, 0x00A)); + if !(ymax > ymin) || ![x0, x1, x2, ymax, ymid, ymin, s0, s1, s2].iter().all(|v| v.is_finite()) { + return true; + } + let (yref, yref2) = (ymax.floor(), ymid.floor()); + // Colour planes c(x, y) = c0 + dx * (x - xs) + dy * (yref - y). + let xs = if ltor { x0.ceil() } else { x0.floor() }; + let n1 = s0.trunc(); + let fix = |v: u32| v as i32 as f64 / 0xFF_F000 as f64; + let plane = |r: &Self, c: u32, de: u32, dx: u32| -> (f64, f64, f64) { + let dx_reg = fix(r.reg(dx)); + let dx = if ltor { dx_reg } else { -dx_reg }; + let mut de = fix(r.reg(de)); + if ltor ^ (s0 >= 0.0) { + de += dx_reg; + } + (fix(r.reg(c)), dx, de - n1 * dx) + }; + let planes = [ + plane(self, 0x05C, 0x060, 0x061), + plane(self, 0x05D, 0x062, 0x063), + plane(self, 0x05E, 0x064, 0x065), + plane(self, 0x05F, 0x066, 0x067), + ]; + // Z: 64-bit hi/lo registers, value * 0x1000, the same rule. + let fix64 = |r: &Self, n: u32| ((r.reg(n) as u64) << 32 | r.reg(n + 1) as u64) as i64 as f64 / 4096.0; + let zplane = { + let dzx_reg = fix64(self, 0x06A); + let dzx = if ltor { dzx_reg } else { -dzx_reg }; + let mut dze = fix64(self, 0x06C); + if ltor ^ (s0 >= 0.0) { + dze += dzx_reg; + } + (fix64(self, 0x068), dzx, dze - n1 * dzx) + }; + // Texture: S/W, T/W and 1/W, the same plane rule at 2^32 (te1.rs). + let texture = self.reg(te_reg::TEXMODE1) & TEXMODE1_ENABLE != 0; + let fixt = |r: &Self, n: u32| ((r.reg(n) as u64) << 32 | r.reg(n + 1) as u64) as i64 as f64 / ITER_ONE; + let tplane = |r: &Self, c: u32, de: u32, dx: u32| -> (f64, f64, f64) { + let dx_reg = fixt(r, dx); + let dx = if ltor { dx_reg } else { -dx_reg }; + let mut de = fixt(r, de); + if ltor ^ (s0 >= 0.0) { + de += dx_reg; + } + (fixt(r, c), dx, de - n1 * dx) + }; + let tex = texture.then(|| { + let planes = [ + tplane(self, te_reg::SW, te_reg::DSWE, te_reg::DSWX), + tplane(self, te_reg::TW, te_reg::DTWE, te_reg::DTWX), + tplane(self, te_reg::WI, te_reg::DWIE, te_reg::DWIX), + ]; + { + if let Some((want, have)) = self.te.stale(&self.regs) { + self.te.stale_events += 1; + self.te.stale_last = [want, have]; + } + (planes, self.te.sampler(&self.regs)) + } + }); + let fog = (self.reg(reg::FOG_ON) != 0).then(|| tplane(self, reg::FOG_F, reg::FOG_F + 4, reg::FOG_F + 2)); + let stipple = self.reg(reg::FILLMODE) & FILL_POLY_STIPPLE != 0; + let j0 = (ymin - 0.5).ceil() as i32; + let j1 = (ymax - 0.5).ceil() as i32; + for j in j0..j1 { + let yc = j as f64 + 0.5; + let major = x0 + s0 * (yref - yc); + let minor = if yc >= ymid { x1 + s1 * (yref - yc) } else { x2 + s2 * (yref2 - yc) }; + let (l, r) = if ltor { (major, minor) } else { (minor, major) }; + let i0 = (l - 0.5).ceil() as i32; + let i1 = (r - 0.5).ceil() as i32; + // Polygon stipple: 32 rows in the device space, window y mod 32, + // the most significant bit at window x mod 32 = 0. + let stip_row = stipple.then(|| self.device.get(POLY_STIPPLE_RAM + (j & 31) as u32)); + for i in i0..i1 { + if let Some(row) = stip_row { + if (row >> (31 - (i & 31))) & 1 == 0 { + continue; + } + } + let xc = i as f64 + 0.5; + let at = |p: (f64, f64, f64)| p.0 + p.1 * (xc - xs) + p.2 * (yref - yc); + let mut rgba = [at(planes[0]), at(planes[1]), at(planes[2]), at(planes[3])]; + if let Some((p, smp)) = &tex { + rgba = self.textured(p, smp, at, rgba); + } + if let Some(fp) = fog { + rgba = self.fogged(rgba, at(fp)); + } + self.gl_fragment(i, j, rgba, at(zplane)); + } + } + true + } + + /// A fragment's colour textured: S/W, T/W, 1/W from their planes (value, + /// d/dx, d/(-y)) evaluated by `at`; the level of detail from the + /// derivatives of s and t in level-0 texels; the texel through the + /// texture environment. + fn textured(&self, p: &[(f64, f64, f64); 3], smp: &Sampler, at: impl Fn((f64, f64, f64)) -> f64, rgba: [f64; 4]) -> [f64; 4] { + let (sw, tw, wi) = (at(p[0]), at(p[1]), at(p[2])); + if wi == 0.0 || !wi.is_finite() { + return rgba; + } + let (s, t) = (sw / wi, tw / wi); + // d(a/w) = (da' - a dw') / w for a' = a/w, w' = 1/w; y runs down + // the plane's third term. + let d = |q: &(f64, f64, f64), v: f64| ((q.1 - v * p[2].1) / wi, (v * p[2].2 - q.2) / wi); + let ((dsx, dsy), (dtx, dty)) = (d(&p[0], s), d(&p[1], t)); + let (w, h) = smp.size(); + let rho = (dsx * w).hypot(dtx * h).max((dsy * w).hypot(dty * h)); + let lambda = if rho > 0.0 { rho.log2() } else { f64::NEG_INFINITY }; + let texel = smp.sample(&self.te, s, t, lambda); + tex_env(self.reg(te_reg::TEXMODE1), self.reg(te_reg::TXENV_RG), self.reg(te_reg::TXENV_B), rgba, texel) + } + + /// Fog applied to a fragment: factor `f` (1 = none) toward the fog + /// colour (FOG_RG / FOG_B), alpha kept. + fn fogged(&self, rgba: [f64; 4], f: f64) -> [f64; 4] { + let f = f.clamp(0.0, 1.0); + let c = |v: u32| (v & 0xFFF) as f64 / 4095.0; + let (rg, b) = (self.reg(reg::FOG_RG), self.reg(reg::FOG_B)); + let fc = [c(rg), c(rg >> 12), c(b)]; + [0, 1, 2, 3].map(|k| if k < 3 { f * rgba[k] + (1.0 - f) * fc[k] } else { rgba[3] }) + } + + /// A GL line (our GE11 HLE, IR `OP_GL_LINE`): from gline_xstartf/ystartf + /// to gline_xendf/yendf (biased floats, as the triangle positions); + /// colours (red..alpha) and Z (z_hi/low) at the start, their steps per + /// pixel along the major axis in dre/dge/dbe/dae and dze. Pixels whose + /// centre's major coordinate lies in [start, end) along the direction + /// of travel (the GL rule for non-antialiased lines), `glineconfig` + 1 + /// pixels wide across the major axis. With line stipple (fill mode bit + /// 5), lspat holds the 16-bit pattern, first bit in bit 15, and lscrl + /// the repeat count less one; the position carries on until lscrl is + /// written again. + fn gl_line(&mut self) -> bool { + let pos = |r: &Self, n: u32| f32::from_bits(r.reg(n)) as f64 - 49151.5; + let (x0, y0, x1, y1) = (pos(self, 0x00C), pos(self, 0x00D), pos(self, 0x00E), pos(self, 0x00F)); + if ![x0, y0, x1, y1].iter().all(|v| v.is_finite()) { + return true; + } + let fix = |r: &Self, n: u32| r.reg(n) as i32 as f64 / 0xFF_F000 as f64; + let fix64 = |r: &Self, n: u32| ((r.reg(n) as u64) << 32 | r.reg(n + 1) as u64) as i64 as f64 / 4096.0; + let c0 = [fix(self, 0x05C), fix(self, 0x05D), fix(self, 0x05E), fix(self, 0x05F)]; + let dc = [fix(self, 0x060), fix(self, 0x062), fix(self, 0x064), fix(self, 0x066)]; + let (z0, dz) = (fix64(self, 0x068), fix64(self, 0x06C)); + let width = (self.reg(reg::GLINECONFIG) & 0xFF) as i32 + 1; + let stipple = self.reg(reg::FILLMODE) & FILL_LINE_STIPPLE != 0; + let (pattern, repeat) = (self.reg(reg::LINE_STIPPLE) & 0xFFFF, (self.reg(reg::LSCRL) & 0xFF) + 1); + // Texture along the line: S/W, T/W, Q/W at the start, steps per + // pixel in the edge step registers; the fog factor likewise. + let fix64t = |r: &Self, n: u32| ((r.reg(n) as u64) << 32 | r.reg(n + 1) as u64) as i64 as f64 / ITER_ONE; + let tex = (self.reg(te_reg::TEXMODE1) & TEXMODE1_ENABLE != 0).then(|| { + let q = |c: u32, d: u32| (fix64t(self, c), fix64t(self, d), 0.0); + ([q(te_reg::SW, te_reg::DSWE), q(te_reg::TW, te_reg::DTWE), q(te_reg::WI, te_reg::DWIE)], self.te.sampler(&self.regs)) + }); + let fog = (self.reg(reg::FOG_ON) != 0).then(|| (fix64t(self, reg::FOG_F), fix64t(self, reg::FOG_F + 2))); + let xmajor = (x1 - x0).abs() >= (y1 - y0).abs(); + let (a0, a1, b0, b1) = if xmajor { (x0, x1, y0, y1) } else { (y0, y1, x0, x1) }; + if a0 == a1 { + return true; + } + let slope = (b1 - b0) / (a1 - a0); + let dir = if a1 > a0 { 1 } else { -1 }; + // First and one-past-last pixel along the major axis. + let (p0, p1) = if dir > 0 { ((a0 - 0.5).ceil() as i32, (a1 - 0.5).ceil() as i32) } else { ((a0 - 0.5).floor() as i32, (a1 - 0.5).floor() as i32) }; + let mut p = p0; + while p != p1 { + let t = ((p as f64 + 0.5) - a0).abs(); + let draw = !stipple || { + let bit = (self.gl_line_stipple_pos / repeat) % 16; + self.gl_line_stipple_pos += 1; + (pattern >> (15 - bit)) & 1 != 0 + }; + if draw { + let b = b0 + slope * ((p as f64 + 0.5) - a0); + let q0 = (b - 0.5 * width as f64 + 0.5).floor() as i32; + let mut rgba = [c0[0] + dc[0] * t, c0[1] + dc[1] * t, c0[2] + dc[2] * t, c0[3] + dc[3] * t]; + if let Some((p, smp)) = &tex { + // Planes (value, d/major, 0) evaluated at t along it. + rgba = self.textured(p, smp, |q: (f64, f64, f64)| q.0 + q.1 * t, rgba); + } + if let Some((f0, df)) = fog { + rgba = self.fogged(rgba, f0 + df * t); + } + for q in q0..q0 + width { + let (wx, wy) = if xmajor { (p, q) } else { (q, p) }; + self.gl_fragment(wx, wy, rgba, z0 + dz * t); + } + } + p += dir; + } + true + } + + /// One GL fragment at window (wx, wy): the per-fragment pipeline our + /// GE11 HLE programs (layouts provisional, see GL_RASTER_STATE): screen + /// masks, alpha test, stencil test, depth test, stencil update, then + /// blending or the logic op, and the colour write masks. `rgba` is + /// 0..1 (red alone is the index in colour-index windows), `z` window + /// depth (0..2^24 - 1). + fn gl_fragment(&mut self, wx: i32, wy: i32, rgba: [f64; 4], z: f64) { + let (fx, fy) = self.to_fb(wx, wy); + if draw_buffer(self.reg(reg::PP1FILLMODE)) == DRAW_CID || !self.visible(fx, fy) { + return; + } + let rgba = rgba.map(|c| c.clamp(0.0, 1.0)); + let af = self.reg(reg::AFUNCMODE); + if af & TEST_ENABLE != 0 && !compare(af & 7, rgba[3], ((af >> 4) & 0xFFF) as f64 / 4096.0) { + return; + } + let st = self.reg(reg::STENCILMODE); + let zm = self.reg(reg::ZMODE); + let (sten, ztest) = (st & TEST_ENABLE != 0, zm & ZMODE_TEST != 0); + let zbuf = Buffer::new(ZST_PAGE, Kind::Wide, self.reg(reg::DRBSIZE)); + let (ux, uy) = (fx as u32, fy as u32); + if sten || ztest { + let zst = self.mem.get(&zbuf, ux, uy); + let s_old = ((zst >> 24) & 0xFF) as u32; + let masks = self.reg(reg::STENCILMASK); + let (cmask, wmask) = (masks & 0xFF, (masks >> 8) & 0xFF); + let sref = (st >> 16) & 0xFF; + let z = z.round().clamp(0.0, 16_777_215.0) as u64; + let zold = zst & 0xFF_FFFF; + let spass = !sten || compare(st & 7, (sref & cmask) as f64, (s_old & cmask) as f64); + let zpass = spass && (!ztest || compare(zm & 7, z as f64, zold as f64)); + let mut new = zst; + if sten { + let op = if !spass { (st >> 4) & 7 } else if !zpass { (st >> 8) & 7 } else { (st >> 12) & 7 }; + let s_new = match op { + 1 => 0, + 2 => sref, + 3 => (s_old + 1).min(0xFF), + 4 => s_old.saturating_sub(1), + 5 => !s_old & 0xFF, + _ => s_old, + }; + let s = (s_old & !wmask) | (s_new & wmask); + new = (new & !(0xFF << 24)) | (s as u64) << 24; + } + if zpass && ztest { + let m = ((zm >> 4) & 0xFF_FFFF) as u64; + new = (new & !m) | (z & m); + } + if new != zst { + self.mem.put(&zbuf, ux, uy, new); + } + if !zpass { + return; + } + } + let b = self.target(); + let dst = self.mem.get(&b, ux, uy) as u32; + let pp1 = self.reg(reg::PP1FILLMODE); + // A logic op other than copy replaces blending (OpenGL). + let logic = pp1 & PP1_LOGIC_OP_ENABLE != 0 && (pp1 >> 26) & 0xF != 3; + let rgb = (pp1 >> 8) & 7 == 2 || self.rgb_mode(); + let src = if rgb { + let blend = self.reg(reg::BLENDFACTOR); + let c = if blend & BLEND_ENABLE != 0 && !logic { + let d = [dst & 0xFF, (dst >> 8) & 0xFF, (dst >> 16) & 0xFF, dst >> 24].map(|v| v as f64 / 255.0); + let f = |code: u32, k: usize| -> f64 { + let sat = rgba[3].min(1.0 - d[3]); + match code { + 0 => 0.0, + 1 => 1.0, + 2 => rgba[k], + 3 => 1.0 - rgba[k], + 4 => rgba[3], + 5 => 1.0 - rgba[3], + 6 => d[3], + 7 => 1.0 - d[3], + 8 => d[k], + 9 => 1.0 - d[k], + 10 => if k == 3 { 1.0 } else { sat }, + _ => 1.0, + } + }; + let (sf, df) = (blend & 0xF, (blend >> 4) & 0xF); + [0, 1, 2, 3].map(|k| (rgba[k] * f(sf, k) + d[k] * f(df, k)).clamp(0.0, 1.0)) + } else { + rgba + }; + let q = |v: f64| (v * 255.0).round() as u32; + q(c[0]) | q(c[1]) << 8 | q(c[2]) << 16 | q(c[3]) << 24 + } else { + (rgba[0] * 4095.0).round() as u32 & 0xFFF + }; + let v = if logic { logic_op(pp1 >> 26, src, dst) } else { src }; + let mask = self.reg(reg::COLORMASKLSBSA) & 0xFF_FFFF | (self.reg(reg::COLORMASKMSBS) & 0xFF) << 24; + self.mem.put(&b, ux, uy, ((dst & !mask) | (v & mask)) as u64); + } + + fn fill(&mut self, b: &Block) { + for row in 0..b.rows() { + for col in 0..b.cols() { + let (x, y) = self.block_px(b, col, row); + self.put(x, y, b.color); + } + } + } + + /// Consume `n` stipple bits (most significant first) into the current + /// character block: 1 bits take the colour, 0 bits leave the pixel (or + /// take the background colour when the stipple is opaque). A row ends at + /// the block's edge, discarding the rest of the chunk. + fn stipple_bits(&mut self, bits: u64, n: u32) -> bool { + self.stats.stipple_chunks += 1; + let Some(mut s) = self.stipple else { return false }; + if s.row >= s.block.rows() { + return false; + } + let background = s.opaque.then(|| self.background()); + for i in 0..n { + let c = if bits & (1u64 << (63 - i)) != 0 { Some(s.block.color) } else { background }; + if let Some(c) = c { + let (x, y) = self.block_px(&s.block, s.col, s.row); + self.put(x, y, c); + } + s.col += 1; + if s.col >= s.block.cols() { + s.col = 0; + s.row += 1; + break; + } + } + self.stipple = Some(s); + true + } + + // ---- pixel transfers ---- + + fn arm_transfer(&mut self, block: Block, read: bool, pio_read: bool) { + let mode = self.reg(reg::XFRMODE); + let begin = (mode >> 8) & 7; + let mut x = Xfer { + block, + read, + pio_read, + width: self.reg(reg::XFRSIZE) & 0xFFFF, + bpp: bytes_per_pixel(mode), + format: ((mode >> 4) & 0xF, mode & 0xF), + begin_skip: begin, + stride_skip: (mode >> 14) & 0x1FF, + line: 0, + line_begin: begin, + pending: 0, + skip: begin, + rd_line: 0, + rd_pos: 0, + rd_begin: begin, + out_lo: 0, + }; + x.rd_settle(); + self.xfer = Some(x); + } + + /// `Some(read)` while a transfer is armed. + pub fn transfer_armed(&self) -> Option { + if self.te_read[0] != 0 { + return Some(true); + } + self.xfer.as_ref().map(|x| x.read) + } + + /// Lines and bytes per line of the armed transfer. + pub fn transfer_shape(&self) -> Option<(u32, u32)> { + if self.te_read[0] != 0 { + return Some((self.te_read[2], self.te_read[1] * Te1::read_texel_bytes(self.regs[reg::XFRMODE as usize]))); + } + self.xfer.as_ref().map(|x| (x.block.rows() as u32, x.line_bytes())) + } + + fn put_line(&mut self, x: &Xfer, line: u32, bytes: &[u8]) { + let bpp = x.bpp as usize; + for (k, px) in bytes.chunks(bpp).enumerate().take(x.width as usize) { + // Big-endian bytes. + let v = px.iter().fold(0u64, |a, &b| (a << 8) | b as u64); + let (fx, fy) = self.block_px(&x.block, k as i32, line as i32); + if x.format == DEPTH_FORMAT { + // Depth into the depth buffer, its stencil kept. + if self.visible(fx, fy) { + let zbuf = Buffer::new(ZST_PAGE, Kind::Wide, self.reg(reg::DRBSIZE)); + let old = self.mem.get(&zbuf, fx as u32, fy as u32); + self.mem.put(&zbuf, fx as u32, fy as u32, (old & !0xFF_FFFF) | from_host(x.format, v) as u64); + } + continue; + } + self.put(fx, fy, from_host(x.format, v)); + } + } + + /// Byte `i` (big-endian) of transfer pixel `k` of `line`, as the host sees it. + fn line_byte(&self, x: &Xfer, line: u32, k: u32, i: u32) -> u8 { + let (fx, fy) = self.block_px(&x.block, k as i32, line as i32); + let v = to_host(x.format, self.get(fx, fy)); + (v >> (8 * (x.bpp - 1 - i))) as u8 + } + + fn get_line(&self, x: &Xfer, line: u32) -> Vec { + (0..x.width).flat_map(|k| (0..x.bpp).map(move |i| (k, i))).map(|(k, i)| self.line_byte(x, line, k, i)).collect() + } + + /// A PIO write doubleword. Each line starts in a fresh doubleword, after + /// its begin offset; the rest of a line's last doubleword is dropped. + fn pio_write(&mut self, dw: u64) -> bool { + let Some(mut x) = self.xfer.take() else { return false }; + self.stats.pio_write_dw += 1; + let mut changed = false; + for i in 0..8 { + if x.line >= x.block.rows() as u32 { + break; + } + if x.skip > 0 { + x.skip -= 1; + continue; + } + if let Some(b) = self.line_buf.get_mut(x.pending as usize) { + *b = (dw >> (56 - 8 * i)) as u8; + } + x.pending += 1; + if x.pending == x.line_bytes() { + let n = (x.pending as usize).min(MAX_LINE_BYTES); + let bytes = self.line_buf[..n].to_vec(); + self.put_line(&x, x.line, &bytes); + changed = true; + x.pending = 0; + x.line += 1; + x.line_begin = x.next_begin(x.line_begin); + x.skip = x.line_begin; + break; + } + } + self.xfer = Some(x); + changed + } + + /// The PIO read doubleword at the stream's current position (0 past the + /// end, or when the transfer is not a PIO read). + fn pio_doubleword(&self) -> u64 { + let Some(x) = self.xfer.as_ref() else { return 0 }; + if !x.pio_read || x.rd_line >= x.block.rows() as u32 { + return 0; + } + (0..8).fold(0u64, |a, j| { + let pos = x.rd_pos + j; + let b = match pos.checked_sub(x.rd_begin) { + Some(p) if p < x.line_bytes() => self.line_byte(x, x.rd_line, p / x.bpp, p % x.bpp), + _ => 0, + }; + (a << 8) | b as u64 + }) + } + + /// PIO read, high half, without taking it (`pio_read_hi` takes it). + pub fn pio_peek_hi(&self) -> u32 { + (self.pio_doubleword() >> 32) as u32 + } + + /// PIO read, high half: takes the next doubleword. + pub fn pio_read_hi(&mut self) -> u32 { + let dw = self.pio_doubleword(); + self.stats.pio_read_dw += 1; + let Some(x) = self.xfer.as_mut() else { return 0 }; + x.out_lo = dw as u32; + if x.pio_read && x.rd_line < x.block.rows() as u32 { + x.rd_pos += 8; + x.rd_settle(); + } + (dw >> 32) as u32 + } + + /// PIO read, low half of the doubleword the last high read took. + pub fn pio_read_lo(&self) -> u32 { + self.xfer.as_ref().map(|x| x.out_lo).unwrap_or(0) + } + + /// DMA into the armed block: line `line`'s pixel bytes. + pub fn dma_write_line(&mut self, line: u32, bytes: &[u8]) { + self.stats.dma_lines_in += 1; + if self.te_load != 0 { + self.te.load_line(&self.regs, line, bytes); + return; + } + if let Some(x) = self.xfer.take() { + self.put_line(&x, line, bytes); + self.xfer = Some(x); + } + } + + /// DMA out of the armed block: line `line`'s pixel bytes. + pub fn dma_read_line(&self, line: u32) -> Vec { + if self.te_read[0] != 0 { + if self.regs[reg::TE_RAW as usize] != 0 { + return self.te.read_raw(&self.regs, line, self.te_read[1]); + } + return self.te.read_line(&self.regs, line, self.te_read[1]); + } + self.xfer.as_ref().map(|x| self.get_line(x, line)).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The X server's screen height in these tests (window origin row 1023). + const SCREEN_H: usize = 1024; + + /// The PROM's 1280x1024 main buffer. + fn main_buffer() -> Buffer { + Buffer::new(0x240, Kind::Wide, 0x31E) + } + + fn px(r: &Rss, x: usize, y_top: usize) -> u32 { + r.mem.get(&main_buffer(), x as u32, (SCREEN_H - 1 - y_top) as u32) as u32 + } + + /// The X server's setup: window origin at the top row, Y-flip on, so + /// block coordinates are X's top-down ones. + fn x_server() -> Box { + let mut r = Rss::new_boxed(); + r.write(reg::CONFIG, 0xCAC, false); + r.write(reg::XYWIN, 1023 << 16, false); + r.write(reg::DRBSIZE, 0x31E, false); + r.write(reg::DRBPOINTERS, 0x240, false); + // All planes writable, as the X server sets them. + r.write(reg::COLORMASKLSBSA, u32::MAX, false); + r.write(reg::PP1FILLMODE, 0x0C00_4504, false); + r + } + + fn block(r: &mut Rss, x0: u32, y0: u32, x1: u32, y1: u32) { + r.write(reg::IR_ALIAS, 0x18, false); + r.write(reg::BLOCKXYSTARTI, x0 << 16 | y0, false); + r.write(reg::BLOCKXYENDI, x1 << 16 | y1, true); + } + + #[test] + fn fast_fill_lands_top_down_with_yflip() { + let mut r = x_server(); + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 0x13, false); + block(&mut r, 10, 20, 12, 21); + assert_eq!(px(&r, 10, 20), 0x13); + assert_eq!(px(&r, 12, 21), 0x13); + assert_eq!(px(&r, 13, 21), 0); + assert_eq!(px(&r, 10, 22), 0); + } + + /// The draw-buffer field picks DRBpointers' first page (1), its second + /// (2) or both (3): an X double-buffered window draws its back buffer + /// with 2 while the screen shows A. + #[test] + fn draw_buffer_field_picks_a_b_or_both() { + let mut r = x_server(); + let a = r.reg(reg::DRBPOINTERS) & 0x3FF; + let b_page = a + 0x100; + r.write(reg::DRBPOINTERS, (r.reg(reg::DRBPOINTERS) & !0xF_FFFF) | b_page << 10 | a, false); + let fm = r.reg(reg::PP1FILLMODE) & !(0x7F << 14); + let read = |r: &Rss, page: u32, x: u32, y: u32| { + let buf = Buffer::new(page, Kind::Wide, r.reg(reg::DRBSIZE)); + r.mem.get(&buf, x, (SCREEN_H - 1 - y as usize) as u32) as u32 & 0xFFF + }; + r.write(reg::FILLMODE, FILL_FAST, false); + for (field, x, c) in [(1u32, 10, 0x11), (2, 20, 0x22), (3, 30, 0x33)] { + r.write(reg::PP1FILLMODE, fm | field << 14, false); + r.write(reg::FILL_COLOR_R, c, false); + block(&mut r, x, 5, x, 5); + } + assert_eq!([read(&r, a, 10, 5), read(&r, b_page, 10, 5)], [0x11, 0], "field 1: A"); + assert_eq!([read(&r, a, 20, 5), read(&r, b_page, 20, 5)], [0, 0x22], "field 2: B"); + assert_eq!([read(&r, a, 30, 5), read(&r, b_page, 30, 5)], [0x33, 0x33], "field 3: both"); + } + + #[test] + fn rgb_fast_fill_packs_components() { + let mut r = x_server(); + r.write(reg::PP1FILLMODE, 3 << 26 | 0x104, false); // RGB pixel type, copy + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 0xF00, false); + r.write(reg::FILL_COLOR_G, 0x800, false); + r.write(reg::FILL_COLOR_B, 0x100, false); + block(&mut r, 0, 0, 0, 0); + assert_eq!(px(&r, 0, 0), 0x10_80F0); + } + + #[test] + fn pio_write_frames_each_line_in_a_fresh_doubleword() { + let mut r = x_server(); + // Three 1-byte pixels per line, two lines, begin skip 2: line 0 is + // bytes 2..5 of its doubleword; line 1 starts at (2 + 3) & 7 = 5. + r.write(reg::FILLMODE, 3 << 22, false); + r.write(reg::XFRMODE, 2 << 8, false); + r.write(reg::XFRSIZE, 2 << 16 | 3, false); + block(&mut r, 100, 50, 102, 51); + assert_eq!(r.transfer_armed(), Some(false)); + r.write(reg::CHAR_H, 0x0000_0102, false); + r.write(reg::CHAR_L, 0x03FF_FFFF, true); + r.write(reg::CHAR_H, 0x0000_0000, false); + r.write(reg::CHAR_L, 0x0004_0506, true); + assert_eq!([px(&r, 100, 50), px(&r, 101, 50), px(&r, 102, 50)], [1, 2, 3]); + assert_eq!([px(&r, 100, 51), px(&r, 101, 51), px(&r, 102, 51)], [4, 5, 6]); + } + + #[test] + fn pio_read_frames_each_line_in_a_fresh_doubleword() { + let mut r = x_server(); + for (k, v) in [1u32, 2, 3, 4, 5, 6].into_iter().enumerate() { + let (x, y) = (100 + k % 3, 50 + k / 3); + r.mem.put(&main_buffer(), x as u32, (SCREEN_H - 1 - y) as u32, v as u64); + } + // Three 1-byte pixels per line, two lines, begin skip 2: line 0 is + // bytes 2..5 of its doubleword; line 1 starts at (2 + 3) & 7 = 5. + r.write(reg::FILLMODE, 2 << 22, false); + r.write(reg::XFRMODE, 2 << 8, false); + r.write(reg::XFRSIZE, 2 << 16 | 3, false); + block(&mut r, 100, 50, 102, 51); + assert_eq!(r.transfer_armed(), Some(true)); + let mut dw = || { + let hi = r.pio_peek_hi(); + assert_eq!(r.pio_read_hi(), hi, "peek and take agree"); + (hi as u64) << 32 | r.pio_read_lo() as u64 + }; + assert_eq!(dw(), 0x0000_0102_0300_0000); + assert_eq!(dw(), 0x0000_0000_0004_0506); + assert_eq!(dw(), 0, "past the end"); + } + + #[test] + fn a_new_block_disarms_a_finished_write_transfer() { + let mut r = x_server(); + r.write(reg::FILLMODE, 5 << 22, false); + r.write(reg::XFRSIZE, 1 << 16 | 1, false); + block(&mut r, 0, 0, 0, 0); + assert!(r.transfer_armed().is_some()); + // A glyph block (block type 1 + char stipple, as X sends it): its + // char data must stipple, not feed the transfer. + r.write(reg::FILLMODE, 1 << 22 | FILL_CHAR_STIPPLE, false); + r.write(reg::RED, 0x7 << 12, false); + block(&mut r, 200, 10, 203, 10); + assert_eq!(r.transfer_armed(), None); + r.write(reg::CHAR_H, 0xA000_0000, true); + assert_eq!([px(&r, 200, 10), px(&r, 201, 10), px(&r, 202, 10)], [7, 0, 7]); + } + + #[test] + fn rgb_host_formats_round_trip() { + for (fmt, v) in [((8, 8), 0x0ABCu64), ((8, 10), 0x7FFF), ((8, 0), 0x00C0_FFEE), ((0, 1), 0xFFF), ((7, 1), 0x1212_3434_5656), ((7, 0), 0x12_3456), ((2, 3), 0x1234_5612), ((8, 1), 0x1212_3434_5656_FFFF)] { + assert_eq!(to_host(fmt, from_host(fmt, v)), v, "{fmt:?}"); + } + assert_eq!(from_host((8, 8), 0x0F0), pack_rgb(0, 0xFF, 0)); + // 8-8-8 host pixels are X pixel values of the visuals, red in 7:0. + assert_eq!(from_host((8, 0), 0x00_00FF), pack_rgb(0xFF, 0, 0)); + } + + #[test] + fn a_24_plane_mask_means_rgb_fills() { + let mut r = x_server(); + r.write(reg::PP1FILLMODE, 0x0C00_6304, false); + r.write(reg::COLORMASKLSBSA, 0xFF_FFFF, false); + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 0x380, false); + r.write(reg::FILL_COLOR_G, 0x8E0, false); + r.write(reg::FILL_COLOR_B, 0x8E0, false); + block(&mut r, 1, 1, 1, 1); + assert_eq!(px(&r, 1, 1), 0x8E_8E38); + } + + #[test] + fn an_all_planes_copy_keeps_24_bit_pixels() { + let mut r = x_server(); + // A window move's write-back: pixel type 2, every plane enabled, + // 4-byte host pixels. + r.write(reg::PP1FILLMODE, 0x0C00_6204, false); + r.write(reg::COLORMASKLSBSA, 0xFFFF_FFFF, false); + r.write(reg::FILLMODE, 5 << 22, false); + r.write(reg::XFRMODE, 0x80, false); + r.write(reg::XFRSIZE, 1 << 16 | 1, false); + block(&mut r, 7, 7, 7, 7); + r.dma_write_line(0, &[0x00, 0x50, 0x50, 0x50]); + assert_eq!(px(&r, 7, 7), 0x50_5050); + } + + #[test] + fn xor_fill_toggles_and_restores() { + let mut r = x_server(); + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 0x13, false); + block(&mut r, 5, 5, 5, 5); + r.write(reg::PP1FILLMODE, 0x0C00_4504 & !(0xF << 26) | 6 << 26, false); + r.write(reg::FILL_COLOR_R, 0x0F, false); + block(&mut r, 5, 5, 5, 5); + assert_eq!(px(&r, 5, 5), 0x13 ^ 0x0F); + block(&mut r, 5, 5, 5, 5); + assert_eq!(px(&r, 5, 5), 0x13); + } + + #[test] + fn stippled_line_skips_zero_bits() { + let mut r = x_server(); + r.write(reg::FILLMODE, FILL_LINE_STIPPLE, false); + r.write(reg::RED, 0x9 << 12, false); + r.write(reg::LINE_STIPPLE, 0xAAAA_AAAA, false); + r.write(reg::IR_ALIAS, 0x15, false); + r.write(reg::LINE_START, 300 << 16 | 40, false); + r.write(reg::LINE_END, 303 << 16 | 40, true); + assert_eq!((300..304).map(|x| px(&r, x, 40)).collect::>(), [9, 0, 9, 0]); + } + + /// What the IRIX desktop sends to shade an icon: fill mode 0x60 (line + /// stipple, opaque), the foreground in RED, the background in BG_COLOR. + /// Both colours land; nothing underneath shows through. + #[test] + fn opaque_stippled_line_draws_zero_bits_in_the_background_colour() { + let mut r = x_server(); + r.write(reg::FILLMODE, FILL_LINE_STIPPLE | FILL_LINE_STIPPLE_OPAQUE, false); + r.write(reg::RED, 0xF << 12, false); + r.write(reg::BG_COLOR, 0x7, false); + r.write(reg::LINE_STIPPLE, 0xAAAA_AAAA, false); + r.write(reg::IR_ALIAS, 0x15, false); + r.write(reg::LINE_START, 300 << 16 | 40, false); + r.write(reg::LINE_END, 303 << 16 | 40, true); + assert_eq!((300..304).map(|x| px(&r, x, 40)).collect::>(), [0xF, 7, 0xF, 7]); + } + + /// In RGB modes the background is three 12-bit components across two + /// registers, as IRIX writes it for a 12-bit window's icon: a 0xf0 grey + /// is 0xf00f00 (blue, green) and 0xf00 (red). These are the values from + /// a trace of the desktop repainting its Icon Catalog. + #[test] + fn opaque_stipple_background_in_rgb_modes() { + let mut r = x_server(); + r.write(reg::PP1FILLMODE, 0x0C00_4004, false); // RGB pixel type + r.write(reg::COLORMASKLSBSA, 0xFFF, false); + r.write(reg::FILLMODE, FILL_LINE_STIPPLE | FILL_LINE_STIPPLE_OPAQUE, false); + r.write(reg::PACKEDCOLOR, 0xA0_A0A0, false); + r.write(reg::BG_COLOR, 0xC00_700, false); + r.write(reg::BG_COLOR_RED, 0x300, false); + r.write(reg::LINE_STIPPLE, 0xAAAA_AAAA, false); + r.write(reg::IR_ALIAS, 0x15, false); + r.write(reg::LINE_START, 300 << 16 | 40, false); + r.write(reg::LINE_END, 301 << 16 | 40, true); + assert_eq!(px(&r, 300, 40), 0xA0_A0A0); + assert_eq!(px(&r, 301, 40), pack_rgb(0x30, 0x70, 0xC0), "red 0x30, green 0x70, blue 0xc0"); + } + + /// Block type 0 is a fill in the iterated colour, drawn at once (the + /// kernel's boot gradient: one block per line, no char data). + #[test] + fn block_type_0_fills_at_once() { + let mut r = x_server(); + r.write(reg::FILLMODE, 0x18000, false); + r.write(reg::RED, 0xDE << 12, false); + block(&mut r, 10, 30, 12, 30); + assert_eq!((10..14).map(|x| px(&r, x, 30)).collect::>(), [0xDE, 0xDE, 0xDE, 0]); + } + + /// An opaque char stipple draws its 0 bits in the background colour. + #[test] + fn opaque_char_stipple_draws_the_background() { + let mut r = x_server(); + r.write(reg::FILLMODE, 1 << 22 | FILL_CHAR_STIPPLE | FILL_CHAR_STIPPLE_OPAQUE, false); + r.write(reg::RED, 0x7 << 12, false); + r.write(reg::BG_COLOR, 0x3, false); + block(&mut r, 200, 10, 203, 10); + r.write(reg::CHAR_H, 0xA000_0000, true); + assert_eq!((200..205).map(|x| px(&r, x, 10)).collect::>(), [7, 3, 7, 3, 0]); + } + + /// X PolyPoint: IR opcode 4, then each execute of xline_xystarti draws + /// one pixel there. + #[test] + fn points_draw_at_the_line_start_register() { + let mut r = x_server(); + r.write(reg::RED, 0x5 << 12, false); + r.write(reg::IR_ALIAS, 0x4, false); + r.write(reg::LINE_START, 1205 << 16 | 174, true); + r.write(reg::LINE_START, 1204 << 16 | 176, true); + assert_eq!((px(&r, 1205, 174), px(&r, 1204, 176), px(&r, 1204, 174)), (5, 5, 0)); + } + + /// Fill mode bit 10 (X's CapNotLast) leaves the end point out. + #[test] + fn skip_last_leaves_the_end_point_out() { + let mut r = x_server(); + r.write(reg::FILLMODE, FILL_LINE_SKIP_LAST, false); + r.write(reg::RED, 0x9 << 12, false); + r.write(reg::IR_ALIAS, 0x15, false); + r.write(reg::LINE_START, 300 << 16 | 40, false); + r.write(reg::LINE_END, 303 << 16 | 40, true); + assert_eq!((300..305).map(|x| px(&r, x, 40)).collect::>(), [9, 9, 9, 0, 0]); + } + + /// Screen masks 1-4: each enabled one must pass (inside or outside, per + /// its window mode bit). + #[test] + fn screen_masks_combine() { + let mut r = x_server(); + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 0x7, false); + // Mask 1: keep inside x 10..20; mask 3: keep outside x 14..15. + r.write(reg::CLIP_X, 10 << 16 | 20, false); + r.write(reg::CLIP_Y, 1023, false); + r.write(reg::CLIP_X + 4, 14 << 16 | 15, false); + r.write(reg::CLIP_Y + 4, 1023, false); + r.write(reg::CLIP_MODE, 0x1 | 0x10 | 0x4, false); + block(&mut r, 0, 5, 30, 5); + let drawn: Vec = (0..31).filter(|&x| px(&r, x, 5) == 7).collect(); + assert_eq!(drawn, [10, 11, 12, 13, 16, 17, 18, 19, 20]); + } + + /// Without the opaque bit, BG_COLOR plays no part. + #[test] + fn transparent_stipple_ignores_the_background_colour() { + let mut r = x_server(); + r.write(reg::FILLMODE, FILL_LINE_STIPPLE, false); + r.write(reg::RED, 0xF << 12, false); + r.write(reg::BG_COLOR, 0x7, false); + r.write(reg::LINE_STIPPLE, 0x5555_5555, false); + r.write(reg::IR_ALIAS, 0x15, false); + r.write(reg::LINE_START, 300 << 16 | 40, false); + r.write(reg::LINE_END, 303 << 16 | 40, true); + assert_eq!((300..304).map(|x| px(&r, x, 40)).collect::>(), [0, 0xF, 0, 0xF]); + } +} + +/// The armed transfer's shape, for the trace. +fn xfer_desc(r: &Rss) -> String { + let mode = r.reg(reg::XFRMODE); + format!("{}x? bpp={} fmt=({}, {}) begin={} stride_skip={}", + r.reg(reg::XFRSIZE) & 0xFFFF, bytes_per_pixel(mode), (mode >> 4) & 0xF, mode & 0xF, (mode >> 8) & 7, (mode >> 14) & 0x1FF) +} diff --git a/src/dev/mgras/te1.rs b/src/dev/mgras/te1.rs new file mode 100644 index 00000000..d2f342f2 --- /dev/null +++ b/src/dev/mgras/te1.rs @@ -0,0 +1,657 @@ +//! TE1, IMPACT's texture engine, and its TRAM. +//! +//! TE1 sits in the raster subsystem between the RE4's rasteriser and its +//! fragment colouring. Its registers are RSS registers (`reg`); what is known +//! of them is in `ignore/gr4/TE1.h`. +//! +//! - **Download.** TRAM has no address/data port. Texels arrive through the +//! raster engine's transfer engine with `XFRCONTROL` pointing at the +//! texture side. The texture loader registers (`TL_*`) place them: page, +//! level size, sub-image origin, components and depth. libGLcore sends the +//! texels through the GE pixel pipeline (SEND_PIXELS routine 0x511A); our +//! GE HLE turns that into the transfer (`gl.rs`). +//! - **Sampling.** Per fragment the RE hands the TE S/W, T/W and 1/W. The TE +//! divides out W, picks a level of detail from the derivatives, then +//! fetches and filters from TRAM (`Sampler`). +//! - **Texture environment.** The texel is combined with the fragment colour +//! in the RE (`tex_env`): the environment registers are RE registers. +//! +//! TRAM layout is ours. libGLcore's texture manager hands out 16 KB pages +//! (4 TRAMs; `TXMIPMAP` / `TL_MIPMAP` hold a page and a bank bit per level), +//! and every texel's address must follow from what the registers say. A +//! level starts at its page. Levels of 16x16 texels and smaller share one +//! page, as libGLcore packs them, each at a fixed offset by size. A texel +//! takes `components * depth` nibbles (depth 4, 8 or 12 bits) rounded up to +//! a power of two, as on the board. +//! +//! - **Save and restore.** libGLcore's texture manager moves textures by +//! saving TRAM pages to host memory and loading them back elsewhere, +//! through a view of TRAM as a level 64 texels wide in the texture's own +//! format (`read_raw`, and a load with TL_SPEC 64 x 8192). It saves and +//! restores in runs of whole pages, so a page here must hold exactly the +//! texels a page holds on the board. +//! +//! A level's border texels (GL borders, and libGLcore's own) go to its +//! border page (TXBORDER per level, TL_BORDER at load), in the page's +//! upper half. GL_CLAMP clamps coordinates to the texture, so linear +//! filtering at an edge reaches the border: the border texel, or the border +//! colour (TXBCOLOR) when the texture has none (TEXMODE2 bit 16). +//! +//! Readback (glGetTexImage): the GE aims the sampler at the level and +//! arms a read transfer on the texture side; each line is a row of texels, +//! RGBA bytes (`read_line`). +//! +//! Not yet: detail and sharpen textures, 3D textures, texture lookup tables, +//! video texture. + +/// TE1 / texture-related RSS registers. +pub mod reg { + pub const TEXMODE1: u32 = 0x111; + pub const TXENV_RG: u32 = 0x142; + pub const TXENV_B: u32 = 0x143; + pub const TEXMODE2: u32 = 0x180; + pub const TXSIZE: u32 = 0x181; + pub const TXLOD: u32 = 0x183; + pub const TXBCOLOR_RG: u32 = 0x184; + pub const TXBCOLOR_BA: u32 = 0x185; + /// Index into the per-level tables below, advanced by each table write. + pub const TXADDR: u32 = 0x190; + pub const TXMIPMAP: u32 = 0x191; + pub const TXBORDER: u32 = 0x193; + pub const DETAILSCALE: u32 = 0x194; + pub const TL_WBUFFER: u32 = 0x1A0; + pub const TL_MODE: u32 = 0x1A1; + pub const TL_SPEC: u32 = 0x1A2; + pub const TL_ADDR: u32 = 0x1A3; + pub const TL_MIPMAP: u32 = 0x1A4; + pub const TL_BORDER: u32 = 0x1A5; + pub const TL_S_SIZE: u32 = 0x1A9; + pub const TL_T_SIZE: u32 = 0x1AA; + pub const TL_S_LEFT: u32 = 0x1AB; + pub const TL_T_BOTTOM: u32 = 0x1AC; + /// Iterators (double buffered; 64-bit hi/lo pairs): S/W, T/W, 1/W at + /// the first pixel, their steps along the span (X) and down the major + /// edge (E), and their Y derivatives. + pub const SW: u32 = 0x080; + pub const TW: u32 = 0x082; + pub const WI: u32 = 0x084; + pub const DWIE: u32 = 0x086; + pub const DWIX: u32 = 0x088; + pub const DWIY: u32 = 0x08A; + pub const DSWE: u32 = 0x0C0; + pub const DTWE: u32 = 0x0C2; + pub const DSWX: u32 = 0x0C4; + pub const DTWX: u32 = 0x0C6; + pub const DSWY: u32 = 0x0C8; + pub const DTWY: u32 = 0x0CA; +} + +/// The texture registers a GL context owns (single buffered), which the GE +/// keeps a shadow of and loads into the TE (`gl.rs`). TXADDR and the +/// per-level tables it indexes (TXMIPMAP, TXBORDER, DETAILSCALE) are kept +/// apart. +pub const CONTEXT_REGS: [u32; 31] = [ + 0x111, 0x142, 0x143, 0x180, 0x181, 0x182, 0x183, 0x184, 0x185, 0x186, 0x187, 0x188, 0x189, 0x18A, + 0x18B, 0x18C, 0x192, 0x195, 0x1A0, 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1A5, 0x1A6, 0x1A7, 0x1A8, 0x1A9, + 0x1AA, 0x1AB, 0x1AC, +]; + +/// Per-level tables written through TXADDR's index. +pub const TABLE_REGS: [u32; 3] = [reg::TXMIPMAP, reg::TXBORDER, reg::DETAILSCALE]; +pub const TABLE_LEN: usize = 32; + +/// Fixed point of the iterator registers: value * 2^32 (our choice; the +/// IDE's values suggest 1/W = 1.0 is 2^20 on the real board). +pub const ITER_ONE: f64 = 4_294_967_296.0; + +/// TEXMODE1: texture enable, environment mode (bits 2:1: modulate, decal, +/// blend, alpha-only modulate), components - 1 (4:3), texel class (11:9: +/// 4 RGB(A), 2 luminance(-alpha), 1 alpha, 3 intensity). +pub const TEXMODE1_ENABLE: u32 = 1; + +/// TEXMODE2: magnification / minification within a level linear, mipmaps +/// (nearest level; with bit 6 blending two), clamp S and T, mipmapping on. +const TM2_MAG_LINEAR: u32 = 1 << 7; +const TM2_MIN_LINEAR: u32 = 1 << 8; +const TM2_MIPMAP: u32 = 1 << 5; +const TM2_MIP_LINEAR: u32 = 1 << 6; +/// Clamp S / T (and R, bit 13) instead of repeating; with bit 14 +/// GL_CLAMP's coordinate clamp to [0, 1] (linear filtering at the edge is +/// half border), without it GL_CLAMP_TO_BORDER_SGIS (coordinates may pass +/// the edge, so samples there are all border). libGLcore +/// __glMgrim_TexParameter: GL_CLAMP sets axis bit | 0x4000, clamp to border +/// the axis bit alone. +const TM2_CLAMP_S: u32 = 1 << 11; +const TM2_CLAMP_T: u32 = 1 << 12; +const TM2_GL_CLAMP: u32 = 1 << 14; +/// The texture has no border: clamped fetches outside it read TXBCOLOR. +const TM2_NO_BORDER: u32 = 1 << 16; +const TM2_MM_ENABLE: u32 = 1 << 19; + +pub const TRAM_BYTES: usize = 4 << 20; +const TRAM_NIBBLES: usize = TRAM_BYTES * 2; +/// A TRAM page: 16 KB with 4 TRAMs. +const PAGE_NIBBLES: usize = 16384 * 2; + +/// Texel offset in a shared page of a level whose larger side is `n` (<= +/// 16): 16x16 first, then 8x8, 4x4, 2x2, 1x1. +fn small_offset(n: usize) -> usize { + let (mut off, mut k) = (0, 16); + while k > n { + off += k * k; + k /= 2; + } + off +} + +/// TRAM nibbles a texel cell takes: four components of `d` nibbles, +/// rounded up to a power of two, whatever the texture's own components +/// (RGB8 and L8 alike take 32 bits: libGLcore counts 4096 texels to a +/// 16 KB page for both, 8192 for RGBA4). Textures of one or two +/// components share cells (`slot`). The texture manager's page saves move +/// 64-cell rows, so our pages must hold what its pages hold. +fn cell_nibbles(d: usize) -> usize { + (4 * d).next_power_of_two() +} + +/// Nibble offset in its cell of component `c` of a texture of `nc` +/// components in select slot `sel` (SGIS_texture_select: TL_MODE bits 4:3 +/// at load, TEXMODE1 bits 8:7 when sampling; traced, TyrQuake packs four +/// 256x256 luminance lightmaps into one set of pages, slots 0-3). +/// Only textures that leave room share cells: four slots for one +/// component, two for two; three- and four-component textures fill the +/// cell, and the select bits mean nothing for them (TyrQuake draws RGBA +/// textures with TEXMODE1 0x409D9, bits 8:7 = 3). +fn comp_offset(sel: usize, nc: usize, c: usize, d: usize) -> usize { + let slot = match nc { + 1 => sel & 3, + 2 => (sel & 1) * 2, + _ => 0, + }; + (slot + c).min(3) * d +} + +/// Nibble address of texel (s, t) in a w x h level at TRAM `page`, texels +/// of `tn` nibbles. +fn texel_addr(page: u32, w: usize, h: usize, s: usize, t: usize, tn: usize) -> usize { + let side = w.max(h); + let off = if side <= 16 { small_offset(side) } else { 0 }; + (page as usize * PAGE_NIBBLES + (off + t * w + s) * tn) % TRAM_NIBBLES +} + +/// Border texel offset in a shared border page for a level whose larger +/// side is `n` (<= 16): each level's border is 4n + 4 texels. +fn small_border_offset(n: usize) -> usize { + let (mut off, mut k) = (0, 16); + while k > n { + off += 4 * k + 4; + k /= 2; + } + off +} + +/// Nibble address of border texel (s, t), s in -1..=w, t in -1..=h, on +/// the border of a w x h level, in the upper half of border page `page`: +/// the bottom row, the top row (w + 2 each, corners included), then the +/// left and right columns (h each). +fn border_addr(page: u32, w: usize, h: usize, s: i64, t: i64, tn: usize) -> usize { + let (wi, hi) = (w as i64, h as i64); + let idx = if t < 0 { + s + 1 + } else if t >= hi { + wi + 2 + s + 1 + } else if s < 0 { + 2 * (wi + 2) + t + } else { + 2 * (wi + 2) + hi + t + } as usize; + let side = w.max(h); + let off = if side <= 16 { small_border_offset(side) } else { 0 }; + (page as usize * PAGE_NIBBLES + PAGE_NIBBLES / 2 + (off + idx) * tn) % TRAM_NIBBLES +} + +/// A component's depth in nibbles, from TL_MODE bits 6:5 / TEXMODE2 bits +/// 3:2: 0, 1, 2 = 4, 8, 12 bits (traced: libGLcore stores RGBA8 at 0 with 1 +/// TRAM, at 1 with 4). +fn depth_nibbles(field: u32) -> usize { + match field & 3 { + 0 => 1, + 2 => 3, + _ => 2, + } +} + +/// The texture engine: TRAM and the per-level tables. Plain data, valid +/// zeroed. +#[repr(C)] +pub struct Te1 { + tram: [u8; TRAM_BYTES], + tables: [[u32; TABLE_LEN]; 3], + pub texels_loaded: u64, + /// Diagnostic: per page, the last level loaded there (page << 16 | + /// TL_SPEC's sizes), for `stale`. + owner: [u32; 256], + /// Diagnostic: stale samples seen, and the last one's (want, have). + pub stale_events: u32, + pub stale_last: [u32; 2], +} + +impl Te1 { + fn nibble(&self, a: usize) -> u32 { + let b = self.tram[a / 2] as u32; + if a & 1 == 0 { b & 0xF } else { b >> 4 } + } + + fn set_nibble(&mut self, a: usize, v: u32) { + let b = &mut self.tram[a / 2]; + *b = if a & 1 == 0 { (*b & 0xF0) | (v as u8 & 0xF) } else { (*b & 0x0F) | (v as u8) << 4 }; + } + + /// Component of `d` nibbles at nibble address `a`, as 0..1. + fn component(&self, a: usize, d: usize) -> f32 { + let v = (0..d).fold(0, |v, i| v | self.nibble((a + i) % TRAM_NIBBLES) << (4 * i)); + v as f32 / ((1u32 << (4 * d)) - 1) as f32 + } + + /// A table write (TXMIPMAP, TXBORDER, DETAILSCALE) at `index`. + pub fn table_write(&mut self, r: u32, index: u32, v: u32) { + if let Some(k) = TABLE_REGS.iter().position(|&t| t == r) { + self.tables[k][index as usize % TABLE_LEN] = v; + } + } + + /// One line of a texture load, 4 bytes a texel (components in order, + /// unused ones ignored), placed by the texture loader registers: line + /// `line` is texel row TL_T_BOTTOM + line, from column TL_S_LEFT, in + /// the level of TL_SPEC's size at TL_MIPMAP's page, TL_MODE's + /// components and depth. + pub fn load_line(&mut self, regs: &[u32], line: u32, bytes: &[u8]) { + let mode = regs[reg::TL_MODE as usize]; + let spec = regs[reg::TL_SPEC as usize]; + let nc = ((mode >> 1) & 3) as usize + 1; + let sel = ((mode >> 3) & 3) as usize; + let d = depth_nibbles(mode >> 5); + let (w, h) = (1usize << ((spec >> 4) & 0xF), 1usize << ((spec >> 8) & 0xF)); + let page = regs[reg::TL_MIPMAP as usize] & 0xFF; + // The origin is signed: a GL border starts at -1. Border texels go + // to the border page; anything further out is dropped. + let border_page = regs[reg::TL_BORDER as usize] & 0xFF; + let s0 = regs[reg::TL_S_LEFT as usize] as i32 as i64; + let t = regs[reg::TL_T_BOTTOM as usize] as i32 as i64 + line as i64; + let (wi, hi) = (w as i64, h as i64); + if t < -1 || t > hi { + return; + } + if line == 0 && w.max(h) > 16 { + // A restore (the 64-wide page view) brings back what was saved: + // whose it is, this diagnostic cannot tell. + let n = (w * h * cell_nibbles(d)).div_ceil(PAGE_NIBBLES).min(64); + let who = if spec & 0xFF0 == 0xD60 { 0 } else { page << 16 | (spec & 0xFF0) }; + for p in page as usize..(page as usize + n).min(256) { + self.owner[p] = who; + } + } + for (k, px) in bytes.chunks_exact(4).enumerate() { + let s = s0 + k as i64; + if s < -1 || s > wi { + continue; + } + let a = if s < 0 || t < 0 || s >= wi || t >= hi { + border_addr(border_page, w, h, s, t, cell_nibbles(d)) + } else { + texel_addr(page, w, h, s as usize, t as usize, cell_nibbles(d)) + }; + for (c, &v) in px.iter().take(nc).enumerate() { + let v = v as u32; + let stored = match d { + 1 => v >> 4, + 3 => v << 4 | v >> 4, + _ => v, + }; + for i in 0..d { + self.set_nibble((a + comp_offset(sel, nc, c, d) + i) % TRAM_NIBBLES, stored >> (4 * i)); + } + } + self.texels_loaded += 1; + } + } + + /// Bytes per texel a readback delivers for transfer mode `xfrmode` + /// (traced, glGetTexImage per internal format): format 9 one + /// component, 0xA two, 7 RGB, 8 RGBA; type 0 bytes, 1 16-bit. + pub fn read_texel_bytes(xfrmode: u32) -> u32 { + let n = match (xfrmode >> 4) & 0xF { + 9 => 1, + 0xA => 2, + 7 => 3, + _ => 4, + }; + n * if xfrmode & 0xF == 1 { 2 } else { 1 } + } + + /// Row `line` of the texture manager's page view (its save of TRAM + /// pages): from the page of level 0 in the TXMIPMAP table, `w` texels + /// a row in a level `w` wide, of the components (TEXMODE1) and depth + /// (TEXMODE2) of the texture being saved. Each texel is its components + /// in storage order, as many as the transfer mode's format carries + /// (traced: RGB textures 0x400070, three bytes; RGBA 0x400080, four), + /// 8 bits each or 16 for type 1. The restore (TL_MODE the same + /// components and depth, TL_SPEC as wide) stores them back. + pub fn read_raw(&self, regs: &[u32], line: u32, w: u32) -> Vec { + let nc = ((regs[reg::TEXMODE1 as usize] >> 3) & 3) as usize + 1; + let d = depth_nibbles(regs[reg::TEXMODE2 as usize] >> 2); + let sel = ((regs[reg::TEXMODE1 as usize] >> 7) & 3) as usize; + let xfrmode = regs[0x159]; + let wide = xfrmode & 0xF == 1; + let n = Self::read_texel_bytes(xfrmode) as usize / if wide { 2 } else { 1 }; + let page = self.tables[0][0] & 0xFF; + let mut out = Vec::with_capacity(w as usize * n * 2); + for s in 0..w as usize { + let a = texel_addr(page, w as usize, 1 << 13, s, line as usize, cell_nibbles(d)); + for c in 0..n { + let o = comp_offset(sel, nc, c, d); + let v = if c < nc { (0..d).fold(0, |v, i| v | self.nibble((a + o + i) % TRAM_NIBBLES) << (4 * i)) } else { 0 }; + // As 16 bits, then the top 8 for a byte. + let v16 = match d { + 1 => v * 0x1111, + 2 => v * 0x101, + _ => v << 4 | v >> 8, + } as u16; + if wide { + out.extend(v16.to_be_bytes()); + } else { + out.push((v16 >> 8) as u8); + } + } + } + out + } + + /// Row `line` of the level the sampler is aimed at (level 0 of the + /// TXMIPMAP table, TXSIZE's size), `w` texels in the transfer mode's + /// layout (XFRMODE): the texture's own components (one: luminance, + /// alpha or intensity; two: luminance-alpha; RGB), or RGBA with the + /// class's components where GL's readback puts them (luminance and + /// intensity in red, alpha in alpha, 1 where absent); 8 or 16 bits. + pub fn read_line(&self, regs: &[u32], line: u32, w: u32) -> Vec { + let smp = self.sampler(regs); + let class = (regs[reg::TEXMODE1 as usize] >> 9) & 7; + let xfrmode = regs[0x159]; + let n = match (xfrmode >> 4) & 0xF { + 9 => 1, + 0xA => 2, + 7 => 3, + _ => 4, + }; + let wide = xfrmode & 0xF == 1; + let mut out = Vec::with_capacity((w * Self::read_texel_bytes(xfrmode)) as usize); + for k in 0..w { + let c = smp.texel(self, 0, k as i64, line as i64); + let rgba = match class { + 1 => [0.0, 0.0, 0.0, c[0]], + 2 => [c[0], 0.0, 0.0, if smp.nc == 2 { c[1] } else { 1.0 }], + 3 => [c[0], 0.0, 0.0, 1.0], + _ => [c[0], c[1], c[2], if smp.nc == 4 { c[3] } else { 1.0 }], + }; + // Two 8-bit components travel as one 16-bit unit, alpha in the + // high byte (traced: libGLcore reads LA8 as A, L; LA12 as L, A). + let comps = match n { + 4 => rgba, + 2 if !wide => [c[1], c[0], 0.0, 0.0], + _ => c, + }; + for &v in comps.iter().take(n) { + let v = v.clamp(0.0, 1.0); + if wide { + out.extend(((v * 65535.0).round() as u16).to_be_bytes()); + } else { + out.push((v * 255.0).round() as u8); + } + } + } + out + } + + /// Diagnostic: level 0 of the bound texture when its page was last + /// loaded with another level ((page, owner) of that load), else None. + pub fn stale(&self, regs: &[u32]) -> Option<(u32, u32)> { + let size = regs[reg::TXSIZE as usize]; + let (ls, lt) = (size & 0xF, (size >> 4) & 0xF); + if ls.max(lt) <= 4 { + return None; + } + let page = self.tables[0][0] & 0xFF; + let want = page << 16 | lt << 8 | ls << 4; + let have = self.owner[page as usize]; + (have != 0 && have != want).then_some((want, have)) + } + + /// The sampler for the registers as they stand (once per primitive). + pub fn sampler(&self, regs: &[u32]) -> Sampler { + let m2 = regs[reg::TEXMODE2 as usize]; + let m1 = regs[reg::TEXMODE1 as usize]; + let size = regs[reg::TXSIZE as usize]; + let mut pages = [0u32; 16]; + let mut border_pages = [0u32; 16]; + for (k, p) in pages.iter_mut().enumerate() { + *p = self.tables[0][k] & 0xFF; + border_pages[k] = self.tables[1][k] & 0xFF; + } + // The border colour as the texel class's components. + let c = |v: u32| (v & 0xFFF) as f32 / 4095.0; + let (rg, ba) = (regs[reg::TXBCOLOR_RG as usize], regs[reg::TXBCOLOR_BA as usize]); + let (r, g, b, a) = (c(rg), c(rg >> 12), c(ba), c(ba >> 12)); + let border_color = match (m1 >> 9) & 7 { + 1 => [a, 0.0, 0.0, 0.0], + 2 => [r, a, 0.0, 0.0], + 3 => [r, 0.0, 0.0, 0.0], + _ => [r, g, b, a], + }; + let mipmaps = m2 & TM2_MM_ENABLE != 0 && m2 & TM2_MIPMAP != 0; + Sampler { + mode2: m2, + ls: size & 0xF, + lt: (size >> 4) & 0xF, + max_level: if mipmaps { (regs[reg::TXLOD as usize] & 0xF).min(15) } else { 0 }, + nc: ((m1 >> 3) & 3) as usize + 1, + sel: ((m1 >> 7) & 3) as usize, + d: depth_nibbles(m2 >> 2), + pages, + border_pages, + border_color, + } + } +} + +/// What the TE needs to sample one primitive's fragments. +#[derive(Clone, Copy)] +pub struct Sampler { + mode2: u32, + /// log2 of level 0's size. + pub ls: u32, + pub lt: u32, + max_level: u32, + nc: usize, + /// Select slot (TEXMODE1 bits 8:7, see `comp_offset`). + sel: usize, + d: usize, + pages: [u32; 16], + border_pages: [u32; 16], + border_color: [f32; 4], +} + +impl Sampler { + /// Level 0's size in texels. + pub fn size(&self) -> (f64, f64) { + ((1u32 << self.ls) as f64, (1u32 << self.lt) as f64) + } + + /// Texel (i, j) of `level`, its components: wrapped (repeat), or + /// outside the level (clamp) the border texel or the border colour. + fn texel(&self, te: &Te1, level: u32, i: i64, j: i64) -> [f32; 4] { + let w = 1usize << self.ls.saturating_sub(level); + let h = 1usize << self.lt.saturating_sub(level); + let axis = |v: i64, n: usize, clamp: bool| if clamp { v.clamp(-1, n as i64) } else { v.rem_euclid(n as i64) }; + let s = axis(i, w, self.mode2 & TM2_CLAMP_S != 0); + let t = axis(j, h, self.mode2 & TM2_CLAMP_T != 0); + let tn = cell_nibbles(self.d); + let a = if s < 0 || t < 0 || s >= w as i64 || t >= h as i64 { + if self.mode2 & TM2_NO_BORDER != 0 { + return self.border_color; + } + border_addr(self.border_pages[level as usize & 15], w, h, s, t, tn) + } else { + texel_addr(self.pages[level as usize & 15], w, h, s as usize, t as usize, tn) + }; + let mut c = [0.0f32; 4]; + for (k, o) in c.iter_mut().enumerate().take(self.nc) { + *o = te.component(a + comp_offset(self.sel, self.nc, k, self.d), self.d); + } + c + } + + /// A level, sampled at (s, t) (texture coordinates, 0..1 over the + /// texture), nearest or bilinear. + fn level(&self, te: &Te1, level: u32, s: f64, t: f64, linear: bool) -> [f32; 4] { + let w = (1u32 << self.ls.saturating_sub(level)) as f64; + let h = (1u32 << self.lt.saturating_sub(level)) as f64; + // GL_CLAMP: coordinates clamped to [0, 1]; nearest then stays in + // the texture, linear reaches half a texel past its edge. Clamp to + // border leaves them be: the texel index clamp reads border there. + let gl_clamp = self.mode2 & TM2_GL_CLAMP != 0; + let s = if gl_clamp && self.mode2 & TM2_CLAMP_S != 0 { s.clamp(0.0, 1.0) } else { s }; + let t = if gl_clamp && self.mode2 & TM2_CLAMP_T != 0 { t.clamp(0.0, 1.0) } else { t }; + let (u, v) = (s * w, t * h); + if !linear { + // At s = 1 exactly, clamped nearest stays on the last texel. + let cap = |x: f64, n: f64, clamp: bool| if clamp && gl_clamp { (x.floor() as i64).min(n as i64 - 1) } else { x.floor() as i64 }; + return self.texel(te, level, cap(u, w, self.mode2 & TM2_CLAMP_S != 0), cap(v, h, self.mode2 & TM2_CLAMP_T != 0)); + } + let (u, v) = (u - 0.5, v - 0.5); + let (i, j) = (u.floor(), v.floor()); + let (a, b) = ((u - i) as f32, (v - j) as f32); + let (i, j) = (i as i64, j as i64); + let (t00, t10) = (self.texel(te, level, i, j), self.texel(te, level, i + 1, j)); + let (t01, t11) = (self.texel(te, level, i, j + 1), self.texel(te, level, i + 1, j + 1)); + let mut c = [0.0f32; 4]; + for k in 0..4 { + c[k] = (1.0 - a) * (1.0 - b) * t00[k] + a * (1.0 - b) * t10[k] + (1.0 - a) * b * t01[k] + a * b * t11[k]; + } + c + } + + /// The filtered texel at (s, t) for level of detail `lambda` (log2 of + /// texels per pixel at level 0): magnification below 0, minification + /// (with mipmaps when on) above. + pub fn sample(&self, te: &Te1, s: f64, t: f64, lambda: f64) -> [f32; 4] { + let m = self.mode2; + if !(lambda > 0.0) { + return self.level(te, 0, s, t, m & TM2_MAG_LINEAR != 0); + } + let linear = m & TM2_MIN_LINEAR != 0; + if self.max_level == 0 { + return self.level(te, 0, s, t, linear); + } + let top = self.max_level as f64; + if m & TM2_MIP_LINEAR == 0 { + let l = ((lambda + 0.5).ceil() - 1.0).clamp(0.0, top) as u32; + return self.level(te, l, s, t, linear); + } + let l0 = lambda.floor().clamp(0.0, top); + let l1 = (l0 + 1.0).min(top); + let f = (lambda - l0).clamp(0.0, 1.0) as f32; + let (a, b) = (self.level(te, l0 as u32, s, t, linear), self.level(te, l1 as u32, s, t, linear)); + [0, 1, 2, 3].map(|k| a[k] + (b[k] - a[k]) * f) + } +} + +/// The texture environment (RE4): fragment colour `f` (0..1) and texel +/// components `tex`, per TEXMODE1 (environment mode, components, texel +/// class) and the environment colour (TXENV_RG: red 11:0, green 23:12; +/// TXENV_B: blue 11:0, alpha 23:12). +pub fn tex_env(mode1: u32, env_rg: u32, env_b: u32, f: [f64; 4], tex: [f32; 4]) -> [f64; 4] { + let tex = tex.map(|c| c as f64); + let nc = ((mode1 >> 3) & 3) + 1; + // (texel colour, texel alpha), each if the format has it. + let (ct, at) = match (mode1 >> 9) & 7 { + 1 => (None, Some(tex[0])), + 2 => (Some([tex[0]; 3]), (nc == 2).then_some(tex[1])), + 3 => (Some([tex[0]; 3]), Some(tex[0])), + _ if nc >= 3 => (Some([tex[0], tex[1], tex[2]]), (nc == 4).then_some(tex[3])), + _ => (Some([tex[0]; 3]), (nc == 2).then_some(tex[1])), + }; + let e = |v: u32| (v & 0xFFF) as f64 / 4095.0; + let cc = [e(env_rg), e(env_rg >> 12), e(env_b)]; + let ac = e(env_b >> 12); + let intensity = (mode1 >> 9) & 7 == 3; + let mut out = f; + match (mode1 >> 1) & 3 { + // Decal: RGB replaces; RGBA blends by the texel's alpha; alpha kept. + 1 => { + if let Some(ct) = ct { + let a = at.unwrap_or(1.0); + for k in 0..3 { + out[k] = f[k] * (1.0 - a) + ct[k] * a; + } + } + } + // Blend: the texel blends the fragment colour toward the + // environment colour. + 2 => { + if let Some(ct) = ct { + for k in 0..3 { + out[k] = f[k] * (1.0 - ct[k]) + cc[k] * ct[k]; + } + } + if let Some(at) = at { + out[3] = if intensity { f[3] * (1.0 - at) + ac * at } else { f[3] * at }; + } + } + // Alpha-only modulate (alpha textures). + 3 => { + if let Some(at) = at { + out[3] = f[3] * at; + } + } + // Modulate (and GL_REPLACE, which libGLcore sends as modulate). + _ => { + if let Some(ct) = ct { + for k in 0..3 { + out[k] = f[k] * ct[k]; + } + } + if let Some(at) = at { + out[3] = f[3] * at; + } + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A 64x128 RGB8 level takes two pages (32-bit texels), so the page + /// view of its second page starts at texel row 64: libGLcore saves a + /// texture page by page, and a run that starts at a page must find + /// the level's texels there. + #[test] + fn rgb8_level_fills_pages_as_on_the_board() { + // SAFETY: Te1 is plain data, valid zeroed. + let mut te = unsafe { Box::::new_zeroed().assume_init() }; + let mut regs = vec![0u32; 0x400]; + regs[reg::TL_MODE as usize] = 0x34E24; + regs[reg::TL_SPEC as usize] = 7 << 8 | 6 << 4; + for t in 0..128u32 { + regs[reg::TL_T_BOTTOM as usize] = 0; + let line: Vec = (0..64u32).flat_map(|s| [s as u8, t as u8, 0xA5, 0]).collect(); + te.load_line(®s, t, &line); + } + regs[reg::TEXMODE1 as usize] = 2 << 3; + regs[reg::TEXMODE2 as usize] = 1 << 2; + te.table_write(reg::TXMIPMAP, 0, 1); + let row = te.read_raw(®s, 6, 64); + assert_eq!(&row[..8], &[0, 70, 0xA5, 0, 1, 70, 0xA5, 0]); + } +} diff --git a/src/dev/mgras/testdata/xsetroot_red.txt b/src/dev/mgras/testdata/xsetroot_red.txt new file mode 100644 index 00000000..a87a4040 --- /dev/null +++ b/src/dev/mgras/testdata/xsetroot_red.txt @@ -0,0 +1,9461 @@ +# xsetroot -solid red on the IRIX 6.5.0 root desktop (High IMPACT): X fills the +# exposed root with index 1 (red in the default 4sight colormap, DID 1's block) +# and 4Dwm repaints the desktop icons. Captured with mgras rec; gio_status polls and checkpoints dropped. +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +T +W 8 61c88 4 +T +W 8 61c88 4 +T +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +W 8 61c88 4 +W 32 61f00 3ff +W 8 61c88 4 +W 32 61f00 3ff +R 8 61208 8 +W 16 60c30 1c11 +W 16 60c30 1c11 +W 8 61c88 4 +W 32 61f00 43ff +W 16 60c30 1c11 +W 32 60d18 ffffbf00 +W 8 61c88 4 +W 32 61f00 3ff +W 8 61c88 4 +W 32 61f00 3ff +T +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 18460400000000 +W 64 70080 1c470404ff0008 +W 64 70080 18460400000009 +W 64 70080 1c4704000800c1 +W 64 70080 18460400720009 +W 64 70080 1c470404ff00c1 +W 64 70080 184604000000c2 +W 64 70080 1c470404ff0122 +W 64 70080 18460400000123 +W 64 70080 1c470400d702b2 +W 64 70080 184604037f0123 +W 64 70080 1c470404ff02b2 +W 64 70080 184604000002b3 +W 64 70080 1c470404ff03ff +T +W 8 61c88 4 +T +W 8 61c88 4 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c60025 +W 64 70080 1c470404c90025 +W 64 70080 18460404c40026 +W 64 70080 1c470404cb0026 +W 64 70080 18460404c20027 +W 64 70080 1c470404cd0027 +W 64 70080 18460404c00028 +W 64 70080 1c470404cb0028 +W 64 70080 18460404be0029 +W 64 70080 1c470404c90029 +W 64 70080 18460404bc002a +W 64 70080 1c470404c7002a +W 64 70080 18460404ba002b +W 64 70080 1c470404c5002b +W 64 70080 18460404b8002c +W 64 70080 1c470404c3002c +W 64 70080 18460404b6002d +W 64 70080 1c470404c1002d +W 64 70080 18460404b4002e +W 64 70080 1c470404bf002e +W 64 70080 18460404b2002f +W 64 70080 1c470404bd002f +W 64 70080 18460404b00030 +W 64 70080 1c470404bb0030 +W 64 70080 18460404ae0031 +W 64 70080 1c470404b90031 +W 64 70080 18460404ac0032 +W 64 70080 1c470404b70032 +W 64 70080 18460404aa0033 +W 64 70080 1c470404b50033 +W 64 70080 18460404a70034 +W 64 70080 1c470404b30034 +W 64 70080 18460404aa0035 +W 64 70080 1c470404b10035 +W 64 70080 18460404ac0036 +W 64 70080 1c470404af0036 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c80012 +W 64 70080 1c410404c90012 +W 64 70080 195a0455555555 +W 64 70080 18400404c60013 +W 64 70080 1c410404c90013 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c40014 +W 64 70080 1c410404c90014 +W 64 70080 195a0455555555 +W 64 70080 18400404c20015 +W 64 70080 1c410404c90015 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c00016 +W 64 70080 1c410404c90016 +W 64 70080 195a0455555555 +W 64 70080 18400404be0017 +W 64 70080 1c410404c90017 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc0018 +W 64 70080 1c410404c90018 +W 64 70080 195a0455555555 +W 64 70080 18400404ba0019 +W 64 70080 1c410404c90019 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b8001a +W 64 70080 1c410404c9001a +W 64 70080 195a0455555555 +W 64 70080 18400404b6001b +W 64 70080 1c410404c9001b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b4001c +W 64 70080 1c410404c9001c +W 64 70080 195a0455555555 +W 64 70080 18400404b2001d +W 64 70080 1c410404c9001d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b0001e +W 64 70080 1c410404c9001e +W 64 70080 195a0455555555 +W 64 70080 18400404ae001f +W 64 70080 1c410404c9001f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0020 +W 64 70080 1c410404c90020 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0021 +W 64 70080 1c410404c80021 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0022 +W 64 70080 1c410404c60022 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0023 +W 64 70080 1c410404c40023 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0024 +W 64 70080 1c410404c20024 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0025 +W 64 70080 1c410404c00025 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0026 +W 64 70080 1c410404be0026 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0027 +W 64 70080 1c410404bc0027 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0028 +W 64 70080 1c410404ba0028 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0029 +W 64 70080 1c410404b90029 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa002a +W 64 70080 1c410404b7002a +W 64 70080 195a0455555555 +W 64 70080 18400404aa002b +W 64 70080 1c410404b5002b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa002c +W 64 70080 1c410404b3002c +W 64 70080 195a0455555555 +W 64 70080 18400404aa002d +W 64 70080 1c410404b1002d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa002e +W 64 70080 1c410404af002e +W 64 70080 195a0455555555 +W 64 70080 18400404aa002f +W 64 70080 1c410404ad002f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0030 +W 64 70080 1c410404ab0030 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404aa0021 +W 64 70080 1c410404ca0011 +W 64 70080 18400404ca0011 +W 64 70080 1c410404ca0020 +W 64 70080 18400404ca0020 +W 64 70080 1c410404aa0031 +W 64 70080 18400404aa0022 +W 64 70080 1c410404aa0032 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c4001d +W 64 70080 1c410404c7001d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c2001e +W 64 70080 1c410404c9001e +W 64 70080 195a0455555555 +W 64 70080 18400404c0001f +W 64 70080 1c410404c8001f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0020 +W 64 70080 1c410404c60020 +W 64 70080 195a0455555555 +W 64 70080 18400404bc0021 +W 64 70080 1c410404c40021 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba0022 +W 64 70080 1c410404c20022 +W 64 70080 195a0455555555 +W 64 70080 18400404b80023 +W 64 70080 1c410404c00023 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b60024 +W 64 70080 1c410404be0024 +W 64 70080 195a0455555555 +W 64 70080 18400404b40025 +W 64 70080 1c410404bc0025 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b20026 +W 64 70080 1c410404ba0026 +W 64 70080 195a0455555555 +W 64 70080 18400404b00027 +W 64 70080 1c410404b80027 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae0028 +W 64 70080 1c410404b60028 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b10029 +W 64 70080 1c410404b40029 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae0028 +W 64 70080 1c410404c6001c +W 64 70080 18400404c6001c +W 64 70080 1c410404ca001e +W 64 70080 18400404ca001e +W 64 70080 1c410404b3002a +W 64 70080 18400404b3002a +W 64 70080 1c410404ae0028 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c40018 +W 64 70080 1c470404c50018 +W 64 70080 18460404c20019 +W 64 70080 1c470404c50019 +W 64 70080 18460404c0001a +W 64 70080 1c470404c5001a +W 64 70080 18460404be001b +W 64 70080 1c470404c5001b +W 64 70080 18460404bc001c +W 64 70080 1c470404c5001c +W 64 70080 18460404b9001d +W 64 70080 1c470404c3001d +W 64 70080 18460404b7001e +W 64 70080 1c470404c1001e +W 64 70080 18460404b5001f +W 64 70080 1c470404bf001f +W 64 70080 18460404b30020 +W 64 70080 1c470404bd0020 +W 64 70080 18460404b10021 +W 64 70080 1c470404bb0021 +W 64 70080 18460404ae0022 +W 64 70080 1c470404b90022 +W 64 70080 18460404ae0023 +W 64 70080 1c470404b70023 +W 64 70080 18460404ae0024 +W 64 70080 1c470404b50024 +W 64 70080 18460404ae0025 +W 64 70080 1c470404b30025 +W 64 70080 18460404ae0026 +W 64 70080 1c470404b10026 +W 64 70080 18460404ae0027 +W 64 70080 1c470404af0027 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae0022 +W 64 70080 1c410404c60017 +W 64 70080 18400404c60017 +W 64 70080 1c410404c6001c +W 64 70080 18400404c6001c +W 64 70080 1c410404ae0028 +W 64 70080 18400404ae0023 +W 64 70080 1c410404ae0029 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b90029 +W 64 70080 1c470404cd0029 +W 64 70080 18460404b3002a +W 64 70080 1c470404d1002a +W 64 70080 18460404b2002b +W 64 70080 1c470404d2002b +W 64 70080 18460404b2002c +W 64 70080 1c470404d2002c +W 64 70080 18460404b2002d +W 64 70080 1c470404d2002d +W 64 70080 18460404b3002e +W 64 70080 1c470404d2002e +W 64 70080 18460404b3002f +W 64 70080 1c470404d1002f +W 64 70080 18460404b50030 +W 64 70080 1c470404cf0030 +W 64 70080 18460404b70031 +W 64 70080 1c470404ce0031 +W 64 70080 18460404b90032 +W 64 70080 1c470404cd0032 +W 64 70080 18460404bd0033 +W 64 70080 1c470404c80033 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000d +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b30024 +W 64 70080 1c470404d20024 +W 64 70080 18460404b20025 +W 64 70080 1c470404d30025 +W 64 70080 18460404b20026 +W 64 70080 1c470404d30026 +W 64 70080 18460404b20027 +W 64 70080 1c470404d30027 +W 64 70080 18460404b20028 +W 64 70080 1c470404d30028 +W 64 70080 18460404b20029 +W 64 70080 1c470404d20029 +W 64 70080 18460404b3002a +W 64 70080 1c470404d2002a +W 64 70080 18460404b4002b +W 64 70080 1c470404d1002b +W 64 70080 18460404b5002c +W 64 70080 1c470404d0002c +W 64 70080 18460404b5002d +W 64 70080 1c470404cf002d +W 64 70080 18460404b8002e +W 64 70080 1c470404cd002e +W 64 70080 18460404ba002f +W 64 70080 1c470404cb002f +W 64 70080 18460404c00030 +W 64 70080 1c470404c80030 +T +W 8 61c88 4 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000f +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c0001c +W 64 70080 1c470404c4001c +W 64 70080 18460404ba001d +W 64 70080 1c470404ca001d +W 64 70080 18460404b9001e +W 64 70080 1c470404cd001e +W 64 70080 18460404b7001f +W 64 70080 1c470404ce001f +W 64 70080 18460404b50020 +W 64 70080 1c470404cf0020 +W 64 70080 18460404b40021 +W 64 70080 1c470404d00021 +W 64 70080 18460404b30022 +W 64 70080 1c470404d10022 +W 64 70080 18460404b20023 +W 64 70080 1c470404d20023 +W 64 70080 18460404b20024 +W 64 70080 1c470404d20024 +W 64 70080 18460404b20025 +W 64 70080 1c470404d20025 +W 64 70080 18460404b20026 +W 64 70080 1c470404d20026 +W 64 70080 18460404b20027 +W 64 70080 1c470404d20027 +W 64 70080 18460404b20028 +W 64 70080 1c470404d20028 +W 64 70080 18460404b20029 +W 64 70080 1c470404d20029 +W 64 70080 18460404b4002a +W 64 70080 1c470404d1002a +W 64 70080 18460404b5002b +W 64 70080 1c470404d0002b +W 64 70080 18460404b5002c +W 64 70080 1c470404cf002c +W 64 70080 18460404b8002d +W 64 70080 1c470404cd002d +W 64 70080 18460404ba002e +W 64 70080 1c470404ca002e +W 64 70080 18460404bd002f +W 64 70080 1c470404c8002f +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404d30028 +W 64 70080 1c410404d30029 +W 64 70080 18400404d30026 +W 64 70080 1c410404d30028 +W 64 70080 18400404d30025 +W 64 70080 1c410404d20022 +W 64 70080 18400404d20022 +W 64 70080 1c410404d10021 +W 64 70080 18400404d10021 +W 64 70080 1c410404d00020 +W 64 70080 18400404d00020 +W 64 70080 1c410404ce001e +W 64 70080 18400404ce001e +W 64 70080 1c410404cb001d +W 64 70080 18400404c9001d +W 64 70080 1c410404cc001d +W 64 70080 18400404c8001d +W 64 70080 1c410404c5001c +W 64 70080 18400404c3001c +W 64 70080 1c410404c6001c +W 64 70080 18400404c1001c +W 64 70080 1c410404c3001c +W 64 70080 18400404c0001c +W 64 70080 1c410404be001d +W 64 70080 18400404bb001d +W 64 70080 1c410404bf001d +W 64 70080 18400404ba001d +W 64 70080 1c410404b7001f +W 64 70080 18400404b7001f +W 64 70080 1c410404b50020 +W 64 70080 18400404b50020 +W 64 70080 1c410404b40021 +W 64 70080 18400404b40021 +W 64 70080 1c410404b20023 +W 64 70080 18400404b20023 +W 64 70080 1c410404b20024 +W 64 70080 18400404b20024 +W 64 70080 1c410404b20027 +W 64 70080 18400404b20027 +W 64 70080 1c410404b20029 +W 64 70080 18400404b20029 +W 64 70080 1c410404b4002a +W 64 70080 18400404b4002a +W 64 70080 1c410404b5002c +W 64 70080 18400404b5002c +W 64 70080 1c410404b8002d +W 64 70080 18400404b8002d +W 64 70080 1c410404ba002e +W 64 70080 18400404ba002e +W 64 70080 1c410404bd002f +W 64 70080 18400404bd002f +W 64 70080 1c410404c00030 +W 64 70080 18400404c00030 +W 64 70080 1c410404c30030 +W 64 70080 18400404c30030 +W 64 70080 1c410404c60030 +W 64 70080 18400404c60030 +W 64 70080 1c410404c9002f +W 64 70080 18400404c9002f +W 64 70080 1c410404cb002e +W 64 70080 18400404cb002e +W 64 70080 1c410404ce002d +W 64 70080 18400404ce002d +W 64 70080 1c410404d0002c +W 64 70080 18400404d0002c +W 64 70080 1c410404d2002a +W 64 70080 18400404d2002a +W 64 70080 1c410404d30028 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404ca001f +W 64 70080 1c410404cc001f +W 64 70080 195a0455555555 +W 64 70080 18400404c90020 +W 64 70080 1c410404cd0020 +W 64 70080 195a0455555555 +W 64 70080 18400404c80021 +W 64 70080 1c410404ce0021 +W 64 70080 195a0455555555 +W 64 70080 18400404c70022 +W 64 70080 1c410404cb0022 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c0001d +W 64 70080 1c410404c5001d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba001e +W 64 70080 1c410404c5001e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b9001f +W 64 70080 1c410404c4001f +W 64 70080 195a0455555555 +W 64 70080 18400404b70020 +W 64 70080 1c410404c40020 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b90021 +W 64 70080 1c410404c00021 +W 64 70080 195a0455555555 +W 64 70080 18400404bb0022 +W 64 70080 1c410404bd0022 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bd0023 +W 64 70080 1c470404bd0023 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c00021 +W 64 70080 1c470404c00021 +W 64 70080 18460404be0022 +W 64 70080 1c470404bf0022 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c10021 +W 64 70080 1c470404c30021 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c5001e +W 64 70080 1c470404c5001e +W 64 70080 18460404c5001f +W 64 70080 1c470404c5001f +W 64 70080 18460404c40020 +W 64 70080 1c470404c40020 +W 64 70080 18460404c40021 +W 64 70080 1c470404c40021 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c4001d +W 64 70080 1c470404c6001d +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c1001d +W 64 70080 1c470404c3001d +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404be001d +W 64 70080 1c470404c0001d +W 64 70080 18460404bd001e +W 64 70080 1c470404bd001e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bb001e +W 64 70080 1c470404bc001e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404ba001e +W 64 70080 1c470404ba001e +W 64 70080 18460404b8001f +W 64 70080 1c470404b9001f +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b70020 +W 64 70080 1c470404b70020 +W 64 70080 18460404b80021 +W 64 70080 1c470404b90021 +W 64 70080 18460404ba0022 +W 64 70080 1c470404bb0022 +W 64 70080 18460404bc0023 +W 64 70080 1c470404bc0023 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c10022 +W 64 70080 1c410404c30022 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf0023 +W 64 70080 1c410404c50023 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0024 +W 64 70080 1c410404c60024 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd0025 +W 64 70080 1c410404c70025 +W 64 70080 195a0455555555 +W 64 70080 18400404bd0026 +W 64 70080 1c410404c70026 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf0027 +W 64 70080 1c410404c60027 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c70027 +W 64 70080 1c410404c80026 +W 64 70080 18400404c80026 +W 64 70080 1c410404c80027 +W 64 70080 18400404c80025 +W 64 70080 1c410404c60023 +W 64 70080 18400404c60023 +W 64 70080 1c410404c40022 +W 64 70080 18400404c20022 +W 64 70080 1c410404c50022 +W 64 70080 18400404c10022 +W 64 70080 1c410404bf0023 +W 64 70080 18400404bf0023 +W 64 70080 1c410404bd0025 +W 64 70080 18400404bd0025 +W 64 70080 1c410404bd0026 +W 64 70080 18400404bd0026 +W 64 70080 1c410404bf0027 +W 64 70080 18400404bf0027 +W 64 70080 1c410404c10028 +W 64 70080 18400404c10028 +W 64 70080 1c410404c50028 +W 64 70080 18400404c50028 +W 64 70080 1c410404c70027 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c10024 +W 64 70080 1c470404c30024 +W 64 70080 18460404c00025 +W 64 70080 1c470404c50025 +W 64 70080 18460404c00026 +W 64 70080 1c470404c50026 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c60026 +W 64 70080 1c410404c60027 +W 64 70080 18400404c60025 +W 64 70080 1c410404c40024 +W 64 70080 18400404c20024 +W 64 70080 1c410404c50024 +W 64 70080 18400404c10024 +W 64 70080 1c410404c00025 +W 64 70080 18400404c00025 +W 64 70080 1c410404c00026 +W 64 70080 18400404c00026 +W 64 70080 1c410404c10027 +W 64 70080 18400404c10027 +W 64 70080 1c410404c40027 +W 64 70080 18400404c40027 +W 64 70080 1c410404c60026 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400005000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ce0022 +W 64 70080 1c410404d00022 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404cb0023 +W 64 70080 1c410404d00023 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c80024 +W 64 70080 1c410404cc0024 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400003000 +W 64 70080 1940040000000d +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c90025 +W 64 70080 1c410404d10025 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000005 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404cc001f +W 64 70080 1c470404cc001f +W 64 70080 18460404cb0020 +W 64 70080 1c470404cb0020 +W 64 70080 18460404c90021 +W 64 70080 1c470404ca0021 +W 64 70080 18460404c80022 +W 64 70080 1c470404c80022 +W 64 70080 18460404c70023 +W 64 70080 1c470404c70023 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000c +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b30027 +W 64 70080 1c470404bc0027 +W 64 70080 18460404b40028 +W 64 70080 1c470404b80028 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c90026 +W 64 70080 1c470404d20026 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000d +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b30025 +W 64 70080 1c470404bb0025 +W 64 70080 18460404b30026 +W 64 70080 1c470404bb0026 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0028 +W 64 70080 1c410404be0028 +W 64 70080 195a0455555555 +W 64 70080 18400404bc0029 +W 64 70080 1c410404c50029 +W 64 70080 195a0455555555 +W 64 70080 18400404b9002a +W 64 70080 1c410404c7002a +W 64 70080 195a0455555555 +W 64 70080 18400404b6002b +W 64 70080 1c410404c8002b +W 64 70080 195a0455555555 +W 64 70080 18400404b9002c +W 64 70080 1c410404c9002c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb002d +W 64 70080 1c410404ca002d +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b6002b +W 64 70080 1c470404b7002b +W 64 70080 18460404b8002c +W 64 70080 1c470404b8002c +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b9002c +W 64 70080 1c470404b9002c +W 64 70080 18460404ba002d +W 64 70080 1c470404bb002d +W 64 70080 18460404bc002e +W 64 70080 1c470404bc002e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bd002e +W 64 70080 1c470404c0002e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c1002e +W 64 70080 1c470404c4002e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c5002e +W 64 70080 1c470404c6002e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c9002d +W 64 70080 1c470404ca002d +W 64 70080 18460404c7002e +W 64 70080 1c470404c8002e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c7002a +W 64 70080 1c470404c7002a +W 64 70080 18460404c8002b +W 64 70080 1c470404c9002b +W 64 70080 18460404ca002c +W 64 70080 1c470404ca002c +W 64 70080 18460404cb002d +W 64 70080 1c470404cb002d +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c40029 +W 64 70080 1c470404c60029 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c20029 +W 64 70080 1c470404c30029 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c00029 +W 64 70080 1c470404c10029 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bf0028 +W 64 70080 1c470404bf0028 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bd0028 +W 64 70080 1c470404be0028 +W 64 70080 18460404ba0029 +W 64 70080 1c470404bc0029 +W 64 70080 18460404b8002a +W 64 70080 1c470404b9002a +W 64 70080 18460404b7002b +W 64 70080 1c470404b7002b +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000a +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b80028 +W 64 70080 1c470404bc0028 +W 64 70080 18460404b40029 +W 64 70080 1c470404b80029 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000c +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c60029 +W 64 70080 1c470404c60029 +W 64 70080 18460404c7002a +W 64 70080 1c470404c7002a +W 64 70080 18460404c8002b +W 64 70080 1c470404c9002b +W 64 70080 18460404ca002c +W 64 70080 1c470404ca002c +W 64 70080 18460404cb002d +W 64 70080 1c470404cb002d +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000002 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404be0028 +W 64 70080 1c470404be0028 +W 64 70080 18460404bc0029 +W 64 70080 1c470404bd0029 +W 64 70080 18460404bb002a +W 64 70080 1c470404bb002a +W 64 70080 18460404b9002b +W 64 70080 1c470404ba002b +W 64 70080 18460404b8002c +W 64 70080 1c470404b8002c +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000004 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b40024 +W 64 70080 1c470404b70024 +W 64 70080 18460404b80025 +W 64 70080 1c470404bc0025 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404cd0020 +W 64 70080 1c470404cd0020 +W 64 70080 18460404cb0021 +W 64 70080 1c470404cc0021 +W 64 70080 18460404c90022 +W 64 70080 1c470404ca0022 +W 64 70080 18460404c80023 +W 64 70080 1c470404c80023 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404cd0021 +W 64 70080 1c470404ce0021 +W 64 70080 18460404ca0022 +W 64 70080 1c470404cc0022 +W 64 70080 18460404c80023 +W 64 70080 1c470404c90023 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000004 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bf0029 +W 64 70080 1c470404bf0029 +W 64 70080 18460404bd002a +W 64 70080 1c470404be002a +W 64 70080 18460404bb002b +W 64 70080 1c470404bc002b +W 64 70080 18460404ba002c +W 64 70080 1c470404ba002c +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000009 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404cd0024 +W 64 70080 1c470404d10024 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b40022 +W 64 70080 1c410404b70022 +W 64 70080 195a0455555555 +W 64 70080 18400404b40023 +W 64 70080 1c410404b90023 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b80024 +W 64 70080 1c410404bb0024 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000005 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404cb001e +W 64 70080 1c470404cb001e +W 64 70080 18460404ca001f +W 64 70080 1c470404ca001f +W 64 70080 18460404c90020 +W 64 70080 1c470404c90020 +W 64 70080 18460404c80021 +W 64 70080 1c470404c80021 +W 64 70080 18460404c70022 +W 64 70080 1c470404c70022 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000004 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bf0029 +W 64 70080 1c470404bf0029 +W 64 70080 18460404be002a +W 64 70080 1c470404be002a +W 64 70080 18460404bd002b +W 64 70080 1c470404bd002b +W 64 70080 18460404bd002c +W 64 70080 1c470404bd002c +W 64 70080 18460404bc002d +W 64 70080 1c470404bc002d +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c5001e +W 64 70080 1c410404c7001e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c5001f +W 64 70080 1c410404c7001f +W 64 70080 195a0455555555 +W 64 70080 18400404c50020 +W 64 70080 1c410404c60020 +W 64 70080 195a0455555555 +W 64 70080 18400404c40021 +W 64 70080 1c410404c60021 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c7001e +W 64 70080 1c470404c7001e +W 64 70080 18460404c8001f +W 64 70080 1c470404c9001f +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c3001e +W 64 70080 1c470404c6001e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404be001e +W 64 70080 1c470404c2001e +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bc001e +W 64 70080 1c470404bd001e +W 64 70080 18460404ba001f +W 64 70080 1c470404bb001f +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404d20028 +W 64 70080 1c470404d20028 +W 64 70080 18460404d10029 +W 64 70080 1c470404d10029 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404d0002a +W 64 70080 1c470404d0002a +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404ce002b +W 64 70080 1c470404cf002b +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404cc002c +W 64 70080 1c470404cd002c +W 64 70080 18460404cb002d +W 64 70080 1c470404cb002d +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404b60007 +W 64 70080 1c410404b70007 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b40008 +W 64 70080 1c410404b70008 +W 64 70080 195a0455555555 +W 64 70080 18400404b20009 +W 64 70080 1c410404b70009 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b0000a +W 64 70080 1c410404b7000a +W 64 70080 195a0455555555 +W 64 70080 18400404ae000b +W 64 70080 1c410404b7000b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac000c +W 64 70080 1c410404b7000c +W 64 70080 195a0455555555 +W 64 70080 18400404aa000d +W 64 70080 1c410404b7000d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a8000e +W 64 70080 1c410404b7000e +W 64 70080 195a0455555555 +W 64 70080 18400404a8000f +W 64 70080 1c410404b7000f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a80010 +W 64 70080 1c410404b70010 +W 64 70080 195a0455555555 +W 64 70080 18400404a80011 +W 64 70080 1c410404b70011 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a80012 +W 64 70080 1c410404b70012 +W 64 70080 195a0455555555 +W 64 70080 18400404a80013 +W 64 70080 1c410404b70013 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a80014 +W 64 70080 1c410404b50014 +W 64 70080 195a0455555555 +W 64 70080 18400404a80015 +W 64 70080 1c410404b30015 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a80016 +W 64 70080 1c410404b10016 +W 64 70080 195a0455555555 +W 64 70080 18400404a80017 +W 64 70080 1c410404af0017 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a80018 +W 64 70080 1c410404ad0018 +W 64 70080 195a0455555555 +W 64 70080 18400404a80019 +W 64 70080 1c410404ab0019 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a8001a +W 64 70080 1c410404a9001a +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b70009 +W 64 70080 1c470404b70009 +W 64 70080 18460404b7000a +W 64 70080 1c470404b7000a +W 64 70080 18460404b7000b +W 64 70080 1c470404b7000b +W 64 70080 18460404b7000c +W 64 70080 1c470404b7000c +W 64 70080 18460404b7000d +W 64 70080 1c470404b7000d +W 64 70080 18460404b7000e +W 64 70080 1c470404b7000e +W 64 70080 18460404b7000f +W 64 70080 1c470404b7000f +W 64 70080 18460404b70010 +W 64 70080 1c470404b70010 +W 64 70080 18460404b70011 +W 64 70080 1c470404b70011 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 194704000004ff +W 64 70080 19480403f703ff +W 64 70080 194f0400000011 +W 64 70080 18400404b70009 +W 64 70080 1c410404b80008 +W 64 70080 194f0400000000 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404b70009 +W 64 70080 1c410404b80008 +W 64 70080 194f0400000000 +W 64 70080 18400404b80008 +W 64 70080 1c410404b80009 +W 64 70080 18400404b80009 +W 64 70080 1c410404b80012 +W 64 70080 18400404b80012 +W 64 70080 1c410404b90012 +W 64 70080 18400404b7000a +W 64 70080 1c410404b70013 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b0000d +W 64 70080 1c470404b0000d +W 64 70080 18460404b0000e +W 64 70080 1c470404b1000e +W 64 70080 18460404b0000f +W 64 70080 1c470404b2000f +W 64 70080 18460404b00010 +W 64 70080 1c470404b20010 +W 64 70080 18460404b00011 +W 64 70080 1c470404b10011 +W 64 70080 18460404b00012 +W 64 70080 1c470404b10012 +W 64 70080 18460404b00013 +W 64 70080 1c470404b00013 +W 64 70080 18460404b00014 +W 64 70080 1c470404b00014 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b00012 +W 64 70080 1c410404b00013 +W 64 70080 18400404b00011 +W 64 70080 1c410404af000c +W 64 70080 18400404af000c +W 64 70080 1c410404b3000f +W 64 70080 18400404b3000f +W 64 70080 1c410404b00015 +W 64 70080 18400404b00013 +W 64 70080 1c410404b00016 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b1000f +W 64 70080 1c470404b1000f +W 64 70080 18460404af0010 +W 64 70080 1c470404b10010 +W 64 70080 18460404ad0011 +W 64 70080 1c470404af0011 +W 64 70080 18460404ac0012 +W 64 70080 1c470404ad0012 +W 64 70080 18460404aa0013 +W 64 70080 1c470404ab0013 +W 64 70080 18460404a80014 +W 64 70080 1c470404a90014 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b2000f +W 64 70080 1c410404b20011 +W 64 70080 18400404b2000e +W 64 70080 1c410404a80014 +W 64 70080 18400404a80014 +W 64 70080 1c410404a80015 +W 64 70080 18400404a80015 +W 64 70080 1c410404b20010 +W 64 70080 19150403ff0000 +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 184604049f0037 +W 64 70080 1c470404d90046 +W 64 70080 19100410400008 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400007000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a10039 +W 64 70080 1c470404aa0043 +W 64 70080 1c700407801cc0 +W 64 70080 1c70041cc00000 +W 64 70080 1c700430c06000 +W 64 70080 1c700460000000 +W 64 70080 1c70046000c000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c000c000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c1800000 +W 64 70080 1c700467000000 +W 64 70080 1c70043c000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ab0039 +W 64 70080 1c470404b50043 +W 64 70080 1c70041f800000 +W 64 70080 1c700418c01860 +W 64 70080 1c700418600000 +W 64 70080 1c700430603060 +W 64 70080 1c700430600000 +W 64 70080 1c7004306060c0 +W 64 70080 1c700460c00000 +W 64 70080 1c700460c06180 +W 64 70080 1c700461800000 +W 64 70080 1c7004c7000000 +W 64 70080 1c7004fc000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b60039 +W 64 70080 1c470404bf0043 +W 64 70080 1c70041f800000 +W 64 70080 1c700418c018c0 +W 64 70080 1c700418c00000 +W 64 70080 1c700430c03180 +W 64 70080 1c700431800000 +W 64 70080 1c70043f006180 +W 64 70080 1c700461800000 +W 64 70080 1c700461806180 +W 64 70080 1c700461800000 +W 64 70080 1c7004c3000000 +W 64 70080 1c7004c3000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c10039 +W 64 70080 1c470404cb0043 +W 64 70080 1c700407801cc0 +W 64 70080 1c70041cc00000 +W 64 70080 1c700430606060 +W 64 70080 1c700460600000 +W 64 70080 1c70046060c060 +W 64 70080 1c7004c0600000 +W 64 70080 1c7004c0c0c0c0 +W 64 70080 1c7004c0c00000 +W 64 70080 1c7004c1800000 +W 64 70080 1c700467000000 +W 64 70080 1c70043c000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404cc0039 +W 64 70080 1c470404d90043 +W 64 70080 1c7004180c181c +W 64 70080 1c7004181c0000 +W 64 70080 1c700418383c78 +W 64 70080 1c70043c780000 +W 64 70080 1c7004345834d0 +W 64 70080 1c700434d00000 +W 64 70080 1c700465b06730 +W 64 70080 1c700467300000 +W 64 70080 1c700467300000 +W 64 70080 1c7004c6600000 +W 64 70080 1c7004c6600000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c70077 +W 64 70080 1c470404c80077 +W 64 70080 18460404c60078 +W 64 70080 1c470404ca0078 +W 64 70080 18460404c50079 +W 64 70080 1c470404cb0079 +W 64 70080 18460404c4007a +W 64 70080 1c470404cc007a +W 64 70080 18460404c3007b +W 64 70080 1c470404cd007b +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c2007b +W 64 70080 1c410404c50079 +W 64 70080 18400404c50079 +W 64 70080 1c410404c70077 +W 64 70080 18400404c70077 +W 64 70080 1c410404c90077 +W 64 70080 18400404c90077 +W 64 70080 1c410404cb0078 +W 64 70080 18400404cb0078 +W 64 70080 1c410404cc0079 +W 64 70080 18400404cc0079 +W 64 70080 1c410404ce007b +W 64 70080 18400404ce007b +W 64 70080 1c410404c3007c +W 64 70080 18400404c3007c +W 64 70080 1c410404c3007d +W 64 70080 18400404c3007b +W 64 70080 1c410404c4007b +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b70074 +W 64 70080 1c470404bb0074 +W 64 70080 18460404b20075 +W 64 70080 1c470404bf0075 +W 64 70080 18460404ae0076 +W 64 70080 1c470404c10076 +W 64 70080 18460404ad0077 +W 64 70080 1c470404c30077 +W 64 70080 18460404ab0078 +W 64 70080 1c470404c50078 +W 64 70080 18460404ab0079 +W 64 70080 1c470404c60079 +W 64 70080 18460404aa007a +W 64 70080 1c470404c7007a +W 64 70080 18460404a9007b +W 64 70080 1c470404c7007b +W 64 70080 18460404a9007c +W 64 70080 1c470404c7007c +W 64 70080 18460404a9007d +W 64 70080 1c470404c7007d +W 64 70080 18460404aa007e +W 64 70080 1c470404c6007e +W 64 70080 18460404ab007f +W 64 70080 1c470404c5007f +W 64 70080 18460404ac0080 +W 64 70080 1c470404c30080 +W 64 70080 18460404af0081 +W 64 70080 1c470404c10081 +W 64 70080 18460404b10082 +W 64 70080 1c470404bf0082 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000f +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bd0061 +W 64 70080 1c470404bd0061 +W 64 70080 18460404bc0062 +W 64 70080 1c470404bd0062 +W 64 70080 18460404bb0063 +W 64 70080 1c470404bd0063 +W 64 70080 18460404ba0064 +W 64 70080 1c470404bd0064 +W 64 70080 18460404ba0065 +W 64 70080 1c470404bd0065 +W 64 70080 18460404ba0066 +W 64 70080 1c470404bd0066 +W 64 70080 18460404ba0067 +W 64 70080 1c470404bd0067 +W 64 70080 18460404ba0068 +W 64 70080 1c470404bd0068 +W 64 70080 18460404ba0069 +W 64 70080 1c470404bd0069 +W 64 70080 18460404ba006a +W 64 70080 1c470404bd006a +W 64 70080 18460404ba006b +W 64 70080 1c470404bd006b +W 64 70080 18460404ba006c +W 64 70080 1c470404bc006c +W 64 70080 18460404ba006d +W 64 70080 1c470404bc006d +W 64 70080 18460404ba006e +W 64 70080 1c470404bb006e +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ba0064 +W 64 70080 1c410404be005f +W 64 70080 18400404be005f +W 64 70080 1c410404be006a +W 64 70080 18400404be006a +W 64 70080 1c410404bb006f +W 64 70080 18400404bb006f +W 64 70080 1c410404ba006e +W 64 70080 18400404ba0065 +W 64 70080 1c410404ba006f +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b50064 +W 64 70080 1c470404bb0064 +W 64 70080 18460404b50065 +W 64 70080 1c470404bb0065 +W 64 70080 18460404b50066 +W 64 70080 1c470404ba0066 +W 64 70080 18460404b60067 +W 64 70080 1c470404ba0067 +W 64 70080 18460404b60068 +W 64 70080 1c470404ba0068 +W 64 70080 18460404b60069 +W 64 70080 1c470404ba0069 +W 64 70080 18460404b6006a +W 64 70080 1c470404ba006a +W 64 70080 18460404b6006b +W 64 70080 1c470404ba006b +W 64 70080 18460404b6006c +W 64 70080 1c470404ba006c +W 64 70080 18460404b6006d +W 64 70080 1c470404ba006d +W 64 70080 18460404b6006e +W 64 70080 1c470404ba006e +W 64 70080 18460404b6006f +W 64 70080 1c470404ba006f +W 64 70080 18460404b60070 +W 64 70080 1c470404ba0070 +W 64 70080 18460404b60071 +W 64 70080 1c470404ba0071 +W 64 70080 18460404b60072 +W 64 70080 1c470404ba0072 +W 64 70080 18460404b60073 +W 64 70080 1c470404ba0073 +W 64 70080 18460404b60074 +W 64 70080 1c470404ba0074 +W 64 70080 18460404b60075 +W 64 70080 1c470404ba0075 +W 64 70080 18460404b60076 +W 64 70080 1c470404ba0076 +W 64 70080 18460404b60077 +W 64 70080 1c470404ba0077 +W 64 70080 18460404b60078 +W 64 70080 1c470404ba0078 +W 64 70080 18460404b60079 +W 64 70080 1c470404ba0079 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b60067 +W 64 70080 1c410404b50066 +W 64 70080 18400404b50066 +W 64 70080 1c410404b50066 +W 64 70080 18400404b50066 +W 64 70080 1c410404b50067 +W 64 70080 18400404b50065 +W 64 70080 1c410404b50066 +W 64 70080 18400404b50064 +W 64 70080 1c410404b60064 +W 64 70080 18400404b60064 +W 64 70080 1c410404b70064 +W 64 70080 18400404b70064 +W 64 70080 1c410404b80064 +W 64 70080 18400404b80064 +W 64 70080 1c410404b90064 +W 64 70080 18400404b90064 +W 64 70080 1c410404ba0064 +W 64 70080 18400404ba0064 +W 64 70080 1c410404bc0064 +W 64 70080 18400404bc0064 +W 64 70080 1c410404bc0065 +W 64 70080 18400404bc0065 +W 64 70080 1c410404bc0066 +W 64 70080 18400404bc0066 +W 64 70080 1c410404bc0066 +W 64 70080 18400404bc0066 +W 64 70080 1c410404bd0066 +W 64 70080 18400404bb0066 +W 64 70080 1c410404bb007a +W 64 70080 18400404b7007a +W 64 70080 1c410404bc007a +W 64 70080 18400404b60068 +W 64 70080 1c410404b6007b +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000f +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404aa0065 +W 64 70080 1c470404b40065 +W 64 70080 18460404aa0066 +W 64 70080 1c470404b40066 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b50065 +W 64 70080 1c410404b50066 +W 64 70080 18400404b50066 +W 64 70080 1c410404aa0067 +W 64 70080 18400404aa0065 +W 64 70080 1c410404aa0068 +W 64 70080 18400404aa0064 +W 64 70080 1c410404b50065 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404af0060 +W 64 70080 1c410404b00060 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0061 +W 64 70080 1c410404b20061 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0062 +W 64 70080 1c410404b30062 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab0063 +W 64 70080 1c410404b40063 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0064 +W 64 70080 1c410404b40064 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b50065 +W 64 70080 1c410404aa0064 +W 64 70080 18400404aa0064 +W 64 70080 1c410404ab0063 +W 64 70080 18400404ab0063 +W 64 70080 1c410404ac0061 +W 64 70080 18400404ac0061 +W 64 70080 1c410404af0060 +W 64 70080 18400404af0060 +W 64 70080 1c410404b10060 +W 64 70080 18400404b10060 +W 64 70080 1c410404b40062 +W 64 70080 18400404b40062 +W 64 70080 1c410404b40062 +W 64 70080 18400404b40062 +W 64 70080 1c410404b60064 +W 64 70080 18400404b60064 +W 64 70080 1c410404b70064 +W 64 70080 18400404b50064 +W 64 70080 1c410404b50065 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000f +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b40060 +W 64 70080 1c470404b40060 +W 64 70080 18460404b40061 +W 64 70080 1c470404b50061 +W 64 70080 18460404b40062 +W 64 70080 1c470404b50062 +W 64 70080 18460404b50063 +W 64 70080 1c470404b60063 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b70064 +W 64 70080 1c410404b80064 +W 64 70080 18400404b60064 +W 64 70080 1c410404b40062 +W 64 70080 18400404b40060 +W 64 70080 1c410404b40063 +W 64 70080 18400404b4005f +W 64 70080 1c410404b70064 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000f +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c1005b +W 64 70080 1c470404c1005b +W 64 70080 18460404c1005c +W 64 70080 1c470404cd005c +W 64 70080 18460404c1005d +W 64 70080 1c470404cd005d +W 64 70080 18460404c1005e +W 64 70080 1c470404cd005e +W 64 70080 18460404c1005f +W 64 70080 1c470404cd005f +W 64 70080 18460404c10060 +W 64 70080 1c470404cd0060 +W 64 70080 18460404c10061 +W 64 70080 1c470404cd0061 +W 64 70080 18460404c10062 +W 64 70080 1c470404cd0062 +W 64 70080 18460404c10063 +W 64 70080 1c470404cd0063 +W 64 70080 18460404c10064 +W 64 70080 1c470404cd0064 +W 64 70080 18460404c10065 +W 64 70080 1c470404cd0065 +W 64 70080 18460404c10066 +W 64 70080 1c470404cd0066 +W 64 70080 18460404c10067 +W 64 70080 1c470404cd0067 +W 64 70080 18460404c10068 +W 64 70080 1c470404cd0068 +W 64 70080 18460404c10069 +W 64 70080 1c470404cd0069 +W 64 70080 18460404c1006a +W 64 70080 1c470404cd006a +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c3005c +W 64 70080 1c410404ce005b +W 64 70080 18400404ce005b +W 64 70080 1c410404ce006a +W 64 70080 18400404ce006a +W 64 70080 1c410404c3006b +W 64 70080 18400404c3006b +W 64 70080 1c410404c4006b +W 64 70080 18400404c2006b +W 64 70080 1c410404c1006a +W 64 70080 18400404c1005c +W 64 70080 1c410404c1006b +W 64 70080 18400404c1005b +W 64 70080 1c410404c2005b +W 64 70080 18400404c2005b +W 64 70080 1c410404c3005c +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c3005e +W 64 70080 1c410404ce005d +W 64 70080 18400404ce005d +W 64 70080 1c410404cf005d +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c30060 +W 64 70080 1c410404c90060 +W 64 70080 18400404c90060 +W 64 70080 1c410404ca0060 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c30062 +W 64 70080 1c410404c80062 +W 64 70080 18400404c80062 +W 64 70080 1c410404c90062 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404bb0066 +W 64 70080 1c410404bb0066 +W 64 70080 195a0455555555 +W 64 70080 18400404bc0067 +W 64 70080 1c410404c70067 +W 64 70080 195a0455555555 +W 64 70080 18400404bd0068 +W 64 70080 1c410404c70068 +W 64 70080 195a0455555555 +W 64 70080 18400404be0069 +W 64 70080 1c410404c70069 +W 64 70080 195a0455555555 +W 64 70080 18400404bf006a +W 64 70080 1c410404c7006a +W 64 70080 195a0455555555 +W 64 70080 18400404c0006b +W 64 70080 1c410404c7006b +W 64 70080 195a0455555555 +W 64 70080 18400404c1006c +W 64 70080 1c410404c7006c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c1006d +W 64 70080 1c410404c7006d +W 64 70080 195a0455555555 +W 64 70080 18400404c1006e +W 64 70080 1c410404c7006e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c1006f +W 64 70080 1c410404c7006f +W 64 70080 195a0455555555 +W 64 70080 18400404c10070 +W 64 70080 1c410404c70070 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c10071 +W 64 70080 1c410404c70071 +W 64 70080 195a0455555555 +W 64 70080 18400404c10072 +W 64 70080 1c410404c70072 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c10073 +W 64 70080 1c410404c70073 +W 64 70080 195a0455555555 +W 64 70080 18400404c10074 +W 64 70080 1c410404c70074 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c10075 +W 64 70080 1c410404c70075 +W 64 70080 195a0455555555 +W 64 70080 18400404c10076 +W 64 70080 1c410404c70076 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c10077 +W 64 70080 1c410404c60077 +W 64 70080 195a0455555555 +W 64 70080 18400404c10078 +W 64 70080 1c410404c50078 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c10079 +W 64 70080 1c410404c50079 +W 64 70080 195a0455555555 +W 64 70080 18400404c1007a +W 64 70080 1c410404c4007a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c1007b +W 64 70080 1c410404c2007b +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb0066 +W 64 70080 1c410404bc0066 +W 64 70080 18400404bc0066 +W 64 70080 1c410404bc0066 +W 64 70080 18400404bc0066 +W 64 70080 1c410404c70067 +W 64 70080 18400404c70067 +W 64 70080 1c410404c80067 +W 64 70080 18400404c80067 +W 64 70080 1c410404c80075 +W 64 70080 18400404c80075 +W 64 70080 1c410404c70077 +W 64 70080 18400404c70077 +W 64 70080 1c410404c60078 +W 64 70080 18400404c60078 +W 64 70080 1c410404c5007a +W 64 70080 18400404c5007a +W 64 70080 1c410404c3007b +W 64 70080 18400404c3007b +W 64 70080 1c410404c1007c +W 64 70080 18400404c1006d +W 64 70080 1c410404c1007d +W 64 70080 18400404c1006c +W 64 70080 1c410404bb0066 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000f +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bb0067 +W 64 70080 1c470404bb0067 +W 64 70080 18460404bb0068 +W 64 70080 1c470404bc0068 +W 64 70080 18460404bb0069 +W 64 70080 1c470404bd0069 +W 64 70080 18460404bb006a +W 64 70080 1c470404be006a +W 64 70080 18460404bb006b +W 64 70080 1c470404bf006b +W 64 70080 18460404bb006c +W 64 70080 1c470404c0006c +W 64 70080 18460404bb006d +W 64 70080 1c470404c0006d +W 64 70080 18460404bb006e +W 64 70080 1c470404c0006e +W 64 70080 18460404bb006f +W 64 70080 1c470404c0006f +W 64 70080 18460404bb0070 +W 64 70080 1c470404c00070 +W 64 70080 18460404bb0071 +W 64 70080 1c470404c00071 +W 64 70080 18460404bb0072 +W 64 70080 1c470404c00072 +W 64 70080 18460404bb0073 +W 64 70080 1c470404c00073 +W 64 70080 18460404bb0074 +W 64 70080 1c470404c00074 +W 64 70080 18460404bb0075 +W 64 70080 1c470404c00075 +W 64 70080 18460404bb0076 +W 64 70080 1c470404c00076 +W 64 70080 18460404bc0077 +W 64 70080 1c470404c00077 +W 64 70080 18460404bd0078 +W 64 70080 1c470404c00078 +W 64 70080 18460404be0079 +W 64 70080 1c470404c00079 +W 64 70080 18460404bf007a +W 64 70080 1c470404c0007a +W 64 70080 18460404c0007b +W 64 70080 1c470404c0007b +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb0066 +W 64 70080 1c410404c1006c +W 64 70080 18400404c1006c +W 64 70080 1c410404c1007c +W 64 70080 18400404c1007c +W 64 70080 1c410404bb0076 +W 64 70080 18400404bb0067 +W 64 70080 1c410404bb0077 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae0077 +W 64 70080 1c410404b00077 +W 64 70080 18400404ad0077 +W 64 70080 1c410404ae0077 +W 64 70080 18400404ac0077 +W 64 70080 1c410404ad0077 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404af007a +W 64 70080 1c410404ae0079 +W 64 70080 18400404ad0079 +W 64 70080 1c410404af0079 +W 64 70080 18400404ac0079 +W 64 70080 1c410404ad0079 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b30078 +W 64 70080 1c410404b40079 +W 64 70080 18400404b40079 +W 64 70080 1c410404b60079 +W 64 70080 18400404b60079 +W 64 70080 1c410404b90079 +W 64 70080 18400404b90079 +W 64 70080 1c410404bb0079 +W 64 70080 18400404bb0079 +W 64 70080 1c410404bc0079 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b3007a +W 64 70080 1c410404b4007b +W 64 70080 18400404b4007b +W 64 70080 1c410404b7007b +W 64 70080 18400404b7007b +W 64 70080 1c410404b8007b +W 64 70080 18400404b8007b +W 64 70080 1c410404b9007b +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c1006c +W 64 70080 1c410404c3006c +W 64 70080 18400404c3006c +W 64 70080 1c410404c4006b +W 64 70080 18400404c4006b +W 64 70080 1c410404c5006a +W 64 70080 18400404c5006a +W 64 70080 1c410404c6006a +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c1006f +W 64 70080 1c410404c3006e +W 64 70080 18400404c3006e +W 64 70080 1c410404c5006d +W 64 70080 18400404c5006d +W 64 70080 1c410404c7006c +W 64 70080 18400404c7006c +W 64 70080 1c410404c8006c +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c10076 +W 64 70080 1c410404c20075 +W 64 70080 18400404c20075 +W 64 70080 1c410404c40074 +W 64 70080 18400404c40074 +W 64 70080 1c410404c50074 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a90067 +W 64 70080 1c410404b50067 +W 64 70080 195a0455555555 +W 64 70080 18400404a90068 +W 64 70080 1c410404b40068 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a90069 +W 64 70080 1c410404b30069 +W 64 70080 195a0455555555 +W 64 70080 18400404a9006a +W 64 70080 1c410404b1006a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a9006b +W 64 70080 1c410404b0006b +W 64 70080 195a0455555555 +W 64 70080 18400404a9006c +W 64 70080 1c410404ae006c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a9006d +W 64 70080 1c410404ae006d +W 64 70080 195a0455555555 +W 64 70080 18400404a9006e +W 64 70080 1c410404ae006e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a9006f +W 64 70080 1c410404ae006f +W 64 70080 195a0455555555 +W 64 70080 18400404a90070 +W 64 70080 1c410404ae0070 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a90071 +W 64 70080 1c410404ae0071 +W 64 70080 195a0455555555 +W 64 70080 18400404a90072 +W 64 70080 1c410404ae0072 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a90073 +W 64 70080 1c410404ae0073 +W 64 70080 195a0455555555 +W 64 70080 18400404a90074 +W 64 70080 1c410404ae0074 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a90075 +W 64 70080 1c410404ae0075 +W 64 70080 195a0455555555 +W 64 70080 18400404a90076 +W 64 70080 1c410404ae0076 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a90077 +W 64 70080 1c410404ae0077 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0078 +W 64 70080 1c410404ae0078 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab0079 +W 64 70080 1c410404ae0079 +W 64 70080 195a0455555555 +W 64 70080 18400404ab007a +W 64 70080 1c410404ae007a +W 64 70080 195a0455555555 +W 64 70080 18400404ac007b +W 64 70080 1c410404ae007b +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b60067 +W 64 70080 1c410404af006c +W 64 70080 18400404af006c +W 64 70080 1c410404af007c +W 64 70080 18400404af007c +W 64 70080 1c410404ac007b +W 64 70080 18400404ac007b +W 64 70080 1c410404ab007a +W 64 70080 18400404ab007a +W 64 70080 1c410404aa0078 +W 64 70080 18400404aa0078 +W 64 70080 1c410404a90077 +W 64 70080 18400404a90076 +W 64 70080 1c410404a90078 +W 64 70080 18400404a90068 +W 64 70080 1c410404a90076 +W 64 70080 18400404a90067 +W 64 70080 1c410404aa0067 +W 64 70080 18400404aa0067 +W 64 70080 1c410404b50066 +W 64 70080 18400404b50066 +W 64 70080 1c410404b50066 +W 64 70080 18400404b50066 +W 64 70080 1c410404b60067 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae006c +W 64 70080 1c410404b0006c +W 64 70080 18400404ad006c +W 64 70080 1c410404ac006a +W 64 70080 18400404ac006a +W 64 70080 1c410404ad006a +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404af0071 +W 64 70080 1c410404ae0070 +W 64 70080 18400404ad0070 +W 64 70080 1c410404af0070 +W 64 70080 18400404ac0070 +W 64 70080 1c410404ad0070 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae0075 +W 64 70080 1c410404b00075 +W 64 70080 18400404ad0075 +W 64 70080 1c410404ae0075 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404af0073 +W 64 70080 1c410404ad0072 +W 64 70080 18400404ad0072 +W 64 70080 1c410404ab0071 +W 64 70080 18400404ab0071 +W 64 70080 1c410404ac0071 +W 64 70080 18400404aa0071 +W 64 70080 1c410404ab0071 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae006e +W 64 70080 1c410404b0006e +W 64 70080 18400404ad006e +W 64 70080 1c410404ac006d +W 64 70080 18400404ac006d +W 64 70080 1c410404ad006d +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000f +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b50068 +W 64 70080 1c470404b50068 +W 64 70080 18460404b40069 +W 64 70080 1c470404b50069 +W 64 70080 18460404b2006a +W 64 70080 1c470404b5006a +W 64 70080 18460404b1006b +W 64 70080 1c470404b5006b +W 64 70080 18460404af006c +W 64 70080 1c470404b5006c +W 64 70080 18460404af006d +W 64 70080 1c470404b5006d +W 64 70080 18460404af006e +W 64 70080 1c470404b5006e +W 64 70080 18460404af006f +W 64 70080 1c470404b5006f +W 64 70080 18460404af0070 +W 64 70080 1c470404b50070 +W 64 70080 18460404af0071 +W 64 70080 1c470404b50071 +W 64 70080 18460404af0072 +W 64 70080 1c470404b50072 +W 64 70080 18460404af0073 +W 64 70080 1c470404b50073 +W 64 70080 18460404af0074 +W 64 70080 1c470404b50074 +W 64 70080 18460404af0075 +W 64 70080 1c470404b50075 +W 64 70080 18460404af0076 +W 64 70080 1c470404b50076 +W 64 70080 18460404af0077 +W 64 70080 1c470404b40077 +W 64 70080 18460404af0078 +W 64 70080 1c470404b30078 +W 64 70080 18460404af0079 +W 64 70080 1c470404b20079 +W 64 70080 18460404af007a +W 64 70080 1c470404b1007a +W 64 70080 18460404af007b +W 64 70080 1c470404b0007b +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b60067 +W 64 70080 1c410404b60076 +W 64 70080 18400404b60076 +W 64 70080 1c410404af007c +W 64 70080 18400404af006d +W 64 70080 1c410404af007d +W 64 70080 18400404af006c +W 64 70080 1c410404b60067 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b70067 +W 64 70080 1c410404ba0067 +W 64 70080 195a0455555555 +W 64 70080 18400404b70068 +W 64 70080 1c410404bb0068 +W 64 70080 195a0455555555 +W 64 70080 18400404b60069 +W 64 70080 1c410404bc0069 +W 64 70080 195a0455555555 +W 64 70080 18400404b5006a +W 64 70080 1c410404bc006a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b5006b +W 64 70080 1c410404bd006b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b4006c +W 64 70080 1c410404be006c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3006d +W 64 70080 1c410404be006d +W 64 70080 195a0455555555 +W 64 70080 18400404b3006e +W 64 70080 1c410404be006e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3006f +W 64 70080 1c410404be006f +W 64 70080 195a0455555555 +W 64 70080 18400404b30070 +W 64 70080 1c410404be0070 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30071 +W 64 70080 1c410404be0071 +W 64 70080 195a0455555555 +W 64 70080 18400404b30072 +W 64 70080 1c410404be0072 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30073 +W 64 70080 1c410404be0073 +W 64 70080 195a0455555555 +W 64 70080 18400404b30074 +W 64 70080 1c410404be0074 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30075 +W 64 70080 1c410404be0075 +W 64 70080 195a0455555555 +W 64 70080 18400404b30076 +W 64 70080 1c410404be0076 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30077 +W 64 70080 1c410404be0077 +W 64 70080 195a0455555555 +W 64 70080 18400404b30078 +W 64 70080 1c410404be0078 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30079 +W 64 70080 1c410404be0079 +W 64 70080 195a0455555555 +W 64 70080 18400404b3007a +W 64 70080 1c410404be007a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3007b +W 64 70080 1c410404be007b +W 64 70080 195a0455555555 +W 64 70080 18400404b3007c +W 64 70080 1c410404be007c +W 64 70080 195a0455555555 +W 64 70080 18400404b6007d +W 64 70080 1c410404ba007d +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb0067 +W 64 70080 1c410404bf006d +W 64 70080 18400404bf006d +W 64 70080 1c410404bf007c +W 64 70080 18400404bf007c +W 64 70080 1c410404bd007d +W 64 70080 18400404bc007d +W 64 70080 1c410404be007d +W 64 70080 18400404bb007d +W 64 70080 1c410404b9007e +W 64 70080 18400404b9007e +W 64 70080 1c410404b6007d +W 64 70080 18400404b5007d +W 64 70080 1c410404b7007d +W 64 70080 18400404b4007d +W 64 70080 1c410404b3007c +W 64 70080 18400404b3006e +W 64 70080 1c410404b3007d +W 64 70080 18400404b3006d +W 64 70080 1c410404b70067 +W 64 70080 18400404b70067 +W 64 70080 1c410404b80067 +W 64 70080 18400404b80067 +W 64 70080 1c410404b90067 +W 64 70080 18400404b90067 +W 64 70080 1c410404bb0067 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b3006d +W 64 70080 1c410404b5006d +W 64 70080 18400404b5006d +W 64 70080 1c410404b7006e +W 64 70080 18400404b7006e +W 64 70080 1c410404b9006d +W 64 70080 18400404b9006d +W 64 70080 1c410404bb006d +W 64 70080 18400404bb006d +W 64 70080 1c410404bd006d +W 64 70080 18400404bd006d +W 64 70080 1c410404be006d +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b3006f +W 64 70080 1c410404b4006f +W 64 70080 18400404b4006f +W 64 70080 1c410404b70070 +W 64 70080 18400404b70070 +W 64 70080 1c410404b90070 +W 64 70080 18400404b90070 +W 64 70080 1c410404bc006f +W 64 70080 18400404bc006f +W 64 70080 1c410404bd006f +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b30071 +W 64 70080 1c410404b40071 +W 64 70080 18400404b40071 +W 64 70080 1c410404b60072 +W 64 70080 18400404b60072 +W 64 70080 1c410404b90072 +W 64 70080 18400404b90072 +W 64 70080 1c410404bc0071 +W 64 70080 18400404bc0071 +W 64 70080 1c410404bd0071 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b30073 +W 64 70080 1c410404b40074 +W 64 70080 18400404b40074 +W 64 70080 1c410404b60074 +W 64 70080 18400404b60074 +W 64 70080 1c410404b80074 +W 64 70080 18400404b80074 +W 64 70080 1c410404bb0074 +W 64 70080 18400404bb0074 +W 64 70080 1c410404bc0074 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b30076 +W 64 70080 1c410404b40076 +W 64 70080 18400404b40076 +W 64 70080 1c410404b60077 +W 64 70080 18400404b60077 +W 64 70080 1c410404b80077 +W 64 70080 18400404b80077 +W 64 70080 1c410404b90077 +W 64 70080 18400404b90077 +W 64 70080 1c410404bb0077 +W 64 70080 18400404bb0077 +W 64 70080 1c410404bd0076 +W 64 70080 18400404bd0076 +W 64 70080 1c410404be0076 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b6005e +W 64 70080 1c410404bb005e +W 64 70080 195a0455555555 +W 64 70080 18400404b4005f +W 64 70080 1c410404bd005f +W 64 70080 195a0455555555 +W 64 70080 18400404b50060 +W 64 70080 1c410404bd0060 +W 64 70080 195a0455555555 +W 64 70080 18400404b60061 +W 64 70080 1c410404bc0061 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b60062 +W 64 70080 1c410404bb0062 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b70063 +W 64 70080 1c410404ba0063 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b70064 +W 64 70080 1c410404b4005f +W 64 70080 18400404b4005f +W 64 70080 1c410404b6005e +W 64 70080 18400404b6005e +W 64 70080 1c410404b8005e +W 64 70080 18400404b8005e +W 64 70080 1c410404ba005e +W 64 70080 18400404ba005e +W 64 70080 1c410404bc005e +W 64 70080 18400404bc005e +W 64 70080 1c410404be005f +W 64 70080 18400404be005f +W 64 70080 1c410404ba0064 +W 64 70080 18400404ba0064 +W 64 70080 1c410404bb0064 +W 64 70080 18400404b90064 +W 64 70080 1c410404ba0064 +W 64 70080 18400404b80064 +W 64 70080 1c410404b90064 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c3005d +W 64 70080 1c410404c30068 +W 64 70080 18400404c3005c +W 64 70080 1c410404c4005c +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c70057 +W 64 70080 1c410404c70057 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c40058 +W 64 70080 1c410404c90058 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c30059 +W 64 70080 1c410404cb0059 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c2005a +W 64 70080 1c410404cc005a +W 64 70080 195a0455555555 +W 64 70080 18400404c2005b +W 64 70080 1c410404cd005b +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c1005b +W 64 70080 1c410404c40058 +W 64 70080 18400404c40058 +W 64 70080 1c410404c70057 +W 64 70080 18400404c70057 +W 64 70080 1c410404c80057 +W 64 70080 18400404c80057 +W 64 70080 1c410404ca0058 +W 64 70080 18400404ca0058 +W 64 70080 1c410404cc0059 +W 64 70080 18400404cc0059 +W 64 70080 1c410404ce005b +W 64 70080 18400404ce005b +W 64 70080 1c410404c3005c +W 64 70080 18400404c3005c +W 64 70080 1c410404c2005b +W 64 70080 18400404c2005b +W 64 70080 1c410404c3005b +W 64 70080 19150403ff0000 +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 184604049d0083 +W 64 70080 1c470404da0092 +W 64 70080 19100410400008 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400007000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049e0085 +W 64 70080 1c470404a8008f +W 64 70080 1c70041f800000 +W 64 70080 1c700418c01860 +W 64 70080 1c700418600000 +W 64 70080 1c700430603060 +W 64 70080 1c700430600000 +W 64 70080 1c7004306060c0 +W 64 70080 1c700460c00000 +W 64 70080 1c700460c06180 +W 64 70080 1c700461800000 +W 64 70080 1c7004c7000000 +W 64 70080 1c7004fc000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a90085 +W 64 70080 1c470404ad008f +W 64 70080 1c700418001800 +W 64 70080 1c700418000000 +W 64 70080 1c700400003000 +W 64 70080 1c700430000000 +W 64 70080 1c700430003000 +W 64 70080 1c700430000000 +W 64 70080 1c700460006000 +W 64 70080 1c700460000000 +W 64 70080 1c700460000000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c0000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ad0088 +W 64 70080 1c470404b4008f +W 64 70080 1c70041e000000 +W 64 70080 1c700433003000 +W 64 70080 1c700430000000 +W 64 70080 1c70041c000600 +W 64 70080 1c700406000000 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004cc000000 +W 64 70080 1c700478000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b50085 +W 64 70080 1c470404bc008f +W 64 70080 1c700418000000 +W 64 70080 1c700418003000 +W 64 70080 1c700430000000 +W 64 70080 1c700433003600 +W 64 70080 1c700436000000 +W 64 70080 1c70043c007800 +W 64 70080 1c700478000000 +W 64 70080 1c70046c006c00 +W 64 70080 1c70046c000000 +W 64 70080 1c7004cc000000 +W 64 70080 1c7004cc000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404be0085 +W 64 70080 1c470404c30092 +W 64 70080 1c70040c001800 +W 64 70080 1c700418000000 +W 64 70080 1c700430006000 +W 64 70080 1c700460000000 +W 64 70080 1c70046000c000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c000c000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c000c000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c0006000 +W 64 70080 1c700460000000 +W 64 70080 1c700460000000 +W 64 70080 1c700430000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c20085 +W 64 70080 1c470404c9008f +W 64 70080 1c70041e000000 +W 64 70080 1c700433006300 +W 64 70080 1c700463000000 +W 64 70080 1c700463004300 +W 64 70080 1c700443000000 +W 64 70080 1c7004c300c200 +W 64 70080 1c7004c2000000 +W 64 70080 1c7004c600c600 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004cc000000 +W 64 70080 1c700478000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ca008e +W 64 70080 1c470404cc0091 +W 64 70080 1c700460000000 +W 64 70080 1c700460000000 +W 64 70080 1c700420000000 +W 64 70080 1c7004c0000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ce0085 +W 64 70080 1c470404d5008f +W 64 70080 1c70043e000000 +W 64 70080 1c700463006300 +W 64 70080 1c700463000000 +W 64 70080 1c700403000700 +W 64 70080 1c700407000000 +W 64 70080 1c70040e003c00 +W 64 70080 1c70043c000000 +W 64 70080 1c70047000e000 +W 64 70080 1c7004e0000000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004fe000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404d50085 +W 64 70080 1c470404da0092 +W 64 70080 1c700430001800 +W 64 70080 1c700418000000 +W 64 70080 1c700418000c00 +W 64 70080 1c70040c000000 +W 64 70080 1c70040c000c00 +W 64 70080 1c70040c000000 +W 64 70080 1c70040c000c00 +W 64 70080 1c70040c000000 +W 64 70080 1c70040c001800 +W 64 70080 1c700418000000 +W 64 70080 1c700418003000 +W 64 70080 1c700430000000 +W 64 70080 1c700460000000 +W 64 70080 1c7004c0000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404af00be +W 64 70080 1c470404b200be +W 64 70080 18460404ad00bf +W 64 70080 1c470404b500bf +W 64 70080 18460404aa00c0 +W 64 70080 1c470404b700c0 +W 64 70080 18460404aa00c1 +W 64 70080 1c470404b700c1 +W 64 70080 18460404aa00c2 +W 64 70080 1c470404b700c2 +W 64 70080 18460404ac00c3 +W 64 70080 1c470404b600c3 +W 64 70080 18460404ae00c4 +W 64 70080 1c470404b400c4 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b800c1 +W 64 70080 1c410404b800c2 +W 64 70080 18400404b800c2 +W 64 70080 1c410404b800c3 +W 64 70080 18400404b800c0 +W 64 70080 1c410404b300be +W 64 70080 18400404b000be +W 64 70080 1c410404b400be +W 64 70080 18400404af00be +W 64 70080 1c410404aa00c0 +W 64 70080 18400404aa00c0 +W 64 70080 1c410404aa00c2 +W 64 70080 18400404aa00c2 +W 64 70080 1c410404aa00c2 +W 64 70080 18400404aa00c2 +W 64 70080 1c410404af00c5 +W 64 70080 18400404af00c5 +W 64 70080 1c410404b300c5 +W 64 70080 18400404b300c5 +W 64 70080 1c410404b800c2 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b500c2 +W 64 70080 1c470404b700c2 +W 64 70080 18460404b300c3 +W 64 70080 1c470404b900c3 +W 64 70080 18460404b100c4 +W 64 70080 1c470404ba00c4 +W 64 70080 18460404b400c5 +W 64 70080 1c470404bc00c5 +W 64 70080 18460404b700c6 +W 64 70080 1c470404be00c6 +W 64 70080 18460404b900c7 +W 64 70080 1c470404bf00c7 +W 64 70080 18460404bc00c8 +W 64 70080 1c470404c100c8 +W 64 70080 18460404bf00c9 +W 64 70080 1c470404c200c9 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404c300c9 +W 64 70080 1c410404b600c1 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404c300c9 +W 64 70080 1c410404b600c1 +W 64 70080 194f0400000000 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404b600c1 +W 64 70080 1c410404b100c4 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404b600c1 +W 64 70080 1c410404b100c4 +W 64 70080 194f0400000000 +W 64 70080 18400404b100c4 +W 64 70080 1c410404c100ca +W 64 70080 18400404c100ca +W 64 70080 1c410404c300c9 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 19400400000008 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b500c1 +W 64 70080 1c410404b900c1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b400c2 +W 64 70080 1c410404bb00c2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b300c3 +W 64 70080 1c410404bd00c3 +W 64 70080 195a0455555555 +W 64 70080 18400404b100c4 +W 64 70080 1c410404be00c4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b100c5 +W 64 70080 1c410404be00c5 +W 64 70080 195a0455555555 +W 64 70080 18400404b100c6 +W 64 70080 1c410404be00c6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b300c7 +W 64 70080 1c410404bc00c7 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bf00c5 +W 64 70080 1c410404bf00c7 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404bf00c4 +W 64 70080 1c410404ba00c1 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404bf00c4 +W 64 70080 1c410404ba00c1 +W 64 70080 194f0400000000 +W 64 70080 18400404b600c1 +W 64 70080 1c410404bb00c1 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404b500c1 +W 64 70080 1c410404b100c4 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404b500c1 +W 64 70080 1c410404b100c4 +W 64 70080 194f0400000000 +W 64 70080 18400404b100c4 +W 64 70080 1c410404b100c6 +W 64 70080 18400404b100c6 +W 64 70080 1c410404b500c8 +W 64 70080 18400404b500c8 +W 64 70080 1c410404ba00c8 +W 64 70080 18400404ba00c8 +W 64 70080 1c410404bf00c6 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404ca00c3 +W 64 70080 1c470404cb00c3 +W 64 70080 18460404cb00c4 +W 64 70080 1c470404cb00c4 +W 64 70080 18460404cc00c5 +W 64 70080 1c470404cc00c5 +W 64 70080 18460404cd00c6 +W 64 70080 1c470404cd00c6 +T +W 8 61c88 4 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c300c4 +W 64 70080 1c470404c400c4 +W 64 70080 18460404c400c5 +W 64 70080 1c470404c500c5 +W 64 70080 18460404c500c6 +W 64 70080 1c470404c500c6 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c700c2 +W 64 70080 1c470404c700c2 +W 64 70080 18460404c700c3 +W 64 70080 1c470404c800c3 +W 64 70080 18460404c600c4 +W 64 70080 1c470404c800c4 +W 64 70080 18460404c600c5 +W 64 70080 1c470404c700c5 +W 64 70080 18460404c600c6 +W 64 70080 1c470404c600c6 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c700c2 +W 64 70080 1c470404c800c2 +W 64 70080 18460404c800c3 +W 64 70080 1c470404c800c3 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c900c2 +W 64 70080 1c470404ca00c2 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c000c1 +W 64 70080 1c470404c300c1 +W 64 70080 18460404c100c2 +W 64 70080 1c470404c300c2 +W 64 70080 18460404c700c2 +W 64 70080 1c470404ca00c2 +W 64 70080 18460404c200c3 +W 64 70080 1c470404c400c3 +W 64 70080 18460404c700c3 +W 64 70080 1c470404c800c3 +W 64 70080 18460404ca00c3 +W 64 70080 1c470404cb00c3 +W 64 70080 18460404c300c4 +W 64 70080 1c470404c400c4 +W 64 70080 18460404c600c4 +W 64 70080 1c470404c800c4 +W 64 70080 18460404cb00c4 +W 64 70080 1c470404cc00c4 +W 64 70080 18460404c400c5 +W 64 70080 1c470404c700c5 +W 64 70080 18460404cc00c5 +W 64 70080 1c470404cd00c5 +W 64 70080 18460404c500c6 +W 64 70080 1c470404c600c6 +W 64 70080 18460404cd00c6 +W 64 70080 1c470404cd00c6 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c000c1 +W 64 70080 1c410404c300c0 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404c300c0 +W 64 70080 1c410404c500c4 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404c300c0 +W 64 70080 1c410404c500c4 +W 64 70080 194f0400000000 +W 64 70080 18400404c500c4 +W 64 70080 1c410404c600c5 +W 64 70080 18400404c600c5 +W 64 70080 1c410404c600c6 +W 64 70080 18400404c600c4 +W 64 70080 1c410404c700c2 +W 64 70080 18400404c700c2 +W 64 70080 1c410404c800c2 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404c800c2 +W 64 70080 1c410404c900c1 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404c800c2 +W 64 70080 1c410404c900c1 +W 64 70080 194f0400000000 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404c900c1 +W 64 70080 1c410404cb00c2 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404c900c1 +W 64 70080 1c410404cb00c2 +W 64 70080 194f0400000000 +W 64 70080 18400404cb00c2 +W 64 70080 1c410404cd00c4 +W 64 70080 18400404cd00c4 +W 64 70080 1c410404ce00c7 +W 64 70080 18400404ce00c7 +W 64 70080 1c410404ce00c8 +W 64 70080 18400404ce00c8 +W 64 70080 1c410404cc00c5 +W 64 70080 18400404cc00c5 +W 64 70080 1c410404ca00c3 +W 64 70080 18400404ca00c3 +W 64 70080 1c410404cb00c3 +W 64 70080 18400404c900c3 +W 64 70080 1c410404c900c4 +W 64 70080 18400404c900c4 +W 64 70080 1c410404c700c6 +W 64 70080 18400404c700c6 +W 64 70080 1c410404c700c7 +W 64 70080 18400404c700c7 +W 64 70080 1c410404c700c7 +W 64 70080 18400404c600c7 +W 64 70080 1c410404c800c7 +W 64 70080 18400404c500c7 +W 64 70080 1c410404c500c7 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404c500c7 +W 64 70080 1c410404c000c1 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404c500c7 +W 64 70080 1c410404c000c1 +W 64 70080 194f0400000000 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b600ac +W 64 70080 1c410404b700ac +W 64 70080 195a0455555555 +W 64 70080 18400404b400ad +W 64 70080 1c410404b800ad +W 64 70080 195a0455555555 +W 64 70080 18400404b300ae +W 64 70080 1c410404b800ae +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b100af +W 64 70080 1c410404b900af +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b200b0 +W 64 70080 1c410404b900b0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b300b1 +W 64 70080 1c410404ba00b1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b400b2 +W 64 70080 1c410404bb00b2 +W 64 70080 195a0455555555 +W 64 70080 18400404b400b3 +W 64 70080 1c410404bb00b3 +W 64 70080 195a0455555555 +W 64 70080 18400404b500b4 +W 64 70080 1c410404bc00b4 +W 64 70080 195a0455555555 +W 64 70080 18400404b600b5 +W 64 70080 1c410404bc00b5 +W 64 70080 195a0455555555 +W 64 70080 18400404b700b6 +W 64 70080 1c410404bd00b6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700b7 +W 64 70080 1c410404be00b7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b800b8 +W 64 70080 1c410404be00b8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b900b9 +W 64 70080 1c410404bf00b9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba00ba +W 64 70080 1c410404bf00ba +W 64 70080 195a0455555555 +W 64 70080 18400404ba00bb +W 64 70080 1c410404c000bb +W 64 70080 195a0455555555 +W 64 70080 18400404bb00bc +W 64 70080 1c410404c100bc +W 64 70080 195a0455555555 +W 64 70080 18400404bc00bd +W 64 70080 1c410404c100bd +W 64 70080 195a0455555555 +W 64 70080 18400404bd00be +W 64 70080 1c410404c200be +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd00bf +W 64 70080 1c410404c200bf +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be00c0 +W 64 70080 1c410404c200c0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf00c1 +W 64 70080 1c410404c100c1 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404b100af +W 64 70080 1c410404bf00c2 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404b100af +W 64 70080 1c410404bf00c2 +W 64 70080 194f0400000000 +W 64 70080 194704007204ff +W 64 70080 194804033e03f6 +W 64 70080 194f0400000011 +W 64 70080 18400404bf00c2 +W 64 70080 1c410404c200c1 +W 64 70080 194f0400000000 +W 64 70080 194704000004ff +W 64 70080 19480402dd033d +W 64 70080 194f0400000011 +W 64 70080 18400404bf00c2 +W 64 70080 1c410404c200c1 +W 64 70080 194f0400000000 +W 64 70080 18400404c200c1 +W 64 70080 1c410404c300bf +W 64 70080 18400404c300bf +W 64 70080 1c410404b700ab +W 64 70080 18400404b700ab +W 64 70080 1c410404b100af +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 1940040000000f +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404ad00a2 +W 64 70080 1c410404b400a2 +W 64 70080 195a0455555555 +W 64 70080 18400404ac00a3 +W 64 70080 1c410404b500a3 +W 64 70080 195a0455555555 +W 64 70080 18400404ab00a4 +W 64 70080 1c410404b600a4 +W 64 70080 195a0455555555 +W 64 70080 18400404aa00a5 +W 64 70080 1c410404b700a5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa00a6 +W 64 70080 1c410404b800a6 +W 64 70080 195a0455555555 +W 64 70080 18400404aa00a7 +W 64 70080 1c410404b900a7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa00a8 +W 64 70080 1c410404b900a8 +W 64 70080 195a0455555555 +W 64 70080 18400404aa00a9 +W 64 70080 1c410404b900a9 +W 64 70080 195a0455555555 +W 64 70080 18400404ab00aa +W 64 70080 1c410404b900aa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab00ab +W 64 70080 1c410404b900ab +W 64 70080 195a0455555555 +W 64 70080 18400404ab00ac +W 64 70080 1c410404b900ac +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab00ad +W 64 70080 1c410404b900ad +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac00ae +W 64 70080 1c410404b800ae +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad00af +W 64 70080 1c410404b700af +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae00b0 +W 64 70080 1c410404b600b0 +W 64 70080 195a0455555555 +W 64 70080 18400404ae00b1 +W 64 70080 1c410404b500b1 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae00b1 +W 64 70080 1c410404ab00ad +W 64 70080 18400404ab00ad +W 64 70080 1c410404aa00a9 +W 64 70080 18400404aa00a6 +W 64 70080 1c410404aa00aa +W 64 70080 18400404aa00a5 +W 64 70080 1c410404ad00a2 +W 64 70080 18400404ad00a2 +W 64 70080 1c410404b100a1 +W 64 70080 18400404b100a1 +W 64 70080 1c410404b500a2 +W 64 70080 18400404b500a2 +W 64 70080 1c410404b900a6 +W 64 70080 18400404b900a6 +W 64 70080 1c410404ba00aa +W 64 70080 18400404ba00aa +W 64 70080 1c410404b900ae +W 64 70080 18400404b900ae +W 64 70080 1c410404b600b1 +W 64 70080 18400404b600b1 +W 64 70080 1c410404b300b2 +W 64 70080 18400404b300b2 +W 64 70080 1c410404ae00b1 +W 64 70080 19150403ff0000 +W 64 70080 19100400000000 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000004 +W 64 70080 1c400404b500ae +W 64 70080 1c400404b400b0 +W 64 70080 1c400404b200ae +W 64 70080 1c400404b000b0 +W 64 70080 1c400404b000ad +W 64 70080 1c400404ae00ae +W 64 70080 1c400404ae00ac +W 64 70080 1c400404ae00aa +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c800ca +W 64 70080 1c470404cb00ca +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ce00cb +W 64 70080 1c410404ca00c9 +W 64 70080 18400404ca00c9 +W 64 70080 1c410404c800ca +W 64 70080 18400404c800ca +W 64 70080 1c410404cc00cb +W 64 70080 18400404cc00cb +W 64 70080 1c410404ce00cb +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c100ca +W 64 70080 1c470404c300ca +W 64 70080 18460404c400cb +W 64 70080 1c470404c500cb +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c700cc +W 64 70080 1c410404c200c9 +W 64 70080 18400404c200c9 +W 64 70080 1c410404c100ca +W 64 70080 18400404c100ca +W 64 70080 1c410404c600cc +W 64 70080 18400404c600cc +W 64 70080 1c410404c700cc +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c800ca +W 64 70080 1c470404c800ca +W 64 70080 18460404c600cb +W 64 70080 1c470404c700cb +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c900ca +W 64 70080 1c410404ca00ca +W 64 70080 18400404c800ca +W 64 70080 1c410404c600cb +W 64 70080 18400404c600cb +W 64 70080 1c410404c700cc +W 64 70080 18400404c700cc +W 64 70080 1c410404c900ca +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 19400400000008 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700b5 +W 64 70080 1c410404b800b5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b600b6 +W 64 70080 1c410404b900b6 +W 64 70080 195a0455555555 +W 64 70080 18400404b400b7 +W 64 70080 1c410404ba00b7 +W 64 70080 195a0455555555 +W 64 70080 18400404b500b8 +W 64 70080 1c410404ba00b8 +W 64 70080 195a0455555555 +W 64 70080 18400404b600b9 +W 64 70080 1c410404b800b9 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b800b4 +W 64 70080 1c410404bb00b8 +W 64 70080 18400404bb00b8 +W 64 70080 1c410404b600ba +W 64 70080 18400404b600ba +W 64 70080 1c410404b400b7 +W 64 70080 18400404b400b7 +W 64 70080 1c410404b800b4 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 19400400000008 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404b700ba +W 64 70080 1c410404b800ba +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700bb +W 64 70080 1c410404b800bb +W 64 70080 195a0455555555 +W 64 70080 18400404b700bc +W 64 70080 1c410404b800bc +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700bd +W 64 70080 1c410404b800bd +W 64 70080 195a0455555555 +W 64 70080 18400404b700be +W 64 70080 1c410404b800be +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700bf +W 64 70080 1c410404b800bf +W 64 70080 195a0455555555 +W 64 70080 18400404b700c0 +W 64 70080 1c410404b800c0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700c1 +W 64 70080 1c410404b800c1 +W 64 70080 195a0455555555 +W 64 70080 18400404b700c2 +W 64 70080 1c410404b800c2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700c3 +W 64 70080 1c410404b800c3 +W 64 70080 195a0455555555 +W 64 70080 18400404b700c4 +W 64 70080 1c410404b800c4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700c5 +W 64 70080 1c410404b700c5 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b700ba +W 64 70080 1c410404b900b9 +W 64 70080 18400404b900b9 +W 64 70080 1c410404b900c2 +W 64 70080 18400404b900c2 +W 64 70080 1c410404b900c4 +W 64 70080 18400404b900c4 +W 64 70080 1c410404b700c6 +W 64 70080 18400404b700bb +W 64 70080 1c410404b700c2 +W 64 70080 18400404b700c2 +W 64 70080 1c410404b700c7 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 19400400000008 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb00b3 +W 64 70080 1c410404bd00b3 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bc00b2 +W 64 70080 1c410404be00b3 +W 64 70080 18400404be00b3 +W 64 70080 1c410404bc00b4 +W 64 70080 18400404bc00b4 +W 64 70080 1c410404bb00b3 +W 64 70080 18400404bb00b3 +W 64 70080 1c410404bc00b2 +W 64 70080 19150403ff0000 +W 64 70080 19100400000000 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 196d0400090240 +W 64 70080 18450400000004 +W 64 70080 1c400404b400a3 +W 64 70080 1c400404af00a3 +W 64 70080 1c400404b100a5 +W 64 70080 1c400404b100a2 +W 64 70080 19150403ff0000 +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 184604049300cf +W 64 70080 1c470404e400de +W 64 70080 19100410400008 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400007000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049400d4 +W 64 70080 1c4704049f00db +W 64 70080 1c700436e00000 +W 64 70080 1c70043bb03330 +W 64 70080 1c700433300000 +W 64 70080 1c700466606660 +W 64 70080 1c700466600000 +W 64 70080 1c700466600000 +W 64 70080 1c7004ccc00000 +W 64 70080 1c7004ccc00000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a000d1 +W 64 70080 1c470404a400db +W 64 70080 1c700418001800 +W 64 70080 1c700418000000 +W 64 70080 1c700400003000 +W 64 70080 1c700430000000 +W 64 70080 1c700430003000 +W 64 70080 1c700430000000 +W 64 70080 1c700460006000 +W 64 70080 1c700460000000 +W 64 70080 1c700460000000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c0000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a400d4 +W 64 70080 1c470404ab00db +W 64 70080 1c70041e003300 +W 64 70080 1c700433000000 +W 64 70080 1c700463006000 +W 64 70080 1c700460000000 +W 64 70080 1c7004c000c600 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004cc007800 +W 64 70080 1c700478000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ac00d4 +W 64 70080 1c470404b200db +W 64 70080 1c700436000000 +W 64 70080 1c70043e003000 +W 64 70080 1c700430000000 +W 64 70080 1c700460006000 +W 64 70080 1c700460000000 +W 64 70080 1c700460000000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c0000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b200d4 +W 64 70080 1c470404b900db +W 64 70080 1c70041e000000 +W 64 70080 1c700433006300 +W 64 70080 1c700463000000 +W 64 70080 1c70046300c600 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004cc000000 +W 64 70080 1c700478000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b900d4 +W 64 70080 1c470404c200de +W 64 70080 1c70041b800000 +W 64 70080 1c70041cc038c0 +W 64 70080 1c700438c00000 +W 64 70080 1c700430c03180 +W 64 70080 1c700431800000 +W 64 70080 1c700431807b00 +W 64 70080 1c70047b000000 +W 64 70080 1c70046e006000 +W 64 70080 1c700460000000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c0000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c300d1 +W 64 70080 1c470404cb00db +W 64 70080 1c700418000000 +W 64 70080 1c700418003000 +W 64 70080 1c700430000000 +W 64 70080 1c700437003b80 +W 64 70080 1c70043b800000 +W 64 70080 1c700431806300 +W 64 70080 1c700463000000 +W 64 70080 1c700463006300 +W 64 70080 1c700463000000 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004c6000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404cc00d4 +W 64 70080 1c470404d300db +W 64 70080 1c70041e000000 +W 64 70080 1c700433006300 +W 64 70080 1c700463000000 +W 64 70080 1c70046300c600 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004cc000000 +W 64 70080 1c700478000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404d400d4 +W 64 70080 1c470404dc00db +W 64 70080 1c700437000000 +W 64 70080 1c700439803180 +W 64 70080 1c700431800000 +W 64 70080 1c700463006300 +W 64 70080 1c700463000000 +W 64 70080 1c700463000000 +W 64 70080 1c7004c6000000 +W 64 70080 1c7004c6000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404dd00d4 +W 64 70080 1c470404e400db +W 64 70080 1c70041e000000 +W 64 70080 1c700433006300 +W 64 70080 1c700463000000 +W 64 70080 1c70047e00c000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004c0000000 +W 64 70080 1c7004cc000000 +W 64 70080 1c700478000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c30109 +W 64 70080 1c470404c60109 +W 64 70080 18460404c1010a +W 64 70080 1c470404c8010a +W 64 70080 18460404bf010b +W 64 70080 1c470404ca010b +W 64 70080 18460404bd010c +W 64 70080 1c470404cc010c +W 64 70080 18460404bb010d +W 64 70080 1c470404ce010d +W 64 70080 18460404b9010e +W 64 70080 1c470404d0010e +W 64 70080 18460404b7010f +W 64 70080 1c470404d1010f +W 64 70080 18460404b50110 +W 64 70080 1c470404cf0110 +W 64 70080 18460404b30111 +W 64 70080 1c470404cd0111 +W 64 70080 18460404b10112 +W 64 70080 1c470404cb0112 +W 64 70080 18460404af0113 +W 64 70080 1c470404c90113 +W 64 70080 18460404ac0114 +W 64 70080 1c470404c70114 +W 64 70080 18460404ae0115 +W 64 70080 1c470404c50115 +W 64 70080 18460404b00116 +W 64 70080 1c470404c30116 +W 64 70080 18460404b20117 +W 64 70080 1c470404c10117 +W 64 70080 18460404b40118 +W 64 70080 1c470404bf0118 +W 64 70080 18460404b60119 +W 64 70080 1c470404bd0119 +W 64 70080 18460404b8011a +W 64 70080 1c470404bb011a +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404d1010a +W 64 70080 1c410404d1010a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404cf010b +W 64 70080 1c410404d1010b +W 64 70080 195a0455555555 +W 64 70080 18400404cd010c +W 64 70080 1c410404d0010c +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404d1010a +W 64 70080 1c410404d2010a +W 64 70080 18400404d2010a +W 64 70080 1c410404d2010b +W 64 70080 18400404d2010b +W 64 70080 1c410404d2010c +W 64 70080 18400404d2010c +W 64 70080 1c410404d3010c +W 64 70080 18400404d1010c +W 64 70080 1c410404d1010d +W 64 70080 18400404d1010d +W 64 70080 1c410404d2010d +W 64 70080 18400404d0010d +W 64 70080 1c410404d1010d +W 64 70080 18400404cf010d +W 64 70080 1c410404cf010d +W 64 70080 18400404cf010d +W 64 70080 1c410404d0010d +W 64 70080 18400404ce010d +W 64 70080 1c410404cd010c +W 64 70080 18400404cd010c +W 64 70080 1c410404cd010c +W 64 70080 18400404cd010c +W 64 70080 1c410404d1010a +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b70115 +W 64 70080 1c410404bb0115 +W 64 70080 195a0455555555 +W 64 70080 18400404b70116 +W 64 70080 1c410404bb0116 +W 64 70080 195a0455555555 +W 64 70080 18400404b80117 +W 64 70080 1c410404bb0117 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bc0114 +W 64 70080 1c410404bc0115 +W 64 70080 18400404bc0115 +W 64 70080 1c410404bc0116 +W 64 70080 18400404bc0116 +W 64 70080 1c410404bc0117 +W 64 70080 18400404bc0117 +W 64 70080 1c410404bb0118 +W 64 70080 18400404bb0118 +W 64 70080 1c410404bb0118 +W 64 70080 18400404bb0118 +W 64 70080 1c410404bc0118 +W 64 70080 18400404ba0118 +W 64 70080 1c410404bb0118 +W 64 70080 18400404b90118 +W 64 70080 1c410404b90118 +W 64 70080 18400404b90118 +W 64 70080 1c410404ba0118 +W 64 70080 18400404b80118 +W 64 70080 1c410404b80119 +W 64 70080 18400404b80117 +W 64 70080 1c410404b90117 +W 64 70080 18400404b70117 +W 64 70080 1c410404b70118 +W 64 70080 18400404b70116 +W 64 70080 1c410404b70117 +W 64 70080 18400404b70115 +W 64 70080 1c410404b70115 +W 64 70080 18400404b70115 +W 64 70080 1c410404bc0114 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404ad0110 +W 64 70080 1c410404ae0110 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0111 +W 64 70080 1c410404af0111 +W 64 70080 195a0455555555 +W 64 70080 18400404ad0112 +W 64 70080 1c410404b00112 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b10112 +W 64 70080 1c410404b10112 +W 64 70080 18400404b10112 +W 64 70080 1c410404b00113 +W 64 70080 18400404b00113 +W 64 70080 1c410404b10113 +W 64 70080 18400404af0113 +W 64 70080 1c410404b00113 +W 64 70080 18400404ae0113 +W 64 70080 1c410404ad0112 +W 64 70080 18400404ad0112 +W 64 70080 1c410404ad0112 +W 64 70080 18400404ad0112 +W 64 70080 1c410404ac0111 +W 64 70080 18400404ac0111 +W 64 70080 1c410404ad0110 +W 64 70080 18400404ad0110 +W 64 70080 1c410404ad0111 +W 64 70080 18400404ad010f +W 64 70080 1c410404b10112 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c300f3 +W 64 70080 1c410404c600f3 +W 64 70080 195a0455555555 +W 64 70080 18400404c100f4 +W 64 70080 1c410404c700f4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf00f5 +W 64 70080 1c410404c800f5 +W 64 70080 195a0455555555 +W 64 70080 18400404bd00f6 +W 64 70080 1c410404c900f6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb00f7 +W 64 70080 1c410404ca00f7 +W 64 70080 195a0455555555 +W 64 70080 18400404b900f8 +W 64 70080 1c410404cc00f8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b700f9 +W 64 70080 1c410404cd00f9 +W 64 70080 195a0455555555 +W 64 70080 18400404b500fa +W 64 70080 1c410404ce00fa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b300fb +W 64 70080 1c410404cf00fb +W 64 70080 195a0455555555 +W 64 70080 18400404b100fc +W 64 70080 1c410404d000fc +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404af00fd +W 64 70080 1c410404d100fd +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac00fe +W 64 70080 1c410404d100fe +W 64 70080 195a0455555555 +W 64 70080 18400404ac00ff +W 64 70080 1c410404d100ff +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0100 +W 64 70080 1c410404d10100 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0101 +W 64 70080 1c410404d10101 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0102 +W 64 70080 1c410404d10102 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0103 +W 64 70080 1c410404d10103 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0104 +W 64 70080 1c410404d10104 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0105 +W 64 70080 1c410404d10105 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0106 +W 64 70080 1c410404d10106 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0107 +W 64 70080 1c410404d10107 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0108 +W 64 70080 1c410404d10108 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0109 +W 64 70080 1c410404d10109 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac010a +W 64 70080 1c410404d1010a +W 64 70080 195a0455555555 +W 64 70080 18400404ac010b +W 64 70080 1c410404cf010b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac010c +W 64 70080 1c410404cd010c +W 64 70080 195a0455555555 +W 64 70080 18400404ac010d +W 64 70080 1c410404cb010d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac010e +W 64 70080 1c410404c9010e +W 64 70080 195a0455555555 +W 64 70080 18400404ac010f +W 64 70080 1c410404c7010f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae0110 +W 64 70080 1c410404c50110 +W 64 70080 195a0455555555 +W 64 70080 18400404b00111 +W 64 70080 1c410404c30111 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b20112 +W 64 70080 1c410404c10112 +W 64 70080 195a0455555555 +W 64 70080 18400404b40113 +W 64 70080 1c410404bf0113 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b60114 +W 64 70080 1c410404bd0114 +W 64 70080 195a0455555555 +W 64 70080 18400404b80115 +W 64 70080 1c410404bb0115 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b90111 +W 64 70080 1c410404b90116 +W 64 70080 18400404b90116 +W 64 70080 1c410404ac010f +W 64 70080 18400404ac010c +W 64 70080 1c410404ac0110 +W 64 70080 18400404ac0108 +W 64 70080 1c410404ac010c +W 64 70080 18400404ac00ff +W 64 70080 1c410404ac0108 +W 64 70080 18400404ac00fe +W 64 70080 1c410404b90109 +W 64 70080 18400404b90109 +W 64 70080 1c410404b9010d +W 64 70080 18400404b9010d +W 64 70080 1c410404c0010a +W 64 70080 18400404c0010a +W 64 70080 1c410404c1010a +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b90109 +W 64 70080 1c410404d200fd +W 64 70080 18400404d200fd +W 64 70080 1c410404d2010a +W 64 70080 18400404d2010a +W 64 70080 1c410404b90116 +W 64 70080 18400404b90116 +W 64 70080 1c410404ba0116 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404d20105 +W 64 70080 1c410404c90109 +W 64 70080 18400404c90109 +W 64 70080 1c410404ca0109 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404af0108 +W 64 70080 1c410404b20108 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404af0109 +W 64 70080 1c410404b40109 +W 64 70080 195a0455555555 +W 64 70080 18400404af010a +W 64 70080 1c410404b2010a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404af010b +W 64 70080 1c410404b2010b +W 64 70080 195a0455555555 +W 64 70080 18400404af010c +W 64 70080 1c410404b2010c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b1010d +W 64 70080 1c410404b4010d +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b5010d +W 64 70080 1c410404b3010e +W 64 70080 18400404b3010e +W 64 70080 1c410404af010c +W 64 70080 18400404af0109 +W 64 70080 1c410404af010d +W 64 70080 18400404af0108 +W 64 70080 1c410404b10107 +W 64 70080 18400404b10107 +W 64 70080 1c410404b50109 +W 64 70080 18400404b50109 +W 64 70080 1c410404b3010a +W 64 70080 18400404b3010a +W 64 70080 1c410404b3010c +W 64 70080 18400404b3010c +W 64 70080 1c410404b5010d +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b3010b +W 64 70080 1c410404b3010d +W 64 70080 18400404b3010d +W 64 70080 1c410404b4010d +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404af0107 +W 64 70080 1c410404b30109 +W 64 70080 18400404b30109 +W 64 70080 1c410404b40109 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000e +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bc00f9 +W 64 70080 1c470404be00f9 +W 64 70080 18460404bb00fa +W 64 70080 1c470404c000fa +W 64 70080 18460404ba00fb +W 64 70080 1c470404c200fb +W 64 70080 18460404ba00fc +W 64 70080 1c470404c500fc +W 64 70080 18460404b900fd +W 64 70080 1c470404c700fd +W 64 70080 18460404b800fe +W 64 70080 1c470404cd00fe +W 64 70080 18460404b500ff +W 64 70080 1c470404cc00ff +W 64 70080 18460404b50100 +W 64 70080 1c470404ca0100 +W 64 70080 18460404b50101 +W 64 70080 1c470404c80101 +W 64 70080 18460404b50102 +W 64 70080 1c470404c60102 +W 64 70080 18460404b50103 +W 64 70080 1c470404c40103 +W 64 70080 18460404b40104 +W 64 70080 1c470404c20104 +W 64 70080 18460404b50105 +W 64 70080 1c470404c00105 +W 64 70080 18460404b60106 +W 64 70080 1c470404be0106 +W 64 70080 18460404b70107 +W 64 70080 1c470404bc0107 +W 64 70080 18460404b80108 +W 64 70080 1c470404ba0108 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b90109 +W 64 70080 1c410404b40104 +W 64 70080 18400404b40104 +W 64 70080 1c410404b500ff +W 64 70080 18400404b500ff +W 64 70080 1c410404b800fe +W 64 70080 18400404b800fe +W 64 70080 1c410404bc00f8 +W 64 70080 18400404bc00f8 +W 64 70080 1c410404ca00fe +W 64 70080 18400404ca00fe +W 64 70080 1c410404ce00fe +W 64 70080 18400404ce00fe +W 64 70080 1c410404b90109 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c300f3 +W 64 70080 1c410404c500f3 +W 64 70080 195a0455555555 +W 64 70080 18400404c100f4 +W 64 70080 1c410404c600f4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf00f5 +W 64 70080 1c410404c700f5 +W 64 70080 195a0455555555 +W 64 70080 18400404bd00f6 +W 64 70080 1c410404c800f6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb00f7 +W 64 70080 1c410404c900f7 +W 64 70080 195a0455555555 +W 64 70080 18400404b900f8 +W 64 70080 1c410404ca00f8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb00f9 +W 64 70080 1c410404cc00f9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc00fa +W 64 70080 1c410404cd00fa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd00fb +W 64 70080 1c410404ce00fb +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be00fc +W 64 70080 1c410404cf00fc +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf00fd +W 64 70080 1c410404d000fd +W 64 70080 195a0455555555 +W 64 70080 18400404c100fe +W 64 70080 1c410404d100fe +W 64 70080 195a0455555555 +W 64 70080 18400404c200ff +W 64 70080 1c410404d100ff +W 64 70080 195a0455555555 +W 64 70080 18400404c30100 +W 64 70080 1c410404cf0100 +W 64 70080 195a0455555555 +W 64 70080 18400404c40101 +W 64 70080 1c410404cd0101 +W 64 70080 195a0455555555 +W 64 70080 18400404c50102 +W 64 70080 1c410404cb0102 +W 64 70080 195a0455555555 +W 64 70080 18400404c60103 +W 64 70080 1c410404c90103 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c60104 +W 64 70080 1c410404c70104 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b900f8 +W 64 70080 1c410404c400f2 +W 64 70080 18400404c400f2 +W 64 70080 1c410404d200fe +W 64 70080 18400404d200fe +W 64 70080 1c410404d200ff +W 64 70080 18400404d200ff +W 64 70080 1c410404c60105 +W 64 70080 18400404c60104 +W 64 70080 1c410404c60106 +W 64 70080 18400404c60103 +W 64 70080 1c410404b900f8 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b200ea +W 64 70080 1c410404b300ea +W 64 70080 195a0455555555 +W 64 70080 18400404b000eb +W 64 70080 1c410404b200eb +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae00ec +W 64 70080 1c410404b100ec +W 64 70080 195a0455555555 +W 64 70080 18400404ac00ed +W 64 70080 1c410404b200ed +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa00ee +W 64 70080 1c410404b300ee +W 64 70080 195a0455555555 +W 64 70080 18400404a800ef +W 64 70080 1c410404b300ef +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a800f0 +W 64 70080 1c410404b400f0 +W 64 70080 195a0455555555 +W 64 70080 18400404a800f1 +W 64 70080 1c410404b400f1 +W 64 70080 195a0455555555 +W 64 70080 18400404a700f2 +W 64 70080 1c410404b500f2 +W 64 70080 195a0455555555 +W 64 70080 18400404a800f3 +W 64 70080 1c410404b600f3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a800f4 +W 64 70080 1c410404b600f4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404a900f5 +W 64 70080 1c410404b700f5 +W 64 70080 195a0455555555 +W 64 70080 18400404a900f6 +W 64 70080 1c410404b700f6 +W 64 70080 195a0455555555 +W 64 70080 18400404aa00f7 +W 64 70080 1c410404b800f7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa00f8 +W 64 70080 1c410404b800f8 +W 64 70080 195a0455555555 +W 64 70080 18400404aa00f9 +W 64 70080 1c410404b600f9 +W 64 70080 195a0455555555 +W 64 70080 18400404ab00fa +W 64 70080 1c410404b400fa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab00fb +W 64 70080 1c410404b200fb +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac00fc +W 64 70080 1c410404b000fc +W 64 70080 195a0455555555 +W 64 70080 18400404ac00fd +W 64 70080 1c410404ae00fd +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ac00fe +W 64 70080 1c410404a700f2 +W 64 70080 18400404a700f2 +W 64 70080 1c410404a800ef +W 64 70080 18400404a800ef +W 64 70080 1c410404b400e9 +W 64 70080 18400404b400e9 +W 64 70080 1c410404b200ec +W 64 70080 18400404b200ec +W 64 70080 1c410404b900f8 +W 64 70080 18400404b900f8 +W 64 70080 1c410404ac00fe +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404a700f2 +W 64 70080 1c410404b200ec +W 64 70080 18400404b200ec +W 64 70080 1c410404b300ec +W 64 70080 19150403ff0000 +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 184604049e011b +W 64 70080 1c470404da0122 +W 64 70080 184604049e0123 +W 64 70080 1c470404da012a +W 64 70080 19100410400008 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040001c000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049f011d +W 64 70080 1c470404a70127 +W 64 70080 1c700400800080 +W 64 70080 1c700400800000 +W 64 70080 1c700401001d00 +W 64 70080 1c70041d000000 +W 64 70080 1c700425004200 +W 64 70080 1c700442000000 +W 64 70080 1c700442008200 +W 64 70080 1c700482000000 +W 64 70080 1c700484000000 +W 64 70080 1c70048c000000 +W 64 70080 1c700474000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a70120 +W 64 70080 1c470404ad0127 +W 64 70080 1c700422000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c700444004400 +W 64 70080 1c700444000000 +W 64 70080 1c700484000000 +W 64 70080 1c700498000000 +W 64 70080 1c700468000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404af0120 +W 64 70080 1c470404b90127 +W 64 70080 1c70042cc00000 +W 64 70080 1c700433204420 +W 64 70080 1c700444200000 +W 64 70080 1c700444404440 +W 64 70080 1c700444400000 +W 64 70080 1c700488800000 +W 64 70080 1c700488800000 +W 64 70080 1c700488800000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b90120 +W 64 70080 1c470404c0012a +W 64 70080 1c700416000000 +W 64 70080 1c700419002100 +W 64 70080 1c700421000000 +W 64 70080 1c700421002100 +W 64 70080 1c700421000000 +W 64 70080 1c700442006200 +W 64 70080 1c700462000000 +W 64 70080 1c70045c004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c20120 +W 64 70080 1c470404c80127 +W 64 70080 1c70041c000000 +W 64 70080 1c700422004000 +W 64 70080 1c700440000000 +W 64 70080 1c700438000400 +W 64 70080 1c700404000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c9011e +W 64 70080 1c470404cd0127 +W 64 70080 1c700410000000 +W 64 70080 1c700410007800 +W 64 70080 1c700478000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480008000 +W 64 70080 1c700480000000 +W 64 70080 1c700460000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404cd0120 +W 64 70080 1c470404d30127 +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c70047e008000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c70048c000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404d50120 +W 64 70080 1c470404da0127 +W 64 70080 1c70042c000000 +W 64 70080 1c700430004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c30157 +W 64 70080 1c470404c60157 +W 64 70080 18460404c10158 +W 64 70080 1c470404c80158 +W 64 70080 18460404bf0159 +W 64 70080 1c470404ca0159 +W 64 70080 18460404bd015a +W 64 70080 1c470404c8015a +W 64 70080 18460404bb015b +W 64 70080 1c470404c6015b +W 64 70080 18460404b9015c +W 64 70080 1c470404c4015c +W 64 70080 18460404b7015d +W 64 70080 1c470404c2015d +W 64 70080 18460404b5015e +W 64 70080 1c470404c0015e +W 64 70080 18460404b3015f +W 64 70080 1c470404be015f +W 64 70080 18460404b10160 +W 64 70080 1c470404bc0160 +W 64 70080 18460404af0161 +W 64 70080 1c470404ba0161 +W 64 70080 18460404ad0162 +W 64 70080 1c470404b80162 +W 64 70080 18460404ab0163 +W 64 70080 1c470404b60163 +W 64 70080 18460404a90164 +W 64 70080 1c470404b40164 +W 64 70080 18460404ab0165 +W 64 70080 1c470404b20165 +W 64 70080 18460404ad0166 +W 64 70080 1c470404b00166 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c4013a +W 64 70080 1c410404c5013a +W 64 70080 195a0455555555 +W 64 70080 18400404c2013b +W 64 70080 1c410404c5013b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c0013c +W 64 70080 1c410404c5013c +W 64 70080 195a0455555555 +W 64 70080 18400404be013d +W 64 70080 1c410404c5013d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc013e +W 64 70080 1c410404c5013e +W 64 70080 195a0455555555 +W 64 70080 18400404ba013f +W 64 70080 1c410404c5013f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b80140 +W 64 70080 1c410404c50140 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b70141 +W 64 70080 1c410404c50141 +W 64 70080 195a0455555555 +W 64 70080 18400404b50142 +W 64 70080 1c410404c50142 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30143 +W 64 70080 1c410404c50143 +W 64 70080 195a0455555555 +W 64 70080 18400404b10144 +W 64 70080 1c410404c50144 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404af0145 +W 64 70080 1c410404c50145 +W 64 70080 195a0455555555 +W 64 70080 18400404ad0146 +W 64 70080 1c410404c50146 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0147 +W 64 70080 1c410404c50147 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0148 +W 64 70080 1c410404c50148 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0149 +W 64 70080 1c410404c50149 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa014a +W 64 70080 1c410404c5014a +W 64 70080 195a0455555555 +W 64 70080 18400404aa014b +W 64 70080 1c410404c5014b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa014c +W 64 70080 1c410404c5014c +W 64 70080 195a0455555555 +W 64 70080 18400404aa014d +W 64 70080 1c410404c5014d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa014e +W 64 70080 1c410404c5014e +W 64 70080 195a0455555555 +W 64 70080 18400404aa014f +W 64 70080 1c410404c5014f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0150 +W 64 70080 1c410404c50150 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0151 +W 64 70080 1c410404c50151 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0152 +W 64 70080 1c410404c70152 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0153 +W 64 70080 1c410404c50153 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0154 +W 64 70080 1c410404c30154 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0155 +W 64 70080 1c410404c10155 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0156 +W 64 70080 1c410404bf0156 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0157 +W 64 70080 1c410404bd0157 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa0158 +W 64 70080 1c410404bb0158 +W 64 70080 195a0455555555 +W 64 70080 18400404aa0159 +W 64 70080 1c410404b90159 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa015a +W 64 70080 1c410404b7015a +W 64 70080 195a0455555555 +W 64 70080 18400404aa015b +W 64 70080 1c410404b5015b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa015c +W 64 70080 1c410404b3015c +W 64 70080 195a0455555555 +W 64 70080 18400404aa015d +W 64 70080 1c410404b1015d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404aa015e +W 64 70080 1c410404af015e +W 64 70080 195a0455555555 +W 64 70080 18400404aa015f +W 64 70080 1c410404ad015f +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404b6013f +W 64 70080 1c410404b7013f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b40140 +W 64 70080 1c410404b70140 +W 64 70080 195a0455555555 +W 64 70080 18400404b20141 +W 64 70080 1c410404b60141 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b00142 +W 64 70080 1c410404b40142 +W 64 70080 195a0455555555 +W 64 70080 18400404b00143 +W 64 70080 1c410404b20143 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b00144 +W 64 70080 1c410404b00144 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ac0160 +W 64 70080 1c410404aa015f +W 64 70080 18400404aa0148 +W 64 70080 1c410404aa0160 +W 64 70080 18400404aa0147 +W 64 70080 1c410404af0145 +W 64 70080 18400404af0145 +W 64 70080 1c410404b00142 +W 64 70080 18400404b00142 +W 64 70080 1c410404b00142 +W 64 70080 18400404b00142 +W 64 70080 1c410404b7013e +W 64 70080 18400404b7013e +W 64 70080 1c410404b80140 +W 64 70080 18400404b80140 +W 64 70080 1c410404c60139 +W 64 70080 18400404c60139 +W 64 70080 1c410404c60151 +W 64 70080 18400404c60151 +W 64 70080 1c410404c80152 +W 64 70080 18400404c80152 +W 64 70080 1c410404c90152 +W 64 70080 19150403ff0000 +T +W 8 61c88 4 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c6013c +W 64 70080 1c410404c7013c +W 64 70080 195a0455555555 +W 64 70080 18400404c4013d +W 64 70080 1c410404c7013d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c2013e +W 64 70080 1c410404c7013e +W 64 70080 195a0455555555 +W 64 70080 18400404c0013f +W 64 70080 1c410404c7013f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0140 +W 64 70080 1c410404c70140 +W 64 70080 195a0455555555 +W 64 70080 18400404bc0141 +W 64 70080 1c410404c70141 +W 64 70080 195a0455555555 +W 64 70080 18400404b90142 +W 64 70080 1c410404c70142 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b90143 +W 64 70080 1c410404c70143 +W 64 70080 195a0455555555 +W 64 70080 18400404b90144 +W 64 70080 1c410404c70144 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b90145 +W 64 70080 1c410404c70145 +W 64 70080 195a0455555555 +W 64 70080 18400404b70146 +W 64 70080 1c410404c70146 +W 64 70080 195a0455555555 +W 64 70080 18400404b40147 +W 64 70080 1c410404c70147 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae0148 +W 64 70080 1c410404c70148 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0149 +W 64 70080 1c410404c70149 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac014a +W 64 70080 1c410404c7014a +W 64 70080 195a0455555555 +W 64 70080 18400404ac014b +W 64 70080 1c410404c7014b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac014c +W 64 70080 1c410404c7014c +W 64 70080 195a0455555555 +W 64 70080 18400404ac014d +W 64 70080 1c410404c7014d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac014e +W 64 70080 1c410404c7014e +W 64 70080 195a0455555555 +W 64 70080 18400404ac014f +W 64 70080 1c410404c7014f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0150 +W 64 70080 1c410404c70150 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0151 +W 64 70080 1c410404c70151 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0152 +W 64 70080 1c410404c70152 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0153 +W 64 70080 1c410404c50153 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0154 +W 64 70080 1c410404c30154 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0155 +W 64 70080 1c410404c10155 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0156 +W 64 70080 1c410404bf0156 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0157 +W 64 70080 1c410404bd0157 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac0158 +W 64 70080 1c410404bb0158 +W 64 70080 195a0455555555 +W 64 70080 18400404ac0159 +W 64 70080 1c410404b90159 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac015a +W 64 70080 1c410404b7015a +W 64 70080 195a0455555555 +W 64 70080 18400404ac015b +W 64 70080 1c410404b5015b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac015c +W 64 70080 1c410404b3015c +W 64 70080 195a0455555555 +W 64 70080 18400404ac015d +W 64 70080 1c410404b1015d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac015e +W 64 70080 1c410404af015e +W 64 70080 195a0455555555 +W 64 70080 18400404ac015f +W 64 70080 1c410404ad015f +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ac014a +W 64 70080 1c410404ac0161 +W 64 70080 18400404ac0149 +W 64 70080 1c410404b00147 +W 64 70080 18400404b00147 +W 64 70080 1c410404b10148 +W 64 70080 18400404b10148 +W 64 70080 1c410404b90145 +W 64 70080 18400404b90143 +W 64 70080 1c410404b90146 +W 64 70080 18400404b90142 +W 64 70080 1c410404c8013b +W 64 70080 18400404c8013b +W 64 70080 1c410404c80152 +W 64 70080 18400404c80152 +W 64 70080 1c410404ac0160 +W 64 70080 19150403ff0000 +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 184604049e0167 +W 64 70080 1c470404db0176 +W 64 70080 19100410400008 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040001c000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049f0169 +W 64 70080 1c470404a90173 +W 64 70080 1c700410200000 +W 64 70080 1c700410202040 +W 64 70080 1c700420400000 +W 64 70080 1c700420402040 +W 64 70080 1c700420400000 +W 64 70080 1c700440804080 +W 64 70080 1c700440800000 +W 64 70080 1c700440808100 +W 64 70080 1c700481000000 +W 64 70080 1c7004c3000000 +W 64 70080 1c70047e000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404aa016c +W 64 70080 1c470404b00173 +W 64 70080 1c70042c000000 +W 64 70080 1c700432004200 +W 64 70080 1c700442000000 +W 64 70080 1c700444004400 +W 64 70080 1c700444000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700488000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b20169 +W 64 70080 1c470404b50173 +W 64 70080 1c700410001000 +W 64 70080 1c700410000000 +W 64 70080 1c700400002000 +W 64 70080 1c700420000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b5016c +W 64 70080 1c470404bc0173 +W 64 70080 1c700421000000 +W 64 70080 1c700412001400 +W 64 70080 1c700414000000 +W 64 70080 1c700408001800 +W 64 70080 1c700418000000 +W 64 70080 1c700424000000 +W 64 70080 1c700444000000 +W 64 70080 1c700482000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404bd0169 +W 64 70080 1c470404c50173 +W 64 70080 1c70041f001080 +W 64 70080 1c700410800000 +W 64 70080 1c700420802080 +W 64 70080 1c700420800000 +W 64 70080 1c700421007e00 +W 64 70080 1c70047e000000 +W 64 70080 1c700441004100 +W 64 70080 1c700441000000 +W 64 70080 1c700481000000 +W 64 70080 1c700482000000 +W 64 70080 1c700482000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c7016c +W 64 70080 1c470404cd0173 +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c700442008400 +W 64 70080 1c700484000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404cf016c +W 64 70080 1c470404d50173 +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c700442008400 +W 64 70080 1c700484000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404d7016a +W 64 70080 1c470404db0173 +W 64 70080 1c700410000000 +W 64 70080 1c700410007800 +W 64 70080 1c700478000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480008000 +W 64 70080 1c700480000000 +W 64 70080 1c700460000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400002000 +W 64 70080 1940040000000c +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c00185 +W 64 70080 1c410404c10185 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0186 +W 64 70080 1c410404c10186 +W 64 70080 195a0455555555 +W 64 70080 18400404bc0187 +W 64 70080 1c410404c10187 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba0188 +W 64 70080 1c410404c10188 +W 64 70080 195a0455555555 +W 64 70080 18400404b80189 +W 64 70080 1c410404c10189 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b6018a +W 64 70080 1c410404c1018a +W 64 70080 195a0455555555 +W 64 70080 18400404b4018b +W 64 70080 1c410404c1018b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b2018c +W 64 70080 1c410404c1018c +W 64 70080 195a0455555555 +W 64 70080 18400404b0018d +W 64 70080 1c410404c1018d +W 64 70080 195a0455555555 +W 64 70080 18400404ad018e +W 64 70080 1c410404c1018e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad018f +W 64 70080 1c410404c1018f +W 64 70080 195a0455555555 +W 64 70080 18400404ad0190 +W 64 70080 1c410404c10190 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad0191 +W 64 70080 1c410404c10191 +W 64 70080 195a0455555555 +W 64 70080 18400404ad0192 +W 64 70080 1c410404c10192 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad0193 +W 64 70080 1c410404c10193 +W 64 70080 195a0455555555 +W 64 70080 18400404ad0194 +W 64 70080 1c410404c10194 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad0195 +W 64 70080 1c410404c10195 +W 64 70080 195a0455555555 +W 64 70080 18400404ad0196 +W 64 70080 1c410404c10196 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad0197 +W 64 70080 1c410404c10197 +W 64 70080 195a0455555555 +W 64 70080 18400404ad0198 +W 64 70080 1c410404c10198 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad0199 +W 64 70080 1c410404c10199 +W 64 70080 195a0455555555 +W 64 70080 18400404ad019a +W 64 70080 1c410404c1019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad019b +W 64 70080 1c410404c1019b +W 64 70080 195a0455555555 +W 64 70080 18400404ad019c +W 64 70080 1c410404c1019c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad019d +W 64 70080 1c410404c1019d +W 64 70080 195a0455555555 +W 64 70080 18400404ad019e +W 64 70080 1c410404c1019e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad019f +W 64 70080 1c410404c1019f +W 64 70080 195a0455555555 +W 64 70080 18400404ad01a0 +W 64 70080 1c410404c101a0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01a1 +W 64 70080 1c410404c101a1 +W 64 70080 195a0455555555 +W 64 70080 18400404ad01a2 +W 64 70080 1c410404c101a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01a3 +W 64 70080 1c410404c101a3 +W 64 70080 195a0455555555 +W 64 70080 18400404ad01a4 +W 64 70080 1c410404c101a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01a5 +W 64 70080 1c410404c101a5 +W 64 70080 195a0455555555 +W 64 70080 18400404ad01a6 +W 64 70080 1c410404c101a6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01a7 +W 64 70080 1c410404bf01a7 +W 64 70080 195a0455555555 +W 64 70080 18400404ad01a8 +W 64 70080 1c410404bd01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01a9 +W 64 70080 1c410404bb01a9 +W 64 70080 195a0455555555 +W 64 70080 18400404ad01aa +W 64 70080 1c410404b901aa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01ab +W 64 70080 1c410404b701ab +W 64 70080 195a0455555555 +W 64 70080 18400404ad01ac +W 64 70080 1c410404b501ac +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01ad +W 64 70080 1c410404b301ad +W 64 70080 195a0455555555 +W 64 70080 18400404ad01ae +W 64 70080 1c410404b101ae +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01af +W 64 70080 1c410404af01af +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c20185 +W 64 70080 1c410404c201a7 +W 64 70080 18400404c20184 +W 64 70080 1c410404ad018e +W 64 70080 18400404ad018e +W 64 70080 1c410404ad01b0 +W 64 70080 18400404ad01b0 +W 64 70080 1c410404c201a6 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400004000 +W 64 70080 19400400000008 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404bd018c +W 64 70080 1c410404bd018c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb018d +W 64 70080 1c410404bd018d +W 64 70080 195a0455555555 +W 64 70080 18400404b9018e +W 64 70080 1c410404bd018e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b7018f +W 64 70080 1c410404bd018f +W 64 70080 195a0455555555 +W 64 70080 18400404b50190 +W 64 70080 1c410404bd0190 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30191 +W 64 70080 1c410404bd0191 +W 64 70080 195a0455555555 +W 64 70080 18400404b30192 +W 64 70080 1c410404bd0192 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30193 +W 64 70080 1c410404bd0193 +W 64 70080 195a0455555555 +W 64 70080 18400404b30194 +W 64 70080 1c410404bd0194 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30195 +W 64 70080 1c410404bd0195 +W 64 70080 195a0455555555 +W 64 70080 18400404b30196 +W 64 70080 1c410404bd0196 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30197 +W 64 70080 1c410404bd0197 +W 64 70080 195a0455555555 +W 64 70080 18400404b30198 +W 64 70080 1c410404bd0198 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30199 +W 64 70080 1c410404bd0199 +W 64 70080 195a0455555555 +W 64 70080 18400404b3019a +W 64 70080 1c410404bd019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3019b +W 64 70080 1c410404bd019b +W 64 70080 195a0455555555 +W 64 70080 18400404b3019c +W 64 70080 1c410404bd019c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3019d +W 64 70080 1c410404bd019d +W 64 70080 195a0455555555 +W 64 70080 18400404b3019e +W 64 70080 1c410404bd019e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3019f +W 64 70080 1c410404bd019f +W 64 70080 195a0455555555 +W 64 70080 18400404b301a0 +W 64 70080 1c410404bd01a0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a1 +W 64 70080 1c410404bd01a1 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a2 +W 64 70080 1c410404bd01a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a3 +W 64 70080 1c410404bd01a3 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a4 +W 64 70080 1c410404bd01a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a5 +W 64 70080 1c410404bd01a5 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a6 +W 64 70080 1c410404bd01a6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a7 +W 64 70080 1c410404bd01a7 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a8 +W 64 70080 1c410404bd01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a9 +W 64 70080 1c410404bb01a9 +W 64 70080 195a0455555555 +W 64 70080 18400404b301aa +W 64 70080 1c410404b901aa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301ab +W 64 70080 1c410404b701ab +W 64 70080 195a0455555555 +W 64 70080 18400404b301ac +W 64 70080 1c410404b501ac +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404be018b +W 64 70080 1c410404b30191 +W 64 70080 18400404b30191 +W 64 70080 1c410404b301ad +W 64 70080 18400404b301ad +W 64 70080 1c410404b301ad +W 64 70080 18400404b301ad +W 64 70080 1c410404be01a8 +W 64 70080 18400404be018c +W 64 70080 1c410404be01a9 +W 64 70080 18400404be018b +W 64 70080 1c410404be018b +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400008000 +W 64 70080 1940040000000b +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd01ab +W 64 70080 1c410404c001ab +W 64 70080 195a0455555555 +W 64 70080 18400404bb01ac +W 64 70080 1c410404c201ac +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901ad +W 64 70080 1c410404c401ad +W 64 70080 195a0455555555 +W 64 70080 18400404b701ae +W 64 70080 1c410404c501ae +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b501af +W 64 70080 1c410404c301af +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b801b0 +W 64 70080 1c410404c101b0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01b1 +W 64 70080 1c410404bf01b1 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b501af +W 64 70080 1c410404bd01b2 +W 64 70080 18400404bd01b2 +W 64 70080 1c410404c601ae +W 64 70080 18400404c601ae +W 64 70080 1c410404bf01aa +W 64 70080 18400404bf01aa +W 64 70080 1c410404b501af +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000b000 +W 64 70080 1940040000000f +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be018c +W 64 70080 1c410404c0018c +W 64 70080 195a0455555555 +W 64 70080 18400404be018d +W 64 70080 1c410404c2018d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be018e +W 64 70080 1c410404c4018e +W 64 70080 195a0455555555 +W 64 70080 18400404be018f +W 64 70080 1c410404c7018f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0190 +W 64 70080 1c410404c90190 +W 64 70080 195a0455555555 +W 64 70080 18400404be0191 +W 64 70080 1c410404cb0191 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0192 +W 64 70080 1c410404cb0192 +W 64 70080 195a0455555555 +W 64 70080 18400404be0193 +W 64 70080 1c410404cb0193 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0194 +W 64 70080 1c410404cb0194 +W 64 70080 195a0455555555 +W 64 70080 18400404be0195 +W 64 70080 1c410404cb0195 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0196 +W 64 70080 1c410404cb0196 +W 64 70080 195a0455555555 +W 64 70080 18400404be0197 +W 64 70080 1c410404cb0197 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0198 +W 64 70080 1c410404cb0198 +W 64 70080 195a0455555555 +W 64 70080 18400404be0199 +W 64 70080 1c410404cb0199 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be019a +W 64 70080 1c410404cb019a +W 64 70080 195a0455555555 +W 64 70080 18400404be019b +W 64 70080 1c410404cb019b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be019c +W 64 70080 1c410404cb019c +W 64 70080 195a0455555555 +W 64 70080 18400404be019d +W 64 70080 1c410404cb019d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be019e +W 64 70080 1c410404cb019e +W 64 70080 195a0455555555 +W 64 70080 18400404be019f +W 64 70080 1c410404cb019f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a0 +W 64 70080 1c410404cb01a0 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a1 +W 64 70080 1c410404cb01a1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a2 +W 64 70080 1c410404cb01a2 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a3 +W 64 70080 1c410404cb01a3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a4 +W 64 70080 1c410404cb01a4 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a5 +W 64 70080 1c410404cb01a5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a6 +W 64 70080 1c410404cb01a6 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a7 +W 64 70080 1c410404cb01a7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a8 +W 64 70080 1c410404cb01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c101a9 +W 64 70080 1c410404cb01a9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c401aa +W 64 70080 1c410404cb01aa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c701ab +W 64 70080 1c410404cb01ab +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ca01ac +W 64 70080 1c410404cb01ac +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404cc0191 +W 64 70080 1c410404be018b +W 64 70080 18400404be018b +W 64 70080 1c410404be01a8 +W 64 70080 18400404be01a8 +W 64 70080 1c410404cc01ad +W 64 70080 18400404cc0192 +W 64 70080 1c410404cc01ae +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400002000 +W 64 70080 1940040000000c +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be018c +W 64 70080 1c410404c0018c +W 64 70080 195a0455555555 +W 64 70080 18400404be018d +W 64 70080 1c410404c2018d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be018e +W 64 70080 1c410404c4018e +W 64 70080 195a0455555555 +W 64 70080 18400404be018f +W 64 70080 1c410404c7018f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0190 +W 64 70080 1c410404c90190 +W 64 70080 195a0455555555 +W 64 70080 18400404be0191 +W 64 70080 1c410404cb0191 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0192 +W 64 70080 1c410404cb0192 +W 64 70080 195a0455555555 +W 64 70080 18400404be0193 +W 64 70080 1c410404cb0193 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0194 +W 64 70080 1c410404cb0194 +W 64 70080 195a0455555555 +W 64 70080 18400404be0195 +W 64 70080 1c410404cb0195 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0196 +W 64 70080 1c410404cb0196 +W 64 70080 195a0455555555 +W 64 70080 18400404be0197 +W 64 70080 1c410404cb0197 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be0198 +W 64 70080 1c410404cb0198 +W 64 70080 195a0455555555 +W 64 70080 18400404be0199 +W 64 70080 1c410404cb0199 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be019a +W 64 70080 1c410404cb019a +W 64 70080 195a0455555555 +W 64 70080 18400404be019b +W 64 70080 1c410404cb019b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be019c +W 64 70080 1c410404cb019c +W 64 70080 195a0455555555 +W 64 70080 18400404be019d +W 64 70080 1c410404cb019d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be019e +W 64 70080 1c410404cb019e +W 64 70080 195a0455555555 +W 64 70080 18400404be019f +W 64 70080 1c410404cb019f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a0 +W 64 70080 1c410404cb01a0 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a1 +W 64 70080 1c410404cb01a1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a2 +W 64 70080 1c410404cb01a2 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a3 +W 64 70080 1c410404cb01a3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a4 +W 64 70080 1c410404cb01a4 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a5 +W 64 70080 1c410404cb01a5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a6 +W 64 70080 1c410404cb01a6 +W 64 70080 195a0455555555 +W 64 70080 18400404be01a7 +W 64 70080 1c410404cb01a7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404be01a8 +W 64 70080 1c410404cb01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c101a9 +W 64 70080 1c410404cb01a9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c401aa +W 64 70080 1c410404cb01aa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c701ab +W 64 70080 1c410404cb01ab +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ca01ac +W 64 70080 1c410404cb01ac +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404cc0191 +W 64 70080 1c410404be018b +W 64 70080 18400404be018b +W 64 70080 1c410404be01a8 +W 64 70080 18400404be01a8 +W 64 70080 1c410404cc01ad +W 64 70080 18400404cc0192 +W 64 70080 1c410404cc01ae +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c8019d +W 64 70080 1c410404c8019d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c8019e +W 64 70080 1c410404c8019e +W 64 70080 195a0455555555 +W 64 70080 18400404c8019f +W 64 70080 1c410404c8019f +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c9019d +W 64 70080 1c410404c9019d +W 64 70080 18400404c9019d +W 64 70080 1c410404c9019e +W 64 70080 18400404c9019e +W 64 70080 1c410404c9019e +W 64 70080 18400404c9019e +W 64 70080 1c410404c9019f +W 64 70080 18400404c9019f +W 64 70080 1c410404c9019f +W 64 70080 18400404c9019f +W 64 70080 1c410404c901a0 +W 64 70080 18400404c901a0 +W 64 70080 1c410404c901a0 +W 64 70080 18400404c901a0 +W 64 70080 1c410404c901a0 +W 64 70080 18400404c901a0 +W 64 70080 1c410404ca01a0 +W 64 70080 18400404c801a0 +W 64 70080 1c410404c801a1 +W 64 70080 18400404c8019f +W 64 70080 1c410404c801a0 +W 64 70080 18400404c8019e +W 64 70080 1c410404c8019e +W 64 70080 18400404c8019e +W 64 70080 1c410404c8019f +W 64 70080 18400404c8019d +W 64 70080 1c410404c8019d +W 64 70080 18400404c8019d +W 64 70080 1c410404c9019d +W 64 70080 18400404c9019d +W 64 70080 1c410404c9019d +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400004000 +W 64 70080 19400400000008 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404bd018c +W 64 70080 1c410404bd018c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb018d +W 64 70080 1c410404bd018d +W 64 70080 195a0455555555 +W 64 70080 18400404b9018e +W 64 70080 1c410404bd018e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b7018f +W 64 70080 1c410404bd018f +W 64 70080 195a0455555555 +W 64 70080 18400404b50190 +W 64 70080 1c410404bd0190 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30191 +W 64 70080 1c410404bd0191 +W 64 70080 195a0455555555 +W 64 70080 18400404b30192 +W 64 70080 1c410404bd0192 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30193 +W 64 70080 1c410404bd0193 +W 64 70080 195a0455555555 +W 64 70080 18400404b30194 +W 64 70080 1c410404bd0194 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30195 +W 64 70080 1c410404bd0195 +W 64 70080 195a0455555555 +W 64 70080 18400404b30196 +W 64 70080 1c410404bd0196 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30197 +W 64 70080 1c410404bd0197 +W 64 70080 195a0455555555 +W 64 70080 18400404b30198 +W 64 70080 1c410404bd0198 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30199 +W 64 70080 1c410404bd0199 +W 64 70080 195a0455555555 +W 64 70080 18400404b3019a +W 64 70080 1c410404bd019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3019b +W 64 70080 1c410404bd019b +W 64 70080 195a0455555555 +W 64 70080 18400404b3019c +W 64 70080 1c410404bd019c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3019d +W 64 70080 1c410404bd019d +W 64 70080 195a0455555555 +W 64 70080 18400404b3019e +W 64 70080 1c410404bd019e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3019f +W 64 70080 1c410404bd019f +W 64 70080 195a0455555555 +W 64 70080 18400404b301a0 +W 64 70080 1c410404bd01a0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a1 +W 64 70080 1c410404bd01a1 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a2 +W 64 70080 1c410404bd01a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a3 +W 64 70080 1c410404bd01a3 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a4 +W 64 70080 1c410404bd01a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a5 +W 64 70080 1c410404bd01a5 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a6 +W 64 70080 1c410404bd01a6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a7 +W 64 70080 1c410404bd01a7 +W 64 70080 195a0455555555 +W 64 70080 18400404b301a8 +W 64 70080 1c410404bd01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301a9 +W 64 70080 1c410404bb01a9 +W 64 70080 195a0455555555 +W 64 70080 18400404b301aa +W 64 70080 1c410404b901aa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301ab +W 64 70080 1c410404b701ab +W 64 70080 195a0455555555 +W 64 70080 18400404b301ac +W 64 70080 1c410404b501ac +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404be018b +W 64 70080 1c410404b30191 +W 64 70080 18400404b30191 +W 64 70080 1c410404b301ad +W 64 70080 18400404b301ad +W 64 70080 1c410404b301ad +W 64 70080 18400404b301ad +W 64 70080 1c410404be01a8 +W 64 70080 18400404be018c +W 64 70080 1c410404be01a9 +W 64 70080 18400404be018b +W 64 70080 1c410404be018b +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400004000 +W 64 70080 1940040000000b +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd018f +W 64 70080 1c410404bd018f +W 64 70080 195a0455555555 +W 64 70080 18400404bd0190 +W 64 70080 1c410404bd0190 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd0191 +W 64 70080 1c410404bd0191 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc0192 +W 64 70080 1c410404bd0192 +W 64 70080 195a0455555555 +W 64 70080 18400404bc0193 +W 64 70080 1c410404bd0193 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc0194 +W 64 70080 1c410404bd0194 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb0195 +W 64 70080 1c410404bd0195 +W 64 70080 195a0455555555 +W 64 70080 18400404bb0196 +W 64 70080 1c410404bd0196 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb0197 +W 64 70080 1c410404bd0197 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba0198 +W 64 70080 1c410404bd0198 +W 64 70080 195a0455555555 +W 64 70080 18400404ba0199 +W 64 70080 1c410404bd0199 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba019a +W 64 70080 1c410404bd019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b9019b +W 64 70080 1c410404bd019b +W 64 70080 195a0455555555 +W 64 70080 18400404b9019c +W 64 70080 1c410404bd019c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b9019d +W 64 70080 1c410404bd019d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b8019e +W 64 70080 1c410404bd019e +W 64 70080 195a0455555555 +W 64 70080 18400404b8019f +W 64 70080 1c410404bd019f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b801a0 +W 64 70080 1c410404bd01a0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b701a1 +W 64 70080 1c410404bd01a1 +W 64 70080 195a0455555555 +W 64 70080 18400404b701a2 +W 64 70080 1c410404bd01a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b701a3 +W 64 70080 1c410404bd01a3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b601a4 +W 64 70080 1c410404bd01a4 +W 64 70080 195a0455555555 +W 64 70080 18400404b601a5 +W 64 70080 1c410404bd01a5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b601a6 +W 64 70080 1c410404bd01a6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b501a7 +W 64 70080 1c410404bd01a7 +W 64 70080 195a0455555555 +W 64 70080 18400404b501a8 +W 64 70080 1c410404bd01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b501a9 +W 64 70080 1c410404bb01a9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b401aa +W 64 70080 1c410404b901aa +W 64 70080 195a0455555555 +W 64 70080 18400404b401ab +W 64 70080 1c410404b701ab +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b401ac +W 64 70080 1c410404b501ac +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400004000 +W 64 70080 1940040000000f +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd0199 +W 64 70080 1c410404bd0199 +W 64 70080 195a0455555555 +W 64 70080 18400404bd019a +W 64 70080 1c410404bd019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd019b +W 64 70080 1c410404bd019b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc019c +W 64 70080 1c410404bd019c +W 64 70080 195a0455555555 +W 64 70080 18400404bc019d +W 64 70080 1c410404bd019d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc019e +W 64 70080 1c410404bd019e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb019f +W 64 70080 1c410404bd019f +W 64 70080 195a0455555555 +W 64 70080 18400404bb01a0 +W 64 70080 1c410404bd01a0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01a1 +W 64 70080 1c410404bd01a1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01a2 +W 64 70080 1c410404bd01a2 +W 64 70080 195a0455555555 +W 64 70080 18400404ba01a3 +W 64 70080 1c410404bd01a3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01a4 +W 64 70080 1c410404bd01a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901a5 +W 64 70080 1c410404bd01a5 +W 64 70080 195a0455555555 +W 64 70080 18400404b901a6 +W 64 70080 1c410404bd01a6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901a7 +W 64 70080 1c410404bd01a7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b801a8 +W 64 70080 1c410404bd01a8 +W 64 70080 195a0455555555 +W 64 70080 18400404b801a9 +W 64 70080 1c410404bb01a9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b801aa +W 64 70080 1c410404b901aa +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400004000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd01a3 +W 64 70080 1c410404bd01a3 +W 64 70080 195a0455555555 +W 64 70080 18400404bd01a4 +W 64 70080 1c410404bd01a4 +W 64 70080 195a0455555555 +W 64 70080 18400404bc01a5 +W 64 70080 1c410404bd01a5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc01a6 +W 64 70080 1c410404bd01a6 +W 64 70080 195a0455555555 +W 64 70080 18400404bc01a7 +W 64 70080 1c410404bd01a7 +W 64 70080 195a0455555555 +W 64 70080 18400404bb01a8 +W 64 70080 1c410404bd01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01a9 +W 64 70080 1c410404bb01a9 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 1976040000000b +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bf01ab +W 64 70080 1c470404c001ab +W 64 70080 18460404bf01ac +W 64 70080 1c470404c201ac +W 64 70080 18460404bf01ad +W 64 70080 1c470404c401ad +W 64 70080 18460404be01ae +W 64 70080 1c470404c501ae +W 64 70080 18460404be01af +W 64 70080 1c470404c301af +W 64 70080 18460404be01b0 +W 64 70080 1c470404c101b0 +W 64 70080 18460404be01b1 +W 64 70080 1c470404bf01b1 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000b000 +W 64 70080 1940040000000f +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c101ac +W 64 70080 1c410404c201ac +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c101ad +W 64 70080 1c410404c401ad +W 64 70080 195a0455555555 +W 64 70080 18400404c101ae +W 64 70080 1c410404c501ae +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c101af +W 64 70080 1c410404c301af +W 64 70080 195a0455555555 +W 64 70080 18400404c101b0 +W 64 70080 1c410404c101b0 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c00190 +W 64 70080 1c470404c00190 +W 64 70080 18460404c00191 +W 64 70080 1c470404c00191 +W 64 70080 18460404c00192 +W 64 70080 1c470404c00192 +W 64 70080 18460404c00193 +W 64 70080 1c470404c00193 +W 64 70080 18460404c00194 +W 64 70080 1c470404c00194 +W 64 70080 18460404c00195 +W 64 70080 1c470404c00195 +W 64 70080 18460404c00196 +W 64 70080 1c470404c00196 +W 64 70080 18460404c00197 +W 64 70080 1c470404c00197 +W 64 70080 18460404c00198 +W 64 70080 1c470404c00198 +W 64 70080 18460404c00199 +W 64 70080 1c470404c00199 +W 64 70080 18460404c0019a +W 64 70080 1c470404c0019a +W 64 70080 18460404c0019b +W 64 70080 1c470404c0019b +W 64 70080 18460404c0019c +W 64 70080 1c470404c0019c +W 64 70080 18460404c0019d +W 64 70080 1c470404c0019d +W 64 70080 18460404c0019e +W 64 70080 1c470404c0019e +W 64 70080 18460404c0019f +W 64 70080 1c470404c0019f +W 64 70080 18460404c001a0 +W 64 70080 1c470404c001a0 +W 64 70080 18460404c001a1 +W 64 70080 1c470404c001a1 +W 64 70080 18460404c001a2 +W 64 70080 1c470404c001a2 +W 64 70080 18460404c001a3 +W 64 70080 1c470404c001a3 +W 64 70080 18460404c001a4 +W 64 70080 1c470404c001a4 +W 64 70080 18460404c001a5 +W 64 70080 1c470404c001a5 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bd01b2 +W 64 70080 1c410404c601ae +W 64 70080 18400404c601ae +W 64 70080 1c410404bf01aa +W 64 70080 18400404bf01aa +W 64 70080 1c410404c001aa +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404be018b +W 64 70080 1c410404be01a8 +W 64 70080 18400404be01a8 +W 64 70080 1c410404b301ad +W 64 70080 18400404b301ad +W 64 70080 1c410404b401ad +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400002000 +W 64 70080 19400400000008 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404ab018e +W 64 70080 1c410404ac018e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab018f +W 64 70080 1c410404ac018f +W 64 70080 195a0455555555 +W 64 70080 18400404ab0190 +W 64 70080 1c410404ac0190 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab0191 +W 64 70080 1c410404ac0191 +W 64 70080 195a0455555555 +W 64 70080 18400404ab0192 +W 64 70080 1c410404ac0192 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab0193 +W 64 70080 1c410404ac0193 +W 64 70080 195a0455555555 +W 64 70080 18400404ab0194 +W 64 70080 1c410404ac0194 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab0195 +W 64 70080 1c410404ac0195 +W 64 70080 195a0455555555 +W 64 70080 18400404ab0196 +W 64 70080 1c410404ac0196 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab0197 +W 64 70080 1c410404ac0197 +W 64 70080 195a0455555555 +W 64 70080 18400404ab0198 +W 64 70080 1c410404ac0198 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab0199 +W 64 70080 1c410404ac0199 +W 64 70080 195a0455555555 +W 64 70080 18400404ab019a +W 64 70080 1c410404ac019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab019b +W 64 70080 1c410404ac019b +W 64 70080 195a0455555555 +W 64 70080 18400404ab019c +W 64 70080 1c410404ac019c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab019d +W 64 70080 1c410404ac019d +W 64 70080 195a0455555555 +W 64 70080 18400404ab019e +W 64 70080 1c410404ac019e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab019f +W 64 70080 1c410404ac019f +W 64 70080 195a0455555555 +W 64 70080 18400404ab01a0 +W 64 70080 1c410404ac01a0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01a1 +W 64 70080 1c410404ac01a1 +W 64 70080 195a0455555555 +W 64 70080 18400404ab01a2 +W 64 70080 1c410404ac01a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01a3 +W 64 70080 1c410404ac01a3 +W 64 70080 195a0455555555 +W 64 70080 18400404ab01a4 +W 64 70080 1c410404ac01a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01a5 +W 64 70080 1c410404ac01a5 +W 64 70080 195a0455555555 +W 64 70080 18400404ab01a6 +W 64 70080 1c410404ac01a6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01a7 +W 64 70080 1c410404ac01a7 +W 64 70080 195a0455555555 +W 64 70080 18400404ab01a8 +W 64 70080 1c410404ac01a8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01a9 +W 64 70080 1c410404ac01a9 +W 64 70080 195a0455555555 +W 64 70080 18400404ab01aa +W 64 70080 1c410404ac01aa +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01ab +W 64 70080 1c410404ac01ab +W 64 70080 195a0455555555 +W 64 70080 18400404ab01ac +W 64 70080 1c410404ac01ac +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01ad +W 64 70080 1c410404ac01ad +W 64 70080 195a0455555555 +W 64 70080 18400404ab01ae +W 64 70080 1c410404ac01ae +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab01af +W 64 70080 1c410404ac01af +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ab018d +W 64 70080 1c410404ab01af +W 64 70080 18400404ab01af +W 64 70080 1c410404ad01b0 +W 64 70080 18400404ad018f +W 64 70080 1c410404ad01b1 +W 64 70080 18400404ad018e +W 64 70080 1c410404ab018d +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400002000 +W 64 70080 1940040000000f +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf0183 +W 64 70080 1c410404c00183 +W 64 70080 195a0455555555 +W 64 70080 18400404bd0184 +W 64 70080 1c410404c10184 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb0185 +W 64 70080 1c410404bf0185 +W 64 70080 195a0455555555 +W 64 70080 18400404b90186 +W 64 70080 1c410404bd0186 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b70187 +W 64 70080 1c410404bb0187 +W 64 70080 195a0455555555 +W 64 70080 18400404b50188 +W 64 70080 1c410404b90188 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b30189 +W 64 70080 1c410404b70189 +W 64 70080 195a0455555555 +W 64 70080 18400404b1018a +W 64 70080 1c410404b5018a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404af018b +W 64 70080 1c410404b3018b +W 64 70080 195a0455555555 +W 64 70080 18400404ad018c +W 64 70080 1c410404b1018c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ab018d +W 64 70080 1c410404af018d +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c00182 +W 64 70080 1c410404ab018d +W 64 70080 18400404ab018d +W 64 70080 1c410404ad018e +W 64 70080 18400404ad018e +W 64 70080 1c410404c20184 +W 64 70080 18400404c20184 +W 64 70080 1c410404c00182 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404bc0197 +W 64 70080 1c410404bc0197 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba0198 +W 64 70080 1c410404bc0198 +W 64 70080 195a0455555555 +W 64 70080 18400404b80199 +W 64 70080 1c410404bc0199 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b6019a +W 64 70080 1c410404bc019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b7019b +W 64 70080 1c410404bc019b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b8019c +W 64 70080 1c410404be019c +W 64 70080 195a0455555555 +W 64 70080 18400404b8019d +W 64 70080 1c410404c0019d +W 64 70080 195a0455555555 +W 64 70080 18400404b9019e +W 64 70080 1c410404c2019e +W 64 70080 195a0455555555 +W 64 70080 18400404ba019f +W 64 70080 1c410404c4019f +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01a0 +W 64 70080 1c410404bc01a0 +W 64 70080 195a0455555555 +W 64 70080 18400404bd01a0 +W 64 70080 1c410404c601a0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01a1 +W 64 70080 1c410404bc01a1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf01a1 +W 64 70080 1c410404c601a1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc01a2 +W 64 70080 1c410404bc01a2 +W 64 70080 195a0455555555 +W 64 70080 18400404c101a2 +W 64 70080 1c410404c601a2 +W 64 70080 195a0455555555 +W 64 70080 18400404bc01a3 +W 64 70080 1c410404bc01a3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c301a3 +W 64 70080 1c410404c601a3 +W 64 70080 195a0455555555 +W 64 70080 18400404c501a4 +W 64 70080 1c410404c601a4 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bd019b +W 64 70080 1c410404c701a0 +W 64 70080 18400404c701a0 +W 64 70080 1c410404c701a5 +W 64 70080 18400404c701a5 +W 64 70080 1c410404bd01a0 +W 64 70080 18400404bd01a0 +W 64 70080 1c410404bd01a5 +W 64 70080 18400404bd01a5 +W 64 70080 1c410404b6019a +W 64 70080 18400404b6019a +W 64 70080 1c410404bd0196 +W 64 70080 18400404bd0196 +W 64 70080 1c410404bd019b +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404b90198 +W 64 70080 1c410404ba0198 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b70199 +W 64 70080 1c410404ba0199 +W 64 70080 195a0455555555 +W 64 70080 18400404b5019a +W 64 70080 1c410404ba019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b3019b +W 64 70080 1c410404ba019b +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b4019c +W 64 70080 1c410404ba019c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b5019d +W 64 70080 1c410404bc019d +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b6019e +W 64 70080 1c410404be019e +W 64 70080 195a0455555555 +W 64 70080 18400404b6019f +W 64 70080 1c410404c0019f +W 64 70080 195a0455555555 +W 64 70080 18400404b701a0 +W 64 70080 1c410404c201a0 +W 64 70080 195a0455555555 +W 64 70080 18400404b801a1 +W 64 70080 1c410404ba01a1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01a1 +W 64 70080 1c410404c401a1 +W 64 70080 195a0455555555 +W 64 70080 18400404b901a2 +W 64 70080 1c410404ba01a2 +W 64 70080 195a0455555555 +W 64 70080 18400404bd01a2 +W 64 70080 1c410404c401a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901a3 +W 64 70080 1c410404ba01a3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf01a3 +W 64 70080 1c410404c401a3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01a4 +W 64 70080 1c410404ba01a4 +W 64 70080 195a0455555555 +W 64 70080 18400404c101a4 +W 64 70080 1c410404c401a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c301a5 +W 64 70080 1c410404c401a5 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb019c +W 64 70080 1c410404c501a1 +W 64 70080 18400404c501a1 +W 64 70080 1c410404c501a6 +W 64 70080 18400404c501a6 +W 64 70080 1c410404bb01a1 +W 64 70080 18400404bb01a1 +W 64 70080 1c410404bb01a6 +W 64 70080 18400404bb01a6 +W 64 70080 1c410404b3019b +W 64 70080 18400404b3019b +W 64 70080 1c410404bb0197 +W 64 70080 18400404bb0197 +W 64 70080 1c410404bb019c +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb0197 +W 64 70080 1c410404bc0197 +W 64 70080 195a0455555555 +W 64 70080 18400404bb0198 +W 64 70080 1c410404bc0198 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb0199 +W 64 70080 1c410404bc0199 +W 64 70080 195a0455555555 +W 64 70080 18400404bb019a +W 64 70080 1c410404bc019a +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb019b +W 64 70080 1c410404bc019b +W 64 70080 195a0455555555 +W 64 70080 18400404bb019c +W 64 70080 1c410404be019c +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd019d +W 64 70080 1c410404c0019d +W 64 70080 195a0455555555 +W 64 70080 18400404bf019e +W 64 70080 1c410404c2019e +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c1019f +W 64 70080 1c410404c4019f +W 64 70080 195a0455555555 +W 64 70080 18400404c301a0 +W 64 70080 1c410404c601a0 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb0197 +W 64 70080 1c410404bb019c +W 64 70080 18400404bb019c +W 64 70080 1c410404c501a1 +W 64 70080 18400404c501a1 +W 64 70080 1c410404c701a0 +W 64 70080 18400404c701a0 +W 64 70080 1c410404bd019b +W 64 70080 18400404bd0197 +W 64 70080 1c410404bd019c +W 64 70080 18400404bd0196 +W 64 70080 1c410404bb0197 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c501a1 +W 64 70080 1c410404c601a1 +W 64 70080 195a0455555555 +W 64 70080 18400404c501a2 +W 64 70080 1c410404c601a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c501a3 +W 64 70080 1c410404c601a3 +W 64 70080 195a0455555555 +W 64 70080 18400404c501a4 +W 64 70080 1c410404c601a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c501a5 +W 64 70080 1c410404c601a5 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c701a5 +W 64 70080 1c410404c501a6 +W 64 70080 18400404c501a2 +W 64 70080 1c410404c501a7 +W 64 70080 18400404c501a1 +W 64 70080 1c410404c701a0 +W 64 70080 18400404c701a0 +W 64 70080 1c410404c701a5 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404bb01a2 +W 64 70080 1c410404bb01a2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01a3 +W 64 70080 1c410404bc01a3 +W 64 70080 195a0455555555 +W 64 70080 18400404bb01a4 +W 64 70080 1c410404bc01a4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01a5 +W 64 70080 1c410404bc01a5 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb01a1 +W 64 70080 1c410404bb01a6 +W 64 70080 18400404bb01a6 +W 64 70080 1c410404bd01a5 +W 64 70080 18400404bd01a4 +W 64 70080 1c410404bd01a6 +W 64 70080 18400404bd01a3 +W 64 70080 1c410404bb01a1 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb019c +W 64 70080 1c410404bd019b +W 64 70080 18400404bd019b +W 64 70080 1c410404be019b +W 64 70080 19150403ff0000 +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 184604048301b3 +W 64 70080 1c470404f201c2 +W 64 70080 19100410400008 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040001c000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604048601b5 +W 64 70080 1c4704049101bf +W 64 70080 1c700484100000 +W 64 70080 1c700484108c20 +W 64 70080 1c70048c200000 +W 64 70080 1c70048c209440 +W 64 70080 1c700494400000 +W 64 70080 1c70049440a480 +W 64 70080 1c7004a4800000 +W 64 70080 1c7004a480c300 +W 64 70080 1c7004c3000000 +W 64 70080 1c700482000000 +W 64 70080 1c700482000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049201b8 +W 64 70080 1c4704049801bf +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c70047e008000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c70048c000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049a01b5 +W 64 70080 1c4704049d01bf +W 64 70080 1c700410001000 +W 64 70080 1c700410000000 +W 64 70080 1c700420002000 +W 64 70080 1c700420000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049d01b8 +W 64 70080 1c470404a301bf +W 64 70080 1c70041c002200 +W 64 70080 1c700422000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480008400 +W 64 70080 1c700484000000 +W 64 70080 1c700488007000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a401b8 +W 64 70080 1c470404aa01bf +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c700442008400 +W 64 70080 1c700484000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ac01b8 +W 64 70080 1c470404b601bf +W 64 70080 1c70042cc00000 +W 64 70080 1c700433204420 +W 64 70080 1c700444200000 +W 64 70080 1c700444404440 +W 64 70080 1c700444400000 +W 64 70080 1c700488800000 +W 64 70080 1c700488800000 +W 64 70080 1c700488800000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b701b8 +W 64 70080 1c470404bd01bf +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c70047e008000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c70048c000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404bd01c2 +W 64 70080 1c470404c401c2 +W 64 70080 1c7004ff000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c701b6 +W 64 70080 1c470404cb01bf +W 64 70080 1c700410000000 +W 64 70080 1c700410007800 +W 64 70080 1c700478000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480008000 +W 64 70080 1c700480000000 +W 64 70080 1c700460000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404cb01b8 +W 64 70080 1c470404d101bf +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c700442008400 +W 64 70080 1c700484000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404d101c2 +W 64 70080 1c470404d801c2 +W 64 70080 1c7004ff000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404db01b5 +W 64 70080 1c470404e301bf +W 64 70080 1c700407000000 +W 64 70080 1c700418802080 +W 64 70080 1c700420800000 +W 64 70080 1c700420001800 +W 64 70080 1c700418000000 +W 64 70080 1c700406000100 +W 64 70080 1c700401000000 +W 64 70080 1c700401008200 +W 64 70080 1c700482000000 +W 64 70080 1c7004c6000000 +W 64 70080 1c700438000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404e401b5 +W 64 70080 1c470404ed01bf +W 64 70080 1c700407800000 +W 64 70080 1c700418402040 +W 64 70080 1c700420400000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700487808080 +W 64 70080 1c700480800000 +W 64 70080 1c700481008100 +W 64 70080 1c700481000000 +W 64 70080 1c700447000000 +W 64 70080 1c700439000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ef01b5 +W 64 70080 1c470404f201bf +W 64 70080 1c700410000000 +W 64 70080 1c700410002000 +W 64 70080 1c700420000000 +W 64 70080 1c700420002000 +W 64 70080 1c700420000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440008000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c301ed +W 64 70080 1c470404c601ed +W 64 70080 18460404c101ee +W 64 70080 1c470404c801ee +W 64 70080 18460404bf01ef +W 64 70080 1c470404ca01ef +W 64 70080 18460404bd01f0 +W 64 70080 1c470404cc01f0 +W 64 70080 18460404bb01f1 +W 64 70080 1c470404ce01f1 +W 64 70080 18460404b901f2 +W 64 70080 1c470404d001f2 +W 64 70080 18460404b701f3 +W 64 70080 1c470404d101f3 +W 64 70080 18460404b501f4 +W 64 70080 1c470404cf01f4 +W 64 70080 18460404b301f5 +W 64 70080 1c470404cd01f5 +W 64 70080 18460404b101f6 +W 64 70080 1c470404cb01f6 +W 64 70080 18460404af01f7 +W 64 70080 1c470404c901f7 +W 64 70080 18460404ac01f8 +W 64 70080 1c470404c701f8 +W 64 70080 18460404ae01f9 +W 64 70080 1c470404c501f9 +W 64 70080 18460404b001fa +W 64 70080 1c470404c301fa +W 64 70080 18460404b201fb +W 64 70080 1c470404c101fb +W 64 70080 18460404b401fc +W 64 70080 1c470404bf01fc +W 64 70080 18460404b601fd +W 64 70080 1c470404bd01fd +W 64 70080 18460404b801fe +W 64 70080 1c470404bb01fe +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000f000 +W 64 70080 19400400000007 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c001db +W 64 70080 1c410404c201db +W 64 70080 195a0455555555 +W 64 70080 18400404bd01dc +W 64 70080 1c410404c201dc +W 64 70080 195a0455555555 +W 64 70080 18400404ba01dd +W 64 70080 1c410404c201dd +W 64 70080 195a0455555555 +W 64 70080 18400404b701de +W 64 70080 1c410404c201de +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b501df +W 64 70080 1c410404c201df +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b201e0 +W 64 70080 1c410404c301e0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404af01e1 +W 64 70080 1c410404c301e1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e2 +W 64 70080 1c410404c301e2 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e3 +W 64 70080 1c410404c301e3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e4 +W 64 70080 1c410404c301e4 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e5 +W 64 70080 1c410404c301e5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e6 +W 64 70080 1c410404c401e6 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e7 +W 64 70080 1c410404c401e7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e8 +W 64 70080 1c410404c401e8 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e9 +W 64 70080 1c410404c401e9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01ea +W 64 70080 1c410404c401ea +W 64 70080 195a0455555555 +W 64 70080 18400404ac01eb +W 64 70080 1c410404c401eb +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01ec +W 64 70080 1c410404c401ec +W 64 70080 195a0455555555 +W 64 70080 18400404ac01ed +W 64 70080 1c410404c301ed +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01ee +W 64 70080 1c410404c201ee +W 64 70080 195a0455555555 +W 64 70080 18400404ac01ef +W 64 70080 1c410404c101ef +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01f0 +W 64 70080 1c410404c001f0 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01f1 +W 64 70080 1c410404c001f1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01f2 +W 64 70080 1c410404bf01f2 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01f3 +W 64 70080 1c410404be01f3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae01f4 +W 64 70080 1c410404bd01f4 +W 64 70080 195a0455555555 +W 64 70080 18400404b001f5 +W 64 70080 1c410404bc01f5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b201f6 +W 64 70080 1c410404bc01f6 +W 64 70080 195a0455555555 +W 64 70080 18400404b401f7 +W 64 70080 1c410404bb01f7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b601f8 +W 64 70080 1c410404ba01f8 +W 64 70080 195a0455555555 +W 64 70080 18400404b801f9 +W 64 70080 1c410404b901f9 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000b000 +W 64 70080 1940040000000f +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404cf01e2 +W 64 70080 1c410404d001e2 +W 64 70080 195a0455555555 +W 64 70080 18400404cc01e3 +W 64 70080 1c410404d001e3 +W 64 70080 195a0455555555 +W 64 70080 18400404c901e4 +W 64 70080 1c410404d001e4 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e5 +W 64 70080 1c410404ae01e5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c701e5 +W 64 70080 1c410404d001e5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e6 +W 64 70080 1c410404b001e6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c401e6 +W 64 70080 1c410404d001e6 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e7 +W 64 70080 1c410404b201e7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c101e7 +W 64 70080 1c410404d001e7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e8 +W 64 70080 1c410404b401e8 +W 64 70080 195a0455555555 +W 64 70080 18400404bf01e8 +W 64 70080 1c410404d001e8 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e9 +W 64 70080 1c410404b601e9 +W 64 70080 195a0455555555 +W 64 70080 18400404bc01e9 +W 64 70080 1c410404d001e9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01ea +W 64 70080 1c410404b801ea +W 64 70080 195a0455555555 +W 64 70080 18400404b901ea +W 64 70080 1c410404d001ea +W 64 70080 195a0455555555 +W 64 70080 18400404ac01eb +W 64 70080 1c410404b901eb +W 64 70080 195a0455555555 +W 64 70080 18400404ba01eb +W 64 70080 1c410404d001eb +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01ec +W 64 70080 1c410404b901ec +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01ec +W 64 70080 1c410404d001ec +W 64 70080 195a0455555555 +W 64 70080 18400404ac01ed +W 64 70080 1c410404b901ed +W 64 70080 195a0455555555 +W 64 70080 18400404ba01ed +W 64 70080 1c410404c401ed +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c501ed +W 64 70080 1c410404d001ed +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01ee +W 64 70080 1c410404b901ee +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01ee +W 64 70080 1c410404c201ee +W 64 70080 195a0455555555 +W 64 70080 18400404c301ee +W 64 70080 1c410404d001ee +W 64 70080 195a0455555555 +W 64 70080 18400404ac01ef +W 64 70080 1c410404b901ef +W 64 70080 195a0455555555 +W 64 70080 18400404ba01ef +W 64 70080 1c410404c001ef +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c101ef +W 64 70080 1c410404d001ef +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01f0 +W 64 70080 1c410404b901f0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01f0 +W 64 70080 1c410404be01f0 +W 64 70080 195a0455555555 +W 64 70080 18400404bf01f0 +W 64 70080 1c410404ce01f0 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01f1 +W 64 70080 1c410404b901f1 +W 64 70080 195a0455555555 +W 64 70080 18400404ba01f1 +W 64 70080 1c410404bc01f1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd01f1 +W 64 70080 1c410404cc01f1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01f2 +W 64 70080 1c410404ca01f2 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01f3 +W 64 70080 1c410404c801f3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae01f4 +W 64 70080 1c410404c601f4 +W 64 70080 195a0455555555 +W 64 70080 18400404b001f5 +W 64 70080 1c410404b801f5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901f5 +W 64 70080 1c410404c301f5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b201f6 +W 64 70080 1c410404b801f6 +W 64 70080 195a0455555555 +W 64 70080 18400404b901f6 +W 64 70080 1c410404c101f6 +W 64 70080 195a0455555555 +W 64 70080 18400404b401f7 +W 64 70080 1c410404b801f7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901f7 +W 64 70080 1c410404bf01f7 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b601f8 +W 64 70080 1c410404b801f8 +W 64 70080 195a0455555555 +W 64 70080 18400404b901f8 +W 64 70080 1c410404bd01f8 +W 64 70080 195a0455555555 +W 64 70080 18400404b801f9 +W 64 70080 1c410404b801f9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901f9 +W 64 70080 1c410404bb01f9 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c501ed +W 64 70080 1c410404ba01f2 +W 64 70080 18400404ba01f2 +W 64 70080 1c410404b901ea +W 64 70080 18400404b901ea +W 64 70080 1c410404d101e1 +W 64 70080 18400404d101e1 +W 64 70080 1c410404d101ef +W 64 70080 18400404d101ef +W 64 70080 1c410404b901fa +W 64 70080 18400404b901f6 +W 64 70080 1c410404b901fb +W 64 70080 18400404b901f5 +W 64 70080 1c410404b901fa +W 64 70080 18400404b901fa +W 64 70080 1c410404ac01f3 +W 64 70080 18400404ac01f0 +W 64 70080 1c410404ac01f4 +W 64 70080 18400404ac01ec +W 64 70080 1c410404ac01f0 +W 64 70080 18400404ac01e5 +W 64 70080 1c410404ac01ec +W 64 70080 18400404ac01e4 +W 64 70080 1c410404b901ea +W 64 70080 18400404b901ea +W 64 70080 1c410404ba01f2 +W 64 70080 18400404ba01f2 +W 64 70080 1c410404c501ed +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ba01e9 +W 64 70080 1c410404d101e1 +W 64 70080 18400404d101e1 +W 64 70080 1c410404d101ef +W 64 70080 18400404d101ef +W 64 70080 1c410404b901fa +W 64 70080 18400404b901fa +W 64 70080 1c410404ba01fa +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404d101ea +W 64 70080 1c410404c901ed +W 64 70080 18400404c901ed +W 64 70080 1c410404ca01ed +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bc01dd +W 64 70080 1c470404c001dd +W 64 70080 18460404bb01de +W 64 70080 1c470404c401de +W 64 70080 18460404ba01df +W 64 70080 1c470404c801df +W 64 70080 18460404ba01e0 +W 64 70080 1c470404cc01e0 +W 64 70080 18460404b901e1 +W 64 70080 1c470404d001e1 +W 64 70080 18460404b801e2 +W 64 70080 1c470404ce01e2 +W 64 70080 18460404b201e3 +W 64 70080 1c470404cb01e3 +W 64 70080 18460404ac01e4 +W 64 70080 1c470404c801e4 +W 64 70080 18460404af01e5 +W 64 70080 1c470404c601e5 +W 64 70080 18460404b101e6 +W 64 70080 1c470404c301e6 +W 64 70080 18460404b301e7 +W 64 70080 1c470404c001e7 +W 64 70080 18460404b501e8 +W 64 70080 1c470404be01e8 +W 64 70080 18460404b701e9 +W 64 70080 1c470404bb01e9 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b901ea +W 64 70080 1c410404ac01e4 +W 64 70080 18400404ac01e4 +W 64 70080 1c410404b801e2 +W 64 70080 18400404b801e2 +W 64 70080 1c410404bc01dc +W 64 70080 18400404bc01dc +W 64 70080 1c410404d101e1 +W 64 70080 18400404d101e1 +W 64 70080 1c410404b901ea +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c801e8 +W 64 70080 1c410404d101e5 +W 64 70080 18400404d101e5 +W 64 70080 1c410404d201e5 +W 64 70080 18400404d001e5 +W 64 70080 1c410404cf01e6 +W 64 70080 18400404cf01e6 +W 64 70080 1c410404ca01e8 +W 64 70080 18400404ca01e8 +W 64 70080 1c410404cb01e8 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c501f2 +W 64 70080 1c410404c201f4 +W 64 70080 18400404c201f4 +W 64 70080 1c410404bd01f6 +W 64 70080 18400404bd01f6 +W 64 70080 1c410404be01f6 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ca01f0 +W 64 70080 1c410404c701f1 +W 64 70080 18400404c701f1 +W 64 70080 1c410404c801f1 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 19400400000001 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae01ec +W 64 70080 1c410404af01ec +W 64 70080 195a0455555555 +W 64 70080 18400404ae01ed +W 64 70080 1c410404b101ed +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b001ee +W 64 70080 1c410404b301ee +W 64 70080 195a0455555555 +W 64 70080 18400404b201ef +W 64 70080 1c410404b401ef +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b401f0 +W 64 70080 1c410404b401f0 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae01eb +W 64 70080 1c410404b501ef +W 64 70080 18400404b501ef +W 64 70080 1c410404b501f1 +W 64 70080 18400404b501f1 +W 64 70080 1c410404ae01ed +W 64 70080 18400404ae01ec +W 64 70080 1c410404ae01ee +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 19400400000001 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b501ef +W 64 70080 1c410404b601ef +W 64 70080 195a0455555555 +W 64 70080 18400404b501f0 +W 64 70080 1c410404b601f0 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b701ee +W 64 70080 1c410404b701f0 +W 64 70080 18400404b701f0 +W 64 70080 1c410404b501f1 +W 64 70080 18400404b501f0 +W 64 70080 1c410404b501f2 +W 64 70080 18400404b501ef +W 64 70080 1c410404b701ee +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 19400400000001 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404ae01eb +W 64 70080 1c410404af01eb +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ae01eb +W 64 70080 1c410404ae01eb +W 64 70080 18400404ae01eb +W 64 70080 1c410404b001eb +W 64 70080 18400404b001eb +W 64 70080 1c410404b001ec +W 64 70080 18400404b001ec +W 64 70080 1c410404ae01eb +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040000b000 +W 64 70080 1940040000000d +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b101df +W 64 70080 1c410404b201df +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ae01e0 +W 64 70080 1c410404b601e0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ad01e1 +W 64 70080 1c410404b701e1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e2 +W 64 70080 1c410404b801e2 +W 64 70080 195a0455555555 +W 64 70080 18400404ac01e3 +W 64 70080 1c410404b901e3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ac01e4 +W 64 70080 1c410404b901e4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404af01e5 +W 64 70080 1c410404b901e5 +W 64 70080 195a0455555555 +W 64 70080 18400404b101e6 +W 64 70080 1c410404b901e6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301e7 +W 64 70080 1c410404b901e7 +W 64 70080 195a0455555555 +W 64 70080 18400404b501e8 +W 64 70080 1c410404b901e8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b701e9 +W 64 70080 1c410404b901e9 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404b901ea +W 64 70080 1c410404ac01e4 +W 64 70080 18400404ac01e3 +W 64 70080 1c410404ac01e5 +W 64 70080 18400404ac01e2 +W 64 70080 1c410404ae01e0 +W 64 70080 18400404ae01e0 +W 64 70080 1c410404b101df +W 64 70080 18400404b101df +W 64 70080 1c410404b301df +W 64 70080 18400404b301df +W 64 70080 1c410404b701e0 +W 64 70080 18400404b701e0 +W 64 70080 1c410404b901e2 +W 64 70080 18400404b901e2 +W 64 70080 1c410404ba01e3 +W 64 70080 18400404ba01e3 +W 64 70080 1c410404b901ea +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ba01e3 +W 64 70080 1c410404ba01e3 +W 64 70080 18400404ba01e3 +W 64 70080 1c410404bb01e3 +W 64 70080 19150403ff0000 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c301ea +W 64 70080 1c410404bd01ec +W 64 70080 18400404bd01ec +W 64 70080 1c410404be01ec +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400001000 +W 64 70080 19400400000009 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404cc01dc +W 64 70080 1c410404ce01dc +W 64 70080 195a0455555555 +W 64 70080 18400404ca01dd +W 64 70080 1c410404cf01dd +W 64 70080 195a0455555555 +W 64 70080 18400404c701de +W 64 70080 1c410404cf01de +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c501df +W 64 70080 1c410404cd01df +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c201e0 +W 64 70080 1c410404ca01e0 +W 64 70080 195a0455555555 +W 64 70080 18400404c001e1 +W 64 70080 1c410404c801e1 +W 64 70080 195a0455555555 +W 64 70080 18400404bd01e2 +W 64 70080 1c410404c501e2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01e3 +W 64 70080 1c410404c301e3 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b801e4 +W 64 70080 1c410404c001e4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901e5 +W 64 70080 1c410404be01e5 +W 64 70080 195a0455555555 +W 64 70080 18400404b901e6 +W 64 70080 1c410404bb01e6 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ce01db +W 64 70080 1c410404d001de +W 64 70080 18400404d001de +W 64 70080 1c410404b901e7 +W 64 70080 18400404b901e7 +W 64 70080 1c410404b801e4 +W 64 70080 18400404b801e4 +W 64 70080 1c410404ce01db +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400001000 +W 64 70080 19400400000009 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404ce01df +W 64 70080 1c410404d001df +W 64 70080 195a0455555555 +W 64 70080 18400404cb01e0 +W 64 70080 1c410404d001e0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c901e1 +W 64 70080 1c410404d001e1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c601e2 +W 64 70080 1c410404ce01e2 +W 64 70080 195a0455555555 +W 64 70080 18400404c401e3 +W 64 70080 1c410404cb01e3 +W 64 70080 195a0455555555 +W 64 70080 18400404c101e4 +W 64 70080 1c410404c801e4 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bf01e5 +W 64 70080 1c410404c601e5 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bc01e6 +W 64 70080 1c410404c301e6 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901e7 +W 64 70080 1c410404c001e7 +W 64 70080 195a0455555555 +W 64 70080 18400404b901e8 +W 64 70080 1c410404be01e8 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b901e9 +W 64 70080 1c410404bb01e9 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404d101e1 +W 64 70080 1c410404d001de +W 64 70080 18400404d001de +W 64 70080 1c410404b901e7 +W 64 70080 18400404b901e7 +W 64 70080 1c410404b901ea +W 64 70080 18400404b901ea +W 64 70080 1c410404d101e1 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400001000 +W 64 70080 19400400000009 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a0455555555 +W 64 70080 18400404c701da +W 64 70080 1c410404ca01da +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c301db +W 64 70080 1c410404c801db +W 64 70080 195a0455555555 +W 64 70080 18400404bf01dc +W 64 70080 1c410404c501dc +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01dd +W 64 70080 1c410404c201dd +W 64 70080 195a0455555555 +W 64 70080 18400404b701de +W 64 70080 1c410404bf01de +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b301df +W 64 70080 1c410404bc01df +W 64 70080 195a0455555555 +W 64 70080 18400404b501e0 +W 64 70080 1c410404b901e0 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ca01d9 +W 64 70080 1c410404cb01da +W 64 70080 18400404cb01da +W 64 70080 1c410404b701e1 +W 64 70080 18400404b701e1 +W 64 70080 1c410404b301df +W 64 70080 18400404b301df +W 64 70080 1c410404ca01d9 +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400001000 +W 64 70080 19400400000009 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c301d9 +W 64 70080 1c410404c901d9 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c001da +W 64 70080 1c410404c701da +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bd01db +W 64 70080 1c410404c501db +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404ba01dc +W 64 70080 1c410404c201dc +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b701dd +W 64 70080 1c410404c001dd +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b401de +W 64 70080 1c410404be01de +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b101df +W 64 70080 1c410404bb01df +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b401e0 +W 64 70080 1c410404b901e0 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c501d8 +W 64 70080 1c410404ca01d9 +W 64 70080 18400404ca01d9 +W 64 70080 1c410404b701e1 +W 64 70080 18400404b701e1 +W 64 70080 1c410404b101df +W 64 70080 18400404b101df +W 64 70080 1c410404c501d8 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c101da +W 64 70080 1c470404c101da +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c201da +W 64 70080 1c410404c201da +W 64 70080 18400404c201da +W 64 70080 1c410404c301db +W 64 70080 18400404c301db +W 64 70080 1c410404c101da +W 64 70080 18400404c101da +W 64 70080 1c410404c201da +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404bf01d9 +W 64 70080 1c470404c101d9 +W 64 70080 18460404bc01da +W 64 70080 1c470404c101da +W 64 70080 18460404b901db +W 64 70080 1c470404be01db +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c201da +W 64 70080 1c410404bb01dc +W 64 70080 18400404bb01dc +W 64 70080 1c410404b901db +W 64 70080 18400404b901db +W 64 70080 1c410404c101d8 +W 64 70080 18400404c101d8 +W 64 70080 1c410404c201da +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b901dc +W 64 70080 1c470404ba01dc +W 64 70080 18460404ba01dd +W 64 70080 1c470404ba01dd +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404bb01de +W 64 70080 1c410404b901dc +W 64 70080 18400404b901dc +W 64 70080 1c410404b901dd +W 64 70080 18400404b901db +W 64 70080 1c410404bb01dc +W 64 70080 18400404bb01dc +W 64 70080 1c410404bb01de +W 64 70080 19150403ff0000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400001000 +W 64 70080 19400400000009 +W 64 70080 196d0400090240 +W 64 70080 19100400000060 +W 64 70080 18450400000015 +W 64 70080 195b04000f0000 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c801da +W 64 70080 1c410404cb01da +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404c501db +W 64 70080 1c410404cd01db +W 64 70080 195a0455555555 +W 64 70080 18400404c301dc +W 64 70080 1c410404cb01dc +W 64 70080 195a0455555555 +W 64 70080 18400404c001dd +W 64 70080 1c410404c901dd +W 64 70080 195a0455555555 +W 64 70080 18400404bd01de +W 64 70080 1c410404c601de +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404bb01df +W 64 70080 1c410404c401df +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b801e0 +W 64 70080 1c410404c101e0 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b501e1 +W 64 70080 1c410404bf01e1 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b601e2 +W 64 70080 1c410404bc01e2 +W 64 70080 195a04aaaaaaaa +W 64 70080 18400404b701e3 +W 64 70080 1c410404ba01e3 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404ca01d9 +W 64 70080 1c410404ce01db +W 64 70080 18400404ce01db +W 64 70080 1c410404b801e4 +W 64 70080 18400404b801e4 +W 64 70080 1c410404b501e1 +W 64 70080 18400404b501e1 +W 64 70080 1c410404ca01d9 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000004 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c201e1 +W 64 70080 1c470404c401e1 +W 64 70080 18460404c301e2 +W 64 70080 1c470404c501e2 +W 64 70080 18460404c401e3 +W 64 70080 1c470404c501e3 +W 64 70080 18460404c401e4 +W 64 70080 1c470404c601e4 +W 64 70080 18460404c501e5 +W 64 70080 1c470404c601e5 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c201e1 +W 64 70080 1c410404c401e0 +W 64 70080 18400404c401e0 +W 64 70080 1c410404c701e5 +W 64 70080 18400404c701e5 +W 64 70080 1c410404c501e6 +W 64 70080 18400404c501e6 +W 64 70080 1c410404c201e1 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000008 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c301e0 +W 64 70080 1c470404c501e0 +W 64 70080 18460404c001e1 +W 64 70080 1c470404c501e1 +W 64 70080 18460404c101e2 +W 64 70080 1c470404c301e2 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c001e1 +W 64 70080 1c410404c101e3 +W 64 70080 18400404c101e3 +W 64 70080 1c410404c601e1 +W 64 70080 18400404c601e0 +W 64 70080 1c410404c601e2 +W 64 70080 18400404c601df +W 64 70080 1c410404c001e1 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +T +W 64 70080 1961040c004504 +W 8 61c88 4 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000004 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c501e6 +W 64 70080 1c470404c601e6 +W 64 70080 18460404c501e7 +W 64 70080 1c470404c601e7 +W 64 70080 18460404c501e8 +W 64 70080 1c470404c601e8 +W 64 70080 18460404c501e9 +W 64 70080 1c470404c601e9 +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c501e6 +W 64 70080 1c410404c701e5 +W 64 70080 18400404c701e5 +W 64 70080 1c410404c701e9 +W 64 70080 18400404c701e9 +W 64 70080 1c410404c401ea +W 64 70080 18400404c401ea +W 64 70080 1c410404c501e6 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000007 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c401e8 +W 64 70080 1c470404c701e8 +W 64 70080 18460404c401e9 +W 64 70080 1c470404c801e9 +W 64 70080 18460404c401ea +W 64 70080 1c470404c901ea +W 64 70080 18460404c401eb +W 64 70080 1c470404c901eb +W 64 70080 18460404c401ec +W 64 70080 1c470404c801ec +W 64 70080 18460404c401ed +W 64 70080 1c470404c701ed +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c401e8 +W 64 70080 1c410404c401ed +W 64 70080 18400404c401ed +W 64 70080 1c410404c701ee +W 64 70080 18400404c701ee +W 64 70080 1c410404ca01eb +W 64 70080 18400404ca01eb +W 64 70080 1c410404c701e7 +W 64 70080 18400404c701e7 +W 64 70080 1c410404c401e8 +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c601e9 +W 64 70080 1c470404c601e9 +W 64 70080 18460404c601ea +W 64 70080 1c470404c701ea +W 64 70080 18460404c501eb +W 64 70080 1c470404c701eb +W 64 70080 19100400000400 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c0400000000 +W 64 70080 196d0400090240 +W 64 70080 18450400000015 +W 64 70080 18400404c601e9 +W 64 70080 1c410404c701e9 +W 64 70080 18400404c701e9 +W 64 70080 1c410404c801ea +W 64 70080 18400404c801ea +W 64 70080 1c410404c701ec +W 64 70080 18400404c701ec +W 64 70080 1c410404c501eb +W 64 70080 18400404c501eb +W 64 70080 1c410404c601ea +W 64 70080 18400404c601ea +W 64 70080 1c410404c601eb +W 64 70080 19150403ff0000 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404a901d6 +W 64 70080 1c470404a901d6 +W 64 70080 18460404aa01d7 +W 64 70080 1c470404aa01d7 +W 64 70080 18460404ab01d8 +W 64 70080 1c470404ab01d8 +W 64 70080 18460404ab01d9 +W 64 70080 1c470404ac01d9 +W 64 70080 18460404ac01da +W 64 70080 1c470404ad01da +W 64 70080 18460404ad01db +W 64 70080 1c470404ae01db +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404b001d1 +W 64 70080 1c470404b001d1 +W 64 70080 18460404b101d2 +W 64 70080 1c470404b101d2 +W 64 70080 18460404b101d3 +W 64 70080 1c470404b201d3 +W 64 70080 18460404b201d4 +W 64 70080 1c470404b301d4 +W 64 70080 18460404b201d5 +W 64 70080 1c470404b401d5 +W 64 70080 18460404b301d6 +W 64 70080 1c470404b501d6 +W 64 70080 18460404b301d7 +W 64 70080 1c470404b601d7 +W 64 70080 18460404b401d8 +W 64 70080 1c470404b601d8 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404ba01d1 +W 64 70080 1c470404ba01d1 +W 64 70080 18460404ba01d2 +W 64 70080 1c470404ba01d2 +W 64 70080 18460404bb01d3 +W 64 70080 1c470404bb01d3 +W 64 70080 18460404bb01d4 +W 64 70080 1c470404bb01d4 +W 64 70080 18460404bb01d5 +W 64 70080 1c470404bc01d5 +W 64 70080 18460404bb01d6 +W 64 70080 1c470404bc01d6 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404c401cf +W 64 70080 1c470404c401cf +W 64 70080 18460404c401d0 +W 64 70080 1c470404c401d0 +W 64 70080 18460404c401d1 +W 64 70080 1c470404c401d1 +W 64 70080 18460404c301d2 +W 64 70080 1c470404c401d2 +W 64 70080 18460404c301d3 +W 64 70080 1c470404c401d3 +W 64 70080 18460404c301d4 +W 64 70080 1c470404c401d4 +W 64 70080 18460404c301d5 +W 64 70080 1c470404c401d5 +W 64 70080 18460404c201d6 +W 64 70080 1c470404c401d6 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404ce01d1 +W 64 70080 1c470404ce01d1 +W 64 70080 18460404cd01d3 +W 64 70080 1c470404cd01d3 +W 64 70080 18460404cc01d4 +W 64 70080 1c470404cd01d4 +W 64 70080 18460404cc01d5 +W 64 70080 1c470404cc01d5 +W 64 70080 18460404cb01d6 +W 64 70080 1c470404cc01d6 +W 64 70080 18460404ca01d7 +W 64 70080 1c470404cc01d7 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404a801e4 +W 64 70080 1c470404a801e4 +W 64 70080 18460404a901e5 +W 64 70080 1c470404a901e5 +W 64 70080 18460404aa01e6 +W 64 70080 1c470404aa01e6 +W 64 70080 19100410100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000003 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 18460404a501db +W 64 70080 1c470404a501db +W 64 70080 18460404a601dc +W 64 70080 1c470404a701dc +W 64 70080 18460404a701dd +W 64 70080 1c470404a901dd +W 64 70080 18460404a801de +W 64 70080 1c470404ab01de +W 64 70080 18460404a901df +W 64 70080 1c470404aa01df +W 64 70080 18450400000018 +W 64 70080 19100400100000 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 19760400000001 +W 64 70080 196d0400090240 +W 64 70080 184604048301ff +W 64 70080 1c470404f2020e +W 64 70080 19100410400008 +W 64 70080 1961040c004504 +W 64 70080 196304000000ff +W 64 70080 196404000000ff +W 64 70080 19620400000000 +W 64 70080 185b0400000000 +W 64 70080 185c040001c000 +W 64 70080 196d0400090240 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404840201 +W 64 70080 1c4704048c020b +W 64 70080 1c70041f001080 +W 64 70080 1c700410800000 +W 64 70080 1c700420802080 +W 64 70080 1c700420800000 +W 64 70080 1c700421007e00 +W 64 70080 1c70047e000000 +W 64 70080 1c700441004100 +W 64 70080 1c700441000000 +W 64 70080 1c700481000000 +W 64 70080 1c700482000000 +W 64 70080 1c700482000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604048e0204 +W 64 70080 1c47040494020b +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c70047e008000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c70048c000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404950204 +W 64 70080 1c4704049d020e +W 64 70080 1c70040e801180 +W 64 70080 1c700411800000 +W 64 70080 1c700421002100 +W 64 70080 1c700421000000 +W 64 70080 1c700441004200 +W 64 70080 1c700442000000 +W 64 70080 1c700446003a00 +W 64 70080 1c70043a000000 +W 64 70080 1c700404000000 +W 64 70080 1c700484000000 +W 64 70080 1c700478000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 184604049e0201 +W 64 70080 1c470404a1020b +W 64 70080 1c700410001000 +W 64 70080 1c700410000000 +W 64 70080 1c700400002000 +W 64 70080 1c700420000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a20204 +W 64 70080 1c470404a8020b +W 64 70080 1c70041c000000 +W 64 70080 1c700422004000 +W 64 70080 1c700440000000 +W 64 70080 1c700438000400 +W 64 70080 1c700404000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404a90202 +W 64 70080 1c470404ad020b +W 64 70080 1c700410000000 +W 64 70080 1c700410007800 +W 64 70080 1c700478000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480008000 +W 64 70080 1c700480000000 +W 64 70080 1c700460000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ad0204 +W 64 70080 1c470404b3020b +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c70047e008000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c70048c000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b50204 +W 64 70080 1c470404ba020b +W 64 70080 1c70042c000000 +W 64 70080 1c700430004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404b8020e +W 64 70080 1c470404bf020e +W 64 70080 1c7004ff000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404c30201 +W 64 70080 1c470404cb020b +W 64 70080 1c7004ff800800 +W 64 70080 1c700408000000 +W 64 70080 1c700410001000 +W 64 70080 1c700410000000 +W 64 70080 1c700410002000 +W 64 70080 1c700420000000 +W 64 70080 1c700420002000 +W 64 70080 1c700420000000 +W 64 70080 1c700440000000 +W 64 70080 1c700440000000 +W 64 70080 1c700440000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ca0204 +W 64 70080 1c470404d0020b +W 64 70080 1c70041c000000 +W 64 70080 1c700422004200 +W 64 70080 1c700442000000 +W 64 70080 1c700442008400 +W 64 70080 1c700484000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700470000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404d0020e +W 64 70080 1c470404d7020e +W 64 70080 1c7004ff000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404dc0201 +W 64 70080 1c470404e7020b +W 64 70080 1c700484100000 +W 64 70080 1c700484108c20 +W 64 70080 1c70048c200000 +W 64 70080 1c70048c209440 +W 64 70080 1c700494400000 +W 64 70080 1c70049440a480 +W 64 70080 1c7004a4800000 +W 64 70080 1c7004a480c300 +W 64 70080 1c7004c3000000 +W 64 70080 1c700482000000 +W 64 70080 1c700482000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404e80201 +W 64 70080 1c470404eb020b +W 64 70080 1c700410001000 +W 64 70080 1c700410000000 +W 64 70080 1c700400002000 +W 64 70080 1c700420000000 +W 64 70080 1c700420004000 +W 64 70080 1c700440000000 +W 64 70080 1c700440004000 +W 64 70080 1c700440000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 1c700480000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +W 64 70080 18450400000018 +W 64 70080 180c0400000001 +W 64 70080 18460404ec0204 +W 64 70080 1c470404f2020b +W 64 70080 1c70042c000000 +W 64 70080 1c700432004200 +W 64 70080 1c700442000000 +W 64 70080 1c700444004400 +W 64 70080 1c700444000000 +W 64 70080 1c700484000000 +W 64 70080 1c700488000000 +W 64 70080 1c700488000000 +W 64 70080 180c0400000000 +W 64 70080 195f0400000000 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c08 0 +W 8 61c88 1 +W 8 61d08 0 +W 8 61c88 1 +W 8 61d08 0 +W 8 61c88 6 +R 32 61f80 10101 +W 8 61c88 1 +W 8 61d08 8 +W 8 61c88 1 +W 8 61d08 8 +W 8 61c88 0 +W 32 61d00 8073f +W 8 61c08 2 +W 8 61c88 1 +W 8 61d08 0 +W 8 61c88 1 +W 8 61d08 0 +W 8 61c88 6 +R 32 61f80 10101 +W 8 61c88 1 +W 8 61d08 8 +W 8 61c88 1 +W 8 61d08 8 +W 8 61c88 0 +W 32 61d00 8073f +W 8 61c08 1 +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 +T +W 8 61c88 4 diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 25bcef36..215514a1 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -1,6 +1,7 @@ //! Emulated hardware. Graphics boards get a directory each (GR2, Newport, //! IMPACT); the rest of the system's devices sit here as single files. +pub mod gl; pub mod gr2; pub mod mgras; pub mod ng1; diff --git a/src/disp.rs b/src/disp.rs index 58a961be..6c3075e2 100644 --- a/src/disp.rs +++ b/src/disp.rs @@ -16,6 +16,13 @@ use crate::debug_overlay::OverlaySource; /// Cached device state for the display pipeline. /// The refresh loop copies hardware state into this struct once per frame, /// then builds a `CompositorSource` from it for the active compositor. +/// Row stride of every display buffer, in pixels. +pub const FB_STRIDE: usize = 2048; +/// Rows of the display buffers and output textures: the tallest frame a +/// board can hand over (Newport and GR2 use the first 1024; IMPACT's +/// 1600x1200 modes need more). +pub const FB_MAX_H: usize = 2048; + pub struct Rex3Screen { pub width: usize, pub height: usize, @@ -71,10 +78,10 @@ impl Rex3Screen { Self { width: 0, height: 0, - fb_rgb: vec![0u32; 2048 * 1024], - fb_aux: vec![0u32; 2048 * 1024], - did: vec![0u8; 2048 * 1024], - rgba: vec![0u32; 2048 * 1024], + fb_rgb: vec![0u32; FB_STRIDE * FB_MAX_H], + fb_aux: vec![0u32; FB_STRIDE * FB_MAX_H], + did: vec![0u8; FB_STRIDE * FB_MAX_H], + rgba: vec![0u32; FB_STRIDE * FB_MAX_H], vc2_ram: vec![0u16; 32768], vc2_regs: [0u16; 32], cmap: [0u32; 8192], @@ -181,8 +188,8 @@ impl Rex3Screen { // ── 1. Copy device state snapshots ────────────────────────────────────── if copy_fb { diag.fetch_or(Rex3::DIAG_LOOP_FB_COPY, Ordering::Relaxed); - self.fb_rgb.copy_from_slice(fb_rgb); - self.fb_aux.copy_from_slice(fb_aux); + self.fb_rgb[..fb_rgb.len()].copy_from_slice(fb_rgb); + self.fb_aux[..fb_aux.len()].copy_from_slice(fb_aux); self.fb_borrowed = false; diag.fetch_and(!Rex3::DIAG_LOOP_FB_COPY, Ordering::Relaxed); } else { diff --git a/src/gl_compositor.rs b/src/gl_compositor.rs index 30959bf9..25ad11b2 100644 --- a/src/gl_compositor.rs +++ b/src/gl_compositor.rs @@ -7,6 +7,8 @@ use crate::dev::ng1::vc2::{ const FB_W: i32 = 2048; const FB_H: i32 = 1024; +/// Output texture rows (prebuilt frames can be taller than Newport's 1024). +const OUT_H: i32 = crate::disp::FB_MAX_H as i32; const CURSOR_TEX_SIZE: i32 = 64; const VERT_SRC: &str = " @@ -314,7 +316,7 @@ impl GlCompositor { gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32); gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32); gl.tex_image_2d(glow::TEXTURE_2D, 0, glow::RGBA as i32, - FB_W, FB_H, 0, glow::RGBA, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice(None)); + FB_W, OUT_H, 0, glow::RGBA, glow::UNSIGNED_BYTE, glow::PixelUnpackData::Slice(None)); self.tex_out = Some(tex_out); let fbo = gl.create_framebuffer().unwrap(); diff --git a/src/machine.rs b/src/machine.rs index 7f6b355c..22fa21cc 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -546,9 +546,8 @@ impl Machine { disk_provenance.sort_by_key(|d| d.id); let nvram_provenance = cfg.nvram.clone(); - // REX3 Graphics — Newport only; skipped in headless mode or when XZ board selected - // An IMPACT board takes the graphics slot and the window; no Newport then. - let rex3: Option> = if cfg.headless || cfg.graphics.board != crate::config::GraphicsBoard::Newport || cfg.impact.any_enabled() { + // 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 { 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))); @@ -583,10 +582,9 @@ 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 { - crate::config::GraphicsBoard::Newport => None, - board => { + crate::config::GraphicsBoard::Xz | crate::config::GraphicsBoard::Extreme => { use crate::dev::gr2::{Gr2, Gr2Stats, Gr2Variant}; - let variant = if 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 @@ -604,13 +602,17 @@ impl Machine { })); Some(g) } + _ => None, }; - // Indigo2 IMPACT graphics in the GIO graphics slot. - let mgras: Option> = if !guinness && cfg.impact.any_enabled() { - Some(Arc::new(crate::dev::mgras::Mgras::new(&cfg.impact, ioc.clone(), heartbeat.clone(), fasttick_count.clone()))) - } else { - None + // IMPACT graphics in the GIO graphics slot. + 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, ioc.clone(), heartbeat.clone(), fasttick_count.clone())) + } + _ => None, }; // N64 development board (Ultra64) — GIO slot 0 at 0x1F400000 diff --git a/src/physical.rs b/src/physical.rs index a2acbfbc..7995df17 100644 --- a/src/physical.rs +++ b/src/physical.rs @@ -319,7 +319,7 @@ pub struct Physical { pub rex3_head1: Option>, /// GR2 graphics (`graphics.board = xz | extreme`), see src/dev/gr2. pub gr2: Option>, - /// Indigo2 IMPACT/MGRAS preview stub (`[impact]` section). + /// IMPACT/MGRAS graphics (`graphics.board = solidimpact | highimpact | maximpact`). pub mgras: Option>, pub ultra64: Option>, /// Bare-metal test device (`--test-device`), in GIO expansion slot 0. diff --git a/src/platform_profile_tests.rs b/src/platform_profile_tests.rs index 7dba0a24..9e66c95a 100644 --- a/src/platform_profile_tests.rs +++ b/src/platform_profile_tests.rs @@ -11,7 +11,7 @@ mod tests { use parking_lot::Mutex; use crate::config::{ - GraphicsBoard, ImpactSection, ImpactSlot, MachineConfig, MachineProfile, + Cli, GraphicsBoard, MachineConfig, MachineProfile, }; use crate::dev::eeprom_93c56::Eeprom93c56; use crate::dev::ioc::{Ioc, IOC_BASE, IOC_SYS_ID, l1_regs, IOC_INT3_L1_STAT}; @@ -43,24 +43,54 @@ mod tests { fn impact_solid_on_indigo2_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::Indigo2Ip22; - cfg.impact = ImpactSection { - gfx: ImpactSlot::Solid, - exp0: ImpactSlot::None, - exp1: ImpactSlot::None, - }; + cfg.graphics.board = GraphicsBoard::SolidImpact; cfg.validate().expect("Solid IMPACT on Indigo2"); } #[test] - fn impact_on_indy_rejected() { + fn impact_on_indy_validates() { let mut cfg = minimal_cfg(); cfg.machine.profile = MachineProfile::IndyIp24; - cfg.impact.gfx = ImpactSlot::Solid; - let err = cfg.validate().unwrap_err(); - assert!( - err.contains("indigo2_ip22"), - "expected Indigo2-only guard, got: {err}" - ); + cfg.graphics.board = GraphicsBoard::SolidImpact; + cfg.validate().expect("Solid IMPACT on Indy"); + } + + #[test] + fn newport_on_ip28_validates() { + let mut cfg = minimal_cfg(); + cfg.machine.profile = MachineProfile::Indigo2Ip28; + cfg.graphics.board = GraphicsBoard::Newport; + cfg.validate().expect("Newport on IP28"); + } + + #[test] + fn xz_on_ip28_validates() { + let mut cfg = minimal_cfg(); + cfg.machine.profile = MachineProfile::Indigo2Ip28; + cfg.graphics.board = GraphicsBoard::Xz; + cfg.validate().expect("XZ on IP28"); + } + + #[test] + fn impact_keywords_parse() { + assert_eq!("solidimpact".parse::().unwrap(), GraphicsBoard::SolidImpact); + assert_eq!("impact:solid".parse::().unwrap(), GraphicsBoard::SolidImpact); + assert_eq!("highimpact".parse::().unwrap(), GraphicsBoard::HighImpact); + assert_eq!("impact:high".parse::().unwrap(), GraphicsBoard::HighImpact); + assert_eq!("maximpact".parse::().unwrap(), GraphicsBoard::MaxImpact); + assert_eq!("impact:max".parse::().unwrap(), GraphicsBoard::MaxImpact); + } + + #[test] + fn cli_graphics_keywords() { + 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); + + let cli = Cli::try_parse_from(["iris", "--board", "solidimpact"]).unwrap(); + let cfg = cli.apply(MachineConfig::default()); + assert_eq!(cfg.graphics.board, GraphicsBoard::SolidImpact); } #[test] @@ -186,10 +216,9 @@ mod tests { #[test] fn mgras_answers_the_gio_id_probe() { - let cfg = ImpactSection { gfx: ImpactSlot::Solid, exp0: ImpactSlot::None, exp1: ImpactSlot::None }; let hb = Arc::new(std::sync::atomic::AtomicU64::new(0)); let ioc = crate::dev::ioc::Ioc::new(false); - let m = Mgras::new(&cfg, ioc, hb.clone(), hb); + let m = Mgras::new(GraphicsBoard::SolidImpact, ioc, hb.clone(), hb); assert_eq!(m.read32(MGRAS_SLOT_GFX_BASE).data, GIO_ID); } diff --git a/src/ui.rs b/src/ui.rs index ed931f33..1b6cfe5e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -592,9 +592,9 @@ impl Renderer for GlRenderer { self.current_win_h = win_h; // Publish the display resolution for the event thread's aspect lock. *self.display_res.lock() = (width as u32, height as u32); - // UV coords into 2048×1024 texture + // UV coords into the 2048×FB_MAX_H output texture let max_u = width as f32 / 2048.0; - let max_v_main = height as f32 / 1024.0; + let max_v_main = height as f32 / crate::disp::FB_MAX_H as f32; unsafe { // Main display quad (letterboxed). Self::upload_quad(gl, state.main_vbo, diff --git a/test/gltest/glprim.c b/test/gltest/glprim.c index 7fde0774..19b68c02 100644 --- a/test/gltest/glprim.c +++ b/test/gltest/glprim.c @@ -55,6 +55,95 @@ static float line_w = 1.0f, point_sz = 1.0f; static int dither = 1; /* --nodither */ static int no_clear = 0; /* --noclear */ static int hold_ms = 2000; /* --hold MS */ +/* Generic raster state, applied after the clear, just before drawing (the + clear colour is the destination for blend / logic op / mask tests). */ +static float vtx_alpha = -1.0f; /* --alpha A: vertex alpha (glColor4f) */ +static GLenum blend_src = 0, blend_dst = 0; /* --blendfunc S,D */ +static GLenum alpha_func = 0; /* --alphafunc F,REF */ +static float alpha_ref = 0.0f; +static GLenum logic_op = 0; /* --logicop OP */ +static GLenum st_func = 0; /* --stencilfunc F,REF,MASK */ +static int st_ref = 0, st_mask = 0xff; +static GLenum st_ops[3] = { GL_KEEP, GL_KEEP, GL_KEEP }; /* --stencilop */ +static int line_stip_factor = 0; /* --linestipple FACTOR,PATTERN */ +static unsigned line_stip_pattern = 0; +static char color_mask[8] = ""; /* --colormask RGBA, e.g. 1010 */ +static int depth_mask = 1; /* --depthmask 0|1 */ +/* Texture scenes: 0 = the scene's default. */ +static GLenum tex_min = 0, tex_mag = 0; /* --texfilter MIN,MAG */ +static GLenum tex_wrap = 0, tex_wrap_t = 0; /* --texwrap S[,T] repeat|clamp */ +static GLenum tex_env = 0; /* --texenv replace|modulate|decal|blend */ +static int tex_abgr = 0; /* --texabgr: host data as GL_ABGR_EXT */ +static int tex_ushort = 0; /* --texushort: host data as GL_UNSIGNED_SHORT */ +static int tex_size = 8, tex_size_t = 0; /* --texsize W[xH] (powers of two, <= 256) */ +static float tex_bcolor[4] = { -1, 0, 0, 0 }; /* --texbcolor R,G,B,A */ +static int tex_border = 0; /* --texborder: one-texel border, magenta */ +static char tex_gen[8] = ""; /* --texgen obj|eye|sphere */ +static int tex_fog = 0; /* --texfog: linear fog, grey, over the scene */ +static int tex_lines = 0; /* --texlines: textured lines and points */ +static int tex_read = 0; /* --texread: glGetTexImage level 0, print it */ +static GLenum tex_ifmt = 0; /* --texifmt NAME: sized internal format (RGBA data) */ +static int tex_sub = 0; /* --texsub: glTexSubImage2D a yellow block */ +static int tex_subrect[4] = { 2, 2, 2, 2 }; /* --texsubrect X,Y,W,H (with --texsub) */ +static char tex_copy[8] = ""; /* --texcopy full|sub: texture from the screen */ + +/* Sized internal formats by name (GL 1.1), for --texifmt. */ +static const struct { const char *n; GLenum e; } ifmt_names[] = { + { "alpha4", GL_ALPHA4 }, { "alpha8", GL_ALPHA8 }, { "alpha12", GL_ALPHA12 }, { "alpha16", GL_ALPHA16 }, + { "luminance4", GL_LUMINANCE4 }, { "luminance8", GL_LUMINANCE8 }, { "luminance12", GL_LUMINANCE12 }, + { "luminance16", GL_LUMINANCE16 }, { "luminance4_alpha4", GL_LUMINANCE4_ALPHA4 }, + { "luminance6_alpha2", GL_LUMINANCE6_ALPHA2 }, { "luminance8_alpha8", GL_LUMINANCE8_ALPHA8 }, + { "luminance12_alpha4", GL_LUMINANCE12_ALPHA4 }, { "luminance12_alpha12", GL_LUMINANCE12_ALPHA12 }, + { "luminance16_alpha16", GL_LUMINANCE16_ALPHA16 }, { "intensity4", GL_INTENSITY4 }, + { "intensity8", GL_INTENSITY8 }, { "intensity12", GL_INTENSITY12 }, { "intensity16", GL_INTENSITY16 }, + { "r3_g3_b2", GL_R3_G3_B2 }, { "rgb4", GL_RGB4 }, { "rgb5", GL_RGB5 }, { "rgb8", GL_RGB8 }, + { "rgb10", GL_RGB10 }, { "rgb12", GL_RGB12 }, { "rgb16", GL_RGB16 }, { "rgba2", GL_RGBA2 }, + { "rgba4", GL_RGBA4 }, { "rgb5_a1", GL_RGB5_A1 }, { "rgba8", GL_RGBA8 }, { "rgb10_a2", GL_RGB10_A2 }, + { "rgba12", GL_RGBA12 }, { "rgba16", GL_RGBA16 }, +}; +static GLenum tex_fmt = 0; /* --texfmt rgba|rgb|luminance|luminance_alpha|alpha|intensity */ + +/* GL enum names the state options take. */ +static const struct { const char *n; GLenum e; } enum_names[] = { + { "never", GL_NEVER }, { "less", GL_LESS }, { "equal", GL_EQUAL }, { "lequal", GL_LEQUAL }, + { "greater", GL_GREATER }, { "notequal", GL_NOTEQUAL }, { "gequal", GL_GEQUAL }, { "always", GL_ALWAYS }, + { "zero", GL_ZERO }, { "one", GL_ONE }, { "src_color", GL_SRC_COLOR }, + { "one_minus_src_color", GL_ONE_MINUS_SRC_COLOR }, { "src_alpha", GL_SRC_ALPHA }, + { "one_minus_src_alpha", GL_ONE_MINUS_SRC_ALPHA }, { "dst_alpha", GL_DST_ALPHA }, + { "one_minus_dst_alpha", GL_ONE_MINUS_DST_ALPHA }, { "dst_color", GL_DST_COLOR }, + { "one_minus_dst_color", GL_ONE_MINUS_DST_COLOR }, { "src_alpha_saturate", GL_SRC_ALPHA_SATURATE }, + { "clear", GL_CLEAR }, { "and", GL_AND }, { "and_reverse", GL_AND_REVERSE }, { "copy", GL_COPY }, + { "and_inverted", GL_AND_INVERTED }, { "noop", GL_NOOP }, { "xor", GL_XOR }, { "or", GL_OR }, + { "nor", GL_NOR }, { "equiv", GL_EQUIV }, { "invert", GL_INVERT }, { "or_reverse", GL_OR_REVERSE }, + { "copy_inverted", GL_COPY_INVERTED }, { "or_inverted", GL_OR_INVERTED }, { "nand", GL_NAND }, + { "set", GL_SET }, { "keep", GL_KEEP }, { "replace", GL_REPLACE }, { "incr", GL_INCR }, { "decr", GL_DECR }, + { "nearest", GL_NEAREST }, { "linear", GL_LINEAR }, + { "nearest_mipmap_nearest", GL_NEAREST_MIPMAP_NEAREST }, { "linear_mipmap_nearest", GL_LINEAR_MIPMAP_NEAREST }, + { "nearest_mipmap_linear", GL_NEAREST_MIPMAP_LINEAR }, { "linear_mipmap_linear", GL_LINEAR_MIPMAP_LINEAR }, + { "repeat", GL_REPEAT }, { "clamp", GL_CLAMP }, { "border", 0x812D /* GL_CLAMP_TO_BORDER_SGIS */ }, { "modulate", GL_MODULATE }, { "decal", GL_DECAL }, + { "blend", GL_BLEND }, { "rgba", GL_RGBA }, { "rgb", GL_RGB }, { "luminance", GL_LUMINANCE }, + { "luminance_alpha", GL_LUMINANCE_ALPHA }, { "alpha", GL_ALPHA }, { "intensity", GL_INTENSITY }, +}; + +static GLenum enum_by_name(const char *s) { + unsigned i; + for (i = 0; i < sizeof(enum_names) / sizeof(enum_names[0]); i++) + if (!strcmp(enum_names[i].n, s)) return enum_names[i].e; + fprintf(stderr, "unknown GL name %s\n", s); + exit(2); + return 0; +} + +/* "a,b,c" -> up to n enum names. */ +static int parse_enums(const char *s, GLenum *out, int n) { + char buf[128], *p, *save; + int k = 0; + strncpy(buf, s, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = 0; + for (p = strtok_r(buf, ",", &save); p && k < n; p = strtok_r(NULL, ",", &save)) + out[k++] = enum_by_name(p); + return k; +} static int finish_first = 1; /* glFinish between clear and draw */ static int stipple = 0; /* --stipple: 1 = test, 2 = half */ static char scene[16] = ""; /* --scene depth|stencil|alphatest|blend */ @@ -165,20 +254,27 @@ static void draw_scene(float w, float h) { glVertex2f(0.6f * w, 0.9f * h); glEnd(); glDisable(GL_BLEND); - } else if (!strcmp(scene, "lit") || !strcmp(scene, "litlocal")) { + } else if (!strcmp(scene, "lit") || !strcmp(scene, "litlocal") || !strcmp(scene, "litrgb")) { /* A fan around the window centre whose vertex normals tilt away from +z: centre (0,0,1), rim normals tilted 60 degrees outwards. lit: directional light from (1,1,2) (w = 0). + litrgb: lit with light and model colours whose components all + differ, to see their order in the command stream. litlocal: positional light at (0.25w, 0.75h, 100) with linear attenuation, plus a spot light pointing down -z. */ static const GLfloat mat_amb[] = { 0.2f, 0.2f, 0.2f, 1 }; static const GLfloat mat_dif[] = { 0.8f, 0.3f, 0.2f, 1 }; static const GLfloat mat_spe[] = { 0.6f, 0.6f, 0.6f, 1 }; static const GLfloat mat_emi[] = { 0.0f, 0.0f, 0.1f, 1 }; - static const GLfloat lt_amb[] = { 0.1f, 0.1f, 0.1f, 1 }; - static const GLfloat lt_dif[] = { 1, 1, 1, 1 }; - static const GLfloat lt_spe[] = { 1, 1, 1, 1 }; - static const GLfloat model_amb[] = { 0.2f, 0.2f, 0.2f, 1 }; + static const GLfloat lt_grey[3][4] = { { 0.1f, 0.1f, 0.1f, 1 }, { 1, 1, 1, 1 }, { 1, 1, 1, 1 } }; + static const GLfloat lt_rgb[3][4] = { { 0.3f, 0.2f, 0.1f, 1 }, { 1, 0.5f, 0.25f, 1 }, { 0.25f, 0.5f, 1, 1 } }; + static const GLfloat model_grey[] = { 0.2f, 0.2f, 0.2f, 1 }; + static const GLfloat model_rgb[] = { 0.05f, 0.1f, 0.15f, 1 }; + int rgb = !strcmp(scene, "litrgb"); + const GLfloat *lt_amb = rgb ? lt_rgb[0] : lt_grey[0]; + const GLfloat *lt_dif = rgb ? lt_rgb[1] : lt_grey[1]; + const GLfloat *lt_spe = rgb ? lt_rgb[2] : lt_grey[2]; + const GLfloat *model_amb = rgb ? model_rgb : model_grey; GLfloat pos[4]; int k; glEnable(GL_LIGHTING); @@ -192,7 +288,7 @@ static void draw_scene(float w, float h) { glLightfv(GL_LIGHT0, GL_AMBIENT, lt_amb); glLightfv(GL_LIGHT0, GL_DIFFUSE, lt_dif); glLightfv(GL_LIGHT0, GL_SPECULAR, lt_spe); - if (!strcmp(scene, "lit")) { + if (strcmp(scene, "litlocal")) { pos[0] = 1; pos[1] = 1; pos[2] = 2; pos[3] = 0; } else { pos[0] = 0.25f * w; pos[1] = 0.75f * h; pos[2] = 100; pos[3] = 1; @@ -264,6 +360,108 @@ static void draw_scene(float w, float h) { glVertex3f(0.9f * w, 0.8f * h, -1.0f); glVertex3f(0.1f * w, 0.8f * h, 0.0f); glEnd(); glDisable(GL_FOG); + } else if (!strcmp(scene, "litcmat") || !strcmp(scene, "litnorm")) { + /* litcmat: colour material (front, ambient and diffuse) tracking + vertex colours R, G, B under a white light from +z. + litnorm: the lit fan's geometry scaled by 2 in the modelview with + GL_NORMALIZE on (without it the normals would be twice as long and + the fan twice as bright). */ + static const GLfloat pos[] = { 0, 0, 1, 0 }; + int k; + glEnable(GL_LIGHTING); + glShadeModel(GL_SMOOTH); + glLightfv(GL_LIGHT0, GL_POSITION, pos); + glEnable(GL_LIGHT0); + if (!strcmp(scene, "litcmat")) { + glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE); + glEnable(GL_COLOR_MATERIAL); + glNormal3f(0, 0, 1); + glBegin(GL_TRIANGLES); + glColor3f(1, 0, 0); glVertex2f(0.2f * w, 0.2f * h); + glColor3f(0, 1, 0); glVertex2f(0.8f * w, 0.2f * h); + glColor3f(0, 0, 1); glVertex2f(0.5f * w, 0.8f * h); + glEnd(); + glDisable(GL_COLOR_MATERIAL); + } else { + glEnable(GL_NORMALIZE); + glPushMatrix(); + glTranslatef(0.5f * w, 0.5f * h, 0); + glScalef(2, 2, 2); + glBegin(GL_TRIANGLE_FAN); + glNormal3f(0, 0, 1); + glVertex3f(0, 0, 0); + for (k = 0; k <= 8; k++) { + float a = (float)k * 3.14159265f / 4.0f; + float cx = (float)cos(a), cy = (float)sin(a); + glNormal3f(0.866f * cx, 0.866f * cy, 0.5f); + glVertex3f(0.2f * h * cx, 0.2f * h * cy, 0); + } + glEnd(); + glPopMatrix(); + glDisable(GL_NORMALIZE); + } + glDisable(GL_LIGHTING); + } else if (!strcmp(scene, "dlist")) { + /* Display lists: list 1 a unit quad; list 2 calls list 1 three + times (red, blue, white squares in the upper half: nested calls); + list 3 a long green strip along the bottom (4000 vertices, long + enough to be split into chained segments). */ + int k; + glNewList(1, GL_COMPILE); + glBegin(GL_QUADS); + glVertex2f(0, 0); glVertex2f(1, 0); glVertex2f(1, 1); glVertex2f(0, 1); + glEnd(); + glEndList(); + glNewList(2, GL_COMPILE); + glPushMatrix(); + glTranslatef(0.1f * w, 0.55f * h, 0); + glScalef(0.2f * w, 0.3f * h, 1); + glColor3f(1, 0, 0); glCallList(1); + glTranslatef(1.5f, 0, 0); + glColor3f(0, 0, 1); glCallList(1); + glTranslatef(1.5f, 0, 0); + glColor3f(1, 1, 1); glCallList(1); + glPopMatrix(); + glEndList(); + glNewList(3, GL_COMPILE); + glColor3f(0, 1, 0); + glBegin(GL_TRIANGLE_STRIP); + for (k = 0; k < 2000; k++) { + float x = 0.05f * w + 0.9f * w * (float)k / 1999.0f; + glVertex2f(x, 0.1f * h); + glVertex2f(x, 0.3f * h); + } + glEnd(); + glEndList(); + glCallList(2); + glCallList(3); + } else if (!strcmp(scene, "clipplane")) { + /* User clip plane 0: keep x <= 0.5w (plane (-1, 0, 0, 0.5w) in + object space), so only the left half of a full quad remains. */ + GLdouble eq[4]; + eq[0] = -1; eq[1] = 0; eq[2] = 0; eq[3] = 0.5 * w; + glClipPlane(GL_CLIP_PLANE0, eq); + glEnable(GL_CLIP_PLANE0); + glBegin(GL_QUADS); + glColor3f(1, 1, 0); + glVertex2f(0.1f * w, 0.2f * h); glVertex2f(0.9f * w, 0.2f * h); + glVertex2f(0.9f * w, 0.8f * h); glVertex2f(0.1f * w, 0.8f * h); + glEnd(); + glDisable(GL_CLIP_PLANE0); + } else if (!strcmp(scene, "fogexp") || !strcmp(scene, "fogexp2")) { + /* EXP / EXP2 fog, density 2, green, over the fog scene's quad. */ + static const GLfloat fc[] = { 0, 1, 0, 1 }; + glEnable(GL_FOG); + glFogi(GL_FOG_MODE, !strcmp(scene, "fogexp") ? GL_EXP : GL_EXP2); + glFogf(GL_FOG_DENSITY, 2.0f); + glFogfv(GL_FOG_COLOR, fc); + glShadeModel(GL_SMOOTH); + glBegin(GL_QUADS); + glColor3f(1, 1, 1); + glVertex3f(0.1f * w, 0.2f * h, 0.0f); glVertex3f(0.9f * w, 0.2f * h, -1.0f); + glVertex3f(0.9f * w, 0.8f * h, -1.0f); glVertex3f(0.1f * w, 0.8f * h, 0.0f); + glEnd(); + glDisable(GL_FOG); } else if (!strcmp(scene, "blendsmooth")) { glShadeModel(GL_FLAT); glBegin(GL_QUADS); @@ -324,6 +522,237 @@ static void draw_scene(float w, float h) { glEnd(); glShadeModel(smooth ? GL_SMOOTH : GL_FLAT); glDisable(GL_STENCIL_TEST); + } else if (!strcmp(scene, "copycolor") || !strcmp(scene, "copydepth")) { + /* glCopyPixels. Left half: a smooth triangle (red, green, blue + corners) whose z runs from -0.9 (left) to 0.9 (right), depth + test on. copycolor: the left half's colour copied to the right + half (raster position 0.5w, 0). copydepth: its depth copied to + the right half, then a white quad at z 0 over the right half with + GL_LESS: white only where the copied depth is farther than 0 + (right part of the triangle's copy and where it was cleared). */ + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glClear(GL_DEPTH_BUFFER_BIT); + glShadeModel(GL_SMOOTH); + glBegin(GL_TRIANGLES); + glColor3f(1, 0, 0); glVertex3f(0.05f * w, 0.1f * h, 0.9f); + glColor3f(0, 1, 0); glVertex3f(0.45f * w, 0.1f * h, -0.9f); + glColor3f(0, 0, 1); glVertex3f(0.25f * w, 0.9f * h, 0.0f); + glEnd(); + glShadeModel(smooth ? GL_SMOOTH : GL_FLAT); + glRasterPos2f(0.5f * w, 0); + if (!strcmp(scene, "copycolor")) { + glCopyPixels(0, 0, (GLsizei)(0.5f * w), (GLsizei)h, GL_COLOR); + } else { + glDisable(GL_DEPTH_TEST); + glDepthFunc(GL_ALWAYS); + glEnable(GL_DEPTH_TEST); + glCopyPixels(0, 0, (GLsizei)(0.5f * w), (GLsizei)h, GL_DEPTH); + glDepthFunc(GL_LESS); + glColor3f(1, 1, 1); + glBegin(GL_QUADS); + glVertex3f(0.5f * w, 0, 0); glVertex3f(w, 0, 0); glVertex3f(w, h, 0); glVertex3f(0.5f * w, h, 0); + glEnd(); + } + glDisable(GL_DEPTH_TEST); + } else if (!strcmp(scene, "tex") || !strcmp(scene, "texmod")) { + /* tex: an 8x8 RGBA texture, texel (s, t) = (s*0x20, t*0x20, 0xa5, + 0xff), GL_NEAREST, GL_REPLACE. Left: a quad over (0.1w..0.45w, + 0.2h..0.8h), texcoords 0..1, so each texel is a block of about + (0.35w/8) x (0.6h/8) pixels. Right: a triangle (0.55w,0.2h) + (0.9w,0.2h) (0.725w,0.8h) with texcoords (-0.5,-0.5) (1.5,-0.5) + (0.5,1.5), so the wrap mode shows. The --tex* options change + filters, wrap, environment (blend colour blue) and format. + texmod: the same texture mipmapped (gluBuild2DMipmaps is not + used: levels 8x8..1x1 built here, each level a flat grey of + 0xff >> level so the chosen level is visible), + GL_LINEAR_MIPMAP_LINEAR, GL_MODULATE with vertex colours red, + green, blue, white, on a quad tilted away in perspective. */ + static GLubyte tex[258 * 258 * 4]; + int s, t, l, i, k, NW = tex_size, NH = tex_size_t ? tex_size_t : tex_size, mod = !strcmp(scene, "texmod"); + int B = tex_border, IW = NW + 2 * B, IH = NH + 2 * B; + GLenum fmt = tex_fmt ? tex_fmt : GL_RGBA; + GLenum ext = fmt == GL_INTENSITY ? GL_LUMINANCE : fmt; + GLenum ifmt = tex_ifmt ? tex_ifmt : fmt; + GLenum fmin = tex_min ? tex_min : mod ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST; + GLenum fmag = tex_mag ? tex_mag : mod ? GL_LINEAR : GL_NEAREST; + GLenum wrap = tex_wrap ? tex_wrap : GL_REPEAT, wrap_t = tex_wrap_t ? tex_wrap_t : wrap; + GLenum env = tex_env ? tex_env : mod ? GL_MODULATE : GL_REPLACE; + int nc = ext == GL_RGBA ? 4 : ext == GL_RGB ? 3 : ext == GL_LUMINANCE_ALPHA ? 2 : 1; + /* Components per format, from the RGBA texel: RGB drops alpha, + luminance (and intensity) is the red ramp, luminance-alpha adds + the green ramp as alpha, alpha is the green ramp. Border texels + (--texborder) are magenta. */ + for (t = -B, i = 0; t < NH + B; t++) + for (s = -B; s < NW + B; s++) { + GLubyte rgba[4]; + int border = s < 0 || t < 0 || s >= NW || t >= NH; + rgba[0] = border ? 0xff : (GLubyte)(s * 256 / NW); + rgba[1] = border ? 0x00 : (GLubyte)(t * 256 / NH); + rgba[2] = border ? 0xff : 0xa5; rgba[3] = 0xff; + if (ext == GL_ALPHA) tex[i++] = rgba[1]; + else if (ext == GL_LUMINANCE) tex[i++] = rgba[0]; + else if (ext == GL_LUMINANCE_ALPHA) { tex[i++] = rgba[0]; tex[i++] = rgba[1]; } + else for (l = 0; l < nc; l++) tex[i++] = rgba[l]; + } + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + if (tex_abgr && nc == 4) + for (i = 0; i < IW * IH * 4; i += 4) { + GLubyte r = tex[i], g = tex[i + 1]; + tex[i] = tex[i + 3]; tex[i + 1] = tex[i + 2]; tex[i + 2] = g; tex[i + 3] = r; + } + if (tex_ushort) { + /* Each byte b becomes the short b * 0x101 (big-endian). */ + static GLushort us[258 * 258 * 4]; + for (i = 0; i < IW * IH * nc; i++) us[i] = (GLushort)(tex[i] * 0x101); + glTexImage2D(GL_TEXTURE_2D, 0, ifmt, IW, IH, B, tex_abgr && nc == 4 ? GL_ABGR_EXT : ext, GL_UNSIGNED_SHORT, us); + } else + glTexImage2D(GL_TEXTURE_2D, 0, ifmt, IW, IH, B, tex_abgr && nc == 4 ? GL_ABGR_EXT : ext, GL_UNSIGNED_BYTE, tex); + if (fmin != GL_NEAREST && fmin != GL_LINEAR) { + /* Levels down to 1x1, each a flat grey of 0xff >> level (alpha + 0xff) so the level chosen shows; borders as level 0's. */ + int lw = NW, lh = NH; + for (l = 1; lw > 1 || lh > 1; l++) { + lw = lw > 1 ? lw / 2 : 1; lh = lh > 1 ? lh / 2 : 1; + for (i = 0, k = 0; k < (lw + 2 * B) * (lh + 2 * B); k++) + for (s = 0; s < nc; s++, i++) + tex[i] = (nc == 4 && s == 3) || (nc == 2 && s == 1) ? 0xff : (GLubyte)(0xff >> l); + glTexImage2D(GL_TEXTURE_2D, l, ifmt, lw + 2 * B, lh + 2 * B, B, ext, GL_UNSIGNED_BYTE, tex); + } + } + if (tex_sub) { + static GLubyte sub[256 * 256 * 4]; + int sx = tex_subrect[0], sy = tex_subrect[1], sw = tex_subrect[2], sh = tex_subrect[3], j; + static const GLubyte yellow[4] = { 255, 255, 0, 255 }; + for (i = 0; i < sw * sh; i++) + for (k = 0; k < nc; k++) sub[i * nc + k] = yellow[nc == 1 ? (ext == GL_ALPHA ? 3 : 0) : k]; + { + GLenum e = glGetError(); + if (e) printf("texsub: error 0x%x pending before glTexSubImage2D\n", e); + } + glTexSubImage2D(GL_TEXTURE_2D, 0, sx, sy, sw, sh, tex_abgr && nc == 4 ? GL_ABGR_EXT : ext, GL_UNSIGNED_BYTE, sub); + printf("texsub: (%d,%d) %dx%d glGetError 0x%x\n", sx, sy, sw, sh, glGetError()); + for (j = 0; j < sh; j++) /* keep tex[] what level 0 now holds, for --texread */ + for (i = 0; i < sw; i++) { + int at = ((sy + j + B) * IW + sx + i + B) * nc; + for (k = 0; k < nc; k++) tex[at + k] = sub[(j * sw + i) * nc + k]; + } + } + if (tex_copy[0]) { + /* Draw the grid, then copy it (or one square) into level 0. */ + int gx = (int)(0.1f * w), gy = (int)(0.2f * h), q; + static const GLfloat cols[4][3] = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }, { 1, 1, 1 } }; + glDisable(GL_TEXTURE_2D); + for (q = 0; q < 4; q++) { + int qx = gx + (q % 2) * NW / 2, qy = gy + (q / 2) * NH / 2; + glColor3fv(cols[q]); + glRecti(qx, qy, qx + NW / 2, qy + NH / 2); + } + if (!strcmp(tex_copy, "sub")) glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gx, gy, NW / 2, NH / 2); + else glCopyTexImage2D(GL_TEXTURE_2D, 0, ifmt, gx, gy, NW, NH, 0); + printf("texcopy: glGetError 0x%x\n", glGetError()); + } + if (tex_read && !B && nc == 4 && !tex_abgr && !tex_ushort && !tex_copy[0]) { + static GLubyte back[256 * 256 * 4]; + int bad = 0; + memset(back, 0x11, sizeof(back)); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, back); + for (i = 0; i < NW * NH * 4; i++) bad += back[i] != tex[i]; + printf("texread: %02x%02x%02x%02x %02x%02x%02x%02x ... %d of %d bytes differ\n", + back[0], back[1], back[2], back[3], back[4], back[5], back[6], back[7], bad, NW * NH * 4); + } + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap_t); + if (tex_bcolor[0] >= 0) glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, tex_bcolor); + if (tex_gen[0]) { + /* Object / eye planes mapping the quad (0.1w..0.45w, 0.2h..0.8h) + to s, t = 0..1 (eye = object here: identity modelview). */ + GLfloat sp[4], tp[4]; + GLenum mode = !strcmp(tex_gen, "sphere") ? GL_SPHERE_MAP : !strcmp(tex_gen, "eye") ? GL_EYE_LINEAR : GL_OBJECT_LINEAR; + sp[0] = 1.0f / (0.35f * w); sp[1] = 0; sp[2] = 0; sp[3] = -0.1f / 0.35f; + tp[0] = 0; tp[1] = 1.0f / (0.6f * h); tp[2] = 0; tp[3] = -0.2f / 0.6f; + glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, mode); + glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, mode); + if (mode != GL_SPHERE_MAP) { + GLenum pl = mode == GL_EYE_LINEAR ? GL_EYE_PLANE : GL_OBJECT_PLANE; + glTexGenfv(GL_S, pl, sp); + glTexGenfv(GL_T, pl, tp); + } + glEnable(GL_TEXTURE_GEN_S); + glEnable(GL_TEXTURE_GEN_T); + } + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, fmin); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, fmag); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, env); + if (env == GL_BLEND) { + static const GLfloat envc[4] = { 0.0f, 0.0f, 1.0f, 1.0f }; + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, envc); + } + glEnable(GL_TEXTURE_2D); + if (mod) { + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glFrustum(-1.0, 1.0, -h / w, h / w, 1.0, 20.0); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + glTranslatef(0, -0.3f, -2.0f); + glRotatef(-70, 1, 0, 0); + glBegin(GL_QUADS); + glColor3f(1, 0, 0); glTexCoord2f(0, 0); glVertex3f(-1, -1, 0); + glColor3f(0, 1, 0); glTexCoord2f(4, 0); glVertex3f(1, -1, 0); + glColor3f(0, 0, 1); glTexCoord2f(4, 16); glVertex3f(1, 6, 0); + glColor3f(1, 1, 1); glTexCoord2f(0, 16); glVertex3f(-1, 6, 0); + glEnd(); + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); + } else { + if (mono) glColor3f(mono_rgb[0], mono_rgb[1], mono_rgb[2]); + else glColor3f(1, 1, 1); + if (tex_fog) { + static const GLfloat grey[4] = { 0.5f, 0.5f, 0.5f, 1.0f }; + glFogi(GL_FOG_MODE, GL_LINEAR); + glFogf(GL_FOG_START, 0.0f); + glFogf(GL_FOG_END, 1.0f); + glFogfv(GL_FOG_COLOR, grey); + glEnable(GL_FOG); + glTranslatef(0, 0, -0.5f); + } + if (tex_lines) { + int k; + glBegin(GL_LINES); + for (k = 0; k < 4; k++) { + glTexCoord2f(0, k / 4.0f + 0.125f); glVertex2f(0.1f * w, (0.05f + 0.03f * k) * h); + glTexCoord2f(1, k / 4.0f + 0.125f); glVertex2f(0.9f * w, (0.05f + 0.03f * k) * h); + } + glEnd(); + glPointSize(4); + glBegin(GL_POINTS); + for (k = 0; k < 8; k++) { + glTexCoord2f(k / 8.0f + 0.0625f, 0.5f); glVertex2f((0.1f + 0.1f * k) * w, 0.9f * h); + } + glEnd(); + glPointSize(1); + } + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(0.1f * w, 0.2f * h); + glTexCoord2f(1, 0); glVertex2f(0.45f * w, 0.2f * h); + glTexCoord2f(1, 1); glVertex2f(0.45f * w, 0.8f * h); + glTexCoord2f(0, 1); glVertex2f(0.1f * w, 0.8f * h); + glEnd(); + glBegin(GL_TRIANGLES); + glTexCoord2f(-0.5f, -0.5f); glVertex2f(0.55f * w, 0.2f * h); + glTexCoord2f(1.5f, -0.5f); glVertex2f(0.9f * w, 0.2f * h); + glTexCoord2f(0.5f, 1.5f); glVertex2f(0.725f * w, 0.8f * h); + glEnd(); + } + glDisable(GL_TEXTURE_2D); + glDisable(GL_TEXTURE_GEN_S); + glDisable(GL_TEXTURE_GEN_T); + glDisable(GL_FOG); } else { printf("unknown scene %s\n", scene); } @@ -380,7 +809,8 @@ static void geometry(GLenum mode, const struct vtx **v, int *n) { /* ---- emit one vertex/colour in the requested call form ------------------ */ static void emit_color(float r, float g, float b) { - if (!strcmp(col_form, "3f")) glColor3f(r, g, b); + if (vtx_alpha >= 0.0f) glColor4f(r, g, b, vtx_alpha); + else if (!strcmp(col_form, "3f")) glColor3f(r, g, b); else if (!strcmp(col_form, "4f")) glColor4f(r, g, b, 1.0f); else if (!strcmp(col_form, "3ub")) glColor3ub((GLubyte)(r * 255), (GLubyte)(g * 255), (GLubyte)(b * 255)); else if (!strcmp(col_form, "4ub")) glColor4ub((GLubyte)(r * 255), (GLubyte)(g * 255), (GLubyte)(b * 255), 255); @@ -433,8 +863,49 @@ static void usage(void) { " --hold MS keep the window up this long before exit (default 2000)\n" " --nofinish no glFinish between clear and draw\n"); printf(" --scene NAME depth | stencil | alphatest | blend | blendsmooth |\n" - " lit | litlocal | twoside | fog (see source)\n" - " --depthfunc F never less equal lequal greater notequal gequal always\n"); + " lit | litlocal | litrgb | dlist | twoside | fog | litcmat | litnorm |\n" + " clipplane | fogexp | fogexp2 | tex | texmod | copycolor |\n" + " copydepth (see source)\n" + " --depthfunc F never less equal lequal greater notequal gequal always\n" + " Raster state, applied after the clear (GL names in lower case, without GL_):\n" + " --alpha A vertex alpha (glColor4f)\n" + " --blendfunc S,D glBlendFunc + enable, e.g. src_alpha,one_minus_src_alpha\n" + " --alphafunc F,REF glAlphaFunc + enable, e.g. greater,0.5\n" + " --logicop OP glLogicOp + enable, e.g. xor\n" + " --stencilfunc F,REF[,MASK] glStencilFunc + enable (asks for a stencil visual)\n" + " --stencilop F,ZF,ZP glStencilOp, e.g. keep,keep,replace\n" + " --linestipple N,PAT glLineStipple + enable, e.g. 2,0x0f0f\n" + " --colormask RGBA glColorMask, 1 = write, e.g. 1010\n" + " --depthmask 0|1 glDepthMask\n" + " Texture scenes (tex, texmod):\n" + " --texfilter MIN,MAG e.g. linear_mipmap_linear,linear (mipmap filters load levels)\n" + " --texwrap S[,T] repeat | clamp | border (GL_CLAMP_TO_BORDER_SGIS); T as S if\n" + " not given\n" + " --texbcolor R,G,B,A GL_TEXTURE_BORDER_COLOR\n" + " --texborder the image has a one-texel border (magenta)\n" + " --texfog linear fog (grey, start 0, end 1: depth 0 clear, 1 fogged;\n" + " the quad at z 0.5 half fogged)\n" + " --texlines also textured lines (along the bottom) and points (top)\n" + " --texifmt NAME sized internal format (RGBA data): alpha4..16, luminance4..16,\n" + " luminance4_alpha4 .. luminance16_alpha16, intensity4..16, r3_g3_b2,\n" + " rgb4 rgb5 rgb8 rgb10 rgb12 rgb16, rgba2 rgba4 rgb5_a1 rgba8 rgb10_a2\n" + " rgba12 rgba16\n" + " --texsub glTexSubImage2D: a yellow 2x2 block at texel (2, 2) of level 0\n" + " --texsubrect X,Y,W,H the yellow block's place and size instead (implies --texsub)\n" + " --texcopy full|sub full: level 0 copied from the screen (glCopyTexImage2D) at\n" + " window (0.1w, 0.2h), where a red/green/blue/white 2x2 grid of\n" + " W/2 x H/2 pixel squares is drawn first; sub: the grid's lower\n" + " left W/2 x H/2 square copied into texel (0, 0) (glCopyTexSubImage2D)\n" + " --texread read level 0 back (glGetTexImage, RGBA ubyte) and print\n" + " its first texels and whether it matches what was loaded\n" + " --texgen M obj | eye | sphere: S and T generated (obj/eye planes map\n" + " the quad to 0..1)\n" + " --texenv E replace | modulate | decal | blend\n" + " --texfmt F rgba | rgb | luminance | luminance_alpha | alpha | intensity\n" + " --texabgr RGBA texels sent as GL_ABGR_EXT (same image)\n" + " --texushort texels sent as GL_UNSIGNED_SHORT (same image)\n" + " --texsize W[xH] level 0 is W x H (default 8), texel (s, t) = (s*256/W, t*256/H, 0xa5)\n" + " (tex: --mono --rgb R,G,B sets the vertex colour, default white)\n"); printf(" --read M[,M...] after drawing, read back: ximage front back depth stencil\n" " --readrect X,Y,W,H area to read (GL window coords; default whole window)\n" " --readfmt rgba|abgr colour format for glReadPixels (default rgba)\n" @@ -523,12 +994,50 @@ static void parse_args(int argc, char **argv) { if (sscanf(next, "%dx%d", &win_w, &win_h) != 2) { fprintf(stderr, "bad --size\n"); exit(2); } } else if (!strcmp(a, "--hold")) { NEED(); hold_ms = atoi(next); } else if (!strcmp(a, "--nofinish")) finish_first = 0; + else if (!strcmp(a, "--texfilter")) { + GLenum f[2]; + NEED(); + if (parse_enums(next, f, 2) != 2) { fprintf(stderr, "bad --texfilter\n"); exit(2); } + tex_min = f[0]; tex_mag = f[1]; + } else if (!strcmp(a, "--texwrap")) { + GLenum wr[2]; + int k; + NEED(); + k = parse_enums(next, wr, 2); + tex_wrap = wr[0]; tex_wrap_t = k == 2 ? wr[1] : wr[0]; + } + else if (!strcmp(a, "--texbcolor")) { NEED(); if (!parse_floats(next, tex_bcolor, 4)) { fprintf(stderr, "bad --texbcolor\n"); exit(2); } } + else if (!strcmp(a, "--texborder")) tex_border = 1; + else if (!strcmp(a, "--texfog")) tex_fog = 1; + else if (!strcmp(a, "--texlines")) tex_lines = 1; + else if (!strcmp(a, "--texread")) tex_read = 1; + else if (!strcmp(a, "--texifmt")) { + unsigned k; + NEED(); + for (k = 0; k < sizeof(ifmt_names) / sizeof(ifmt_names[0]); k++) + if (!strcmp(ifmt_names[k].n, next)) tex_ifmt = ifmt_names[k].e; + if (!tex_ifmt) { fprintf(stderr, "unknown internal format %s\n", next); exit(2); } + } + else if (!strcmp(a, "--texsub")) tex_sub = 1; + else if (!strcmp(a, "--texsubrect")) { NEED(); if (!parse_ints(next, tex_subrect, 4)) { fprintf(stderr, "bad --texsubrect\n"); exit(2); } tex_sub = 1; } + else if (!strcmp(a, "--texcopy")) { NEED(); strncpy(tex_copy, next, sizeof(tex_copy) - 1); } + else if (!strcmp(a, "--texgen")) { NEED(); strncpy(tex_gen, next, sizeof(tex_gen) - 1); } + else if (!strcmp(a, "--texenv")) { NEED(); tex_env = enum_by_name(next); } + else if (!strcmp(a, "--texfmt")) { NEED(); tex_fmt = enum_by_name(next); } + else if (!strcmp(a, "--texabgr")) tex_abgr = 1; + else if (!strcmp(a, "--texushort")) tex_ushort = 1; + else if (!strcmp(a, "--texsize")) { + NEED(); + if (sscanf(next, "%dx%d", &tex_size, &tex_size_t) < 2) tex_size_t = tex_size; + if (tex_size < 1 || tex_size > 256 || (tex_size & (tex_size - 1)) || tex_size_t < 1 || tex_size_t > 256 || (tex_size_t & (tex_size_t - 1))) { fprintf(stderr, "bad --texsize\n"); exit(2); } + } else if (!strcmp(a, "--scene")) { NEED(); strncpy(scene, next, sizeof(scene) - 1); if (!strcmp(scene, "depth")) depth = 1; if (!strcmp(scene, "stencil")) want_stencil = 1; if (!strcmp(scene, "quadrants")) { depth = 1; want_stencil = 1; } + if (!strncmp(scene, "copy", 4)) depth = 1; } else if (!strcmp(a, "--read")) { char buf[64], *t; NEED(); @@ -548,6 +1057,29 @@ static void parse_args(int argc, char **argv) { else if (!strcmp(a, "--packrow")) { NEED(); pack_row = atoi(next); } else if (!strcmp(a, "--readout")) { NEED(); read_out = next; } else if (!strcmp(a, "--backclear")) { NEED(); if (!parse_floats(next, back_rgb, 3)) { fprintf(stderr, "bad --backclear\n"); exit(2); } } + else if (!strcmp(a, "--alpha")) { NEED(); vtx_alpha = (float)atof(next); } + else if (!strcmp(a, "--blendfunc")) { + GLenum e[2]; NEED(); + if (parse_enums(next, e, 2) != 2) { fprintf(stderr, "bad --blendfunc\n"); exit(2); } + blend_src = e[0]; blend_dst = e[1]; + } else if (!strcmp(a, "--alphafunc")) { + char f[32]; NEED(); + if (sscanf(next, "%31[^,],%f", f, &alpha_ref) != 2) { fprintf(stderr, "bad --alphafunc\n"); exit(2); } + alpha_func = enum_by_name(f); + } else if (!strcmp(a, "--logicop")) { NEED(); logic_op = enum_by_name(next); } + else if (!strcmp(a, "--stencilfunc")) { + char f[32]; NEED(); + if (sscanf(next, "%31[^,],%d,%i", f, &st_ref, &st_mask) < 2) { fprintf(stderr, "bad --stencilfunc\n"); exit(2); } + st_func = enum_by_name(f); + want_stencil = 1; + } else if (!strcmp(a, "--stencilop")) { + NEED(); + if (parse_enums(next, st_ops, 3) != 3) { fprintf(stderr, "bad --stencilop\n"); exit(2); } + } else if (!strcmp(a, "--linestipple")) { + NEED(); + if (sscanf(next, "%d,%i", &line_stip_factor, (int *)&line_stip_pattern) != 2) { fprintf(stderr, "bad --linestipple\n"); exit(2); } + } else if (!strcmp(a, "--colormask")) { NEED(); strncpy(color_mask, next, 4); } + else if (!strcmp(a, "--depthmask")) { NEED(); depth_mask = atoi(next); } else if (!strcmp(a, "--depthfunc")) { static const char *names[] = { "never", "less", "equal", "lequal", "greater", "notequal", "gequal", "always" }; NEED(); @@ -894,6 +1426,28 @@ int main(int argc, char **argv) { | (want_stencil ? GL_STENCIL_BUFFER_BIT : 0)); if (finish_first) glFinish(); /* separates clear from draw in the trace */ + /* ---- generic raster state (after the clear) ---- */ + if (blend_src) { glEnable(GL_BLEND); glBlendFunc(blend_src, blend_dst); } + if (alpha_func) { glEnable(GL_ALPHA_TEST); glAlphaFunc(alpha_func, alpha_ref); } + if (logic_op) { + /* GL 1.0's GL_LOGIC_OP is colour index only; RGBA needs 1.1's. */ +#ifdef GL_COLOR_LOGIC_OP + glEnable(GL_COLOR_LOGIC_OP); +#else + glEnable(GL_LOGIC_OP); +#endif + glLogicOp(logic_op); + } + if (st_func) { + glEnable(GL_STENCIL_TEST); + glStencilFunc(st_func, st_ref, (GLuint)st_mask); + glStencilOp(st_ops[0], st_ops[1], st_ops[2]); + } + if (line_stip_factor) { glEnable(GL_LINE_STIPPLE); glLineStipple(line_stip_factor, (GLushort)line_stip_pattern); } + if (color_mask[0]) + glColorMask(color_mask[0] == '1', color_mask[1] == '1', color_mask[2] == '1', color_mask[3] == '1'); + if (!depth_mask) glDepthMask(GL_FALSE); + /* ---- one primitive (or a scene) ---- */ geometry(mode, &verts, &nverts); if (scene[0]) draw_scene(w, h); diff --git a/test/gltest/irix.sh b/test/gltest/irix.sh index 6f6fab49..c40ad6cf 100755 --- a/test/gltest/irix.sh +++ b/test/gltest/irix.sh @@ -1,15 +1,17 @@ #!/bin/sh # Build the GL tests on IRIX with MIPSpro cc (stock IRIX has no bash). -# Usage: sh irix.sh [target...] targets: gltest glprim (default: both) +# Usage: sh irix.sh [target...] targets: gltest glprim xresize xclick (default: all) targets="$*" -[ -z "$targets" ] && targets="gltest glprim" +[ -z "$targets" ] && targets="gltest glprim xresize xclick" status=0 for t in $targets; do case $t in gltest) src=main.c; libs="-lX11 -lGL -lm" ;; glprim) src=glprim.c; libs="-lX11 -lGL -lm" ;; + xresize) src=xresize.c; libs="-lX11" ;; + xclick) src=xclick.c; libs="-lXtst -lXext -lX11" ;; *) echo "unknown target $t"; exit 2 ;; esac echo "cc -o $t $src" diff --git a/test/gltest/main.c b/test/gltest/main.c index fab8d637..98fb9166 100644 --- a/test/gltest/main.c +++ b/test/gltest/main.c @@ -331,6 +331,12 @@ int main(int argc, char *argv[]) { int repeat = 1; /* --repeat: timed runs, reported as min/median/max */ int depth = 0; /* --depth: enable depth testing (Z fill rate) */ int warmup = 0; /* --warmup: untimed runs before timing starts */ + int nodepth = 0; /* --nodepth: interactive mode without the depth test */ + int cull = 0; /* --cull: back-face culling */ + int frames = 0; /* --frames N: exit after N frames (0 = run until Esc) */ + int hold_ms = 0; /* --hold MS: keep the last frame up this long before exiting */ + float spin[3] = { 0.0f, 0.0f, 0.0f }; /* --spin: degrees per frame */ + int frame = 0; int i; Display *dpy; Window root; @@ -361,6 +367,20 @@ int main(int argc, char *argv[]) { } else if (strcmp(argv[i], "--warmup") == 0 && i + 1 < argc) { warmup = atoi(argv[++i]); if (warmup < 0) warmup = 0; + } else if (strcmp(argv[i], "--object") == 0 && i + 1 < argc) { + currentObject = atoi(argv[++i]) & 3; + } else if (strcmp(argv[i], "--rot") == 0 && i + 1 < argc) { + sscanf(argv[++i], "%f,%f,%f", &xRot, &yRot, &zRot); + } else if (strcmp(argv[i], "--spin") == 0 && i + 1 < argc) { + sscanf(argv[++i], "%f,%f,%f", &spin[0], &spin[1], &spin[2]); + } else if (strcmp(argv[i], "--nodepth") == 0) { + nodepth = 1; + } else if (strcmp(argv[i], "--cull") == 0) { + cull = 1; + } else if (strcmp(argv[i], "--hold") == 0 && i + 1 < argc) { + hold_ms = atoi(argv[++i]); + } else if (strcmp(argv[i], "--frames") == 0 && i + 1 < argc) { + frames = atoi(argv[++i]); } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { printf("usage: gltest [options]\n" " --bench N fill-rate benchmark: N full-screen quads\n" @@ -369,7 +389,14 @@ int main(int argc, char *argv[]) { " --repeat N run the benchmark N times, report min/median/max\n" " --warmup N N untimed runs first (lets the JIT compile)\n" " --depth enable depth testing (Z-buffered fill rate)\n" - " (no option) interactive spinning-cube mode\n" + " (no option) interactive mode (depth test on if the visual has Z)\n" + " --object N 0 triangle, 1 rectangle, 2 cube, 3 octahedron\n" + " --rot X,Y,Z initial rotation in degrees\n" + " --spin DX,DY,DZ rotate by this much every frame (no keyboard needed)\n" + " --nodepth interactive mode without the depth test\n" + " --cull back-face culling (glCullFace(GL_BACK))\n" + " --frames N exit after N frames\n" + " --hold MS with --frames: keep the last frame up MS ms first\n" "\n" "--tribench is the GFIFO-sensitive one; --bench is rasterizer-bound.\n"); exit(0); @@ -435,7 +462,14 @@ int main(int argc, char *argv[]) { } // Init OpenGL state - glEnable(GL_DEPTH_TEST); + if (nodepth) + glDisable(GL_DEPTH_TEST); + else + glEnable(GL_DEPTH_TEST); + if (cull) { + glCullFace(GL_BACK); + glEnable(GL_CULL_FACE); + } glShadeModel(GL_SMOOTH); glClearColor(0.4f, 0.1f, 0.6f, 1.0f); // Purple background @@ -533,7 +567,23 @@ int main(int argc, char *argv[]) { } glFlush(); - + xRot += spin[0]; + yRot += spin[1]; + zRot += spin[2]; + if (frames > 0 && ++frame >= frames) { + glFinish(); + /* IRIX usleep() rejects a second or more. */ + if (hold_ms >= 1000) + sleep(hold_ms / 1000); + if (hold_ms % 1000) + usleep((hold_ms % 1000) * 1000); + glXMakeCurrent(dpy, None, NULL); + glXDestroyContext(dpy, glc); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; + } + // Sleep for a short time to prevent maxing out the CPU (~60 FPS cap) usleep(16000); } diff --git a/test/gltest/texsweep.sh b/test/gltest/texsweep.sh new file mode 100644 index 00000000..77a890e6 --- /dev/null +++ b/test/gltest/texsweep.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# texsweep.sh: run in the guest (cd to glprim's directory first). Every +# sized internal texture format through glprim --scene tex --texread (load, +# draw, glGetTexImage), then the wrap-mode / border cases, the sub-image +# and screen-copy cases. Prints glprim's texread / error lines; watch the +# window, or take screenshots during --hold, for the images. +H=${HOLD:-1500} +for f in alpha4 alpha8 alpha12 alpha16 luminance4 luminance8 luminance12 luminance16 \ + luminance4_alpha4 luminance6_alpha2 luminance8_alpha8 luminance12_alpha4 \ + luminance12_alpha12 luminance16_alpha16 intensity4 intensity8 intensity12 \ + intensity16 r3_g3_b2 rgb4 rgb5 rgb8 rgb10 rgb12 rgb16 rgba2 rgba4 rgb5_a1 \ + rgba8 rgb10_a2 rgba12 rgba16; do + echo "$f: `./glprim -p none --scene tex --texifmt $f --texread --hold $H 2>&1 | egrep 'texread|error'`" +done +for w in repeat,repeat clamp,repeat repeat,clamp clamp,clamp border,border clamp,border; do + echo "wrap $w"; ./glprim -p none --scene tex --texwrap $w --texfilter linear,linear --hold $H >/dev/null 2>&1 + echo "wrap $w, border"; ./glprim -p none --scene tex --texwrap $w --texborder --texfilter linear,linear --hold $H >/dev/null 2>&1 +done +echo "border colour"; ./glprim -p none --scene tex --texwrap clamp --texbcolor 1,1,0,1 --texfilter linear,linear --hold $H >/dev/null 2>&1 +echo "sub-image: `./glprim -p none --scene tex --texsize 64 --texsubrect 32,8,32,8 --texread --hold $H 2>&1 | egrep 'texsub|texread'`" +echo "copy full: `./glprim -p none --scene tex --texcopy full --hold $H 2>&1 | egrep 'texcopy'`" +echo "copy sub: `./glprim -p none --scene tex --texsize 64 --texcopy sub --hold $H 2>&1 | egrep 'texcopy'`" +echo "copy pixels"; ./glprim -p none --scene copycolor --smooth --hold $H >/dev/null 2>&1 +echo "copy depth"; ./glprim -p none --scene copydepth --smooth --hold $H >/dev/null 2>&1 diff --git a/test/gltest/xclick.c b/test/gltest/xclick.c new file mode 100644 index 00000000..6c4480a0 --- /dev/null +++ b/test/gltest/xclick.c @@ -0,0 +1,77 @@ +/* + * xclick: click (or press a key) in a window found by name, through XTest, + * so demos that wait for input can be driven from a script. + * + * xclick NAME [BUTTON [X Y]] click BUTTON (default 1) at X,Y in the + * window (default: its centre) + * xclick NAME key KEYSYM press and release a key (e.g. space) + * xclick NAME 0 X Y only move the pointer there (to press + * and hold buttons by other means) + * xclick NAME raise raise the window to the top + * + * NAME matches the start of WM_NAME, as in xresize. + */ +#include +#include +#include +#include +#include +#include + +static Window find(Display *dpy, Window w, const char *name) { + Window root, parent, *kids = NULL, hit = 0; + unsigned n = 0, i; + char *wn = NULL; + if (XFetchName(dpy, w, &wn) && wn) { + int match = !strncmp(wn, name, strlen(name)); + XFree(wn); + if (match) return w; + } + if (!XQueryTree(dpy, w, &root, &parent, &kids, &n)) return 0; + for (i = 0; i < n && !hit; i++) hit = find(dpy, kids[i], name); + if (kids) XFree(kids); + return hit; +} + +int main(int argc, char **argv) { + Display *dpy; + Window w, child; + XWindowAttributes wa; + int x, y, rx, ry; + if (argc < 2) { + fprintf(stderr, "usage: xclick NAME [BUTTON [X Y]] | xclick NAME key KEYSYM\n"); + return 2; + } + dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "cannot open display\n"); return 1; } + w = find(dpy, DefaultRootWindow(dpy), argv[1]); + if (!w) { fprintf(stderr, "no window named %s\n", argv[1]); return 1; } + if (argc > 2 && !strcmp(argv[2], "raise")) { + XRaiseWindow(dpy, w); + XSync(dpy, False); + printf("xclick: raised 0x%lx\n", (unsigned long)w); + return 0; + } + XGetWindowAttributes(dpy, w, &wa); + x = wa.width / 2; + y = wa.height / 2; + if (argc > 4) { x = atoi(argv[3]); y = atoi(argv[4]); } + XTranslateCoordinates(dpy, w, DefaultRootWindow(dpy), x, y, &rx, &ry, &child); + XTestFakeMotionEvent(dpy, -1, rx, ry, 0); + if (argc > 3 && !strcmp(argv[2], "key")) { + KeyCode kc = XKeysymToKeycode(dpy, XStringToKeysym(argv[3])); + if (!kc) { fprintf(stderr, "unknown keysym %s\n", argv[3]); return 1; } + XTestFakeKeyEvent(dpy, kc, True, 0); + XTestFakeKeyEvent(dpy, kc, False, 50); + } else { + unsigned button = argc > 2 ? (unsigned)atoi(argv[2]) : 1; + if (button) { + XTestFakeButtonEvent(dpy, button, True, 0); + XTestFakeButtonEvent(dpy, button, False, 50); + } + } + XSync(dpy, False); + printf("xclick: window 0x%lx at %d,%d\n", (unsigned long)w, rx, ry); + XCloseDisplay(dpy); + return 0; +} diff --git a/test/gltest/xresize.c b/test/gltest/xresize.c new file mode 100644 index 00000000..85aa25ba --- /dev/null +++ b/test/gltest/xresize.c @@ -0,0 +1,65 @@ +/* + * xresize: resize a window found by name, repeatedly, to exercise window + * reconfiguration under a running GL program (scripted, no mouse needed). + * + * xresize NAME W1 H1 W2 H2 [COUNT] [DELAY_MS] + * + * Finds the first window whose WM_NAME starts with NAME (searching the + * whole tree), then alternates between W1xH1 and W2xH2 COUNT times (default + * 4), DELAY_MS apart (default 1500). Resizes the window manager frame's + * client, i.e. the application window itself. + */ +#include +#include +#include +#include +#include +#include + +static Window find(Display *dpy, Window w, const char *name) { + Window root, parent, *kids = NULL, hit = 0; + unsigned n = 0, i; + char *wn = NULL; + if (XFetchName(dpy, w, &wn) && wn) { + int match = !strncmp(wn, name, strlen(name)); + XFree(wn); + if (match) return w; + } + if (!XQueryTree(dpy, w, &root, &parent, &kids, &n)) return 0; + for (i = 0; i < n && !hit; i++) hit = find(dpy, kids[i], name); + if (kids) XFree(kids); + return hit; +} + +static void ms_sleep(int ms) { + if (ms >= 1000) sleep(ms / 1000); + if (ms % 1000) usleep((ms % 1000) * 1000); +} + +int main(int argc, char **argv) { + Display *dpy; + Window w; + int w1, h1, w2, h2, count = 4, delay = 1500, i; + if (argc < 6) { + fprintf(stderr, "usage: xresize NAME W1 H1 W2 H2 [COUNT] [DELAY_MS]\n"); + return 2; + } + w1 = atoi(argv[2]); h1 = atoi(argv[3]); w2 = atoi(argv[4]); h2 = atoi(argv[5]); + if (argc > 6) count = atoi(argv[6]); + if (argc > 7) delay = atoi(argv[7]); + dpy = XOpenDisplay(NULL); + if (!dpy) { fprintf(stderr, "cannot open display\n"); return 1; } + w = find(dpy, DefaultRootWindow(dpy), argv[1]); + if (!w) { fprintf(stderr, "no window named %s\n", argv[1]); return 1; } + printf("xresize: window 0x%lx\n", (unsigned long)w); + for (i = 0; i < count; i++) { + int even = (i % 2) == 0; + XResizeWindow(dpy, w, even ? w1 : w2, even ? h1 : h2); + XSync(dpy, False); + printf("xresize: %dx%d\n", even ? w1 : w2, even ? h1 : h2); + fflush(stdout); + ms_sleep(delay); + } + XCloseDisplay(dpy); + return 0; +} diff --git a/tools/analyze_decode_cache.py b/tools/analyze_decode_cache.py new file mode 100644 index 00000000..e84eb63f --- /dev/null +++ b/tools/analyze_decode_cache.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Analyze the IRIS decode cache binary to understand instruction word distribution.""" + +import struct +import sys +import os +from collections import Counter +from pathlib import Path + +def load_cache(path): + with open(path, 'rb') as f: + magic = f.read(4) + if magic != b'IRDP': + print(f"Bad magic: {magic}", file=sys.stderr) + sys.exit(1) + version = struct.unpack('B', f.read(1))[0] + count = struct.unpack('> 16 for w in words] + hi16_counts = Counter(hi16) + unique_hi16 = len(hi16_counts) + max_bucket = max(hi16_counts.values()) + avg_bucket = n / unique_hi16 + filled_slots = unique_hi16 + total_slots = 65536 + + print(f"\n=== Upper 16-bit index (hi16 → bucket) ===") + print(f" Unique hi16 values : {unique_hi16} / {total_slots} ({unique_hi16*100/total_slots:.1f}% filled)") + print(f" Max bucket size : {max_bucket}") + print(f" Avg bucket size : {avg_bucket:.2f}") + + bucket_size_dist = Counter(hi16_counts.values()) + print(f" Bucket size distribution:") + for size in sorted(bucket_size_dist): + print(f" {size:3d} entries: {bucket_size_dist[size]:5d} buckets") + + # --- upper N bits --- + print(f"\n=== Index width sweep (unique keys / total slots / max bucket) ===") + for bits in [8, 10, 12, 13, 14, 15, 16, 17, 18]: + shifted = [w >> (32 - bits) for w in words] + c = Counter(shifted) + slots = 1 << bits + print(f" top {bits:2d} bits: {len(c):6d} unique / {slots:6d} slots ({len(c)*100/slots:5.1f}% fill) max_bucket={max(c.values())} avg={n/len(c):.2f}") + + # --- opcode field (bits 31:26) --- + opcodes = Counter(w >> 26 for w in words) + print(f"\n=== Opcode distribution (bits 31:26) ===") + for op, cnt in sorted(opcodes.items(), key=lambda x: -x[1]): + bar = '#' * int(cnt * 40 / n) + print(f" op 0x{op:02x} ({op:2d}): {cnt:6d} ({cnt*100/n:5.1f}%) {bar}") + + # --- SPECIAL funct field (bits 5:0) when op==0 --- + specials = [w & 0x3f for w in words if (w >> 26) == 0] + if specials: + sc = Counter(specials) + print(f"\n=== SPECIAL funct (bits 5:0, op=0), {len(specials)} words ===") + for funct, cnt in sorted(sc.items(), key=lambda x: -x[1])[:20]: + print(f" funct 0x{funct:02x} ({funct:2d}): {cnt:5d} ({cnt*100/len(specials):5.1f}%)") + + # --- load/store immediate spread (bits 15:0 when op is load/store) --- + LOAD_STORE_OPS = {0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f, + 0x30,0x31,0x35,0x37,0x38,0x39,0x3d,0x3f} + ls_imms = [w & 0xffff for w in words if (w >> 26) in LOAD_STORE_OPS] + if ls_imms: + unique_imms = len(set(ls_imms)) + print(f"\n=== Load/store immediate (bits 15:0): {len(ls_imms)} words, {unique_imms} unique values ===") + + # --- how many words share hi16 with another word --- + shared = sum(v for v in hi16_counts.values() if v > 1) + print(f"\n=== Hi16 sharing ===") + print(f" Words in a shared hi16 bucket : {shared} ({shared*100/n:.1f}%)") + print(f" Words alone in their bucket : {n - shared} ({(n-shared)*100/n:.1f}%)") + + # --- perfect hash candidate: would hi16 table + linear scan work? --- + print(f"\n=== Hi16 table + linear scan feasibility ===") + print(f" Table size : {total_slots} entries × 8B (ptr+len) = {total_slots*8//1024} KB overhead") + print(f" Max scan len : {max_bucket} (worst case comparisons per lookup)") + print(f" 99th pct scan: ", end='') + sizes = sorted(hi16_counts.values()) + p99 = sizes[int(len(sizes)*0.99)] + print(f"{p99}") + +def analyze_hash(words, label, hashfn, table_size): + buckets = Counter(hashfn(w) % table_size for w in words) + n = len(words) + sizes = sorted(buckets.values()) + empty = table_size - len(buckets) + max_chain = max(sizes) + avg_chain = n / len(buckets) + p50 = sizes[len(sizes)//2] + p90 = sizes[int(len(sizes)*0.90)] + p99 = sizes[int(len(sizes)*0.99)] + # Expected comparisons per lookup (avg chain length for occupied slots, + # weighted by lookup frequency assuming uniform access) + # E[comparisons] = sum(k * (k/n)) for each bucket of size k = sum(k^2)/n + expected_cmps = sum(v*v for v in buckets.values()) / n + print(f"\n=== Hash: {label}, table={table_size} ===") + print(f" Occupied slots : {len(buckets)} / {table_size} ({len(buckets)*100/table_size:.1f}%)") + print(f" Empty slots : {empty} ({empty*100/table_size:.1f}%)") + print(f" Max chain : {max_chain}") + print(f" Avg chain : {avg_chain:.2f}") + print(f" p50/p90/p99 : {p50} / {p90} / {p99}") + print(f" E[comparisons] : {expected_cmps:.2f} (lower=better, 1.0=perfect)") + +if __name__ == '__main__': + path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home() / '.iris' / 'decode-cache.bin' + words = load_cache(path) + analyze(words) + + print("\n" + "="*60) + print("HASH QUALITY ANALYSIS") + print("="*60) + + hashes = [ + ("raw ^ (raw>>16)", lambda w: w ^ (w >> 16), 65536), + ("raw ^ (raw>>16) 128K", lambda w: w ^ (w >> 16), 131072), + ("(raw>>16) only", lambda w: w >> 16, 65536), + ("raw * 0x9e3779b9 >> 16", lambda w: (w * 0x9e3779b9 & 0xffffffff) >> 16, 65536), + ("raw * 0x9e3779b9 >> 15", lambda w: (w * 0x9e3779b9 & 0xffffffff) >> 15, 131072), + ("(raw^(raw>>13)^(raw>>26))",lambda w: (w ^ (w>>13) ^ (w>>26)) & 0xffff, 65536), + ("fibhash32 >> 16", lambda w: ((w * 0x61C88647) & 0xffffffff) >> 16, 65536), + ] + for label, fn, size in hashes: + analyze_hash(words, label, fn, size) diff --git a/tools/analyze_rex3_buslog.py b/tools/analyze_rex3_buslog.py new file mode 100644 index 00000000..b0482bb8 --- /dev/null +++ b/tools/analyze_rex3_buslog.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Analyse a `rex buslog on` log (`reg=XXXX(NAME) val=...` format). + +Reconstructs REX3 register state and reports, per GO, the effective DRAWMODE0/1, +span geometry and per-vertex colour/alpha, so you can see what GL asked for. + +Internal coordinate format is 21.11 fixed point (see src/dev/ng1/rex3.rs:581-600): + XSTARTI/XYSTARTI : integer, << 11 + XSTARTF/XENDF : IEEE float on the wire; HW masks to mantissa bits 22:7 +Colours are o12.11 (24-bit), integer part = val >> 11. +""" +import re, sys +from collections import Counter + +LOG = sys.argv[1] if len(sys.argv) > 1 else "rex3.log" +def arg(n, d): + return int(sys.argv[sys.argv.index(n)+1]) if n in sys.argv else d + +BIAS = 4096 +OPCODES = {0:'NOOP',1:'READ',2:'DRAW',3:'SCR2SCR'} +ADRMODES = {0:'SPAN',1:'BLOCK',2:'ILINE',3:'FLINE',4:'ALINE'} +PLANES = {0:'NONE',1:'RGB',2:'RGBA',4:'OLAY',5:'PUP',6:'CID'} +DEPTHS = {0:'4bpp',1:'8bpp',2:'12bpp',3:'24bpp'} +LOGICOPS = {0:'ZERO',1:'AND',2:'ANDR',3:'SRC',4:'ANDI',5:'DST',6:'XOR',7:'OR', + 8:'NOR',9:'XNOR',10:'NDST',11:'ORR',12:'NSRC',13:'ORI',14:'NAND',15:'ONE'} +# Per REX3 spec Tables 13/14. SFACTOR 010/011 = dest color; DFACTOR 010/011 = source +# color; 100/101 = source alpha in both. 110/111 undefined. +SFACTOR_NAMES = ['ZERO','ONE','DC','MDC','SA','MSA','?6','?7'] +DFACTOR_NAMES = ['ZERO','ONE','SC','MSC','SA','MSA','?6','?7'] +CMPFN = {0:'never',1:'<',2:'==',3:'<=',4:'>',5:'!=',6:'>=',7:'always'} + +def bit(v,n): return (v>>n)&1 +def bits(v,hi,lo): return (v>>lo)&((1<<(hi-lo+1))-1) +def from_f(v): return (v & 0x007fff80) # 21.11 internal +def from_i(v): return ((v & 0xffff) - (0x10000 if v & 0x8000 else 0)) << 11 +def px(v): return v / 2048.0 # 21.11 -> pixels (pre-bias) +def col(v): return (v & 0xffffff) / 2048.0 # o12.11 -> 0..4095-ish + +def dec_dm0(v): + f=[n for b,n in [(5,'DOSETUP'),(6,'COLORHOST'),(7,'ALPHAHOST'),(8,'STOPONX'), + (9,'STOPONY'),(10,'SKIPFIRST'),(11,'SKIPLAST'),(12,'ENZPAT'),(13,'ENLSPAT'), + (14,'LSADVLAST'),(15,'LEN32'),(16,'ZPOPAQUE'),(17,'LSOPAQUE'),(18,'SHADE'), + (19,'LRONLY'),(20,'XYOFFSET'),(21,'CICLAMP'),(22,'ENDPTFILT'),(23,'YSTRIDE')] if bit(v,b)] + return f"{OPCODES.get(bits(v,1,0),'?')} {ADRMODES.get(bits(v,4,2),'?')} {' '.join(f)}" + +def dec_dm1(v): + f=[n for b,n in [(5,'DBLSRC'),(6,'YFLIP'),(7,'RWPACKED'),(10,'RWDOUBLE'),(11,'SWAPEND')] if bit(v,b)] + f.append('RGB' if bit(v,15) else 'CI') + if bit(v,16): f.append('DITHER') + if bit(v,17): f.append('FASTCLEAR') + if bit(v,18): f.append(f'BLEND({SFACTOR_NAMES[bits(v,21,19)]}+{DFACTOR_NAMES[bits(v,24,22)]})') + for b,n in [(25,'BACKBLEND'),(26,'PREFETCH'),(27,'BLENDALPHA')]: + if bit(v,b): f.append(n) + return (f"planes={PLANES.get(bits(v,2,0),'?')} {DEPTHS.get(bits(v,4,3),'?')} " + f"afn:{CMPFN[bits(v,14,12)]} logop={LOGICOPS.get(bits(v,31,28),'?')} {' '.join(f)}") + +LINE = re.compile(r'reg=([0-9a-f]+)\(([A-Z0-9_]+)\) val=([0-9a-f]+)') +R = dict(DM1=0x0000, DM0=0x0004, ALPHAREF=0x0020, XSTARTF=0x0138, YSTARTF=0x013c, + XENDF=0x0140, YENDF=0x0144, XSTARTI=0x0148, XYSTARTI=0x0150, + RED=0x0200, ALPHA=0x0204, GRN=0x0208, BLUE=0x020c, WRMASK=0x0220) + +st, gos, pending = {}, [], [] +n_go = 0 +lo, hi = arg('--from',0), arg('--to', 1<<62) + +for line in open(LOG): + m = LINE.search(line) + if not m: continue + off, name, val = int(m.group(1),16), m.group(2), int(m.group(3),16) & 0xffffffff + is_go = ' GO ' in line + st[off] = val + if not is_go: + pending.append(name); continue + n_go += 1 + if lo <= n_go <= hi: + # x start: XYSTARTI (packed) or XSTARTI wins depending on which came last; + # both write the same internal xstart, so reconstruct from whichever is set. + xs = from_i(st.get(R['XSTARTI'],0)) + ys = from_i(st.get(R['XYSTARTI'],0) & 0xffff) + xe = from_f(st.get(R['XENDF'],0)) + gos.append(dict(n=n_go, dm0=st.get(R['DM0'],0), dm1=st.get(R['DM1'],0), + xs=xs, ys=ys, xe=xe, + r=st.get(R['RED'],0), g=st.get(R['GRN'],0), b=st.get(R['BLUE'],0), + a=st.get(R['ALPHA'],0), aref=st.get(R['ALPHAREF'],0), + setup=list(pending))) + pending = [] + +print(f"Total GOs: {n_go}\n") + +print("=== GO by (DRAWMODE0, DRAWMODE1) ===") +for (d0,d1),c in Counter((g['dm0'],g['dm1']) for g in gos).most_common(): + print(f"[{c:7d}x] dm0={d0:#010x} dm1={d1:#010x}") + print(f" {dec_dm0(d0)}") + print(f" {dec_dm1(d1)}") + +print("\n=== DRAW SPAN width in pixels (xend - xstart) ===") +w = Counter() +for g in gos: + if bits(g['dm0'],1,0)==2 and bits(g['dm0'],4,2)==0: + w[round(px(g['xe']) - px(g['xs']))] += 1 +for width,c in sorted(w.items())[:25]: + print(f" {width:6d} px : {c:8d} spans") +print(f" ({len(w)} distinct widths over {sum(w.values())} spans)") + +print("\n=== Register writes preceding each GO (top 12) ===") +for s,c in Counter(','.join(g['setup']) for g in gos).most_common(12): + print(f" [{c:7d}x] {s or '(nothing — GO with no new state)'}") + +print("\n=== ALPHA values seen at DRAW SPAN (top 15) ===") +for a,c in Counter(g['a'] for g in gos if bits(g['dm0'],1,0)==2).most_common(15): + print(f" [{c:7d}x] raw={a:#010x} alpha={col(a):8.2f} (/4095 = {col(a)/4095:.3f})") +print("\nALPHAREF values:", {hex(k):v for k,v in Counter(g['aref'] for g in gos).items()}) + +# --- geometry + mode correlation: which draws cover which screen area --- +print("\n=== Screen coverage by DRAWMODE1 (physical px, bias 4096 removed) ===") +import collections +agg = collections.defaultdict(lambda: [1e9,1e9,-1e9,-1e9,0]) +for g in gos: + if bits(g['dm0'],1,0) != 2: continue + x0 = px(g['xs']) - BIAS; x1 = px(g['xe']) - BIAS; y = px(g['ys']) - BIAS + if not (-100 < x0 < 2000 and -100 < y < 1200): continue + a = agg[g['dm1']] + a[0]=min(a[0],x0); a[1]=min(a[1],y); a[2]=max(a[2],x1); a[3]=max(a[3],y); a[4]+=1 +for dm1,(x0,y0,x1,y1,c) in sorted(agg.items(), key=lambda kv:-kv[1][4]): + print(f"dm1={dm1:#010x} [{c:7d} spans] bbox x {x0:7.1f}..{x1:7.1f} y {y0:6.1f}..{y1:6.1f}") + print(f" {dec_dm1(dm1)}") + +print("\n=== alpha=0 spans that still get drawn (blend on, cmp=always) ===") +n_bad = n_ok = 0 +for g in gos: + if bits(g['dm0'],1,0)!=2: continue + a_stored = g['a'] & 0xFFFFF + a_int = (a_stored>>11)&0x1FF + alpha = 0 if (a_stored & (1<<31)) or a_int>=0x180 else min(a_int,0xFF) + if alpha != 0: continue + if bit(g['dm1'],18) and bits(g['dm1'],14,12)==7: n_bad += 1 + else: n_ok += 1 +print(f" alpha==0 spans with blend+cmp:always (rely on blend to vanish): {n_bad}") +print(f" alpha==0 spans otherwise (alpha test or no blend) : {n_ok}") diff --git a/tools/analyze_rex3_raw.py b/tools/analyze_rex3_raw.py new file mode 100644 index 00000000..79074cb6 --- /dev/null +++ b/tools/analyze_rex3_raw.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Analyse a `log rex ` raw REX3 device log. + +Unlike the GFIFO buslog (see analyze_rex3_buslog.py), this log carries both the +CPU-side `Write32` (before the GFIFO) and the `Process` (after it), plus the +emulator's own decoded Draw/Coords lines. That lets us check whether a draw +executed against the DRAWMODE1 the CPU intended, or against a stale one. + +Usage: + python3 tools/analyze_rex3_raw.py raw.log [--bbox X0 Y0 X1 Y1] [--limit N] +""" +import re, sys +from collections import Counter, defaultdict + +LOG = sys.argv[1] if len(sys.argv) > 1 else "raw.log" +def flag(n): return n in sys.argv +def arg(n, d): + return int(sys.argv[sys.argv.index(n)+1]) if n in sys.argv else d + +W = re.compile(r'REX3 Write32: Offset ([0-9a-f]+) \(Reg ([0-9a-f]+) ([A-Z0-9_]+)\) Val ([0-9a-f]+)') +P = re.compile(r'REX3 Process: Offset ([0-9a-f]+) \(([A-Z0-9_]+)\) Val ([0-9a-f]+)') +D = re.compile(r'REX3 Draw: (\S+) (\S+) Mode0=([0-9a-f]+) Mode1=([0-9a-f]+)') +C = re.compile(r'Coords: Start\(([-\d.]+), ([-\d.]+)\) End\(([-\d.]+), ([-\d.]+)\)') + +def bit(v,n): return (v>>n)&1 +def bits(v,hi,lo): return (v>>lo)&((1<<(hi-lo+1))-1) +def clamp(c): + if c & (1<<31): return 0 + v=(c>>11)&0x1FF + return 0 if v>=0x180 else (0xFF if v>0xFF else v) +def alpha_of(raw): return clamp(raw & 0xFFFFF) + +SFN = ['ZERO','ONE','DC','MDC','SA','MSA','?6','?7'] +DFN = ['ZERO','ONE','SC','MSC','SA','MSA','?6','?7'] +def dm1_str(v): + s = f"planes={bits(v,2,0)} depth={bits(v,4,3)} cmp={bits(v,14,12)}" + s += " BLEND(%s+%s)" % (SFN[bits(v,21,19)], DFN[bits(v,24,22)]) if bit(v,18) else " noblend" + if bit(v,16): s += " DITHER" + if bit(v,5): s += " DBLSRC" + s += f" logicop={bits(v,31,28)}" + return s + +# --- pass 1: walk the log, tracking CPU-side vs processed DRAWMODE1 --- +cpu_dm1 = None # last DRAWMODE1 the CPU wrote (Write32) +proc_dm1 = None # last DRAWMODE1 the GFIFO processed +proc_alpha = None # last COLORALPHA processed +draws = [] +stale = 0 + +for line in open(LOG): + m = W.search(line) + if m: + reg = int(m.group(2),16); val = int(m.group(4),16) + if reg == 0x0000: cpu_dm1 = val + continue + m = P.search(line) + if m: + off = int(m.group(1),16); val = int(m.group(3),16) + if off == 0x0000: proc_dm1 = val + elif off == 0x0204: proc_alpha = val + continue + m = D.search(line) + if m: + adr, op, dm0, dm1 = m.group(1), m.group(2), int(m.group(3),16), int(m.group(4),16) + draws.append(dict(adr=adr, op=op, dm0=dm0, dm1=dm1, + a=alpha_of(proc_alpha) if proc_alpha is not None else None, + cpu_dm1=cpu_dm1, x0=None)) + if cpu_dm1 is not None and cpu_dm1 != dm1: + stale += 1 + continue + m = C.search(line) + if m and draws: + draws[-1]['x0']=float(m.group(1)); draws[-1]['y0']=float(m.group(2)) + draws[-1]['x1']=float(m.group(3)); draws[-1]['y1']=float(m.group(4)) + +print(f"Draws: {len(draws)} draws whose Mode1 != last CPU-written DRAWMODE1: {stale}\n") + +print("=== Draws by (adrmode, op, Mode1) ===") +for (adr,op,dm1),c in Counter((d['adr'],d['op'],d['dm1']) for d in draws).most_common(12): + print(f"[{c:7d}x] {adr:8s} {op:8s} dm1={dm1:#010x} {dm1_str(dm1)}") + +print("\n=== Alpha distribution per Mode1 (DRAW only) ===") +per = defaultdict(Counter) +for d in draws: + if d['op']=='DRAW' and d['a'] is not None: + per[d['dm1']][d['a']] += 1 +for dm1, cnt in sorted(per.items(), key=lambda kv:-sum(kv[1].values())): + tot=sum(cnt.values()); a0=cnt.get(0,0) + print(f"dm1={dm1:#010x} n={tot:7d} alpha==0: {a0:7d} ({100*a0/tot:5.1f}%) {dm1_str(dm1)}") + top=', '.join(f"a={a}:{n}" for a,n in cnt.most_common(6)) + print(f" {top}") + +# --- geometry: bounding box per Mode1, on-screen draws only --- +print("\n=== On-screen bounding box per Mode1 ===") +bb=defaultdict(lambda:[1e9,1e9,-1e9,-1e9,0]) +for d in draws: + if d.get('x0') is None: continue + b=bb[d['dm1']] + b[0]=min(b[0],d['x0'],d['x1']); b[1]=min(b[1],d['y0'],d['y1']) + b[2]=max(b[2],d['x0'],d['x1']); b[3]=max(b[3],d['y0'],d['y1']); b[4]+=1 +for dm1,b in sorted(bb.items(), key=lambda kv:-kv[1][4]): + print(f"dm1={dm1:#010x} n={b[4]:7d} x {b[0]:8.1f}..{b[2]:8.1f} y {b[1]:8.1f}..{b[3]:8.1f}") diff --git a/tools/decode_jit_entries.py b/tools/decode_jit_entries.py new file mode 100644 index 00000000..01caf85d --- /dev/null +++ b/tools/decode_jit_entries.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Decode REX JIT compiled/dispatch log lines. + +Usage: + # Pipe emulator output: + iris ... 2>&1 | python3 decode_jit_entries.py + + # Or pass a log file: + python3 decode_jit_entries.py iris.log + +Reads lines matching "REX JIT: compiled dm0=... dm1=..." or +"REX JIT dispatch: dm0=... dm1=..." and prints decoded drawmode fields. +Also accepts bare hex pairs as arguments: + python3 decode_jit_entries.py 0x00001106 0x30097011 +""" + +import re, sys + +OPCODES = {0:'NOOP', 1:'READ', 2:'DRAW', 3:'SCR2SCR'} +ADRMODES = {0:'SPAN', 1:'BLOCK', 2:'ILINE', 3:'FLINE', 4:'ALINE'} +PLANES = {0:'NONE', 1:'RGB', 2:'RGBA', 4:'OLAY', 5:'PUP', 6:'CID'} +DEPTHS = {0:'4bpp', 1:'8bpp', 2:'12bpp', 3:'24bpp'} +LOGICOPS = {0:'ZERO',1:'AND',2:'ANDR',3:'SRC',4:'ANDI',5:'DST',6:'XOR',7:'OR', + 8:'NOR',9:'XNOR',10:'NDST',11:'ORR',12:'NSRC',13:'ORI',14:'NAND',15:'ONE'} +SF_NAMES = ['ZERO','ONE','DCOL','1-DCOL','DALPHA','1-DALPHA','SALPHA','1-SALPHA'] + + +def decode_dm0(v): + op = OPCODES.get(v & 3, '?') + adr = ADRMODES.get((v >> 2) & 7, '?') + flags = [] + if v & (1 << 5): flags.append('DOSETUP') + if v & (1 << 6): flags.append('COLORHOST') + if v & (1 << 7): flags.append('ALPHAHOST') + if v & (1 << 8): flags.append('STOPONX') + if v & (1 << 9): flags.append('STOPONY') + if v & (1 << 10): flags.append('SKIPFIRST') + if v & (1 << 11): flags.append('SKIPLAST') + if v & (1 << 12): flags.append('ENZPAT') + if v & (1 << 13): flags.append('ENLSPAT') + if v & (1 << 14): flags.append('LSADVLAST') + if v & (1 << 15): flags.append('LEN32') + if v & (1 << 16): flags.append('ZPOPAQUE') + if v & (1 << 17): flags.append('LSOPAQUE') + if v & (1 << 18): flags.append('SHADE') + if v & (1 << 19): flags.append('LRONLY') + if v & (1 << 20): flags.append('XYOFFSET') + if v & (1 << 21): flags.append('CICLAMP') + if v & (1 << 23): flags.append('YSTRIDE') + return f"{op} {adr}" + (f" [{' '.join(flags)}]" if flags else "") + + +def decode_dm1(v): + planes = PLANES.get(v & 7, '?') + depth = DEPTHS.get((v >> 3) & 3, '?') + hdepth = {0:'12bpp', 1:'8bpp', 2:'4bpp', 3:'32bpp'}.get((v >> 9) & 3, '?') + logop = LOGICOPS.get((v >> 28) & 0xf, '?') + flags = [] + if v & (1 << 5): flags.append('DBLSRC') + if v & (1 << 15): flags.append('RGB') + if v & (1 << 16): flags.append('DITHER') + if v & (1 << 17): flags.append('FASTCLEAR') + if v & (1 << 18): flags.append('BLEND') + if v & (1 << 25): flags.append('BACKBLEND') + sf = SF_NAMES[(v >> 19) & 7] + df = SF_NAMES[(v >> 22) & 7] + flag_str = (' [' + ' '.join(flags) + ']') if flags else '' + return f"planes={planes} {depth} host:{hdepth} logop={logop}{flag_str} sf={sf} df={df}" + + +def decode_pair(dm0, dm1, prefix=''): + print(f"{prefix}dm0={dm0:#010x} dm1={dm1:#010x}") + print(f" DM0: {decode_dm0(dm0)}") + print(f" DM1: {decode_dm1(dm1)}") + + +# --- bare hex pair mode --- +args = sys.argv[1:] +if len(args) >= 2 and args[0].startswith('0x'): + try: + dm0 = int(args[0], 16) + dm1 = int(args[1], 16) + decode_pair(dm0, dm1) + sys.exit(0) + except ValueError: + pass + +# --- log file / stdin mode --- +pattern = re.compile( + r'REX JIT.*?dm0=(0x[0-9a-fA-F]+)\s+dm1=(0x[0-9a-fA-F]+)' + r'(?:\s+\((\d+)B,\s*total:\s*(\d+)\))?' +) + +src = open(args[0]) if args else sys.stdin +seen = [] +for line in src: + m = pattern.search(line) + if not m: + continue + dm0 = int(m.group(1), 16) + dm1 = int(m.group(2), 16) + size = m.group(3) + total = m.group(4) + prefix = f"[#{total} {size}B] " if total else '' + decode_pair(dm0, dm1, prefix) + seen.append((dm0, dm1)) + print() + +if len(seen) > 1: + print(f"=== {len(seen)} entries ===") diff --git a/tools/decode_rex3_log.py b/tools/decode_rex3_log.py new file mode 100644 index 00000000..7da486e0 --- /dev/null +++ b/tools/decode_rex3_log.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Decode a rex3 bus log (produced by `rex buslog on`) and summarise GO draws. + +Usage: + python3 decode_rex3_log.py menu.log [--pup] [--rgb N] [--all] + +Options: + --pup Show PUP/OLAY/CID draws (default: shown) + --rgb N Show first N RGB draws (default: 40) + --all Show all draws regardless of plane +""" + +import re, sys +from collections import Counter + +LOG_FILE = sys.argv[1] if len(sys.argv) > 1 else "menu.log" + +OPCODES = {0:'NOOP', 1:'READ', 2:'DRAW', 3:'SCR2SCR'} +ADRMODES = {0:'SPAN', 1:'BLOCK', 2:'ILINE', 3:'FLINE', 4:'ALINE'} +PLANES = {0:'NONE', 1:'RGB', 2:'RGBA', 4:'OLAY', 5:'PUP', 6:'CID'} +DEPTHS = {0:'4bpp', 1:'8bpp', 2:'12bpp', 3:'24bpp'} +LOGICOPS = {0:'ZERO',1:'AND',2:'ANDR',3:'SRC',4:'ANDI',5:'DST',6:'XOR',7:'OR', + 8:'NOR',9:'XNOR',10:'NDST',11:'ORR',12:'NSRC',13:'ORI',14:'NAND',15:'ONE'} +# Per REX3 spec Tables 13/14. SFACTOR 010/011 = dest color; DFACTOR 010/011 = source +# color; 100/101 = source alpha in both. 110/111 undefined. +SFACTOR_NAMES = ['ZERO','ONE','DC','MDC','SA','MSA','?6','?7'] +DFACTOR_NAMES = ['ZERO','ONE','SC','MSC','SA','MSA','?6','?7'] + + +def _bit(v, n): + return (v >> n) & 1 + + +def _bits(v, hi, lo): + return (v >> lo) & ((1 << (hi - lo + 1)) - 1) + + +def decode_dm0(v): + # Bit layout mirrors DrawMode0 in src/dev/ng1/rex3.rs — keep in sync. + op = OPCODES.get(_bits(v, 1, 0), '?') + adr = ADRMODES.get(_bits(v, 4, 2), '?') + flags = [] + if _bit(v, 5): flags.append('DOSETUP') + if _bit(v, 6): flags.append('COLORHOST') + if _bit(v, 7): flags.append('ALPHAHOST') + if _bit(v, 8): flags.append('STOPONX') + if _bit(v, 9): flags.append('STOPONY') + if _bit(v, 10): flags.append('SKIPFIRST') + if _bit(v, 11): flags.append('SKIPLAST') + if _bit(v, 12): flags.append('ENZPAT') + if _bit(v, 13): flags.append('ENLSPAT') + if _bit(v, 14): flags.append('LSADVLAST') + if _bit(v, 15): flags.append('LEN32') + if _bit(v, 16): flags.append('ZPOPAQUE') + if _bit(v, 17): flags.append('LSOPAQUE') + if _bit(v, 18): flags.append('SHADE') + if _bit(v, 19): flags.append('LRONLY') + if _bit(v, 20): flags.append('XYOFFSET') + if _bit(v, 21): flags.append('CICLAMP') + if _bit(v, 22): flags.append('ENDPTFILT') + if _bit(v, 23): flags.append('YSTRIDE') + return f"{op} {adr} {' '.join(flags)}" + + +def decode_dm1(v): + # Bit layout mirrors DrawMode1 in src/dev/ng1/rex3.rs — keep in sync. + planes = PLANES.get(_bits(v, 2, 0), '?') + depth = DEPTHS.get(_bits(v, 4, 3), '?') + hdepth = {0: '12bpp', 1: '8bpp', 2: '4bpp', 3: '32bpp'}.get(_bits(v, 9, 8), '?') + cmp_ = _bits(v, 14, 12) + logop = LOGICOPS.get(_bits(v, 31, 28), '?') + sf = SFACTOR_NAMES[_bits(v, 21, 19)] + df = DFACTOR_NAMES[_bits(v, 24, 22)] + flags = [] + if _bit(v, 5): flags.append('DBLSRC') + if _bit(v, 6): flags.append('YFLIP') + if _bit(v, 7): flags.append('RWPACKED') + if _bit(v, 10): flags.append('RWDOUBLE') + if _bit(v, 11): flags.append('SWAPEND') + flags.append('RGB' if _bit(v, 15) else 'CI') + if _bit(v, 16): flags.append('DITHER') + if _bit(v, 17): flags.append('FASTCLEAR') + if _bit(v, 18): flags.append(f'BLEND({sf}+{df})') + if _bit(v, 25): flags.append('BACKBLEND') + if _bit(v, 26): flags.append('PREFETCH') + if _bit(v, 27): flags.append('BLENDALPHA') + return f"planes={planes} {depth} host:{hdepth} cmp:{cmp_} logicop={logop} {' '.join(flags)}" + + +def parse_log(path): + state = {} + draws = [] + with open(path) as f: + for line in f: + m = re.search(r'(?:Write32|Process).*Offset ([0-9a-fA-F]+).*Val ([0-9a-fA-F]+)', line) + if not m: + continue + off = int(m.group(1), 16) + val = int(m.group(2), 16) + reg = off & ~0x800 # strip GO bit + + # XYSTARTI/XYENDI can appear at offset 0x0150/0x0154 or 0x0950/0x0954 (GO variants) + state[reg] = val + + if off & 0x800: # GO bit set + dm0 = state.get(0x0004, 0) + dm1 = state.get(0x0000, 0) + xsi = state.get(0x0150, 0) + xei = state.get(0x0154, 0) + x0i = (xsi >> 16) & 0xffff + y0i = xsi & 0xffff + x1i = (xei >> 16) & 0xffff + y1i = xei & 0xffff + plane = PLANES.get(dm1 & 7, '?') + cr = state.get(0x0200, 0) # COLORRED + cg = state.get(0x0208, 0) # COLORGRN + cb = state.get(0x020c, 0) # COLORBLUE + cback = state.get(0x0018, 0) # COLORBACK + cvram = state.get(0x001c, 0) # COLORVRAM + wrmask = state.get(0x0220, 0xffffff) # WRMASK + draws.append(dict( + dm0=dm0, dm1=dm1, + x0=x0i, y0=y0i, x1=x1i, y1=y1i, + plane=plane, cr=cr, cg=cg, cb=cb, + colorback=cback, colorvram=cvram, wrmask=wrmask, + )) + return draws + + +def main(): + show_all = '--all' in sys.argv + show_pup = '--pup' in sys.argv or not show_all + rgb_limit = 40 + for i, a in enumerate(sys.argv): + if a == '--rgb' and i+1 < len(sys.argv): + rgb_limit = int(sys.argv[i+1]) + + draws = parse_log(LOG_FILE) + + print(f"Total GO draws: {len(draws)}\n") + plane_counts = Counter(d['plane'] for d in draws) + print("By plane:", dict(plane_counts)) + print() + + if show_pup or show_all: + pup_draws = [d for d in draws if d['plane'] in ('PUP','OLAY','CID')] + print(f"=== PUP/OLAY/CID draws ({len(pup_draws)}) ===") + for d in pup_draws: + print(f" {d['plane']} ({d['x0']},{d['y0']})-({d['x1']},{d['y1']}) " + f"dm0={d['dm0']:#010x} dm1={d['dm1']:#010x}") + print(f" {decode_dm0(d['dm0'])}") + print(f" {decode_dm1(d['dm1'])}") + print(f" colorvram={d['colorvram']:#010x} colorback={d['colorback']:#010x} " + f"cr={d['cr']:#010x} cg={d['cg']:#010x} cb={d['cb']:#010x} " + f"wrmask={d['wrmask']:#010x}") + print() + + rgb_draws = [d for d in draws if d['plane'] == 'RGB' or show_all] + print(f"=== RGB draws (showing first {rgb_limit} of {len(rgb_draws)}) ===") + for d in rgb_draws[:rgb_limit]: + print(f" {d['plane']} ({d['x0']},{d['y0']})-({d['x1']},{d['y1']}) " + f"dm0={d['dm0']:#010x} dm1={d['dm1']:#010x}") + print(f" {decode_dm0(d['dm0'])}") + print(f" {decode_dm1(d['dm1'])}") + print(f" colorvram={d['colorvram']:#010x} colorback={d['colorback']:#010x} " + f"cr={d['cr']:#010x} cg={d['cg']:#010x} cb={d['cb']:#010x} " + f"wrmask={d['wrmask']:#010x}") + + # Summary: unique (dm0, dm1) pairs used + print() + unique = sorted(set((d['dm0'], d['dm1']) for d in draws)) + print(f"=== Unique (dm0, dm1) pairs: {len(unique)} ===") + for dm0, dm1 in unique: + count = sum(1 for d in draws if d['dm0']==dm0 and d['dm1']==dm1) + print(f" [{count:4d}x] dm0={dm0:#010x} dm1={dm1:#010x} " + f"{decode_dm0(dm0)} | {decode_dm1(dm1)}") + + +if __name__ == '__main__': + main() diff --git a/tools/decode_vc2_ram.py b/tools/decode_vc2_ram.py new file mode 100755 index 00000000..18ef9123 --- /dev/null +++ b/tools/decode_vc2_ram.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 +""" +decode_vc2_ram.py - Decode VC2 RAM dump from 'vc2 ramdump' monitor command. + +Usage: + iris$ vc2 ramdump > vc2.dump + python3 tools/decode_vc2_ram.py vc2.dump + +Output: + - VT (Video Timing) table: each line entry decoded with signal names, + durations in pixel clocks, and cumulative pixel offsets. + - DID table: run-length entries per scanline. + +VC2 VT encoding (from VC2 datasheet): + Each state run is 1 or 2 words: + Word 1: bit15=EOL, bit14:8=duration(SRUN, 7 bits, units of 2px), bit7=SB/SC_absent, bit6:0=state_A + Word 2 (if present): bit15=EOL, bit14:8=state_B (7 bits), bit7=1 (always), bit6:0=state_C + + State A (bit positions, active-low _N signals): + bit0 = VIS_LN_VC_N + bit1 = HPOS_VC_N + bit2 = DSPLY_EN_RO_N + bit3 = SER_EN_RO_N + bit4 = TX_REQ_REX_N + bit5 = CSYNC_DAC_N + bit6 = CBLANK_DAC_N + + State B (bit positions): + bit0 = HBLANK_AB_N + bit1 = EOF_AB_N + bit2 = CBLANK_CMAP_N + bit3 = SET_TSC_REX_N + bit4 = ODDFIELD_VC_N + bit5 = EOF_VC_N + bit6 = VPOS_VC_N + + State C (bit positions): + bit0 = VERT_INT_REX_N + bit1 = VSYNC_ARC_N + bit2 = HSYNC_ARC_N + bit3 = CSYNC_ARC_N + bit4 = VERT_STAT_GIO_N + bit5 = spare + bit6 = CBLANK_XMAP_N +""" + +import sys +import argparse + +STATE_A_NAMES = [ + "VIS_LN", # bit0 + "HPOS", # bit1 + "DSPLY_EN", # bit2 + "SER_EN", # bit3 + "TX_REQ", # bit4 + "CSYNC_DAC", # bit5 + "CBLANK_DAC",# bit6 +] + +STATE_B_NAMES = [ + "HBLANK_AB", # bit0 + "EOF_AB", # bit1 + "CBLANK_CMAP", # bit2 + "SET_TSC", # bit3 + "ODDFIELD", # bit4 + "EOF_VC", # bit5 + "VPOS", # bit6 +] + +STATE_C_NAMES = [ + "VERT_INT", # bit0 + "VSYNC_ARC", # bit1 + "HSYNC_ARC", # bit2 + "CSYNC_ARC", # bit3 + "VERT_STAT_GIO", # bit4 + "spare", # bit5 + "CBLANK_XMAP", # bit6 +] + +def active_signals(val, names): + """Return list of asserted (active-low = bit clear) signal names.""" + result = [] + for i, name in enumerate(names): + if not (val & (1 << i)): + result.append(name) + return result + +def inactive_signals(val, names): + """Return list of deasserted signal names.""" + result = [] + for i, name in enumerate(names): + if val & (1 << i): + result.append(name + "_n") + return result + +def decode_state_run(w1, w2=None): + """Decode one state run. Returns (duration_px, state_a, state_b, state_c, eol).""" + eol = bool(w1 & 0x8000) + duration = (w1 >> 8) & 0x7F # in units of 2 pixel clocks + sb_sc_absent = bool(w1 & 0x0080) + state_a = w1 & 0x7F + + state_b = 0 + state_c = 0 + if not sb_sc_absent and w2 is not None: + state_b = (w2 >> 8) & 0x7F + state_c = w2 & 0x7F + + duration_px = duration * 2 + return duration_px, state_a, state_b, state_c, eol + +def sig_str(state, names, show_inactive=False): + active = active_signals(state, names) + parts = [f"\033[1m{s}\033[0m" for s in active] + if show_inactive: + inactive = [names[i] for i in range(7) if state & (1 << i)] + parts += [f"\033[2m{s}!\033[0m" for s in inactive] + return " ".join(parts) if parts else "-" + +def decode_line(ram, ptr, verbose=False): + """ + Decode one VT line starting at ptr. + Returns list of (px_start, duration_px, state_a, state_b, state_c, eol) and next_ptr. + """ + entries = [] + px = 0 + state_b, state_c = 0, 0 + safety = 0 + while True: + if ptr >= len(ram): + break + w1 = ram[ptr]; ptr += 1 + eol = bool(w1 & 0x8000) + duration = (w1 >> 8) & 0x7F + sb_sc_absent = bool(w1 & 0x0080) + state_a = w1 & 0x7F + + if not sb_sc_absent: + if ptr >= len(ram): + break + w2 = ram[ptr]; ptr += 1 + if w2 & 0x8000: + eol = True + state_b = (w2 >> 8) & 0x7F + state_c = w2 & 0x7F + # else reuse previous state_b, state_c + + duration_px = duration * 2 + entries.append((px, duration_px, state_a, state_b, state_c)) + px += duration_px + if eol: + break + safety += 1 + if safety > 4096: + print(" WARNING: line safety limit hit") + break + + # next line pointer follows the EOL entry + if ptr < len(ram): + next_ptr = ram[ptr] + ptr += 1 + else: + next_ptr = 0xFFFF + + return entries, next_ptr, ptr + +def print_line(entries, line_idx=None, indent=" "): + prefix = f" Line {line_idx}: " if line_idx is not None else indent + print(f"{indent}{'px_start':>8} {'dur':>5} | A: {'SIGNALS (active=asserted)':40} | B: {'':30} | C: {'':30}") + print(f"{indent}{'--------':>8} {'---':>5} | {'':41} | {'':31} | {'':31}") + for (px, dur, sa, sb, sc) in entries: + a_str = " ".join(active_signals(sa, STATE_A_NAMES)) or "-" + b_str = " ".join(active_signals(sb, STATE_B_NAMES)) or "-" + c_str = " ".join(active_signals(sc, STATE_C_NAMES)) or "-" + print(f"{indent}{px:8d} {dur:5d} | A: {a_str:40s} | B: {b_str:30s} | C: {c_str:30s}") + +def decode_vt(ram, frame_ptr, verbose=False): + print(f"\n=== VIDEO TIMING TABLE (frame_ptr=0x{frame_ptr:04x}) ===") + curr = frame_ptr + seq_idx = 0 + total_lines = 0 + safety = 0 + + while curr + 1 < len(ram): + line_seq_ptr = ram[curr] + line_count = ram[curr + 1] + if line_count == 0: + print(f"\n [seq {seq_idx}] End of frame (line_count=0)") + break + print(f"\n [seq {seq_idx}] line_seq_ptr=0x{line_seq_ptr:04x} repeat={line_count} lines") + + # Decode the unique lines in this sequence by following next_ptr links, + # stopping when we see a self-loop or have walked enough distinct lines. + # We decode each unique line once, then report the repeat count separately. + unique_lines = [] # list of (line_ptr, entries, next_ptr) + line_ptr = line_seq_ptr + visited_in_seq = set() + seq_safety = 0 + while True: + if line_ptr in visited_in_seq: + break # hit a cycle — done collecting unique lines + if line_ptr == 0xFFFF or line_ptr >= len(ram): + break + visited_in_seq.add(line_ptr) + entries, next_ptr, _ = decode_line(ram, line_ptr, verbose) + unique_lines.append((line_ptr, entries, next_ptr)) + if next_ptr == line_ptr: + break # self-loop + line_ptr = next_ptr + seq_safety += 1 + if seq_safety > 200: + print(" WARNING: sequence walk safety limit hit") + break + + n_unique = len(unique_lines) + + # Print each unique line once, with timing analysis. + for ui, (lp, entries, np) in enumerate(unique_lines): + total_px = sum(d for (_, d, _, _, _) in entries) + print(f" Line @0x{lp:04x}: {len(entries)} runs, total {total_px} px, next=0x{np:04x}") + if verbose: + print_line(entries, indent=" ") + else: + compact = [] + for (px, dur, sa, sb, sc) in entries: + a = " ".join(active_signals(sa, STATE_A_NAMES)) or "-" + c_cblank = "CBLANK_XMAP" if not (sc & 0x40) else "" + compact.append(f"@{px}+{dur}px [{a}]{' C:'+c_cblank if c_cblank else ''}") + print(f" " + " ".join(compact)) + _analyze_line_timing(entries) + + # Report how many times the sequence cycles to produce line_count lines. + if n_unique > 0: + full_cycles = line_count // n_unique + remainder = line_count % n_unique + if n_unique == 1: + print(f" (line repeats {line_count}x total)") + else: + print(f" ({n_unique} unique lines, {full_cycles} full cycles" + + (f" + {remainder} extra" if remainder else "") + f" = {line_count} lines total)") + + total_lines += line_count + curr += 2 + seq_idx += 1 + safety += 1 + if safety > 200: + print(" WARNING: frame table safety limit hit") + break + + print(f"\n Total lines in frame: {total_lines}") + +def _analyze_line_timing(entries): + """Print key signal transitions with pixel offsets for timing analysis.""" + key_events = [] + for (px, dur, sa, sb, sc) in entries: + active_a = active_signals(sa, STATE_A_NAMES) + active_c = active_signals(sc, STATE_C_NAMES) + if active_a or ("CBLANK_XMAP" in active_c): + key_events.append((px, dur, sa, sb, sc)) + + if not key_events: + return + + # Find key pixel offsets + ser_en_start = None + dsply_en_start = None + vis_ln_start = None + cblank_xmap_start = None + hpos_start = None + + for (px, dur, sa, sb, sc) in entries: + if not (sa & 0x08) and ser_en_start is None: ser_en_start = px # SER_EN active + if not (sa & 0x04) and dsply_en_start is None: dsply_en_start = px # DSPLY_EN active + if not (sa & 0x01) and vis_ln_start is None: vis_ln_start = px # VIS_LN active + if not (sc & 0x40) and cblank_xmap_start is None: cblank_xmap_start = px # CBLANK_XMAP active + if not (sa & 0x02) and hpos_start is None: hpos_start = px # HPOS active + + parts = [] + if hpos_start is not None: parts.append(f"HPOS@{hpos_start}") + if ser_en_start is not None: parts.append(f"SER_EN@{ser_en_start}") + if dsply_en_start is not None: parts.append(f"DSPLY_EN@{dsply_en_start}") + if vis_ln_start is not None: parts.append(f"VIS_LN@{vis_ln_start}") + if cblank_xmap_start is not None: parts.append(f"CBLANK_XMAP@{cblank_xmap_start}") + + if len(parts) > 1: + print(f" --> Timing: {' | '.join(parts)}") + # Show offsets between key signals + if ser_en_start is not None and dsply_en_start is not None: + delta = dsply_en_start - ser_en_start + print(f" SER_EN → DSPLY_EN: {delta:+d} px") + if dsply_en_start is not None and vis_ln_start is not None: + delta = vis_ln_start - dsply_en_start + print(f" DSPLY_EN → VIS_LN: {delta:+d} px") + if ser_en_start is not None and vis_ln_start is not None: + delta = vis_ln_start - ser_en_start + print(f" SER_EN → VIS_LN: {delta:+d} px ← VRAM shifts {delta} px before vis area") + if ser_en_start is not None and cblank_xmap_start is not None: + delta = cblank_xmap_start - ser_en_start + print(f" SER_EN → CBLANK_XMAP: {delta:+d} px") + +def decode_did(ram, did_ptr, scan_len, verbose=False): + print(f"\n=== DID TABLE (did_ptr=0x{did_ptr:04x}, scan_len={scan_len}) ===") + y = 0 + table_idx = did_ptr + safety = 0 + while y < 1200 and table_idx < len(ram): + line_ptr = ram[table_idx] + if line_ptr == 0xFFFF: + print(f" y={y}: END marker (0xFFFF)") + break + ptr = line_ptr + runs = [] + x = 0 + if ptr < len(ram): + entry = ram[ptr]; ptr += 1 + current_did = entry & 0x1F + while x < scan_len and ptr < len(ram): + next_entry = ram[ptr]; ptr += 1 + next_x_raw = (next_entry >> 5) & 0x7FF + next_did = next_entry & 0x1F + is_eol = next_x_raw == 0x7FF + next_x = scan_len if is_eol else next_x_raw + run_end = min(max(next_x, x), scan_len) + runs.append((x, run_end - x, current_did)) + x = run_end + current_did = next_did + if is_eol: + break + if verbose or y < 10: + run_str = " ".join(f"x{r[0]}+{r[1]}:DID{r[2]}" for r in runs) + print(f" y={y:4d} @0x{line_ptr:04x}: {run_str}") + elif y == 10: + print(f" ... (use -v to show all lines)") + y += 1 + table_idx += 1 + safety += 1 + if safety > 2000: + break + +def load_dump(path): + ram = {} + meta = {} + with open(path) as f: + for line in f: + line = line.strip() + if not line: + continue + if line.startswith('#'): + # Parse metadata comments + if '=' in line: + # e.g. "# VT_FRAME_PTR = 0042" + parts = line.lstrip('#').strip().split('=') + key = parts[0].strip() + val_str = parts[1].strip().split()[0] + try: + meta[key] = int(val_str, 16) + except ValueError: + pass + continue + # Data line: "addr: w0 w1 w2 ..." + colon = line.index(':') + addr = int(line[:colon], 16) + words = [int(w, 16) for w in line[colon+1:].split()] + for i, w in enumerate(words): + ram[addr + i] = w + + max_addr = max(ram.keys()) if ram else 0 + ram_list = [ram.get(i, 0) for i in range(max_addr + 1)] + return ram_list, meta + +def main(): + parser = argparse.ArgumentParser(description="Decode VC2 RAM dump (from 'vc2 ramdump')") + parser.add_argument("dump_file", help="Path to vc2 ramdump output") + parser.add_argument("-v", "--verbose", action="store_true", help="Show all line runs") + parser.add_argument("--vt-only", action="store_true", help="Only decode VT table") + parser.add_argument("--did-only", action="store_true", help="Only decode DID table") + parser.add_argument("--line", type=lambda x: int(x, 0), default=None, + help="Verbose decode of a specific line address (hex)") + args = parser.parse_args() + + ram, meta = load_dump(args.dump_file) + print(f"Loaded {len(ram)} words from {args.dump_file}") + print(f"Metadata: {meta}") + + vt_ptr = meta.get("VIDEO_ENTRY_PTR", meta.get("VT_FRAME_PTR", 0)) + did_ptr = meta.get("DID_ENTRY_PTR", 0) + scan_len_raw = meta.get("SCANLINE_LEN", 0) + scan_len = scan_len_raw >> 5 + + if args.line is not None: + entries, next_ptr, _ = decode_line(ram, args.line, verbose=True) + total_px = sum(d for (_, d, _, _, _) in entries) + print(f"\nLine @0x{args.line:04x}: {len(entries)} runs, {total_px} px, next=0x{next_ptr:04x}") + print_line(entries, indent=" ") + _analyze_line_timing(entries) + return + + if not args.did_only: + decode_vt(ram, vt_ptr, verbose=args.verbose) + + if not args.vt_only: + decode_did(ram, did_ptr, scan_len if scan_len > 0 else 1280, verbose=args.verbose) + +if __name__ == "__main__": + main() diff --git a/tools/glprim-case.py b/tools/glprim-case.py new file mode 100644 index 00000000..0b0aae1e --- /dev/null +++ b/tools/glprim-case.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Run one glprim case in the IRIX guest and check the result (GR2 GL loop). + +Steps: start `gr2 trace` on the IRIS monitor, run /tmp/gltest/glprim with the +given options in the background in the guest (tools/irix-sh.py), wait for it +to draw, probe pixels with `gr2 pix`, dump the framebuffer, crop the glprim +window to /.png, stop the trace (/.log). + +Probes are in GL window coordinates (origin bottom-left of the client area): + --probe 200,120=0000ff expect rgb (hex, +-tolerance per channel) + --probe 200,120 just print +Exit status: number of failed probes (0 = pass), 100+ on setup errors. + +Example: + tools/glprim-case.py tri-flat -- --hold 6000 + tools/glprim-case.py tri-smooth --probe 200,120=555555 -- --smooth +""" +import argparse +import os +import re +import socket +import subprocess +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +IRIX_SH = os.path.join(HERE, "irix-sh.py") + + +class Monitor: + def __init__(self, host="127.0.0.1", port=8888): + self.s = socket.create_connection((host, port), timeout=30) + self.buf = b"" + self._until_prompt() + + def _until_prompt(self): + while not re.search(rb"(^|\n)> $", self.buf): + chunk = self.s.recv(65536) + if not chunk: + raise ConnectionError("monitor closed") + self.buf += chunk + out, self.buf = self.buf, b"" + return out.decode("latin-1", "replace") + + def cmd(self, c): + self.s.sendall((c + "\n").encode()) + out = self._until_prompt() + return re.sub(r"\n?> $", "", out).strip() + + +def guest(*cmds, timeout=120): + r = subprocess.run([sys.executable, IRIX_SH, "--timeout", str(timeout), "run", "-k", *cmds], + capture_output=True, text=True, timeout=timeout + 60) + return r.returncode, r.stdout + r.stderr + + +def main(): + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("case") + p.add_argument("--out", default=os.environ.get("GLPRIM_OUT", "/tmp/glprim-cases")) + p.add_argument("--probe", action="append", default=[]) + p.add_argument("--tol", type=int, default=2) + p.add_argument("--settle", type=float, default=6.0, help="seconds to wait for the draw") + p.add_argument("--notrace", action="store_true") + argv = sys.argv[1:] + split = argv.index("--") if "--" in argv else len(argv) + a = p.parse_args(argv[:split]) + gargs = argv[split + 1:] + if not any(x == "--hold" for x in gargs): + gargs += ["--hold", "15000"] + os.makedirs(a.out, exist_ok=True) + log = os.path.join(a.out, a.case + ".log") + mon = Monitor() + if not a.notrace: + print(mon.cmd(f"gr2 trace {log} all")) + quoted = " ".join("'" + x.replace("'", "'\\''") + "'" for x in gargs) + rc, out = guest(f"cd /tmp/gltest && (./glprim {quoted} > /tmp/glprim.out 2>&1 &)", + f"sleep {int(a.settle)}; cat /tmp/glprim.out") + print(out.strip()) + m = re.search(r"window origin (-?\d+),(-?\d+) size (\d+)x(\d+)", out) + if not m: + print("glprim did not report a window (see output above)") + if not a.notrace: + mon.cmd("gr2 trace off") + return 101 + ox, oy, w, h = map(int, m.groups()) + fails = 0 + for pr in a.probe: + pos, _, want = pr.partition("=") + gx, gy = map(int, pos.split(",")) + dx, dy = ox + gx, oy + h - 1 - gy + res = mon.cmd(f"gr2 pix {dx} {dy}") + mm = re.search(r"rgb=([0-9a-f]{6})", res) + got = mm.group(1) if mm else "??????" + status = "" + if want: + gv = [int(got[i:i + 2], 16) for i in (0, 2, 4)] if mm else None + wv = [int(want[i:i + 2], 16) for i in (0, 2, 4)] + ok = gv is not None and all(abs(x - y) <= a.tol for x, y in zip(gv, wv)) + status = "ok " if ok else "FAIL" + fails += 0 if ok else 1 + print(f" {status} gl ({gx:4d},{gy:4d}) screen ({dx:4d},{dy:4d}) rgb={got} {('want ' + want) if want else ''}") + dump = os.path.join(a.out, a.case + "-fb") + mon.cmd(f"gr2 fbdump {dump}") + try: + from PIL import Image + im = Image.open(os.path.join(dump, "screen.png")) + im.crop((max(ox - 8, 0), max(oy - 8, 0), ox + w + 8, oy + h + 8)).save(os.path.join(a.out, a.case + ".png")) + print(f" window image: {os.path.join(a.out, a.case + '.png')}") + except Exception as e: # noqa: BLE001 + print(f" (crop failed: {e})") + if not a.notrace: + print(mon.cmd("gr2 trace off")) + # Let glprim exit before the next case. + guest("sleep 1; ps -ef | grep -v grep | grep glprim >/dev/null && kill `ps -ef | grep -v grep | grep glprim | awk '{print $2}'`; true") + print(f"{a.case}: {'PASS' if fails == 0 else f'{fails} FAILED'}") + return fails + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/irix-sh.py b/tools/irix-sh.py new file mode 100755 index 00000000..5dc05a0f --- /dev/null +++ b/tools/irix-sh.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""Single-shot shell access to an IRIX guest over telnet (host port forward). + +Each invocation opens a telnet session to the guest (default localhost:2323, +the [[port_forward]] rule in iris.toml), logs in (root, no password by +default), runs the requested action, prints the result and exits with the +guest command's exit status. Designed for scripted/autonomous loops, not for +interactive use. + +Actions: + wait poll until a login prompt answers (boot / network up) + run CMD [CMD ...] run shell commands (sh), print output, exit = last rc + push LOCAL REMOTE copy a TEXT file into the guest (heredoc) + pull REMOTE [LOCAL] print / save a text file from the guest + mount-shared mkdir /shared and mount the host NFS share there + +Every session exports DISPLAY=:0.0 and starts in /tmp. Command completion is +detected with a unique end marker, never with sleeps. + +Examples: + tools/irix-sh.py wait --timeout 600 + tools/irix-sh.py mount-shared + tools/irix-sh.py run 'cp /shared/gltest/*.c /shared/gltest/irix.sh /tmp' \\ + 'cd /tmp && sh irix.sh glprim' + tools/irix-sh.py run 'cd /tmp && ./glprim --hold 3000' +""" +import argparse +import os +import random +import re +import socket +import sys +import time + +IAC, DONT, DO, WONT, WILL, SB, SE = 255, 254, 253, 252, 251, 250, 240 +OPT_ECHO, OPT_SGA = 1, 3 + + +class Telnet: + def __init__(self, host, port, timeout): + self.sock = socket.create_connection((host, port), timeout=timeout) + self.buf = b"" + self.log = os.environ.get("IRIX_SH_DEBUG") + + def close(self): + try: + self.sock.close() + except OSError: + pass + + def _negotiate(self, data): + """Strip telnet commands from `data`, answering option requests: + accept the server's ECHO/SGA, refuse everything else.""" + out = bytearray() + i = 0 + while i < len(data): + c = data[i] + if c != IAC: + out.append(c) + i += 1 + continue + if i + 1 >= len(data): + break + cmd = data[i + 1] + if cmd == IAC: + out.append(IAC) + i += 2 + elif cmd in (DO, DONT, WILL, WONT) and i + 2 < len(data): + opt = data[i + 2] + if cmd == DO: + self.sock.sendall(bytes([IAC, WILL if opt == OPT_SGA else WONT, opt])) + elif cmd == WILL: + self.sock.sendall(bytes([IAC, DO if opt in (OPT_ECHO, OPT_SGA) else DONT, opt])) + i += 3 + elif cmd == SB: + end = data.find(bytes([IAC, SE]), i) + i = len(data) if end < 0 else end + 2 + else: + i += 2 + return bytes(out) + + def read_until(self, pattern, timeout): + """Read until regex `pattern` (bytes) matches; return text up to and + including the match. Raises TimeoutError.""" + rx = re.compile(pattern) + deadline = time.time() + timeout + while True: + m = rx.search(self.buf) + if m: + text = self.buf[: m.end()] + self.buf = self.buf[m.end():] + return text, m + left = deadline - time.time() + if left <= 0: + raise TimeoutError(f"timed out waiting for {pattern!r}; last output:\n" + + self.buf[-400:].decode("latin-1", "replace")) + self.sock.settimeout(min(left, 1.0)) + try: + chunk = self.sock.recv(65536) + except socket.timeout: + continue + if not chunk: + raise ConnectionError("connection closed; last output:\n" + + self.buf[-400:].decode("latin-1", "replace")) + chunk = self._negotiate(chunk) + if self.log: + sys.stderr.write(chunk.decode("latin-1", "replace")) + self.buf += chunk + + def send(self, text): + self.sock.sendall(text.replace("\n", "\r\n").encode("latin-1")) + + +class Session: + def __init__(self, args): + self.args = args + self.t = Telnet(args.host, args.port, args.connect_timeout) + + def login(self): + a = self.args + self.t.read_until(rb"login: ?$", a.login_timeout) + self.t.send(a.user + "\n") + text, _ = self.t.read_until(rb"(Password: ?$|[#$] ?$)", a.login_timeout) + if text.rstrip().endswith(b"Password:"): + self.t.send((a.password or "") + "\n") + self.t.read_until(rb"[#$] ?$", a.login_timeout) + # Quiet, predictable shell: no echo of our input, and after the ready + # marker no prompts at all (they would leak into command output). + # Root's login shell may be csh (IRIX 6.5.0), so switch to sh first. + self.t.send("exec /bin/sh\n") + # Wait for the new shell's prompt: input sent before it starts can + # be lost with the old one. + self.t.read_until(rb"[#$] ?$", a.login_timeout) + self.t.send("stty -echo; DISPLAY=:0.0; export DISPLAY; cd /tmp; " + "echo '__IRIX''SH_READY__'; PS1=''; PS2=''; export PS1 PS2\n") + self.t.read_until(rb"__IRIXSH_READY__\r?\n", a.login_timeout) + + def run(self, cmd, timeout): + """Run one shell command; return (output, rc). The markers are split + in the command text ('__B''EGIN') so an echoed command line can never + match them.""" + tag = "%08x" % random.getrandbits(32) + begin, end = f"__IRIXSH_BEGIN_{tag}__", f"__IRIXSH_END_{tag}_" + self.t.send(f"echo '{begin[:6]}''{begin[6:]}'; {cmd}\n" + f"echo '{end[:6]}''{end[6:]}'$?__\n") + self.t.read_until(re.escape(begin.encode()) + rb"\r?\n", timeout) + text, m = self.t.read_until(re.escape(end.encode()) + rb"(\d+)__", timeout) + out = text[: m.start()].decode("latin-1", "replace").replace("\r\n", "\n").replace("\r", "") + return out, int(m.group(1)) + + def close(self): + try: + self.t.send("exit\n") + except OSError: + pass + self.t.close() + + +def act_wait(args): + deadline = time.time() + args.timeout + while True: + try: + s = Session(args) + s.t.read_until(rb"login: ?$", 30) + s.t.close() + print("login prompt is up") + return 0 + except (OSError, TimeoutError, ConnectionError) as e: + if time.time() > deadline: + print(f"no login prompt after {args.timeout}s: {e}", file=sys.stderr) + return 1 + time.sleep(5) + + +def with_session(args, fn): + s = Session(args) + try: + s.login() + return fn(s) + finally: + s.close() + + +def act_run(args): + def go(s): + rc = 0 + for c in args.cmds: + out, rc = s.run(c, args.timeout) + sys.stdout.write(out) + if args.verbose: + print(f"[rc={rc}] {c}", file=sys.stderr) + if rc != 0 and not args.keep_going: + break + return rc + return with_session(args, go) + + +def act_push(args): + data = open(args.local, "r", encoding="latin-1").read() + if not data.endswith("\n"): + data += "\n" + eof = "__IRIXSH_EOF__" + if eof in data: + print("file contains the heredoc terminator", file=sys.stderr) + return 2 + + def go(s): + s.t.send(f"cat > '{args.remote}' <<'{eof}'\n{data}{eof}\n") + out, rc = s.run(f"wc -c < '{args.remote}'", args.timeout) + print(f"pushed {args.local} -> {args.remote} ({out.strip()} bytes)") + return rc + return with_session(args, go) + + +def act_pull(args): + def go(s): + out, rc = s.run(f"cat '{args.remote}'", args.timeout) + if args.local: + open(args.local, "w", encoding="latin-1").write(out) + print(f"pulled {args.remote} -> {args.local} ({len(out)} bytes)") + else: + sys.stdout.write(out) + return rc + return with_session(args, go) + + +def act_mount_shared(args): + def go(s): + out, rc = s.run("mkdir -p /shared; (mount | grep -q ' /shared ') || " + f"mount {args.nfs_host}:/shared /shared; mount | grep ' /shared '", + args.timeout) + sys.stdout.write(out) + return rc + return with_session(args, go) + + +def main(): + # Common options are accepted before or after the action. + common = argparse.ArgumentParser(add_help=False) + for name, kw in [("--host", dict(default="localhost")), + ("--port", dict(type=int, default=2323)), + ("--user", dict(default="root")), + ("--password", dict(default=None)), + ("--connect-timeout", dict(type=float, default=10)), + ("--login-timeout", dict(type=float, default=60)), + ("--timeout", dict(type=float, default=300, help="per-command timeout (wait: total)")), + ("--nfs-host", dict(default="192.168.0.1"))]: + common.add_argument(name, **kw) + sub_common = argparse.ArgumentParser(add_help=False) + for a in common._actions: + sub_common.add_argument(*a.option_strings, dest=a.dest, type=a.type, + default=argparse.SUPPRESS, help=a.help) + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + parents=[common]) + sub = p.add_subparsers(dest="action", required=True) + sub.add_parser("wait", parents=[sub_common]) + r = sub.add_parser("run", parents=[sub_common]) + r.add_argument("cmds", nargs="+") + r.add_argument("-k", "--keep-going", action="store_true", help="continue after a failing command") + r.add_argument("-v", "--verbose", action="store_true") + ps = sub.add_parser("push", parents=[sub_common]) + ps.add_argument("local") + ps.add_argument("remote") + pl = sub.add_parser("pull", parents=[sub_common]) + pl.add_argument("remote") + pl.add_argument("local", nargs="?") + sub.add_parser("mount-shared", parents=[sub_common]) + args = p.parse_args() + fn = {"wait": act_wait, "run": act_run, "push": act_push, "pull": act_pull, + "mount-shared": act_mount_shared}[args.action] + try: + sys.exit(fn(args)) + except (TimeoutError, ConnectionError, OSError) as e: + print(f"irix-sh: {e}", file=sys.stderr) + sys.exit(124) + + +if __name__ == "__main__": + main() diff --git a/tools/mgras-capture.py b/tools/mgras-capture.py new file mode 100644 index 00000000..21e53d3c --- /dev/null +++ b/tools/mgras-capture.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Drive a recording IMPACT session for the MGRAS replay goldens. + +Start iris first with a recording from power-on, e.g. + + IRIS_MGRAS_REC=ignore/mgras-golden/desktop.rec \\ + target/release/iris --config ignore/ip28/iris-650.toml + +then run this script. It starts the CPU (developer builds wait for `start`), +waits for the autologin desktop over telnet, and runs scripted scenes, +taking a recording checkpoint (`mgras rec mark`) and a screenshot after each: + + desktop the session as autologin leaves it + xterm an xterm full of text (and xterm-closed once it exits) + drag a window dragged by its title bar (root's Console winterm) + menu the 4Dwm root menu (overlay planes) opened and dismissed + +Screenshots land next to the recording as ..png. The recording +is stopped at the end (`mgras rec off`), which checkpoints the final state. +""" +import argparse +import os +import re +import socket +import subprocess +import sys +import time +import functools + +print = functools.partial(print, flush=True) + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +class Monitor: + def __init__(self, host, port): + self.sock = socket.create_connection((host, port), timeout=30) + self.read_prompt() + + def read_prompt(self): + data = b"" + while not data.endswith(b"> "): + chunk = self.sock.recv(65536) + if not chunk: + raise ConnectionError("monitor closed") + data += chunk + return data[:-2].decode("utf-8", "replace").strip() + + def cmd(self, text): + self.sock.sendall(text.encode() + b"\n") + return self.read_prompt() + + +def guest(*cmds, timeout=120): + r = subprocess.run([sys.executable, os.path.join(HERE, "irix-sh.py"), "run", "-k", *cmds], + capture_output=True, text=True, timeout=timeout) + return r.stdout + + +def wait_desktop(limit): + deadline = time.time() + limit + while time.time() < deadline: + try: + out = guest("ps -ef | grep -c '[4]Dwm'", "ps -ef | grep -c '[t]oolchest'", timeout=60) + counts = [int(x) for x in out.split() if x.isdigit()] + if len(counts) == 2 and min(counts) > 0: + return + except (subprocess.TimeoutExpired, ValueError): + pass + time.sleep(10) + raise TimeoutError("desktop did not come up") + + +class Mouse: + """Closed-loop pointer steering from the VC3 cursor position, so X's + pointer acceleration and the PS/2 axis sense don't matter.""" + + def __init__(self, mon): + self.mon = mon + self.ysign = None + + def pos(self): + m = re.search(r"cursor at \((-?\d+), (-?\d+)\)", self.mon.cmd("mgras")) + return (int(m.group(1)), int(m.group(2))) if m else None + + def nudge(self, dx, dy, buttons): + self.mon.cmd(f"ps2 mouse {dx} {dy} {buttons}") + time.sleep(0.05) + + def calibrate(self): + a = self.pos() + self.nudge(0, 4, 0) + time.sleep(0.3) + b = self.pos() + self.ysign = -1 if b[1] < a[1] else 1 + + def goto(self, x, y, buttons=0, tol=2): + for _ in range(400): + cx, cy = self.pos() + ex, ey = x - cx, y - cy + if abs(ex) <= tol and abs(ey) <= tol: + return + # Small steps stay below X's acceleration threshold. + step = lambda e: max(-4, min(4, e)) + self.nudge(step(ex), self.ysign * step(ey), buttons) + raise RuntimeError(f"pointer did not reach ({x}, {y})") + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("rec", help="recording path iris was started with (for screenshot names)") + ap.add_argument("--monitor-port", type=int, default=8888) + ap.add_argument("--boot-timeout", type=float, default=1200) + ap.add_argument("--no-start", action="store_true", help="CPU already running") + a = ap.parse_args() + + mon = None + for _ in range(60): + try: + mon = Monitor("127.0.0.1", a.monitor_port) + break + except OSError: + time.sleep(1) + if mon is None: + sys.exit("no monitor") + if not a.no_start: + print(mon.cmd("start")) + + def checkpoint(scene): + time.sleep(3) + print(f"[{scene}]", mon.cmd("mgras rec mark")) + print(mon.cmd(f"mgras shot {a.rec}.{scene}.png")) + + t0 = time.time() + wait_desktop(a.boot_timeout) + print(f"desktop up after {time.time() - t0:.0f}s") + time.sleep(20) + checkpoint("desktop") + + guest("xhost + >/dev/null 2>&1; true", + "DISPLAY=:0 /usr/bin/X11/xterm -geometry 80x24+80+200 -e sh -c " + "'ls -laR /usr/include | head -400; sleep 30' /dev/null 2>&1 &", "sleep 12") + checkpoint("xterm") + time.sleep(30) + checkpoint("xterm-closed") + + mouse = Mouse(mon) + mouse.calibrate() + # Root's Console winterm sits bottom left; grab its title bar, drag it up. + mouse.goto(300, 632) + mouse.nudge(0, 0, 1) + mouse.goto(500, 300, buttons=1) + mouse.nudge(0, 0, 0) + checkpoint("drag") + + mouse.goto(300, 700) + mouse.nudge(0, 0, 2) + time.sleep(2) + print(mon.cmd(f"mgras shot {a.rec}.menu-open.png")) + mouse.goto(150, 900, buttons=2) + mouse.nudge(0, 0, 0) + checkpoint("menu") + + print(mon.cmd("mgras rec off")) + + +if __name__ == "__main__": + main() diff --git a/tools/mgras-rec.py b/tools/mgras-rec.py new file mode 100644 index 00000000..ed167cf1 --- /dev/null +++ b/tools/mgras-rec.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Inspect and convert MGRAS recordings (src/dev/mgras/record.rs). + + mgras-rec.py text IN.rec OUT.txt [--from N] [--to M] [--no-hash] + write records [N, M) in the text form; --no-hash drops checkpoints + (a slice starting mid-session cannot match a fresh board's state) + mgras-rec.py summary IN.rec + record counts by kind and the busiest offsets +""" +import argparse +import collections +import struct +import sys + +SIZES = {ord('W'): 14, ord('R'): 14, ord('M'): 10, ord('m'): 10, ord('H'): 33, + ord('F'): 1, ord('T'): 1, ord('C'): 1} + + +def records(path): + data = open(path, 'rb').read() + if not data.startswith(b'MGRASREC'): + # Already text: pass lines through as (kind, line). + for line in data.decode().splitlines(): + line = line.split('#')[0].strip() + if line: + yield line.split()[0], line + return + i = 12 + while i < len(data): + t = data[i] + n = SIZES.get(t) + if n is None or i + n > len(data): + return + c = chr(t) + if c in 'WR': + bits = data[i + 1] + off, = struct.unpack_from('= a.last: + break + if a.no_hash and kind == 'H': + continue + out.write(line + "\n") + else: + kinds = collections.Counter() + offs = collections.Counter() + for kind, line in records(a.inp): + kinds[kind] += 1 + if kind in 'WR': + offs[(kind, line.split()[2])] += 1 + print("records by kind:", dict(kinds)) + print("busiest offsets:", [(k, f"0x{o}", n) for (k, o), n in offs.most_common(12)]) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/rex3-draw-decode.py b/tools/rex3-draw-decode.py new file mode 100644 index 00000000..cb314103 --- /dev/null +++ b/tools/rex3-draw-decode.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Decode a REX3 DRAWMODE0/DRAWMODE1 register pair into human-readable fields. + +Bit layout mirrors the `DrawMode0`/`DrawMode1` bitfields in src/dev/ng1/rex3.rs — keep +in sync if those change. + +Usage: + python3 rex3-draw-decode.py [ ...] + + Values may be hex (0x... or bare hex like 000000c6) or decimal. + +Examples: + python3 rex3-draw-decode.py 0xc6 0x3009d7b1 + python3 rex3-draw-decode.py 000000c6 3009d7b1 000000c2 3009d7b1 +""" + +import sys + +OPCODES = {0: 'NOOP', 1: 'READ', 2: 'DRAW', 3: 'SCR2SCR'} +ADRMODES = {0: 'SPAN', 1: 'BLOCK', 2: 'ILINE', 3: 'FLINE', 4: 'ALINE'} +PLANES = {0: 'NONE', 1: 'RGB', 2: 'RGBA', 4: 'OLAY', 5: 'PUP', 6: 'CID'} +DEPTHS = {0: '4bpp', 1: '8bpp', 2: '12bpp', 3: '24bpp'} +HOSTDEPTHS = {0: '12bpp', 1: '8bpp', 2: '4bpp', 3: '32bpp'} +LOGICOPS = {0: 'ZERO', 1: 'AND', 2: 'ANDR', 3: 'SRC', 4: 'ANDI', 5: 'DST', 6: 'XOR', 7: 'OR', + 8: 'NOR', 9: 'XNOR', 10: 'NDST', 11: 'ORR', 12: 'NSRC', 13: 'ORI', 14: 'NAND', 15: 'ONE'} +SF_NAMES = ['ZERO', 'ONE', 'DCOL', '1-DCOL', 'DALPHA', '1-DALPHA', 'SALPHA', '1-SALPHA'] + + +def bit(v, n): + return (v >> n) & 1 + + +def bits(v, hi, lo): + return (v >> lo) & ((1 << (hi - lo + 1)) - 1) + + +def decode_dm0(v): + op = OPCODES.get(bits(v, 1, 0), '?') + adr = ADRMODES.get(bits(v, 4, 2), '?') + flags = [] + if bit(v, 5): flags.append('DOSETUP') + if bit(v, 6): flags.append('COLORHOST') + if bit(v, 7): flags.append('ALPHAHOST') + if bit(v, 8): flags.append('STOPONX') + if bit(v, 9): flags.append('STOPONY') + if bit(v, 10): flags.append('SKIPFIRST') + if bit(v, 11): flags.append('SKIPLAST') + if bit(v, 12): flags.append('ENZPAT') + if bit(v, 13): flags.append('ENLSPAT') + if bit(v, 14): flags.append('LSADVLAST') + if bit(v, 15): flags.append('LEN32') + if bit(v, 16): flags.append('ZPOPAQUE') + if bit(v, 17): flags.append('LSOPAQUE') + if bit(v, 18): flags.append('SHADE') + if bit(v, 19): flags.append('LRONLY') + if bit(v, 20): flags.append('XYOFFSET') + if bit(v, 21): flags.append('CICLAMP') + if bit(v, 22): flags.append('ENDPTFILT') + if bit(v, 23): flags.append('YSTRIDE') + return f"{op} {adr} {' '.join(flags)}".rstrip() + + +def decode_dm1(v): + planes = PLANES.get(bits(v, 2, 0), '?') + depth = DEPTHS.get(bits(v, 4, 3), '?') + hdepth = HOSTDEPTHS.get(bits(v, 9, 8), '?') + cmp_ = bits(v, 14, 12) + logicop = LOGICOPS.get(bits(v, 31, 28), '?') + sf = SF_NAMES[bits(v, 21, 19)] + df = SF_NAMES[bits(v, 24, 22)] + flags = [] + if bit(v, 5): flags.append('DBLSRC') + if bit(v, 6): flags.append('YFLIP') + if bit(v, 7): flags.append('RWPACKED') + if bit(v, 10): flags.append('RWDOUBLE') + if bit(v, 11): flags.append('SWAPEND') + flags.append('RGB' if bit(v, 15) else 'CI') + if bit(v, 16): flags.append('DITHER') + if bit(v, 17): flags.append('FASTCLEAR') + if bit(v, 18): flags.append(f'BLEND({sf}+{df})') + if bit(v, 25): flags.append('BACKBLEND') + if bit(v, 26): flags.append('PREFETCH') + if bit(v, 27): flags.append('BLENDALPHA') + return f"planes={planes} {depth} host:{hdepth} cmp:{cmp_} logicop={logicop} {' '.join(flags)}".rstrip() + + +def parse_val(s): + return int(s, 16) if s.lower().startswith('0x') else int(s, 16) if any(c in 'abcdefABCDEF' for c in s) else int(s) + + +def main(): + args = sys.argv[1:] + if len(args) < 2 or len(args) % 2 != 0: + print(__doc__) + sys.exit(1) + + for i in range(0, len(args), 2): + dm0 = parse_val(args[i]) + dm1 = parse_val(args[i + 1]) + print(f"DM0={dm0:#010x} DM1={dm1:#010x}") + print(f" dm0: {decode_dm0(dm0)}") + print(f" dm1: {decode_dm1(dm1)}") + print() + + +if __name__ == '__main__': + main() diff --git a/tools/rex3_replay_png.py b/tools/rex3_replay_png.py new file mode 100644 index 00000000..300652fe --- /dev/null +++ b/tools/rex3_replay_png.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Replay a `log rex ` REX3 trace into PNG images of what the HOST sent. + +This deliberately does NOT model the emulator's pixel pipeline (no blending, no +dithering, no 12bpp quantisation, no alpha test). It just plots, per drawn pixel, +the RGBA the host loaded into COLORRED/GRN/BLUE/ALPHA. The point is to see the +application's intent — where it thinks the rock is opaque and where transparent — +so pipeline bugs can be told apart from bad input. + +Key trace facts this relies on (see rules/rex3/blendalpha-and-alpha-blending.md): + * STOPONX=0 on every billboard draw, so ONE GO == ONE PIXEL at (xstart, ystart). + xend is the remaining distance to the polygon edge, NOT a span width. + * Colour registers are IEEE floats biased by +4096; the hardware masks off the + exponent. COLORRED keeps 24 bits, the others 20; integer part is bits 22:11. + +Outputs (default prefix "rock"): +

_rgb.png source colour as sent, alpha ignored +

_alpha.png source alpha as greyscale (black=0, white=255) +

_rgba.png colour composited over a checkerboard using the sent alpha — + i.e. what the app intends you to see +

_lowalpha.png magenta where alpha < THRESHOLD, matching `rex alphadebug` + +Usage: + python3 tools/rex3_replay_png.py raw.log [--prefix rock] [--frame N] + [--threshold 16] [--dm1 3165d011,3165d031] +""" +import re, sys, os +from collections import defaultdict + +try: + from PIL import Image +except ImportError: + sys.exit("needs Pillow: pip install Pillow") + + +def arg(name, default=None, cast=str): + if name in sys.argv: + return cast(sys.argv[sys.argv.index(name) + 1]) + return default + + +LOG = sys.argv[1] if len(sys.argv) > 1 else "raw.log" +PREFIX = arg("--prefix", "rock") +THRESHOLD = arg("--threshold", 16, int) +FRAME = arg("--frame", None, int) +DM1_FILTER = arg("--dm1", None) +if DM1_FILTER: + DM1_FILTER = {int(v, 16) for v in DM1_FILTER.split(",")} + +P = re.compile(r'REX3 Process: Offset ([0-9a-f]+) \(([A-Z0-9_]+)\) Val ([0-9a-f]+)') +D = re.compile(r'REX3 Draw: (\S+) (\S+) Mode0=([0-9a-f]+) Mode1=([0-9a-f]+)') +C = re.compile(r'Coords: Start\(([-\d.]+), ([-\d.]+)\) End\(([-\d.]+), ([-\d.]+)\)') + +R_RED, R_ALPHA, R_GRN, R_BLUE = 0x0200, 0x0204, 0x0208, 0x020c + + +def clamp_component(c): + """o12.11 DDA register -> 0..255, matching Rex3Context::clamp_color_component.""" + if c & (1 << 31): + return 0 + v = (c >> 11) & 0x1FF + if v >= 0x180: + return 0 + return 0xFF if v > 0xFF else v + + +def main(): + st = {} + pending = None + pixels = [] # (x, y, r, g, b, a) + frame = 0 + # A frame boundary is hard to detect reliably; use FASTCLEAR/BLOCK clears as a + # hint, falling back to "all draws" when the caller doesn't ask for one frame. + for line in open(LOG): + m = P.search(line) + if m: + st[int(m.group(1), 16)] = int(m.group(3), 16) + continue + m = D.search(line) + if m: + adr, dm1 = m.group(1), int(m.group(4), 16) + if adr == 'BLOCK': + frame += 1 + if DM1_FILTER is not None and dm1 not in DM1_FILTER: + pending = None + continue + if FRAME is not None and frame != FRAME: + pending = None + continue + pending = ( + clamp_component(st.get(R_RED, 0) & 0xFFFFFF), + clamp_component(st.get(R_GRN, 0) & 0xFFFFF), + clamp_component(st.get(R_BLUE, 0) & 0xFFFFF), + clamp_component(st.get(R_ALPHA, 0) & 0xFFFFF), + ) + continue + m = C.search(line) + if m and pending is not None: + # STOPONX=0 -> one pixel per GO, located at (xstart, ystart). + x = int(float(m.group(1))) + y = int(float(m.group(2))) + r, g, b, a = pending + pixels.append((x, y, r, g, b, a)) + pending = None + + if not pixels: + sys.exit("no matching draws found") + + xs = [p[0] for p in pixels] + ys = [p[1] for p in pixels] + x0, x1 = min(xs), max(xs) + y0, y1 = min(ys), max(ys) + w, h = x1 - x0 + 1, y1 - y0 + 1 + print(f"{len(pixels)} drawn pixels bbox x {x0}..{x1} y {y0}..{y1} -> {w}x{h}") + + rgb = Image.new("RGB", (w, h), (0, 0, 0)) + alpha = Image.new("L", (w, h), 0) + over = Image.new("RGB", (w, h), (0, 0, 0)) + low = Image.new("RGB", (w, h), (0, 0, 0)) + prgb, palpha, pover, plow = rgb.load(), alpha.load(), over.load(), low.load() + + # checkerboard so "transparent" is visually obvious in the composited view + for yy in range(h): + for xx in range(w): + c = 96 if ((xx >> 3) + (yy >> 3)) & 1 else 160 + pover[xx, yy] = (c, c, c) + plow[xx, yy] = (c, c, c) + + n_low = 0 + for x, y, r, g, b, a in pixels: + xx, yy = x - x0, y - y0 + prgb[xx, yy] = (r, g, b) + palpha[xx, yy] = a + # composite source over whatever the checkerboard/previous pixel holds + br, bg, bb = pover[xx, yy] + pover[xx, yy] = ((r * a + br * (255 - a)) // 255, + (g * a + bg * (255 - a)) // 255, + (b * a + bb * (255 - a)) // 255) + if a < THRESHOLD: + plow[xx, yy] = (255, 0, 255) + n_low += 1 + else: + plow[xx, yy] = (r, g, b) + + for img, suffix in ((rgb, "rgb"), (alpha, "alpha"), (over, "rgba"), (low, "lowalpha")): + path = f"{PREFIX}_{suffix}.png" + img.save(path) + print(f" wrote {path}") + print(f" pixels with alpha < {THRESHOLD}: {n_low} ({100*n_low/len(pixels):.1f}%)") + + +if __name__ == "__main__": + main() diff --git a/tools/xwd-compare.py b/tools/xwd-compare.py new file mode 100644 index 00000000..d4a8b31c --- /dev/null +++ b/tools/xwd-compare.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Compare an X window dump (xwd -root, i.e. XGetImage of the screen) with +the IMPACT framebuffer the emulator holds (`mgras fbdump`'s fb.bin). + + xwd-compare.py root.xwd mgrasdump/fb.bin [--diff out.png] + +For an 8-bit (PseudoColor) dump each pixel must equal the low byte of the +framebuffer word at the same screen position (fb.bin is big-endian u32, +2048x2048, row 0 at the bottom; the screen is its bottom-left corner, so +screen row y is fb row height - 1 - y, height being the dump's). Prints the header, the match rate and +where mismatches are. +""" +import argparse +import struct +import sys +import zlib + +W, H = 2048, 2048 # fb.bin: the whole framebuffer + + +def read_xwd(path): + d = open(path, 'rb').read() + f = struct.unpack('>25I', d[:100]) + names = ['header_size', 'file_version', 'pixmap_format', 'pixmap_depth', 'pixmap_width', 'pixmap_height', + 'xoffset', 'byte_order', 'bitmap_unit', 'bitmap_bit_order', 'bitmap_pad', 'bits_per_pixel', + 'bytes_per_line', 'visual_class', 'red_mask', 'green_mask', 'blue_mask', 'bits_per_rgb', + 'colormap_entries', 'ncolors', 'window_width', 'window_height', 'window_x', 'window_y', + 'window_bdrwidth'] + h = dict(zip(names, f)) + colors = [] + off = h['header_size'] + for i in range(h['ncolors']): + pix, r, g, b, flags, pad = struct.unpack('>IHHHBB', d[off + 12 * i: off + 12 * i + 12]) + colors.append((pix, r >> 8, g >> 8, b >> 8)) + data = d[off + 12 * h['ncolors']:] + return h, colors, data + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('xwd') + ap.add_argument('fb') + ap.add_argument('--diff') + a = ap.parse_args() + h, colors, data = read_xwd(a.xwd) + print({k: h[k] for k in ('pixmap_format', 'pixmap_depth', 'pixmap_width', 'pixmap_height', 'bits_per_pixel', + 'bytes_per_line', 'visual_class', 'ncolors', 'byte_order')}) + fb = open(a.fb, 'rb').read() + if h['bits_per_pixel'] != 8: + sys.exit("only 8-bit dumps are compared so far") + w, hh, bpl = h['pixmap_width'], h['pixmap_height'], h['bytes_per_line'] + word = lambda x, y: struct.unpack_from('>I', fb, ((hh - 1 - y) * W + x) * 4)[0] + bad = [] + for y in range(hh): + row = data[y * bpl: y * bpl + w] + for x in range(w): + if row[x] != word(x, y) & 0xFF: + bad.append((x, y, row[x], word(x, y))) + total = w * hh + print(f"{total - len(bad)}/{total} pixels match ({100.0 * (total - len(bad)) / total:.3f}%)") + if bad: + xs = [b[0] for b in bad] + ys = [b[1] for b in bad] + print("mismatch bbox", (min(xs), min(ys), max(xs), max(ys))) + print("first mismatches (x, y, xwd, fb):", [(x, y, hex(p), hex(q)) for x, y, p, q in bad[:12]]) + if a.diff: + rows = [] + badset = {(x, y) for x, y, _, _ in bad} + for y in range(hh): + r = bytearray() + for x in range(w): + r += b'\xff\x00\x00' if (x, y) in badset else bytes([data[y * bpl + x]] * 3) + rows.append(b'\x00' + bytes(r)) + + def chunk(t, dd): + return struct.pack('>I', len(dd)) + t + dd + struct.pack('>I', zlib.crc32(t + dd) & 0xffffffff) + open(a.diff, 'wb').write(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, hh, 8, 2, 0, 0, 0)) + + chunk(b'IDAT', zlib.compress(b''.join(rows))) + chunk(b'IEND', b'')) + + +if __name__ == '__main__': + main()