Document VPX emulator status and recovery plan

This commit is contained in:
MrARM
2026-07-14 23:10:10 -05:00
parent 717cc7d94f
commit 32751448c6
9 changed files with 1073 additions and 13 deletions

197
native-playable-table.md Normal file
View File

@@ -0,0 +1,197 @@
# Native macOS Playable Table
## Status
The first native playable-cabinet implementation lives under `emulation/`.
It combines two existing, independently useful runtimes:
- QEMU 11 with HVF runs the generated ARM64 guest and genuine Pokémon Pro game
code. Its Cocoa window and CoreAudio output are the backbox, game display,
music, speech, and game effects.
- Visual Pinball X 10.8.1 owns the physical balls, collision geometry,
switches, trough inventory, and moving playfield mechanisms.
The supported first release is deliberately two-window. It does not use VNC,
noVNC, browser capture, JPEG screendumps, or the dashboard for gameplay. The
dashboard remains an optional diagnostic monitor. This is an
evidence-calibrated development recreation; it is not yet a playable release
and is not a measurement-validated replica of ramp heights, coil forces, or
rebound curves.
The one-command Finder entry point is `emulation/run-playable.command`; the
terminal entry point is `emulation/scripts/run-playable-macos.sh`.
### Current stop points (2026-07-14)
The latest native launch is blocked first by a VPX script error:
```text
Runtime error: Variable is undefined: 'Service1Key'
```
The table uses the unsupported legacy names `Service1Key` through
`Service4Key`. The pinned VPX host exposes those actions as
`VPXActionKey(25)` through `VPXActionKey(28)`. Since the references are inside
the common key handler, an input event can abort before even non-service input
is forwarded. This must be repaired and exercised in the native host before
the cabinet path can be called playable.
The next confirmed blocker is the power-distribution input model. Start and
Action physically enter the power-distribution board and are returned to the
game by command `0x15`, active-low in bits 1 and 0. Bit 2 is the active-low
cabinet-door input. The current generic `0xff` response therefore leaves both
buttons released and the door open, which is consistent with the observed
factory/pre-play setup screen remaining stuck. Correct idle/Start/Action/both
responses are `0x03`, `0x01`, `0x02`, and `0x00` respectively.
Separately, the game's CPU-SPI receive alignment is `00 || rx[0..6]`.
`0-SW-23` must therefore be raw byte 1 bit 7 low, making the correct idle raw
SPI record `ff7fffffffffffff`. The complete ordered plan and acceptance
checklist are in `emulation/VPX_EMULATOR_STATUS.md` in the parent emulator
repository.
## Runtime ownership
| Concern | Authority |
| --- | --- |
| Rules, scoring, player/ball progression | Genuine game process in QEMU |
| Display, music, speech, game effects | QEMU Cocoa/CoreAudio |
| Ball physics and collision geometry | Native VPX at a 1,000 Hz physics step |
| Physical switch state and pulses | VPX table script and controller plugin |
| Lamps and mechanism driver commands | Genuine game through netbridge |
| Mechanical ball/rubber/flipper/gate sounds | VPX only |
| Window placement and focus | Native macOS helper |
`emulation/vpx/layout.json` is the single mapping source. It records 45
playfield switches plus 11 sensors, 13 drivers, 113 logical lights expanded to
150 exact SPIKE channels, six serveable trough balls, and a separate captive
ball. The plugin mapping header is generated from this manifest by
`emulation/vpx-plugin/generate-layout-mapping.py`; it is not a second
handwritten address map.
## Playfield protocol and safe recovery
`analysis/netbridge-emulator/netbridge_emu.py` provides a persistent,
loopback-only JSONL endpoint at `127.0.0.1:8091` using
`spike3-playfield/v1`. VPX sends `hello`, `switch.set`, `switch.pulse`,
`cabinet.set`, `release_transient`, and `heartbeat`. The bridge sends complete
`snapshot` records, coalescible `lamp.delta` records, lossless ordered
`driver.pulse` records, and `status` records.
Every server record has a process-unique `bridge_session`, a monotonic launcher
`bridge_generation`, sequence, model revision, and monotonic timestamp. A new
connection is not considered ready after TCP connect or `hello`. The bridge
first sends a disconnected snapshot and `synchronizing`; the plugin releases
transients, restores stable switch/cabinet state, and sends a heartbeat as an
ordering barrier. The bridge reports `ready` and sends QEMU monitor `cont` only
after that barrier and after the guest UART is connected.
With `physics_authority=vpx`, the netbridge synthetic shooter, trough, outlane,
VUK, flipper-EOS, and ball-return shortcuts are disabled from process start.
On a VPX disconnect, transient contacts open, VPX mechanisms de-energize,
stable trough/held-ball contacts remain, and QEMU receives monitor `stop`. A
supervised bridge restart removes the old readiness record, restores only
stable contacts plus lamp/driver snapshot baselines, assigns a new session and
generation, and never replays cumulative driver counts as new pulses.
The implementation and synchronization tests are in
`analysis/netbridge-emulator/test_netbridge_emu.py`. The native supervisor is
`emulation/scripts/run-macos.sh`; the outer cabinet lifecycle and identity
checks are in `emulation/scripts/run-playable-macos.sh`.
## VPX plugin and table modes
`emulation/vpx-plugin/` is rebased to the installed VPX commit
`ca80d1604e6a525db2a3f6d1ec704fc651d573fa`. Its builder checks the exact Git
revision, host binary SHA-256, ARM64 architecture, and bundle version before
writing `emulation/work/vpx-plugin/Spike3Controller/vpx-host-attestation.txt`.
The playable launcher copies the attested app to a private workspace runtime,
installs the matching plugin there, and ad-hoc signs that private copy. It does
not modify the user's source VPX app.
Network work runs on a background thread. Bounded queues keep socket I/O away
from VPX's render/physics thread; lamp state is coalesced, while driver events
retain order and cumulative counters. Reconnect snapshots establish a driver
counter baseline and cannot become stale physical events.
The table has explicit modes:
- `standalone`: preserves the self-contained greybox rules and mechanisms.
- `spike3`: QEMU is authoritative for lamps and every driver-driven mechanism.
- `qa-spike3`: integrated handshake/input/output acceptance without falling
back to standalone behavior.
- Other `qa-*` modes are deterministic native geometry, shot, sling-authority,
plunger, and inventory checks documented in `emulation/vpx/README.md`.
In integrated mode, a missing controller is fail-safe: the table remains in
SPIKE mode, disables standalone serving/GI, de-energizes mechanisms, and logs
the error. It does not silently animate local flippers. Physical flippers,
slings, pops, trough eject, auto-launch, posts, gate, Meowth motor, and ejects
move only from mapped game driver pulses.
## Physics and private visual assets
The versioned `pokemon-pro-vpx-physics/v1` record in
`emulation/vpx/layout.json` fixes the inspected service-manual values at a
6.5-degree pitch, 1-1/16-inch steel ball, and 50 Shore A rubber. Every physics
or dimension value is tagged `manual`, `photo_estimate`, `comparable_part`, or
`playability_tuned`.
The registered overhead photograph is the provisional playfield texture; the
manual locator remains a hidden registration/QA layer. Reconstructed post and
pop meshes sit over simple collision bodies. Extracted Pokémon images are used
only as local toy decals or temporary plastics, and unmeasured toys/wireforms
remain labeled proxy geometry. The table, extracted artwork, reference photos,
manual, and game audio are private-use inputs and are excluded from any
redistributable package.
## Controls and native windows
The launcher writes a private VPX input profile without overwriting existing
service bindings. Defaults are:
| Input | Function |
| --- | --- |
| Left/Right Shift or LB/RB | Flippers |
| Return, right trigger, or right stick | Plunger/launch |
| `1` or Menu | Start |
| `5` or View | Coin |
| Return, Left Option, or A | Action |
| `7`/`8`/`9`/`0` or D-pad | Service back/down/up/enter |
VPX receives focus. On two displays the AppKit/Accessibility helper attempts a
fullscreen VPX playfield plus fullscreen QEMU backbox. On one display it uses
approximately 65% for VPX and 35% for a 16:9 backbox. If Accessibility access
is declined or a window does not expose fullscreen through AX, both processes
remain running and the launcher prints the saved placement log and manual
layout.
## Verification boundary
The reproducible checks are split by what they prove:
- `python3 -m unittest discover -s analysis/netbridge-emulator -v` verifies
recovered serial behavior, protocol ordering, output identity, safe pause,
snapshot restore, synchronization, and lossless driver delivery.
- `emulation/vpx-plugin/tests/run-tests.sh` verifies the generated mapping,
background client, ordered input edges, output records, driver baseline, and
reconnect behavior against a loopback server.
- `emulation/vpx/build.sh` plus `emulation/vpx/qa.sh` verifies the assembled
VPX, source round-trip, exact mapping counts, physics tags, ball inventory,
and collision topology.
- The native `emulation/vpx/qa-*.sh` launchers exercise VPX itself rather than
only inspecting generated JSON.
- A complete cabinet run additionally has to prove the real QEMU game accepts
inputs/physical switches and drives VPX mechanisms through the live bridge.
Do not infer a normal three-ball game, multiball, measured frame-rate percentile,
or 60-minute invariant soak from the static/unit suites. Those acceptance rows
must remain explicitly open until their corresponding native run logs exist.
## Optional composed backbox
QEMU D-Bus shared-memory scanout is a follow-up only. It must demonstrate
correct page flips, sustained 60 fps for ten minutes, and no more than one
added frame before being connected to a VPX Backglass/ScoreView texture. The
working two-window Cocoa path remains the fallback; VNC/browser/screendump
capture is not an accepted gameplay fallback.