Skip to content

[codex] Add visionOS simulator support#127

Open
douglance wants to merge 2 commits intoPerryTS:mainfrom
douglance:codex/visionos-support
Open

[codex] Add visionOS simulator support#127
douglance wants to merge 2 commits intoPerryTS:mainfrom
douglance:codex/visionos-support

Conversation

@douglance
Copy link
Copy Markdown

@douglance douglance commented Apr 22, 2026

What changed

  • add first-class visionos / visionos-simulator targets across Perry CLI flows: compile, run, publish, setup, docs, and doc-tests
  • add a dedicated perry-ui-visionos backend crate as a sibling to perry-ui-ios
  • host visionOS apps through a SwiftUI WindowGroup bridge that embeds Perry's UIKit view tree
  • add safe visionOS fallbacks for currently unstable simulator controls, including toggle and picker paths
  • document visionOS as a 2D windowed target and remove unused immersive-focused default link frameworks

Follow-up polish

Addressed review feedback in 830711f:

  • removed bring-up debug artifacts from the SwiftUI host
  • removed the dead UIKit scene/app-delegate bootstrap from the visionOS backend
  • added explicit TODOs to the temporary Toggle and Picker fallbacks
  • dropped ARKit and CompositorServices from the default visionOS link line to match the current 2D-only scope

Why it changed

Perry had no repo-side visionOS support. Reusing the iOS-style UIApplicationMain lifecycle was enough to get partway through the port, but it did not produce a stable visionOS simulator path. visionOS expects a SwiftUI-hosted window scene, and some UIKit control paths were crashing under simulator focus/tint handling.

This PR brings the visionOS simulator path up to a working state while keeping unsupported widget behavior non-fatal.

Impact

  • Perry apps can now compile for the Apple Vision Pro simulator with --target visionos-simulator
  • the counter example renders in the simulator
  • the gallery example renders in the simulator with current safe fallbacks for unsupported native controls
  • the visionOS target is wired through the same repo workflows as the other Apple targets

Current caveats

  • some visionOS controls are still intentional safe fallbacks rather than native equivalents
  • there is still a linker warning about duplicate -lSystem
  • this PR only covers repo-side support; any external Perry Hub/backend handling remains separate

Validation

Repo checks:

  • cargo check -p perry
  • cargo check -p perry-ui-visionos

Simulator compile checks:

  • target/debug/perry compile docs/examples/ui/counter.ts --target visionos-simulator -o /tmp/perry_vision_counter --no-auto-optimize
  • target/debug/perry compile docs/examples/ui/gallery.ts --target visionos-simulator -o /tmp/perry_vision_gallery --no-auto-optimize

Fresh simulator proof rerun on commit 830711f:

  • cold-installed and launched both apps on a booted Apple Vision Pro simulator
  • verified visible rendering for both counter and gallery
  • generated fresh in-app screenshots for both apps during the proof run
  • checked for new perry_vision_counter / perry_vision_gallery crash reports after the proof start time and found none

Copy link
Copy Markdown
Contributor

