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
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ camera = ["dep:nokhwa", "dep:v4l"]
pcap = ["dep:pcap"]
# N64 development board (Ultra64) — GIO slot 0 + shm IPC.
ultra64 = ["dep:shared_memory", "dep:raw_sync"]
# DaynaPort SCSI/Link target: a SCSI-attached Ethernet adapter (type 3
# Processor device) selectable per SCSI id, giving the guest a second network
# path that does not go through the onboard SEEQ. Needs a guest driver — see
# github.com/techomancer/irixdayna for the IRIX one. Each target runs its own
# NAT gateway (or PCAP bridge with --features pcap) on its own subnet.
# Off by default — enable with `cargo build --features daynaport`, then select
# it at runtime with `kind = "daynaport"` in an `[scsi.N]` section.
daynaport = []

[dependencies]
clap = { version = "4", features = ["derive"] }
Expand Down
9 changes: 9 additions & 0 deletions HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,15 @@ cdrom = true
# cdrom = true
# discs = ["irix65.iso", "extras.iso", "patches.iso"]

# DaynaPort SCSI/Link — Ethernet over the SCSI bus. Needs a build with
# --features daynaport and a driver in the guest (IRIX: irixdayna -> dp0).
# It has no disk image; mac and subnet are optional (defaults derived from the
# SCSI id / 192.168.10.0/24). See docs/daynaport.md.
# [scsi.3]
# kind = "daynaport"
# mac = "00:80:19:12:34:56"
# subnet = "192.168.10.0/24"

