mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
feat(input): TR10 bank-switching analog / driving controller (#437)
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]>
This commit is contained in:
1 parent
ec94a00612
commit
def8635e2e
14 files changed
+1712
-36
No files matched your search
@@ -66,6 +66,7 @@ test/tools/netlink_game
|
||||
test/tools/joymatrix_identity
|
||||
test/tools/mouse_decode_test
|
||||
test/tools/rotary_decode_test
|
||||
test/tools/analog_decode_test
|
||||
test/tools/tuning_identity
|
||||
# vjtrace flight-recorder analyzer/smoke tools -- built on demand by
|
||||
# test/tools/vjtrace_selftest.sh (and by hand per each tool's own header
|
||||
|
||||
@@ -928,7 +928,8 @@ clean:
|
||||
test/test_titledb test/test_titlehook test/tools/test_hook_gate \
|
||||
test/tools/test_wedge_spin test/tools/test_texdump test/tools/i2s_lag_probe \
|
||||
test/tools/joymatrix_identity test/tools/mouse_decode_test \
|
||||
test/tools/rotary_decode_test test/tools/tuning_identity \
|
||||
test/tools/rotary_decode_test test/tools/analog_decode_test \
|
||||
test/tools/tuning_identity \
|
||||
test/tools/blitter_static_leak \
|
||||
test/test_quadrature test/test_axistune \
|
||||
test/.skipped-checks
|
||||
@@ -987,7 +988,8 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
test/tools/test_hook_gate \
|
||||
test/tools/i2s_lag_probe test/tools/joymatrix_identity \
|
||||
test/test_quadrature test/test_axistune test/tools/mouse_decode_test \
|
||||
test/tools/rotary_decode_test test/tools/tuning_identity \
|
||||
test/tools/rotary_decode_test test/tools/analog_decode_test \
|
||||
test/tools/tuning_identity \
|
||||
test/tools/blitter_static_leak \
|
||||
tools/jagcd/jagcd-chd-check
|
||||
@# Skip ledger: truncate FIRST so a previous run's rows cannot resurface
|
||||
@@ -1283,6 +1285,14 @@ test: test/test_dram_timing test/test_cheat test/test_event_queue test/test_jlin
|
||||
@# two-controller Pause unlock, the C2/C3 ID bits and a savestate
|
||||
@# round-trip mid-rotation.
|
||||
./test/tools/rotary_decode_test ./$(TARGET) test/roms/yarc.j64 --quiet
|
||||
@# TR10 bank-switching analog / driving controller (#437): NO RELEASED
|
||||
@# TITLE reads this protocol, so this synthetic register-level suite IS
|
||||
@# the device's verification (the tool's header says so at length): a
|
||||
@# TR10-faithful driver scans both banks out of $F14000/$F14002 and
|
||||
@# asserts the layout, the bank cycling (incl. other-socket interleave
|
||||
@# immunity), the type ID bits, the engagement guardrail, the absolute
|
||||
@# tuning semantics and the extended v12 savestate chunk.
|
||||
./test/tools/analog_decode_test ./$(TARGET) test/roms/yarc.j64 --quiet
|
||||
@# Per-axis input tuning (#439), no core: the identity at defaults over
|
||||
@# the whole int16 range, the dead zone as a gate rather than a re-base,
|
||||
@# the curve's fixed point at QUAD_MAX_BACKLOG and its exact values.
|
||||
@@ -1695,6 +1705,13 @@ test/tools/rotary_decode_test: test/tools/rotary_decode_test.c \
|
||||
test/harness/harness.c \
|
||||
$(if $(filter Linux,$(shell uname -s)),-ldl) -lm
|
||||
|
||||
test/tools/analog_decode_test: test/tools/analog_decode_test.c \
|
||||
test/harness/harness.c test/harness/harness.h
|
||||
$(CC) -O2 -Wall -std=c99 $(INCFLAGS) \
|
||||
-o $@ test/tools/analog_decode_test.c \
|
||||
test/harness/harness.c \
|
||||
$(if $(filter Linux,$(shell uname -s)),-ldl) -lm
|
||||
|
||||
test/test_quadrature: test/test_quadrature.c src/jerry/quadrature.c src/jerry/quadrature.h
|
||||
$(CC) -O2 -Wall $(INCFLAGS) -o $@ test/test_quadrature.c src/jerry/quadrature.c
|
||||
|
||||
|
||||
@@ -189,10 +189,54 @@ row scans. We reproduce that, so three odd-looking behaviours are faithful:
|
||||
All three happen on real hardware with a real adapter. They are commented as
|
||||
such at the code sites so nobody "fixes" them later.
|
||||
|
||||
## Analog / driving controller (#437)
|
||||
|
||||
Atari specified — but **never released** — an analog controller: a
|
||||
bank-switching matrix device with its own ADC chip, documented in the Jaguar
|
||||
Technical Reference V10 ("Analogue Joystick and 'Driving' Controllers",
|
||||
"Reading Bank Switching Controllers"). The core emulates that specification
|
||||
faithfully on both ports, as two option values that are one wire protocol:
|
||||
|
||||
| Option value | Host mapping |
|
||||
|---|---|
|
||||
| *Analog Joystick (bank-switching)* | Left stick X/Y (stick forward = TR10 +Y). A/B/C on the pad's A/B/Y, D on X, hat on the d-pad. |
|
||||
| *Driving Controller (bank-switching)* | Left stick X = steering; R2/L2 analog triggers = accelerator/brake (stick Y as fallback). D-pad up/down = gear shift. |
|
||||
|
||||
Facts to know before selecting one:
|
||||
|
||||
- **No released title reads this protocol.** The research for #437
|
||||
established that the controller never shipped and no commercial software
|
||||
polls it (BigPEmu's Checkered Flag "analog" support is a game *patch*, not
|
||||
this device). It exists here for homebrew and BigPEmu parity, and its
|
||||
verification is the synthetic register-level suite
|
||||
`test/tools/analog_decode_test.c` — there is no game to boot against.
|
||||
- **The port stays a RetroPad until the stick moves** (the same liveness rule
|
||||
as the mouse). Consequence: a title that probes controller types once at
|
||||
boot will see a standard pad unless you deflect the stick first.
|
||||
- Per-axis tuning (`virtualjaguar_analog_*`) goes through the same shared
|
||||
layer as the mouse and rotary, but with *absolute* semantics: units are ADC
|
||||
counts (127 = full deflection), the dead zone re-bases smoothly instead of
|
||||
gating, and the response curve is anchored at full deflection (so it costs
|
||||
no range and there is no sensitivity option).
|
||||
|
||||
Two neighbouring findings, recorded so they stay asked-and-answered:
|
||||
|
||||
- **"ADC-Reg" (BigPEmu's third analog type) is out of scope as unsourced.**
|
||||
Early *development* Jaguars had an 8-bit ADC on the motherboard (the
|
||||
connector's PAD0X/PAD0Y/PAD1X/PAD1Y pins; GPIO5 `$F17C00-$F17FFF` is
|
||||
labelled "Paddle Interface" in TRM rev 8) — but TR10 records it as
|
||||
**deleted** from production hardware, no document we hold specifies the
|
||||
register layout, and the only known software (the JANALOG.ABS test) targets
|
||||
dev units. Emulating it would mean inventing register behaviour; if a
|
||||
source surfaces, it can be added.
|
||||
- **Head tracker (Jaguar VR):** a bank-switching type ID exists for it in
|
||||
TR10, but no released software uses it (Missile Command 3D was a
|
||||
prototype). Out of scope, per the epic.
|
||||
|
||||
## Not yet implemented
|
||||
|
||||
Lightgun support (#438) and analog / driving controllers (#437) are open.
|
||||
The **Tempest rotary** (#436) and per-axis tuning (#439) have since shipped —
|
||||
Lightgun support (#438) is open. The **Tempest rotary** (#436), analog /
|
||||
driving controllers (#437) and per-axis tuning (#439) have since shipped —
|
||||
see [Core options](#core-options) above.
|
||||
|
||||
## See also
|
||||
|
||||
+193
-5
@@ -231,6 +231,15 @@ static uint8_t *joypad_buttons[2] = { joypad0Buttons, joypad1Buttons };
|
||||
#define RETRO_DEVICE_JAG_MOUSE_AMIGA RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_MOUSE, 1)
|
||||
#define RETRO_DEVICE_JAG_MOUSE_AMIGA_AD RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_MOUSE, 2)
|
||||
#define RETRO_DEVICE_JAG_ROTARY RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_MOUSE, 3)
|
||||
/* The TR10 bank-switching analog / driving controller (#437) is a
|
||||
* RETRO_DEVICE_ANALOG subclass: it is a genuine absolute-stick device.
|
||||
* A PLAIN RETRO_DEVICE_ANALOG deliberately maps to the standard pad in
|
||||
* retro_set_controller_port_device -- users routinely pick "RetroPad
|
||||
* w/ Analog" for stick-to-dpad, and silently swapping the Jaguar pad
|
||||
* for a bank-switching peripheral no released title reads would break
|
||||
* them. Only the explicit subclasses attach it. */
|
||||
#define RETRO_DEVICE_JAG_ANALOG RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_ANALOG, 0)
|
||||
#define RETRO_DEVICE_JAG_DRIVING RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_ANALOG, 1)
|
||||
|
||||
/* Device the frontend explicitly selected via
|
||||
* retro_set_controller_port_device, or INPUTDEV_PAD when it never did.
|
||||
@@ -264,6 +273,16 @@ static int32_t mouse_exponent_q8[2] = { 256, 256 };
|
||||
static int32_t rotary_deadzone = 0;
|
||||
static int32_t rotary_offset = 0;
|
||||
static int32_t rotary_exponent_q8 = 256;
|
||||
/* Analog / driving controller ladder (#437). Shared by both ports like
|
||||
* the rotary's, and in ADC counts (127 = full deflection) rather than
|
||||
* host units per poll -- the tuning runs in the device's own 8-bit
|
||||
* domain, see inputdev_analog_byte(). No sensitivity entry: on an
|
||||
* absolute axis a linear gain only trades range for saturation, and the
|
||||
* response exponent is the control that actually shapes it. */
|
||||
static int32_t analog_deadzone[2] = { 0, 0 };
|
||||
static int32_t analog_offset[2] = { 0, 0 };
|
||||
static int32_t analog_exponent_q8[2] = { 256, 256 };
|
||||
static bool show_analog_options = true;
|
||||
|
||||
/* Has this port actually received non-zero mouse state from the frontend?
|
||||
*
|
||||
@@ -316,6 +335,8 @@ static const char *inputdev_type_name(InputDevType t)
|
||||
case INPUTDEV_MOUSE_AMIGA_ADAPTER: return "Amiga mouse (Amiga adapter)";
|
||||
case INPUTDEV_MOUSE_AMIGA_ON_ST: return "Amiga mouse (ST adapter)";
|
||||
case INPUTDEV_ROTARY: return "Tempest rotary";
|
||||
case INPUTDEV_ANALOG: return "Analog joystick (bank-switching)";
|
||||
case INPUTDEV_DRIVING: return "Driving controller (bank-switching)";
|
||||
default: break;
|
||||
}
|
||||
return "standard joypad";
|
||||
@@ -328,6 +349,11 @@ static bool inputdev_is_mouse_type(InputDevType t)
|
||||
|| t == INPUTDEV_MOUSE_AMIGA_ON_ST);
|
||||
}
|
||||
|
||||
static bool inputdev_is_analog_type(InputDevType t)
|
||||
{
|
||||
return (t == INPUTDEV_ANALOG || t == INPUTDEV_DRIVING);
|
||||
}
|
||||
|
||||
/* Push the sensitivity ladder AND the per-axis tuning (#439) that belong to
|
||||
* whatever device is currently in `port`.
|
||||
*
|
||||
@@ -346,7 +372,19 @@ static bool inputdev_is_mouse_type(InputDevType t)
|
||||
* property: its initialiser has to be a safe value, not a sentinel. */
|
||||
static void apply_port_tuning(int port)
|
||||
{
|
||||
if (InputDevGetType(port) == INPUTDEV_ROTARY)
|
||||
if (inputdev_is_analog_type(InputDevGetType(port)))
|
||||
{
|
||||
/* The analog device has no sensitivity ladder (see the statics),
|
||||
* but the port scale is still pinned to unity so an analog ->
|
||||
* mouse/rotary switch cannot inherit a stale multiplier -- the
|
||||
* same hygiene the rotary branch applies to its unused Y tune. */
|
||||
InputDevSetScale(port, 256);
|
||||
InputDevSetTune(port, INPUTDEV_AXIS_X, analog_deadzone[0],
|
||||
analog_offset[0], analog_exponent_q8[0]);
|
||||
InputDevSetTune(port, INPUTDEV_AXIS_Y, analog_deadzone[1],
|
||||
analog_offset[1], analog_exponent_q8[1]);
|
||||
}
|
||||
else if (InputDevGetType(port) == INPUTDEV_ROTARY)
|
||||
{
|
||||
InputDevSetScale(port, rotary_scale_q8);
|
||||
/* A rotary is one wheel and never reads its Y tune, but both axes are
|
||||
@@ -663,6 +701,35 @@ static bool update_option_visibility(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* Analog / driving tuning (#437) means nothing unless one of the two
|
||||
* types is attached to a port -- same machinery as the rotary gate. */
|
||||
{
|
||||
static const char * const analog_keys[] = {
|
||||
"virtualjaguar_analog_deadzone_x",
|
||||
"virtualjaguar_analog_deadzone_y",
|
||||
"virtualjaguar_analog_offset_x",
|
||||
"virtualjaguar_analog_offset_y",
|
||||
"virtualjaguar_analog_exponent_x",
|
||||
"virtualjaguar_analog_exponent_y",
|
||||
};
|
||||
bool show_analog_prev = show_analog_options;
|
||||
|
||||
show_analog_options = (inputdev_is_analog_type(InputDevGetType(0))
|
||||
|| inputdev_is_analog_type(InputDevGetType(1)));
|
||||
|
||||
if (show_analog_options != show_analog_prev)
|
||||
{
|
||||
option_display.visible = show_analog_options;
|
||||
for (i = 0; i < ARRAY_SIZE(analog_keys); i++)
|
||||
{
|
||||
option_display.key = analog_keys[i];
|
||||
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY,
|
||||
&option_display);
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* The 16bpp preview knob only means anything while texture dump is
|
||||
* enabled (#369) -- same machinery as the mouse/rotary gates above. */
|
||||
{
|
||||
@@ -924,6 +991,10 @@ static InputDevType p2_device_from_option(void)
|
||||
p2 = INPUTDEV_MOUSE_AMIGA_ADAPTER;
|
||||
else if (!strcmp(var.value, "rotary"))
|
||||
p2 = INPUTDEV_ROTARY;
|
||||
else if (!strcmp(var.value, "analog"))
|
||||
p2 = INPUTDEV_ANALOG;
|
||||
else if (!strcmp(var.value, "driving"))
|
||||
p2 = INPUTDEV_DRIVING;
|
||||
/* "pad" and "auto" (with no DB row) both mean pad. */
|
||||
}
|
||||
|
||||
@@ -932,8 +1003,10 @@ static InputDevType p2_device_from_option(void)
|
||||
|
||||
/* Port 1 controller type, same contract as p2_device_from_option().
|
||||
*
|
||||
* Port 1 offers pad or rotary only. It needed no such helper while it was
|
||||
* pad-only -- retro_set_controller_port_device() could hardcode
|
||||
* Port 1 offers pad, rotary, or the analog / driving controller (#437 --
|
||||
* TR10 restricts the bank-switching device to neither socket). It needed
|
||||
* no such helper while it was pad-only -- retro_set_controller_port_device()
|
||||
* could hardcode
|
||||
* INPUTDEV_PAD when the frontend released the port -- but with a rotary
|
||||
* reachable there, that hardcode would re-detach it on a frontend's
|
||||
* routine post-load JOYPAD assignment, which is exactly the bug the port-2
|
||||
@@ -954,6 +1027,10 @@ static InputDevType p1_device_from_option(void)
|
||||
{
|
||||
if (!strcmp(var.value, "rotary"))
|
||||
p1 = INPUTDEV_ROTARY;
|
||||
else if (!strcmp(var.value, "analog"))
|
||||
p1 = INPUTDEV_ANALOG;
|
||||
else if (!strcmp(var.value, "driving"))
|
||||
p1 = INPUTDEV_DRIVING;
|
||||
/* "pad" and "auto" both mean pad. */
|
||||
}
|
||||
|
||||
@@ -1452,6 +1529,15 @@ static void check_variables(void)
|
||||
rotary_offset = read_tune_units("virtualjaguar_rotary_offset");
|
||||
rotary_exponent_q8 = read_tune_exponent("virtualjaguar_rotary_exponent");
|
||||
|
||||
/* Analog / driving ladder (#437); units are ADC counts, see the
|
||||
* statics. AxisTuneSet() owns the bounds, same as the others. */
|
||||
analog_deadzone[0] = read_tune_units("virtualjaguar_analog_deadzone_x");
|
||||
analog_deadzone[1] = read_tune_units("virtualjaguar_analog_deadzone_y");
|
||||
analog_offset[0] = read_tune_units("virtualjaguar_analog_offset_x");
|
||||
analog_offset[1] = read_tune_units("virtualjaguar_analog_offset_y");
|
||||
analog_exponent_q8[0] = read_tune_exponent("virtualjaguar_analog_exponent_x");
|
||||
analog_exponent_q8[1] = read_tune_exponent("virtualjaguar_analog_exponent_y");
|
||||
|
||||
/* One scale and one tuning set per port, picked by what is actually
|
||||
* plugged into it -- the two ladders are separate options because a
|
||||
* spinner and a mouse want very different multipliers. */
|
||||
@@ -1758,6 +1844,89 @@ static void update_input(void)
|
||||
if (t == INPUTDEV_PAD)
|
||||
continue;
|
||||
|
||||
/* Analog / driving controller (#437): an ABSOLUTE device fed
|
||||
* from RETRO_DEVICE_ANALOG, not the relative mouse path below.
|
||||
*
|
||||
* LIVENESS: same rule as the mouse, adapted to an absolute
|
||||
* source. A centred stick is indistinguishable from "no analog
|
||||
* routed at all", so the port keeps its RetroPad -- and the
|
||||
* device stays entirely inert, controller-type probes included
|
||||
* (inputdev.h, ENGAGEMENT) -- until a deflection past the
|
||||
* existing stick-to-dpad threshold proves the frontend is
|
||||
* routing analog state. Button presses deliberately do NOT
|
||||
* count: they are ambiguous with pad presses. Once live, the
|
||||
* whole pad is suppressed (the physical device has no keypad /
|
||||
* Pause / Option -- TR10 gives it A-D, a hat and two axes). */
|
||||
if (inputdev_is_analog_type(t))
|
||||
{
|
||||
int32_t ax, ay;
|
||||
uint32_t sw = 0;
|
||||
|
||||
ax = (int32_t)input_state_cb(player, RETRO_DEVICE_ANALOG,
|
||||
RETRO_DEVICE_INDEX_ANALOG_LEFT,
|
||||
RETRO_DEVICE_ID_ANALOG_X);
|
||||
ay = (int32_t)input_state_cb(player, RETRO_DEVICE_ANALOG,
|
||||
RETRO_DEVICE_INDEX_ANALOG_LEFT,
|
||||
RETRO_DEVICE_ID_ANALOG_Y);
|
||||
|
||||
if (t == INPUTDEV_DRIVING)
|
||||
{
|
||||
/* Driving skin: steering on stick X; accelerator / brake
|
||||
* on the R2 / L2 analog triggers when the frontend
|
||||
* reports them (0..32767 each), stick Y otherwise. The
|
||||
* feed convention is +y DOWN (host), which the device
|
||||
* flips to TR10's +accelerator, so accel must arrive
|
||||
* negative here. */
|
||||
int32_t r2 = (int32_t)input_state_cb(player,
|
||||
RETRO_DEVICE_ANALOG,
|
||||
RETRO_DEVICE_INDEX_ANALOG_BUTTON,
|
||||
RETRO_DEVICE_ID_JOYPAD_R2);
|
||||
int32_t l2 = (int32_t)input_state_cb(player,
|
||||
RETRO_DEVICE_ANALOG,
|
||||
RETRO_DEVICE_INDEX_ANALOG_BUTTON,
|
||||
RETRO_DEVICE_ID_JOYPAD_L2);
|
||||
|
||||
if (r2 || l2)
|
||||
ay = l2 - r2;
|
||||
}
|
||||
|
||||
if (!inputdev_live[player]
|
||||
&& (ax > ANALOG_THRESHOLD || ax < -ANALOG_THRESHOLD
|
||||
|| ay > ANALOG_THRESHOLD || ay < -ANALOG_THRESHOLD))
|
||||
{
|
||||
inputdev_live[player] = true;
|
||||
LOG_INF("[input] port %d: analog controller is live, "
|
||||
"RetroPad released\n", player + 1);
|
||||
}
|
||||
|
||||
if (inputdev_live[player])
|
||||
{
|
||||
/* A/B/C from the same RetroPad slots the pad maps them to
|
||||
* (A/B/Y); D -- TR10's fourth button -- on X. Hat (gear
|
||||
* shift Up/Down on the driving skin) from the d-pad. */
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_A))
|
||||
sw |= INPUTDEV_SW_A;
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_B))
|
||||
sw |= INPUTDEV_SW_B;
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_Y))
|
||||
sw |= INPUTDEV_SW_C;
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_X))
|
||||
sw |= INPUTDEV_SW_D;
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_UP))
|
||||
sw |= INPUTDEV_SW_UP;
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_DOWN))
|
||||
sw |= INPUTDEV_SW_DOWN;
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_LEFT))
|
||||
sw |= INPUTDEV_SW_LEFT;
|
||||
if (ret[player] & (1 << RETRO_DEVICE_ID_JOYPAD_RIGHT))
|
||||
sw |= INPUTDEV_SW_RIGHT;
|
||||
|
||||
memset(joypad_buttons[player], 0x00, BUTTON_LAST + 1);
|
||||
InputDevFeedAnalog((int)player, ax, ay, sw);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
dx = (int32_t)input_state_cb(player, RETRO_DEVICE_MOUSE, 0,
|
||||
RETRO_DEVICE_ID_MOUSE_X);
|
||||
dy = 0;
|
||||
@@ -1887,6 +2056,14 @@ void retro_set_controller_port_device(unsigned port, unsigned device)
|
||||
case RETRO_DEVICE_JAG_ROTARY:
|
||||
type = INPUTDEV_ROTARY;
|
||||
break;
|
||||
case RETRO_DEVICE_JAG_ANALOG:
|
||||
type = INPUTDEV_ANALOG;
|
||||
break;
|
||||
case RETRO_DEVICE_JAG_DRIVING:
|
||||
type = INPUTDEV_DRIVING;
|
||||
break;
|
||||
/* A plain RETRO_DEVICE_ANALOG falls through to the pad on purpose
|
||||
* -- see the subclass definitions. */
|
||||
default:
|
||||
type = INPUTDEV_PAD;
|
||||
break;
|
||||
@@ -3230,6 +3407,8 @@ void retro_init(void)
|
||||
static const struct retro_controller_description port1_devices[] = {
|
||||
{ "Standard Joypad", RETRO_DEVICE_JAG_PAD },
|
||||
{ "Rotary (Tempest)", RETRO_DEVICE_JAG_ROTARY },
|
||||
{ "Analog Joystick (bank-switching)", RETRO_DEVICE_JAG_ANALOG },
|
||||
{ "Driving Controller (bank-switching)", RETRO_DEVICE_JAG_DRIVING },
|
||||
};
|
||||
static const struct retro_controller_description port2_devices[] = {
|
||||
{ "Standard Joypad", RETRO_DEVICE_JAG_PAD },
|
||||
@@ -3237,10 +3416,12 @@ void retro_init(void)
|
||||
{ "Amiga Mouse (ST adapter)", RETRO_DEVICE_JAG_MOUSE_AMIGA },
|
||||
{ "Amiga Mouse (Amiga adapter)", RETRO_DEVICE_JAG_MOUSE_AMIGA_AD },
|
||||
{ "Rotary (Tempest)", RETRO_DEVICE_JAG_ROTARY },
|
||||
{ "Analog Joystick (bank-switching)", RETRO_DEVICE_JAG_ANALOG },
|
||||
{ "Driving Controller (bank-switching)", RETRO_DEVICE_JAG_DRIVING },
|
||||
};
|
||||
static const struct retro_controller_info ports[] = {
|
||||
{ port1_devices, 2 },
|
||||
{ port2_devices, 5 },
|
||||
{ port1_devices, 4 },
|
||||
{ port2_devices, 7 },
|
||||
{ NULL, 0 },
|
||||
};
|
||||
|
||||
@@ -3314,6 +3495,13 @@ void retro_deinit(void)
|
||||
rotary_deadzone = 0;
|
||||
rotary_offset = 0;
|
||||
rotary_exponent_q8 = 256;
|
||||
analog_deadzone[0] = 0;
|
||||
analog_deadzone[1] = 0;
|
||||
analog_offset[0] = 0;
|
||||
analog_offset[1] = 0;
|
||||
analog_exponent_q8[0] = 256;
|
||||
analog_exponent_q8[1] = 256;
|
||||
show_analog_options = true;
|
||||
headroom_logged = false;
|
||||
video_buffer_alloc_pixels = VIDEO_BUFFER_PIXELS;
|
||||
hires_restart_notice_logged = 0;
|
||||
|
||||
+132
-2
@@ -543,7 +543,7 @@ struct retro_core_option_v2_definition option_defs_us[] = {
|
||||
"virtualjaguar_p2_device",
|
||||
"Port 2 > Controller Type",
|
||||
"Controller Type",
|
||||
"Which peripheral is plugged into controller port 2. 'Atari ST / PS2 Mouse' is the wiring used by the AtariAge and Brewing Academy ST adapters and by PS/2 mouse adapters. 'Amiga Mouse (ST adapter)' is an Amiga mouse plugged into an ST-wired adapter -- this is what an in-game 'Atari / Amiga' selector normally chooses between. 'Amiga Mouse (Amiga adapter)' is the rarer dedicated adapter. A mouse asserts its state in every row scan, exactly as the real row-blind adapter does, so the port-2 RetroPad is disconnected while one is selected. 'Rotary (Tempest)' is the Tempest spinner: it removes Up and Down and reports wheel rotation on Left/Right instead, and is driven by relative mouse X. Its buttons stay on the RetroPad. Tempest 2000 hides its rotary support behind an unlock -- from SELECT GAME TYPE TO PLAY press Option on controller 1, then press Pause on BOTH controllers at once to reveal CONTROLLER TYPE. The unlock is saved to the game's EEPROM, so it is only needed once.",
|
||||
"Which peripheral is plugged into controller port 2. 'Atari ST / PS2 Mouse' is the wiring used by the AtariAge and Brewing Academy ST adapters and by PS/2 mouse adapters. 'Amiga Mouse (ST adapter)' is an Amiga mouse plugged into an ST-wired adapter -- this is what an in-game 'Atari / Amiga' selector normally chooses between. 'Amiga Mouse (Amiga adapter)' is the rarer dedicated adapter. A mouse asserts its state in every row scan, exactly as the real row-blind adapter does, so the port-2 RetroPad is disconnected while one is selected. 'Rotary (Tempest)' is the Tempest spinner: it removes Up and Down and reports wheel rotation on Left/Right instead, and is driven by relative mouse X. Its buttons stay on the RetroPad. Tempest 2000 hides its rotary support behind an unlock -- from SELECT GAME TYPE TO PLAY press Option on controller 1, then press Pause on BOTH controllers at once to reveal CONTROLLER TYPE. The unlock is saved to the game's EEPROM, so it is only needed once. 'Analog Joystick' and 'Driving Controller' are Atari's bank-switching analog device (one protocol, two skins) -- NO RELEASED TITLE reads it, so these exist for homebrew. Driven by the left analog stick (the driving skin also takes the L2/R2 triggers as brake/accelerator); the port stays a RetroPad until the stick actually moves, so a game that probes controller types at boot only sees the analog device if the stick is deflected first.",
|
||||
NULL,
|
||||
"input_p2",
|
||||
{
|
||||
@@ -553,6 +553,8 @@ struct retro_core_option_v2_definition option_defs_us[] = {
|
||||
{ "mouse_amiga", "Amiga Mouse (ST adapter)" },
|
||||
{ "mouse_amiga_adapter", "Amiga Mouse (Amiga adapter)" },
|
||||
{ "rotary", "Rotary (Tempest)" },
|
||||
{ "analog", "Analog Joystick (bank-switching)" },
|
||||
{ "driving", "Driving Controller (bank-switching)" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"auto"
|
||||
@@ -561,13 +563,15 @@ struct retro_core_option_v2_definition option_defs_us[] = {
|
||||
"virtualjaguar_p1_device",
|
||||
"Port 1 > Controller Type",
|
||||
"Controller Type",
|
||||
"Which peripheral is plugged into controller port 1. 'Rotary (Tempest)' is the Tempest spinner: it removes Up and Down and reports wheel rotation on Left/Right instead, and is driven by relative mouse X. Its buttons (A, B, C, Option, Pause and the keypad) stay on the RetroPad, which is what a real rotary has. There is no per-title default for this option and there never will be -- selecting a rotary removes Up and Down, so it would break menu navigation for anyone using a pad. Tempest 2000 hides its rotary support behind an unlock -- from SELECT GAME TYPE TO PLAY press Option on controller 1, then press Pause on BOTH controllers at once to reveal CONTROLLER TYPE. The unlock is saved to the game's EEPROM, so it is only needed once.",
|
||||
"Which peripheral is plugged into controller port 1. 'Rotary (Tempest)' is the Tempest spinner: it removes Up and Down and reports wheel rotation on Left/Right instead, and is driven by relative mouse X. Its buttons (A, B, C, Option, Pause and the keypad) stay on the RetroPad, which is what a real rotary has. There is no per-title default for this option and there never will be -- selecting a rotary removes Up and Down, so it would break menu navigation for anyone using a pad. Tempest 2000 hides its rotary support behind an unlock -- from SELECT GAME TYPE TO PLAY press Option on controller 1, then press Pause on BOTH controllers at once to reveal CONTROLLER TYPE. The unlock is saved to the game's EEPROM, so it is only needed once. 'Analog Joystick' and 'Driving Controller' are Atari's bank-switching analog device (one protocol, two skins) -- NO RELEASED TITLE reads it, so these exist for homebrew. Driven by the left analog stick (the driving skin also takes the L2/R2 triggers as brake/accelerator); the port stays a RetroPad until the stick actually moves, so a game that probes controller types at boot only sees the analog device if the stick is deflected first.",
|
||||
NULL,
|
||||
"input_p1",
|
||||
{
|
||||
{ "auto", "Auto (per-title default)" },
|
||||
{ "pad", "Standard Joypad" },
|
||||
{ "rotary", "Rotary (Tempest)" },
|
||||
{ "analog", "Analog Joystick (bank-switching)" },
|
||||
{ "driving", "Driving Controller (bank-switching)" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"auto"
|
||||
@@ -674,6 +678,132 @@ struct retro_core_option_v2_definition option_defs_us[] = {
|
||||
},
|
||||
"100"
|
||||
},
|
||||
/* Analog / driving controller tuning (#437). SHARED BY BOTH PORTS,
|
||||
* like the rotary ladder, and applied through the same shared layer
|
||||
* (src/jerry/axistune.c) -- but on an ABSOLUTE axis, so the units are
|
||||
* ADC counts (127 = full stick deflection, the device's own 8-bit
|
||||
* domain) and the dead zone RE-BASES instead of gating: positions
|
||||
* inside it read as centred, the edge maps smoothly to centre, and
|
||||
* full deflection still reads full scale (see axistune.h, "THE
|
||||
* ABSOLUTE-AXIS ANSWER"). Defaults are the exact identity. */
|
||||
{
|
||||
"virtualjaguar_analog_deadzone_x",
|
||||
"Analog Controller Dead Zone (X)",
|
||||
"Analog Dead Zone (X)",
|
||||
"Stick positions within this many ADC counts of centre (127 = full deflection) read as exactly centred. The rest of the travel is rescaled so the response is smooth at the edge and full deflection still reads full scale.",
|
||||
NULL,
|
||||
"input",
|
||||
{
|
||||
{ "0", "Off" },
|
||||
{ "4", "4 counts (~3%)" },
|
||||
{ "8", "8 counts (~6%)" },
|
||||
{ "12", "12 counts (~9%)" },
|
||||
{ "16", "16 counts (~13%)" },
|
||||
{ "24", "24 counts (~19%)" },
|
||||
{ "32", "32 counts (~25%)" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"0"
|
||||
},
|
||||
{
|
||||
"virtualjaguar_analog_deadzone_y",
|
||||
"Analog Controller Dead Zone (Y)",
|
||||
"Analog Dead Zone (Y)",
|
||||
"As Analog Controller Dead Zone (X), for the Y axis (pitch, or accelerator/brake on the driving controller).",
|
||||
NULL,
|
||||
"input",
|
||||
{
|
||||
{ "0", "Off" },
|
||||
{ "4", "4 counts (~3%)" },
|
||||
{ "8", "8 counts (~6%)" },
|
||||
{ "12", "12 counts (~9%)" },
|
||||
{ "16", "16 counts (~13%)" },
|
||||
{ "24", "24 counts (~19%)" },
|
||||
{ "32", "32 counts (~25%)" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"0"
|
||||
},
|
||||
{
|
||||
"virtualjaguar_analog_offset_x",
|
||||
"Analog Controller Offset (X)",
|
||||
"Analog Offset (X)",
|
||||
"Subtracts a constant (in ADC counts) from every X sample, in host orientation before any device convention. Cancels a stick that rests off-centre; a centred stick is moved by it, which is the point.",
|
||||
NULL,
|
||||
"input",
|
||||
{
|
||||
{ "-16", "-16" },
|
||||
{ "-8", "-8" },
|
||||
{ "-4", "-4" },
|
||||
{ "-2", "-2" },
|
||||
{ "0", "Off" },
|
||||
{ "2", "+2" },
|
||||
{ "4", "+4" },
|
||||
{ "8", "+8" },
|
||||
{ "16", "+16" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"0"
|
||||
},
|
||||
{
|
||||
"virtualjaguar_analog_offset_y",
|
||||
"Analog Controller Offset (Y)",
|
||||
"Analog Offset (Y)",
|
||||
"As Analog Controller Offset (X), for the Y axis.",
|
||||
NULL,
|
||||
"input",
|
||||
{
|
||||
{ "-16", "-16" },
|
||||
{ "-8", "-8" },
|
||||
{ "-4", "-4" },
|
||||
{ "-2", "-2" },
|
||||
{ "0", "Off" },
|
||||
{ "2", "+2" },
|
||||
{ "4", "+4" },
|
||||
{ "8", "+8" },
|
||||
{ "16", "+16" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"0"
|
||||
},
|
||||
{
|
||||
"virtualjaguar_analog_exponent_x",
|
||||
"Analog Controller Response Curve (X)",
|
||||
"Analog Response Curve (X)",
|
||||
"Response exponent for the X axis, anchored at full deflection: an exponent above 1.00 gives finer control near centre while full deflection still reads full scale. Unlike the mouse/rotary curves this costs no top speed, so there is no paired sensitivity control.",
|
||||
NULL,
|
||||
"input",
|
||||
{
|
||||
{ "100", "Linear (1.00)" },
|
||||
{ "125", "1.25" },
|
||||
{ "150", "1.50" },
|
||||
{ "175", "1.75" },
|
||||
{ "200", "2.00" },
|
||||
{ "250", "2.50" },
|
||||
{ "300", "3.00" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"100"
|
||||
},
|
||||
{
|
||||
"virtualjaguar_analog_exponent_y",
|
||||
"Analog Controller Response Curve (Y)",
|
||||
"Analog Response Curve (Y)",
|
||||
"As Analog Controller Response Curve (X), for the Y axis.",
|
||||
NULL,
|
||||
"input",
|
||||
{
|
||||
{ "100", "Linear (1.00)" },
|
||||
{ "125", "1.25" },
|
||||
{ "150", "1.50" },
|
||||
{ "175", "1.75" },
|
||||
{ "200", "2.00" },
|
||||
{ "250", "2.50" },
|
||||
{ "300", "3.00" },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"100"
|
||||
},
|
||||
{
|
||||
"virtualjaguar_mouse_sensitivity",
|
||||
"Port 2 > Mouse Sensitivity",
|
||||
|
||||
+6
-1
@@ -53,7 +53,12 @@ extern "C" {
|
||||
* quadrature accumulator and phase are machine-visible — the phase IS
|
||||
* what the game reads at $F14000 — so a state restored without them
|
||||
* replays different motion. Older states load with the encoders
|
||||
* reset, which is what a pre-v12 core was. develop only. */
|
||||
* reset, which is what a pre-v12 core was. develop only.
|
||||
* EXTENDED IN PLACE (still v12, develop only, one bump per release):
|
||||
* the analog / driving controller (#437) appended 5 bytes per port —
|
||||
* bank, last row, latched ADC X/Y, switch mask — to the chunk; a
|
||||
* pre-extension v12 state reads them from the zero-fill tail, which
|
||||
* is inert (see inputdev.h). */
|
||||
#define STATE_MAGIC 0x564A5353 /* "VJSS" */
|
||||
#define STATE_VERSION 12
|
||||
/* Oldest layout retro_unserialize still accepts. States between
|
||||
|
||||
@@ -243,3 +243,60 @@ int32_t AxisTuneApply(const axis_tune *t, int32_t raw, int32_t *gain_q8)
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
int32_t AxisTuneApplyAbs(const axis_tune *t, int32_t raw, int32_t range)
|
||||
{
|
||||
int32_t v, mag, neg, out;
|
||||
uint32_t n_q16, p_q16;
|
||||
|
||||
if (range <= 0)
|
||||
return 0;
|
||||
|
||||
/* Same structural identity guarantee as the relative path: defaults
|
||||
* return the argument before any arithmetic (clamped to the domain the
|
||||
* caller declared, which the identity cannot widen). */
|
||||
if (AxisTuneIsIdentity(t))
|
||||
{
|
||||
if (raw > range) return range;
|
||||
if (raw < -range) return -range;
|
||||
return raw;
|
||||
}
|
||||
|
||||
/* NO raw == 0 early-out here -- on an absolute axis 0 is a sample (the
|
||||
* stick at rest), not "no sample", and a non-zero offset must move it.
|
||||
* See "THE ABSOLUTE-AXIS ANSWER" in axistune.h. */
|
||||
v = raw - t->offset;
|
||||
|
||||
if (v > range) v = range;
|
||||
if (v < -range) v = -range;
|
||||
|
||||
neg = (v < 0);
|
||||
mag = neg ? -v : v;
|
||||
|
||||
/* RE-BASE, not gate: the dead-zone edge maps to centre and full
|
||||
* deflection still maps to full scale, so the response is continuous
|
||||
* where a gate would step. dz >= range means the whole travel is
|
||||
* inside the zone. */
|
||||
if (mag <= t->deadzone)
|
||||
return 0;
|
||||
if (t->deadzone >= range)
|
||||
return 0;
|
||||
if (t->deadzone > 0)
|
||||
mag = (int32_t)(((int64_t)(mag - t->deadzone) * range)
|
||||
/ (range - t->deadzone));
|
||||
|
||||
/* Curve anchored at `range`: out = range * (mag/range)^e, so the
|
||||
* centre and full deflection are fixed points for every exponent. */
|
||||
if (t->exponent_q8 != 256 && mag < range)
|
||||
{
|
||||
n_q16 = ((uint32_t)mag << 16) / (uint32_t)range;
|
||||
p_q16 = axistune_pow_q16(n_q16, (uint32_t)t->exponent_q8);
|
||||
mag = (int32_t)((((uint32_t)range * p_q16) + 32768u) >> 16);
|
||||
}
|
||||
|
||||
if (mag > range)
|
||||
mag = range;
|
||||
|
||||
out = neg ? -mag : mag;
|
||||
return out;
|
||||
}
|
||||
+42
-4
@@ -32,10 +32,38 @@
|
||||
* gate-only: below the threshold the sample is dropped, above it the
|
||||
* sample passes at full magnitude.
|
||||
*
|
||||
* An absolute-axis consumer (#437) will have to answer the re-basing
|
||||
* question for itself. It is deliberately NOT pre-answered here with an
|
||||
* unused flag: one caller does not justify a mode switch, and the shape of
|
||||
* that answer belongs to the ticket that has a device to test it against.
|
||||
* THE ABSOLUTE-AXIS ANSWER (#437), now that the consumer exists
|
||||
* =============================================================
|
||||
* The analog / driving controller (TR10 bank-switching, inputdev.c) is an
|
||||
* ABSOLUTE consumer: the quantity is a stick POSITION, not a speed, and
|
||||
* the answer deferred above is now decided the other way around --
|
||||
* AxisTuneApplyAbs() RE-BASES its dead zone. The drift argument that
|
||||
* forbade re-basing on a relative axis does not exist here: nothing
|
||||
* integrates a position, so subtracting the dead-zone edge cannot walk a
|
||||
* pointer away from the hand. What a GATE would cost on a position is a
|
||||
* step: as the stick crosses the edge the output would jump from centre
|
||||
* straight to the edge value, which a game turns into a steering snap.
|
||||
* So the surviving magnitude is remapped (mag - dz) * range / (range -
|
||||
* dz): continuous at the edge, and full deflection still reads full
|
||||
* scale.
|
||||
*
|
||||
* THE IDENTITY RULE DIFFERS TOO, deliberately. On the relative path
|
||||
* raw == 0 means "no sample this poll" and must stay 0 under every tune
|
||||
* (the #474 lesson, spelled out in AxisTuneApply). On an absolute axis
|
||||
* raw == 0 IS a sample -- the stick resting at centre -- so the rule
|
||||
* becomes: dead zone and exponent are magnitude-symmetric and must FIX
|
||||
* the centre (raw 0 -> out 0 for every dz / e), and OFFSET ALONE may move
|
||||
* it, because cancelling a mis-centred rest position is offset's entire
|
||||
* job. There is consequently no raw == 0 early-out in the absolute
|
||||
* path: with a non-zero offset, a centred stick must read -offset.
|
||||
*
|
||||
* The exponent's reference is the caller's `range` (full deflection),
|
||||
* not AXISTUNE_REF: an absolute axis has a natural full scale and the
|
||||
* curve's fixed point belongs there, so full deflection is never
|
||||
* attenuated and e > 1 buys finer control near centre. The curve comes
|
||||
* back folded into the returned position rather than as a gain -- there
|
||||
* is no QuadFeed carry downstream to keep a fraction alive, and a
|
||||
* position quantised to the device's own 8-bit ADC step loses nothing.
|
||||
*
|
||||
* OFFSET IS A PER-SAMPLE BIAS, subtracted before the gate. A real mouse
|
||||
* reports exactly zero at rest and is unaffected by any offset. The case
|
||||
@@ -154,6 +182,16 @@ int AxisTuneIsIdentity(const axis_tune *t);
|
||||
* the returned delta. */
|
||||
int32_t AxisTuneApply(const axis_tune *t, int32_t raw, int32_t *gain_q8);
|
||||
|
||||
/* Absolute-axis variant (#437) -- see "THE ABSOLUTE-AXIS ANSWER" above.
|
||||
* `raw` is a position in [-range, +range] with 0 at centre; the return is
|
||||
* the tuned position in the same domain, clamped. Offset is subtracted
|
||||
* first (host orientation -- the caller applies any device-specific
|
||||
* inversion to the RESULT, as inputdev_feed_axis does on the relative
|
||||
* path); the dead zone re-bases; the exponent's fixed point is `range`.
|
||||
* dz >= range degenerates to "everything gated", never a divide by
|
||||
* zero. */
|
||||
int32_t AxisTuneApplyAbs(const axis_tune *t, int32_t raw, int32_t range);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+270
-4
@@ -64,6 +64,18 @@ typedef struct
|
||||
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];
|
||||
@@ -81,6 +93,13 @@ static int inputdev_is_mouse(InputDevType t)
|
||||
|| 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
|
||||
@@ -97,6 +116,15 @@ static void inputdev_reset_dynamic(void)
|
||||
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;
|
||||
@@ -150,14 +178,15 @@ void InputDevSetType(int port, InputDevType type)
|
||||
if (port < 0 || port > 1)
|
||||
return;
|
||||
|
||||
/* Mouse is port 2 only (see inputdev.h); the rotary is valid on both.
|
||||
* Anything else is a plain pad, i.e. not attached. */
|
||||
/* 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)
|
||||
else if (type != INPUTDEV_ROTARY && !inputdev_is_analog(type))
|
||||
type = INPUTDEV_PAD;
|
||||
|
||||
if (inputdev_ports[port].type != type)
|
||||
@@ -167,6 +196,15 @@ void InputDevSetType(int port, InputDevType type)
|
||||
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
|
||||
@@ -313,6 +351,54 @@ void InputDevFeed(int port, int32_t dx, int32_t dy, uint32_t buttons)
|
||||
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)
|
||||
@@ -320,6 +406,46 @@ void InputDevArm(void)
|
||||
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
|
||||
@@ -431,15 +557,68 @@ void InputDevClock(uint8_t row0, uint8_t row1)
|
||||
inputdev_armed = 0;
|
||||
}
|
||||
|
||||
uint16_t InputDevOverlayF14000(uint16_t data)
|
||||
/* 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;
|
||||
@@ -488,6 +667,62 @@ uint16_t InputDevOverlayF14002(uint16_t data, uint8_t row0, uint8_t row1)
|
||||
&& 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))
|
||||
@@ -527,6 +762,19 @@ size_t InputDevStateSave(uint8_t *buf)
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -572,5 +820,23 @@ size_t InputDevStateLoad(const uint8_t *buf)
|
||||
|
||||
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);
|
||||
}
|
||||
+126
-4
@@ -43,6 +43,84 @@
|
||||
* mouse's row-blind overlay. Two peripherals, one encoder, two entirely
|
||||
* different assertion mechanisms.
|
||||
*
|
||||
* THE ANALOG / DRIVING CONTROLLER IS A BANK-SWITCHING MATRIX DEVICE (#437)
|
||||
* ========================================================================
|
||||
* TR10 sections "Analogue Joystick and 'Driving' Controllers",
|
||||
* "Identifying Controller Types" and "Reading Bank Switching Controllers"
|
||||
* are the complete wire spec, and the first surprise is that there is no
|
||||
* console-side ADC in it AT ALL: "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 a
|
||||
* microcontroller (Atari's prototype used a 68HC05P9) that digitises the
|
||||
* pots itself and answers the ordinary $F14000/$F14002 row scan with
|
||||
* data bits instead of switch closures. So this device, like the rotary,
|
||||
* is row-select-driven matrix synthesis -- no new bus plumbing.
|
||||
*
|
||||
* The layout, per TR10's Bank 0 / Bank 1 tables (port 1 columns are
|
||||
* B0/B1/J8..J11, port 2 columns B2/B3/J12..J15; J8..J15 are $F14000 bits
|
||||
* 8..15, B0..B3 are $F14002 bits 0..3):
|
||||
*
|
||||
* Bank 0 C column B column J+0 J+1 J+2 J+3
|
||||
* Row 0 0 (bank id) button A X0 X1 X2 X3
|
||||
* Row 1 C1 = 1 button B X4 X5 X6 X7
|
||||
* Row 2 (C table) button C Y0 Y1 Y2 Y3
|
||||
* Row 3 (C table) button D Y4 Y5 Y6 Y7
|
||||
*
|
||||
* Bank 1 C column B column J+0 J+1 J+2 J+3
|
||||
* Row 0 1 (bank id) 1 Up Down Left Right
|
||||
* Rows 1-3 (C table) 1 1 1 1 1
|
||||
*
|
||||
* X/Y are the 8-bit ADC values presented as logic levels (NOT active-low
|
||||
* switches); the buttons and the hat ARE switches and read active low.
|
||||
* C2 C3 = 0 1 identifies "Bank Switching" (see the rotary's C-row table
|
||||
* below for which row carries which C bit on which port), and the B-column
|
||||
* bits of the last bank -- all 1s across rows 3..0 -- identify "Analogue
|
||||
* Joystick or Driving Controller" among the bank-switching types.
|
||||
*
|
||||
* THE BANK SWITCHES ON THE ROW SELECT, NOT ON READS: "Bank switching is
|
||||
* done automatically when the controller sees a transition from row 3 to
|
||||
* row 0 (of the same controller socket)." The row select is a latched
|
||||
* output the 68K WRITES, so the device clocks its bank from
|
||||
* InputDevRowSelect() (called on every $F14000 write), not from
|
||||
* InputDevClock(). A nibble that is not a socket-0 row code (or a write
|
||||
* with the output enable bit clear) breaks the row-3 -> row-0 adjacency,
|
||||
* exactly as the physical lines leaving the row-code space would.
|
||||
*
|
||||
* TWO DELIBERATE SIMPLIFICATIONS, both supersets of the spec:
|
||||
* 1. TR10 requires ~25us of settling per row and ~300us per bank
|
||||
* change, and tells games to delay before reading. The emulated
|
||||
* controller answers instantly; a compliant driver's delay loop just
|
||||
* spins over valid data.
|
||||
* 2. Real controllers have arbitrary centres and ranges (TR10's example
|
||||
* driving controller reads 160 centred / 245 hard right / 75 hard
|
||||
* left, drifting with temperature) and games MUST calibrate. The
|
||||
* emulated device is ideal -- 128 centred, symmetric -- which every
|
||||
* calibration routine handles trivially.
|
||||
*
|
||||
* NO RELEASED TITLE READS THIS PROTOCOL (research for #437: Atari never
|
||||
* shipped the controller; the one known consumer of console-side analog
|
||||
* input, JANALOG.ABS, targets the deleted early-board ADC instead). The
|
||||
* device exists for homebrew and for parity with BigPEmu's analog /
|
||||
* driving types, and its verification is therefore the synthetic
|
||||
* register-level suite in test/tools/analog_decode_test.c, clearly
|
||||
* labelled as such.
|
||||
*
|
||||
* ANALOG vs DRIVING: identical on the wire -- TR10 defines ONE protocol
|
||||
* and two interpretations (stick: X=roll +right, Y=pitch +forward;
|
||||
* driving: X=steering +right, Y=accelerator +/brake -). The two
|
||||
* InputDevTypes differ only in how libretro.c sources host values.
|
||||
*
|
||||
* ENGAGEMENT (the liveness guardrail, core side). A configured analog
|
||||
* device must not steal the port from a pad the frontend is actually
|
||||
* routing (same rule as the mouse's inputdev_live). Until the first
|
||||
* InputDevFeedAnalog() call the device drives NOTHING -- overlays and
|
||||
* bank clock inert, the port bit-identical to a pad -- and libretro.c
|
||||
* only starts feeding once the stick has proven live. Consequence worth
|
||||
* stating: a title that probes controller types once at boot sees a
|
||||
* standard pad unless the stick moves first; deflect the stick during
|
||||
* boot (or before the title's controller menu) to be identified. Like
|
||||
* inputdev_live, engagement is host-routing-derived and NOT serialized.
|
||||
*
|
||||
* PORT SCOPE
|
||||
* ==========
|
||||
* The mouse is a PORT 2 device only. The adapter is vendor-documented as
|
||||
@@ -77,7 +155,9 @@ typedef enum
|
||||
INPUTDEV_MOUSE_ST, /* case 1: ST adapter + ST mouse (also PS/2) */
|
||||
INPUTDEV_MOUSE_AMIGA_ADAPTER, /* case 2: dedicated Amiga adapter + Amiga */
|
||||
INPUTDEV_MOUSE_AMIGA_ON_ST, /* case 3: Amiga mouse in an ST-wired adapter */
|
||||
INPUTDEV_ROTARY /* TR10 "Tempest" rotary (#436) */
|
||||
INPUTDEV_ROTARY, /* TR10 "Tempest" rotary (#436) */
|
||||
INPUTDEV_ANALOG, /* TR10 bank-switching analogue stick (#437) */
|
||||
INPUTDEV_DRIVING /* same wire protocol, driving skin (#437) */
|
||||
} InputDevType;
|
||||
|
||||
void InputDevInit(void);
|
||||
@@ -95,6 +175,24 @@ int InputDevAnyAttached(void); /* 0 => bit-identical to pad-only */
|
||||
#define INPUTDEV_BTN_RIGHT 0x02
|
||||
void InputDevFeed(int port, int32_t dx, int32_t dy, uint32_t buttons);
|
||||
|
||||
/* Analog / driving feed (#437). x and y are ABSOLUTE positions in the
|
||||
* libretro int16 convention (-32768..32767, +x right, +y DOWN); the
|
||||
* device applies the per-axis tuning in host orientation and THEN flips Y
|
||||
* to TR10's +forward/+accelerator -- same offset-before-inversion order
|
||||
* the relative path uses, and for the same #474 reason. `switches` is a
|
||||
* mask of INPUTDEV_SW_*; the hat doubles as the driving controller's
|
||||
* gear shift (Up/Down) and Spare 1/2 per TR10. First call marks the
|
||||
* device ENGAGED (see the header comment). */
|
||||
#define INPUTDEV_SW_A 0x01
|
||||
#define INPUTDEV_SW_B 0x02
|
||||
#define INPUTDEV_SW_C 0x04
|
||||
#define INPUTDEV_SW_D 0x08
|
||||
#define INPUTDEV_SW_UP 0x10
|
||||
#define INPUTDEV_SW_DOWN 0x20
|
||||
#define INPUTDEV_SW_LEFT 0x40
|
||||
#define INPUTDEV_SW_RIGHT 0x80
|
||||
void InputDevFeedAnalog(int port, int32_t x, int32_t y, uint32_t switches);
|
||||
|
||||
/* Sensitivity, Q8 (256 == 1.0), from the core options. */
|
||||
void InputDevSetScale(int port, int32_t scale_q8);
|
||||
|
||||
@@ -227,8 +325,18 @@ void InputDevSetRotaryID(int port, int reports_rotary);
|
||||
* cannot distinguish "port 1 row 0" from "port 1 not addressed" (that
|
||||
* port's row-0 mask is 0xFFFF, i.e. it clears nothing). */
|
||||
void InputDevArm(void); /* $F14000 write */
|
||||
/* Row-select change notification (#437), called from JoystickWriteWord on
|
||||
* every offset-0 write with the DECODED socket-0 row per port (0-3, or
|
||||
* 0xFF when that nibble is not a socket-0 code or the output enable bit
|
||||
* is clear). This is the analog controller's bank clock -- TR10: banks
|
||||
* switch on the row-3 -> row-0 transition of the device's own socket --
|
||||
* and a no-op for every other device type. */
|
||||
void InputDevRowSelect(uint8_t row0, uint8_t row1);
|
||||
void InputDevClock(uint8_t row0, uint8_t row1);/* $F14000 read, offset 0 */
|
||||
uint16_t InputDevOverlayF14000(uint16_t data);
|
||||
/* row0/row1: decoded socket-0 row per port, as in InputDevOverlayF14002.
|
||||
* The mouse ignores them (row-blind); the analog controller (#437) needs
|
||||
* them because its J-line nibble is a different slice of X/Y per row. */
|
||||
uint16_t InputDevOverlayF14000(uint16_t data, uint8_t row0, uint8_t row1);
|
||||
/* row0/row1 are the decoded socket-0 row (0-3) for each port, or 0xFF
|
||||
* when that port's nibble is not a socket-0 code. The mouse ignores them
|
||||
* -- it is row-blind -- but the rotary's C2/C3 controller-type reporting
|
||||
@@ -254,8 +362,22 @@ uint16_t InputDevOverlayF14002(uint16_t data, uint8_t row0, uint8_t row1);
|
||||
* in this chunk; its published phase lives in joypadNButtons[], which
|
||||
* JoystickStateSave() already serializes; and the controller-type ID flag
|
||||
* is option-derived like the scale. Hence no STATE_VERSION bump beyond
|
||||
* the v12 this chunk already introduced. */
|
||||
#define INPUTDEV_STATE_SIZE 41
|
||||
* the v12 this chunk already introduced.
|
||||
*
|
||||
* The analog controller (#437) DOES grow the chunk, by 5 bytes per port:
|
||||
* bank, last observed row, the latched X/Y ADC bytes and the switch mask
|
||||
* are all machine-visible (the bank decides which table a read decodes
|
||||
* from, and TR10's own driver recipe -- "read all banks into a table,
|
||||
* find bank 0 by the flag bit" -- depends on it surviving a rollback).
|
||||
* v12 is still develop-only (v3.3.0 shipped v11), so per the
|
||||
* one-bump-per-release policy the v12 layout is extended IN PLACE rather
|
||||
* than minting a v13; a pre-extension develop state loads these ten
|
||||
* bytes from the zero-fill tail -- bank 0, row 0, zeroed latches --
|
||||
* which is inert unless an analog device is both selected and engaged,
|
||||
* and the first frame's feed overwrites the latches anyway. Engagement
|
||||
* itself is host-routing-derived and deliberately NOT saved, same
|
||||
* argument as libretro.c's inputdev_live. */
|
||||
#define INPUTDEV_STATE_SIZE 51
|
||||
size_t InputDevStateSave(uint8_t *buf);
|
||||
size_t InputDevStateLoad(const uint8_t *buf);
|
||||
|
||||
|
||||
+38
-10
@@ -20,6 +20,19 @@
|
||||
|
||||
// Global vars
|
||||
|
||||
// Socket-0 row-code decode: low/high nibble of the JOYSTICK register's
|
||||
// row-select byte -> joypadNButtons base offset (row * 4), or 0xFF when
|
||||
// the nibble is not a socket-0 code. File-scope so the write path can
|
||||
// decode the row for InputDevRowSelect() (#437) with the same tables the
|
||||
// read path uses.
|
||||
/* E, D, B, 7 */
|
||||
static const uint8_t joypad0Offset[16] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0xFF, 0xFF, 0xFF, 0x08, 0xFF, 0x04, 0x00, 0xFF
|
||||
};
|
||||
static const uint8_t joypad1Offset[16] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x04, 0xFF, 0x08, 0x0C, 0xFF
|
||||
};
|
||||
|
||||
static uint8_t joystick_ram[4];
|
||||
uint8_t joypad0Buttons[21];
|
||||
uint8_t joypad1Buttons[21];
|
||||
@@ -52,14 +65,6 @@ void JoystickDone(void)
|
||||
|
||||
uint16_t JoystickReadWord(uint32_t offset)
|
||||
{
|
||||
/* E, D, B, 7 */
|
||||
const uint8_t joypad0Offset[16] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0xFF, 0xFF, 0xFF, 0x08, 0xFF, 0x04, 0x00, 0xFF
|
||||
};
|
||||
const uint8_t joypad1Offset[16] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x04, 0xFF, 0x08, 0x0C, 0xFF
|
||||
};
|
||||
|
||||
offset &= 0x03;
|
||||
|
||||
if (offset == 0)
|
||||
@@ -117,8 +122,12 @@ uint16_t JoystickReadWord(uint32_t offset)
|
||||
data &= msk2[offset1 / 4];
|
||||
}
|
||||
|
||||
// Row-independent overlay for a non-pad device (inputdev.h).
|
||||
return InputDevOverlayF14000(data);
|
||||
// Overlay for a non-pad device (inputdev.h). The mouse is
|
||||
// row-independent and ignores the rows; the analog controller
|
||||
// (#437) drives a different slice of X/Y per row and needs them.
|
||||
return InputDevOverlayF14000(data,
|
||||
offset0 == 0xFF ? 0xFF : offset0 / 4,
|
||||
offset1 == 0xFF ? 0xFF : offset1 / 4);
|
||||
}
|
||||
else if (offset == 2)
|
||||
{
|
||||
@@ -170,10 +179,29 @@ void JoystickWriteWord(uint32_t offset, uint16_t data)
|
||||
|
||||
if (offset == 0)
|
||||
{
|
||||
uint8_t offset0, offset1;
|
||||
|
||||
audioEnabled = ((data & 0x0100) ? true : false);
|
||||
joysticksEnabled = ((data & 0x8000) ? true : false);
|
||||
// Arm the non-pad emission clock (inputdev.h).
|
||||
InputDevArm();
|
||||
|
||||
// Row-select change notification: the analog controller's bank
|
||||
// clock (#437) -- TR10 switches banks on the row-3 -> row-0
|
||||
// transition of the device's own socket, and the row select is
|
||||
// this latched write, not a read. With bit 15 clear the JOY
|
||||
// outputs are disabled (tri-state, pulled up), so the device
|
||||
// sees no row code at all.
|
||||
if (joysticksEnabled)
|
||||
{
|
||||
offset0 = joypad0Offset[joystick_ram[1] & 0x0F];
|
||||
offset1 = joypad1Offset[(joystick_ram[1] >> 4) & 0x0F];
|
||||
}
|
||||
else
|
||||
offset0 = offset1 = 0xFF;
|
||||
|
||||
InputDevRowSelect(offset0 == 0xFF ? 0xFF : offset0 / 4,
|
||||
offset1 == 0xFF ? 0xFF : offset1 / 4);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -399,6 +399,122 @@ static void test_set_clamps(void)
|
||||
"the clamped ceiling exponent still passes a 1-unit sample");
|
||||
}
|
||||
|
||||
/* ---- the ABSOLUTE path (#437) --------------------------------------- *
|
||||
*
|
||||
* AxisTuneApplyAbs answers the question this header deferred to the first
|
||||
* absolute consumer: on a POSITION the dead zone RE-BASES (edge maps to
|
||||
* centre, full deflection preserved), the exponent anchors at the
|
||||
* caller's range, and raw == 0 is a real sample -- fixed by dead zone and
|
||||
* exponent, moved only by offset. See "THE ABSOLUTE-AXIS ANSWER" in
|
||||
* axistune.h. */
|
||||
|
||||
static void test_abs_identity(void)
|
||||
{
|
||||
axis_tune t;
|
||||
int32_t raw;
|
||||
int ok = 1;
|
||||
|
||||
printf("absolute path: identity over the whole domain\n");
|
||||
|
||||
AxisTuneReset(&t);
|
||||
for (raw = -127; raw <= 127; raw++)
|
||||
if (AxisTuneApplyAbs(&t, raw, 127) != raw)
|
||||
ok = 0;
|
||||
check(ok, "identity tune returns every position in [-127,127] unchanged");
|
||||
|
||||
check(AxisTuneApplyAbs(&t, 500, 127) == 127
|
||||
&& AxisTuneApplyAbs(&t, -500, 127) == -127,
|
||||
"out-of-domain input is clamped, even by the identity");
|
||||
}
|
||||
|
||||
static void test_abs_deadzone_rebases(void)
|
||||
{
|
||||
axis_tune t;
|
||||
int32_t prev, raw, out;
|
||||
int ok;
|
||||
|
||||
printf("absolute path: dead zone re-bases (the deferred question)\n");
|
||||
|
||||
AxisTuneSet(&t, 16, 0, 256);
|
||||
|
||||
check(AxisTuneApplyAbs(&t, 0, 127) == 0, "centre stays centred");
|
||||
check(AxisTuneApplyAbs(&t, 16, 127) == 0, "the dead-zone edge reads centred");
|
||||
check(AxisTuneApplyAbs(&t, 10, 127) == 0
|
||||
&& AxisTuneApplyAbs(&t, -10, 127) == 0,
|
||||
"positions inside the zone read centred, both signs");
|
||||
|
||||
/* THE RE-BASE ASSERTION: just past the edge the output is small, not a
|
||||
* step to the edge value. A gate implementation returns 17 here. */
|
||||
out = AxisTuneApplyAbs(&t, 17, 127);
|
||||
check(out >= 1 && out <= 2,
|
||||
"one count past the edge is a small value, not a 17-count step");
|
||||
|
||||
check(AxisTuneApplyAbs(&t, 127, 127) == 127
|
||||
&& AxisTuneApplyAbs(&t, -127, 127) == -127,
|
||||
"full deflection still reads full scale after re-basing");
|
||||
|
||||
/* Monotone across the whole travel: a re-base with a dip would read
|
||||
* as steering that stalls mid-turn. */
|
||||
ok = 1;
|
||||
prev = 0;
|
||||
for (raw = 0; raw <= 127; raw++)
|
||||
{
|
||||
out = AxisTuneApplyAbs(&t, raw, 127);
|
||||
if (out < prev)
|
||||
ok = 0;
|
||||
prev = out;
|
||||
}
|
||||
check(ok, "re-based response is monotone non-decreasing");
|
||||
|
||||
/* Degenerate: a zone covering the whole travel gates everything and
|
||||
* must not divide by zero. */
|
||||
AxisTuneSet(&t, AXISTUNE_REF, 0, 256);
|
||||
check(AxisTuneApplyAbs(&t, 40, AXISTUNE_REF) == 0,
|
||||
"dz == range gates the whole travel without dividing by zero");
|
||||
}
|
||||
|
||||
static void test_abs_offset_moves_centre(void)
|
||||
{
|
||||
axis_tune t;
|
||||
|
||||
printf("absolute path: offset is the ONLY field that moves the centre\n");
|
||||
|
||||
AxisTuneSet(&t, 0, 8, 256);
|
||||
check(AxisTuneApplyAbs(&t, 0, 127) == -8,
|
||||
"raw 0 is a SAMPLE here: a +8 offset moves rest to -8 "
|
||||
"(no relative-style raw==0 early-out)");
|
||||
check(AxisTuneApplyAbs(&t, 8, 127) == 0,
|
||||
"a stick resting at +8 is re-centred exactly");
|
||||
|
||||
/* Dead zone and exponent fix the centre. */
|
||||
AxisTuneSet(&t, 24, 0, 512);
|
||||
check(AxisTuneApplyAbs(&t, 0, 127) == 0,
|
||||
"dead zone + exponent leave the centre fixed");
|
||||
}
|
||||
|
||||
static void test_abs_curve_anchor(void)
|
||||
{
|
||||
axis_tune t;
|
||||
int32_t out;
|
||||
|
||||
printf("absolute path: curve anchored at full deflection\n");
|
||||
|
||||
AxisTuneSet(&t, 0, 0, 512); /* e = 2.0 */
|
||||
|
||||
check(AxisTuneApplyAbs(&t, 127, 127) == 127
|
||||
&& AxisTuneApplyAbs(&t, -127, 127) == -127,
|
||||
"full deflection is the fixed point for e = 2.0");
|
||||
|
||||
/* Half deflection squared: 127 * (64/127)^2 = 32.25... -> 32 +/- 1
|
||||
* for fixed-point rounding. */
|
||||
out = AxisTuneApplyAbs(&t, 64, 127);
|
||||
check(out >= 31 && out <= 33,
|
||||
"half deflection at e = 2.0 curves to ~a quarter scale");
|
||||
|
||||
out = AxisTuneApplyAbs(&t, -64, 127);
|
||||
check(out <= -31 && out >= -33, "and symmetrically for negative");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== Per-axis input tuning (axistune) ===\n\n");
|
||||
@@ -419,6 +535,14 @@ int main(void)
|
||||
test_response_is_monotone();
|
||||
printf("\n");
|
||||
test_set_clamps();
|
||||
printf("\n");
|
||||
test_abs_identity();
|
||||
printf("\n");
|
||||
test_abs_deadzone_rebases();
|
||||
printf("\n");
|
||||
test_abs_offset_moves_centre();
|
||||
printf("\n");
|
||||
test_abs_curve_anchor();
|
||||
|
||||
printf("\n%s\n", failures ? "FAILED" : "All axistune checks passed");
|
||||
return failures ? 1 : 0;
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
/*
|
||||
* test/tools/analog_decode_test.c -- TR10 bank-switching analog / driving
|
||||
* controller register-level test (#437).
|
||||
*
|
||||
* SYNTHETIC VERIFICATION, CLEARLY LABELLED AS SUCH
|
||||
* ================================================
|
||||
* No released Jaguar title reads this protocol -- Atari never shipped the
|
||||
* controller (research recorded in inputdev.h and
|
||||
* docs/input-devices-user-guide.md). There is therefore no game to boot
|
||||
* against, and THIS SUITE IS THE DEVICE'S VERIFICATION: synthetic host
|
||||
* stick state goes in through InputDevFeedAnalog(), and a driver written
|
||||
* here -- independently, from the Jaguar Technical Reference V10 tables
|
||||
* ("Analogue Joystick and 'Driving' Controllers", "Identifying Controller
|
||||
* Types", "Reading Bank Switching Controllers") -- scans the banks out of
|
||||
* $F14000 / $F14002 exactly as TR10 tells a 68K game to, and asserts the
|
||||
* decoded values.
|
||||
*
|
||||
* WHAT IS PINNED
|
||||
* - Bank 0 layout: X low/high nibbles in rows 0/1, Y in rows 2/3, on
|
||||
* the port's four J lines; buttons A-D active low on the B column.
|
||||
* - Bank 1 layout: hat switches in row 0, all-ones elsewhere.
|
||||
* - Bank cycling on the row-3 -> row-0 transition of the OWN socket,
|
||||
* including TR10's interleaving rule: reads of the other socket
|
||||
* between banks must not desynchronise the device.
|
||||
* - Controller identification: C2 C3 = 0 1 ("Bank Switching") with the
|
||||
* port-dependent C-row mapping, the row-0 bank-0 flag, and the
|
||||
* last-bank B-column 1111 ("Analogue Joystick or Driving
|
||||
* Controller").
|
||||
* - Value mapping: centre = 128, +X (right) high, +Y (stick FORWARD,
|
||||
* i.e. libretro -Y) high.
|
||||
* - ENGAGEMENT: a selected-but-never-fed device is bit-identical to a
|
||||
* pad -- the liveness guardrail's core half.
|
||||
* - Tuning through the shared axistune layer, absolute semantics:
|
||||
* dead zone RE-BASES (edge reads centred, full deflection preserved),
|
||||
* offset moves the rest position, exponent anchors at full
|
||||
* deflection.
|
||||
* - Savestate: the v12 chunk grew to 51 bytes, and a mid-scan rollback
|
||||
* replays the remaining banks identically.
|
||||
*
|
||||
* USAGE
|
||||
* ./test/tools/analog_decode_test ./virtualjaguar_libretro.dylib [rom]
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "../harness/harness.h"
|
||||
|
||||
/* Mirrors src/jerry/inputdev.h -- restated by hand on purpose, so a
|
||||
* renumbering there cannot silently propagate here (same reasoning as
|
||||
* mouse_decode_test.c). */
|
||||
typedef enum {
|
||||
DEV_PAD = 0,
|
||||
DEV_MOUSE_ST = 1,
|
||||
DEV_MOUSE_AMIGA_ADAPTER = 2,
|
||||
DEV_MOUSE_AMIGA_ON_ST = 3,
|
||||
DEV_ROTARY = 4,
|
||||
DEV_ANALOG = 5,
|
||||
DEV_DRIVING = 6
|
||||
} InputDevType;
|
||||
|
||||
#define SW_A 0x01
|
||||
#define SW_B 0x02
|
||||
#define SW_C 0x04
|
||||
#define SW_D 0x08
|
||||
#define SW_UP 0x10
|
||||
#define SW_DOWN 0x20
|
||||
#define SW_LEFT 0x40
|
||||
#define SW_RIGHT 0x80
|
||||
|
||||
/* Mirrors INPUTDEV_STATE_SIZE: the pre-#437 41 bytes plus 2 ports x
|
||||
* (bank + last row + ADC X + ADC Y + switch mask). */
|
||||
#define INPUTDEV_STATE_SIZE_EXPECTED 51
|
||||
|
||||
/* Socket-0 row-select words, output enable set (bit 15). Port 1 rows
|
||||
* live in the low nibble, port 2 rows in the high nibble; the unused
|
||||
* port's nibble is F (no row). */
|
||||
static const uint16_t row_word_p1[4] = { 0x81FE, 0x81FD, 0x81FB, 0x81F7 };
|
||||
static const uint16_t row_word_p2[4] = { 0x817F, 0x81BF, 0x81DF, 0x81EF };
|
||||
|
||||
/* $F14000 J-line base bit and $F14002 column bits per port. */
|
||||
static const int j_base[2] = { 8, 12 };
|
||||
static const int c_bit[2] = { 0, 2 };
|
||||
static const int b_bit[2] = { 1, 3 };
|
||||
|
||||
static const char *port_name[2] = { "port 1", "port 2" };
|
||||
|
||||
/* ---- core entry points ---------------------------------------------- */
|
||||
|
||||
static uint16_t (*p_ReadWord)(uint32_t);
|
||||
static void (*p_WriteWord)(uint32_t, uint16_t);
|
||||
static void (*p_SetType)(int, InputDevType);
|
||||
static void (*p_SetTune)(int, int, int32_t, int32_t, int32_t);
|
||||
static void (*p_Reset)(void);
|
||||
static void (*p_FeedAnalog)(int, int32_t, int32_t, uint32_t);
|
||||
static InputDevType (*p_GetType)(int);
|
||||
static size_t (*p_StateSave)(uint8_t *);
|
||||
|
||||
static uint8_t *p_joypad[2];
|
||||
|
||||
static size_t (*p_serialize_size)(void);
|
||||
static bool (*p_serialize)(void *, size_t);
|
||||
static bool (*p_unserialize)(const void *, size_t);
|
||||
|
||||
/* ---- reporting ------------------------------------------------------- */
|
||||
|
||||
static int failures;
|
||||
static char detail[320];
|
||||
|
||||
static void report(int cond, const char *what)
|
||||
{
|
||||
if (cond)
|
||||
printf(" ok %s\n", what);
|
||||
else
|
||||
{
|
||||
printf(" FAIL %s\n", what);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- the TR10 driver ------------------------------------------------- */
|
||||
|
||||
static uint16_t row_word(int port, int row)
|
||||
{
|
||||
return port ? row_word_p2[row] : row_word_p1[row];
|
||||
}
|
||||
|
||||
/* One row: write the select, read JOYSTICK and JOYBUTS (TR10's own table
|
||||
* shape: "this example assumes you are always reading both registers"). */
|
||||
static void scan_row(int port, int row, uint16_t *joy, uint16_t *but)
|
||||
{
|
||||
p_WriteWord(0, row_word(port, row));
|
||||
*joy = p_ReadWord(0);
|
||||
*but = p_ReadWord(2);
|
||||
}
|
||||
|
||||
/* One full bank in row order 0..3, as TR10 requires. */
|
||||
static void scan_bank(int port, uint16_t joy[4], uint16_t but[4])
|
||||
{
|
||||
int r;
|
||||
for (r = 0; r < 4; r++)
|
||||
scan_row(port, r, &joy[r], &but[r]);
|
||||
}
|
||||
|
||||
static int bitv(uint16_t v, int bit)
|
||||
{
|
||||
return (int)((v >> bit) & 1u);
|
||||
}
|
||||
|
||||
/* The full TR10 recipe: every scan flips the bank on its own row-3 ->
|
||||
* row-0 transition, so a driver must read ALL banks into a table and
|
||||
* find bank 0 by the row-0 flag bit afterwards. Returns the index (0 or
|
||||
* 4) of bank 0's rows within the 8-row table. */
|
||||
static int scan_banks(int port, uint16_t joy[8], uint16_t but[8])
|
||||
{
|
||||
scan_bank(port, &joy[0], &but[0]);
|
||||
scan_bank(port, &joy[4], &but[4]);
|
||||
return ((but[0] >> (port ? 2 : 0)) & 1u) == 0 ? 0 : 4;
|
||||
}
|
||||
|
||||
static int nib(uint16_t joy, int port)
|
||||
{
|
||||
return (joy >> j_base[port]) & 0x0F;
|
||||
}
|
||||
|
||||
/* Decode an 8-bit value from a bank-0 scan: low nibble in `lo_row`, high
|
||||
* nibble in `lo_row + 1`. */
|
||||
static int decode8(int port, const uint16_t joy[4], int lo_row)
|
||||
{
|
||||
return nib(joy[lo_row], port) | (nib(joy[lo_row + 1], port) << 4);
|
||||
}
|
||||
|
||||
static void attach(int port, InputDevType type)
|
||||
{
|
||||
p_SetType(port, type);
|
||||
p_SetTune(port, 0, 0, 0, 256);
|
||||
p_SetTune(port, 1, 0, 0, 256);
|
||||
p_Reset();
|
||||
memset(p_joypad[port], 0x00, 21);
|
||||
}
|
||||
|
||||
/* Feed and thereby ENGAGE (libretro.c only feeds once the stick has
|
||||
* proven live; at this layer the feed IS the engagement). */
|
||||
static void feed(int port, int32_t x, int32_t y, uint32_t sw)
|
||||
{
|
||||
p_FeedAnalog(port, x, y, sw);
|
||||
}
|
||||
|
||||
/* ---- individual checks ----------------------------------------------- */
|
||||
|
||||
/* The liveness guardrail's core half: selected but never fed reads
|
||||
* bit-identical to a pad, controller-type probe included. */
|
||||
static void test_inert_until_fed(int port)
|
||||
{
|
||||
uint16_t joy_pad[4], but_pad[4], joy_an[4], but_an[4];
|
||||
int r, same;
|
||||
|
||||
printf("%s: inert until engaged (liveness guardrail)\n",
|
||||
port_name[port]);
|
||||
|
||||
attach(port, DEV_PAD);
|
||||
scan_bank(port, joy_pad, but_pad);
|
||||
|
||||
attach(port, DEV_ANALOG);
|
||||
scan_bank(port, joy_an, but_an);
|
||||
|
||||
same = 1;
|
||||
for (r = 0; r < 4; r++)
|
||||
if (joy_an[r] != joy_pad[r] || but_an[r] != but_pad[r])
|
||||
same = 0;
|
||||
|
||||
report(same,
|
||||
"a selected-but-never-fed analog device reads bit-identical "
|
||||
"to a pad in every row of both registers");
|
||||
|
||||
attach(port, DEV_PAD);
|
||||
}
|
||||
|
||||
/* Bank 0: the X/Y nibbles, the buttons, the C column and the bank flag. */
|
||||
static void test_bank0_layout(int port)
|
||||
{
|
||||
uint16_t joy[4], but[4];
|
||||
int x, y;
|
||||
|
||||
printf("%s: bank 0 layout (TR10 table)\n", port_name[port]);
|
||||
|
||||
attach(port, DEV_ANALOG);
|
||||
/* +X full right, stick full FORWARD (libretro -Y). */
|
||||
feed(port, 32767, -32768, SW_A | SW_C);
|
||||
scan_bank(port, joy, but);
|
||||
|
||||
x = decode8(port, joy, 0);
|
||||
y = decode8(port, joy, 2);
|
||||
|
||||
sprintf(detail, "full right reads X = 255 (got %d)", x);
|
||||
report(x == 255, detail);
|
||||
sprintf(detail, "full forward reads Y = 255 (got %d) -- TR10 +Y is "
|
||||
"forward, libretro -Y", y);
|
||||
report(y == 255, detail);
|
||||
|
||||
/* Buttons A-D active low on the B column, one per row. */
|
||||
report(bitv(but[0], b_bit[port]) == 0, "A held: row 0 B bit low");
|
||||
report(bitv(but[1], b_bit[port]) == 1, "B released: row 1 B bit high");
|
||||
report(bitv(but[2], b_bit[port]) == 0, "C held: row 2 B bit low");
|
||||
report(bitv(but[3], b_bit[port]) == 1, "D released: row 3 B bit high");
|
||||
|
||||
/* Bank-0 flag: C column low in row 0. */
|
||||
report(bitv(but[0], c_bit[port]) == 0,
|
||||
"row 0 C column reads 0 (the bank-0 flag)");
|
||||
|
||||
/* C1/C2/C3: C1 = 1 in row 1 on both ports; C2 = 0 in row 2 (port 1) /
|
||||
* row 3 (port 2); C3 = 1 in the other. */
|
||||
report(bitv(but[1], c_bit[port]) == 1, "row 1 reads C1 = 1");
|
||||
if (port == 0)
|
||||
{
|
||||
report(bitv(but[2], c_bit[port]) == 0, "row 2 reads C2 = 0 (port 1)");
|
||||
report(bitv(but[3], c_bit[port]) == 1, "row 3 reads C3 = 1 (port 1)");
|
||||
}
|
||||
else
|
||||
{
|
||||
report(bitv(but[2], c_bit[port]) == 1, "row 2 reads C3 = 1 (port 2)");
|
||||
report(bitv(but[3], c_bit[port]) == 0, "row 3 reads C2 = 0 (port 2)");
|
||||
}
|
||||
|
||||
attach(port, DEV_PAD);
|
||||
}
|
||||
|
||||
/* Centre and hard-left mappings. */
|
||||
static void test_value_mapping(int port)
|
||||
{
|
||||
uint16_t joy[8], but[8];
|
||||
int base, x, y;
|
||||
|
||||
printf("%s: value mapping (two-bank scans, TR10 recipe)\n",
|
||||
port_name[port]);
|
||||
|
||||
attach(port, DEV_ANALOG);
|
||||
feed(port, 0, 0, 0);
|
||||
base = scan_banks(port, joy, but);
|
||||
x = decode8(port, &joy[base], 0);
|
||||
y = decode8(port, &joy[base], 2);
|
||||
sprintf(detail, "centred stick reads X = Y = 128 (got %d / %d)", x, y);
|
||||
report(x == 128 && y == 128, detail);
|
||||
|
||||
feed(port, -32768, 32767, 0);
|
||||
base = scan_banks(port, joy, but);
|
||||
x = decode8(port, &joy[base], 0);
|
||||
y = decode8(port, &joy[base], 2);
|
||||
sprintf(detail,
|
||||
"full left / full back reads X = Y = 1 (got %d / %d) -- the "
|
||||
"symmetric 1..255 range, TR10 forbids assuming the endpoints",
|
||||
x, y);
|
||||
report(x == 1 && y == 1, detail);
|
||||
|
||||
attach(port, DEV_PAD);
|
||||
}
|
||||
|
||||
/* Bank cycling, the bank-1 layout, and the type identification a TR10
|
||||
* driver derives from a full multi-bank scan. */
|
||||
static void test_bank_cycle_and_id(int port)
|
||||
{
|
||||
uint16_t joy[4], but[4];
|
||||
int r, ok;
|
||||
|
||||
printf("%s: bank cycling and controller identification\n",
|
||||
port_name[port]);
|
||||
|
||||
attach(port, DEV_ANALOG);
|
||||
feed(port, 0, 0, SW_UP | SW_LEFT);
|
||||
|
||||
/* First full scan: this is bank 0 (flag low in row 0). */
|
||||
scan_bank(port, joy, but);
|
||||
report(bitv(but[0], c_bit[port]) == 0, "first scan is bank 0");
|
||||
|
||||
/* The row-3 -> row-0 transition of the NEXT scan switches banks. */
|
||||
scan_bank(port, joy, but);
|
||||
report(bitv(but[0], c_bit[port]) == 1,
|
||||
"second scan reads the bank-1 flag (row-3 -> row-0 switched)");
|
||||
|
||||
/* Bank 1 row 0: hat switches on the J lines, active low. */
|
||||
report(bitv(joy[0], j_base[port] + 0) == 0
|
||||
&& bitv(joy[0], j_base[port] + 1) == 1
|
||||
&& bitv(joy[0], j_base[port] + 2) == 0
|
||||
&& bitv(joy[0], j_base[port] + 3) == 1,
|
||||
"bank 1 row 0 reads the hat: Up and Left low, Down and Right high");
|
||||
|
||||
/* Bank 1 rows 1-3: J lines all high; B column 1 in every row -- the
|
||||
* last-bank 1111 that identifies "Analogue Joystick or Driving
|
||||
* Controller" among the bank-switching types. */
|
||||
ok = 1;
|
||||
for (r = 1; r < 4; r++)
|
||||
if (nib(joy[r], port) != 0x0F)
|
||||
ok = 0;
|
||||
report(ok, "bank 1 rows 1-3 read all 1s on the J lines");
|
||||
|
||||
ok = 1;
|
||||
for (r = 0; r < 4; r++)
|
||||
if (bitv(but[r], b_bit[port]) != 1)
|
||||
ok = 0;
|
||||
report(ok, "bank 1 B column reads 1111: the analog/driving type ID");
|
||||
|
||||
/* Third scan cycles back to bank 0. */
|
||||
scan_bank(port, joy, but);
|
||||
report(bitv(but[0], c_bit[port]) == 0, "third scan is bank 0 again");
|
||||
|
||||
attach(port, DEV_PAD);
|
||||
}
|
||||
|
||||
/* TR10: "It is acceptable to read a bank from one controller, followed by
|
||||
* a bank or multiple banks from other controllers, and then come back" --
|
||||
* other-socket rows must neither switch banks nor desynchronise. */
|
||||
static void test_interleave_immunity(int port)
|
||||
{
|
||||
uint16_t joy[4], but[4], j, b;
|
||||
int other = port ^ 1;
|
||||
int r;
|
||||
|
||||
printf("%s: other-socket interleave immunity\n", port_name[port]);
|
||||
|
||||
attach(port, DEV_ANALOG);
|
||||
attach(other, DEV_PAD);
|
||||
feed(port, 0, 0, 0);
|
||||
|
||||
/* Scan bank 0 of our port, but wedge a full scan of the OTHER port
|
||||
* between our row 3 and our next row 0. */
|
||||
scan_bank(port, joy, but);
|
||||
for (r = 0; r < 4; r++)
|
||||
scan_row(other, r, &j, &b);
|
||||
|
||||
/* Our next row 0 must still see the 3 -> 0 transition: bank 1. */
|
||||
scan_row(port, 0, &j, &b);
|
||||
report(bitv(b, c_bit[port]) == 1,
|
||||
"a full other-socket scan between row 3 and row 0 does not "
|
||||
"eat the bank switch");
|
||||
|
||||
/* And the interleaved reads themselves must not have advanced the
|
||||
* cycle: rows 1-3 then 0 again -> back to bank 0. */
|
||||
scan_row(port, 1, &j, &b);
|
||||
scan_row(port, 2, &j, &b);
|
||||
scan_row(port, 3, &j, &b);
|
||||
scan_row(port, 0, &j, &b);
|
||||
report(bitv(b, c_bit[port]) == 0,
|
||||
"exactly one switch per own-socket 3 -> 0 transition");
|
||||
|
||||
attach(port, DEV_PAD);
|
||||
}
|
||||
|
||||
/* The driving skin is the same wire device. */
|
||||
static void test_driving_same_wire(int port)
|
||||
{
|
||||
uint16_t joy_a[4], but_a[4], joy_d[4], but_d[4];
|
||||
int r, same;
|
||||
|
||||
printf("%s: driving controller is the same wire protocol\n",
|
||||
port_name[port]);
|
||||
|
||||
attach(port, DEV_ANALOG);
|
||||
feed(port, 12345, -6789, SW_B | SW_UP);
|
||||
scan_bank(port, joy_a, but_a);
|
||||
|
||||
attach(port, DEV_DRIVING);
|
||||
feed(port, 12345, -6789, SW_B | SW_UP);
|
||||
scan_bank(port, joy_d, but_d);
|
||||
|
||||
same = 1;
|
||||
for (r = 0; r < 4; r++)
|
||||
if (joy_a[r] != joy_d[r] || but_a[r] != but_d[r])
|
||||
same = 0;
|
||||
report(same,
|
||||
"identical feeds read identically as 'analog' and 'driving'");
|
||||
|
||||
attach(port, DEV_PAD);
|
||||
}
|
||||
|
||||
/* Tuning integration through the shared axistune layer, absolute
|
||||
* semantics, asserted at the register level. */
|
||||
static int read_x(int port)
|
||||
{
|
||||
uint16_t joy[8], but[8];
|
||||
int base = scan_banks(port, joy, but);
|
||||
return decode8(port, &joy[base], 0);
|
||||
}
|
||||
|
||||
static void test_tuning(int port)
|
||||
{
|
||||
int x;
|
||||
|
||||
printf("%s: tuning integration (absolute semantics)\n",
|
||||
port_name[port]);
|
||||
|
||||
attach(port, DEV_ANALOG);
|
||||
|
||||
/* Dead zone 16 ADC counts. A deflection inside it reads centred. */
|
||||
p_SetTune(port, 0, 16, 0, 256);
|
||||
feed(port, 10 * 258, 0, 0); /* ~10 counts */
|
||||
x = read_x(port);
|
||||
sprintf(detail, "10 counts inside a 16-count dead zone reads centred "
|
||||
"(got %d, expect 128)", x);
|
||||
report(x == 128, detail);
|
||||
|
||||
/* RE-BASE, not gate: just past the edge is a small value, not a step
|
||||
* to 17 counts. */
|
||||
feed(port, 18 * 258, 0, 0); /* ~18 counts */
|
||||
x = read_x(port);
|
||||
sprintf(detail, "18 counts re-bases to a small deflection "
|
||||
"(got %d, expect 129..131, NOT ~146)", x);
|
||||
report(x >= 129 && x <= 131, detail);
|
||||
|
||||
/* Full deflection is preserved by the re-base. */
|
||||
feed(port, 32767, 0, 0);
|
||||
x = read_x(port);
|
||||
sprintf(detail, "full deflection still reads 255 through the dead zone "
|
||||
"(got %d)", x);
|
||||
report(x == 255, detail);
|
||||
|
||||
/* Offset moves the rest position -- the absolute path's raw==0 rule:
|
||||
* 0 is a sample, and offset is the one field allowed to move it. */
|
||||
p_SetTune(port, 0, 0, 8, 256);
|
||||
feed(port, 0, 0, 0);
|
||||
x = read_x(port);
|
||||
sprintf(detail, "a +8-count offset moves a centred stick to 120 "
|
||||
"(got %d)", x);
|
||||
report(x == 120, detail);
|
||||
|
||||
/* Exponent 2.0 anchors at full deflection: half stick curves toward
|
||||
* centre, full stick is untouched. */
|
||||
p_SetTune(port, 0, 0, 0, 512);
|
||||
feed(port, 16384, 0, 0); /* ~half deflection */
|
||||
x = read_x(port);
|
||||
/* 16384/258 = 63 counts; 127 * (63/127)^2 = 31.25 -> ~31, so the byte
|
||||
* reads ~159 instead of the linear ~191. */
|
||||
sprintf(detail, "half deflection at e = 2.0 curves toward centre "
|
||||
"(got %d, expect ~159)", x);
|
||||
report(x >= 157 && x <= 161, detail);
|
||||
|
||||
feed(port, 32767, 0, 0);
|
||||
x = read_x(port);
|
||||
sprintf(detail, "full deflection at e = 2.0 still reads 255 (got %d)",
|
||||
x);
|
||||
report(x == 255, detail);
|
||||
|
||||
p_SetTune(port, 0, 0, 0, 256);
|
||||
attach(port, DEV_PAD);
|
||||
}
|
||||
|
||||
/* Cross-port isolation: an engaged analog device on one port leaves the
|
||||
* other port's pad matrix untouched. */
|
||||
static void test_port_isolation(void)
|
||||
{
|
||||
uint16_t j, b;
|
||||
|
||||
printf("cross-port isolation\n");
|
||||
|
||||
attach(0, DEV_ANALOG);
|
||||
attach(1, DEV_PAD);
|
||||
feed(0, 32767, 0, 0);
|
||||
|
||||
/* Press port-2 pad Up (slot 0 of joypad1Buttons). */
|
||||
p_joypad[1][0] = 0xFF;
|
||||
scan_row(1, 0, &j, &b);
|
||||
report(bitv(j, 12) == 0,
|
||||
"port-2 pad Up still reads through with an engaged analog "
|
||||
"device on port 1");
|
||||
p_joypad[1][0] = 0x00;
|
||||
|
||||
attach(0, DEV_PAD);
|
||||
}
|
||||
|
||||
/* Savestate: chunk size, and a mid-cycle rollback replays identically. */
|
||||
static void test_savestate(void)
|
||||
{
|
||||
uint8_t chunk[256];
|
||||
uint8_t *snap;
|
||||
size_t sz, n;
|
||||
uint16_t joy_c[4], but_c[4], joy_r[4], but_r[4];
|
||||
int r, same;
|
||||
|
||||
printf("savestate\n");
|
||||
|
||||
n = p_StateSave(chunk);
|
||||
sprintf(detail, "InputDevStateSave wrote %u bytes (expect %d: the v12 "
|
||||
"chunk grew by 5 per port for #437)",
|
||||
(unsigned)n, INPUTDEV_STATE_SIZE_EXPECTED);
|
||||
report(n == (size_t)INPUTDEV_STATE_SIZE_EXPECTED, detail);
|
||||
|
||||
if (!p_serialize || !p_unserialize || !p_serialize_size)
|
||||
{
|
||||
report(0, "retro_serialize/retro_unserialize resolvable");
|
||||
return;
|
||||
}
|
||||
|
||||
attach(0, DEV_ANALOG);
|
||||
feed(0, 23456, -12345, SW_D);
|
||||
|
||||
/* Scan bank 0 fully, so the device sits at row 3 of bank 0: the very
|
||||
* next row-0 write must switch to bank 1. */
|
||||
scan_bank(0, joy_c, but_c);
|
||||
|
||||
sz = p_serialize_size();
|
||||
snap = (uint8_t *)malloc(sz);
|
||||
if (!snap)
|
||||
{
|
||||
report(0, "snapshot allocation");
|
||||
return;
|
||||
}
|
||||
report(p_serialize(snap, sz) ? 1 : 0, "retro_serialize mid-cycle");
|
||||
|
||||
/* Continue: the next scan is bank 1. */
|
||||
scan_bank(0, joy_c, but_c);
|
||||
|
||||
/* Roll back and replay the same scan. */
|
||||
report(p_unserialize(snap, sz) ? 1 : 0, "retro_unserialize accepted");
|
||||
scan_bank(0, joy_r, but_r);
|
||||
|
||||
same = 1;
|
||||
for (r = 0; r < 4; r++)
|
||||
if (joy_r[r] != joy_c[r] || but_r[r] != but_c[r])
|
||||
same = 0;
|
||||
report(same && bitv(but_r[0], c_bit[0]) == 1,
|
||||
"the rolled-back scan replays bank 1 word-identically -- "
|
||||
"bank counter and latches survived");
|
||||
|
||||
free(snap);
|
||||
attach(0, DEV_PAD);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
harness_config cfg = HARNESS_CONFIG_DEFAULT;
|
||||
harness_result result;
|
||||
char summary[128];
|
||||
int port;
|
||||
|
||||
cfg.frames = 0;
|
||||
if (!harness_init_from_args(&cfg, argc, argv))
|
||||
return 1;
|
||||
|
||||
if (!cfg.rom_path)
|
||||
cfg.rom_path = "test/roms/yarc.j64";
|
||||
if (!harness_load_rom(&cfg))
|
||||
return 1;
|
||||
|
||||
p_ReadWord = (uint16_t (*)(uint32_t))harness_dlsym(&cfg, "JoystickReadWord");
|
||||
p_WriteWord = (void (*)(uint32_t, uint16_t))harness_dlsym(&cfg, "JoystickWriteWord");
|
||||
p_SetType = (void (*)(int, InputDevType))
|
||||
harness_dlsym(&cfg, "InputDevSetType");
|
||||
p_SetTune = (void (*)(int, int, int32_t, int32_t, int32_t))
|
||||
harness_dlsym(&cfg, "InputDevSetTune");
|
||||
p_Reset = (void (*)(void))harness_dlsym(&cfg, "InputDevReset");
|
||||
p_FeedAnalog = (void (*)(int, int32_t, int32_t, uint32_t))
|
||||
harness_dlsym(&cfg, "InputDevFeedAnalog");
|
||||
p_GetType = (InputDevType (*)(int))harness_dlsym(&cfg, "InputDevGetType");
|
||||
p_StateSave = (size_t (*)(uint8_t *))harness_dlsym(&cfg, "InputDevStateSave");
|
||||
|
||||
p_joypad[0] = (uint8_t *)harness_dlsym(&cfg, "joypad0Buttons");
|
||||
p_joypad[1] = (uint8_t *)harness_dlsym(&cfg, "joypad1Buttons");
|
||||
|
||||
p_serialize_size = (size_t (*)(void))harness_dlsym(&cfg, "retro_serialize_size");
|
||||
p_serialize = (bool (*)(void *, size_t))harness_dlsym(&cfg, "retro_serialize");
|
||||
p_unserialize = (bool (*)(const void *, size_t))
|
||||
harness_dlsym(&cfg, "retro_unserialize");
|
||||
|
||||
if (!p_ReadWord || !p_WriteWord || !p_SetType || !p_SetTune || !p_Reset
|
||||
|| !p_FeedAnalog || !p_GetType || !p_StateSave
|
||||
|| !p_joypad[0] || !p_joypad[1])
|
||||
{
|
||||
fprintf(stderr,
|
||||
"analog_decode_test: missing test-ABI symbols. The wide "
|
||||
"test ABI must export Joystick*, InputDev* and "
|
||||
"joypadNButtons (exports-test.list and link-test.T). "
|
||||
"Build with TEST_EXPORTS=1.\n");
|
||||
harness_shutdown(&cfg);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Both ports and both skins are valid (TR10 restricts neither). */
|
||||
for (port = 0; port < 2; port++)
|
||||
{
|
||||
sprintf(detail, "InputDevSetType accepts analog on %s",
|
||||
port_name[port]);
|
||||
p_SetType(port, DEV_ANALOG);
|
||||
report(p_GetType(port) == DEV_ANALOG, detail);
|
||||
sprintf(detail, "InputDevSetType accepts driving on %s",
|
||||
port_name[port]);
|
||||
p_SetType(port, DEV_DRIVING);
|
||||
report(p_GetType(port) == DEV_DRIVING, detail);
|
||||
p_SetType(port, DEV_PAD);
|
||||
}
|
||||
|
||||
for (port = 0; port < 2; port++)
|
||||
{
|
||||
test_inert_until_fed(port);
|
||||
test_bank0_layout(port);
|
||||
test_value_mapping(port);
|
||||
test_bank_cycle_and_id(port);
|
||||
test_interleave_immunity(port);
|
||||
test_driving_same_wire(port);
|
||||
test_tuning(port);
|
||||
}
|
||||
|
||||
test_port_isolation();
|
||||
test_savestate();
|
||||
|
||||
sprintf(summary, "%d failure(s)", failures);
|
||||
result.status = failures ? "FAIL" : "PASS";
|
||||
result.name = "analog_decode";
|
||||
result.detail = summary;
|
||||
harness_report(&cfg, &result, 1);
|
||||
|
||||
harness_shutdown(&cfg);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -80,8 +80,9 @@ typedef enum {
|
||||
|
||||
/* Mirrors INPUTDEV_STATE_SIZE in src/jerry/inputdev.h, restated here
|
||||
* on purpose: 2 ports x (2 axes x (int32 backlog + int32 carry +
|
||||
* uint8 phase) + 2 button latches) + 1 armed byte. */
|
||||
#define INPUTDEV_STATE_SIZE_EXPECTED 41
|
||||
* uint8 phase) + 2 button latches) + 1 armed byte, plus the #437 analog
|
||||
* trailer: 2 ports x (bank + last row + ADC X + ADC Y + switch mask). */
|
||||
#define INPUTDEV_STATE_SIZE_EXPECTED 51
|
||||
|
||||
/* Port-2 socket-0 row codes: rows 0, 1, 2, 3 (mapping doc section 2). */
|
||||
static const uint16_t row_words[4] = { 0x817F, 0x81BF, 0x81DF, 0x81EF };
|
||||
|
||||
Reference in new issue
Block a user