test(input): cover the two Team Tap paths joymatrix_identity cannot reach

joymatrix_identity proves the DECODE by writing joypad0Buttons[] directly
and running zero frames -- which is what makes its digests reproducible,
and exactly why it never executes either path below.

1. update_teamtap_input()'s frontend-port -> socket fill.  teamtap_user[][],
   the socket loop and teamtap_map[] had no coverage at all, so "the decode
   works" and "the feature works for a user" were different claims with
   evidence behind only the first.

2. The v13 savestate chunk.  It is the LAST thing retro_serialize writes,
   so nothing after it can be misaligned -- a JoystickTeamTapStateSave()
   that wrote zero bytes passed test_state_compat, test_runahead_determinism
   and state_tail_is_zero_padded unchanged, because all three run with no
   tap and all-zero sockets.  A named requirement with an argument instead
   of a measurement.

Both negative-controlled before committing: stubbing the chunk to return 0
fails teamtap_state_roundtrip with "63 wrong", and pointing teamtap_user[][]
at a port nothing is bound to fails both fill assertions.  Runs on the
committed yarc.j64, so it is part of `make test`.

Also: teamtap_rom_probe and teamtap_ports are added to the Makefile's tool
aggregates (`make clean` was leaving the binaries behind), and
docs/lightgun-design.md's summary line still listed Team Tap as "still
open" after the body and the Q3 row had been corrected.  One more caveat in
the user guide: the core advertises and reads eight ports, but a frontend
that leaves the new ones at "None" reports no input for them, so the extra
pads have to be assigned in Controls.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Opus 5 committed 2026-08-20 19:14:30 -04:00
1 parent d909f88c55
commit 79e061024d
5 files changed
+277 -2

No files matched your search

