Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 80 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Boots IRIX 6.5 and 5.3. Has networking. Has a framebuffer.
## Q&A

**Q: What is it?**
An SGI Indy (MIPS R4400) emulator. Emulates enough of the hardware that IRIX
actually boots to a usable system shell, networking, X11, the works.
An SGI Indy (MIPS R4400) emulator. Emulates enough hardware that IRIX
boots to a usable system: shell, networking, X11, the works.

**Q: But why?**
Wanted to see how far vibe coding could go, and to learn some Rust along the way.
Expand All @@ -38,7 +38,11 @@ Yes.

- IRIX 6.5 boots to multiuser, networking works (ping, telnet, ftp)
- IRIX 5.3 works too
- X11 / Newport (REX3) graphics works
- X11 / Newport (REX3) graphics works, with mouse and keyboard input
- Cranelift JIT compiler for MIPS to x86_64 translation (optional)
- Copy-on-write disk overlay. Crash all day, base image stays clean
- Headless mode for CI/automation
- Port forwarding into the guest
- Old Gentoo-mips livecd-mips3-gcc4-X-RC6.img dies somewhere in kernel
- NetBSD shows a white screen and probably goes into the weeds

Expand All @@ -47,18 +51,88 @@ Yes.

You need:
- `scsi1.raw` — raw hard disk image with IRIX 6.5.22 for Indy
for a quick start get the mame irix image from https://mirror.rqsall.com/sgi-mame/ and convert to raw using chdman extractraw
(for a quick start get the MAME IRIX image from https://mirror.rqsall.com/sgi-mame/ and convert to raw using `chdman extractraw`)
- `070-9101-011.bin` — Indy PROM image (optional; a default is embedded)

```
cargo run --release
you can add --features lightning for a little more speed
```

See [HELP.md](HELP.md) for the full rundown — serial ports, monitor console,
Build variants:
```
cargo run --release --features lightning # disable breakpoints for ~10% more speed
cargo run --release --features jit # enable Cranelift JIT compiler
```

See [HELP.md](HELP.md) for the full rundown: serial ports, monitor console,
NVRAM/MAC address setup, disk image prep, and more.


## JIT compiler

Optional Cranelift-based JIT. Compiles hot MIPS basic blocks to native x86_64.
Enable with `--features jit` at build time and `IRIS_JIT=1` at runtime.

Three tiers: blocks start ALU-only (registers + branches), promote to
Loads (+ memory reads), then Full (+ stores) based on stable execution. Probe
interval is adaptive. Hot block profiles persist across sessions.

```
IRIS_JIT=1 cargo run --release --features jit
```

| Variable | Default | Description |
|----------|---------|-------------|
| `IRIS_JIT` | 0 | Enable JIT (1) or interpreter-only (0) |
| `IRIS_JIT_MAX_TIER` | 2 | Cap tier: 0=ALU, 1=Loads, 2=Full |
| `IRIS_JIT_VERIFY` | 0 | Run each block through interpreter and compare (debug) |
| `IRIS_JIT_PROBE` | 200 | Base probe interval (steps between cache checks) |


## Copy-on-write disk overlay

Protects disk images from corruption during development and testing. The base
`.raw` file is opened read-only and writes go to a sparse overlay file. Kill
the emulator whenever you want. Delete the overlay to reset to the clean base.

Enable in `iris.toml`:
```toml
[scsi.1]
path = "scsi1.raw"
cdrom = false
overlay = true
```

Writes go to `scsi1.raw.overlay`. Monitor commands:
- `cow status` - show dirty sector count
- `cow commit` - merge overlay into base image (permanent)
- `cow reset` - discard all overlay writes


## Input

Click the window to grab mouse and keyboard. Right Ctrl releases the grab.
Mouse and keyboard use standard PS/2 emulation through the IOC.

**Note:** Alt-tabbing away from the window can garble keyboard input in IRIX
terminal apps. Use `telnet 127.0.0.1 2323` (with port forwarding configured)
for a clean terminal instead.


## Rules

The `rules/` directory contains hard-won lessons from debugging the JIT and
getting IRIX running. These are meant for both humans and AI assistants working
on the codebase.

- `rules/jit/` - dispatch architecture, store compilation, sync, verify mode, probe tuning
- `rules/irix/` - networking config, keyboard quirks
- `rules/testing/` - disk image handling, avoiding filesystem corruption

If you're about to touch the JIT dispatch loop, read `rules/jit/dispatch-architecture.md`
first. It'll save you a few days.


## License

BSD 3-Clause
Expand Down
20 changes: 20 additions & 0 deletions rules/irix/keyboard-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# IRIX Keyboard Issues

## Alt-tab corrupts X11 keyboard input

After alt-tabbing away from the Rex window and returning, IRIX X11 terminal
apps (Console, Terminal, xterm) show escape codes instead of typed characters.
The IRIX login dialog still works (different input path).

**Cause:** The Alt key release event from alt-tab confuses IRIX's X keyboard
state machine. The PS/2 scancode for LAlt (0x19 in set 3) is delivered as a
release without a matching press.

**Workarounds:**
1. Don't alt-tab while interacting with IRIX GUI — use Right Ctrl to ungrab mouse
2. Use telnet via port forwarding (host 2323 -> guest 23) for terminal access
3. Mount the disk image directly to edit files from the host

**Status:** Pre-existing emulator issue, not introduced by any recent changes.
Proper fix would require filtering or suppressing stale modifier key events
in the UI event handler when focus is regained.
60 changes: 60 additions & 0 deletions rules/irix/networking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# IRIX 6.5 Networking Configuration

## Required files

| File | Contents | Example |
|------|----------|---------|
| /etc/sys_id | Hostname | `IRIS` |
| /etc/hosts | IP-to-hostname mapping | `192.168.0.2 IRIS` |
| /etc/config/ifconfig-ec0.options | IP + netmask (hex) | `192.168.0.2 netmask 0xffffff00` |
| /etc/config/static-route.options | Default gateway | `$ROUTE $QUIET add net default 192.168.0.1` |
| /etc/config/network | Enable networking | `on` |

## Common mistakes

- **Wrong filename:** Use `ifconfig-ec0.options`, NOT `ifconfig-1.options`.
IRIX names config files after the interface device name.

- **Missing IP in options:** The IP address goes IN `ifconfig-ec0.options`
along with the netmask. It's not just options — it's the full ifconfig args.

- **Wrong gateway file:** Use `/etc/config/static-route.options`, NOT
`/etc/defaultrouter`. The format uses shell variables: `$ROUTE $QUIET add net default <ip>`.

- **Netmask format:** IRIX uses hex notation: `0xffffff00` for 255.255.255.0.

## NVRAM MAC address (one-time setup)

The Seeq Ethernet controller reads its MAC from NVRAM. A fresh install has
no MAC set, which prevents networking.

1. Boot to PROM monitor (press Escape during countdown)
2. `>> setenv -f eaddr 08:00:69:de:ad:01` (any SGI OUI `08:00:69` MAC)
3. From iris monitor (telnet 127.0.0.1 8888): `rtc save`

## iris emulator network configuration

The emulator provides a NAT gateway with built-in DHCP:
- Gateway: 192.168.0.1 (hardcoded in GatewayConfig)
- Guest: 192.168.0.2 (assigned via DHCP or static)
- Netmask: 255.255.255.0
- DNS: forwarded to host's resolver

Port forwarding configured in iris.toml:
```toml
[[port_forward]]
proto = "tcp"
host_port = 2323
guest_port = 23
bind = "localhost"
```

## Keyboard workaround

Alt-tabbing away from the Rex window corrupts IRIX X11 keyboard input
(terminal apps show escape codes). Once networking is up, use:
```bash
telnet 127.0.0.1 2323
```
This connects via the port forward to IRIX's telnet daemon with a clean
terminal — no keyboard corruption issues.
40 changes: 40 additions & 0 deletions rules/jit/dispatch-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# JIT Dispatch Architecture Rules

## Interpreter-first, never JIT-first

The dispatch loop must run the interpreter in sustained bursts (hundreds of steps)
between JIT block executions. The interpreter's step() does critical per-instruction
bookkeeping: cp0_count advancement, interrupt checking, cp0_compare crossover,
delay slot state machine.

**NEVER** check the JIT cache every iteration (JIT-first). Even one exec.step()
between JIT blocks is insufficient. Tested at 58% JIT ratio — kernel panicked.

**Minimum probe interval: 100.** Below this, the system approaches JIT-first
behavior and crashes. The adaptive ProbeController enforces this via IRIS_JIT_PROBE_MIN.

## No block chaining

**NEVER** execute multiple JIT blocks consecutively without returning to the
interpreter. Manual interrupt checks between chained blocks are insufficient —
they miss CP0 timing, soft reset, software interrupts. Tested with up to 16
chained blocks — kernel panic at 0x880097ac.

## Post-block bookkeeping is mandatory

After every JIT block execution (normal exit path):
1. Advance cp0_count by `block_len * count_step`
2. Check cp0_compare crossover for timer interrupt (CAUSE_IP7)
3. Credit `local_cycles += block_len`
4. Check for pending interrupts via atomic load
5. Merge external IP bits into cp0_cause
6. If unmasked interrupt pending, call exec.step() to service it

On the exception path:
1. Advance cp0_count for instructions executed BEFORE the fault:
`instrs_before_fault = (ctx.pc - block_start_pc) / 4`

Omitting post-block cp0_count advancement causes timer drift and kernel panics.
This was present in the original initial JIT but accidentally dropped in the
rewrite. The bug was masked with short straight-line ALU blocks but manifested
immediately with branch compilation (longer, more frequent blocks).
36 changes: 36 additions & 0 deletions rules/jit/probe-tuning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# JIT Probe Tuning Rules

## Minimum probe interval: 100

Never allow the probe interval to drop below ~100 interpreter steps. Below
this threshold, the system approaches JIT-first behavior and the interpreter
doesn't get enough sustained runs for kernel timing stability.

An earlier adaptive formula `200_000 / cache_size` gave a value of 9 with
21K blocks, effectively making probe=32. This crashed.

## Use sqrt-based cache pressure

Cache size pressure formula: `1.0 / (cache_size / 100.0).sqrt()`

This degrades gracefully:
- 100 blocks: factor 1.0 (no change)
- 1000 blocks: factor 0.68
- 10000 blocks: factor 0.46
- 50000 blocks: factor 0.31

Combined with min_interval=100, the effective probe never drops dangerously low.

## Asymmetric EWMA response

Hits pull the interval down aggressively (~3% per hit, factor 31/32).
Misses push the interval up gently (~1% per miss, factor 33/32).
This exploits hot code quickly without overreacting to cold regions.

## Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| IRIS_JIT_PROBE | 200 | Base probe interval |
| IRIS_JIT_PROBE_MIN | 100 | Minimum (critical floor) |
| IRIS_JIT_PROBE_MAX | 2000 | Maximum |
62 changes: 62 additions & 0 deletions rules/jit/store-compilation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# JIT Store Compilation Rules

## Full-tier blocks must be non-speculative

Set `speculative: tier != BlockTier::Full` in the compiler.

**Why:** Snapshot rollback restores CPU+TLB but NOT memory. If a store block
does read-modify-write (LW, ADDIU, SW) and then hits an exception, rollback
rewinds CPU to pre-block state but memory has the modified value. The
interpreter re-runs from block entry, reads the modified value, modifies it
again. Counters become N+2 instead of N+1. This corrupts kernel data structures.

## Full-tier blocks must terminate at the first store

In trace_block, break after pushing the first store instruction at Full tier:
```rust
if tier == BlockTier::Full && is_compilable_store(&d) {
break;
}
```

**Why:** Long blocks with multiple load/store helper calls create complex CFG
(ok_block/exc_block diamond patterns per helper). This triggers Cranelift
regalloc2 codegen issues on x86_64 — rare but fatal corruption that manifests
after millions of block executions. Short blocks (~3-10 instructions) work
perfectly. Confirmed empirically: short blocks = stable with 5K+ Full
promotions; long blocks = crash at 780M instructions.

## Write helpers must use status != EXEC_COMPLETE

```rust
if status != EXEC_COMPLETE { ctx.exit_reason = EXIT_EXCEPTION; ... }
```

**NEVER** use `status & EXEC_IS_EXCEPTION != 0`. BUS_BUSY (0x100) does not
have the EXEC_IS_EXCEPTION bit (bit 27) set, so it would be treated as
success. But BUS_BUSY means the write was NOT performed. This silently drops
uncached writes (MMIO stores to device registers), causing slow corruption.

## Verify mode cannot validate stores

Verify mode snapshots CPU/TLB but NOT memory. After a JIT block with stores
modifies memory, the interpreter re-run reads the JIT-modified values.
Read-modify-write sequences get double-applied. Verify mode is only valid
for ALU and Load tiers.

## Delay-slot stores should be excluded from compilation

In trace_block, when checking the delay slot instruction for a branch, exclude
stores:
```rust
if is_compilable_for_tier(&delay_d, tier) && !is_compilable_store(&delay_d) {
instrs.push((delay_raw, delay_d));
delay_ok = true;
}
```

**Why:** If a delay-slot store faults, sync_to_executor clears in_delay_slot.
exec.step() re-executes the store as a non-delay-slot instruction.
handle_exception sets cp0_epc to the store PC (not the branch PC) and doesn't
set the BD bit. On ERET, the branch is permanently skipped, corrupting control
flow. This is defensive — the block length fix is the primary fix for stores.
29 changes: 29 additions & 0 deletions rules/jit/sync-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# JIT Sync Architecture Rules

## sync_to_executor: minimal writeback only

sync_to_executor must ONLY write back:
- GPRs (core.gpr)
- PC (core.pc)
- hi, lo

It must NOT write back:
- cp0_status, cp0_cause, cp0_epc, cp0_badvaddr
- cp0_count, cp0_compare, count_step
- nanotlb (all 3 entries)
- fpr, fpu_fcsr
- local_cycles, cached_pending

**Why:** JIT memory helpers (read/write) call exec methods directly, which
modify these fields on the executor in-place. The JitContext copy is stale
for these fields after helpers run. Writing them back would clobber changes
made by exception handlers and TLB fill operations.

## sync_to_executor must clear delay slot state

Always set:
- exec.in_delay_slot = false
- exec.delay_slot_target = 0

JIT blocks handle delay slots internally. Clearing prevents the interpreter
from jumping to a stale target on the next step().
17 changes: 17 additions & 0 deletions rules/jit/verify-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# JIT Verify Mode Rules

## Timing false positive detection

Verify mode re-runs each block through the interpreter at a different wall-clock
time. The interpreter may see different external interrupt state via the atomic
and take an exception the JIT didn't see (or vice versa).

Detection: if the interpreter PC is in exception vectors (0x80000000-0x80000400
or 0x80000180) but the JIT PC is not, it's a timing false positive. Keep the
block, don't invalidate. Use the interpreter's result as authoritative.

## Verify mode is invalid for store blocks

See store-compilation.md. Memory is not part of the snapshot, so verify mode
double-applies read-modify-write sequences for blocks containing stores.
Only use verify mode for ALU and Loads tiers.
Loading
Loading