mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 12:47:58 -10:00
Third device in the #428 input epic, on the scaffolding the mouse (#429), rotary (#436) and shared per-axis tuning (#439/#474) established. HARDWARE RESEARCH (Jaguar Technical Reference V10, sections "Analogue Joystick and 'Driving' Controllers", "Identifying Controller Types", "Reading Bank Switching Controllers"): - There is NO console-side ADC to model. TR10: "Early versions of the Jaguar included an 8 bit ADC on the motherboard. This has been deleted -- analogue controllers now require their own ADC chip." The controller carries its own microcontroller (Atari's prototype used a 68HC05P9) and answers the ordinary $F14000/$F14002 row scan with data bits. Like the rotary, this is row-select-driven matrix synthesis, not new bus plumbing. - Two banks, switched on the row-3 -> row-0 transition of the device's OWN socket -- i.e. clocked by row-select WRITES, not reads -- with interleaved reads of other sockets explicitly required not to desynchronise it. Bank 0 carries X/Y as four nibbles and buttons A-D on the B column; bank 1 carries the hat / gear shift in row 0 and the all-ones type ID elsewhere. - Identification: C2 C3 = 0 1 ("Bank Switching"), row-0 C column = the bank-0 flag, last-bank B column 1111 = "Analogue Joystick or Driving Controller". The C-column row assignment differs by port, per TR10's port-headed pinout table (the same reading the rotary already uses). - "Analog" and "driving" are ONE wire protocol with two interpretations; the two types differ only in host mapping. NO RELEASED TITLE READS IT. Atari never shipped the controller; BigPEmu's Checkered Flag analog support is a game patch, and JANALOG.ABS targets the deleted early-board ADC. Per #437's research-first mandate the deliverable is therefore the device type plus a documented synthetic verification: test/tools/analog_decode_test.c drives a TR10-faithful scan, written independently of the emulation, and is labelled as such in its header and in the user guide. ADC-Reg and the head tracker are recorded as out of scope-because-unsourced rather than left unasked. ABSOLUTE-AXIS TUNING (answers the question axistune.h deferred to the first absolute consumer): the dead zone RE-BASES rather than gates. On a position nothing integrates, so the drift argument that forbade re-basing on a relative axis does not apply, while a gate would put a steering-snap step at the edge. The identity rule inverts too: on a relative axis raw == 0 means "no sample" and must stay 0 (#474); on an absolute axis it IS a sample, so dead zone and exponent fix the centre and OFFSET ALONE may move it. The exponent anchors at full deflection, so it costs no range -- hence no sensitivity option. Implemented as AxisTuneApplyAbs() in the same shared layer; there is no second tuning path. Opt-in throughout: default stays the pad, a plain RETRO_DEVICE_ANALOG maps to the pad (so "RetroPad w/ Analog" users are never silently switched), and the port keeps its RetroPad -- with the device wholly inert, probes included -- until the stick proves live. Savestate: bank, last row, latched ADC bytes and switch mask are machine-visible and join the input-device chunk (41 -> 51 bytes). v12 is develop-only, so the layout is extended in place per the one-bump-per- release policy; a pre-extension state reads the inert zero-fill tail. joymatrix_identity digests unmoved (0xC24DDCDE / 0xD0CD02E2 / 0x5E1858EA); mouse, rotary and tuning_identity all still pass. make -j8 TEST_EXPORTS=1 test exits 0. Closes #437. Co-Authored-By: Claude Opus 5 <[email protected]>
843 lines
30 KiB
C
843 lines
30 KiB
C
/*
|
|
* src/jerry/inputdev.c -- per-port non-pad input device model.
|
|
*
|
|
* See inputdev.h for the row-blindness argument, the port scope and the
|
|
* emission rule. Bit assignments below come from
|
|
* docs/jaguar-mouse-adapter-mapping.md section 4d and are NOT guesses.
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include "inputdev.h"
|
|
#include "quadrature.h"
|
|
#include "axistune.h"
|
|
#include "joystick.h"
|
|
#include "state.h"
|
|
|
|
/* Wiring table. Values are $F14000 bit positions for PORT 2, all active
|
|
* low (0 = asserted). Written as a literal table so the three cases
|
|
* cannot drift apart.
|
|
*
|
|
* case 1 ST adapter + ST mouse, and the PS/2 adapter (vendor-documented
|
|
* as "select Atari ST mouse") X = 13/12 Y = 14/15
|
|
* case 2 dedicated Amiga adapter + Amiga X = 13/12 Y = 15/14
|
|
* case 3 Amiga mouse in an ST-wired
|
|
* adapter -- what an in-game
|
|
* "Atari / Amiga" selector picks X = 13/15 Y = 12/14
|
|
*
|
|
* Do NOT "fix" the case-1 Y-phase assignment from Domin's twomice2.s
|
|
* variable names: that source labels bit 15 YA and bit 14 YB, the reverse
|
|
* of the wiring diagram, and its own decode table absorbs the difference
|
|
* (mapping doc section 8 C). The table below follows the physical wiring,
|
|
* which two independent sources agree on.
|
|
*
|
|
* UNVERIFIED -- case 2's Y sign. The dedicated Amiga adapter lands the
|
|
* Amiga's V (Y phase A) on bit 15 and VQ (Y phase B) on bit 14, i.e. the
|
|
* A/B pair is swapped relative to case 1, which inverts Y. Domin
|
|
* nonetheless drives both with one software mode, so either ST and Amiga
|
|
* mice differ in encoder orientation and the swap cancels it, or the
|
|
* wiki's (YA)/(YB) labels on the Amiga column are editorial rather than
|
|
* measured, or he simply tolerated an inverted Y. No document settles it
|
|
* and NO FIXTURE IN THIS REPO CAN: case-2 hardware is rare and no
|
|
* available title is known to target it. The table implements the wiring
|
|
* literally, as recorded, rather than guessing a sign. Do not "correct"
|
|
* it without a source. */
|
|
typedef struct
|
|
{
|
|
uint8_t xa, xb, ya, yb;
|
|
} MouseWiring;
|
|
|
|
static const MouseWiring mouse_wiring[3] = {
|
|
/* INPUTDEV_MOUSE_ST */ { 13, 12, 14, 15 },
|
|
/* INPUTDEV_MOUSE_AMIGA_ADAPTER */ { 13, 12, 15, 14 },
|
|
/* INPUTDEV_MOUSE_AMIGA_ON_ST */ { 13, 15, 12, 14 }
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
InputDevType type;
|
|
quad_axis x;
|
|
quad_axis y;
|
|
int32_t scale_q8;
|
|
axis_tune tune_x; /* option-derived; see InputDevSetTune (#439) */
|
|
axis_tune tune_y;
|
|
uint8_t btn_left;
|
|
uint8_t btn_right;
|
|
uint8_t rotary_id; /* option-derived; see InputDevSetRotaryID */
|
|
/* Analog / driving controller (#437) -- TR10 bank-switching device.
|
|
* an_bank / an_row are the microcontroller's bank counter and the row
|
|
* code it last saw on its socket; an_x / an_y are the latched 8-bit
|
|
* ADC values (128 = centre); an_sw is the INPUTDEV_SW_* mask. All
|
|
* five are machine-visible and serialized. an_engaged is the
|
|
* host-routing liveness latch (inputdev.h, ENGAGEMENT) and is NOT. */
|
|
uint8_t an_bank;
|
|
uint8_t an_row; /* last row decoded, 0-3; 0xFF = none yet */
|
|
uint8_t an_x;
|
|
uint8_t an_y;
|
|
uint8_t an_sw;
|
|
uint8_t an_engaged;
|
|
} InputDevPort;
|
|
|
|
static InputDevPort inputdev_ports[2];
|
|
static uint32_t inputdev_attach_mask;
|
|
static uint8_t inputdev_armed;
|
|
|
|
/* joypadNButtons[], indexed by port. A rotary is a matrix device and
|
|
* reports through these slots; the mouse never touches them. */
|
|
static uint8_t *const inputdev_pad[2] = { joypad0Buttons, joypad1Buttons };
|
|
|
|
static int inputdev_is_mouse(InputDevType t)
|
|
{
|
|
return (t == INPUTDEV_MOUSE_ST
|
|
|| t == INPUTDEV_MOUSE_AMIGA_ADAPTER
|
|
|| t == INPUTDEV_MOUSE_AMIGA_ON_ST) ? 1 : 0;
|
|
}
|
|
|
|
/* Analog stick and driving controller are one wire protocol (inputdev.h);
|
|
* the two types differ only in how libretro.c sources host values. */
|
|
static int inputdev_is_analog(InputDevType t)
|
|
{
|
|
return (t == INPUTDEV_ANALOG || t == INPUTDEV_DRIVING) ? 1 : 0;
|
|
}
|
|
|
|
/* Dynamic (machine-visible) state only: encoders, button latches and the
|
|
* armed flag. The device type, the sensitivity scale, and the per-axis
|
|
* tuning (tune_x/tune_y, #439 -- dead zone, offset, response exponent) are
|
|
* all owned by the core options and survive a machine reset, exactly as a
|
|
* physical mouse stays plugged in across a console reset. Deliberate, and
|
|
* load-bearing: see test/tools/tuning_identity.c's sweep-order comment. */
|
|
static void inputdev_reset_dynamic(void)
|
|
{
|
|
unsigned p;
|
|
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
QuadReset(&inputdev_ports[p].x);
|
|
QuadReset(&inputdev_ports[p].y);
|
|
inputdev_ports[p].btn_left = 0;
|
|
inputdev_ports[p].btn_right = 0;
|
|
/* Analog device (#437): the microcontroller powers up in bank 0
|
|
* with nothing latched. Engagement clears too -- the port has to
|
|
* prove live again after a reset, exactly as after a type change. */
|
|
inputdev_ports[p].an_bank = 0;
|
|
inputdev_ports[p].an_row = 0xFF;
|
|
inputdev_ports[p].an_x = 128;
|
|
inputdev_ports[p].an_y = 128;
|
|
inputdev_ports[p].an_sw = 0;
|
|
inputdev_ports[p].an_engaged = 0;
|
|
}
|
|
|
|
inputdev_armed = 0;
|
|
}
|
|
|
|
void InputDevInit(void)
|
|
{
|
|
unsigned p;
|
|
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
if (inputdev_ports[p].scale_q8 <= 0)
|
|
inputdev_ports[p].scale_q8 = 256;
|
|
|
|
/* A zero-filled axis_tune already reads as the identity (see
|
|
* axistune.h), so this is belt-and-braces rather than load-bearing
|
|
* -- but it makes the default explicit at exactly the place the
|
|
* scale's default is, instead of leaving it to a reader to work out
|
|
* that exponent_q8 == 0 means linear. */
|
|
if (inputdev_ports[p].tune_x.exponent_q8 <= 0)
|
|
AxisTuneReset(&inputdev_ports[p].tune_x);
|
|
if (inputdev_ports[p].tune_y.exponent_q8 <= 0)
|
|
AxisTuneReset(&inputdev_ports[p].tune_y);
|
|
}
|
|
|
|
inputdev_reset_dynamic();
|
|
}
|
|
|
|
void InputDevReset(void)
|
|
{
|
|
inputdev_reset_dynamic();
|
|
}
|
|
|
|
void InputDevShutdown(void)
|
|
{
|
|
/* iOS cannot dlclose a core, so every static has to go back to its
|
|
* load-time value here -- including the option-derived ones. */
|
|
memset(inputdev_ports, 0, sizeof(inputdev_ports));
|
|
inputdev_ports[0].scale_q8 = 256;
|
|
inputdev_ports[1].scale_q8 = 256;
|
|
AxisTuneReset(&inputdev_ports[0].tune_x);
|
|
AxisTuneReset(&inputdev_ports[0].tune_y);
|
|
AxisTuneReset(&inputdev_ports[1].tune_x);
|
|
AxisTuneReset(&inputdev_ports[1].tune_y);
|
|
inputdev_attach_mask = 0;
|
|
inputdev_armed = 0;
|
|
}
|
|
|
|
void InputDevSetType(int port, InputDevType type)
|
|
{
|
|
if (port < 0 || port > 1)
|
|
return;
|
|
|
|
/* Mouse is port 2 only (see inputdev.h); the rotary and the analog /
|
|
* driving controller (#437 -- TR10 restricts neither socket) are valid
|
|
* on both. Anything else is a plain pad, i.e. not attached. */
|
|
if (inputdev_is_mouse(type))
|
|
{
|
|
if (port != 1)
|
|
type = INPUTDEV_PAD;
|
|
}
|
|
else if (type != INPUTDEV_ROTARY && !inputdev_is_analog(type))
|
|
type = INPUTDEV_PAD;
|
|
|
|
if (inputdev_ports[port].type != type)
|
|
{
|
|
inputdev_ports[port].type = type;
|
|
QuadReset(&inputdev_ports[port].x);
|
|
QuadReset(&inputdev_ports[port].y);
|
|
inputdev_ports[port].btn_left = 0;
|
|
inputdev_ports[port].btn_right = 0;
|
|
/* Analog dynamic state is per-plug too: a freshly attached
|
|
* controller is in bank 0 with nothing latched, and has to earn
|
|
* engagement again (inputdev.h). */
|
|
inputdev_ports[port].an_bank = 0;
|
|
inputdev_ports[port].an_row = 0xFF;
|
|
inputdev_ports[port].an_x = 128;
|
|
inputdev_ports[port].an_y = 128;
|
|
inputdev_ports[port].an_sw = 0;
|
|
inputdev_ports[port].an_engaged = 0;
|
|
/* The arm flag is part of the dynamic state too: without this a
|
|
* mouse -> pad -> mouse round trip leaves a stale arm behind, worth
|
|
* one unrequested advance on the next read. Cleared only on a real
|
|
* type change -- apply_port_device() calls this on every
|
|
* check_variables(), and an unconditional clear there would eat a
|
|
* legitimate arm in the common path. */
|
|
inputdev_armed = 0;
|
|
}
|
|
|
|
if (type == INPUTDEV_PAD)
|
|
inputdev_attach_mask &= ~(1u << port);
|
|
else
|
|
inputdev_attach_mask |= (1u << port);
|
|
}
|
|
|
|
InputDevType InputDevGetType(int port)
|
|
{
|
|
if (port < 0 || port > 1)
|
|
return INPUTDEV_PAD;
|
|
return inputdev_ports[port].type;
|
|
}
|
|
|
|
int InputDevAnyAttached(void)
|
|
{
|
|
return inputdev_attach_mask ? 1 : 0;
|
|
}
|
|
|
|
void InputDevSetScale(int port, int32_t scale_q8)
|
|
{
|
|
if (port < 0 || port > 1)
|
|
return;
|
|
if (scale_q8 < 1)
|
|
scale_q8 = 1;
|
|
if (scale_q8 > 4096)
|
|
scale_q8 = 4096;
|
|
inputdev_ports[port].scale_q8 = scale_q8;
|
|
}
|
|
|
|
void InputDevSetTune(int port, int axis, int32_t deadzone, int32_t offset,
|
|
int32_t exponent_q8)
|
|
{
|
|
if (port < 0 || port > 1)
|
|
return;
|
|
|
|
AxisTuneSet(axis == INPUTDEV_AXIS_Y ? &inputdev_ports[port].tune_y
|
|
: &inputdev_ports[port].tune_x,
|
|
deadzone, offset, exponent_q8);
|
|
}
|
|
|
|
void InputDevSetRotaryID(int port, int reports_rotary)
|
|
{
|
|
if (port < 0 || port > 1)
|
|
return;
|
|
inputdev_ports[port].rotary_id = reports_rotary ? 1 : 0;
|
|
}
|
|
|
|
/* The single tuned feed path (#439). Every analog source funnels through
|
|
* here, so the tuning has exactly one implementation and cannot drift
|
|
* between devices.
|
|
*
|
|
* `invert` negates the TUNED delta, not the raw one, and the order matters:
|
|
* the dead zone and the curve are magnitude-symmetric and commute with a
|
|
* sign flip, but the OFFSET DOES NOT. An offset cancels a bias measured on
|
|
* the host axis, so it has to be subtracted in host orientation, before any
|
|
* device-specific inversion. Do not "simplify" this by negating `raw` at
|
|
* the call site.
|
|
*
|
|
* The curve arrives as a Q8 gain and multiplies into the port's Q8
|
|
* sensitivity, which is what lets QuadFeed's existing fractional carry keep
|
|
* small movement alive -- axistune.h explains why a curved delta could not.
|
|
* The product is clamped to the same 1..4096 window InputDevSetScale
|
|
* enforces, because QuadFeed's overflow argument is written against a 4096
|
|
* ceiling and two multipliers in series would otherwise walk past it. */
|
|
static void inputdev_feed_axis(quad_axis *q, const axis_tune *t,
|
|
int32_t raw, int invert, int32_t scale_q8)
|
|
{
|
|
int32_t gain_q8 = 256;
|
|
int32_t d = AxisTuneApply(t, raw, &gain_q8);
|
|
int32_t eff;
|
|
|
|
if (invert)
|
|
d = -d;
|
|
|
|
/* At defaults gain_q8 is exactly 256 and this is exactly scale_q8, so
|
|
* the call below is bit-for-bit the pre-#439 one. */
|
|
eff = (scale_q8 * gain_q8) / 256;
|
|
if (eff < 1)
|
|
eff = 1;
|
|
if (eff > 4096)
|
|
eff = 4096;
|
|
|
|
QuadFeed(q, d, eff);
|
|
}
|
|
|
|
void InputDevFeed(int port, int32_t dx, int32_t dy, uint32_t buttons)
|
|
{
|
|
InputDevPort *p;
|
|
|
|
if (port < 0 || port > 1)
|
|
return;
|
|
|
|
p = &inputdev_ports[port];
|
|
|
|
if (p->scale_q8 <= 0)
|
|
p->scale_q8 = 256;
|
|
|
|
if (p->type == INPUTDEV_ROTARY)
|
|
{
|
|
/* One axis, not two: the rotary is a single wheel. dy is ignored,
|
|
* and so are the mouse buttons -- a rotary's A/B/C/Option/Pause and
|
|
* keypad are real matrix switches and come from the retropad.
|
|
*
|
|
* DIRECTION. TR10 gives the phase sequences as pin levels:
|
|
* anticlockwise J10 0 1 1 0 ... clockwise J10 0 0 1 1 ...
|
|
* J11 0 0 1 1 ... J11 0 1 1 0 ...
|
|
* i.e. anticlockwise = (A,B) walking 00,10,11,01 = INCREASING Gray
|
|
* index = A leads B, and clockwise = decreasing.
|
|
*
|
|
* Mapping host +X to clockwise is a HOST-SIDE UX CONVENTION, not a
|
|
* hardware fact: pushing a spinner to the right turns the knob
|
|
* clockwise. Hence the negation. There is deliberately no invert
|
|
* option -- an unsourced sign knob is how a real wiring bug gets
|
|
* papered over instead of found. The negation is applied to the
|
|
* TUNED delta by inputdev_feed_axis -- see the note there on why the
|
|
* offset must be subtracted in host orientation first. */
|
|
inputdev_feed_axis(&p->x, &p->tune_x, dx, 1, p->scale_q8);
|
|
(void)dy;
|
|
(void)buttons;
|
|
return;
|
|
}
|
|
|
|
if (!inputdev_is_mouse(p->type))
|
|
return;
|
|
|
|
/* One libretro unit = one Gray-code state at scale 1.0. Domin's
|
|
* decoder increments its position by one per state transition, so one
|
|
* host "pixel" of motion maps to one decoded count. Positive dx is
|
|
* right and positive dy is down in libretro, and A-leads-B (increasing
|
|
* phase) is right/down on the wire, so the mapping is direct. */
|
|
inputdev_feed_axis(&p->x, &p->tune_x, dx, 0, p->scale_q8);
|
|
inputdev_feed_axis(&p->y, &p->tune_y, dy, 0, p->scale_q8);
|
|
|
|
p->btn_left = (buttons & INPUTDEV_BTN_LEFT) ? 1 : 0;
|
|
p->btn_right = (buttons & INPUTDEV_BTN_RIGHT) ? 1 : 0;
|
|
}
|
|
|
|
/* Absolute-axis conversion for the analog device (#437): libretro int16
|
|
* -> tuned 8-bit ADC byte. The tuning runs in the device's own 8-bit
|
|
* domain (range 127) so the shared dead-zone/offset option units mean
|
|
* "ADC counts" here, comparable in magnitude to the relative path's
|
|
* "host units per poll". `invert` flips the TUNED value -- the offset
|
|
* must be subtracted in host orientation first, exactly as
|
|
* inputdev_feed_axis does on the relative path (#474). */
|
|
static uint8_t inputdev_analog_byte(const axis_tune *t, int32_t v16,
|
|
int invert)
|
|
{
|
|
int32_t v8;
|
|
|
|
/* -32768..32767 -> -128..127, then symmetrized to -127..127 so the
|
|
* tuned magnitude maps cleanly onto 128 +/- 127 (byte range 1..255;
|
|
* TR10 forbids games from assuming the endpoints anyway). */
|
|
v8 = v16 / 258;
|
|
if (v8 > 127) v8 = 127;
|
|
if (v8 < -127) v8 = -127;
|
|
|
|
v8 = AxisTuneApplyAbs(t, v8, 127);
|
|
|
|
if (invert)
|
|
v8 = -v8;
|
|
|
|
return (uint8_t)(128 + v8);
|
|
}
|
|
|
|
void InputDevFeedAnalog(int port, int32_t x, int32_t y, uint32_t switches)
|
|
{
|
|
InputDevPort *p;
|
|
|
|
if (port < 0 || port > 1)
|
|
return;
|
|
|
|
p = &inputdev_ports[port];
|
|
|
|
if (!inputdev_is_analog(p->type))
|
|
return;
|
|
|
|
/* TR10: +X = right (matches the libretro convention, no inversion) and
|
|
* +Y = forward / accelerator, which is libretro's -Y -- hence the
|
|
* inversion, applied to the tuned value. */
|
|
p->an_x = inputdev_analog_byte(&p->tune_x, x, 0);
|
|
p->an_y = inputdev_analog_byte(&p->tune_y, y, 1);
|
|
p->an_sw = (uint8_t)(switches & 0xFF);
|
|
p->an_engaged = 1;
|
|
}
|
|
|
|
void InputDevArm(void)
|
|
{
|
|
if (!inputdev_attach_mask)
|
|
return;
|
|
inputdev_armed = 1;
|
|
}
|
|
|
|
void InputDevRowSelect(uint8_t row0, uint8_t row1)
|
|
{
|
|
uint8_t row[2];
|
|
unsigned p;
|
|
|
|
if (!inputdev_attach_mask)
|
|
return;
|
|
|
|
row[0] = row0;
|
|
row[1] = row1;
|
|
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
InputDevPort *dp = &inputdev_ports[p];
|
|
|
|
if (!inputdev_is_analog(dp->type) || !dp->an_engaged)
|
|
continue;
|
|
|
|
/* TR10: "Bank switching is done automatically when the controller
|
|
* sees a transition from row 3 to row 0 (of the same controller
|
|
* socket)", and interleaved reads of OTHER sockets "must not cause
|
|
* the controller to lose synchronisation or perform any bank
|
|
* switching" -- TR10's own driver recipe reads a bank from one
|
|
* controller, banks from others, then comes back. While another
|
|
* socket is addressed (or the output enable is clear), OUR lines
|
|
* sit at the pulled-up no-row pattern; the microcontroller treats
|
|
* that as idle and REMEMBERS the last row it decoded, so a
|
|
* row-3 ... idle ... row-0 sequence still switches. Hence a
|
|
* non-row code is skipped, not recorded. Only two banks exist on
|
|
* this device, so the cycle is a toggle. */
|
|
if (row[p] > 3)
|
|
continue;
|
|
|
|
if (dp->an_row == 3 && row[p] == 0)
|
|
dp->an_bank ^= 1;
|
|
|
|
dp->an_row = row[p];
|
|
}
|
|
}
|
|
|
|
/* Write a rotary port's current phase into its pad slots.
|
|
*
|
|
* POLARITY IS INVERTED RELATIVE TO THE MOUSE OVERLAY, which is the single
|
|
* easiest way to ship a backwards spinner. TR10 gives phase sequences as
|
|
* PIN LEVELS, and "reading a zero means the appropriate button is
|
|
* depressed"; joystick.c does `data &= (joypadNButtons[slot] ? mask :
|
|
* 0xFFFF)`, so a NON-ZERO array entry pulls the bit low. Therefore
|
|
* pin level 1 -> slot 0x00, pin level 0 -> slot 0xFF. The mouse overlay
|
|
* drives raw $F14000 bits where "0 = asserted" applies with no inversion.
|
|
* Two conventions in one feature, which is exactly why QuadLevels()
|
|
* returns logic levels and each sink applies its own polarity here.
|
|
*
|
|
* Phase 0 -> BUTTON_L, Phase 1 -> BUTTON_R (TR10's row-0 matrix: J10/J14
|
|
* and J11/J15, the same slot indices on both ports). BUTTON_U / BUTTON_D
|
|
* are forced clear: the controller physically has no up/down, which is
|
|
* why TR10 tells rotary games to use A = Up and C = Down for menus.
|
|
*
|
|
* The encoder's rest phase is (1,1) -- QUAD_REST_PHASE, chosen by #429 --
|
|
* so an idle rotary writes 0x00 into all four slots and is bit-identical
|
|
* to an idle pad. */
|
|
static void inputdev_publish_rotary(unsigned port)
|
|
{
|
|
uint8_t *pad = inputdev_pad[port];
|
|
int a, b;
|
|
|
|
QuadLevels(&inputdev_ports[port].x, &a, &b);
|
|
|
|
pad[BUTTON_U] = 0x00;
|
|
pad[BUTTON_D] = 0x00;
|
|
pad[BUTTON_L] = a ? 0x00 : 0xFF;
|
|
pad[BUTTON_R] = b ? 0x00 : 0xFF;
|
|
}
|
|
|
|
void InputDevClock(uint8_t row0, uint8_t row1)
|
|
{
|
|
uint8_t row[2];
|
|
unsigned p;
|
|
int armed, consumed;
|
|
|
|
if (!inputdev_attach_mask)
|
|
return;
|
|
|
|
row[0] = row0;
|
|
row[1] = row1;
|
|
armed = inputdev_armed ? 1 : 0;
|
|
consumed = 0;
|
|
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
InputDevType t = inputdev_ports[p].type;
|
|
|
|
if (inputdev_is_mouse(t))
|
|
{
|
|
/* Row-blind: every read of ITS PORT is a sample, whatever the
|
|
* row, so any armed read with that port addressed advances.
|
|
* Both axes, independently -- they are four parallel wires, not
|
|
* a time-multiplexed stream. */
|
|
if (armed && row[p] != 0xFF)
|
|
{
|
|
QuadAdvance(&inputdev_ports[p].x);
|
|
QuadAdvance(&inputdev_ports[p].y);
|
|
consumed = 1;
|
|
}
|
|
}
|
|
else if (t == INPUTDEV_ROTARY)
|
|
{
|
|
/* Row-gated: a rotary's phases exist in row 0 only, so the only
|
|
* read that samples it is a row-0 read of its own port. See the
|
|
* measured Tempest 2000 scan in inputdev.h -- advancing on all
|
|
* eight of its per-frame armed reads would decode as garbage,
|
|
* including a sign inversion at three states. */
|
|
if (armed && row[p] == 0)
|
|
{
|
|
QuadAdvance(&inputdev_ports[p].x);
|
|
consumed = 1;
|
|
}
|
|
|
|
/* Published on EVERY call, not only when it advanced:
|
|
* update_input() zeroes a rotary port's four direction slots
|
|
* each frame, so without an unconditional publish a rotary would
|
|
* read as "both phases high" for every read between the frame
|
|
* boundary and the next armed row-0 advance. */
|
|
inputdev_publish_rotary(p);
|
|
}
|
|
}
|
|
|
|
/* The arm is spent only by a read some attached device could decode
|
|
* from (inputdev.h, ARM CONSUMPTION).
|
|
*
|
|
* THIS GUARDS NOTHING REACHABLE TODAY, and the comment here used to
|
|
* claim otherwise. The reasoning that motivated it -- a port-1-only
|
|
* read eating the arm a port-2 mouse was owed -- cannot happen on the
|
|
* bus: the row select lives in joystick_ram[1], the only writer is
|
|
* JoystickWriteWord at offset 0, and that path arms unconditionally.
|
|
* So the arm is always set at the first read after any row change,
|
|
* and a later read at unchanged rows re-runs this same gate to the
|
|
* same answer. Measured, not argued: 2.4M random write/read
|
|
* sequences across six device combinations digest bit-identically
|
|
* with and without the `if`.
|
|
*
|
|
* It stays because it is the honest statement of the contract -- an
|
|
* arm is a permission for ONE decodable sample, not for one bus read
|
|
* -- and the next device that samples on some other condition (a
|
|
* lightgun latch, a second row-blind peripheral) makes the difference
|
|
* real. Pinned by test_arm_is_spent_only_by_a_decodable_read in
|
|
* mouse_decode_test.c, which reaches it by calling InputDevArm() and
|
|
* InputDevClock() directly, because no aligned bus sequence can. */
|
|
if (consumed)
|
|
inputdev_armed = 0;
|
|
}
|
|
|
|
/* The nibble an engaged analog controller drives on its four J lines for
|
|
* the current row and bank, as logic levels (bit i of the return = line
|
|
* J+i). TR10 Bank 0/Bank 1 tables, restated in inputdev.h. */
|
|
static uint8_t inputdev_analog_nibble(const InputDevPort *p, uint8_t row)
|
|
{
|
|
if (p->an_bank == 0)
|
|
{
|
|
switch (row)
|
|
{
|
|
case 0: return (uint8_t)(p->an_x & 0x0F); /* X0..X3 */
|
|
case 1: return (uint8_t)((p->an_x >> 4) & 0x0F); /* X4..X7 */
|
|
case 2: return (uint8_t)(p->an_y & 0x0F); /* Y0..Y3 */
|
|
default: return (uint8_t)((p->an_y >> 4) & 0x0F); /* Y4..Y7 */
|
|
}
|
|
}
|
|
|
|
/* Bank 1: rows 1-3 read all 1s; row 0 carries the hat / gear shift
|
|
* switches, active low (pressed pulls the line to 0). */
|
|
if (row != 0)
|
|
return 0x0F;
|
|
|
|
return (uint8_t)(0x0F
|
|
& ~((p->an_sw & INPUTDEV_SW_UP ? 1u : 0u)
|
|
| (p->an_sw & INPUTDEV_SW_DOWN ? 2u : 0u)
|
|
| (p->an_sw & INPUTDEV_SW_LEFT ? 4u : 0u)
|
|
| (p->an_sw & INPUTDEV_SW_RIGHT ? 8u : 0u)));
|
|
}
|
|
|
|
uint16_t InputDevOverlayF14000(uint16_t data, uint8_t row0, uint8_t row1)
|
|
{
|
|
const MouseWiring *w;
|
|
InputDevPort *p;
|
|
uint8_t row[2];
|
|
unsigned q;
|
|
int ax, bx, ay, by;
|
|
|
|
if (!inputdev_attach_mask)
|
|
return data;
|
|
|
|
row[0] = row0;
|
|
row[1] = row1;
|
|
|
|
/* Analog / driving controller (#437): drives its port's four J lines
|
|
* ($F14000 bits 8-11 for port 1, 12-15 for port 2) whenever its own
|
|
* row select is asserted. The matrix contributes nothing there (an
|
|
* engaged analog port's pad slots are suppressed by update_input, and
|
|
* the bus default is all-high), so clearing the zero-level lines is a
|
|
* complete drive, same AND-only shape as the mouse overlay below. */
|
|
for (q = 0; q < 2; q++)
|
|
{
|
|
InputDevPort *ap = &inputdev_ports[q];
|
|
uint8_t nib;
|
|
|
|
if (!inputdev_is_analog(ap->type) || !ap->an_engaged)
|
|
continue;
|
|
if (row[q] > 3)
|
|
continue;
|
|
|
|
nib = inputdev_analog_nibble(ap, row[q]);
|
|
data &= (uint16_t)~(((uint16_t)(~nib & 0x0F)) << (8 + 4 * q));
|
|
}
|
|
|
|
p = &inputdev_ports[1];
|
|
if (!inputdev_is_mouse(p->type))
|
|
return data;
|
|
|
|
w = &mouse_wiring[(int)p->type - (int)INPUTDEV_MOUSE_ST];
|
|
|
|
QuadLevels(&p->x, &ax, &bx);
|
|
QuadLevels(&p->y, &ay, &by);
|
|
|
|
/* Active low: a logic 0 on the wire pulls the register bit low. No
|
|
* row test -- the adapter never sees the row select, so it asserts
|
|
* identically in every row (including the "not a socket-0 code" case,
|
|
* where the pad matrix contributes nothing at all). */
|
|
if (!ax)
|
|
data &= (uint16_t)~(1u << w->xa);
|
|
if (!bx)
|
|
data &= (uint16_t)~(1u << w->xb);
|
|
if (!ay)
|
|
data &= (uint16_t)~(1u << w->ya);
|
|
if (!by)
|
|
data &= (uint16_t)~(1u << w->yb);
|
|
|
|
return data;
|
|
}
|
|
|
|
uint16_t InputDevOverlayF14002(uint16_t data, uint8_t row0, uint8_t row1)
|
|
{
|
|
InputDevPort *p;
|
|
|
|
if (!inputdev_attach_mask)
|
|
return data;
|
|
|
|
/* Rotary controller-type identification (TR10 "Identifying Controller
|
|
* Types"; see InputDevSetRotaryID). C2 C3 = 1 0 is "Tempest Rotary",
|
|
* and the C-column rows differ by port: C3 is $F14002 bit 0 in row 3
|
|
* on port 1, and bit 2 in row 2 on port 2. C2 is left high, which is
|
|
* what joystick.c already returns.
|
|
*
|
|
* joystick.c drives only bit 1 (port 1) / bit 3 (port 2) in those
|
|
* rows, so clearing the C bit here cannot collide with a button. */
|
|
if (inputdev_ports[0].type == INPUTDEV_ROTARY
|
|
&& inputdev_ports[0].rotary_id && row0 == 3)
|
|
data &= (uint16_t)~(1u << 0);
|
|
|
|
if (inputdev_ports[1].type == INPUTDEV_ROTARY
|
|
&& inputdev_ports[1].rotary_id && row1 == 2)
|
|
data &= (uint16_t)~(1u << 2);
|
|
|
|
/* Analog / driving controller (#437): the B and C columns.
|
|
*
|
|
* Port 1's columns are B0 (bit 0, the C column) and B1 (bit 1, the
|
|
* button column); port 2's are B2 (bit 2) and B3 (bit 3).
|
|
*
|
|
* C column: in row 0 it is the BANK FLAG -- "It will always be zero in
|
|
* Bank 0, while all other banks will return 1" (TR10, Reading Bank
|
|
* Switching Controllers). In rows 1-3 it carries C1/C2/C3, whose
|
|
* row assignment differs by port (the same port-headed TR10 tables the
|
|
* rotary ID above uses): port 1 reads C1/C2/C3 in rows 1/2/3, port 2
|
|
* reads C1/C3/C2 in rows 1/2/3. The values are fixed -- C1 = 1,
|
|
* C2 = 0, C3 = 1, i.e. C2 C3 = 0 1 = "Bank Switching" -- so the only
|
|
* bit to clear is C2: row 2 on port 1, row 3 on port 2.
|
|
*
|
|
* B column: bank 0 carries buttons A/B/C/D in rows 0-3, active low;
|
|
* bank 1 reads 1 in every row, which is the "1111 = Analogue Joystick
|
|
* or Driving Controller" type identifier among the bank-switching
|
|
* types. All-high needs no clearing. */
|
|
{
|
|
unsigned q;
|
|
uint8_t arow[2];
|
|
|
|
arow[0] = row0;
|
|
arow[1] = row1;
|
|
|
|
for (q = 0; q < 2; q++)
|
|
{
|
|
InputDevPort *ap = &inputdev_ports[q];
|
|
unsigned c_bit = 0 + 2 * q;
|
|
unsigned b_bit = 1 + 2 * q;
|
|
uint8_t r = arow[q];
|
|
|
|
if (!inputdev_is_analog(ap->type) || !ap->an_engaged)
|
|
continue;
|
|
if (r > 3)
|
|
continue;
|
|
|
|
if (r == 0)
|
|
{
|
|
if (ap->an_bank == 0)
|
|
data &= (uint16_t)~(1u << c_bit);
|
|
}
|
|
else if (r == (q == 0 ? 2u : 3u))
|
|
data &= (uint16_t)~(1u << c_bit); /* C2 = 0 */
|
|
|
|
if (ap->an_bank == 0)
|
|
{
|
|
static const uint8_t sw_of_row[4] = {
|
|
INPUTDEV_SW_A, INPUTDEV_SW_B, INPUTDEV_SW_C, INPUTDEV_SW_D
|
|
};
|
|
if (ap->an_sw & sw_of_row[r])
|
|
data &= (uint16_t)~(1u << b_bit);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* The mouse is row-blind, so its own contribution ignores both rows. */
|
|
p = &inputdev_ports[1];
|
|
if (!inputdev_is_mouse(p->type))
|
|
return data;
|
|
|
|
/* LMB -> B3 ($F14002 bit 3), RMB -> B2 (bit 2), in EVERY row.
|
|
*
|
|
* Both of these are faithful hardware quirks, not oversights:
|
|
* joystick.c decodes bit 3 as A / B / C / Option depending on the row,
|
|
* so a held LMB reads as all four; and bit 2 is the C1/C2/C3
|
|
* controller-type field in rows 1-3, so a held RMB changes the
|
|
* reported controller type. Do not add a row test to "fix" either. */
|
|
if (p->btn_left)
|
|
data &= (uint16_t)~(1u << 3);
|
|
if (p->btn_right)
|
|
data &= (uint16_t)~(1u << 2);
|
|
|
|
return data;
|
|
}
|
|
|
|
size_t InputDevStateSave(uint8_t *buf)
|
|
{
|
|
uint8_t *start = buf;
|
|
unsigned p;
|
|
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].x.backlog);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].x.frac);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].x.phase);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].y.backlog);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].y.frac);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].y.phase);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].btn_left);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].btn_right);
|
|
}
|
|
|
|
STATE_SAVE_VAR(buf, inputdev_armed);
|
|
|
|
/* Analog controller (#437), appended so the pre-analog field order is
|
|
* untouched. Machine-visible only: bank, last row, latched ADC bytes
|
|
* and switch mask; engagement is host-routing-derived and stays out
|
|
* (inputdev.h). */
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].an_bank);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].an_row);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].an_x);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].an_y);
|
|
STATE_SAVE_VAR(buf, inputdev_ports[p].an_sw);
|
|
}
|
|
|
|
return (size_t)(buf - start);
|
|
}
|
|
|
|
/* A savestate is untrusted input. `phase` was already masked on load;
|
|
* `backlog` and `frac` need the same treatment, because QuadFeed() clamps
|
|
* only AFTER `q->backlog += states`, so a corrupt backlog near INT32_MAX
|
|
* overflows before the clamp can see it. The invariants are the ones
|
|
* QuadFeed/QuadAdvance maintain: |backlog| <= QUAD_MAX_BACKLOG and
|
|
* |frac| < 256 (it is a Q8 remainder). */
|
|
static void inputdev_sanitize_axis(quad_axis *q)
|
|
{
|
|
q->phase &= 3;
|
|
|
|
if (q->backlog > QUAD_MAX_BACKLOG)
|
|
q->backlog = QUAD_MAX_BACKLOG;
|
|
if (q->backlog < -QUAD_MAX_BACKLOG)
|
|
q->backlog = -QUAD_MAX_BACKLOG;
|
|
|
|
if (q->frac > 255)
|
|
q->frac = 255;
|
|
if (q->frac < -255)
|
|
q->frac = -255;
|
|
}
|
|
|
|
size_t InputDevStateLoad(const uint8_t *buf)
|
|
{
|
|
const uint8_t *start = buf;
|
|
unsigned p;
|
|
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].x.backlog);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].x.frac);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].x.phase);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].y.backlog);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].y.frac);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].y.phase);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].btn_left);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].btn_right);
|
|
inputdev_sanitize_axis(&inputdev_ports[p].x);
|
|
inputdev_sanitize_axis(&inputdev_ports[p].y);
|
|
}
|
|
|
|
STATE_LOAD_VAR(buf, inputdev_armed);
|
|
|
|
/* Analog controller (#437). A state is untrusted input: the bank has
|
|
* exactly two values and the row is 0-3 or the none-yet sentinel, so
|
|
* anything else is coerced. The ADC bytes and the switch mask are
|
|
* full-range by construction. A pre-extension v12 develop state
|
|
* reads the zero-fill tail here (see inputdev.h) -- bank 0, row 0,
|
|
* zeroed latches -- inert unless a device is selected and engaged. */
|
|
for (p = 0; p < 2; p++)
|
|
{
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].an_bank);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].an_row);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].an_x);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].an_y);
|
|
STATE_LOAD_VAR(buf, inputdev_ports[p].an_sw);
|
|
inputdev_ports[p].an_bank &= 1;
|
|
if (inputdev_ports[p].an_row > 3)
|
|
inputdev_ports[p].an_row = 0xFF;
|
|
}
|
|
|
|
return (size_t)(buf - start);
|
|
}
|