Add VICE C64 emulator integration to Android companion app

Cross-compiles VICE 3.8 headless for ARM64 and x86_64 as part of the
  Android Studio build (Gradle buildVice task + build_vice.sh). The JNI
  bridge (vice_jni.c) renders frames into a shared framebuffer read by
  C64DisplayView, and forwards key events from C64KeyboardView to VICE's
  keyboard matrix. Without ROM files the app shows a placeholder blue
  screen; with ROMs and a .d64 disk image the full C64 emulator runs.
This commit is contained in:
ml
2026-06-04 16:24:14 +02:00
parent 724157aca3
commit 8c7f2e3306
16 changed files with 1022 additions and 44 deletions
+134
View File
@@ -0,0 +1,134 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project goal
Port the classic German C64 RPG series **Schwert und Magie** (8 episodes, German Design Group, 19891992) to a Pebble Time 2 smartwatch. The phone runs a VICE C64 emulator as a companion app; the watch displays the game and sends button presses back.
## Repository layout
```
SchwertUndMagieOnPebbleWatchApp/ Pebble watchapp (C + PebbleKit JS)
SchwertUndMagieOnPebbleCompanionApp/ Android companion app (Kotlin + NDK)
versions/ Original .d64 disk images (4 disks, 8 episodes)
docs/ Background notes
```
## Watch app
### Build & install
```bash
cd SchwertUndMagieOnPebbleWatchApp
pebble build # all platforms
pebble install --phone <phone-ip> # sideload via Pebble/Core app on the phone
pebble logs --phone <phone-ip> # stream JS + C app logs
```
In headless / CI environments, add `--vnc` to every emulator command:
```bash
pebble install --emulator emery --vnc
pebble screenshot --vnc --scale 6 --no-open screenshot.png
pebble emu-button --emulator emery --vnc click select
```
### Key files
| File | Purpose |
|---|---|
| `src/c/SchwertUndMagieOnPebbleFrontend.c` | Single-file C watchapp |
| `src/pkjs/index.js` | PebbleKit JS — runs inside Core for Pebble on the phone |
| `package.json` | UUID, target platforms, `messageKeys` |
### Communication protocol
AppMessage keys (defined in both C and JS):
| Key | Direction | Value |
|---|---|---|
| `0` (TIME) | phone → watch | `"HH:mm:ss"` string (later: C64 framebuffer) |
| `1` (COMMAND) | watch → phone | button name string (`"UP"`, `"DOWN"`, `"SELECT"`) |
The watch UUID is `8039ba8c-f1e8-4620-838d-650fa35335b7`.
### PebbleKit JS notes (Core for Pebble quirks)
- **Use `127.0.0.1` not `localhost`** — Core for Pebble's JS runtime does not resolve `localhost`.
- **Use numeric keys in `sendAppMessage`** (`{ 0: value }` not `{ 'TIME': value }`) — symbolic key resolution from `package.json` messageKeys is unreliable with Core for Pebble.
- PebbleKit Android (`com.getpebble.android.*`) does **not** work with Core for Pebble; all phone↔watch communication goes through PebbleKit JS + the HTTP server.
## Android companion app
### Build
Open `SchwertUndMagieOnPebbleCompanionApp/` in Android Studio, or:
```bash
cd SchwertUndMagieOnPebbleCompanionApp
./gradlew assembleDebug
./gradlew installDebug
```
Android SDK is at `/opt/android-sdk`.
### Communication architecture
```
Watch (AppMessage BT)
PebbleKit JS — index.js runs inside Core for Pebble
↕ HTTP on 127.0.0.1:8888
Android companion app — NanoHTTPD server
GET /time → {"time":"HH:mm:ss"}
GET /key?cmd=UP → logs keystroke
```
`MainActivity` starts both the NanoHTTPD server and the VICE emulator loop thread. All HTTP callbacks marshal to the main thread via `mainHandler`.
### VICE integration (NDK)
VICE 3.8 tarball is at `SchwertUndMagieOnPebbleCompanionApp/res/vice-3.8.tar.gz`.
**Without VICE built** (first state): `vice_jni.c` fills the framebuffer with a placeholder blue screen; the app builds and runs normally.
**VICE compilation is automatic.** The Gradle `buildVice` task runs before every native CMake build. On the first build it:
1. Calls `app/src/main/jni/build_vice.sh` with `$NDK` set from the Android Studio SDK config
2. The script unpacks the tarball, cross-compiles VICE headless for ARM64 and x86_64, and produces `vice-libs/<abi>/libvice.a`
3. `CMakeLists.txt` auto-detects the library via `EXISTS` and links it in — no manual flags needed
The NDK must be installed: **Android Studio → SDK Manager → SDK Tools → NDK (Side by side)**.
After the first successful build, subsequent builds skip the VICE compilation step entirely (outputs are up-to-date).
**C64 ROM files** must be pushed to the device separately (not redistributable).
VICE expects files named exactly `kernal`, `basic`, `chargen` (no extensions) in the
app's private `filesDir`: `/data/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/`
```bash
adb push kernal /data/local/tmp/kernal
adb push basic /data/local/tmp/basic
adb push chargen /data/local/tmp/chargen
adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/kernal files/kernal
adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/basic files/basic
adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/chargen files/chargen
```
### Key Kotlin/C files
| File | Purpose |
|---|---|
| `MainActivity.kt` | Emulator loop thread (50 fps), NanoHTTPD server, keyboard event logging |
| `C64Engine.kt` | JNI interface to VICE — init, runFrame, getVideoBuffer, injectKey, loadDisk |
| `C64DisplayView.kt` | SurfaceView — blits 320×200 ARGB framebuffer, scaled with correct aspect ratio |
| `C64KeyboardView.kt` | Multi-touch virtual C64 keyboard; fires `KeyEventListener` on press/release |
| `jni/vice_jni.c` | JNI wrapper — custom VICE video canvas writes frames to `g_framebuf[320×200]` |
| `jni/CMakeLists.txt` | NDK build; conditionally links `libvice.a` when `HAVE_VICE_SRC=1` |
### Disk images
The four original `.d64` disk images are in `versions/`. Load via:
```kotlin
engine.loadDisk(filesDir.absolutePath + "/schwert_und_magie_1.d64")
```
(Copy the relevant `.d64` to `filesDir` first via `adb push`.)