@proggeramlug proggeramlug left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First off — thank you enormously for this. This is a serious amount of work and a real bet on Perry. visionOS is a platform we genuinely wanted coverage for and would not have prioritized on our own for a while. The structural choice of bringing up the backend as a sibling of perry-ui-ios is exactly right, the SwiftUI bridge follows the same @main struct App pattern we shipped for watchOS in v0.5.122, and the fact that cargo check -p perry is clean against current main says the CLI-side wiring (compile.rs, run.rs, publish.rs, setup.rs, commands.md, flags.md, perry-toml.md, SUMMARY.md) is all sound. The objc2 v0.6 usage in the widget files (e.g. toggle.rs uses the modern define_class! + #[unsafe(super(NSObject))] + #[unsafe(method(...))] forms per our CLAUDE.md conventions) is correct — whoever (or whatever) wrote this paid attention to the modern objc2 API, which is non-trivial.

AI authorship is completely fine, for the record — we're building Perry with the same tools and we'd rather have a great AI-assisted contribution than none. A few concrete adjustments, almost all polish:

Code-level:

  1. crates/perry-ui-visionos/swift/PerryVisionApp.swift:25, 55host-makeUIView.txt / host-capture.txt debug writes to ~/. Same in crates/perry-ui-visionos/src/app.rs (the ws_log! trace file writes). These look like debugging artifacts from bring-up. Either drop them or gate behind #[cfg(debug_assertions)] (Rust side) / a DEBUG compilation condition (Swift side). Same for the three print(...) calls in PerryVisionApp.swift (PerryVisionBootstrap.init, PerryHostedView.makeUIView, PerryHostedView.capture) — leave them in only if they're behind a test-mode check.

  2. crates/perry-ui-visionos/src/widgets/toggle.rs — the toggle is implemented as a UIButton wearing "On"/"Off" titles instead of a real UISwitch, because (per the PR description) visionOS simulator throws an Obj-C exception on UISwitch construction. Totally reasonable as a first-pass; two requests:

    • Add an in-code // TODO(visionos): replace UIButton stub with UISwitch once sim-side construction crash is resolved — see PR #127 for the original symptom above the create(...) body, so the next person reading this file knows it's a fallback, not the target design.
    • Same treatment for crates/perry-ui-visionos/src/widgets/picker.rs:27 — it's a UILabel showing only the first item, which is intentional but undocumented at the call site. A one-liner TODO mirrors the pattern we already use in other platform stubs.
  3. crates/perry-ui-visionos/src/widgets/picker.rs:27pub fn create(_label_ptr, _on_change, _style: i64)_style is declared and never read anywhere in the file. If the codegen dispatch passes a style argument that will be wired through later, keep it but add a // TODO: wire --style through to UIPickerView.appearance() once migrated off UILabel stub. If it's dead, drop the parameter so the FFI shape matches iOS exactly (iOS picker.rs::create takes 2 args, not 3) — a silent signature mismatch between backends is a future-debugging footgun.

  4. crates/perry/src/commands/compile.rs (visionOS link step, ~5731) — -framework ARKit and -framework CompositorServices are linked into every visionOS binary, but your own docs (docs/src/platforms/visionos.md) explicitly scope to 2D windowed apps. Unused frameworks add bundle weight and confuse future contributors about what's supported. Suggest either:

    • Dropping both frameworks from the link line until an immersive/volume story actually ships, or
    • Adding an inline comment explaining they're pre-wired for a follow-up PR that'll exercise them.
  5. crates/perry-ui-visionos/src/app.rs — the scene_will_connect callback and PerryAppDelegate class are registered, but the SwiftUI @main struct App path in PerryVisionApp.swift uses WindowGroup + UIViewRepresentable, which does NOT invoke UIApplicationDelegate scene callbacks. Looks like those ~90 lines of Rust are effectively dead code for the SwiftUI-only visionOS path — they survive from a UIKit-app-delegate bring-up attempt. Not broken (they're just never called), but either:

    • Remove them if the SwiftUI path is the permanent choice (cleaner), or
    • Keep them under a #[cfg(feature = "visionos-uikit-delegate")] for future exploration, with a comment explaining the SwiftUI fork chose not to use them.

Merge mechanics (not a code concern — the rebase maintainer would handle):

  • Branch is based on v0.5.138, currently 5 patch versions behind main. On merge we'd rebase onto current main.
  • You don't need to touch the version yourself — we just carved out a rule in CLAUDE.md that says external contributors should leave [workspace.package] version, the **Current Version:** line, and "Recent Changes" entries alone; the maintainer folds those in at merge. So you can drop the 0.5.138 bump and the CLAUDE.md version edit from this branch whenever you rebase.

Pre-merge-from-draft checklist (my opinion, non-binding):

  • Address #1#5 above (all small).
  • One clean visionOS-sim cold-launch test confirming gallery.ts renders end-to-end and the toggle/picker stubs don't crash on tap.
  • Rebase onto current main and drop the version bump.

Genuinely, thank you. A 10k-line bring-up of a Tier-3 Apple target is a lot to drop into an compiler, and the care shown in mirroring the ios backend file-for-file made the review fast. Please ping when you flip this out of draft — happy to run it through CI (we'll need to approve the first-contributor workflow) and do a closer pass on the runtime-side widget signatures at that point.

proggeramlug added a commit that referenced this pull request Apr 22, 2026
First inbound external PR (#126) landed today, and a second substantial
one (#127) is in flight — time to make the project legible as a real
community project instead of a solo push.

Adds:
- CONTRIBUTING.md — build-from-source prereqs derived from
  .github/workflows/test.yml, PR guidelines (including the "don't bump
  version or touch CLAUDE.md" rule we carved out this week), conventional-
  commit guidance as loose convention, how to claim an issue, pointers to
  good-first-issue / help-wanted labels.
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1 verbatim, contact email
  set to ralph@skelpo.com. Fetched directly from the EthicalSource canon.
- .github/ISSUE_TEMPLATE/bug_report.md — requires Perry version, target,
  host OS, and a minimal repro.
- .github/ISSUE_TEMPLATE/feature_request.md — problem / proposed solution
  / alternatives / scope estimate.
- .github/ISSUE_TEMPLATE/config.yml — disables blank issues, routes
  questions to Discussions and security reports to email.
- .github/pull_request_template.md — leads with "do NOT bump version or
  edit CLAUDE.md" so the next contributor doesn't retrace #126's
  version-collision friction.

Explicitly NOT doing (decided earlier in the design pass):
- MAINTAINERS.md — skipped until there's a real list beyond "Ralph,
  everything".
- All Contributors bot — defer until contributor volume justifies the
  badge wall + permanent bot webhook.
- Community docs page inside mdBook — would duplicate CONTRIBUTING.md
  and go stale.

No DCO / CLA required; commit style is guidance-only, not enforced.
No code or build-config changes.
@douglance
Copy link
Copy Markdown
Author

Thanks for the detailed review. I addressed the polish items on the branch and pushed them in 830711f.

What changed:

  • removed the visionOS bring-up debug artifacts from crates/perry-ui-visionos/swift/PerryVisionApp.swift
    • dropped the print(...) calls
    • dropped the host-makeUIView.txt / host-capture.txt writes
  • removed the dead UIKit delegate / scene-delegate bring-up path from crates/perry-ui-visionos/src/app.rs
    • the SwiftUI @main host is now the only visionOS startup path
  • added explicit TODO(visionos) comments to the temporary Toggle and Picker fallbacks
  • kept the picker style argument for now, but documented it as reserved for the eventual native picker implementation
  • removed ARKit and CompositorServices from the default visionOS link line so the backend matches the current 2D-only scope

I also reran the simulator proof from the current branch head:

  • cold-launch docs/examples/ui/counter.ts
  • cold-launch docs/examples/ui/gallery.ts
  • both rendered visibly in the Apple Vision Pro simulator
  • no new perry_vision_counter / perry_vision_gallery crash reports were generated during the proof run

I updated the PR description with the current validation and caveats as well.

On merge mechanics: the branch is already on top of current main, and I am not carrying a CLAUDE/version bump anymore. Happy to flip this out of draft whenever you want to send it through CI / first-contributor approval.

@proggeramlug
Copy link
Copy Markdown
Contributor

Thanks for the quick turnaround on the polish items — I eyeballed 830711f and all five items from the review look addressed (debug writes/prints gone, dead UIKit delegate path removed, TODO(visionos) comments on the toggle/picker stubs, _style documented as reserved, ARKit/CompositorServices dropped from the link line).

To get this merge-ready on our side, two asks:

  1. Flip out of draft. Nothing triggers CI while it's a draft, so we can't actually see whether Tests + Simulator Tests go green until you do. Once it's ready-for-review I'll approve the first-contributor CI run.
  2. Rebase onto current main and drop the version/changelog edits. You're currently based on v0.5.138; main is now at v0.5.164 (43 commits ahead). There's one real content conflict — Cargo.lock — everything else automerges. Per the contributor rule we carved out, you don't need to carry a version bump or a CLAUDE.md "Recent Changes" entry at all; I fold those in at merge time. So on rebase please drop:
    • the [workspace.package] version = "0.5.138" bump in Cargo.toml
    • the **Current Version:** line edit in CLAUDE.md
    • the visionOS entry under "Recent Changes" in CLAUDE.md

Once those two land and CI is green I'll do a closer pass on the runtime-side widget signatures and send it through. Really appreciate the work here.

@douglance douglance marked this pull request as ready for review April 23, 2026 01: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.

2 participants