Skip to content

feat(android): v1.8.0 beta.1 — rustynes-mobile UniFFI bridge + Android host foundation - #157

Merged
doublegate merged 18 commits into
mainfrom
feat/v1.8.0-android
Jun 20, 2026
Merged

doublegate merged 18 commits into
mainfrom
feat/v1.8.0-android

Conversation

@doublegate

Copy link
Copy Markdown
Owner

v1.8.0 "Android" — beta.1 (Workstream A: foundation & build)

The first platform (not accuracy) release begins. RustyNES ports to Android the same way it ports to the browser: the pure-Rust, byte-identical core is untouched, and only a new host is written. AccuracyCoin 100% (139/139) and every host gate stay green; the new Android CI only proves the build links.

This PR is the foundation the remaining betas build on. It is fully verifiable on host CI and the NDK cross-build is verified end-to-end.

What's in it

New crate rustynes-mobile — the shared, platform-agnostic UniFFI bridge over rustynes_core::Nes. NesController exposes the typed control surface (load ROM from bytes, per-port Buttons mask + individual buttons, run/step frame, drain audio, .rns save/restore, cartridge info). UniFFI generates the Kotlin (Android) and Swift (iOS, v1.9.0) bindings from #[uniffi::export]. std, host-testable (6 unit tests boot a synthetic NROM and round-trip input + save-state — no commercial ROM), and adds no new determinism surface. Reused verbatim by the future iOS host.

