Files
virtualjaguar-libretro/app/test/input_e2e.cpp
Joseph MattielloandClaude Opus 5.5 a79b0820dd feat(app): controller remapping incl. the 12-key Jaguar keypad (#837)
Profiles (keyboard, every gamepad, one controller model) with per-game
overrides keyed by cartridge CRC, SDL3 gamepad auto-detection plus an
optional gamecontrollerdb.txt, "press a button to bind" capture, and a
modal keypad overlay.

The keypad reaches the core through the standard retro_* input API: pad
controls ride the RetroPad bitmask; keypad keys (which have no RetroPad
button for 7, 8, 9, * and #) are synthesised RETRO_DEVICE_KEYBOARD keys on
the keys libretro.c's default path polls, and the host announces a keyboard
after every load so a gamepad-only player is not locked out.

Tests: remap_unit (pure logic) and input_e2e (drives the real core and reads
back the joypad matrix for every control on both ports).

Refs #837

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 22:01:47 -04:00

190 lines
6.7 KiB
C++

// End-to-end input test: drives the REAL core (statically linked, through the retro_*
// API via CoreHost) with the app's control -> core translation (CoreInput) and
// reads back the Jaguar's own joypad matrix (joypad0Buttons / joypad1Buttons, the
// bytes joystick.c serves to the 68000). For every Jaguar control on both ports:
// exactly that matrix slot is set and no other. This is what proves the keypad
// (incl. 7, 8, 9, * and #, which have no RetroPad button) really reaches the core.
//
// usage: vj_input_e2e <rom.j64>
#include "core_host.h"
#include "paths.h"
#include "remap.h"
#include "libretro.h"
#include <cstdio>
#include <ctime>
#include <filesystem>
#include <string>
extern "C" {
// src/jerry/joystick.h: socket 0 = slots [0..20]; 4 sockets x 21 slots.
extern uint8_t joypad0Buttons[];
extern uint8_t joypad1Buttons[];
}
using namespace vj;
static int g_fail = 0, g_checks = 0;
#define CHECK(cond) \
do { \
g_checks++; \
if (!(cond)) { \
g_fail++; \
std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \
} \
} while (0)
static const int kSlots = 4 * 21;
// Expected matrix slot per control, transcribed from the BUTTON_* enum in src/jerry/joystick.h
// (BUTTON_s=4 BUTTON_7=5 BUTTON_4=6 BUTTON_1=7 BUTTON_0=8 BUTTON_8=9 BUTTON_5=10 BUTTON_2=11
// BUTTON_d=12 BUTTON_9=13 BUTTON_6=14 BUTTON_3=15; A=16 B=17 C=18 OPTION=19 PAUSE=20).
static int slotFor(vj::Ctl c) {
switch (c) {
case vj::C_UP: return 0;
case vj::C_DOWN: return 1;
case vj::C_LEFT: return 2;
case vj::C_RIGHT: return 3;
case vj::C_A: return 16;
case vj::C_B: return 17;
case vj::C_C: return 18;
case vj::C_OPTION: return 19;
case vj::C_PAUSE: return 20;
case vj::C_K0: return 8;
case vj::C_K1: return 7;
case vj::C_K2: return 11;
case vj::C_K3: return 15;
case vj::C_K4: return 6;
case vj::C_K5: return 10;
case vj::C_K6: return 14;
case vj::C_K7: return 5;
case vj::C_K8: return 9;
case vj::C_K9: return 13;
case vj::C_STAR: return 4;
case vj::C_HASH: return 12;
default: return -1;
}
}
static CoreInput g_in;
// How many matrix bytes are set on a port, and the (first) slot.
static int pressed(unsigned port, int* firstSlot) {
const uint8_t* m = port == 0 ? joypad0Buttons : joypad1Buttons;
int n = 0;
*firstSlot = -1;
for (int i = 0; i < kSlots; i++) {
if (m[i]) {
if (*firstSlot < 0) *firstSlot = i;
n++;
}
}
return n;
}
static void runWith(CoreHost& host, unsigned port, ControlSet c) {
g_in.beginFrame();
g_in.addControls(port, c);
host.runFrame();
host.runFrame(); // the held state must be stable across frames, not a one-frame artefact
}
int main(int argc, char** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: vj_input_e2e <rom.j64>\n");
return 2;
}
namespace fs = std::filesystem;
std::error_code ec;
fs::path tmp = fs::temp_directory_path(ec) / ("vj-input-e2e-" + std::to_string((long long)std::time(nullptr)));
fs::create_directories(tmp / "saves", ec);
fs::create_directories(tmp / "system", ec);
std::string sys = (tmp / "system").string() + "/", sav = (tmp / "saves").string() + "/";
CoreHost& host = CoreHost::instance();
host.setDirectories(sys, sav);
host.inputPoll = []() {};
host.inputState = [](unsigned p, unsigned d, unsigned i, unsigned id) { return g_in.state(p, d, i, id); };
std::string err;
// ---- the keyboard-presence announcement is what unlocks the keypad ------------------------
host.announceKeyboard = false;
if (!host.loadContent(argv[1], err)) {
std::fprintf(stderr, "load failed: %s\n", err.c_str());
return 2;
}
{
int slot = -1;
runWith(host, 0, ControlSet());
CHECK(pressed(0, &slot) == 0);
ControlSet a;
a.set(C_A);
runWith(host, 0, a);
CHECK(pressed(0, &slot) == 1 && slot == slotFor(C_A)); // the RetroPad needs no announcement
ControlSet k;
k.set(C_K7);
runWith(host, 0, k);
CHECK(pressed(0, &slot) == 0); // keypad 7: no keyboard seen yet -> nothing
host.sendKeyEvent(false, 0, 0, 0); // ...the announcement finishLoad() makes
runWith(host, 0, k);
CHECK(pressed(0, &slot) == 1 && slot == slotFor(C_K7));
}
// ---- every control, both ports, with the announcement as the app makes it -------------------
host.announceKeyboard = true;
if (!host.loadContent(argv[1], err)) {
std::fprintf(stderr, "reload failed: %s\n", err.c_str());
return 2;
}
for (unsigned port = 0; port < 2; port++) {
for (int i = 0; i < kJaguarControls; i++) {
Ctl c = (Ctl)i;
ControlSet cs;
cs.set(c);
runWith(host, port, cs);
int slot = -1, n = pressed(port, &slot), other = -1;
CHECK(n == 1);
CHECK(slot == slotFor(c));
CHECK(pressed(1 - port, &other) == 0);
if (n != 1 || slot != slotFor(c))
std::fprintf(stderr, " port %u control %s: %d slot(s) set, first=%d, want %d\n", port + 1, ctlId(c), n, slot,
slotFor(c));
}
}
// Both players' keypads at once, and a chord of pad + keypad on one port.
{
g_in.beginFrame();
ControlSet a;
a.set(C_K7);
a.set(C_B);
a.set(C_HASH);
ControlSet b;
b.set(C_STAR);
b.set(C_K9);
g_in.addControls(0, a);
g_in.addControls(1, b);
host.runFrame();
host.runFrame();
CHECK(joypad0Buttons[slotFor(C_K7)] && joypad0Buttons[slotFor(C_B)] && joypad0Buttons[slotFor(C_HASH)]);
CHECK(joypad1Buttons[slotFor(C_STAR)] && joypad1Buttons[slotFor(C_K9)]);
int s;
CHECK(pressed(0, &s) == 3);
CHECK(pressed(1, &s) == 2);
}
// Releasing everything releases everything.
{
runWith(host, 0, ControlSet());
int s;
CHECK(pressed(0, &s) == 0 && pressed(1, &s) == 0);
}
host.unloadContent();
fs::remove_all(tmp, ec); // our own temp dir only
std::printf("input_e2e: %d checks, %d failed\n", g_checks, g_fail);
return g_fail ? 1 : 0;
}