+1
View File
@@ -66,6 +66,7 @@ test/tools/perf_iface_witness
test/tools/netlink_game
test/tools/joymatrix_identity
test/tools/teamtap_rom_probe
test/tools/teamtap_ports
test/tools/mouse_decode_test
test/tools/rotary_decode_test
test/tools/analog_decode_test
+22 -1
View File
@@ -1003,7 +1003,8 @@ clean:
test/test_biosdb test/test_cart_bios_loader \
test/test_titledb test/test_titlehook test/tools/test_hook_gate \
test/tools/test_wedge_spin test/tools/test_texdump test/tools/test_texreplace test/tools/i2s_lag_probe \
test/tools/joymatrix_identity test/tools/mouse_decode_test \
test/tools/joymatrix_identity test/tools/teamtap_ports \
test/tools/teamtap_rom_probe test/tools/mouse_decode_test \
test/tools/rotary_decode_test test/tools/analog_decode_test \
test/tools/paddle_decode_test \
test/tools/tuning_identity test/tools/test_lightgun \
@@ -1074,6 +1075,7 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
test/tools/netlink_pair test/tools/netlink_latency test/tools/netlink_delay_proxy test/tools/netlink_discover_probe test/tools/netlink_rebuild_witness test/tools/netlink_mismatch_witness test/tools/perf_iface_witness test/tools/voicemodem_pair test/tools/netlink_game test/tools/test_pertitle_db \
test/tools/test_hook_gate \
test/tools/i2s_lag_probe test/tools/joymatrix_identity \
test/tools/teamtap_ports \
test/test_quadrature test/test_axistune test/tools/mouse_decode_test \
test/tools/rotary_decode_test test/tools/analog_decode_test \
test/tools/paddle_decode_test \
@@ -1426,6 +1428,12 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
@# an FNV digest measured on develop. Committed before any device code
@# so later PRs are measured against a number that predates them.
./test/tools/joymatrix_identity ./$(TARGET) test/roms/yarc.j64 --quiet
@# Team Tap (#513): the two paths joymatrix_identity structurally
@# cannot reach -- update_teamtap_input()'s frontend-port -> socket
@# fill, and the trailing v13 state chunk. That chunk is written
@# LAST, so one that writes nothing at all misaligns nothing and
@# passes test_state_compat and test_runahead_determinism unchanged.
./test/tools/teamtap_ports ./$(TARGET) test/roms/yarc.j64 --quiet
@# Cross-load static leak (#479): a fast-blitter session must not
@# contaminate the savestate of the accurate session that follows it
@# while the core stays resident (iOS cannot dlclose). The run1-vs-run2
@@ -1925,6 +1933,19 @@ test/tools/teamtap_rom_probe: test/tools/teamtap_rom_probe.c \
test/harness/harness.c \
$(if $(filter Linux,$(shell uname -s)),-ldl) -lm
# Team Tap frontend plumbing + savestate chunk (#513). Covers the two
# paths joymatrix_identity structurally cannot: update_teamtap_input()'s
# frontend-port -> socket fill, and the trailing v13 state chunk (which is
# written last, so an empty one misaligns nothing and passes every other
# state test). Runs on the committed yarc.j64, so it IS part of `make test`.
test/tools/teamtap_ports: test/tools/teamtap_ports.c \
src/jerry/joystick.h src/core/state.h \
test/harness/harness.c test/harness/harness.h
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
-o $@ test/tools/teamtap_ports.c \
test/harness/harness.c \
$(if $(filter Linux,$(shell uname -s)),-ldl) -lm
# Cross-load static-state leak gate (#479). Pins the core with an extra
# dlopen ref (the iOS no-dlclose regime) and proves an accurate-blitter
# session run AFTER a fast-blitter session serialises byte-identical
+7
View File
@@ -357,6 +357,13 @@ extra sockets are appended after them:
ports 1, 3, 4 and 5** — not 1 to 4. Frontend port 2 is still Jaguar port 2,
where a fifth pad would go.
**If the extra pads do nothing, check that your frontend has actually
assigned those ports.** The core advertises eight ports and reads all of
them, but a frontend that leaves ports it has never been told about set to
*None* reports no input for them. In RetroArch that is *Settings → Input →
Port N Controls → Device Type = Standard Joypad*, plus binding a physical
controller to the port.
### Remapping the extra pads
*Port N > Button Remapping* and *Numpad to Keyboard* apply to **socket 0
+5 -1
View File
@@ -104,7 +104,11 @@ Nothing else in this core reads `LPH`, so there is no second consumer to keep co
| 2 | Yes — but see Deviation 3: calibration converging proves less than §6 assumed. |
| 6 | Resolved: `BALLOONS.BIN`, 15904 bytes, md5 `cda44eeb071b5bd29582665ce8405eaa`. |
Still open: §5.3 (Team Tap), §5.4 (gun controller-ID bits), §5.5 (building Bullets).
Still open: §5.4 (gun controller-ID bits), §5.5 (building Bullets).
§5.3 (Team Tap) was **answered by #513's TR10 pass, and the answer was the opposite of
what this document guessed** — `LP` reaches TOM through the adapter but is not isolated
from the other sockets' `B0`, so the gun is not immune to a Team Tap. See §4.2 item 2 and
the §5 table.
Bullets remains untestable, so which button *it* reads is unverified; if it turns out to
want `A`, that is `AUX_A` today and a mapping question, not a design one.
+242
View File
@@ -0,0 +1,242 @@
/*
* test/tools/teamtap_ports.c -- Team Tap frontend plumbing + savestate (#513).
*
* WHAT THIS COVERS THAT joymatrix_identity DOES NOT
* =================================================
* joymatrix_identity proves the DECODE: the right row codes reach the right
* sockets, the detect bit answers both ways, and nothing moves with no tap
* selected. It does that by writing joypad0Buttons[] directly and running
* zero frames, which is exactly what makes its digests reproducible -- and
* exactly why it never executes either of the two paths below.
*
* 1. THE FRONTEND -> SOCKET FILL. update_teamtap_input() in libretro.c maps
* frontend ports 3-5 to Jaguar port 1's tap sockets 1-3, and ports 6-8 to
* Jaguar port 2's. Nothing else exercises teamtap_user[][], the socket
* loop, or teamtap_map[]. Without this, "the decode works" and "the
* feature works for a user" are different claims and only the first has
* evidence.
*
* 2. THE SAVESTATE CHUNK. JoystickTeamTapStateSave() is the LAST thing
* retro_serialize writes, so nothing after it can be misaligned by a
* chunk that writes zero bytes -- test_state_compat and
* test_runahead_determinism both pass with a save/load that carries
* nothing at all, because both run with no tap and all-zero sockets.
* Requirement: what a pad behind the adapter holds is machine-visible at
* $F14000 the instant a title selects its row code, so it has to survive
* a state round trip or run-ahead and netplay diverge (cf. #400, #479).
*
* Both run against the committed test/roms/yarc.j64, so this is a CI test,
* unlike test/tools/teamtap_rom_probe (private corpus).
*
* USAGE
* make TEST_EXPORTS=1 test/tools/teamtap_ports
* ./test/tools/teamtap_ports ./virtualjaguar_libretro.dylib test/roms/yarc.j64
*
* Exit codes: 0 = pass, 1 = fail, 2 = harness error.
*/
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "../harness/harness.h"
#include "../../src/jerry/joystick.h"
#include "state.h"
#include "libretro.h"
#define TAP_TEST_FRAMES 40
/* The scripted press has to still be held on the LAST frame: update_input()
* clears every slot at the top of each frame and refills from the current
* frame's input, so what the arrays hold after harness_run() is the final
* frame's state and nothing earlier. */
#define PRESS_FIRST 1
#define PRESS_HOLD (TAP_TEST_FRAMES + 8)
typedef bool (*serialize_fn)(void *data, size_t size);
typedef bool (*unserialize_fn)(const void *data, size_t size);
static harness_result results[8];
static char details[8][192];
static unsigned num_results;
static void check(int ok, const char *name, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsnprintf(details[num_results], sizeof(details[0]), fmt, ap);
va_end(ap);
results[num_results].status = ok ? "PASS" : "FAIL";
results[num_results].name = name;
results[num_results].detail = details[num_results];
num_results++;
}
int main(int argc, char **argv)
{
harness_config cfg = HARNESS_CONFIG_DEFAULT;
uint8_t *p0, *p1;
void *state;
serialize_fn ser;
unserialize_fn unser;
unsigned s1_up, s2_up, s3_up, p2s1_up, s1_a;
int all_ok;
unsigned i;
cfg.frames = TAP_TEST_FRAMES;
if (!harness_init_from_args(&cfg, argc, argv))
return 2;
if (!cfg.rom_path)
cfg.rom_path = "test/roms/yarc.j64";
/* Taps on BOTH ports, so the port-2 half of teamtap_user[][] is
* exercised too -- a transposed row there would otherwise only show up
* as "player 6 does nothing", in a frontend, months later. */
harness_set_option(&cfg, "virtualjaguar_p1_device", "teamtap");
harness_set_option(&cfg, "virtualjaguar_p2_device", "teamtap");
/* One distinct button per socket, so a fill that lands in the wrong
* socket cannot be mistaken for a pass. */
harness_press(&cfg, 2, RETRO_DEVICE_ID_JOYPAD_UP, PRESS_FIRST, PRESS_HOLD);
harness_press(&cfg, 3, RETRO_DEVICE_ID_JOYPAD_DOWN, PRESS_FIRST, PRESS_HOLD);
harness_press(&cfg, 4, RETRO_DEVICE_ID_JOYPAD_A, PRESS_FIRST, PRESS_HOLD);
harness_press(&cfg, 5, RETRO_DEVICE_ID_JOYPAD_B, PRESS_FIRST, PRESS_HOLD);
if (!harness_load_rom(&cfg))
{
harness_shutdown(&cfg);
return 2;
}
p0 = (uint8_t *)harness_dlsym(&cfg, "joypad0Buttons");
p1 = (uint8_t *)harness_dlsym(&cfg, "joypad1Buttons");
ser = (serialize_fn)harness_dlsym(&cfg, "retro_serialize");
unser = (unserialize_fn)harness_dlsym(&cfg, "retro_unserialize");
if (!p0 || !p1 || !ser || !unser)
{
fprintf(stderr,
"teamtap_ports: missing test-ABI symbols (joypad0Buttons / "
"joypad1Buttons / retro_serialize / retro_unserialize). "
"Build with TEST_EXPORTS=1.\n");
harness_shutdown(&cfg);
return 2;
}
harness_run(&cfg);
/* ---- 1. frontend port -> tap socket ------------------------------ */
s1_up = p0[1 * JOYPAD_SOCKET_SLOTS + BUTTON_U];
s2_up = p0[2 * JOYPAD_SOCKET_SLOTS + BUTTON_D];
s3_up = p0[3 * JOYPAD_SOCKET_SLOTS + BUTTON_A];
p2s1_up = p1[1 * JOYPAD_SOCKET_SLOTS + BUTTON_B];
check(s1_up && s2_up && s3_up,
"teamtap_port_to_socket_p1",
"frontend ports 3/4/5 -> Jaguar port 1 sockets 1/2/3: "
"Up=%u Down=%u A=%u (all must be non-zero)",
s1_up, s2_up, s3_up);
check(p2s1_up != 0,
"teamtap_port_to_socket_p2",
"frontend port 6 -> Jaguar port 2 socket 1: B=%u (must be non-zero)",
p2s1_up);
/* Socket 0 must NOT have picked any of them up. A fill that ignored
* the socket index would set socket 0 and satisfy nothing else here --
* frontend ports 3-6 are not port 1's own pad. */
check(p0[BUTTON_U] == 0 && p0[BUTTON_D] == 0 && p0[BUTTON_A] == 0
&& p1[BUTTON_B] == 0,
"teamtap_socket0_untouched",
"socket 0 of both ports saw none of ports 3-6's presses "
"(p0 Up=%u Down=%u A=%u, p1 B=%u)",
p0[BUTTON_U], p0[BUTTON_D], p0[BUTTON_A], p1[BUTTON_B]);
/* ---- 2. savestate round trip ------------------------------------- */
state = malloc(STATE_SIZE);
if (!state)
{
harness_shutdown(&cfg);
return 2;
}
/* Seed a distinctive pattern across every tap slot of both ports, so a
* chunk that saves the wrong range (or nothing) cannot survive by
* accident. Socket 0 is left as the run left it. */
for (i = JOYPAD_SOCKET_SLOTS; i < JOYPAD_BUTTON_SLOTS; i++)
{
p0[i] = (uint8_t)(0x40 + (i & 0x3F));
p1[i] = (uint8_t)(0x80 + (i & 0x3F));
}
s1_a = p0[1 * JOYPAD_SOCKET_SLOTS + BUTTON_A];
if (!ser(state, STATE_SIZE))
{
fprintf(stderr, "teamtap_ports: retro_serialize failed\n");
free(state);
harness_shutdown(&cfg);
return 2;
}
memset(p0 + JOYPAD_SOCKET_SLOTS, 0,
JOYPAD_BUTTON_SLOTS - JOYPAD_SOCKET_SLOTS);
memset(p1 + JOYPAD_SOCKET_SLOTS, 0,
JOYPAD_BUTTON_SLOTS - JOYPAD_SOCKET_SLOTS);
if (!unser(state, STATE_SIZE))
{
fprintf(stderr, "teamtap_ports: retro_unserialize failed\n");
free(state);
harness_shutdown(&cfg);
return 2;
}
{
int restored = 1;
unsigned bad = 0;
for (i = JOYPAD_SOCKET_SLOTS; i < JOYPAD_BUTTON_SLOTS; i++)
{
if (p0[i] != (uint8_t)(0x40 + (i & 0x3F))
|| p1[i] != (uint8_t)(0x80 + (i & 0x3F)))
{
restored = 0;
bad++;
}
}
check(restored,
"teamtap_state_roundtrip",
"all %d tap slots per port survive serialize/unserialize "
"(%u wrong; socket 1 A read back 0x%02X, seeded 0x%02X)",
(int)(JOYPAD_BUTTON_SLOTS - JOYPAD_SOCKET_SLOTS), bad,
p0[1 * JOYPAD_SOCKET_SLOTS + BUTTON_A], s1_a);
}
/* Socket 0 has to come back too -- the mid-blob joystick chunk and the
* trailing tap chunk are separate writes, and a bug that swapped their
* ranges would restore the tap sockets while corrupting socket 0. */
check(p0[JOYPAD_SOCKET_SLOTS - 1] != 0xFF,
"teamtap_socket0_chunk_intact",
"socket 0's last slot after the round trip: 0x%02X "
"(the two chunks did not overlap)",
p0[JOYPAD_SOCKET_SLOTS - 1]);
free(state);
all_ok = 1;
for (i = 0; i < num_results; i++)
if (strcmp(results[i].status, "PASS") != 0)
all_ok = 0;
harness_report(&cfg, results, num_results);
harness_shutdown(&cfg);
return all_ok ? 0 : 1;
}