New crate rustynes-android — the Android platform crate, carrying only the narrow JNI/surface/audio hot glue UniFFI can't express (the ANativeWindow → wgpu handoff + audio sink, filled in across beta.2/beta.3) plus the android_main entry for the beta.1 winit+wgpu+egui spike. Everything Android-specific is behind #[cfg(target_os = "android")], so on host it's a near-empty lint-only shell. It is the one mobile crate that carries unsafe (each site // SAFETY:-noted).

android/ Gradle module — a Material-3 Jetpack Compose first-boot shell: opens a ROM via the Storage Access Framework (no path, raw bytes → the core), runs the emulator through the generated NesController, blits each RGBA frame to a Bitmap, and routes an on-screen D-pad/A/B/Select/Start overlay + hardware gamepad keys into the single late-latched controller mask. cargo-ndk + uniffi-bindgen are wired as Gradle build steps; release targets an AAB (minSdk 26, targetSdk 35, arm64 ship ABI + x86_64, R8 + proguard keeping JNA + the generated bindings).

CI — .github/workflows/android.yml: installs the Android Rust targets + cargo-ndk, cross-compiles both crates for arm64 + x86_64 against the NDK, generates the Kotlin bindings (smoke), checks 16 KB ELF alignment on the shipped arm64 .so, best-effort-bundles the AAB. Not a required check — accuracy is gated only on host CI.

Docs — docs/android.md (architecture, build, signing, CI, the remaining-beta map) + ADR 0024 (the UniFFI-bridge + hybrid wgpu/Compose-host decision). CHANGELOG [Unreleased].

Verified this session

  • Host: cargo build --workspace, cargo test -p rustynes-mobile (6 pass), cargo clippy -D warnings (whole workspace, via pre-commit), cargo fmt --check, RUSTDOCFLAGS=-D warnings cargo doc, markdownlint.
  • UniFFI Kotlin generation produces a clean NesController (runFrame(): ByteArray, setButton(port, NesButton, Boolean), saveState()/loadRom(), info(): RomInfo); the Compose shell's enum/method/package usage matches the generated bindings exactly.
  • NDK r29 cross-compile to aarch64-linux-android builds librustynes_mobile.so + librustynes_android.so warning-free.

Hard contract held

Determinism untouched; input converges on the one late-latched mask; the .rns format is unchanged, so desktop⇄Android save-state/.rnm cross-play stays valid; no commercial ROMs bundled (SAF only).

Remaining betas (on-device work, tracked in docs/android.md)

Beta Adds
beta.2 (B) wgpu on a SurfaceView + surface-loss lifecycle + Choreographer pacing
beta.3 (C+D) AAudio low-latency sink + audio focus; gamepad/haptics polish
beta.4 (E) full SAF library + persistable URI grants; .rns/SRAM in filesDir; save-on-background
beta.5 (F) Material-3 feature port (shaders, TAS, palettes, per-game DB, HD-pack)
rc.1 (G) Play App Signing, IARC rating, data-safety, the sideload/F-Droid channel

Netplay, RetroAchievements, and Lua are deferred to a follow-up mobile point release per the locked MVP.

🤖 Generated with Claude Code

…d host foundation

First *platform* release (Workstream A, foundation & build). Additive only;
the byte-identical core is untouched, so AccuracyCoin 100% (139/139) and every
host gate stay green.

New crates:
- rustynes-mobile: platform-agnostic UniFFI bridge over rustynes-core::Nes
  (NesController control surface: load ROM from bytes, per-port Buttons mask,
  run/step frame, drain audio, .rns save/restore, cartridge info). UniFFI
  generates the Kotlin/Swift bindings; shared verbatim with the v1.9.0 iOS host.
  6 host unit tests (synthetic NROM, no commercial ROM). No new determinism
  surface.
- rustynes-android: JNI/surface/audio hot glue, all #[cfg(target_os="android")]
  so it is a host-safe lint-only shell; holds android_main for the winit+wgpu
  spike. The one mobile crate carrying unsafe (// SAFETY: noted).

android/ Gradle module: a Material-3 Jetpack Compose first-boot shell that opens
a ROM via the Storage Access Framework, drives the core through the generated
NesController, blits each RGBA frame to a Bitmap, and routes an on-screen
overlay + hardware gamepad keys into the single late-latched controller mask.
cargo-ndk + uniffi-bindgen wired as Gradle build steps; AAB, minSdk 26 /
targetSdk 35, arm64 ship ABI + x86_64.

CI: .github/workflows/android.yml cross-compiles both crates for arm64+x86_64
against the NDK, generates the Kotlin bindings (smoke), checks 16 KB ELF
alignment, best-effort-bundles the AAB. Not a required check.

Docs: docs/android.md + ADR 0024 (UniFFI bridge + hybrid wgpu/Compose host).

Verified end-to-end: host build + tests, clippy -D warnings, rustdoc, fmt,
markdownlint; UniFFI Kotlin generation; NDK r29 cross-compile to arm64
producing both .so libraries warning-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 20, 2026 00:23

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request establishes the foundation for the Android platform release of RustyNES, introducing a shared mobile bridge crate (rustynes-mobile) with UniFFI bindings, an Android platform crate (rustynes-android) for JNI and native surface glue, and a Jetpack Compose-based application shell. The review feedback highlights three critical improvements: first, in rustynes-mobile, active button masks should be re-applied immediately after restoring a save state to prevent input desynchronization; second, in MainActivity.kt, LocalContext.current must be used instead of LocalActivity to avoid a compilation error with the specified Compose version; and third, the pixel-packing routine packRgbaToArgb should be moved to a background thread to prevent UI thread jank.

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.

Comment thread crates/rustynes-mobile/src/lib.rs
Comment thread android/app/src/main/java/com/doublegate/rustynes/MainActivity.kt Outdated
Comment thread android/app/src/main/java/com/doublegate/rustynes/MainActivity.kt Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces the first Android foundation for RustyNES v1.8.0 beta.1 by adding a shared UniFFI-based mobile bridge crate, an Android host/platform crate, and an initial Gradle/Compose Android app module, plus documentation and CI to validate cross-compilation and binding generation.

Changes:

  • Added rustynes-mobile, a host-testable UniFFI-exported control surface over rustynes-core::Nes (ROM from bytes, input mask/buttons, frame stepping, audio drain, save-state, cart info).
  • Added rustynes-android, a platform crate intended to hold Android-only JNI/surface/audio glue (currently a gated shell with logging + android_main placeholder).
  • Added an android/ Gradle module and an Android GitHub Actions workflow to cross-build/link the .so artifacts and smoke-generate Kotlin bindings; updated docs/ADR/CHANGELOG and workspace membership.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
docs/android.md Adds Android architecture/build/signing/CI documentation and beta roadmap.
docs/adr/0024-mobile-bridge-and-hybrid-android-host.md ADR describing the UniFFI bridge + hybrid host rendering decision.
crates/rustynes-mobile/src/lib.rs Implements the UniFFI-exported mobile control surface + unit tests.
crates/rustynes-mobile/src/bin/uniffi-bindgen.rs Adds a standalone UniFFI bindgen entrypoint for Kotlin/Swift generation.
crates/rustynes-mobile/Cargo.toml Declares the new bridge crate outputs (lib/cdylib/staticlib) and deps.
crates/rustynes-android/src/lib.rs Introduces the Android host crate skeleton and Android-only module gating.
crates/rustynes-android/Cargo.toml Declares the new Android platform crate and Android-target-only deps.
CHANGELOG.md Documents the new Android workstream A foundation deliverables.
Cargo.toml Adds the new crates to the workspace + workspace dependency entry.
Cargo.lock Locks new dependencies (UniFFI, Android tooling crates, etc.).
android/settings.gradle.kts Adds the Android Gradle settings and repository configuration.
android/gradle/wrapper/gradle-wrapper.properties Pins the Gradle wrapper distribution.
android/gradle.properties Enables Gradle properties for the Android module build.
android/build.gradle.kts Defines root Android plugin versions.
android/app/src/main/res/values/themes.xml Adds the Android launch theme resources.
android/app/src/main/res/values/strings.xml Adds the app name string resource.
android/app/src/main/res/values/ic_launcher_background.xml Adds launcher background color resource.
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml Adds adaptive launcher icon definition.
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml Adds round adaptive launcher icon definition.
android/app/src/main/res/drawable/ic_launcher_foreground.xml Adds vector foreground icon asset.
android/app/src/main/java/com/doublegate/rustynes/MainActivity.kt Implements the first-boot Compose shell driving the UniFFI controller.
android/app/src/main/AndroidManifest.xml Adds manifest with activity, features, and no-permissions stance.
android/app/proguard-rules.pro Adds R8/Proguard keep rules for UniFFI/JNA bindings.
android/app/build.gradle.kts Wires cargo-ndk + UniFFI bindgen into Gradle and sets app config/deps.
android/.gitignore Adds Android-module ignore rules and re-includes required XML resources.
.github/workflows/android.yml Adds Android cross-build + UniFFI generation workflow and packaging job.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread android/app/src/main/java/com/doublegate/rustynes/MainActivity.kt
Comment thread .github/workflows/android.yml
Comment thread .github/workflows/android.yml
Comment thread android/app/build.gradle.kts Outdated
Comment thread android/gradle.properties Outdated
doublegate and others added 17 commits June 19, 2026 20:35
The beta.1 module now builds a real installable APK end-to-end
(cargo-ndk -> UniFFI -> Compose). Two build-blockers fixed:

- rustynes-mobile: rename the MobileError variant field `message` -> `reason`.
  UniFFI maps error enums to Kotlin Exception subclasses, so a `message` field
  collided with `Throwable.message` and broke the generated bindings' compile.
- Compose shell: use `LocalContext` (activity-compose 1.9.3) instead of
  `LocalActivity` (only in 1.10+) to obtain the ContentResolver for SAF.

Also: pin the Gradle wrapper to 8.13, drop an invalid AGP property
(`enableNewResourceShrinker`) for `nonTransitiveRClass`, commit the Gradle
wrapper (jar + gradlew[.bat]) so the module builds without a pre-installed
Gradle, and simplify the CI bundle step to use ./gradlew directly.

Verified: `cargo test -p rustynes-mobile` (6 pass) + clippy clean; the Gradle
`:app:assembleDebug` produces a 22.5 MB app-debug.apk packaging both
librustynes_{mobile,android}.so for arm64-v8a + x86_64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-device validation on a Galaxy Z Fold 7 (Android 16, arm64): nestest boots,
renders with the correct NES palette, and reports all 14 suites OK via the
on-screen Start button — the core, UniFFI bridge, render path, and touch input
all work end-to-end on real hardware.

- Layout: the NES image now takes the remaining vertical space (`weight(1f)`,
  letterboxed) instead of driving its height from width, which overflowed on a
  wide/foldable display and pushed the Open-ROM button + touch overlay off-screen.
- Foldable: the activity is `resizeableActivity` and absorbs the fold/unfold +
  posture config changes (screenSize/smallestScreenSize/screenLayout/orientation
  /density) so the emulation thread + surface survive the cover<->inner screen
  swap with no restart; orientation is unlocked so the large inner screen can
  present in any posture. The Compose layout is responsive.
- Debug-only autoload: a ROM pushed to the app's external files dir
  (`autoload.nes`) is loaded on boot (BuildConfig.DEBUG-gated) so the render path
  is testable on-device without driving the SAF picker by hand; release boots to
  the picker. Enabled the `buildConfig` build feature for the gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… controls

Verified on the Z Fold 7 inner (1968x2184) + cover displays: the status bar,
navigation bar, and large-screen taskbar are hidden in sticky immersive mode and
only reappear transiently on an edge swipe, then auto-hide — so they never sit on
top of the on-screen D-pad / face buttons. Re-asserted on every window-focus gain
(via onWindowFocusChanged) so the bars stay hidden across the SAF picker, dialogs,
and the cover<->inner fold transition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t cut)

Verified on the Z Fold 7: the apu_mixer square-wave ROM is audible, no
underruns in logcat. The core emits deterministic mono f32 samples
(NesController.drainAudio); a low-latency mono PCM_FLOAT AudioTrack
(USAGE_GAME, MODE_STREAM, PERFORMANCE_MODE_LOW_LATENCY, ~4-frame buffer) plays
them. The blocking write paces the emulation loop to real time when sound is
present, with a wall-clock NTSC-frame floor so silent ROMs still run at ~60 Hz.

The sink is a pure consumer of already-deterministic samples, so the determinism
contract is untouched. Known follow-up: UniFFI maps Vec<f32> to a boxed
List<Float>; a later increment moves the audio pull into the rustynes-android
JNI hot path (primitive float[]/native ring) to drop the per-frame boxing, plus
AAudio focus handling + DRC, per the plan's Workstream C.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (beta.4 E)

Workstream E persistence, built on the working SAF load:

- SaveStateStore: `.rns` blobs at filesDir/states/<rom-sha256>/<slot>.rns — the
  same per-ROM-SHA layout the desktop host uses, so states stay byte-identical and
  portable across devices. Save/Load slot-1 buttons in a new control bar.
- Save-on-background + auto-resume: onPause writes the `auto` slot for the current
  ROM (the bridge lock serialises it against the emu thread); opening that ROM
  again auto-loads it, so the player resumes where they left off. Battery/SRAM is
  captured in the snapshot, so in-cart saves persist too.
- RomLibrary: recent ROMs persisted as the SAF content URIs with
  takePersistableUriPermission, so they survive reboot; an idle-screen recents
  list re-opens them in a tap. Display names resolved via OpenableColumns.
- Loads now funnel through one `loadRom` helper (picker, recents, debug autoload):
  build controller -> key by SHA-256 -> auto-resume -> grant + remember.

Verified on-device: builds, installs, boots; the Open/Save/Load/Reset bar renders
and the autoloaded ROM runs. The `.rns` format is unchanged, so determinism +
desktop<->Android state portability hold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pack, ABIs)

