Skip to content

Commit f4e6156

Browse files
authored
Merge pull request #165 from doublegate/feat/v1.8.7-connectivity-completion
v1.8.7 "Android" — Connectivity completion (CGNAT netplay + controllers)
2 parents ddc213e + 19917ec commit f4e6156

33 files changed

Lines changed: 6863 additions & 296 deletions

‎CHANGELOG.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
1616
## [Unreleased]
1717

18+
## [1.8.7] - 2026-06-20 - "Android" (Connectivity completion)
19+
20+
Completes v1.8.6's connectivity work: **CGNAT / TURN room-code netplay** so phones on
21+
cellular (symmetric-NAT) networks can play, plus a **robust hardware-controller** input
22+
pipeline (wired USB + Bluetooth), a **controller-aware UI**, and **Chromecast prep**
23+
behind a default-off flag. Additive and host-side only — the emulation core stays
24+
byte-identical, so **AccuracyCoin holds 100% (139/139)** on host CI and the desktop is
25+
unchanged. This is a sideload-only / GitHub-Releases build (`PLAY_BUILD=false`); the
26+
Google Play launch moves to **v1.8.8 "Atlas"**.
27+
28+
### Added
29+
30+
- **CGNAT / TURN room-code netplay.** A `NatConnect` orchestrator (STUN discovery →
31+
UDP hole-punch → a relay fallback for symmetric NAT) drives a `Registering →
32+
Discovering → Exchanging → Punching → (Relaying) → Synced` pump, exchanging each
33+
peer's public address via a new `SignalMessage::PublicAddr` routed over the same
34+
`signaling::Relay` as browser SDP. The bridge gains `np_host_room` / `np_join_room`
35+
(a host emits a deterministic 6-char room code; a peer joins by code), and the
36+
Android UI adds an **"Online (room code)"** mode beside the v1.8.6 LAN Host/Join —
37+
share the code, join by code, and watch the Registering / Punching / Relaying
38+
progress. For symmetric NAT the relay-transport hand-off (`is_relayed`) routes the
39+
whole GGPO session over the TURN relay via an `RelayUdpSocket` shim, so
40+
`RollbackSession` stays a single transport type. The blocking `tungstenite`
41+
signaling client cross-compiles with no tokio. Verified in-process via
42+
`nat_loopback` (hole-punch) and `relay_loopback` (a mock RFC 8656 TURN relay,
43+
90-frame confirmed-digest parity). The live cross-NAT path needs the maintainer's
44+
hosted `deploy/` relay + two devices (carryover); the placeholder signaling URL is
45+
overridable in Settings.
46+
- **Robust hardware controllers (wired USB + Bluetooth).** The Android input pipeline
47+
is overhauled from "P1 buttons only" to full pad support: a new `onGenericMotionEvent`
48+
handler (previously absent, so analog sticks and the d-pad-as-HAT axis silently
49+
failed on 8BitDo / DualSense / OTG pads) reads `AXIS_HAT_X/Y`, sticks past the
50+
reported dead-zone, and triggers; per-port P1–P4 masks with `InputManager` hot-plug
51+
(auto Four Score at ≥ 3 pads); per-descriptor remapping persisted as JSON keyed by
52+
`InputDevice.getDescriptor()`; and turbo / autofire. No Rust change — the bridge
53+
already exposed `setButtons(port, …)` + `setFourScore`.
54+
- **Controller-aware UI.** Connecting a hardware controller hides the on-screen virtual
55+
controller and maximizes the ROM display (8:7 PAR, valuable on the Z Fold 7 inner
56+
screen); disconnecting restores it. The **Guide** button (best-effort,
57+
`KEYCODE_BUTTON_MODE`) or **Start + Select** (guaranteed) opens a controller-navigable
58+
menu driven by the d-pad + A/B.
59+
- **Chromecast prep.** A CAF sender (`ChromecastSender` + a `MediaRouteButton`) and a
60+
static Web Receiver render a ~20–30 fps spectator mirror, gated behind a default-off
61+
`CHROMECAST_ENABLED` BuildConfig flag — when off the sender is dead code and behaviour
62+
is unchanged; the low-latency **Presentation API** stays the primary cast path. The
63+
frame is down-sampled to a 128×120 6-bit index plane (~20.5 KB) under the CAF message
64+
cap. AirPlay was researched and skipped (no maintained FOSS sender; FairPlay / legal).
65+
Live use needs the maintainer's Cast App ID + an HTTPS-hosted receiver (carryover).
66+
1867
## [1.8.6] - 2026-06-20 - "Android" (Connectivity & scripting)
1968

