Skip to content

Iris gui net indicator rebased updated#39

Merged
techomancer merged 27 commits into
techomancer:mainfrom
danifunker:iris-gui-net-indicator-rebased-updated
Jun 19, 2026
Merged

Iris gui net indicator rebased updated#39
techomancer merged 27 commits into
techomancer:mainfrom
danifunker:iris-gui-net-indicator-rebased-updated

Conversation

@danifunker

Copy link
Copy Markdown
Contributor

iris-gui: in-core NFS, networking redesign, CHD copy-on-write, macOS App Store polish

TL;DR

This branch makes iris-gui a self-contained, friendly macOS app — no external
helpers, file sharing that "just works," and disks that behave normally:

  1. In-core NFS server (NFSv2 + v3 over UDP) — replaces external unfsd;
    pure-Rust, runs entirely inside the NAT, zero host sockets, sandbox-safe.
  2. Networking config-tab redesign — subnet presets, NAT diagnostics, live
    reconfigure (no reboot), plug-and-play adoption, FTP passive-mode ALG.
  3. CHD copy-on-write + "Synchronizing disks" fold-on-exit — compressed CHDs
    accumulate writes in a .diff.chd; this folds them back into the base so the
    disk acts normal, with a per-disk COW toggle for keep-separate / rollback /
    commit.
  4. GUI/UX fixes + macOS App Store enablement — powered-off overlay, capture
    debounce, NFS share help, a config-save fix, folder-grant for the sandbox,
    NSWorkspace reveal.

1. In-core NFS server (src/nfsudp.rs, src/net.rs)

Pure-Rust NFSv2 (IRIX 5.3) + NFSv3 (IRIX 6.x) over UDP, plus MOUNT (v1/v3) and
portmap, answered entirely inside the user-mode NAT — no unfsd, no host
sockets, works on every platform and inside the App Sandbox. Auto/v2/v3 selectable
in the GUI. Inbound IP reassembly for large writes; outbound fragmentation for
large reads.

  • Fix: NFSv2 READDIR now respects the client's count and pages within a
    single unfragmented UDP datagram — a large ls previously failed with
    "Can't decode result." Regression-tested; confirmed on a real IRIX boot.

2. Networking redesign (iris-gui Networking tab, src/net.rs)

Network/mask preset dropdowns + derived summary, an Add-forward menu, a NET
traffic-light indicator, and a "Check networking" dialog that explains the
plug-and-play adoption model and can set IRIS's subnet to the guest's live. NAT
gains live subnet apply, host-conflict guard on adoption, live port-forward
rebind, and an FTP passive-mode ALG — all without a guest reboot.

3. CHD copy-on-write + sync (src/chd_disk.rs, scsi.rs, wd33c93a.rs, GUI)

