Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8eee1cd
staging changes for iris-gui default window sizing and 5:4 ratios
danifunker Jun 15, 2026
d0c3bb0
finished staging all files for iris-gui sizing and enhanced power-off…
danifunker Jun 15, 2026
345c54a
Fix App Store 2.5.1 rejection: drop winit's private blur API
danifunker Jun 16, 2026
4f93a83
App Store 2.4.5: make camera + network.server entitlements testable
danifunker Jun 16, 2026
1e5dc22
iris-gui: adaptive framebuffer filtering + left control column
danifunker Jun 16, 2026
673ed6b
iris-gui: add ▶ disclosure arrows to the control-column menus
danifunker Jun 16, 2026
68448a1
jit: drop dead modified_gprs restore (fix unused_assignments warning)
danifunker Jun 16, 2026
470980a
iris-gui: fit window to monitor on open, trim border, Help diagnostics
danifunker Jun 16, 2026
0078089
iris-gui: Help explainer + gate diagnostics + auto-release mouse on halt
danifunker Jun 16, 2026
ea36530
chore: drop local "snow" references (upstream prep)
danifunker Jun 18, 2026
a4801f2
iris-gui: windowed-first sizing, VM-screen scale slider, mouse/menu f…
danifunker Jun 17, 2026
3fd7528
iris-gui: clear the framebuffer on restart so the last frame isn't shown
danifunker Jun 17, 2026
7f480f7
iris-gui: store NVRAM at a stable per-user path (unify across launch …
danifunker Jun 17, 2026
a6218a9
iris-gui: guided "no Ethernet MAC" setup prompt
danifunker Jun 17, 2026
245c34d
iris-gui: hold a no-MAC machine at the PROM instead of booting IRIX
danifunker Jun 17, 2026
9c352d6
iris-gui: detect + auto-write the NVRAM Ethernet MAC (raw bytes at 0x…
danifunker Jun 17, 2026
b8705c0
iris-gui: map Key::Pipe and Key::Questionmark to the guest keyboard
danifunker Jun 17, 2026
e596baa
iris-gui: drop the dead MAC-setup modal / monitor-command fallback
danifunker Jun 17, 2026
47d4b45
iris-gui: show absolute paths in the machine summary
danifunker Jun 17, 2026
a5f31fe
iris-gui: seed a default NVRAM on first run + "Reset NVRAM (fresh PRAM)"
danifunker Jun 17, 2026
eb4550a
iris-gui: create new disk images in a managed, absolute location
danifunker Jun 17, 2026
39fe9b0
iris-gui: don't resize the window when the VM launches
danifunker Jun 18, 2026
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
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,16 @@ name = "chd_extract"
path = "src/bin/chd_extract.rs"
required-features = ["chd"]

# Mac App Store fix (guideline 2.5.1): eframe's winit (0.30) calls the private
# SkyLight API `CGSSetWindowBackgroundBlurRadius`, whose symbol Apple's binary
# scan rejects. Our vendored copy stubs WindowDelegate::set_blur to a no-op and
# drops the private extern declarations; iris-gui never requests window blur,
# so behaviour is unchanged. Only the 0.30.x requirement (eframe → egui-winit →
# glutin-winit) matches this patch; iris's own winit 0.29 dependency is the
# keyboard-KeyCode type only and creates no window in iris-gui, so its blur code
# is dead-stripped. See rules/macos/appstore-private-api.md.
[patch.crates-io]
winit = { path = "third_party/winit-0.30.13" }



108 changes: 108 additions & 0 deletions docs/appstore-review-response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# App Store review response — IRIS (Submission 2ed07ab1…)

Covers the two issues raised on the 1.0 (20260610.2118) review (June 16, 2026):

1. **Guideline 2.5.1** — private API `_CGSSetWindowBackgroundBlurRadius`.
2. **Guideline 2.4.5(i)** — entitlements without obvious matching functionality
(`com.apple.security.device.camera`, `com.apple.security.network.server`).

---

## 1. Guideline 2.5.1 — private API (fixed in binary)

The symbol came from the `winit` windowing crate (pulled in by `eframe`), whose
macOS backend calls `CGSSetWindowBackgroundBlurRadius` in `WindowDelegate::set_blur`.
IRIS never requests window blur, but the symbol is linked in regardless and
Apple's static scan flags it.

**Fix:** vendored a patched `winit` (`third_party/winit-0.30.13/`, wired via
`[patch.crates-io]` in the root `Cargo.toml`) that removes the private extern
declarations and makes `set_blur` a no-op. Verified the symbol is gone from the
linked binary:

```
nm -u target/release/iris-gui | grep CGSSetWindowBackgroundBlurRadius # → no output
```

(`_CGShieldingWindowLevel` remains; it is a public CoreGraphics API and was not
flagged.) See `rules/macos/appstore-private-api.md`.

A new binary is required for this fix, so we also strengthened the two
entitlements below with visible, testable functionality rather than removing
them.

---

## 2. Guideline 2.4.5(i) — entitlement justifications

Both entitlements back real functionality. To make them easy to verify we added
in-app features that exercise each one directly, without needing to boot IRIX.

### `com.apple.security.device.camera`

**What it's for:** IRIS emulates the SGI Indy's **IndyCam** / VINO video-input
hardware. When the user selects the host camera as the video source, IRIS
captures live frames from the Mac's camera (AVFoundation) and feeds them to the
emulated VINO device. The matching `NSCameraUsageDescription` is in `Info.plist`.

**How to test (reviewer steps):**
1. Launch IRIS. In the launcher, open the **Video-In** tab.
2. Click **📷 Test Camera**.
3. macOS shows the camera-permission prompt; allow it.
4. A live preview from the Mac camera appears, with a status line showing the
capture resolution and a rising frame count. Closing the window releases the
camera (indicator light turns off).

> Paste-ready reply:
>
> IRIS emulates the SGI Indy IndyCam (VINO video-input) hardware. The camera
> entitlement lets the app capture live video from the Mac's camera and feed it
> to the emulated video-input device. You can verify this directly: open the
> **Video-In** tab and click **Test Camera** — macOS will prompt for camera
> access and the app then shows a live preview from the camera. The matching
> NSCameraUsageDescription is included in Info.plist.

### `com.apple.security.network.server`

**What it's for:** two things —
- The emulator exposes the emulated machine's **serial console** (IRIX ttyd1,
`127.0.0.1:8881`) and **PROM monitor** on loopback TCP, so a terminal can
attach to the guest console. The app's own **Serial console…** window connects
to this server (loopback), which is the visible end-to-end demonstration: the
emulator *listens* (network.server) and the viewer *connects* (network.client).
- It binds **inbound port-forwards** into the emulated SGI Ethernet (SEEQ 8003)
when the user configures them on the Networking tab.

(The clean-shutdown "Send IRIX halt" action no longer uses a socket — it now
types at the console in-process — so the server entitlement is only used for the
genuine server features above.)

**How to test (reviewer steps):**
1. Launch IRIS and **Start** a machine (the bundled config boots to the PROM).
2. Open **Machine → Serial console…**.
3. The window shows "● connected to 127.0.0.1:8881" and streams the live guest
serial console. Typing a line and pressing Enter sends it to the guest.
This confirms the app's loopback serial **server** is live and accepting a
connection.

> Paste-ready reply:
>
> IRIS exposes the emulated workstation's serial console and PROM monitor as
> loopback TCP servers (e.g. 127.0.0.1:8881) so a terminal can attach to the
> guest console, and it binds user-configured inbound port-forwards into the
> emulated Ethernet. You can verify this without external tools: Start a machine
> and open **Machine → Serial console…** — the app connects to its own loopback
> serial server and streams the live guest console, and you can type into it.

---

## Summary of binary changes in this resubmission

- Vendored/patched `winit` to drop the private blur API (2.5.1). No window blur
was ever used.
- Added **Video-In → Test Camera** (live host-camera preview) so the camera
entitlement is user-visible.
- Added **Machine → Serial console…** (in-app loopback serial viewer) so the
network.server entitlement is user-visible.
- Moved "Send IRIX halt" to an in-process console path (no longer opens a
loopback socket).
14 changes: 12 additions & 2 deletions installer/iris-gui.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
<key>com.apple.security.cs.allow-jit</key>
<true/>

<!-- IndyCam / VINO emulation -->
<!-- Host-camera capture for the emulated IndyCam (VINO video-in source).
User-facing: Video-In tab → "Test Camera" (live preview), and
[vino] source = "camera" once IRIX is booted. Requires the matching
NSCameraUsageDescription in Info.plist. See
docs/appstore-review-response.md. -->
<key>com.apple.security.device.camera</key>
<true/>

Expand All @@ -34,9 +38,15 @@
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>

<!-- Ethernet emulation (SEEQ 8003) -->
<!-- Outbound: emulated SEEQ 8003 Ethernet (guest → internet via user-mode
NAT) and the in-app serial-console viewer connecting to loopback. -->
<key>com.apple.security.network.client</key>
<true/>
<!-- Inbound (listen/accept): the emulator exposes the guest's serial
console (ttyd1, 127.0.0.1:8881) and PROM monitor on loopback TCP — the
in-app "Serial console…" viewer connects to these — and binds
user-configured inbound port-forwards into the emulated Ethernet.
See docs/appstore-review-response.md. -->
<key>com.apple.security.network.server</key>
<true/>
</dict>
Expand Down
Binary file added iris-gui/assets/nvram-default.bin
Binary file not shown.
150 changes: 150 additions & 0 deletions iris-gui/src/camera_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! "Test Camera" support.
//!
//! Opens the host camera through the same [`iris::camera::CameraSource`] the
//! VINO / IndyCam emulation uses, on a background thread, and parks the latest
//! frame (converted to RGBA) plus a status line for the GUI to display. This
//! gives the user — and an App Review tester — a way to confirm the host-camera
//! capability works (it triggers the macOS camera-permission prompt and shows a
//! live preview) without booting IRIX and configuring the VINO video source.
//!
//! Dropping `CameraTest` stops the worker, which drops the `CameraSource` and
//! releases the camera (indicator light off).

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;

use parking_lot::Mutex;

use iris::camera::CameraSource;
use iris::video_source::{Field, VideoSource, VideoStandard};

#[derive(Default)]
struct Shared {
/// One-line capture status (frame count, capture resolution, …).
status: String,
/// Set if the camera could not be opened (permission denied / no device).
error: Option<String>,
/// Latest preview frame: (width, height, RGBA bytes).
frame: Option<(u32, u32, Vec<u8>)>,
/// Bumped on each new frame so the GUI can skip redundant texture uploads.
seq: u64,
}

pub struct CameraTest {
shared: Arc<Mutex<Shared>>,
running: Arc<AtomicBool>,
worker: Option<JoinHandle<()>>,
}

impl CameraTest {
/// Start capturing from host camera `index` using `standard`'s field size.
pub fn start(standard: VideoStandard, index: u32) -> Self {
let shared = Arc::new(Mutex::new(Shared {
status: "opening camera…".into(),
..Shared::default()
}));
let running = Arc::new(AtomicBool::new(true));
let s2 = shared.clone();
let r2 = running.clone();

let worker = std::thread::Builder::new()
.name("iris-gui-camtest".into())
.spawn(move || {
let cam = match CameraSource::new_with_index(standard, index) {
Ok(c) => c,
Err(e) => {
s2.lock().error = Some(e);
return;
}
};
// next_field() paces itself to the field rate, so this loop
// runs at ~50–60 Hz without a manual sleep.
while r2.load(Ordering::Relaxed) {
let field = cam.next_field();
let rgba = uyvy_field_to_rgba(&field);
let status = cam.status();
let mut g = s2.lock();
g.status = status;
g.frame = Some((field.width, field.height, rgba));
g.seq = g.seq.wrapping_add(1);
g.error = None;
}
// `cam` drops here → camera stream closed, device released.
})
.expect("spawn camera-test worker");

Self { shared, running, worker: Some(worker) }
}

pub fn status(&self) -> String {
self.shared.lock().status.clone()
}

pub fn error(&self) -> Option<String> {
self.shared.lock().error.clone()
}

/// Return the latest frame if it is newer than `last_seq` (which is then
/// advanced). `None` when there is nothing new to upload.
pub fn take_new_frame(&self, last_seq: &mut u64) -> Option<(u32, u32, Vec<u8>)> {
let g = self.shared.lock();
if g.seq != *last_seq {
*last_seq = g.seq;
g.frame.clone()
} else {
None
}
}
}

impl Drop for CameraTest {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
if let Some(w) = self.worker.take() {
let _ = w.join();
}
}
}

