The API
At the game-library level:
int vpy_j1_x(void); /* -127..127 */
int vpy_j1_y(void); /* -127..127, + = up */
int vpy_j1_button(int n); /* n = 1..4 -> 0/1 */
void vpy_update_buttons(void); /* force a re-read mid-frame */At the portable PiTrex-style level:
v_readButtons(); /* -> currentButtonState, bit n-1 = button n */
v_readJoystick1Analog(); /* -> currentJoy1X, currentJoy1Y (-127..127) */And the raw SDK calls, if you need both controllers:
uint8_t uvm2_read_buttons(void); /* RAW: active-low, J1 in bits 0-3, J2 in 4-7 */
uint32_t uvm2_read_axes(void); /* four packed int8: j1x, j1y, j2x, j2y */
void uvm2_input_set_analog(int enable);vpy_run() samples the input before each call to your loop(), so in most games you just read the values.
Reads happen between frames
A read needs the data bus turned around in the middle of a cycle, so it cannot be recorded into the command list. Input is read directly, between frames, while /ZERO holds the beam clamped at the centre. Under dual core this happens on core 1, and the game's calls answer from a cache. Reading costs the game nothing and never fights the beam.
Even between frames, reads aren't free for the picture: removing the input read entirely dropped stray bright vectors from 4–5 per frame to 1. That's why /RAMP is held off through the whole conversion.
Digital or analog
Every axis is read with successive approximation, the same algorithm as the BIOS's Joy_Analog, starting from the sign bit. The stick's rest position is not zero (one console measured X 4..12, Y 31..48), so the first reading is taken as the centre and subtracted from every later one.
uvm2_input_set_analog() picks what the game sees:
| mode | values | use it for |
|---|---|---|
| digital (default) | -127, 0 or +127, past half the travel | ordinary if (x > 32) game code, which then behaves as it does on a real cartridge |
| analog | the centred value, 0 inside a small dead zone | steering, aiming, anything proportional |
The console's reset button
Every image built with the SDK watches the Vectrex's own reset button:
| held | does |
|---|---|
| under 1 s | nothing; brushing against the button costs nothing |
| 1 to 3 s, then released | the game restarts from scratch |
| 3 s | back to the UVMC2's menu, at once |
The reset line isn't wired to the cartridge edge, but a VIA in reset stops its timers. Between frames, core 1 reads Timer 1 twice, 300 bus cycles apart; if it hasn't moved, the button is down. While it's held, and for two frames after, the controllers aren't read: a VIA in reset reads back zeros, which on active-low buttons means "all pressed".
A restart boots the image still in SRAM again. Initial values in .data are saved at startup and restored before the reboot. If .data is larger than 4 KB the restart is refused and counted in uvm2_restart_refused.
For debugging over SWD, uvm2_reset_seen counts presses noticed and uvm2_reset_held_us is how long the current one has lasted, so "it never reboots" can be told apart from "it never saw the button".
Debug Cart: this reset handling doesn't apply. The Debug Cart's BIOS has its own way back to its menu: hold all four buttons for a second.
Reserved button combinations
A few combinations are taken by the SDK. Avoid giving them meaning in your game:
| combination | where | does |
|---|---|---|
| buttons 2 + 3 held while launching | any SDK game | opens the calibration screen |
| buttons 1 + 4 held for 2 s | any .um2 game | toggles the diagnostics HUD (see Debugging) |
| buttons 3 + 4 | Debug Cart BIOS | dumps the current command list to the SD card |
| all four, 1 s | Debug Cart BIOS | back to the BIOS menu |