Adopted the three actionable bot findings from the #157 review:

- HIGH (rustynes-mobile): load_state now re-applies the host-held controller
  masks after nes.restore overwrites the core's input latch, so a button held
  across a state load no longer sticks/desyncs (mirrors the desktop host). Guarded
  by a new load_state_preserves_held_input test (7 tests pass).
- MEDIUM (Compose): the 61k-pixel RGBA->ARGB pack now runs inside the
  Dispatchers.Default block with runFrame/audio; only setPixels + asImageBitmap
  stay on the UI thread, removing per-frame main-thread jank.
- LOW (Gradle): split builtAbis (arm64+x86_64, cross-built for CI/emulator) from
  the release variant's shipAbi (arm64-only AAB); debug keeps x86_64 for the
  emulator. Comment now matches the actual packaging intent.

The other flagged threads were already fixed on the branch (LocalContext, the
gradle.properties cleanup) or not applicable (ANDROID_NDK_LATEST_HOME is exported
by the GitHub runners). Verified: cargo test/clippy green; the debug APK builds +
boots clean on the Z Fold 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts from the v1.7.1 release:
- Cargo.toml: take main's 1.7.1 workspace + dep-pin bumps; keep the new
  rustynes-mobile/rustynes-android members + the rustynes-mobile dep (1.7.1).
