Files
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

223 lines
10 KiB
Markdown
Raw Permalink 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.
# Debugging guide
How to get diagnostic output from both apps, plus a catalog of real bugs hit
during development — each written as symptom → diagnosis → fix, since that's
the order you'll actually encounter them in. See `CLAUDE.md` for build/install
commands and `docs/architecture.md` for what each component does.
## 1. Getting logs
### Watch app
```bash
pebble logs --phone <phone-ip>
```
Streams both the C app's `APP_LOG`/`text_layer` state changes and the
PebbleKit JS `console.log` output from inside Core for Pebble, interleaved.
This is the only place JS errors (failed `fetch`, JSON parse errors,
`sendAppMessage` failures) show up — they are otherwise silent on the watch.
### Companion app
```bash
adb logcat | grep -E 'ViceJNI|SuM'
```
- Tag `ViceJNI` (`vice_jni.c`) carries two kinds of lines:
- Direct `LOGI`/`LOGE` calls from our own JNI code, e.g.
`machine_read_snapshot → 0`.
- Everything VICE itself prints to stdout/stderr, redirected through a pipe
and re-emitted with a `VICE: ` prefix by `stderr_reader_thread`.
**These two categories run on different threads** — see §3.3, it's a useful
fact when you need to tell whether code is actually running on the VICE
thread.
- Tag `SuM` (`MainActivity.kt`, `TAG` constant) carries UI-level events: watch
key presses, disk load/attach results, HTTP server start/stop.
## 2. Watch app pitfalls
### 2.1 App shows splash, then exits back to the watch's launcher
**Symptom:** the splash window appears, the timer fires, and instead of
transitioning to the main screen the app quits entirely (back to the watch
face or app list) — not a crash log, just gone.
**Diagnosis:** the Pebble window stack was briefly empty. The original splash
implementation did:
```c
static void splash_timeout_handler(void *data) {
window_stack_push(s_main_window, true); // queued, not necessarily applied yet
window_stack_remove(s_splash_window, false);
}
```
Pebble's own docs warn: *"If there are no windows for the app left on the
stack, the app will be killed by the system, shortly."* Push and remove are
not guaranteed to be atomic with respect to each other from the app's
perspective, so this push-then-remove ordering can race.
**Fix:** never let the stack reach zero windows, even momentarily. Push the
*replacement* window first and keep it on the stack permanently underneath
the transient one, so the timeout handler only ever pops — it never has to
push-and-remove in the same breath:
```c
// init(): main_window pushed first, splash pushed on top of it.
window_stack_push(s_main_window, true);
...
window_stack_push(s_splash_window, true);
// timeout handler: main_window is still underneath, so the stack never empties.
static void splash_timeout_handler(void *data) {
window_stack_remove(s_splash_window, true);
}
```
### 2.2 AppMessage silently never arrives / `sendAppMessage failed`
**Symptom:** PebbleKit JS logs a `sendAppMessage failed` error, or nothing at
all and the watch's `TextLayer` never updates.
**Diagnosis checklist:**
- **Inbox too small.** The watch's `app_message_open(inboxSize, outboxSize)`
inbox must fit the *largest* payload plus dictionary overhead. The screen
mirror payload is up to ~2080 bytes (40×25 cells, up to 2 UTF-8 bytes each
for umlaut overrides, + 25 newlines) — the inbox is opened at 2200 bytes to
leave headroom. If you add a new larger payload type, bump this.
- **Symbolic keys.** `{ 'SCREEN': value }` does not reliably resolve via
`package.json`'s `messageKeys` under Core for Pebble. Always use the
hardcoded numeric key, identically defined in both the C `enum` and
`index.js` (`KEY_SCREEN = 2` in both places).
- **`localhost` vs `127.0.0.1`.** Core for Pebble's JS runtime does not
resolve `localhost``pkjs`'s `fetch`/`XMLHttpRequest` calls must target
`127.0.0.1:8888`.
### 2.3 Pebble build fails with a Python traceback mentioning `relpath`
**Symptom:**
```
AttributeError: 'NoneType' object has no attribute 'relpath'
File ".../waflib/extras/process_sdk_resources.py", line 22, in find_most_specific_filename
```
**Diagnosis:** the Pebble SDK's build system hardcodes the resource folder
name to `resources/` (`bld.path.find_node("resources")` in
`pebble_sdk.py`) — *not* `res/`. If `package.json`'s `resources.media[].file`
points at a file that doesn't exist under a real top-level `resources/`
folder, `resources_node` resolves to `None` and the path-join call inside the
SDK throws this traceback instead of a normal "file not found" error.
**Fix:** keep all `media` resource files (PNGs, etc.) under a top-level
`resources/` directory, never `res/`.
## 3. Companion app / VICE pitfalls
### 3.1 Snapshot restore: screen looks right, but buttons do nothing
**Symptom:** after `loadState()`, the saved screen renders correctly (memory,
VIC-II, SID, CIA state are all genuinely restored), but the game never
responds to keyboard/joystick input again.
**Diagnosis:** `maincpu_mainloop()` keeps the 6510's registers as
**stack-local C variables** (`reg_pc`, `reg_a`, ...), not in the global
`maincpu_regs` struct. They're synced only inside `DO_INTERRUPT()`'s
`EXPORT_REGISTERS()` / `IMPORT_REGISTERS()` macros. Calling
`machine_write_snapshot()` / `machine_read_snapshot()` directly from
`video_canvas_refresh()` (i.e. outside an interrupt) means:
- **Save** records whatever `maincpu_regs.pc` happened to hold from the *last*
interrupt — stale, not the actual current PC.
- **Load** writes the saved PC into `maincpu_regs.pc`, but the CPU loop keeps
running from its own unrelated `reg_pc` — the restored PC never takes
effect.
The CPU ends up executing from a PC that has nothing to do with the saved
game state. Screen/CIA/SID content looks fine because *those* are restored
correctly; only the actual instruction pointer is wrong, which silently
breaks "the game keeps running but never processes input" without crashing.
**Fix:** route both save and load through `interrupt_maincpu_trigger_trap()`,
so the snapshot call happens inside `DO_INTERRUPT(IK_TRAP)` and gets the
`EXPORT_REGISTERS()`/`IMPORT_REGISTERS()` sync for free. See
`save_state_trap()` / `load_state_trap()` in `vice_jni.c`, and
`docs/architecture.md` §4.4 for the full sequence diagram.
**How this was actually diagnosed:** by comparing thread IDs in `adb logcat`.
`LOGI("machine_read_snapshot → %d", r)` (called directly from inside the trap
function) appeared on the same TID as `android_sound_close()`'s Android
audio-stack log lines — i.e. the VICE/CPU thread. The `VICE: ...`-prefixed
lines (from `stderr_reader_thread`) appeared on a *different* TID. That
confirmed the trap really was running on the CPU thread, which ruled out a
threading mistake and pointed at the register-sync logic instead.
### 3.2 `VICE: Sync reset` in the log is not a C64 reset
**Symptom:** `VICE: Sync reset` appears in logcat right after a snapshot
load, looking like the machine just got reset (which would explain broken
input — but doesn't, in fact).
**Diagnosis:** this line comes from `vsync_suspend_speed_eval()` in
`vsync.c`, triggered when the sound device closes/reopens during the
snapshot's `sound_snapshot_finish()` call. It resets internal frame-timing
statistics only — it is unrelated to `machine_reset()`/`maincpu_reset()` and
does not touch CPU or memory state. Don't chase it as the cause of a
post-restore bug; it's noise.
### 3.3 `VICE: Error - T64 snapshot support is not implemented`
**Symptom:** this warning appears on every snapshot load, but
`machine_read_snapshot()` still returns `0` (success).
**Diagnosis:** snapshots are saved with `save_disks=0` (disk management is
handled by our own UI, not VICE's), so the drive module writes a minimal
entry on save. On restore, `drive_snapshot_read_module()` logs this warning
while parsing that minimal entry but still returns success — harmless.
### 3.4 Disk/reset/snapshot APIs must only be called from the VICE thread
**Symptom:** intermittent corruption or crashes after calling `loadDisk()`,
`attachDisk()`, `resetMachine()`, `saveState()`, or `loadState()` directly
from the calling (UI or HTTP) thread.
**Diagnosis:** all five funnel into mutex-guarded pending-path buffers
(`g_pending_disk`, `g_pending_attach`, `g_pending_reset`,
`g_pending_save_state`, `g_pending_load_state`) that `video_canvas_refresh()`
drains once per rendered frame, on the VICE thread. This is intentional —
`autostart_disk()`, `file_system_attach_disk()`, `machine_trigger_reset()`,
and the snapshot calls are not safe to call cross-thread.
**Fix:** never call these JNI entry points' underlying VICE functions
directly from Kotlin; always go through the existing pending-queue pattern
in `vice_jni.c` if adding a new one.
### 3.5 `injectKey()` / `getScreenText()` have no locking — is that a bug?
No — this is deliberate, not a bug to "fix" if you notice it while reading
the code. Both are called directly from non-VICE threads (the UI thread for
on-screen keyboard taps, an HTTP worker thread for watch input and the screen
mirror poll) without synchronizing with the VICE thread. Worst case, a race
produces one stale keyboard-matrix bit or one stale screen byte for a single
frame, which self-corrects on the next call. This is a different category
from §3.1/§3.4: those are correctness-critical (a wrong PC or a cross-thread
VICE API call corrupts state permanently), this isn't.
## 4. Isolating the watch UI from the companion app
To iterate on watch-side rendering/layout without a live companion app or
phone, simulate the AppMessage payload directly against an emulator instance:
```bash
pebble install --emulator emery --vnc
pebble send-app-message --emulator emery --vnc --string 2="<40x25 test text>"
pebble screenshot --vnc --no-open /tmp/screen.png
```
(Key `2` is `KEY_SCREEN` — see the protocol table in `docs/architecture.md`.)
This was how word-wrap and clipping behavior were verified across screen
sizes (emery vs. basalt) before wiring up the real HTTP/VICE pipeline. Prefer
testing on a real watch once the companion app is in the loop — the emulator
is best for fast, isolated layout iteration, not for verifying end-to-end
behavior.