/// Convert one packed UYVY 4:2:2 field to RGBA8 (BT.601 limited range).
/// Each 4-byte group `U Y0 V Y1` yields two pixels sharing the U/V chroma.
fn uyvy_field_to_rgba(field: &Field) -> Vec<u8> {
let w = field.width as usize;
let h = field.height as usize;
let src = &field.pixels;
let mut out = vec![0u8; w * h * 4];

for y in 0..h {
let row = y * w * 2;
for pair in 0..(w / 2) {
let i = row + pair * 4;
if i + 3 >= src.len() {
break;
}
let u = src[i] as i32;
let y0 = src[i + 1] as i32;
let v = src[i + 2] as i32;
let y1 = src[i + 3] as i32;

let o = (y * w + pair * 2) * 4;
yuv_to_rgba(y0, u, v, &mut out[o..o + 4]);
yuv_to_rgba(y1, u, v, &mut out[o + 4..o + 8]);
}
}
out
}

#[inline]
fn yuv_to_rgba(y: i32, u: i32, v: i32, out: &mut [u8]) {
let c = y - 16;
let d = u - 128;
let e = v - 128;
let r = (298 * c + 409 * e + 128) >> 8;
let g = (298 * c - 100 * d - 208 * e + 128) >> 8;
let b = (298 * c + 516 * d + 128) >> 8;
out[0] = r.clamp(0, 255) as u8;
out[1] = g.clamp(0, 255) as u8;
out[2] = b.clamp(0, 255) as u8;
out[3] = 255;
}
45 changes: 43 additions & 2 deletions iris-gui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ pub enum ConfigAction {
/// and, if accepted, clear `cfg.prom` (an empty path falls back to the
/// built-in PROM in `iris::prom::Prom::from_file_or_embedded`).
RequestEmbeddedProm,
/// User clicked "Test Camera" on the Video-In tab; the app should open the
/// host camera and show a live preview (using the current `[vino]` standard
/// and camera index).
TestCamera,
}

pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv) -> ConfigAction {
Expand All @@ -102,7 +106,7 @@ pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv
Tab::Network => { show_network(ui, cfg); ConfigAction::None }
Tab::Memory => { show_memory(ui, cfg); ConfigAction::None }
Tab::Display => { show_display(ui, cfg); ConfigAction::None }
Tab::VideoIn => { show_vino(ui, cfg); ConfigAction::None }
Tab::VideoIn => show_vino(ui, cfg),
Tab::Debug => { show_debug(ui, cfg, jit); ConfigAction::None }
Tab::Ci => { show_ci(ui, cfg); ConfigAction::None }
}).inner
Expand Down Expand Up @@ -370,7 +374,8 @@ fn show_network(ui: &mut Ui, cfg: &mut MachineConfig) {
}
}

fn show_vino(ui: &mut Ui, cfg: &mut MachineConfig) {
fn show_vino(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction {
let mut action = ConfigAction::None;
ui.heading("Video-In (IndyCam)");
Grid::new("vino_grid").num_columns(2).striped(true).show(ui, |ui| {
ui.label("Source");
Expand Down Expand Up @@ -407,6 +412,42 @@ fn show_vino(ui: &mut Ui, cfg: &mut MachineConfig) {
ui.add(DragValue::new(&mut cfg.vino.camera_index).range(0..=15));
ui.end_row();
});

// Live host-camera test: opens the selected camera directly (no IRIX boot
// needed) so the user can confirm the capture path works — and, on macOS,
// grant the camera permission. This exercises the same host-capture code
// the VINO/IndyCam source uses.
ui.add_space(8.0);
if build_features::CAMERA {
ui.horizontal(|ui| {
if ui.button("📷 Test Camera").clicked() {
action = ConfigAction::TestCamera;
}
ui.label(
RichText::new(format!(
"Preview host camera #{} live ({}).",
cfg.vino.camera_index,
match cfg.vino.standard { VinoStandard::Ntsc => "NTSC", VinoStandard::Pal => "PAL" },
))
.weak(),
);
});
ui.label(
RichText::new(
"On first use macOS will ask for camera permission. The camera \
is released when you close the preview.",
)
.weak()
.small(),
);
} else {
ui.label(
RichText::new("Camera test unavailable — this build was compiled without --features camera.")
.weak(),
);
}

action
}

fn show_debug(ui: &mut Ui, cfg: &mut MachineConfig, jit: &mut JitEnv) {
Expand Down
Loading
Loading