diff --git a/.gitignore b/.gitignore index f5baa3ad..aa112cee 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 5d9ed7fa..ad5bc947 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/docs/input-devices-user-guide.md b/docs/input-devices-user-guide.md index b0bfade5..e3523811 100644 --- a/docs/input-devices-user-guide.md +++ b/docs/input-devices-user-guide.md @@ -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 diff --git a/docs/lightgun-design.md b/docs/lightgun-design.md index 2d000d64..f5349428 100644 --- a/docs/lightgun-design.md +++ b/docs/lightgun-design.md @@ -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. diff --git a/test/tools/teamtap_ports.c b/test/tools/teamtap_ports.c new file mode 100644 index 00000000..1b5a022a --- /dev/null +++ b/test/tools/teamtap_ports.c @@ -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 +#include +#include +#include +#include + +#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; +}