- CHANGELOG.md: keep the v1.8.0 "Android" Added block under [Unreleased] and
  main's new [1.7.1] release section.
cargo-deny flagged bincode 1.3.3 as unmaintained (RUSTSEC-2025-0141). bincode is
NOT a workspace dependency — no crate declares or uses it; it was pulled purely as
a build-time transitive dep of uniffi_macros 0.28. Rather than ignore the advisory,
upgrading uniffi to the current 0.31.2 removes bincode from the tree entirely
(along with other stale transitive deps: askama_escape, mime/mime_guess, unicase).

`cargo deny check advisories` now passes with no new ignore. The proc-macro
surface (setup_scaffolding!, #[uniffi::export], the uniffi-bindgen CLI) is
unchanged across 0.28->0.31, so the generated Kotlin NesController API is
identical. Verified: cargo test/clippy green (7 mobile tests); the debug APK
builds with the 0.31 bindgen and boots clean on the Z Fold 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Emulator quality-of-life on the working render path:
- Pause/Resume: the emulation loop idles (no frames advance) while paused.
- Fast-forward (>>): drops the frame-pace delay and the audio write so the core
  runs ahead of real time; releasing returns to 60 Hz with audio.
- Mute: discards the drained audio samples (the core still produces them, so the
  timeline is unchanged).
All three are @volatile flags on EmulatorHandle read by the loop — frontend-only,
determinism untouched. The control bar is now horizontally scrollable so all
controls reach on a narrow cover screen. Verified building + running on the Z Fold 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docs/android.md: status banner + workstream table now reflect the
  on-device-verified reality (nestest all-OK, audio, input, save-states,
  pause/FF/mute, foldable+immersive on a Z Fold 7); wgpu/shaders marked the
  documented next increment.
- Verified Play packaging end-to-end: `gradle :app:bundleRelease` produces a
  3.2 MB arm64-only AAB (R8-minified; the ABI split keeps x86_64 out of the
  release), and a debug-key-signed `assembleRelease` APK installs + runs on the
  Z Fold 7 with no R8/JNA/UniFFI reflection breakage (proguard keep-rules hold).
- Added android/keystore.properties.example documenting the upload-key format
  (the real keystore.properties stays gitignored / maintainer-manual).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… demo

Maintainer-locked (2026-06-19): ship on Google Play as a free download with a
one-time non-consumable "Full Unlock" IAP at $2.99 (Play Billing, single-listing
freemium — not a paid app, not a second demo listing). The free tier is a
time-limited demo: a 10-minute per-launch session with save-states,
save-on-background/resume, and on-cart battery-backed SRAM persistence disabled;
everything else (accuracy, video, audio, input, pause/FF, ROM library) is
identical to paid. Added Workstream M (Entitlement/BillingManager, the three
persistence locks + session timer, unlock/restore UI, debug override) and folded
it into rc.1 of the release train.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rkstream M)

