Files
schwert_und_magie_on_pebble/docs/architecture.md
T
ml aaa6e772a7 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)
2026-06-25 09:27:19 +02:00

163 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```