(winraw) Fix stale and misindexed hat state on partial HID reports

winraw_joypad_parse_hid_report() walks pad->val_caps[] and reads each
usage with HidP_GetUsageValue(), skipping to the next cap when the call
does not return HIDP_STATUS_SUCCESS. Two problems follow from that.

First, the destination index came from a counter that only advanced
after a successful read:

   if (usage == HID_USAGE_GENERIC_HATSWITCH)
   {
      if (hat_idx < max_hats)
      {
         pad->hats[hat_idx] = ...;
         hat_idx++;
      }
   }

whereas num_hats/num_axes were counted at enumeration time by walking
the same array unconditionally. Any usage missing from the current
report therefore shifted every later value down one slot, so on a
two-hat device a report lacking the first hat wrote the second hat's
direction into hats[0].

Second, hats were never cleared between reports. Buttons already are:

   memset(pad->buttons, 0, num_buttons * sizeof(pad->buttons[0]));

but a hat whose usage was absent simply kept its previous value.

HidP_GetUsageValue() returns HIDP_STATUS_INCOMPATIBLE_REPORT_ID when
the arriving report does not carry the requested usage, which is
routine rather than exceptional: many gamepads spread their state
across several report IDs, and some emit separate reports for motion,
battery or audio. On such a device a D-pad direction held at the time
of the last report that did carry the hat usage stays latched for as
long as the intervening reports keep arriving. pad->hats[] is consumed
directly by winraw_joypad_button() with no freshness check.

Derive the destination index from the position of the cap in
val_caps[], matching enumeration, and clear the hats at the start of
each report alongside the buttons. Zero is the centred bitmask in this
driver - winraw_joypad_hat_value_to_bitmask() converts the raw HID
value before storing it - so clearing reads as 'no direction', not as
north.

Axes are deliberately left uncleared: unlike a hat, an axis has no
value that reliably means 'no input', and zeroing would misreport
sticks that legitimately rest off-centre.
This commit is contained in:
libretroadmin committed 2026-07-20 21:20:33 +02:00
1 parent 8b85429fcc
commit 483f6e99bc
1 file changed
+37 -11
+37 -11
View File
@@ -570,6 +570,17 @@ static void winraw_joypad_parse_hid_report(winraw_joypad_joypad_data_t *pad,
if (num_buttons > 0)
memset(pad->buttons, 0, num_buttons * sizeof(pad->buttons[0]));
/* Hats are cleared for the same reason as buttons: a hat whose
* usage is absent from this report must read as centred rather
* than retaining the direction from the previous one. Zero is
* the centred bitmask here (see winraw_joypad_hat_value_to_bitmask),
* not a direction. Axes are deliberately not cleared - a missing
* axis reads as its neutral scaled value only if it was centred,
* and zeroing them would fight sticks that legitimately rest
* off-centre. */
if (pad->num_hats > 0)
memset(pad->hats, 0, pad->num_hats * sizeof(pad->hats[0]));
usage_count = (ULONG)num_buttons;
if (usage_count > RAWINPUT_MAX_BUTTONS)
usage_count = RAWINPUT_MAX_BUTTONS;
@@ -606,7 +617,27 @@ static void winraw_joypad_parse_hid_report(winraw_joypad_joypad_data_t *pad,
USAGE usage = pad->val_caps[i].IsRange
? pad->val_caps[i].Range.UsageMin
: pad->val_caps[i].NotRange.Usage;
bool is_hat = (usage == HID_USAGE_GENERIC_HATSWITCH);
bool is_axis = !is_hat && winraw_joypad_is_axis_usage(&pad->val_caps[i]);
unsigned slot_idx;
/* Assign the destination index from the position of this
* cap in val_caps[], matching how num_hats/num_axes were
* counted at enumeration time. Deriving it from a running
* counter that only advances on a successful read would
* shift every subsequent value by one whenever a usage is
* missing from the current report. */
if (is_hat)
slot_idx = hat_idx++;
else if (is_axis)
slot_idx = axis_idx++;
else
continue; /* unknown/vendor value cap */
/* HidP_GetUsageValue() returns HIDP_STATUS_INCOMPATIBLE_REPORT_ID
* when the arriving report does not carry this usage, which is
* routine for devices that split their state across several
* report IDs. Leave the (already cleared) entry alone. */
if (HidP_GetUsageValue(HidP_Input,
pad->val_caps[i].UsagePage,
0, usage,
@@ -614,32 +645,27 @@ static void winraw_joypad_parse_hid_report(winraw_joypad_joypad_data_t *pad,
(PCHAR)raw_data, raw_data_size) != HIDP_STATUS_SUCCESS)
continue;
if (usage == HID_USAGE_GENERIC_HATSWITCH)
if (is_hat)
{
if (hat_idx < max_hats)
{
pad->hats[hat_idx] = winraw_joypad_hat_value_to_bitmask(
if (slot_idx < max_hats)
pad->hats[slot_idx] = winraw_joypad_hat_value_to_bitmask(
(LONG)value,
pad->val_caps[i].LogicalMin,
pad->val_caps[i].LogicalMax);
hat_idx++;
}
}
else if (winraw_joypad_is_axis_usage(&pad->val_caps[i]))
else
{
if (axis_idx < max_axes)
if (slot_idx < max_axes)
{
LONG signed_value = (pad->val_caps[i].LogicalMin < 0)
? winraw_joypad_sign_extend(value, pad->val_caps[i].BitSize)
: (LONG)value;
pad->axes[axis_idx] = winraw_joypad_scale_axis(
pad->axes[slot_idx] = winraw_joypad_scale_axis(
signed_value,
pad->val_caps[i].LogicalMin,
pad->val_caps[i].LogicalMax);
axis_idx++;
}
}
/* else: unknown/vendor value cap — skip */
}
}
}