Skip to content

feat(ios): embedded iOS simulator (no Simulator.app) — I1 headless frame acquisition - #72

Merged
dev-josias merged 8 commits into
mainfrom
feat/ios-embedded-stream
Jul 3, 2026
Merged

feat(ios): embedded iOS simulator (no Simulator.app) — I1 headless frame acquisition#72
dev-josias merged 8 commits into
mainfrom
feat/ios-embedded-stream

Conversation

@dev-josias

Copy link
Copy Markdown
Contributor

Big brick, first slice. Embed the iOS simulator directly in UMIDE without the separate Simulator.app window — the iOS half of what the embedded Android emulator already does. Proven live end-to-end: a real 1179×2556 iPhone-16 home screen captured through UMIDE's own gRPC client from a fully headless simulator (Simulator.app closed).

The load-bearing discovery

A simctl boot-ed device has no rendering surface — idb's screenshot returns "No Image available to encode" and even simctl io screenshot hangs ~60s. Booting via idb_companion --boot (IosCompanion::boot_headless) attaches the CoreSimulator framebuffer without opening Simulator.app, and the screenshot RPC then returns a live frame.

Architecture decision: the embedded panel's frame source is the on-demand screenshot RPC polled at a target fps — robust regardless of screen activity, decoded via the image crate to RGBA and fed to the existing VideoFrame/frame_signal pipeline. H.264 video_stream is a future low-latency optimization (doesn't emit over companion v1.1.8).

What's in I1

  • proto/idb.proto — a wire-compatible subset of facebook/idb v1.1.8 (video_stream + hid + screenshot); trimming also sidesteps a prost codegen name collision in the full file.
  • build.rs compiles it beside emulator_controller.proto (client only).
  • src/ios_grpc.rsIosCompanion (spawn serving companion, read gRPC port, boot_headless, available() for graceful fallback), connect(), screenshot(), video_stream_h264(), payload_bytes(). Unit-tested port parser.
  • examples/ios_grab_frame.rs — the milestone proof (iOS twin of grab_frame).

External dep: brew install idb-companion; available() gates the embedded path so the panel falls back to Simulator.app when it's absent. macOS-only (iOS Simulator is macOS-only) — does not touch the Windows/Linux build.

Next

  • I2: panel embedded-iOS view on this frame source + boot_headless (no open -a Simulator).
  • I3: pointer/keyboard via the hid RPC.

…r.app)

Proven live end-to-end: a real 1179x2556 iPhone-16 home screen captured
through UMIDE's own gRPC client from a FULLY HEADLESS simulator, with
Simulator.app closed.

The load-bearing discovery: a `simctl boot`-ed device has no rendering
surface — idb's screenshot returns "No Image available to encode" and even
`simctl io screenshot` hangs ~60s. Booting via `idb_companion --boot`
(IosCompanion::boot_headless) attaches the CoreSimulator framebuffer WITHOUT
opening Simulator.app, and the `screenshot` RPC then returns a live frame. So
the embedded panel's frame source is the on-demand screenshot RPC polled at a
target fps (robust regardless of screen activity); H.264 video_stream is a
future low-latency optimization (doesn't emit over companion v1.1.8).

This is the iOS twin of the Android emulator gRPC path:
- proto/idb.proto: a wire-compatible SUBSET of facebook/idb v1.1.8 (video_stream
  + hid + screenshot); trimming also sidesteps a prost name collision in the
  full file (DapResponse's nested `message Event` vs its `oneof event`).
- build.rs compiles it alongside emulator_controller.proto (client only).
- ios_grpc.rs: IosCompanion (spawn a serving companion, read its gRPC port,
  boot_headless, availability probe for graceful fallback), connect(),
  screenshot(), video_stream_h264(), payload_bytes().
- examples/ios_grab_frame.rs: the milestone proof (screenshot path primary,
  video_stream characterized), the iOS twin of grab_frame.

idb_companion is an external dep (brew install idb-companion); available()
gates the embedded path so the panel can fall back to Simulator.app when it's
absent. Next: I2 wire the panel's embedded-iOS view onto this + boot_headless;
I3 pointer/keyboard via the hid RPC.
…l loop)

start_ios_stream(udid, frame_signal, native_size, status) -> IosStreamHandle:
the iOS twin of start_emulator_stream. Boots the sim headless
(IosCompanion::boot_headless — framebuffer, no Simulator.app), serves it over
gRPC, then polls the screenshot RPC, decodes each PNG to a DecodedFrame
(software RGBA) and pushes it into the same frame_signal the VideoFrame panel
view renders. native_size feeds pointer mapping; status drives the panel's
connecting overlay. Handle drop ends the loop and kills the companion (device
stays booted). macOS-only module (cfg gate in panel/mod.rs).

Measured live on an iPhone 16 (12/12 frames decoded, 1179x2556): the
screenshot RPC costs ~450ms/call at full res, so the effective rate is ~2 fps
— a usable "see your app" PREVIEW, not smooth animation. video_stream H.264
does not emit via tonic against companion v1.1.8 (bidi call hangs even with the
framebuffer attached) — documented as the future rate optimization. Examples:
ios_poll_stream (sustained loop proof), ios_grab_frame (UMIDE_FORCE_VIDEO
characterization). Next: I2b wire the panel's embedded-iOS view onto this; I3
hid input.
… Simulator.app)