# VINO video-in (IndyCam emulation).
# source: "test_pattern" | "camera" | "black"
# standard: "ntsc" | "pal"
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ cargo run --release --features ci_clock # synthetic deterministic C
cargo run --release --features chd # mount .chd disk/CD-ROM images directly (via libchdman-rs); off by default to keep builds light
cargo run --release --features camera # use host camera as the IndyCam video source (macOS AVFoundation via nokhwa). See [vino] in iris.toml.
cargo run --release --features pcap # bridge guest networking onto a real host interface via libpcap instead of the built-in NAT gateway. See [network] in iris.toml.
cargo run --release --features daynaport # DaynaPort SCSI/Link: Ethernet over the SCSI bus, selectable per SCSI id. Needs a guest driver. See docs/daynaport.md.
```

### CHD image support (`--features chd`)
Expand Down Expand Up @@ -195,6 +196,28 @@ back to the NAT gateway, and `--list-net-interfaces` reports that the feature
is missing.


## DaynaPort SCSI/Link (`--features daynaport`)

A SCSI-attached Ethernet adapter (SCSI type 3, Processor) selectable on any
SCSI id — a second network path for the guest that goes over the SCSI bus
instead of the onboard SEEQ. Off by default, because it is only useful with a
guest driver; IRIX has none in the box (see
[irixdayna](https://github.com/techomancer/irixdayna), where it appears as
`dp0`).

```toml
[scsi.3]
kind = "daynaport" # default "disk"; "cdrom" / cdrom = true unchanged
mac = "00:80:19:12:34:56" # optional; default derived from the SCSI id
subnet = "192.168.10.0/24" # optional; this target's own NAT subnet
```

Each DaynaPort runs its own NAT gateway (or PCAP bridge, in a `--features pcap`
build) on its own subnet, so `dp0` and `ec0` never share a network. `scsi dayna`
in the monitor shows its MAC, addresses and counters. Full protocol and
verification notes: [docs/daynaport.md](docs/daynaport.md).


## R5000 CPU (`--features r5k`)

Switches the emulated CPU from R4400 to R5000:
Expand Down
178 changes: 178 additions & 0 deletions docs/daynaport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# DaynaPort SCSI/Link target

IRIS can present a **DaynaPort SCSI/Link** (DP0801 / DP0802) — a SCSI-attached
Ethernet adapter — on any SCSI id. It is a second, architecture-independent
network path for the guest: no GIO card, no onboard SEEQ, just the SCSI bus.

The device is a SCSI **type 3 (Processor)** target that moves Ethernet frames
with five vendor-specific 6-byte CDBs. Modern re-implementations (BlueSCSI V2,
ZuluSCSI, PiSCSI, SCSI2SD) speak the same protocol, which is how vintage SGI,
Mac and Atari machines get networking today.

**It needs a guest driver.** IRIX has no DaynaPort driver in the box; without
one the target is visible on the bus (`hinv` shows a SCSI device at that id) and
nothing else happens. The IRIX driver lives at
[github.com/techomancer/irixdayna](https://github.com/techomancer/irixdayna)
(6.5 in the root, 5.3 under `irix5.3/`), where it appears as `dp0`.

## Build

Off by default — it is only useful with that driver:

```sh
cargo build --release --features daynaport # iris CLI
cargo build --release -p iris-gui --features daynaport # GUI
```

Without the feature, a config that asks for one fails at startup with
`DaynaPort support not compiled in (rebuild with --features daynaport)`.

## Configure

```toml
[scsi.3]
kind = "daynaport" # default "disk"; "cdrom" (or cdrom = true) unchanged
mac = "00:80:19:12:34:56" # optional
subnet = "192.168.10.0/24" # optional; this target's own NAT subnet
```

- `kind` is the new spelling of the target type. `cdrom = true` still means
`kind = "cdrom"`, so no existing config changes.
- `mac` defaults to `00:80:19:44:50:<scsi-id>` — the real DaynaPort `00:80:19`
OUI, then `44 50` ("DP") and the target id, so two targets never collide. It
is deliberately *not* the IRIX driver's `00:80:19:00:00:NN` placeholder, so a
MAC actually read from the device is visibly different from a made-up one.
- `subnet` defaults to `192.168.10.0/24`: gateway `.1`, guest `.2`. It must
differ from the machine-wide `nat_subnet` (ec0's) — startup validation rejects
a collision.
- `path`, `discs`, `overlay`, `scratch` do not apply and are rejected: there is
no image behind a network adapter.

In the GUI the target type is a dropdown on the Disks tab (HDD / CD-ROM /
DaynaPort), with MAC and subnet fields next to it.

## Networking topology

Each DaynaPort runs **its own `NatEngine`** on its own thread (`daynaN-nat`),
separate from the onboard SEEQ's. So `dp0` and `ec0` are on different subnets
and traffic through the DaynaPort is unmistakable — useful for testing, and it
keeps a broken DaynaPort from disturbing the onboard NIC.

Inherited from the machine-wide config:

- **Backend selection** (`[network] mode`): `nat` or, in a `--features pcap`
build, `pcap` — a DaynaPort can be bridged onto a real host interface exactly
as `ec0` can.
- **The NFS export** (`[nfs]`), which is served in-process with no host sockets,
so the guest can mount it over either interface.

**Not** inherited: host **port forwards**. Only one engine can own a host
listening port, so forwards stay with the onboard NIC.

## Monitor

```
scsi dayna # MAC, gateway/client/netmask, enable + broadcast state, counters
scsi status # one line per DaynaPort, then the CD-ROM listing
net status # NAT tables (shared command; shows the onboard NIC's engine)
```

## Protocol

Reference: **SLINKCMD.TXT** (Roger Burrows, rev 1.20). Implemented in
`src/daynaport.rs`; the record format is what `dp_do_rx()` in the IRIX driver
consumes. All multi-byte fields are big-endian on the wire.

| Opcode | Name | Direction |
|---|---|---|
| `0x08` | READ — receive packet(s) | device → host |
| `0x09` | RETRIEVE STATS | device → host |
| `0x0A` | WRITE — transmit packet | host → device |
| `0x0C` | SET INTERFACE MODE | no data |
| `0x0E` | ENABLE/DISABLE | no data |

`0x08` and `0x0A` are the same opcodes as SCSI READ(6)/WRITE(6). A DaynaPort is
dispatched on device kind **before** the storage opcodes in
`ScsiDevice::request` (`src/scsi.rs`) and answers no storage command at all —
no READ CAPACITY, no MODE SENSE, no READ TOC. The WD33C93A also needs to know:
a DaynaPort WRITE(6) transfers a plain byte count from CDB 3..4, not
`blocks × 512`.

### READ response

Records back to back, each:

```
offset size field
0 2 pktlen, BIG-ENDIAN — frame length INCLUDING a 4-byte trailing
CRC, EXCLUDING this 6-byte header
2 4 flags, BIG-ENDIAN — 0x00000010 = more packets still queued,
0x00000000 = last record, 0xFFFFFFFF = dropped (unused here)
6 pktlen the Ethernet frame, then 4 CRC bytes
```

Rules that matter, all covered by the unit tests in `src/daynaport.rs`:

- **`pktlen` includes the 4 CRC bytes and the payload physically carries
them.** Getting this wrong truncates every frame by 4 bytes — frames that are
*almost* right, the worst kind of wrong. The CRC value is not checked by
anyone; zeros are fine.
- **`pktlen == 0` means "no more records".** An idle device answers with six
zero bytes immediately. READ never blocks: the driver polls it every 10 ms and
a blocking read wedges the interface.
- **MORE (`0x10`) is set on every record but the last of a response**, and on
the last one too if frames are still queued (the driver then issues another
READ instead of waiting for its next tick). The driver stops parsing at the
first record without MORE, so an intermediate record without it silently drops
the rest of the response.
- **A record is never emitted past the requested transfer length.** The frame
stays queued for the next READ instead. At the driver's 3072-byte ask, two
max-size frames fit and a third does not.

`0xFFFFFFFF` (dropped → the driver does a full disable/enable/set-mode cycle) is
never emitted: a full RX ring discards silently, which is less disruptive.

## Snapshots

Nothing DaynaPort-specific is saved, matching `seeq8003`: the backend's sockets
and NAT tables can't be snapshotted anyway, and in-flight frames are dropped.
On restore the interface comes back disabled with empty queues, and the guest
driver's next ENABLE/SET MODE brings it up. A machine reset (`power_on`) does the
same and flushes the NAT tables.

## Verifying it end to end

`hinv` from the PROM command monitor proves INQUIRY and the type-3 dispatch:

```
>> hinv -v
SCSI Device: Controller 0 ID 3
```

Everything past that needs the guest driver. From a checkout of `irixdayna`
next to `iris`:

```sh
cd ../irixdayna
scripts/iris-build.sh --release 5.3 --boot-test
```

The acceptance ladder, in order — each rung isolates a different part of the
protocol:

1. **Detected** — `dp0: DaynaPort SCSI/Link at scsi(0) target N lun 0`
(INQUIRY + type-3 dispatch).
2. **MAC read** — `ifconfig dp0 <ip> up` logs the configured MAC rather than the
`00:80:19:00:00:NN` placeholder (`0x09`, `0x0E`, `0x0C`).
3. **ARP** — `arp -a` after pinging the gateway shows a resolved entry. First
proof both directions work, and the first thing broadcast filtering breaks.
4. **Ping** — `ping 192.168.10.1` gets replies.
5. **TCP** — `ftp`/`telnet` through the gateway; stress-tests the multi-packet
READ path.
6. **Throughput** — an order of magnitude below reference suggests the MORE flag
is never set and every frame costs a full 10 ms poll.

If ARP resolves but ping does not, suspect `pktlen` off by the 4 CRC bytes. If
nothing resolves at all, suspect byte order in the record header, or broadcast
being filtered out. Building the driver with `-DDP_LOG_NET` plus IRIS's own
`eth_summary()` traces gives both ends of every frame.
7 changes: 7 additions & 0 deletions iris-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ appstore = ["bundled"]
# interfaces in a dropdown and the in-process VM can actually bridge onto them.
# Build with: cargo build -p iris-gui --features pcap
pcap = ["iris/pcap"]
# DaynaPort SCSI/Link target — Ethernet over the SCSI bus, selectable per SCSI
# id on the Disks tab. Off by default: it is only useful with a guest driver
# (IRIX: github.com/techomancer/irixdayna), and without one the device just sits
# on the bus. The Disks tab still shows the option in a build without it, with a
# "rebuild with --features daynaport" warning, so a config stays editable.
# Build with: cargo build -p iris-gui --features daynaport
daynaport = ["iris/daynaport"]
# Emulate an R5000 CPU instead of the default R4400 (compile-time: the cache
# model differs deeply). Surfaced read-only on the Memory tab via
# iris::build_features::CPU. Build with: cargo build -p iris-gui --features r5k
Expand Down
96 changes: 81 additions & 15 deletions iris-gui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize};
use std::path::Path;
use iris::config::{
ForwardBind, ForwardProto, GraphicsBoard, JitConfig, MachineConfig, MachineProfile, NetMode,
NfsConfig, PortForwardConfig, ScsiDeviceConfig, VinoSource, VinoStandard, VALID_BANK_SIZES,
NfsConfig, PortForwardConfig, ScsiDeviceConfig, ScsiKind, VinoSource, VinoStandard,
VALID_BANK_SIZES,
};
use iris::nfsudp::NfsVersion;
use iris::vc2_timings::NewportResolution;
Expand Down Expand Up @@ -553,15 +554,53 @@ fn show_disks(ui: &mut Ui, cfg: &mut MachineConfig) -> (PathEdit, ConfigAction)
} else if ui.button("Attach…").clicked() {
cfg.scsi.insert(id, ScsiDeviceConfig {
path: format!("scsi{id}.raw"),
discs: vec![],
cdrom: false,
overlay: false,
scratch: false,
size_mb: None,
..Default::default()
});
}
});
if let Some(dev) = cfg.scsi.get_mut(&id) {
// A DaynaPort has no image, no media and no overlay — it is an
// Ethernet adapter on the SCSI bus. Show its own short form
// instead of the storage rows below.
if dev.is_daynaport() {
Grid::new(("scsi_grid", id)).num_columns(2).striped(true).show(ui, |ui| {
ui.label("Type");
scsi_type_combo(ui, id, dev, &mut edit);
ui.end_row();

if !build_features::DAYNAPORT {
ui.label("");
ui.label(RichText::new(
"⚠ this build lacks DaynaPort support — rebuild with --features daynaport")
.color(Color32::from_rgb(230, 140, 70)));
ui.end_row();
}

ui.label("MAC address")
.on_hover_text("Blank = derived from the SCSI id (00:80:19:44:50:<id>).");
let mut mac = dev.mac.clone().unwrap_or_default();
if ui.add(TextEdit::singleline(&mut mac).hint_text("00:80:19:44:50:03")).changed() {
dev.mac = if mac.trim().is_empty() { None } else { Some(mac) };
edit.changed = true;
}
ui.end_row();

ui.label("NAT subnet")
.on_hover_text("This target runs its own NAT gateway, separate from ec0's. \
Gateway gets .1, the guest gets .2. Blank = 192.168.10.0/24.");
let mut subnet = dev.subnet.clone().unwrap_or_default();
if ui.add(TextEdit::singleline(&mut subnet).hint_text("192.168.10.0/24")).changed() {
dev.subnet = if subnet.trim().is_empty() { None } else { Some(subnet) };
edit.changed = true;
}
ui.end_row();
});
ui.label(RichText::new(
"DaynaPort SCSI/Link — Ethernet over the SCSI bus. The guest needs a driver \
for it (IRIX: github.com/techomancer/irixdayna); IRIX sees it as dp0, separate \
from the onboard ec0.").weak().small());
continue;
}
Grid::new(("scsi_grid", id)).num_columns(2).striped(true).show(ui, |ui| {
ui.label("Image path");
let e = path_row(ui, ("scsi_path", id), &mut dev.path,
Expand Down Expand Up @@ -610,21 +649,14 @@ fn show_disks(ui: &mut Ui, cfg: &mut MachineConfig) -> (PathEdit, ConfigAction)

ui.label("Type");
let was_cd = dev.cdrom;
let mut is_cd = dev.cdrom;
ComboBox::from_id_salt(("type", id))
.selected_text(if is_cd { "CD-ROM" } else { "HDD" })
.show_ui(ui, |ui| {
ui.selectable_value(&mut is_cd, false, "HDD");
ui.selectable_value(&mut is_cd, true, "CD-ROM");
});
scsi_type_combo(ui, id, dev, &mut edit);
// Switching to CD-ROM defaults to an empty drive (no media):
// clear the auto-generated HDD placeholder path so it doesn't
// look like a (missing) disc. Load media via "Insert disc…" in
// the SCSI menu, or just type a path here.
if is_cd && !was_cd && dev.path == format!("scsi{id}.raw") {
if dev.cdrom && !was_cd && dev.path == format!("scsi{id}.raw") {
dev.path.clear();
}
dev.cdrom = is_cd;
ui.end_row();
if dev.cdrom && dev.path.is_empty() {
ui.label("");
Expand Down Expand Up @@ -1591,6 +1623,40 @@ struct PathEdit {
picked: bool,
}

/// SCSI target-type picker. `kind`/`cdrom` are two spellings of the same
/// setting in the config, so write both from one place: a DaynaPort must not
/// keep a stale `cdrom = true`, and a disk/CD-ROM must not keep
/// `kind = "daynaport"`. DaynaPort is offered even in a build without the
/// feature — with a warning next to it — so an existing config stays editable.
fn scsi_type_combo(ui: &mut Ui, id: u8, dev: &mut ScsiDeviceConfig, edit: &mut PathEdit) {
let mut kind = dev.kind();
let before = kind;
ComboBox::from_id_salt(("type", id))
.selected_text(match kind {
ScsiKind::Disk => "HDD",
ScsiKind::Cdrom => "CD-ROM",
ScsiKind::Daynaport => "DaynaPort (Ethernet)",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut kind, ScsiKind::Disk, "HDD");
ui.selectable_value(&mut kind, ScsiKind::Cdrom, "CD-ROM");
ui.selectable_value(&mut kind, ScsiKind::Daynaport, "DaynaPort (Ethernet)");
});
if kind != before {
edit.changed = true;
if kind == ScsiKind::Daynaport {
// No image, no media, no overlay behind a network adapter — and
// leaving them set makes the config fail validation at Start.
dev.path.clear();
dev.discs.clear();
dev.overlay = false;
dev.scratch = false;
}
}
dev.kind_field = kind;
dev.cdrom = kind == ScsiKind::Cdrom;
}

/// A TextEdit + 📁 Browse button that updates `value` in place. See [`PathEdit`].
fn path_row(
ui: &mut Ui,
Expand Down
Loading
Loading