2069
The connectivity & scripting pass: **Lua**, **RetroAchievements**, and **Netplay** on

‎Cargo.lock‎

Lines changed: 20 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ members = [
1919
]
2020

2121
[workspace.package]
22-
version = "1.8.6"
22+
version = "1.8.7"
2323
edition = "2024"
2424
rust-version = "1.96"
2525
license = "MIT OR Apache-2.0"

‎README.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
1111
[![Build Status](https://github.com/doublegate/RustyNES/workflows/CI/badge.svg)](https://github.com/doublegate/RustyNES/actions)
1212
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
13-
[![Version](https://img.shields.io/badge/version-v1.8.6-blue.svg)](https://github.com/doublegate/RustyNES/releases)
13+
[![Version](https://img.shields.io/badge/version-v1.8.7-blue.svg)](https://github.com/doublegate/RustyNES/releases)
1414
[![Rust: 1.96](https://img.shields.io/badge/rust-1.96-orange.svg)](rust-toolchain.toml)
1515
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Web-lightgrey.svg)](#platform-support)
1616
[![AccuracyCoin](<https://img.shields.io/badge/AccuracyCoin-100%25%20(139%2F139)-brightgreen.svg>)](#compatibility-and-accuracy)
@@ -645,16 +645,18 @@ workspace API docs (rustdoc) at
645645

646646
## Version History
647647

648-
The current release is **v1.8.6 "Android"**, continuing the first **platform** (not
648+
The current release is **v1.8.7 "Android"**, continuing the first **platform** (not
649649
accuracy) release line **v1.8.0 "Android"** — a complete Android app, verified
650650
end-to-end on a Samsung Galaxy Z Fold 7. It builds on the bugfix patch **v1.7.1** and
651651
the writable + programmable **v1.7.0 "Forge"** line, on the cycle-accurate v1.0.0
652-
production core. (v1.8.6 makes the Android app feature-complete — **Lua**,
653-
**RetroAchievements**, and **netplay** — and gates the **Google Play launch** as its
654-
capstone; v1.8.0–v1.8.6 are GitHub-Releases / sideload, full-featured.) The road so far:
652+
production core. (v1.8.7 completes the connectivity work — **CGNAT / TURN room-code
653+
netplay**, robust **hardware controllers**, and a controller-aware UI — atop the
654+
feature-complete v1.8.6; v1.8.0–v1.8.7 are GitHub-Releases / sideload, full-featured.
655+
The **Google Play launch** moves to **v1.8.8 "Atlas"**.) The road so far:
655656

656657
| Version | Highlights |
657658
| ---------- | ------------------------------------------------------------------------------------------- |
659+
| **v1.8.7** | "Android" — connectivity completion. **CGNAT / TURN room-code netplay** completes v1.8.6's direct-IP / LAN netplay: a **`NatConnect`** orchestrator (STUN discovery → UDP hole-punch → a `SignalMessage::PublicAddr` exchange over the same `signaling::Relay` → a **TURN relay fallback** for symmetric NAT, `is_relayed`) with `np_host_room` / `np_join_room` bridge methods and an **"Online (room code)"** Android UI (share a 6-char code, join by code, Registering / Punching / Relaying progress + a "via relay" badge). Plus **robust hardware controllers** (the input pipeline gains `onGenericMotionEvent` — analog sticks + the d-pad-as-HAT axis silently failed on 8BitDo / DualSense / OTG pads before — with per-port P1–P4 masks, `InputManager` hot-plug, per-descriptor remapping, and turbo / autofire; no Rust change), a **controller-aware UI** (connect a pad → the on-screen controller hides + the ROM display maximizes; Guide or Start + Select opens a d-pad-navigable menu), and **Chromecast prep** behind a default-off `CHROMECAST_ENABLED` flag (a CAF sender + a static Web Receiver spectator mirror; the Presentation API stays primary). Loopback / mock-verified; live cross-NAT + the hosted relay are maintainer carryovers. All host-side; AccuracyCoin holds 100% (139/139). Sideload-only — the **Google Play launch** is **v1.8.8 "Atlas"**. |
658660
| **v1.8.6** | "Android" — connectivity & scripting. **Lua** (the sandboxed desktop engine over the bridge — made `Send`; SAF `.lua` load + an on-screen log overlay), **RetroAchievements** (login + hardcore + unlock toasts + a `.rap` progress sidecar; the session extracted into a new shared **`rustynes-ra`** crate, `ureq` + `rustls` + `ring` TLS — no OpenSSL), and **direct-IP / LAN netplay** (GGPO rollback over `rustynes-netplay`; the loop calls `np_advance_frame` instead of `run_frame` while connected), plus an **Open / Close ROM** toggle. The shared `rustynes-mobile` bridge is now connectivity-complete, so **v1.9.0 iOS inherits all three**. All host-side; AccuracyCoin holds 100% (139/139). The **Google Play launch** (`$2.99` Full Unlock) is the decoupled capstone; CGNAT/TURN netplay is deferred to v1.8.7. |
659661
| **v1.8.5** | "Android" — power-user features. **Custom `.pal` palettes** + **compressed `.zip` ROM loading** (extract the first NES entry, like desktop); **Bisqwit composite NTSC on the GPU** (the desktop WGSL shared via `rustynes-gfx-shaders`, an `R16Uint` palette-index pipeline fed by the bridge); **TAS `.rnm` movies** (record / play / save via SAF, determinism-exact); a **per-game settings DB** (each game reopens with its last video filter, keyed by ROM SHA); and **HD-packs on Android** — the HD-pack loader + compositor + HD audio were **extracted into a new shared `rustynes-hdpack` crate** (the core is `#![no_std]`) so the bridge can composite the upscaled picture (Bitmap path). All presentation / host-side; AccuracyCoin holds 100% (139/139). |
660662
| **v1.8.4** | "Android" — native wgpu renderer & shaders. The NES picture now draws through **wgpu on a `SurfaceView`** (Vulkan/GLES) instead of the Compose `Bitmap` blit, reusing the desktop WGSL: a shared **`rustynes-gfx-shaders`** crate serves the **CRT / scanline + LMP88959 NTSC** post-passes to both desktop and Android, with **per-filter tuning sliders** (intensity / count / mask / saturation / sharpness …) and a native-audio hot path (bytes, no boxing). Opt-in behind a "GPU renderer" setting (API 33+); the Bitmap path stays the fallback. |
@@ -797,7 +799,7 @@ If you use RustyNES in academic research, please cite:
797799
author = {RustyNES Contributors},
798800
title = {RustyNES: A Cycle-Accurate NES Emulator in Rust},
799801
year = {2026},
800-
version = {1.8.6},
802+
version = {1.8.7},
801803
url = {https://github.com/doublegate/RustyNES},
802804
note = {Cycle-accurate NES emulator on a master-clock-precise scheduler;
803805
AccuracyCoin 100\% (139/139), nestest 0-diff; 168 mapper families,

‎android/app/build.gradle.kts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,24 @@ android {
3030
applicationId = "com.doublegate.rustynes"
3131
minSdk = 26 // AAudio floor.
3232
targetSdk = 35 // Play mandate since 2025-08-31.
33-
versionCode = 10806 // 1.8.6
34-
versionName = "1.8.6"
33+
versionCode = 10807 // 1.8.7
34+
versionName = "1.8.7"
3535
// No abiFilters here — set per buildType so release ships arm64 only
3636
// while debug keeps x86_64 for the emulator.
3737
// PLAY_BUILD gates the freemium (demo timer + persistence locks + Billing).
3838
// Default false → sideload/GitHub/dev builds are full-featured; the Google
39-
// Play AAB sets it true (v1.8.6 launch). See the v1.8.0 plan's monetization
40-
// timing.
39+
// Play AAB sets it true (v1.8.8 "Atlas" launch — postponed from v1.8.6). See
40+
// the v1.8.0 plan's monetization timing.
4141
buildConfigField("boolean", "PLAY_BUILD", "false")
42+
// CHROMECAST_ENABLED gates the experimental Cast Application Framework
43+
// (CAF) sender path (v1.8.7, #38) — a ~20-30fps SPECTATOR mirror to a
44+
// custom Web Receiver, distinct from the primary low-latency Presentation
45+
// API cast (Cast.kt, which is always available). Default false: no Cast
46+
// button, no CastContext init, zero behavior change. It stays off until the
47+
// maintainer does the deferred ops (a $5 Cast Developer Console account, a
48+
// registered Receiver App ID, and HTTPS hosting of android/cast-receiver/).
49+
// See android/cast-receiver/README.md + ChromecastSender.kt.
50+
buildConfigField("boolean", "CHROMECAST_ENABLED", "false")
4251
}
4352

4453
// Release signing reads `keystore.properties` (gitignored) or env vars; when
@@ -152,6 +161,8 @@ dependencies {
152161
implementation("androidx.core:core-ktx:1.15.0")
153162
implementation("androidx.activity:activity-compose:1.9.3")
154163
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
164+
// collectAsStateWithLifecycle for the controller-connect StateFlow (v1.8.7, #41).
165+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
155166
implementation("androidx.compose.ui:ui")
156167
implementation("androidx.compose.ui:ui-graphics")
157168
implementation("androidx.compose.material3:material3")
@@ -161,4 +172,8 @@ dependencies {
161172
implementation("net.java.dev.jna:jna:5.15.0@aar")
162173
// Play Billing — the one-time "Full Unlock" IAP (Workstream M, freemium model).
163174
implementation("com.android.billingclient:billing-ktx:8.0.0")
175+
// Cast Application Framework sender (v1.8.7, #38). Linked but DORMANT: it does
176+
// nothing until CastContext is initialized, which only happens behind the
177+
// default-off BuildConfig.CHROMECAST_ENABLED flag (see ChromecastSender.kt).
178+
implementation("com.google.android.gms:play-services-cast-framework:21.5.0")
164179
}

‎android/app/src/main/AndroidManifest.xml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@
2424
android:resizeableActivity="true"
2525
android:theme="@style/Theme.RustyNES">
2626

27+
<!-- Cast Application Framework OptionsProvider (v1.8.7, #38). REQUIRED by
28+
the SDK to be declared, but only instantiated when CastContext is
29+
initialized — and that init is gated behind the default-off
30+
BuildConfig.CHROMECAST_ENABLED flag (see ChromecastSender.kt +
31+
MainActivity). With the flag off the provider is never touched, so the
32+
placeholder Receiver App ID inside it is harmless and the app launches
33+
unchanged. The maintainer registers a real App ID before enabling. -->
34+
<meta-data
35+
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
36+
android:value="com.doublegate.rustynes.RustyNesCastOptionsProvider" />
37+
2738
<!-- Foldable / large-screen support: the activity is resizeable and
2839
absorbs the fold/unfold + posture config changes itself (so the
2940
emulation thread + GL surface survive the transition with no

0 commit comments

Comments
 (0)