feat(mobile): v1.14.0 "Foundry" - UniFFI bridge foundations#99
Conversation
Adds rustysnes-mobile: a UniFFI bridge over rustysnes_core::facade:: EmuCore (the same std-only facade the desktop frontend and rustysnes-libretro already drive the emulator through). MVP surface: ROM load/close, run_frame, the peripheral setters (Gamepad/Mouse/ Super Scope/Multitap), framebuffer + audio drain, save/load state, reset/power-cycle. Verified for real, not just claimed: a genuine `cargo ndk` cross- compile to arm64-v8a produced an actual ARM64 .so (confirmed via `file`), and `uniffi-bindgen` generated correctly-shaped Kotlin and Swift bindings from the compiled library (inspected method names/ types/throws). Expands the no_std CI gate from one aggregate rustysnes-core build to a per-crate matrix (cpu/ppu/apu/cart/core each build standalone). Reverses v1.0.0's "no mobile appetite" default (docs/adr/0012), adds docs/mobile-readiness.md as the living status page. Android-first: this environment has a real NDK, so v1.15.0's Android work can be genuinely built/tested here. No macOS/Xcode toolchain exists here, so iOS work (v1.16.0) will be written and compile-checked but needs the project owner's own Mac for real verification -- flagged honestly, not silently claimed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the rustysnes-mobile crate, establishing a UniFFI bridge to generate Kotlin and Swift bindings over the EmuCore emulation facade. It also adds architecture decision records and mobile readiness documentation, and expands the no_std CI gate to a per-crate matrix. Feedback suggests optimizing the performance-critical audio draining path by using a caller-provided reusable buffer to avoid heap allocations, and updating the pinned workspace dependency versions in Cargo.toml to prevent version mismatches.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
Introduces the foundations for a mobile embedding by adding a new rustysnes-mobile crate that exposes an FFI-safe UniFFI surface over rustysnes_core::facade::EmuCore, alongside documentation/ADR updates and CI adjustments to strengthen the no_std gate across individual chip crates.
Changes:
- Add
crates/rustysnes-mobileUniFFI bridge crate (ROM lifecycle, frame stepping, framebuffer/audio access, peripherals, save/load state). - Expand the
no_stdCI job from a single aggregate build into a per-crate matrix (rustysnes-{cpu,ppu,apu,cart,core}). - Add ADR + “mobile readiness” status doc and update roadmap/version-plan/changelog to reflect the mobile track.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| to-dos/VERSION-PLAN.md | Updates the long-term ladder text to include the mobile track and a v1.14.0 "Foundry" entry. |
| to-dos/ROADMAP.md | Updates roadmap narrative to include the mobile bridge foundations rung. |
| docs/mobile-readiness.md | New living status page describing the mobile bridge surface, what’s verified, and what’s deferred. |
| docs/adr/0012-mobile-platform-target.md | New ADR recording the decision to target mobile via UniFFI and defining constraints/consequences. |
| crates/rustysnes-mobile/src/lib.rs | Implements the UniFFI-exposed MobileCore wrapper over EmuCore + unit tests. |
| crates/rustysnes-mobile/src/bin/uniffi-bindgen.rs | Adds a helper binary entrypoint for running UniFFI bindgen. |
| crates/rustysnes-mobile/Cargo.toml | Adds the new crate manifest (cdylib/staticlib/rlib + UniFFI deps). |
| CHANGELOG.md | Notes the new mobile bridge crate and the no_std CI matrix expansion. |
| Cargo.toml | Adds crates/rustysnes-mobile to the workspace members list. |
| Cargo.lock | Locks new UniFFI-related dependencies. |
| .github/workflows/ci.yml | Updates no_std job into a per-crate matrix build. |
Addresses gemini/copilot findings on PR #99: - Gemini's &mut Vec<i16> out-parameter suggestion for drain_audio doesn't type-check under #[uniffi::export] (its generated FFI glue always lowers &mut T to &T -- confirmed by actually trying it, real compile error). Kept the owned-Vec return (required by UniFFI's calling convention) but pre-sized its capacity to avoid mid-loop reallocation, the achievable half of the perf win. - Copilot: drain_audio's name implied FIFO/pop semantics it doesn't have -- it mirrors EmuCore::audio()'s existing non-destructive contract (call once per run_frame), exactly like every other consumer (rustysnes-frontend's sync and emu-thread paths) already relies on. Corrected the doc comment, CHANGELOG, and docs/mobile-readiness.md wording instead of inventing new drain-tracking logic no other consumer needs. - Copilot: VERSION-PLAN.md/ROADMAP.md prematurely marked v1.14.0 RELEASED in this feature PR, before the version bump -- removed; the RELEASED marker belongs in the separate chore(release) closeout PR, matching every prior rung's two-PR pattern. - Copilot: uniffi's `cli` feature (askama/clap/goblin -- host-only binding-generator tooling) was pulled into every build including Android/iOS cross-compiles. Gated behind a new opt-in `bindgen` Cargo feature + `required-features` on the uniffi-bindgen binary; re-verified the real cargo-ndk cross-compile and Kotlin/Swift binding generation both still work with the leaner default. - Gemini's rustysnes-core/-cart version-pin suggestion (1.4.0 -> 1.13.0) not applied: matches the exact pre-existing pin convention every other crate in this workspace already uses (path deps pin a stable minimum-compatible version, not the current release -- confirmed against rustysnes-frontend's own identical pins). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
rustysnes-mobile: aUniFFIbridge overrustysnes_core::facade::EmuCore— the samestd-only facade the desktop frontend andrustysnes-libretroalready drive the emulator through. Adds no new emulation logic, only an FFI-safe wrapper.run_frame, the peripheral setters (Gamepad/Mouse/Super Scope/Multitap), framebuffer + audio drain, save/load state, reset/power-cycle. 7 host-side unit tests.cargo ndk -t arm64-v8a buildcross-compile against this environment's actual NDK (r29) produced a realELF 64-bit ARM aarch64 ... for Android 21shared object (confirmed viafile, not just a green exit code).uniffi-bindgengenerated real Kotlin AND Swift bindings from the compiled library, inspected for correct method names/types/throws/@Throws.no_stdCI gate from one aggregaterustysnes-corebuild into a per-crate matrix (rustysnes-{cpu,ppu,apu,cart,core}each build standalone againstthumbv7em-none-eabihf) — verified locally before pushing.v1.0.0's "no mobile appetite" default: newdocs/adr/0012-mobile-platform-target.mdrecords the decision, newdocs/mobile-readiness.mdis the living status page.to-dos/VERSION-PLAN.md/to-dos/ROADMAP.md's stale no-appetite lines are corrected in the same change.v1.15.0's Android work is genuinely buildable/testable here going forward. It has no macOS/Xcode toolchain at all, sov1.16.0's iOS work will be written and Rust-side compile-checked but needs the project owner's own Mac for real Xcode verification — documented explicitly indocs/mobile-readiness.md, not silently glossed over.Test plan
cargo fmt --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace(all green, including 7 newrustysnes-mobiletests)cargo build -p rustysnes-{cpu,ppu,apu,cart,core} --target thumbv7em-none-eabihf --no-default-features(the new per-crate matrix, verified locally for all 5)cargo ndk -t arm64-v8a build -p rustysnes-mobileagainst a real NDK — output confirmed viafileto be a genuine ARM64 Android shared objectuniffi-bindgen generate --library <compiled .so> --language kotlinand--language swift— both inspected, every method present with correct names/typesRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-depsmarkdownlint-cli2on every touched/new doc — zero new findings (two new files at zero findings; touched files at pre-existing baseline counts)🤖 Generated with Claude Code