Files
Joseph Mattiello 443595d8aa fix(input): PR #449 review fixes -- frontend detach, idle assertion, row gate, live guardrail
Addresses the four review lenses on #449.  No change to the wiring
tables, to STATE_VERSION, or to EXPECT_DIGEST_FULL / EXPECT_DIGEST_PORT1
(both re-measured bit-identical).

BLOCKING

A  retro_set_controller_port_device forced INPUTDEV_PAD on any JOYPAD or
   NONE assignment, with a comment claiming "the core option takes over
   again on the next check_variables()".  Nothing schedules one --
   check_variables() runs from retro_load_game and from retro_run only
   behind GET_VARIABLE_UPDATE -- and RetroArch issues a per-port
   controller init right after load, so the mouse the core option had
   just attached was detached for the whole session.  This is why the
   feature was never seen working in a frontend.  The option resolution
   is now p2_device_from_option() and is re-run immediately when the
   frontend releases its claim.  Called that early TitleDBOverride
   returns NULL, so the "auto -> per-title DB" leg resolves at the
   retro_load_game check_variables(), not on the early call.

A2 update_input suppressed port 2's RetroPad unconditionally on the
   configured device type, before reading any mouse state, so a frontend
   that does not route mouse state to the port lost BOTH devices.  Rule
   adopted and commented: selecting a mouse must never leave the port
   with no working input.  Mouse state is read first; the pad is released
   only once the mouse is live (non-zero delta or button), and stays
   released until the device type changes.

B  joymatrix_identity's third assertion compared mouse_port1 only --
   mouse_full was formatted into the detail string and never checked.
   Adds EXPECT_DIGEST_FULL_WITH_MOUSE (0x5E1858EA, measured on this
   branch after D and E) as a fourth assertion, moves the InputDev*
   sweep ahead of the --print early return so the constant can be
   re-measured at all, and puts all four results in the exit status (the
   old code folded the mouse result into ok_port1; the new rows would
   have printed FAIL and still exited 0).  The stale "draining throughout
   the sweep" comment is corrected: the feed clamps to QUAD_MAX_BACKLOG,
   so the encoder drains over the first 64 qualifying polls of ~131,072
   and then holds -- which is what catches the polarity class.

C  Missing InputDev* symbols now exit 77 after harness_shutdown instead
   of printing a note and returning 0.  make invokes the tool bare, so 77
   fails the build: loud, never a silent pass.

STRONG

D  QuadReset parked at phase 0 = (A=0,B=0); the overlay is active low, so
   an idle mouse asserted all four direction lines -- row 0 reading
   U+D+L+R held, rows 1-3 four keypad digits -- after every JERRYReset,
   device change and pre-v12 state load.  Parks at QUAD_REST_PHASE = 2 =
   (1,1), the only rest position where an idle mouse is
   indistinguishable from an idle pad.  No source names a rest phase; the
   comment says so.

E  The emission clock fired on any $F14000 offset-0 read.  Now gated on
   port 2's row select (offset1 != 0xFF -- any port-2 row, not row 0),
   per mapping doc section 5: a title polling port 1 rapidly was
   advancing the port-2 encoder between the port-2 poller's own samples,
   which loses motion outright rather than merely lagging.  The overlay
   is untouched and still asserts in every row.

F  InputDevSetType reset the encoders and button latches but not
   inputdev_armed, so mouse -> pad -> mouse left a stale arm worth one
   unrequested advance.  Cleared on the type-change path only.

G  InputDevStateLoad sanitized phase but loaded backlog and frac raw;
   QuadFeed clamps only after `backlog += states`, so a corrupt state
   could overflow first.  Both clamped on load.

H  headroom_logged was a function-local static in retro_serialize and so
   unreachable from retro_deinit.  Hoisted to file scope and added to the
   reset block (iOS cannot dlclose a core).

I  The mouse sensitivity percent is clamped to 1600 (InputDevSetScale's
   4096 ceiling as a percent) before the *256 multiply, so a hand-edited
   config value cannot overflow a signed int.

J  The joymatrix_identity make rule now lists src/jerry/inputdev.h as a
   prerequisite -- the tool includes it for InputDevType, and an enum
   renumber was leaving a stale binary testing wrong constants.

TESTS

test_quadrature asserts the rest phase and that it drives both lines
high.  mouse_decode_test gains test_port1_poll_does_not_clock: 200
port-1 row polls must not advance the port-2 encoder and must not lose
the queued motion.  Both assertions fail with E reverted.
2026-08-15 00:06:07 -04:00

252 lines
6.4 KiB
C