Compressed CHDs can't be written in place, so writes land in an uncompressed
.diff.chd (MAME's model). New machinery folds that back:

  • flatten_diff rebuilds the base from the merged reopen_diff view, preserving
    codecs/geometry (compressed stays compressed), via temp + fsync + atomic
    rename
    , then deletes the diff. Crash/cancel-safe (base+diff intact on error).
  • "Synchronizing disks…" progress modal folds pending diffs on a clean exit
    and on a safe Stop.
  • Per-disk Copy-on-write toggle (the existing overlay flag, now honored for
    CHDs): on → always overlay + never auto-fold (keep changes separate); off →
    in-place (uncompressed) / auto-fold (compressed). Defaults off.
  • SCSI menu → Commit / Discard (while stopped) to apply or roll back an
    overlay (CHD .diff.chd or raw .overlay); Discard confirms first. The
    monitor cow status|commit|reset drives CHDs too.

4. GUI / UX + macOS App Store

  • Powered-off overlay — dims the frozen final frame with "⏻ Powered off" once
    the guest soft-powers-off (new precise cpu_stopped signal).
  • Capture debounce — a transient macOS focus flicker no longer drops
    mouse/keyboard capture mid-typing.
  • Config-save fix — picking a path (NFS shared dir, etc.) now marks the config
    dirty so the choice persists (was silently lost).
  • NFS share Help window — exact mount command with the live gateway,
    version-aware notes, a 2 GiB NFSv2 file-size warning, and a limitations section.
  • App Store — "Grant a disk folder…" mints a recursive directory bookmark so
    the CHD fold (and an NFS shared subfolder under it) works under the sandbox;
    "📂 Reveal in file manager" via NSWorkspace (no open subprocess).

Testing

  • Unit/integration: 20 NFS, 28 iris-gui, 3 CHD-flatten/COW, plus NAT/netplan
    tests — all green. Builds clean for default, bundled, and appstore.
  • Verified on a real IRIX boot: NFSv2 ls (the READDIR fix).
  • NOT yet verified on a real boot (planned): CHD COW fold / commit / rollback
    end-to-end; the capture debounce feel; and — on a signed App Store build —
    the folder-grant fold, NSWorkspace reveal, and shared-subfolder creation. The
    destructive core (flatten_diff) is unit-tested (atomic rename, cancel-safe).

Not in scope (follow-ups)

In-place incremental flatten for uncompressed bases (avoids a full rebuild); a
Cancel button on the sync/commit modal; GUI surfacing of cow status.

Files changed outside iris-gui/ (core crate)

All core (src/) changes belong to one of the three features below. (No vendored
or unrelated code — the winit/App-Store work from #38 is already in main.)

In-core NFS server

  • src/nfsudp.rs (new, ~1.9k lines) — the entire server: backing store +
    fileid↔path map, XDR/RPC framing, NFSv2 (IRIX 5.3) and NFSv3 (IRIX 6.x)
    procedures, MOUNT v1/v3, portmap, and the duplicate-request cache. Includes the
    NFSv2 READDIR count/single-datagram fix.
  • src/net.rs — dispatches guest portmap/MOUNT/NFS UDP datagrams to the
    in-core server, with inbound IP reassembly (large writes) and outbound
    fragmentation (large reads). (Also carries the networking work below.)
  • src/config.rsNfsConfig { shared_dir, version } + the NfsVersion
    enum (Auto/v2/v3); removes the old external-unfsd config fields.
  • src/main.rs (standalone iris CLI) — wires the in-core NFS server into
    the CLI's NAT, replacing the external unfsd launch.
  • src/lib.rspub mod nfsudp.

Networking redesign / NAT diagnostics

  • src/net.rs — live NAT subnet apply (no reboot), host-conflict guard on
    plug-and-play adoption, live port-forward rebind, and the FTP passive-mode ALG.
  • src/seeq8003.rs — exposes guest Ethernet frame counts (drives the GUI's
    NET traffic-light) and NAT-diagnostic hooks (SEEQ 8003 = emulated Ethernet).
  • src/z85c30.rs — SCC serial inject/drain hooks used by the in-app serial
    console / networking diagnostics.
  • src/machine.rs (networking part) — accessors for NAT state (expected
    addresses, observed guest IP/gateway) and the live subnet / port-forward requests.

CHD copy-on-write + sync

  • src/chd_disk.rsflatten_diff (fold a .diff.chd into its base,
    preserving codecs; temp + atomic rename), the per-disk COW flag on ChdHd,
    overlay/pending accessors, and pub diff_path_for.
  • src/scsi.rs — extends the COW backend (cow_commit/cow_reset/is_cow/
    cow_dirty_count + pending-sync) to CHD devices.
  • src/wd33c93a.rs — passes the COW flag into ChdHd::open, adds
    sync_chd_disks (fold pending diffs outside the device lock), and the monitor
    cow status|commit|reset command now drives CHDs.
  • src/machine.rs (COW part)pending_chd_sync_count / sync_chd_disks
    accessors the GUI worker calls.
  • src/bin/chd_extract.rs — one-line update for the new ChdHd::open(path, cow)
    signature.

How to review

26 commits on top of upstream/main. Suggested order: src/nfsudp.rs (NFS) →
src/chd_disk.rs + src/scsi.rs + src/wd33c93a.rs (COW core) →
iris-gui/src/{main,handle,config_ui}.rs (UI). Design notes live in
docs/cow-chd-sync-plan.md, docs/nfsudp-plan.md, and
docs/networking-tab-redesign.md.

danifunker and others added 27 commits June 18, 2026 23:26
Add a red/green/grey "NET" dot to the control-column status footer, next
to MIPS, reflecting whether the guest's internal NAT networking is alive:

- grey  — machine not running / halted / idle at the PROM
- red   — running, but no NAT IP traffic seen this run (a hint the guest
          has no IP, or the wrong IP for the configured NAT subnet)
- green — NAT IP traffic has flowed (latched for the run, so it doesn't
          flicker back to red during idle lulls)

The NAT engine counts only guest IP frames (NatControl.guest_frames,
bumped in NatEngine::process) — ARP and other link-layer chatter happen
even with no/wrong IP and would light it green misleadingly. The count is
surfaced via Seeq8003::nat_control / Machine::net_guest_frames, sampled by
the GUI status worker into Status.net_frames, and turned into a NetState
by EmulatorHandle. Pure net_state_for() helper is unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Replace the floating "Click to capture / Ctrl+Alt+Esc" hint drawn over
the framebuffer with a section in the left control column, between the
config controls and the status footer (shown only while a machine is up):

- not captured: "Mouse/Keyboard Capture Disabled" + a "Capture
  mouse/keyboard" button
- captured: "Mouse/Keyboard Captured" + "To disable: Ctrl+Alt+Esc"

Release stays the Ctrl+Alt+Esc hotkey rather than a button: while
captured the host pointer is grabbed by the guest, so a panel button
couldn't be clicked. Factor the capture-engage path out of input::pump
into input::engage_capture so both the framebuffer click and the new
button share it. Drop the now-unused fb_rect.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Three issues hit when launching with no persisted prefs (no gui.json):

- Settings::load() early-returned Self::default() on a missing/unreadable
  file, skipping the scale sanitizer — so vm_scale/ui_scale came back as
  the struct's zero Default (0.0) instead of their real defaults. Always
  run the sanitizer now.
- snap_window_to_fb's clamp(0.05, target) panicked (min > max) when target
  fell below the floor (e.g. a 0.0 vm_scale). Clamp against
  0.05.min(target) so it can't.
- VM_SCALE_DEFAULT 1.0 -> 0.75: 1.0 means 'native, clamped to fit', which
  on a typical laptop clamps to a fractional scale and leaves letterbox
  slack. 0.75 opens target-bound — sized to the 5:4 picture exactly, so
  the window is narrower and the footer reads a clean 0.75x.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…dow size

The window size was persisted and restored, so the vm_scale 'default' only
applied on a true first run and the effective scale drifted across launches
(0.56 / 0.75 / 0.83 depending on the saved size). Make the launcher fit run on
every launch instead, and drop window_size + fullscreen from GuiSettings — only
vm_scale and ui_scale persist now, so the window opens at the configured scale
consistently. Old gui.json files still load (unknown keys ignored).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Networking tab (config_ui.rs, new netplan.rs, if-addrs dep):
- Split the subnet into a plain network-address control + a mask dropdown
  (no CIDR field); the two compose into nat_subnet, snapped to a clean
  network address.
- Network dropdown lists the first-available 192.168/172.16/10 network at
  the selected mask (host interfaces enumerated via if-addrs); mask
  dropdown offers /8 /12 /16 /22 /24 /25 /26 + Custom (a /bits field beside
  the base address).
- Live derived line (gateway, Indy ec0, usable hosts, broadcast), RFC1918 +
  host-overlap conflict checks, snap-to-network for off-boundary input, and
  an "Override Sanity Checks / Use suggested / Cancel" modal.
- Add-forward menu (Telnet/FTP/Custom), NFS blurb + live mount command, and
  the Indy's expected ec0 surfaced in the Check-networking window.
- netplan.rs is pure and unit-tested (subnet math, first-free, conflict,
  classify); all Networking UI strings are ASCII (no tofu glyphs).
- Networking edits now mark the config dirty via a new TabOutcome.

NAT diagnostics backend (net.rs, machine.rs, seeq8003.rs, z85c30.rs,
handle.rs, netfix.rs):
- Passively observe the guest's source IP and the in-subnet gateway it
  ARPs for, surfaced to the GUI's Check-networking window.
- netfix.rs: pure, testable logic for diagnosing ec0 vs the expected NAT
  address and the runtime fix commands.

docs/networking-tab-redesign.md: design + phased plan (Phase 0/1 done;
FTP ALG and the in-app file bridge are still pending).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…-networking dialog

Check-networking dialog:
- Drop the misleading "IRIS expects ec0 at X" line — IRIS is plug-and-play and
  adopts whatever gateway the guest ARPs for, so the guest's own subnet is fine.
  The dialog now explains that and points at the real fix (a default route).
- Offer "Set IRIS's NAT subnet to <guest>/24" when the guest is on a different
  subnet, applied live (no reboot) when running, else saved for next launch.
- If the guest's subnet overlaps a host network, refuse to switch and instead
  tell the user to renumber the guest's ec0 onto IRIS's own subnet (shows the
  exact ifconfig/route commands via netfix). IRIS never shadows the host's LAN.

Live NAT reconfigure (no reboot):
- NatControl::request_subnet + a NAT-thread apply path (swap gateway/client/
  netmask, flush connection tables). Plumbed via Machine::set_nat_subnet and a
  new Cmd::SetNatSubnet(cidr) on the GUI worker.

Host-conflict guard (backend):
- NatControl::set_host_nets / host_conflict; the GUI passes the host's
  interface networks at Start. Plug-and-play adoption now refuses to move onto a
  subnet that overlaps a host network (LAN/VPN/Docker), so a guest sharing the
  host's subnet stays unrouted and the dialog guides the user to renumber it.
- Convert the TEMP NET-DIAG eprintln spam to dlog_dev!(LogModule::Net).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Make the FTP port-forward (host 2121 -> guest 21) usable by a host's own FTP
client. On an inbound FTP control connection (server = gateway, guest port 21),
when the guest's ftpd sends a passive "227 Entering Passive Mode
(h1,h2,h3,h4,p1,p2)" reply, the NAT now:
  - binds a localhost data listener and rewrites the 227 to advertise it, so the
    host client connects to 127.0.0.1:<port> instead of the unreachable guest IP;
  - registers a transient forward from that host port to the guest's data port,
    reusing the existing port-forward machinery.

The length-changing rewrite is safe because the NAT relays application bytes
between an OS host socket and the userspace guest-side TCP (no seq/ack surgery);
client_seq still advances by the original payload length. ftp_pasv_rewrite is
pure and unit-tested.

Transient data forwards are FIFO-bounded (16) and dropped on reset / live subnet
apply. Passive mode only for now (no active/PORT, no EPSV, no cross-segment 227
reassembly). Unverified against a real IRIX boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Decide Phase 3 (in-app file bridge) architecture B (pure NAT-side, no host
sockets). FTP reuses suppaftp via a transport-generic fork rather than a
hand-rolled client; full spec in docs/suppaftp-emu-fork-prompt.md (FtpConnector
trait, default TcpConnector preserves behavior, passive-first, TLS only on the
TCP path, in-memory mock-transport test). With a virtual-net connector the
bridge needs no PASV/ALG rewrite.

Two tracks recorded: the suppaftp-emu fork (separate crate, user-driven) and the
IRIS foundation (NatEngine in-process peer seam + VirtualTcpStream +
VirtualConnector + dual-pane UI + hand-rolled rcp), which resumes once the crate
API exists. Paused after delivering the prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
NAT adoption was gated on guest_frames == 0 (any IP frame), so local or
broadcast IP chatter — e.g. a ping to x.x.x.255 — permanently disarmed
plug-and-play adoption before the guest's gateway ARP could trigger it, leaving
the gateway unanswered. Gate adoption instead on a new `routed` flag, set only
when the guest sends a frame to the gateway's MAC (real off-subnet traffic), so
local/broadcast traffic no longer disarms it. Re-armed on reset and live
subnet-apply.

Also surface the most common "no networking" cause: when IRIX has networking
turned off (/etc/config/network / chkconfig network on) the guest emits no
traffic at all, so the Check-networking window only showed "No guest traffic
seen yet". It now suggests `chkconfig network on`. Documented as the first
"Common mistake" in rules/irix/networking.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
NFS needs the external unfsd (UNFS3) server, which has no clean native Windows
build (Cygwin only) and which macOS bundled / App Store builds can neither ship
nor spawn in the sandbox. Gate the NFS section off on Windows and on macOS
bundled builds (showing a note pointing at a port forward instead, with a button
to clear NFS from an imported config). On macOS source builds, show install
guidance: no Homebrew formula exists, so build UNFS3 from source or use MacPorts.
Linux is unchanged (apt install unfs3).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Port-forward listeners were bound once at NAT construction and never replaced,
so a forward added from the GUI while the machine ran wasn't listening (FTP
client got ECONNREFUSED on 127.0.0.1:2121 until a restart).

Add a live rebind path mirroring the live-subnet apply: NatControl gains
pending_forwards + apply_forwards; the NAT thread rebinds its static listeners
on its next loop (factored the bind loop into bind_forwards; rebind_forwards
swaps the static set and preserves in-flight FTP-ALG data forwards and
established connections). Plumbed via Machine::set_port_forwards and a new
Cmd::SetPortForwards. The Networking tab now reports forwards_changed and the app
pushes the rebind whenever a forward is added/removed/edited while running
(latest-wins coalesces in the NAT thread).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Thorough plan to replace external unfsd with a synchronous, pure-Rust NFSv3/UDP
server living inside the NAT: the whole protocol stays in-process (NAT dispatches
RPC to the server and injects replies; zero host sockets), the only host I/O is
the user's backing folder. Reuses the existing in-NAT portmap, the UDP dispatch
hook in handle_udp, and ip_frames_udp (outbound fragmentation already works).
Vendors nfsserve's XDR/NFS/MOUNT structs (BSD-3) for a sync rewrite; mirrorfs as
the local-dir backend model. Includes phasing (read-only -> read-write -> perf)
and 18 flagged open questions (NFSv2-vs-v3 and UDP duplicate-request cache being
the riskiest).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Foundation for the in-core, pure-Rust NFS server that replaces external unfsd
(plan: docs/nfsv3udp-plan.md). This increment is the version-agnostic backend,
fully unit-tested without IRIS/IRIX/the NAT:

- NfsBacking: one exported folder + a stable fileid<->relative-path map so the
  guest's opaque handles resolve back to host paths.
- Synthetic/"faked" unix attributes (fixed uid/gid 0, mode heuristic dir 0755 /
  file 0644 + exec bit on unix) so the export behaves identically on
  Linux/macOS/Windows.
- File ops: lookup, attr, readdir, read, write, create, mkdir, remove, rmdir,
  rename — all path-contained (rejects .., separators, NUL; one normal component
  only) so the guest can't escape the export.
- 6 unit tests over a temp dir.

Decisions folded into the plan: support NFSv2 (IRIX 5.3) + NFSv3 (IRIX 6.x) by
dispatching on the RPC version (optional Auto/v2/v3 config), full read-write,
inbound IP reassembly, simplest synthetic perms, NFS-only mount instructions.

Next increments: XDR + RPC framing, v2/v3 procedure encoders, MOUNT, NAT wiring
+ inbound reassembly, GUI un-gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Transport-agnostic wire layer shared by NFSv2 and v3 (one UDP datagram = one RPC
message, no TCP record marking):
- Xdr encoder (big-endian, 4-byte aligned: u32/i32/u64/bool/opaque/fixed).
- Cur decode cursor (bounds-checked, returns Option; opaque/fixed consume pad).
- parse_call: parse the RPC CALL header (xid/prog/vers/proc), skip cred+verf
  (every host allowed), hand back a cursor on the procedure args.
- reply: begin an accepted reply with an AUTH_NULL verifier + accept_stat.
- 4 wire unit tests (xdr roundtrip, call parse skips auth, rejects non-call /
  bad rpcvers, reply header bytes). 10 nfsudp tests total.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
fattr3 encoding (full st_mode like knfsd), fileid-as-handle, post_op_attr, and
the read-side NFSv3 procedures over the backend: NULL, GETATTR, LOOKUP, ACCESS
(grants all), READ, READDIR/READDIRPLUS (name-sorted, cookie = stable index,
byte-budgeted paging + eof), FSINFO (advertises rt/wt sizes), FSSTAT, PATHCONF,
plus an nfs3_call dispatcher. 4 new unit tests build real RPC calls and parse the
replies (getattr root = dir, lookup+read, readdir lists entries, fsinfo sizes).
14 nfsudp tests total.

Write-side procedures (increment 4), NFSv2, MOUNT, and the NAT wiring follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…sServer

Write-side NFSv3: SETATTR (honors size/truncate, ignores chmod/chown), WRITE,
CREATE, MKDIR, REMOVE, RMDIR, RENAME, COMMIT (no-op — writes are synchronous),
with wcc_data + sattr3 parsing. NfsServer wraps the backing store, the chosen
NfsVersion (Auto/V2/V3), and a duplicate-request cache (UDP retransmits must not
re-apply non-idempotent ops) — its handle(datagram) is the entry point the NAT
will call. 2 new tests (write-through + DRC dedup of a same-xid retransmit;
create+remove via the server). 16 nfsudp tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Parallel NFSv2 (RFC 1094) wire encoding over the same backend: 32-bit fattr,
fixed 32-byte file handles, microsecond timevals, v2 procedure numbers, and the
v2 procedures (GETATTR/SETATTR/LOOKUP/READ/WRITE/CREATE/MKDIR/REMOVE/RMDIR/
RENAME/READDIR/STATFS). NfsServer.dispatch now routes by RPC version (v3 -> v2
fall-through honoring the Auto/V2/V3 setting), and the duplicate-request cache is
version-aware. 2 new tests (v2 getattr/lookup/read/write; V3-only server rejects
v2 with PROG_MISMATCH). 18 nfsudp tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
MOUNT program 100005: MNT returns the root file handle (v1 = fhstatus + 32-byte
fhandle for NFSv2; v3 = mountstat3 + fhandle3 + AUTH_NULL flavor). NULL, UMNT/
UMNTALL (void), DUMP (empty), EXPORT (single "/" export, anyone). The export path
is ignored — single export, allow all. NfsServer.dispatch routes MOUNT_PROG to
it. 1 new test (MNT v3 + v1 both hand back the root handle). 19 nfsudp tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The NAT now serves NFS in-process: NatEngine owns an Option<NfsServer> built from
the configured export at construction; handle_udp intercepts guest UDP to
gateway:2049 (NFS) / :1234 (mountd) and dispatches to NfsServer::handle, injecting
the reply via ip_frames_udp (large reads auto-fragment). portmap is already
answered in-NAT. Added inbound IP-fragment reassembly to handle_ip (keyed by
src/id/proto, 5s eviction) so large NFS writes work.

Removed the external unfsd path from the CLI (start_unfsd/UnfsdProc) — it spawned
a binary that isn't available cross-platform / in the sandbox, and it cleared
cfg.nfs on failure, which would have disabled the in-core server. The whole NFS
protocol now stays inside the NAT; the only host I/O is the backing folder.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
NFS is now in-process and cross-platform, so:
- NfsConfig drops unfsd/nfs_host_port/mountd_host_port; keeps shared_dir and adds
  a serde NfsVersion (Auto/V2/V3, default Auto), plumbed to NfsServer at start.
- GUI: remove the Windows/macOS-bundled gating and the macOS install hint;
  drop the unfsd-binary + host/mountd-port fields; add an NFS-version dropdown;
  the live mount hint now uses the export root ("mount <gw>:/ /shared").
- Drop the CLI --unfsd/--nfs-port/--mountd-port flags and the dead NFS branches
  in nfs_remap_dst/nfs_unmap_src; drop the unfsd sandbox bookmark.

Builds clean on default + appstore; 19 nfsudp + 28 gui tests pass. The NFS
feature is now code-complete (pure-Rust, zero host sockets, no external binary on
any platform) — pending real-boot validation against IRIX.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A large directory (e.g. `ls` of a real Mac ~/Downloads over NFSv2) failed
on the guest with "NFS2 readdir failed ... Can't decode result": the v2
READDIR path ignored the request's `count` and used a fixed ~8 KB budget,
which overran the client's count-sized decode buffer AND spilled into
multiple IP fragments. Both readdir paths now cap each reply at the
client's count/maxcount; v2 additionally caps to a single unfragmented
UDP datagram and pages the remainder via the cookie.

Regression test: `v2_readdir_pages_within_one_datagram`. Write-up in
rules/irix/. Confirmed working on a real IRIX boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Compressed CHDs can't be written in place, so writes land in an
uncompressed `.diff.chd` sidecar (MAME's model). This adds the machinery
to fold that diff back into the base so the disk "acts normal", and wires
CHDs into the existing per-disk copy-on-write toggle.

Core (`chd_disk.rs`):
- `flatten_diff(base, diff, progress, cancel)` rebuilds the base from the
  merged `reopen_diff` view via `create_from_reader`, preserving the
  base's codecs/geometry (compressed stays compressed), into a temp file
  + fsync + atomic rename over the base, then deletes the diff. Any
  error/cancel leaves base+diff intact. `pub diff_path_for`.
- `ChdHd::open(path, cow)`: COW on -> always overlay (even an uncompressed
  base; base never written in-session) and never auto-fold; COW off ->
  in-place (uncompressed) / auto-folding diff (compressed).
- `pending_sync()` (auto-fold) gated on `!cow`; `overlay_paths()`,
  `is_cow()`, `diff_dirty()` accessors.

Plumbing: `ScsiDevice::{pending_chd_sync, take_pending_chd_sync}` +
`cow_commit`/`cow_reset`/`is_cow`/`cow_dirty_count` extended to CHDs
(commit = flatten + reopen; reset = delete diff + reopen = rollback) ->
`Wd33c93a::{pending_chd_sync_count, sync_chd_disks}` (rebuild outside the
device lock) -> `Machine` accessors. The monitor `cow status|commit|reset`
is backend-agnostic, so it now drives CHDs too.

Tests: flatten_folds_diff_into_compressed_base,
cancelled_flatten_preserves_base_and_diff,
cow_keeps_changes_separate_and_rolls_back. COW defaults off.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…e help, macOS folder grant

CHD copy-on-write / sync:
- "Synchronizing disks…" modal folds pending CHD diffs back into their
  bases on a clean exit (close-intercept) and on a safe Stop, with a
  progress bar (Cmd::SyncDisks, Evt::SyncProgress/SyncDone, status
  `chd_sync_pending`).
- SCSI menu "Copy-on-write changes" section (while stopped): per-disk
  "Commit changes to disk" / "Discard changes (roll back)" — file-level
  worker ops (Cmd::CowCommit/CowReset) so they can't corrupt a running
  guest. Discard asks "Are you sure? You will lose any changes to the
  disk." Disks-tab checkbox relabelled "Copy-on-write".

Other GUI work:
- Powered-off overlay: dim the frozen final frame + "⏻ Powered off" once
  the guest soft-powers-off (new `cpu_stopped` status, distinct from the
  PROM-idle `cpu_halted`).
- Mouse/keyboard capture: debounce focus-loss release (300 ms) so a
  transient macOS focus flicker no longer drops capture mid-typing.
- Fix: picking the NFS shared folder (and other path fields) now marks the
  config dirty — `path_row` returns whether it changed — so the choice
  persists (it was silently lost).
- Help → "Mount the shared folder in IRIX": exact mount command with the
  live gateway, version-aware notes, a 2 GiB NFSv2 file-size warning, and
  a collapsible limitations section. Window auto-sizes (resizable(false)).
- macOS App Store: "Grant a disk folder…" (File menu) mints a recursive
  directory bookmark so the CHD fold (temp + rename beside the base) works
  under the sandbox and an NFS shared subfolder under it is covered too;
  NFS section offers "Use <granted>/shared". "📂 Reveal in file manager"
  on every path field via NSWorkspace (sandbox-safe, no `open` subprocess).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@techomancer

Copy link
Copy Markdown
Owner

awesome, good riddance of unfs3 hack ;-)

@techomancer techomancer merged commit fa5683f into techomancer:main Jun 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants