Replace bundled ROM extraction with runtime import/download flow

Commodore ROMs are copyrighted and can no longer ship inside the APK.
MainActivity now detects missing ROMs on first launch and blocks
startup with a dialog offering two paths: pick files via the system
file picker, or download the official VICE 3.8 tarball and extract the
four ROMs from it client-side (minimal USTAR reader, no extra deps).

- Drop the extractViceRoms Gradle task and asset bundling; add
  res/extract_roms.sh for local sideloading during development instead
- gitignore keystores/keystore.properties ahead of a signed release
- Move architecture.md into docs/, refresh it for the screen-mirroring
  and watch-input additions, and add accompanying Mermaid diagrams
- Add docs/debugging.md and docs/publish.md (Play Store release notes,
  ROM-import compliance rationale)
This commit is contained in:
ml
2026-06-25 09:27:19 +02:00
parent 89c99b50e2
commit aaa6e772a7
26 changed files with 893 additions and 172 deletions
+162
View File
@@ -0,0 +1,162 @@
# Architecture
This document describes the current runtime architecture of the two apps in this
repo: the **Pebble watch app** and the **Android companion app**. For build/install
commands, see `CLAUDE.md`.
## 1. System overview
Three processes cooperate across two devices. **Core for Pebble** is a separate
app on the phone (not part of this repo) that bridges Bluetooth AppMessage traffic
to a JS runtime; our companion app talks to it only via loopback HTTP.
![System overview diagram](diagrams/system-overview.png)
## 2. Pebble watch app
`SchwertUndMagieOnPebbleWatchApp/src/c/SchwertUndMagieOnPebbleFrontend.c` is a
single-file C watchapp built around three `Window`s on a shared stack, plus
`src/pkjs/index.js` running inside Core for Pebble.
![Watch app window stack state diagram](diagrams/watch-window-stack.png)
- **Splash** — `BitmapLayer` showing `resources/splash.png`, centered, black
backdrop. Pushed *on top of* the already-pushed Main window at startup (not
in place of an empty stack — removing the last window on the stack kills the
app), then removed by an `AppTimer` after 1.8s.
- **Main** — a `ScrollLayer` wrapping a `TextLayer` that mirrors the C64 text
screen. UP/DOWN are claimed entirely by the ScrollLayer's built-in click
config (pan only); SELECT is added via `scroll_layer_set_callbacks()`'s
`click_config_provider` hook and opens the wheel. Content height is
recomputed via `graphics_text_layout_get_content_size()` every time new
screen text arrives, since wrapped height varies frame to frame.
- **Wheel** — a single large `TextLayer` cycling through a curated,
navigation-only key set: `1``9`, `0`, `RETURN`, `SPACE` (movement in this
game is mostly done with number keys). UP/DOWN rotate the index, SELECT
sends the highlighted label and pops back to Main, BACK cancels for free
(default window-pop behavior, left unsubscribed).
### AppMessage protocol
| Key | Value | Direction | Payload |
|---|---|---|---|
| `TIME` | 0 | — | Unused (legacy; originally the clock, replaced by `SCREEN`) |
| `COMMAND` | 1 | watch → phone | Wheel item label: one of `1`..`9`, `0`, `RETURN`, `SPACE` |
| `SCREEN` | 2 | phone → watch | UTF-8 C64 screen text, 40×25 cells, `\n` per row |
Numeric keys are hardcoded identically in the C app and `index.js` — symbolic
key resolution from `package.json`'s `messageKeys` is unreliable with Core for
Pebble. The watch's AppMessage inbox is opened at 2200 bytes to fit the
worst-case screen payload (40×25 cells × up to 2 UTF-8 bytes for umlaut
overrides, + 25 newlines).
## 3. Android companion app
![Companion app thread diagram](diagrams/companion-threads.png)
- **Main/UI thread** — `MainActivity`'s `Choreographer.postFrameCallback` loop
drives rendering: on every hardware vsync it calls `display.captureFrame(engine)`
(copies VICE's 320×200 ARGB framebuffer into a Bitmap) and `invalidate()`.
VICE itself runs continuously and asynchronously in its own thread, decoupled
from this vsync sampling. Touch input (`C64KeyboardView`, disk drawer
buttons) and NanoHTTPD callbacks (marshaled via `mainHandler`) also run here.
- **VICE thread** (`vice_thread` in `vice_jni.c`) — runs `main_program()`
`maincpu_mainloop()`, VICE's own CPU/VICII loop. `video_canvas_refresh()` is
our hook into this loop, called once per rendered region: it drains
mutex-guarded pending-operation queues (disk autostart/attach, hard reset,
snapshot save/load) written from the UI or HTTP threads, then renders into
`g_framebuf`.
- **NanoHTTPD worker thread(s)** — `CompanionServer` (in `MainActivity.kt`)
serves `/time`, `/key?cmd=`, `/screen` to Core for Pebble's JS. `onKey` and
`getScreenText` call directly into the JNI layer from this thread (see
§5 on tolerated races).
- **stdout/stderr reader threads** — pipe VICE's redirected stdout/stderr to
Logcat under tag `ViceJNI`, prefixed `VICE: `.
- **OpenSL ES callback thread** — pulls PCM samples from a ring buffer filled
by VICE's registered `android` sound driver.
### Key source files
| File | Role |
|---|---|
| `MainActivity.kt` | UI, Choreographer render loop, NanoHTTPD server, disk drawer, watch key wheel → `injectKey` mapping |
| `C64Engine.kt` | JNI external-function declarations + C64 keyboard matrix constants |
| `C64DisplayView.kt` | Double-buffered View blitting the 320×200 ARGB framebuffer |
| `C64KeyboardView.kt` | On-screen virtual C64 keyboard (multi-touch, sticky shift) |
| `vice_jni.c` | VICE integration: thread management, video/sound drivers, pending-op queues, snapshot CPU-trap dispatch, `getScreenText()` |
## 4. Data flows
### 4.1 Screen mirror (VICE → watch)
![Screen mirror sequence diagram](diagrams/screen-mirror-flow.png)
`getScreenText()` reads C64 screen RAM at the fixed default address `$0400`
(same assumption `autostart.c` makes when checking for KERNAL "READY." text —
this game never relocates the VIC-II screen pointer) and converts each
screencode → PETSCII → ASCII via VICE's own `charset.c` tables. The game
uploads a **custom character set** that redefines a consecutive run of
otherwise-unused screencodes to draw German umlauts; a small override table in
`getScreenText()` catches these and emits proper UTF-8 before falling through
to the standard conversion:
| Screencode | Stock glyph | Overridden to |
|---|---|---|
| `0x1B` | `[` | ä |
| `0x1C` | `£` | ö |
| `0x1D` | `]` | ü |
| `0x1E` | `↑` | ß |
### 4.2 On-screen keyboard input (phone touch → VICE)
![On-screen keyboard input sequence diagram](diagrams/keyboard-input-flow.png)
Composite keys (e.g. ↑ = LSHIFT + CUR_UD) carry a list of codes; all are
pressed/released together.
### 4.3 Watch key wheel input (watch → VICE)
![Watch key wheel input sequence diagram](diagrams/watch-wheel-input-flow.png)
### 4.4 Snapshot save/load (CPU-trap register sync)
![Snapshot save/load CPU-trap sequence diagram](diagrams/snapshot-save-load-flow.png)
Both save and load **must** run inside a CPU trap. `maincpu_mainloop()` keeps
CPU registers as stack-local variables (`reg_pc`, `reg_a`, ...), syncing them
with the global `maincpu_regs` struct only via `EXPORT_REGISTERS()` /
`IMPORT_REGISTERS()` inside `DO_INTERRUPT`. Calling `machine_write_snapshot`/
`machine_read_snapshot` directly from `video_canvas_refresh()` (outside a trap)
would read/write a stale `maincpu_regs.pc` — the snapshot would record (or
restore) the wrong program counter, leaving the CPU executing from the wrong
address after a load even though screen/CIA/SID state all looked correct.
### 4.5 Disk load / attach / reset
`loadDisk()` (full reset + autostart, used for A-side episode disks) and
`attachDisk()` (hot-swap, used for B-side/hero disks) both just write a path
into a mutex-guarded pending buffer; `video_canvas_refresh()` drains it on the
VICE thread and calls `autostart_disk()` or `file_system_attach_disk()`
accordingly — disk and reset APIs, like snapshot APIs, must only be called
from the VICE thread.
## 5. Tolerated cross-thread races
Two JNI calls are invoked directly from non-VICE threads with no locking:
`injectKey()` (writes the keyboard matrix from the UI thread *or* an HTTP
worker thread) and `getScreenText()` (reads screen RAM from an HTTP worker
thread). Both are deliberate: a keyboard matrix write or a screen-text read
racing with the VICE thread can produce at most one stale byte for one frame,
which self-corrects on the next poll/keypress — acceptable for display and
input purposes. This is a different category from §4.4: snapshot register
sync is correctness-critical (a wrong PC corrupts execution permanently), so
it goes through the CPU trap; keyboard/display reads are not, so they don't.
---
Diagrams are rendered PNGs under `diagrams/`; each has a matching `.mmd`
Mermaid source in the same folder. To regenerate one after editing its source:
```bash
npx @mermaid-js/mermaid-cli -i diagrams/<name>.mmd -o diagrams/<name>.png -b white -s 3
```