test(input): validate Team Tap and 6D controller against AtariJaguarPadtest

Adds test/tools/padtest_probe.c, a test/harness-based tool that drives
darthcloud's AtariJaguarPadtest ROM (github.com/darthcloud/AtariJaguarPadtest,
v1.0, not committed -- private/unredistributable) through the real
update_input() delivery path, closing the blind spot PR #547 recorded:
register-level tests (sixd_decode_test) call InputDevFeed6D() directly and
can never see a frontend-shaped defect like Z/TY reaching the wire as
analog-button-only values.

Team Tap (#513): fully corroborated end-to-end. padtest's own bus decode
and a direct joypad0Buttons[] memory dump agree exactly with what was fed
to all four sockets. Also reproduces and dismisses padtest's own documented
"small visual glitches" trap with hard evidence (a single glyph rendering
miss with the underlying bits proven correct via memory dump).

6D controller (#538): could not be corroborated, but not because of a core
defect. padtest's detect_ctrl() only calls its bank-switching decoder on
socket 1, and only when a Team Tap is already present; without a tap it
hardcodes socket 0 -- the only socket a bank-switching device can ever
occupy -- to STDPAD unconditionally. This is proven via source (the
padtest.c quote), ground truth (InputDevGetType()==INPUTDEV_6D and
InputDevAnyAttached()==1 throughout), and screen evidence. The TR V10 X-sign
and bank-1 button-order ambiguities from docs/teamtap-procontroller-spike.md
and the 6D user-guide section remain open; findings and the two false-
positive traps are documented in docs/input-devices-user-guide.md.

make TEST_EXPORTS=1 test: 1266 passed, 0 failed, exit 0.

Refs #513, #538.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Opus 5 committed 2026-08-20 23:29:40 -04:00
1 parent 1ced42b14d
commit 5d6fe4cc08
3 files changed
+675 -2

No files matched your search

+1
View File
@@ -137,6 +137,7 @@ test/vendor/
/test/tools/hires_box_check
/test/tools/hires_state_digest
/test/tools/hires_shot
/test/tools/padtest_probe
test/tools/__pycache__/
.build-config
.link-mode
+143 -2
View File
@@ -470,6 +470,51 @@ Reports that the adapter is **inert** on a title that should support it are
just as valuable as crash reports — that is the failure mode the tester ROM
cannot catch.
### Validated end-to-end against a second test ROM (2026-08-20)
The Joypad-TeamTap Tester above proves detection only. A second, independent
program closes that gap: **AtariJaguarPadtest**
(<https://github.com/darthcloud/AtariJaguarPadtest>, Apache 2.0, release v1.0,
`padtest.rom`, md5 `2e1663f2c8bc557f2f94229d8d73e7dc`), by darthcloud — the
BlueRetro author and the same source as the *Technical Reference V10* used to
implement #513/#538. It reads `$F14000` directly, does its own bank and
socket-3 detection, and prints a live per-socket, per-button decode — a
program driving the register set independently of anything in this repo.
Crucially, it was run through the **real delivery path**
(`update_input()` → `$F14000`/`$F14002`), not the register-level decode
functions directly — a shared `test/harness`-based tool
(`test/tools/padtest_probe.c`, not wired into `make test` because the ROM is
private and unredistributable) stands in for the frontend, feeding known
RetroPad state through `input_state_cb` exactly as RetroArch would.
With *Team Tap* selected on Jaguar port 1 and one distinct direction held per
player (socket 0 = Up, socket 1 = Down, socket 2 = Left, socket 3 = Right),
padtest reported all four sockets as `STDPAD` and highlighted exactly the
button fed to each — `^` on socket 0, `v` on socket 1, `<` on socket 2, `>`
on socket 3, nothing else. Cross-checked against the core's own
`joypad0Buttons[]` array (dumped via `harness_dlsym`) after the run: each
socket's slot held exactly the fed direction and nothing else, and
`JoystickGetTeamTap(0)` read true. **Two independent readers — padtest's own
bus decode and a direct memory dump — agree exactly with what was fed.**
This is the first full per-socket, per-button confirmation this feature has
had; the PD tester above only ever exercised the detection bit.
Two things noticed along the way are padtest's own documented known issues,
not bugs in this core, and are recorded here so nobody re-files them:
1. **Frame rate is terrible.** padtest re-detects every controller on every
single frame, by design — a real game does it once at boot.
2. **Occasional single-glyph rendering misses**, reproduced here: holding Up
*and* A together on the plain-joypad path left `^` unhighlighted on
screen while `A` rendered correctly, on two separate runs. A direct
`joypad0Buttons[]` dump proved both bits were correctly set
(`U=255 A=255`) the whole time — the delivery was right and the glitch is
in padtest/JagStudio's own rendering (JagStudio does its own controller
polling that conflicts with padtest's main poll; the padtest author could
not disable it). Any single miss-highlighted glyph should be checked
against a memory dump before being read as a real defect.
## Pro Controller (#514)
Set *Port 1/2 > Controller Type* to **Pro Controller (6-button)**. Default is
@@ -568,7 +613,12 @@ ever been checked against. What ships here is a best attempt built from the
*Jaguar Technical Reference V10* alone — pages 15, 16, 21, 22, 23 and 27–28.
It is conformant to the manual (`test/tools/sixd_decode_test` asserts that,
cell by cell) and **unvalidated against any real program**. Please do not
read "supported" as "verified".
read "supported" as "verified". One published test ROM claims 6D support and
was tried against this core's real delivery path in 2026-08; it turned out
to be structurally unable to reach its own 6D decoder for a directly-attached
device, so this remains unvalidated — see "Attempted validation against
AtariJaguarPadtest" below for the full account and why it isn't the
corroboration it looks like at first.
### What the device is
@@ -657,6 +707,94 @@ the buttons run **A, B, C, D** up the rows, but in bank 1 they run
we implement it as printed — it is the most likely place for the manual (or
our reading of it) to be wrong.
### Attempted validation against AtariJaguarPadtest, and why it could not settle anything (2026-08-20)
The same second test ROM used to validate Team Tap above —
**AtariJaguarPadtest** (<https://github.com/darthcloud/AtariJaguarPadtest>,
darthcloud, v1.0, `padtest.rom`, md5 `2e1663f2c8bc557f2f94229d8d73e7dc`) —
also claims 6D support (its enum has a `SIXDPAD` case and a
`print_6dpad_btns()` that prints `X`/`Y`/`Z`/`TX`/`TY`/`TZ` and the `A`–`G`
buttons). It was run the same way, through `update_input()` via
`test/tools/padtest_probe.c`, feeding a distinct nonzero value per axis
(left stick, right stick, both shoulder pairs) and holding every 6D button.
**It could not corroborate anything about this device, and the reason is a
limitation in padtest itself, not in this core.** padtest's own
`detect_ctrl()` only calls its bank-switching decoder
(`get_basic_type()`/`get_banked_type()`) on **socket 1**, and only when a
Team Tap is already detected on that port. Without a tap it hardcodes socket
0 — the *only* socket a bank-switching device can ever occupy, on this core
and on real hardware (TR10 itself: behind a tap, "software control of
advanced features like … analogue/digital mode will not be possible") — to
`STDPAD`, unconditionally:
```c
/* padtest.c, detect_ctrl(), the no-adapter branch */
dev_type[i][0] = STDPAD;
...
dev_type[i][1] = get_basic_type(i, 1); /* the ONLY socket ever checked */
```
Socket 1 is provably dead in this branch too: its row codes
(`sockets_row_codes[1]`, low nibbles `$0`-`$3`) all decode to Team-Tap socket
1 in this core's own tables, which read idle with no tap attached — matching
the `S1: NONE` padtest prints in every screenshot below. So **padtest cannot
reach its own SIXDPAD decode path for a directly-attached device under any
core option this core offers**, because the one configuration where a
bank-switching device can appear (no tap, socket 0) is exactly the one
branch that never calls the decoder.
This was confirmed three ways, not just inferred from source:
1. **Ground truth, independent of padtest's rendering:** `InputDevGetType(0)`
read `9` (`INPUTDEV_6D`) and `InputDevAnyAttached()` read `1` throughout
— the core correctly selected and engaged the device every time.
2. **Source**: the `detect_ctrl()` quote above.
3. **Screen**: every 6D run printed `P0: S0: STDPAD`, never `SIXDPAD`, with a
handful of standard-pad glyphs (`O C B A` and some digits) lit up more or
less at random — padtest reading our bank-switching bit stream through
the *wrong* (plain-joypad) lookup table. Rerunning with the fed X axis
flipped (`+20500` → `-20500`) and separately with the two shoulder pairs
fed as pure digital presses instead of real analog pressure (see below)
produced **pixel-identical** screenshots outside the frame counter in
both cases (verified with a pixel diff, not by eye) — further evidence
that whatever padtest is showing here is not a meaningful decode of this
device, not evidence about the axes themselves.
One narrower thing this run *does* show, independent of padtest's broken
label: the two runs that fed Z (`R2 − L2`) and TY (`R − L`) as real analog
pressure versus as **digital-only** presses (analog-button read forced to 0,
only the `RETRO_DEVICE_ID_JOYPAD_MASK` bit set — the exact "frontend with no
analog-button support" shape of the original #547 defect) produced
byte-identical output at the bus. That is consistent with `update_input()`'s
fallback (`if (sh[i]==0 && (ret&bit)) sh[i]=32767;`) carrying full-scale Z/TY
across correctly even when only digital state is available — the specific
failure class this whole validation effort exists to catch — but it is *not*
the same as padtest confirming the values are numerically right, which it
cannot do at all for this device. Treat it as "the fallback still fires and
reaches the wire," not as "Z and TY read correctly."
**Net effect on the two open ambiguities below: still open.** The X-sign
question and the bank-1 button order are exactly as unresolved as before
this session — padtest cannot see either one for a directly-attached device.
The liveness gate (STDPAD until an axis deflects) still could not be
isolated as a separate behaviour in this run, since `6D controller selected,
nothing deflected` and `padtest can't decode this device at all` both print
`STDPAD` and are indistinguishable on screen; don't read a `STDPAD` result
here as proof the liveness gate fired, only as consistent with it. The
`Rezero` button is additionally unverifiable through padtest by construction
— its own button table (`btns_6d[]`) has only seven glyphs, `A`-`G`; Rezero
has no glyph in the ROM's own display, tap present or not. Nothing here
displays a torque *sign* either (`TX`/`TY`/`TZ` print as plain signed/unsigned
magnitudes), so the torque-sign question is exactly as unresolvable as
[`docs/teamtap-procontroller-spike.md`](teamtap-procontroller-spike.md) and
the section below already say.
The tool (`test/tools/padtest_probe.c`) is still useful for the *next*
attempt at this — a future BlueRetro-side or homebrew program that reads a
6D controller correctly on socket 0 without a tap could be pointed at it and
would get a real answer.
### Please test this, and tell us what you find
There is no game to try, so what would help is:
@@ -664,7 +802,10 @@ There is no game to try, so what would help is:
- **Homebrew.** Write something that scans the three banks the way TR10
describes, and tell us whether the values arrive where you expect. If you
are testing controller *detection*, remember to deflect an axis first —
otherwise the port honestly reports a standard joypad.
otherwise the port honestly reports a standard joypad. **AtariJaguarPadtest
is not this** — see the subsection above for why its own detection logic
can never reach a directly-attached bank-switching device; a new homebrew
program (or a fix to padtest upstream) is still needed.
- **Real hardware.** If a 6D prototype, or any device that implements this
protocol, actually exists somewhere, a capture of a working scan would
settle the X sign, the torque signs and the bank-1 button order in one go.
+531
View File
@@ -0,0 +1,531 @@
/*
* padtest_probe.c -- drives darthcloud's AtariJaguarPadtest ROM through the
* REAL delivery path (update_input(), not InputDevFeed*() directly) to
* validate Team Tap (#513) and the 6D controller (#538) against a program
* that reads $F14000/$F14002 the way an actual game would.
*
* Why this exists: PR #547 (6D controller) shipped with a register-level
* test (sixd_decode_test) that calls InputDevFeed6D() directly and never
* enters update_input(). That blind spot let Z and TY reach the wire as
* ANALOG-BUTTON-only values; a frontend that does not implement analog
* button reads answered 0 for a held shoulder, leaving two of six axes
* permanently dead even though every register-level assertion passed.
* This tool exercises the full RetroPad -> update_input() -> $F14000
* path with a harness input_callback standing in for the frontend, so it
* CAN see that class of defect. See docs/input-devices-user-guide.md,
* the "Validated end-to-end against a second test ROM" (Team Tap) and
* "Attempted validation against AtariJaguarPadtest" (6D controller)
* subsections.
*
* TWO FALSE-POSITIVE TRAPS (both padtest's own documented known issues,
* neither an emulation bug -- see the app's README):
* 1. Terrible frame rate: padtest re-detects every controller on every
* single frame by design (a real game does it once at boot). Do not
* chase this as a performance regression, and this tool deliberately
* does not measure or report frame timing (see CLAUDE.md rule 6: no
* wall-clock numbers with a concurrent agent fleet on the host).
* 2. Small visual glitches: JagStudio (the dev kit padtest links against)
* does its own controller polling that conflicts with padtest's main
* poll loop. The author could not disable it.
* A third trap belongs on this list, not in padtest's docs but in ours:
* the 6D controller (like the analog/driving controller before it) stays
* a plain RetroPad -- reported as STDPAD, not SIXDPAD -- until an axis
* deflects past ANALOG_THRESHOLD. A "rest" scenario that shows STDPAD is
* the liveness gate working, not a delivery failure.
*
* Build:
* cc -O2 -Wall -std=c99 -I. -I./libretro-common/include \
* -o test/tools/padtest_probe test/tools/padtest_probe.c \
* test/harness/harness.c -ldl -lm
*
* Run (one scenario per invocation; see SCENARIOS below):
* VJ_EXPECT_BUILD=$(./scripts/build-id.sh) ./test/tools/padtest_probe \
* ./virtualjaguar_libretro.dylib test/roms/private/padtest/padtest.rom \
* --scenario 6d_live --option virtualjaguar_p1_device=6d \
* --out /tmp/padtest_6d_live.ppm
*
* The tool always prints, to stdout, the exact digital/analog values it
* fed the core for the run -- read that alongside the screenshot rather
* than trusting recollection of what a scenario "should" have sent.
*/
#include "../harness/harness.h"
#include "../../libretro-common/include/libretro.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PT_MAX_BTN 17
typedef struct {
const char *name;
/* Port 0 (Jaguar port 1) digital buttons held, RETRO_DEVICE_ID_JOYPAD_*
* ids, terminated by -1. */
int p0_buttons[PT_MAX_BTN];
/* Team Tap: three extra sockets on frontend ports 2, 3, 4 (teamtap_user[0]
* in libretro.c), each its own digital button list. */
int use_teamtap;
int tap_buttons[3][PT_MAX_BTN];
/* 6D controller: four analog axes plus four analog-button shoulder
* reads (fed as real pressure unless digital_fallback is set, in which
* case the analog-button read is always 0 and the fallback in
* update_input() -- "if (sh[i]==0 && (ret&bit)) sh[i]=32767" -- has to
* carry Z/TY on its own, exactly the frontend class that produced the
* original defect). */
int use_6d;
int digital_fallback;
int32_t lx, ly, rx, ry; /* left stick X/Y, right stick X/Y */
int32_t sh_l2, sh_r2, sh_l, sh_r; /* shoulder pressure (or digital press w/ 0 analog) */
} pt_scenario;
static pt_scenario g_sc;
static const int PT_NONE[PT_MAX_BTN] = {
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
};
/* -------------------------------------------------------------------
* Scenario table. One entry per padtest_probe --scenario NAME.
* ------------------------------------------------------------------- */
static void pt_scenario_standard(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "standard";
/* Baseline: plain joypad on port 1, hold Up + A so padtest's
* print_stdpad_btns highlights two distinct characters we can read
* off the screenshot ('^' and 'A'). */
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_UP;
sc->p0_buttons[1] = RETRO_DEVICE_ID_JOYPAD_A;
sc->p0_buttons[2] = -1;
}
/* Diagnostic only: press every digital button padtest's stdpad table
* covers (U/D/L/R + P/O/C/B/A), to see whether direction glyphs ever
* render "pressed" the way letter glyphs visibly do. */
static void pt_scenario_standard_all(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "standard_all";
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_UP;
sc->p0_buttons[1] = RETRO_DEVICE_ID_JOYPAD_DOWN;
sc->p0_buttons[2] = RETRO_DEVICE_ID_JOYPAD_LEFT;
sc->p0_buttons[3] = RETRO_DEVICE_ID_JOYPAD_RIGHT;
sc->p0_buttons[4] = RETRO_DEVICE_ID_JOYPAD_SELECT; /* Pause -> 'P' */
sc->p0_buttons[5] = RETRO_DEVICE_ID_JOYPAD_START; /* Option -> 'O' */
sc->p0_buttons[6] = RETRO_DEVICE_ID_JOYPAD_Y; /* C */
sc->p0_buttons[7] = RETRO_DEVICE_ID_JOYPAD_B; /* B */
sc->p0_buttons[8] = RETRO_DEVICE_ID_JOYPAD_A; /* A */
sc->p0_buttons[9] = -1;
}
static void pt_scenario_dirs_only(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "dirs_only";
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_UP;
sc->p0_buttons[1] = RETRO_DEVICE_ID_JOYPAD_DOWN;
sc->p0_buttons[2] = RETRO_DEVICE_ID_JOYPAD_LEFT;
sc->p0_buttons[3] = RETRO_DEVICE_ID_JOYPAD_RIGHT;
sc->p0_buttons[4] = -1;
}
static void pt_scenario_up_only(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "up_only";
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_UP;
sc->p0_buttons[1] = -1;
}
static void pt_scenario_letters_only(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "letters_only";
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_A;
sc->p0_buttons[1] = RETRO_DEVICE_ID_JOYPAD_B;
sc->p0_buttons[2] = -1;
}
static void pt_scenario_up_and_pause(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "up_and_pause";
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_UP;
sc->p0_buttons[1] = RETRO_DEVICE_ID_JOYPAD_SELECT; /* Pause -> 'P', row0 B0 */
sc->p0_buttons[2] = -1;
}
static void pt_scenario_down_and_a(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "down_and_a";
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_DOWN;
sc->p0_buttons[1] = RETRO_DEVICE_ID_JOYPAD_A;
sc->p0_buttons[2] = -1;
}
static void pt_scenario_teamtap(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "teamtap";
sc->use_teamtap = 1;
/* Socket 0 (the main port-1 pad): Up */
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_UP;
sc->p0_buttons[1] = -1;
/* Socket 1 (frontend port 2): Down */
sc->tap_buttons[0][0] = RETRO_DEVICE_ID_JOYPAD_DOWN;
sc->tap_buttons[0][1] = -1;
/* Socket 2 (frontend port 3): Left */
sc->tap_buttons[1][0] = RETRO_DEVICE_ID_JOYPAD_LEFT;
sc->tap_buttons[1][1] = -1;
/* Socket 3 (frontend port 4): Right */
sc->tap_buttons[2][0] = RETRO_DEVICE_ID_JOYPAD_RIGHT;
sc->tap_buttons[2][1] = -1;
}
static void pt_scenario_6d_rest(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "6d_rest";
sc->use_6d = 1;
sc->p0_buttons[0] = -1;
/* Every axis centred: liveness must NOT trip. Documents false-positive
* trap #3 (see file header) -- padtest should show STDPAD, not
* SIXDPAD, and that is correct, not a bug. */
}
/* Distinct nonzero value per axis so a single screenshot identifies which
* displayed field corresponds to which fed axis, and so the X sign
* (TR V10 p.23's prose/figure contradiction) can be read directly: LEFT
* stick X is pushed RIGHT (positive host value). All buttons pressed too,
* to check bank-0 (A-D) and bank-1 (E/F/G, descending) against
* INPUTDEV_SW_* -- Rezero is fed but padtest's own btns_6d table has no
* glyph for it, so it will NOT appear on screen; that is padtest's
* limitation, not ours (see docs). */
static void pt_scenario_6d_live(pt_scenario *sc)
{
memset(sc, 0, sizeof(*sc));
sc->name = "6d_live";
sc->use_6d = 1;
sc->lx = 20500; /* host RIGHT on left stick -> X (sign under test) */
sc->ly = -10000; /* host UP on left stick -> Y */
sc->rx = 8000; /* host RIGHT on right stick -> TX */
sc->ry = -12000; /* host UP on right stick -> TZ */
sc->sh_r2 = 20000; sc->sh_l2 = 0; /* Z = R2 - L2, real pressure */
sc->sh_r = 15000; sc->sh_l = 0; /* TY = R - L, real pressure */
sc->p0_buttons[0] = RETRO_DEVICE_ID_JOYPAD_A; /* -> SW_A */
sc->p0_buttons[1] = RETRO_DEVICE_ID_JOYPAD_B; /* -> SW_B */
sc->p0_buttons[2] = RETRO_DEVICE_ID_JOYPAD_Y; /* -> SW_C */
sc->p0_buttons[3] = RETRO_DEVICE_ID_JOYPAD_X; /* -> SW_D */
sc->p0_buttons[4] = RETRO_DEVICE_ID_JOYPAD_L3; /* -> SW_E */
sc->p0_buttons[5] = RETRO_DEVICE_ID_JOYPAD_R3; /* -> SW_F */
sc->p0_buttons[6] = RETRO_DEVICE_ID_JOYPAD_START; /* -> SW_G */
sc->p0_buttons[7] = RETRO_DEVICE_ID_JOYPAD_SELECT; /* -> SW_REZERO (not shown by padtest) */
sc->p0_buttons[8] = RETRO_DEVICE_ID_JOYPAD_L2; /* digital companion to sh_r2/l2 */
sc->p0_buttons[9] = RETRO_DEVICE_ID_JOYPAD_R2;
sc->p0_buttons[10] = RETRO_DEVICE_ID_JOYPAD_L;
sc->p0_buttons[11] = RETRO_DEVICE_ID_JOYPAD_R;
sc->p0_buttons[12] = -1;
}
static void pt_scenario_6d_x_negative(pt_scenario *sc)
{
pt_scenario_6d_live(sc);
sc->name = "6d_x_negative";
sc->lx = -20500; /* host LEFT on left stick: the other sign */
}
/* Same axes as 6d_live, but the analog-button reads for L2/R2/L/R are
* forced to 0 -- the exact "frontend with no analog-button support"
* scenario that left Z and TY dead in the merged PR. Only the digital
* JOYPAD_MASK bit says the shoulder is held; update_input()'s fallback
* ("if (sh[i]==0 && (ret&bit)) sh[i]=32767") is the only thing that can
* carry Z/TY across in this run. If Z or TY reads 0 here while 6d_live
* shows them nonzero, the fallback is broken -- a real, reportable
* defect, not a false positive. */
static void pt_scenario_6d_digital_fallback(pt_scenario *sc)
{
pt_scenario_6d_live(sc);
sc->name = "6d_digital_fallback";
sc->digital_fallback = 1;
}
typedef void (*pt_scenario_fn)(pt_scenario *);
static const struct { const char *name; pt_scenario_fn fn; } SCENARIOS[] = {
{ "standard", pt_scenario_standard },
{ "standard_all", pt_scenario_standard_all },
{ "dirs_only", pt_scenario_dirs_only },
{ "up_only", pt_scenario_up_only },
{ "letters_only", pt_scenario_letters_only },
{ "up_and_pause", pt_scenario_up_and_pause },
{ "down_and_a", pt_scenario_down_and_a },
{ "teamtap", pt_scenario_teamtap },
{ "6d_rest", pt_scenario_6d_rest },
{ "6d_live", pt_scenario_6d_live },
{ "6d_x_negative", pt_scenario_6d_x_negative },
{ "6d_digital_fallback", pt_scenario_6d_digital_fallback },
{ NULL, NULL }
};
static int pt_has(const int *list, int id)
{
int i;
for (i = 0; i < PT_MAX_BTN && list[i] >= 0; i++)
if (list[i] == id) return 1;
return 0;
}
static int16_t pt_input_cb(void *ud, unsigned port, unsigned device,
unsigned index, unsigned id)
{
const pt_scenario *sc = (const pt_scenario *)ud;
const int *list = NULL;
if (device == RETRO_DEVICE_JOYPAD)
{
if (port == 0)
list = sc->p0_buttons;
else if (sc->use_teamtap && port == 2)
list = sc->tap_buttons[0];
else if (sc->use_teamtap && port == 3)
list = sc->tap_buttons[1];
else if (sc->use_teamtap && port == 4)
list = sc->tap_buttons[2];
else
list = PT_NONE;
if (id == RETRO_DEVICE_ID_JOYPAD_MASK)
{
int16_t mask = 0;
int i;
for (i = 0; i < PT_MAX_BTN && list[i] >= 0; i++)
mask = (int16_t)(mask | (1 << list[i]));
return mask;
}
return (int16_t)(pt_has(list, (int)id) ? 1 : 0);
}
if (device == RETRO_DEVICE_ANALOG && port == 0 && sc->use_6d)
{
if (index == RETRO_DEVICE_INDEX_ANALOG_LEFT)
{
if (id == RETRO_DEVICE_ID_ANALOG_X) return (int16_t)sc->lx;
if (id == RETRO_DEVICE_ID_ANALOG_Y) return (int16_t)sc->ly;
}
else if (index == RETRO_DEVICE_INDEX_ANALOG_RIGHT)
{
if (id == RETRO_DEVICE_ID_ANALOG_X) return (int16_t)sc->rx;
if (id == RETRO_DEVICE_ID_ANALOG_Y) return (int16_t)sc->ry;
}
else if (index == RETRO_DEVICE_INDEX_ANALOG_BUTTON)
{
if (sc->digital_fallback)
return 0;
if (id == RETRO_DEVICE_ID_JOYPAD_L2) return (int16_t)sc->sh_l2;
if (id == RETRO_DEVICE_ID_JOYPAD_R2) return (int16_t)sc->sh_r2;
if (id == RETRO_DEVICE_ID_JOYPAD_L) return (int16_t)sc->sh_l;
if (id == RETRO_DEVICE_ID_JOYPAD_R) return (int16_t)sc->sh_r;
}
}
return 0;
}
/* -------------------------------------------------------------------
* Screenshot: PPM, same convert-with-sips convention as
* test/tools/cd_visual_verify.c.
* ------------------------------------------------------------------- */
typedef struct {
uint32_t *px;
unsigned w, h;
unsigned cur_frame;
} pt_video;
static void pt_video_cb(void *ud, const void *data, unsigned w, unsigned h,
size_t pitch)
{
pt_video *v = (pt_video *)ud;
unsigned y;
const uint8_t *src = (const uint8_t *)data;
if (!data || !w || !h) return;
free(v->px);
v->px = (uint32_t *)malloc((size_t)w * h * 4);
if (!v->px) return;
for (y = 0; y < h; y++)
memcpy(&v->px[y * w], src + y * pitch, (size_t)w * 4);
v->w = w; v->h = h;
}
static bool pt_frame_cb(void *ud, unsigned frame)
{
pt_video *v = (pt_video *)ud;
v->cur_frame = frame;
return true;
}
static void pt_write_ppm(const pt_video *v, const char *path)
{
FILE *f;
unsigned x, y;
if (!v->px || !v->w || !v->h || !path) return;
f = fopen(path, "wb");
if (!f) { fprintf(stderr, "padtest_probe: cannot write %s\n", path); return; }
fprintf(f, "P6\n%u %u\n255\n", v->w, v->h);
for (y = 0; y < v->h; y++)
for (x = 0; x < v->w; x++)
{
uint32_t p = v->px[y * v->w + x];
unsigned char rgb[3];
rgb[0] = (unsigned char)((p >> 16) & 0xFF);
rgb[1] = (unsigned char)((p >> 8) & 0xFF);
rgb[2] = (unsigned char)(p & 0xFF);
fwrite(rgb, 1, 3, f);
}
fclose(f);
fprintf(stderr, "padtest_probe: wrote %s (%ux%u)\n", path, v->w, v->h);
}
static void pt_dump_scenario(const pt_scenario *sc)
{
int i;
printf("padtest_probe: scenario=%s\n", sc->name);
printf(" p0_buttons:");
for (i = 0; i < PT_MAX_BTN && sc->p0_buttons[i] >= 0; i++)
printf(" %d", sc->p0_buttons[i]);
printf("\n");
if (sc->use_teamtap)
{
int s;
for (s = 0; s < 3; s++)
{
printf(" tap_socket[%d]_buttons:", s + 1);
for (i = 0; i < PT_MAX_BTN && sc->tap_buttons[s][i] >= 0; i++)
printf(" %d", sc->tap_buttons[s][i]);
printf("\n");
}
}
if (sc->use_6d)
{
printf(" lx=%d ly=%d rx=%d ry=%d\n", sc->lx, sc->ly, sc->rx, sc->ry);
printf(" sh_l2=%d sh_r2=%d sh_l=%d sh_r=%d digital_fallback=%d\n",
sc->sh_l2, sc->sh_r2, sc->sh_l, sc->sh_r, sc->digital_fallback);
}
}
int main(int argc, char **argv)
{
harness_config cfg = HARNESS_CONFIG_DEFAULT;
pt_video video;
const char *scenario_name = NULL;
const char *out_path = "/tmp/padtest_probe.ppm";
int i;
memset(&video, 0, sizeof(video));
for (i = 1; i < argc; i++)
{
if (strcmp(argv[i], "--scenario") == 0 && i + 1 < argc)
scenario_name = argv[++i];
else if (strcmp(argv[i], "--out") == 0 && i + 1 < argc)
out_path = argv[++i];
}
if (!scenario_name)
{
fprintf(stderr,
"usage: padtest_probe <core> <padtest.rom> --scenario NAME "
"[--out FILE.ppm] [--option K=V ...] [--frames N]\n"
"scenarios:");
for (i = 0; SCENARIOS[i].name; i++)
fprintf(stderr, " %s", SCENARIOS[i].name);
fprintf(stderr, "\n");
return 1;
}
for (i = 0; SCENARIOS[i].name; i++)
if (strcmp(SCENARIOS[i].name, scenario_name) == 0) break;
if (!SCENARIOS[i].name)
{
fprintf(stderr, "padtest_probe: unknown scenario '%s'\n", scenario_name);
return 1;
}
SCENARIOS[i].fn(&g_sc);
pt_dump_scenario(&g_sc);
cfg.frames = 120;
cfg.video_callback = pt_video_cb;
cfg.video_callback_data = &video;
cfg.frame_callback = pt_frame_cb;
cfg.frame_callback_data = &video;
cfg.input_callback = pt_input_cb;
cfg.input_callback_data = &g_sc;
if (!harness_init_from_args(&cfg, argc, argv)) return 1;
if (!cfg.rom_path)
{
fprintf(stderr, "padtest_probe: missing ROM path\n");
return 1;
}
if (!harness_load_rom(&cfg)) return 1;
harness_run(&cfg);
/* Ground-truth check, independent of padtest's own rendering: read the
* core's OWN joypad0Buttons[] array directly. BUTTON_U=0, D=1, L=2,
* R=3, A=16, B=17, C=18, PAUSE=19, OPTION=20 (src/jerry/joystick.h). */
{
uint8_t *jb = (uint8_t *)harness_dlsym(&cfg, "joypad0Buttons");
if (jb)
printf("padtest_probe: joypad0Buttons[U,D,L,R,A,B,C,PAUSE,OPTION] "
"= %u %u %u %u %u %u %u %u %u\n",
jb[0], jb[1], jb[2], jb[3], jb[16], jb[17], jb[18], jb[19], jb[20]);
else
fprintf(stderr, "padtest_probe: joypad0Buttons not exported -- "
"build the core with TEST_EXPORTS=1\n");
}
if (g_sc.use_6d)
{
typedef int (*get_type_fn)(int);
typedef int (*any_attached_fn)(void);
get_type_fn get_type = (get_type_fn)harness_dlsym(&cfg, "InputDevGetType");
any_attached_fn any_attached =
(any_attached_fn)harness_dlsym(&cfg, "InputDevAnyAttached");
if (get_type)
printf("padtest_probe: InputDevGetType(0)=%d "
"(INPUTDEV_6D is the last enum value; see inputdev.h)\n",
get_type(0));
if (any_attached)
printf("padtest_probe: InputDevAnyAttached()=%d\n", any_attached());
}
if (g_sc.use_teamtap)
{
typedef bool (*get_tap_fn)(int);
get_tap_fn get_tap = (get_tap_fn)harness_dlsym(&cfg, "JoystickGetTeamTap");
uint8_t *jb = (uint8_t *)harness_dlsym(&cfg, "joypad0Buttons");
if (get_tap)
printf("padtest_probe: after run, JoystickGetTeamTap(0)=%d "
"JoystickGetTeamTap(1)=%d\n", get_tap(0), get_tap(1));
if (jb)
{
int s;
printf("padtest_probe: joypad0Buttons ground truth "
"(U,D,L,R,...,A,B,C @ socket*21):\n");
for (s = 0; s < 4; s++)
printf(" socket%d: U=%u D=%u L=%u R=%u A=%u B=%u C=%u\n",
s, jb[s*21+0], jb[s*21+1], jb[s*21+2], jb[s*21+3],
jb[s*21+16], jb[s*21+17], jb[s*21+18]);
}
}
pt_write_ppm(&video, out_path);
free(video.px);
harness_shutdown(&cfg);
return 0;
}