Single free Play listing + a one-time non-consumable "full_unlock" IAP at $2.99
via Play Billing 8 (LicenseManager: connect, query product, queryPurchases,
launchBillingFlow, acknowledge, restore; entitlement cached in SharedPreferences
but re-verified against Play on each foreground). isUnlocked is Compose-observable.

Demo gating (all host policy, core/determinism untouched):
- 10-minute per-launch session timer (60 s in debug); on expiry the emulator
  pauses and a blocking DemoExpiredOverlay offers Unlock + Restore.
- Save-state Save/Load buttons hidden in demo.
- Save-on-background + auto-resume disabled in demo (onPause writes nothing; a ROM
  cold-boots), so battery-backed SRAM never persists across a close.
- An "Unlock $price" button + "Demo · M:SS" countdown in the control bar; both
  vanish the instant the entitlement flips (purchase / restore).

Verified on the Z Fold 7 via a BuildConfig.DEBUG-only unlock toggle (the real
Play purchase flow needs a Play Console product + license-tested account): in demo
Save/Load are hidden and the Unlock button + live countdown show; toggling unlock
restores Save/Load and removes the demo UI. The product id, $2.99 pricing, and the
closed-track purchase test remain maintainer-manual.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Monetization section (the $2.99 Full Unlock IAP + the 10-min demo's three
persistence locks + session timer) and the maintainer-manual Play Console steps
(create the `full_unlock` managed product at $2.99, closed-track + license-tested
purchase verification). Mark Workstreams G + M done in the status table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s/QoL/freemium)