/*
* test/test_quadrature.c -- unit test for the Gray-code quadrature encoder.
*
* Standalone: compiles src/jerry/quadrature.c directly, no core, no
* dlopen. Asserts the properties the mouse (#429) and the rotary (#436)
* both depend on:
*
* 1. the Gray sequence, forwards and backwards, against the table in
* quadrature.h (which is docs/jaguar-mouse-adapter-mapping.md
* section 5 and the Technical Reference V10's rotary sequence -- the
* two agree bit-for-bit);
* 2. QuadAdvance() moves AT MOST one state, ever. This is the whole
* rate policy: a two-state jump indexes the "undetermined" entry of
* a real decoder's table and is silently discarded, so emitting one
* is worse than emitting nothing;
* 3. the backlog clamp;
* 4. the Q8 remainder carry, so a <1.0 sensitivity decimates smoothly
* instead of quantising every small movement to zero.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../src/jerry/quadrature.h"
static int failures;
static void check(int cond, const char *what)
{
if (cond)
printf(" ok %s\n", what);
else
{
printf(" FAIL %s\n", what);
failures++;
}
}
/* Reference phase table, written out independently of the implementation
* so a change to either side has to be deliberate. */
static const int ref_a[4] = { 0, 1, 1, 0 };
static const int ref_b[4] = { 0, 0, 1, 1 };
static void test_sequence_forward(void)
{
quad_axis q;
int i;
int ok = 1;
printf("Gray sequence, positive direction (A leads B)\n");
QuadReset(&q);
QuadFeed(&q, 8, 256);
for (i = 0; i < 8; i++)
{
int a, b;
int expect = (QUAD_REST_PHASE + i + 1) & 3;
if (!QuadAdvance(&q))
{
ok = 0;
break;
}
QuadLevels(&q, &a, &b);
if (a != ref_a[expect] || b != ref_b[expect])
ok = 0;
}
check(ok, "8 advances walk the Gray table upward from the rest phase");
check(q.backlog == 0, "backlog fully drained after 8 advances");
check(QuadAdvance(&q) == 0, "an empty backlog does not advance");
}
static void test_sequence_reverse(void)
{
quad_axis q;
int i;
int ok = 1;
printf("Gray sequence, negative direction (B leads A)\n");
QuadReset(&q);
QuadFeed(&q, -8, 256);
for (i = 0; i < 8; i++)
{
int a, b;
int expect = (QUAD_REST_PHASE - (i + 1)) & 3; /* downward */
if (!QuadAdvance(&q))
{
ok = 0;
break;
}
QuadLevels(&q, &a, &b);
if (a != ref_a[expect] || b != ref_b[expect])
ok = 0;
}
check(ok, "8 advances walk the Gray table downward from the rest phase");
check(q.backlog == 0, "backlog fully drained after 8 advances");
}
static void test_one_state_per_advance(void)
{
quad_axis q;
int i;
int ok = 1;
printf("one state per advance (the rate policy)\n");
/* Motion arriving faster than it can drain: 100 units fed per poll,
* one poll per iteration. Every advance must still move exactly one
* state -- this is the case where a naive implementation would "catch
* up" by jumping, which a real decoder discards outright. */
QuadReset(&q);
for (i = 0; i < 200; i++)
{
uint8_t before;
int step;
QuadFeed(&q, 100, 256);
before = q.phase;
QuadAdvance(&q);
step = ((int)q.phase - (int)before + 4) & 3;
if (step != 1)
ok = 0;
}
check(ok, "200 saturated polls each move exactly 1 state (never a jump)");
/* And when the backlog IS empty, an advance is a no-op rather than a
* wrap: a decoder reads "no movement", which is harmless. */
ok = 1;
QuadReset(&q);
QuadFeed(&q, 3, 256);
for (i = 0; i < 10; i++)
{
uint8_t before = q.phase;
int step;
QuadAdvance(&q);
step = ((int)q.phase - (int)before + 4) & 3;
if (step > 1)
ok = 0;
if (i >= 3 && step != 0)
ok = 0;
}
check(ok, "advances past an empty backlog hold the phase steady");
}
static void test_backlog_clamp(void)
{
quad_axis q;
printf("backlog clamp\n");
QuadReset(&q);
QuadFeed(&q, 100000, 256);
check(q.backlog == QUAD_MAX_BACKLOG, "positive backlog clamps at +64");
QuadReset(&q);
QuadFeed(&q, -100000, 256);
check(q.backlog == -QUAD_MAX_BACKLOG, "negative backlog clamps at -64");
}
static void test_q8_carry(void)
{
quad_axis q;
int i;
int advanced = 0;
printf("Q8 sensitivity carry\n");
/* 25% sensitivity: four one-unit feeds must produce exactly one state,
* not zero (which is what dropping the remainder would give). */
QuadReset(&q);
for (i = 0; i < 4; i++)
QuadFeed(&q, 1, 64);
check(q.backlog == 1, "4 x (1 unit @ 25%) accumulates exactly 1 state");
QuadReset(&q);
for (i = 0; i < 4; i++)
QuadFeed(&q, -1, 64);
check(q.backlog == -1, "4 x (-1 unit @ 25%) accumulates exactly -1 state");
/* 200% doubles. */
QuadReset(&q);
QuadFeed(&q, 3, 512);
check(q.backlog == 6, "3 units @ 200% is 6 states");
/* A single sub-unit feed emits nothing yet but is not lost. */
QuadReset(&q);
QuadFeed(&q, 1, 64);
check(q.backlog == 0, "1 unit @ 25% emits nothing yet");
for (i = 0; i < 3; i++)
QuadFeed(&q, 1, 64);
while (QuadAdvance(&q))
advanced++;
check(advanced == 1, "...and the carried remainder completes the state");
}
static void test_reset(void)
{
quad_axis q;
printf("reset\n");
QuadReset(&q);
QuadFeed(&q, 7, 256);
QuadAdvance(&q);
QuadFeed(&q, 1, 64);
QuadReset(&q);
check(q.backlog == 0 && q.frac == 0 && q.phase == QUAD_REST_PHASE,
"QuadReset zeroes backlog and carry and parks at the rest phase");
/* The rest phase must be the (1,1) state: the mouse overlay is active
* low, so any other parking position leaves an IDLE mouse asserting
* direction lines at $F14000 -- see QUAD_REST_PHASE in quadrature.h. */
{
int a = -1, b = -1;
QuadLevels(&q, &a, &b);
check(a == 1 && b == 1,
"the rest phase drives both lines high (idle asserts nothing)");
}
}
int main(void)
{
printf("=== quadrature encoder ===\n");
test_sequence_forward();
test_sequence_reverse();
test_one_state_per_advance();
test_backlog_clamp();
test_q8_carry();
test_reset();
if (failures)
{
printf("FAILED: %d check(s)\n", failures);
return 1;
}
printf("All quadrature checks passed\n");
return 0;
}