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
25 changes: 16 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,28 @@ ultra64 = ["dep:shared_memory", "dep:raw_sync"]
[dependencies]
clap = { version = "4", features = ["derive"] }
log = "0.4"
env_logger = "0.10"
env_logger = "0.11"
winit = "0.30"
glutin = "0.32"
glutin-winit = "0.5"
glow = "0.13"
raw-window-handle = "0.6"
rtrb = "0.3"
socket2 = { version = "0.5", features = ["all"] }
socket2 = { version = "0.6", features = ["all"] }
crossbeam-utils = "0.8"
bitfield = "0.14"
cpal = "0.15"
bitfield = "0.19"
cpal = "0.18"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0"
toml = "1.0.3"
postcard = { version = "1", features = ["alloc"] }
blake3 = "1"
png = "0.17"
png = "0.18"
parking_lot = "0.12"
spin = "0.10.0"
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"] }
spin = "0.12"
# rfd >= 0.16 dropped the async-std/tokio runtime features: `xdg-portal` now
# pulls its own `pollster` executor to block on the portal's async D-Bus calls.
rfd = { version = "0.17", default-features = false, features = ["xdg-portal"] }
gdbstub = { version = "0.7", features = ["std"] }
gdbstub_arch = "0.3"
cranelift-codegen = { version = "0.134", optional = true }
Expand All @@ -125,11 +127,16 @@ cranelift-jit = { version = "0.134", optional = true }
cranelift-module = { version = "0.134", optional = true }
cranelift-native = { version = "0.134", optional = true }
target-lexicon = { version = "0.13", optional = true }
libchdman-rs = { version = "0.288.9", features = ["prebuilt"], optional = true }
libchdman-rs = { version = "0.289", features = ["prebuilt"], optional = true }
pcap = { version = "2", optional = true }
shared_memory = { version = "0.12", optional = true }
raw_sync = { version = "0.1", optional = true }
windows-sys = { version = "0.52", features = ["Win32_System_Threading"] }
# thread_affinity.rs calls SetThreadAffinityMask/GetCurrentThread, whose
# signatures name Foundation::HANDLE — so Win32_Foundation is required on top of
# Win32_System_Threading. It used to arrive by feature unification from rfd /
# socket2 / anstyle-wincon; declare it so the Windows build doesn't depend on
# who else happens to be in the graph.
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading"] }