Live-verified end-to-end in the app: click Start on an iPhone 16 → the panel
boots the sim HEADLESS and renders its home screen inside UMIDE's right dock,
with NO Simulator.app window (confirmed: Simulator.app not running).

New macOS `ios_panel_portable` (the iOS twin of `android_panel_portable`):
a device list whose Start boots headless via start_ios_stream (never
launch_device / `open -a Simulator`), a floem `video_frame` fed by the
screenshot-poll stream, a status overlay (Booting…/Connecting…), and a
Stop/Hide sidebar. `ios_platform_view` chooses this embedded path when
idb_companion is installed (embedded_ios_available), else falls back to the
Simulator.app overlay `platform_panel` — so machines without the companion are
unaffected. Header carries an EMBEDDED badge. Stop drops the stream handle
(ends the poll loop + kills the companion) and shuts the device down.

Pointer/keyboard over the idb hid RPC is the next slice (I3); this delivers the
visible embedded view the whole brick was for.
… (hid RPC)

The embedded device is now fully interactive — ALL THREE modalities verified
live in the app, Simulator.app closed: a click inside the panel opened
Settings on the iPhone; a drag scrolled its list; typing entered "wifi" into
its search field.

- ios_grpc: hid_tap / hid_swipe / hid_key over the companion's client-streaming
  `hid` RPC (one short stream per gesture — the pattern idb ui tap uses);
  char_to_hid (USB-HID usage page 0x07 incl. shifted chars) + hid_usage names;
  `describe` RPC added to the proto subset (ScreenDimensions only — proto3
  skips the rest) for the pixels-per-point scale.
- Coordinate space settled EMPIRICALLY (examples/ios_tap_test): HID touch is in
  screen POINTS, not pixels — a points-space tap opened Safari. The panel maps
  view → native pixels (same letterbox as Android) → ÷ point_scale (describe;
  @3x fallback).
- ios_stream: IosInput (the iOS twin of EmulatorInput — bounded queue into a
  worker task with its own connection to the same companion), point_scale +
  input_out signals from start_ios_stream.
- Panel: PointerDown/Up → tap-or-swipe (8pt slop, measured gesture duration —
  the idb HID has no touch-move, so gestures resolve on release); KeyDown →
  chars + Enter/Backspace/Tab/Escape/arrows. The video_frame is focusable, so
  clicking into the device routes typing to it.
@dev-josias
dev-josias merged commit df7b6bd into main Jul 3, 2026
7 checks passed
@dev-josias
dev-josias deleted the feat/ios-embedded-stream branch July 3, 2026 16:08
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.

1 participant