Expand the [Unreleased] Android entry beyond the beta.1 foundation to record the
on-device-verified emulator (audio, input, save-states/SRAM/recents, pause/FF/mute,
foldable+immersive UI, release AAB) and the freemium $2.99 unlock + 10-min demo
gating; note wgpu/shaders as the documented next increment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deliver the GPU shader experience on the Compose render path with an AGSL
RuntimeShader (API 33+) applied via Modifier.graphicsLayer { renderEffect = … }:
None / Scanlines / CRT (soft 240-line scanlines + a 3px RGB aperture mask +
vignette), an "FX:" cycle button with the choice persisted, feature-detected
(videoFiltersSupported) with the plain Bitmap blit as the always-on fallback.
Post-processing only — the emulated framebuffer/audio is untouched, determinism
holds.

This is the pragmatic, Compose-native, low-risk path to the shader goal; the full
wgpu-on-SurfaceView native renderer (reusing the desktop WGSL NTSC/CRT/Bisqwit
stack) is documented as the deeper-architecture follow-up in the plan. Verified on
the Z Fold 7: FX cycles None->Scanlines->CRT and the CRT scanline/mask/vignette
applies to the NES image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ery)

Register a PowerManager.OnThermalStatusChangedListener (API 29+): when the device
reports THERMAL_STATUS_SEVERE+, cancel fast-forward (emulator.turbo = false). The
NES is light, but uncapped fast-forward can heat a phone; this only adjusts host
pacing, never emulation speed, so determinism is unaffected. Unregistered in
onDestroy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cut the v1.8.0 "Android" platform release: bump the workspace + all crate version
pins 1.7.1 -> 1.8.0, and finalize the CHANGELOG [Unreleased] -> [1.8.0] "Android"
section (the full Android cut: foundation, audio, input, save-states/SRAM/recents,
QoL pause/FF/mute, foldable+immersive UI, freemium $2.99 unlock + 10-min demo,
AGSL CRT/scanline filters, thermal backoff, verified release AAB).

The pure-Rust core is byte-identical on ARM (additive host only), so AccuracyCoin
100% (139/139) and every desktop gate are unchanged. The full wgpu-SurfaceView
native renderer + desktop WGSL shader stack remain the documented next increment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doublegate
doublegate merged commit 503c128 into main Jun 20, 2026
22 checks passed
@doublegate
doublegate deleted the feat/v1.8.0-android branch June 20, 2026 03:10
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