[target.'cfg(target_os = "macos")'.dependencies]
nokhwa = { version = "0.10", features = ["input-avfoundation"], optional = true }
Expand Down
14 changes: 8 additions & 6 deletions iris-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,21 @@ parking_lot = "0.12"
# Match iris's winit version — Ps2Controller::push_kb takes
# winit::keyboard::KeyCode.
winit = "0.30"
png = "0.17"
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"] }
png = "0.18"
# rfd >= 0.16 dropped the async-std/tokio runtime features: `xdg-portal` now
# pulls its own `pollster` executor to block on the portal's async D-Bus calls.
rfd = { version = "0.17", default-features = false, features = ["xdg-portal"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
dirs = "5"
toml = "1"
dirs = "6"
log = "0.4"
env_logger = "0.10"
env_logger = "0.11"
# Read the host's own interface addresses (Networking tab: first-free subnet
# presets + overlap warnings). Reads addresses via getifaddrs — no entitlement,
# and it does not trigger the macOS 15 Local Network prompt (that's for talking
# *to* LAN peers, not enumerating your own NICs).
if-addrs = "0.13"
if-addrs = "0.15"

# macOS App Sandbox: NSURL security-scoped bookmarks let the App Store build
# reopen user-selected disk images / PROMs / ISOs across launches. These crates
Expand Down
2 changes: 1 addition & 1 deletion iris-gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn load_icon() -> egui::IconData {
const ICON_PNG: &[u8] = include_bytes!("../assets/icons/icon-256.png");
let decoder = png::Decoder::new(std::io::Cursor::new(ICON_PNG));
let mut reader = decoder.read_info().expect("decode icon PNG header");
let mut rgba = vec![0u8; reader.output_buffer_size()];
let mut rgba = vec![0u8; reader.output_buffer_size().expect("icon PNG size fits usize")];
let info = reader.next_frame(&mut rgba).expect("decode icon PNG pixels");
rgba.truncate(info.buffer_size());
egui::IconData {
Expand Down
91 changes: 91 additions & 0 deletions rules/build/dependency-upgrade-gotchas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Dependency upgrade gotchas (2026-08 sweep)

Notes from the crate-update pass that moved iris off `bitfield 0.14 / cpal 0.15
/ env_logger 0.10 / png 0.17 / rfd 0.15 / socket2 0.5 / spin 0.10 /
windows-sys 0.52 / libchdman-rs 0.288` and iris-gui off `dirs 5 / if-addrs 0.13
/ toml 0.8`. Only the non-obvious parts are recorded here.

## windows-sys: `Win32_Foundation` was arriving by accident

`src/thread_affinity.rs` calls `SetThreadAffinityMask`/`GetCurrentThread`, and
both signatures name `Foundation::HANDLE`. windows-sys gates that module behind
the **`Win32_Foundation`** feature, which `Win32_System_Threading` does *not*
imply — in 0.52 the generated binding even carried a
`#[doc = "Required features: \"Win32_Foundation\""]` marker.

iris only ever declared `Win32_System_Threading`. The Windows build worked
purely because `rfd`, `socket2`, and `anstyle-wincon` resolve to the *same*
windows-sys version and enable `Win32_Foundation` themselves, so Cargo's feature
unification filled the gap. That is a build that depends on who else happens to
be in the graph — drop or re-version any of those crates and the Windows build
breaks in a way that reproduces on no other platform.

`Cargo.toml` now declares both features explicitly. **Don't "simplify" that back
down to one feature.**

macOS/Linux dev boxes cannot catch this: `windows-sys` is a hard (non
target-gated) dependency, so Cargo *validates the feature names* everywhere, but
the module itself only compiles on Windows. Cargo accepting the manifest proves
nothing about the Windows build.

## cpal 0.18 API churn (`src/hal2.rs`)

Four unrelated breaks in the audio-output path:

- `SampleRate` is now `pub type SampleRate = u32` — a plain alias, not a tuple
struct. `cpal::SampleRate(rate)` → `rate`.
- All error types collapsed into one `cpal::Error` + `ErrorKind`. The stream
error callback takes `cpal::Error`; `cpal::StreamError` is gone.
- `build_output_stream` takes `config: StreamConfig` **by value**. `StreamConfig`
is `Copy`, so the f32-then-i16 fallback can still pass the same config twice.
- `DeviceTrait::name()` is gone. `DeviceTrait: Display`, so the device name is
`device.to_string()` / `{}`. (`description()` returns the structured
`DeviceDescription` if more than the name is ever needed.)

`HostTrait` lost `id()`, but the concrete `cpal::Host` keeps an inherent `id()`,
so `host.id()` still compiles.

## rfd ≥ 0.16 dropped the async-runtime features

`rfd 0.15` took `features = ["xdg-portal", "async-std"]`. 0.16+ removed the
`async-std`/`tokio` executor features outright; `xdg-portal` now implies its own
`pollster` executor. The manifest is just `features = ["xdg-portal"]` — listing
`pollster` alongside it is redundant.

rfd also gained a `wayland` feature (for parenting dialogs to a Wayland surface).
iris calls no `set_parent`, so it stays off, and this is not a regression: the
old `default-features = false` config never had an equivalent either.

## Smaller renames

- **socket2 0.6**: `Socket::set_ttl` → `set_ttl_v4` (disambiguated from the IPv6
hop limit). `src/net.rs` sets it on an `IPV4` ICMP socket, so the rename is a
straight substitution.
- **png 0.18**: `Reader::output_buffer_size()` returns `Option<usize>` (`None`
when the size would overflow `usize`).

## Verified drop-ins — don't re-derive these

- **`dirs` 5 → 6 does not move any user data.** `config_dir()` and `data_dir()`
are byte-identical in both versions on Linux (XDG), macOS (Application
Support), and Windows (Roaming AppData). iris-gui's `gui.json` machine store
and `iris-gui.pid` stay exactly where they were; no migration needed.
- **`bitfield` 0.14 → 0.19 is a clean drop-in** despite five major versions.
All eight `bitfield! { ... }` blocks (rex3, vc2, mips_cache_v2, hal2,
saa7191, mips_exec) compile untouched.
- **`libchdman-rs` 0.288 → 0.289** needs no code change. The `chd_disk` tests
exercise it for real — they create compressed CHDs, write COW diffs, and
flatten them — so a green `cargo test --features chd chd_disk` is meaningful
coverage of the upgrade, not just a compile check.

## What this sweep deliberately left alone

`egui`/`eframe` (0.35, though 0.36.1 exists) and the whole windowing stack that
shares crates with them: `winit`, `glutin`, `glutin-winit`, `glow`,
`raw-window-handle`. `winit` in particular is `[patch.crates-io]`-ed to
`third_party/winit-0.30.13` for the App Store private-API fix — see
`rules/macos/appstore-private-api.md` before touching it.

Side effect worth knowing: iris's own `glow` (0.13) and egui's (0.17) are still
two versions in the lockfile. `png` used to be split the same way and is now
unified on 0.18.
153 changes: 153 additions & 0 deletions scripts/build-manifest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/bin/bash
# Emit a build manifest: every crate version that went into a specific build,
# plus the toolchain and git state that produced it.
#
# Why: Cargo.lock is gitignored (see rules/build/dependency-upgrade-gotchas.md),
# so every CI run re-resolves the whole graph from scratch. A build that was
# green yesterday can go red today because a transitive crate five levels down
# shipped a new minor — and nothing in the diff shows it. Shipping this file
# with each release turns that class of failure into a `diff` of two manifests.
#
# Usage — pass the SAME cargo selection flags the build used, after `--`:
#
# ./scripts/build-manifest.sh -o dist/manifest.txt -- -p iris-gui --features premiere,pcap
# ./scripts/build-manifest.sh -o dist/manifest.txt -- --features lightning,rex-jit,chd
# ./scripts/build-manifest.sh # defaults: root pkg, default features, stdout
#
# Run it AFTER the build, in the same job, on the same machine. It reads the
# Cargo.lock the build just produced; run it before, or with different feature
# flags, and it describes a resolve that never shipped.
#
# The fingerprint at the bottom is a sha256 of the crate list alone. Two
# releases with the same fingerprint had byte-identical dependency graphs, so a
# behaviour difference between them is NOT a dependency change — look
# elsewhere. Different fingerprints: `diff` the two manifests and the culprit
# is on the changed line.

set -euo pipefail

OUT=""
CARGO_ARGS=()

while [ $# -gt 0 ]; do
case "$1" in
-o|--output) OUT="$2"; shift 2 ;;
--) shift; CARGO_ARGS=("$@"); break ;;
-h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $1 (cargo flags go after --)" >&2; exit 2 ;;
esac
done

# cargo has to run from the workspace, but a relative -o should still land where
# the caller expects, not silently under the repo root.
case "${OUT:-}" in
""|/*) ;;
*) OUT="$PWD/$OUT" ;;
esac
cd "$(dirname "$0")/.."

# --- git state ---------------------------------------------------------------
if git rev-parse --git-dir >/dev/null 2>&1; then
GIT_COMMIT=$(git rev-parse --short HEAD)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ -n "$(git status --porcelain)" ]; then
GIT_STATE="DIRTY (uncommitted changes — this build is not reproducible from git)"
else
GIT_STATE="clean"
fi
else
GIT_COMMIT="(not a git checkout)"; GIT_BRANCH="-"; GIT_STATE="-"
fi

# --- what was actually selected ----------------------------------------------
# cargo tree resolves for the host target unless --target is passed through, and
# honours -p / --features exactly as cargo build does. Keeping the args verbatim
# is what makes this manifest describe THIS build rather than some other one.
HOST=$(rustc -vV | awk '/^host:/ {print $2}')
SELECTION="${CARGO_ARGS[*]:-(none — root package, default features)}"

# {p} renders "name vX.Y.Z" for registry crates and appends the path/git source
# for everything else, so [patch.crates-io] entries (the vendored winit) are
# visible rather than masquerading as the crates.io release.
# The ${a[@]+"${a[@]}"} dance keeps `set -u` happy on bash 3.2 (what macOS
# ships) when no cargo args were passed — a plain "${a[@]}" is an unbound-
# variable error there, and "${a[@]:-}" would smuggle in an empty argument.
TREE=$(cargo tree --edges normal --prefix none --format '{p}' \
${CARGO_ARGS[@]+"${CARGO_ARGS[@]}"} 2>/dev/null \
| sed 's/ (\*)$//' | grep -v '^[[:space:]]*$' | sort -u)

CRATES=$(printf '%s\n' "$TREE" | sed -E 's/^([^ ]+) v([^ ]+).*/\1 \2/' | sort -u)

# Non-registry = a path or URL source in the trailing parens. Excludes the
# "(proc-macro)" marker cargo also renders there, and excludes this workspace's
# own members (always path deps, never interesting) so the section shows only
# genuine overrides — the [patch.crates-io] winit, or any git dependency.
# --no-deps limits `packages` to workspace members. The "name" -> "version" key
# pair is unique to a package entry; dependency entries pair "name" with
# "source"/"req", so this can't accidentally match one.
MEMBERS=$(cargo metadata --no-deps --format-version 1 2>/dev/null \
| grep -o '"name":"[^"]*","version":"' | sed -E 's/"name":"([^"]*)".*/\1/' || true)
NONREG=$(printf '%s\n' "$TREE" | grep -E ' \((/|[a-z+]+://)' || true)
for m in $MEMBERS; do
NONREG=$(printf '%s\n' "$NONREG" | grep -v "^${m} v" || true)
done
NONREG=$(printf '%s\n' "$NONREG" | grep -v '^[[:space:]]*$' || true)

COUNT=$(printf '%s\n' "$CRATES" | grep -c '' || true)
FINGERPRINT=$(printf '%s\n' "$CRATES" | { shasum -a 256 2>/dev/null || sha256sum; } | cut -d' ' -f1)

# --- emit --------------------------------------------------------------------
emit() {
cat <<EOF
IRIS build manifest
===================
git commit : $GIT_COMMIT ($GIT_BRANCH)
git state : $GIT_STATE
built (UTC) : $(date -u +%Y-%m-%dT%H:%M:%SZ)
host : $HOST
cargo select : $SELECTION

toolchain
---------
$(rustc --version)
$(cargo --version)

crates ($COUNT resolved)
$(printf '%.0s-' $(seq 1 $((${#COUNT} + 18))))
$CRATES
EOF

if [ -n "$NONREG" ]; then
cat <<EOF

non-registry sources
--------------------
These did NOT come from crates.io. A [patch.crates-io] path override or git
dependency means the published version number alone does not identify the code
that shipped — check the patch table in Cargo.toml before trusting a version
match here.

$NONREG
EOF
fi

cat <<EOF

dependency fingerprint
----------------------
sha256(crate list) = $FINGERPRINT

Same fingerprint as a previous release => identical dependency graph, so any
behaviour change between them came from iris's own code or the toolchain, not
from a crate update. Different => diff the two manifests to find which crate
moved.
EOF
}

if [ -n "$OUT" ]; then
mkdir -p "$(dirname "$OUT")"
emit > "$OUT"
echo "wrote $OUT ($COUNT crates, fingerprint ${FINGERPRINT:0:12})"
else
emit
fi
12 changes: 6 additions & 6 deletions src/hal2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,11 @@ fn open_persistent_output(underruns: Arc<AtomicU64>, playing: Arc<AtomicBool>) -
for &rate in PREFERRED_RATES {
let config = cpal::StreamConfig {
channels: 2,
sample_rate: cpal::SampleRate(rate),
sample_rate: rate,
buffer_size: cpal::BufferSize::Default,
};
let ring_size = prebuf_samples(rate) * RING_BUF_MULTIPLIER;
let err_fn = |err: cpal::StreamError| { eprintln!("HAL2: cpal stream error: {:?}", err); };
let err_fn = |err: cpal::Error| { eprintln!("HAL2: cpal stream error: {:?}", err); };

// Try f32 first (macOS CoreAudio native), then i16 (Linux ALSA).
let (producer, stream) = {
Expand All @@ -335,7 +335,7 @@ fn open_persistent_output(underruns: Arc<AtomicU64>, playing: Arc<AtomicBool>) -
};
}
};
match device.build_output_stream(&config, data_fn, err_fn.clone(), None) {
match device.build_output_stream(config, data_fn, err_fn.clone(), None) {
Ok(s) => (p, s),
Err(_) => {
// f32 failed, try i16
Expand All @@ -355,7 +355,7 @@ fn open_persistent_output(underruns: Arc<AtomicU64>, playing: Arc<AtomicBool>) -
};
}
};
match device.build_output_stream(&config, data_fn, err_fn.clone(), None) {
match device.build_output_stream(config, data_fn, err_fn.clone(), None) {
Ok(s) => (p, s),
Err(e) => {
eprintln!("HAL2: cpal build_output_stream failed at {}Hz: {:?}", rate, e);
Expand All @@ -366,8 +366,8 @@ fn open_persistent_output(underruns: Arc<AtomicU64>, playing: Arc<AtomicBool>) -
}
};
if stream.play().is_err() { continue; }
println!("HAL2: audio output: {:?} via {:?} at {}Hz",
device.name().unwrap_or_default(), host.id(), rate);
// cpal 0.18 dropped DeviceTrait::name(); a Device's Display impl is its name.
println!("HAL2: audio output: {} via {:?} at {}Hz", device, host.id(), rate);
return Some(AudioOut {
stream_rate: rate,
producer,
Expand Down
2 changes: 1 addition & 1 deletion src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1524,7 +1524,7 @@ impl NatEngine {
// at the right hop count. On Windows (SOCK_RAW) and macOS (SOCK_DGRAM) those
// replies arrive back on this socket and we forward them to the guest. On Linux they
// are silently dropped by the kernel — traceroute sees * * *.
let _ = sock.set_ttl(ttl as u32);
let _ = sock.set_ttl_v4(ttl as u32);
let dest = SocketAddr::new(IpAddr::V4(dst_ip), 0);
let _ = sock.send_to(payload, &dest.into());
}
Expand Down
Loading