From: Vicki Pfau <vi@endrift.com>
To: Dmitry Torokhov <dmitry.torokhov@gmail.com>, linux-input@vger.kernel.org
Cc: Vicki Pfau <vi@endrift.com>
Subject: [PATCH v4 12/12] Input: xbox_gip - Add wheel support
Date: Fri, 21 Aug 2026 19:11:36 -0700 [thread overview]
Message-ID: <20260822021140.1149546-13-vi@endrift.com> (raw)
In-Reply-To: <20260822021140.1149546-1-vi@endrift.com>
This adds preliminary support for racing wheel support in xbox_gip,
exposing them mapped to the newly added axes.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/input/joystick/gip/Makefile | 1 +
drivers/input/joystick/gip/gip-core.c | 4 +-
drivers/input/joystick/gip/gip-drivers.c | 10 +
drivers/input/joystick/gip/gip-wheel.c | 349 +++++++++++++++++++++++
drivers/input/joystick/gip/gip.h | 6 +
5 files changed, 369 insertions(+), 1 deletion(-)
create mode 100644 drivers/input/joystick/gip/gip-wheel.c
diff --git a/drivers/input/joystick/gip/Makefile b/drivers/input/joystick/gip/Makefile
index 62c016d6c11e..7d4209eb32c3 100644
--- a/drivers/input/joystick/gip/Makefile
+++ b/drivers/input/joystick/gip/Makefile
@@ -5,3 +5,4 @@ xbox-gip-y := gip-core.o gip-drivers.o gip-security.o
# Additional device support
xbox-gip-y += gip-arcade-stick.o
xbox-gip-y += gip-pdp-jaguar.o
+xbox-gip-y += gip-wheel.o
diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index c03d730f2968..82f25929480e 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -8,7 +8,7 @@
* - Audio device support
* - Event logging
* - Raw character device
- * - Wheel support
+ * - Wheel force feedback
* - Flight stick support
* - More arcade stick testing
* - Arcade stick extra buttons
@@ -331,6 +331,8 @@ static const struct gip_driver *base_drivers[] = {
&gip_driver_gamepad,
&gip_driver_arcade_stick,
&gip_driver_pdp_jaguar,
+ &gip_driver_trueforce_wheel,
+ &gip_driver_wheel,
NULL /* Sentinel */
};
diff --git a/drivers/input/joystick/gip/gip-drivers.c b/drivers/input/joystick/gip/gip-drivers.c
index 74bc858e71cc..7ba11b19c8ee 100644
--- a/drivers/input/joystick/gip/gip-drivers.c
+++ b/drivers/input/joystick/gip/gip-drivers.c
@@ -142,6 +142,11 @@ static int gip_setup_navigation_input(struct gip_attachment *attachment, struct
input_set_capability(input, EV_KEY, BTN_TR);
input_set_capability(input, EV_KEY, BTN_TL);
+ if (attachment->quirks & GIP_QUIRK_FORCE_GAMEPAD_SB) {
+ input_set_capability(input, EV_KEY, BTN_THUMBR);
+ input_set_capability(input, EV_KEY, BTN_THUMBL);
+ }
+
attachment->dpad_as_buttons = dpad_as_buttons;
if (attachment->dpad_as_buttons) {
input_set_capability(input, EV_KEY, BTN_DPAD_UP);
@@ -193,6 +198,11 @@ static int gip_handle_navigation_report(struct gip_attachment *attachment,
input_report_key(input, BTN_TR, bytes[1] & BIT(5));
}
+ if (attachment->quirks & GIP_QUIRK_FORCE_GAMEPAD_SB) {
+ input_report_key(input, BTN_THUMBL, bytes[1] & BIT(6));
+ input_report_key(input, BTN_THUMBR, bytes[1] & BIT(7));
+ }
+
return 0;
}
diff --git a/drivers/input/joystick/gip/gip-wheel.c b/drivers/input/joystick/gip/gip-wheel.c
new file mode 100644
index 000000000000..333910448c90
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-wheel.c
@@ -0,0 +1,349 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Drivers for GIP racing wheel devices
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+
+#include <linux/unaligned.h>
+#include "gip.h"
+
+#define GIP_QUIRK_WHEEL_FORCE_HANDBRAKE BIT(31)
+
+/* Wheel vendor messages */
+#define GIP_CMD_SET_APPLICATION_MEMORY 0x0b
+#define GIP_CMD_SET_EQUATIONS_STATES 0x0c
+#define GIP_CMD_SET_EQUATION 0x0d
+
+/* Wheel-specific flags */
+#define GIP_WHEEL_HAS_POWER BIT(3)
+#define GIP_WHEEL_HANDBRAKE_CONN BIT(4)
+#define GIP_WHEEL_CLUTCH_CONN BIT(5)
+#define GIP_WHEEL_BRAKE_CONN BIT(6)
+#define GIP_WHEEL_THROTTLE_CONN BIT(7)
+
+#define GIP_HSHIFTER_NONE 0
+#define GIP_HSHIFTER_2POS 1 /* 2 position, no neutral */
+#define GIP_HSHIFTER_2POS_N 2 /* 2 position, neutral */
+#define GIP_HSHIFTER_RTL_1TL 3 /* Reverse top left, first top left */
+#define GIP_HSHIFTER_RTL_1BL 4 /* Reverse top left, first bottom left */
+#define GIP_HSHIFTER_RBL 5 /* Reverse bottom left */
+#define GIP_HSHIFTER_RTR 6 /* Reverse top right */
+#define GIP_HSHIFTER_RBR 7 /* Reverse bottom right */
+
+struct gip_wheel_info {
+ uint8_t shifter_type: 3;
+ uint8_t max_gear: 5;
+ uint16_t angle_setting;
+ uint16_t max_angle;
+ uint16_t max_throttle;
+ uint16_t max_brake;
+ uint16_t max_clutch;
+ uint8_t max_handbrake;
+ int8_t value_retries;
+};
+
+struct gip_initial_reports_request {
+ uint8_t type;
+ uint8_t data[2];
+};
+
+static int gip_wheel_probe(struct gip_attachment *attachment)
+{
+ struct gip_wheel_info *info = kzalloc(sizeof(*info), GFP_KERNEL);
+
+ if (!info)
+ return -ENOMEM;
+ attachment->driver_data = info;
+
+ return 0;
+}
+
+static void gip_wheel_remove(struct gip_attachment *attachment)
+{
+ kfree(attachment->driver_data);
+ attachment->driver_data = NULL;
+}
+
+static int gip_wheel_init(struct gip_attachment *attachment)
+{
+ struct gip_initial_reports_request request = { 0 };
+ int rc = gip_send_vendor_message(attachment,
+ GIP_CMD_INITIAL_REPORTS_REQUEST, 0, &request,
+ sizeof(request));
+
+ if (rc < 0)
+ return rc;
+
+ return GIP_INIT_NO_INPUT;
+}
+
+static int gip_setup_wheel_input(struct gip_attachment *attachment, struct input_dev *input)
+{
+ int rc = gip_driver_navigation.setup_input(attachment, input);
+ struct gip_wheel_info *info = attachment->driver_data;
+
+ if (rc < 0)
+ return rc;
+
+ if (!info)
+ return -ENODEV;
+
+ input_set_abs_params(input, ABS_WHEEL, -info->max_angle - 1, info->max_angle, 0, 0);
+ input_abs_set_res(input, ABS_WHEEL, info->angle_setting);
+ if (info->max_throttle)
+ input_set_abs_params(input, ABS_GAS, 0, info->max_throttle, 0, 0);
+
+ if (info->max_brake)
+ input_set_abs_params(input, ABS_BRAKE, 0, info->max_brake, 0, 0);
+
+ if (info->max_clutch)
+ input_set_abs_params(input, ABS_CLUTCH, 0, info->max_clutch, 0, 0);
+
+ if (info->max_handbrake)
+ input_set_abs_params(input, ABS_HANDBRAKE, 0, info->max_handbrake, 0, 0);
+
+ if (info->shifter_type)
+ input_set_abs_params(input, ABS_SHIFTER, -1, info->max_gear, 0, 0);
+
+ return 0;
+}
+
+static int gip_handle_wheel_ll_input_report(struct gip_attachment *attachment,
+ const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+ int rc = 0;
+ struct gip_wheel_info *info = attachment->driver_data;
+
+ if (num_bytes < 17)
+ return -EINVAL;
+
+ if (!info)
+ return -ENODEV;
+
+ info->max_gear = bytes[11] & 0x1F;
+ info->shifter_type = bytes[11] >> 5;
+ info->angle_setting = get_unaligned_le16(&bytes[13]);
+
+ gip_dbg(attachment,
+ "Racing wheel with %u max gear, %u degree rotation\n",
+ info->max_gear, info->angle_setting);
+
+ if (info->angle_setting && info->max_angle) {
+ if (info->value_retries-- > 0)
+ return 0;
+
+ rc = gip_setup_input_device(attachment);
+ } else {
+ return 0;
+ }
+
+ if (rc < 0)
+ return rc;
+
+ /* Now that we're done configuring, fall back to default handler */
+ attachment->vendor_handlers[GIP_LL_INPUT_REPORT] = NULL;
+
+ return 0;
+}
+
+static int gip_handle_wheel_report(struct gip_attachment *attachment,
+ struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+ uint8_t connections;
+ struct gip_wheel_info *info = attachment->driver_data;
+ int rc = gip_driver_navigation.handle_input_report(attachment, input, bytes, num_bytes);
+
+ if (rc < 0)
+ return rc;
+
+ if (!info)
+ return -ENODEV;
+
+ if (num_bytes < 17)
+ return -EINVAL;
+
+ input_report_abs(input, ABS_WHEEL, get_unaligned_le16(&bytes[2]) - 0x8000);
+
+ connections = bytes[16];
+
+ if (attachment->quirks & GIP_QUIRK_WHEEL_FORCE_HANDBRAKE)
+ connections |= GIP_WHEEL_HANDBRAKE_CONN;
+
+ if (connections & GIP_WHEEL_THROTTLE_CONN)
+ input_report_abs(input, ABS_GAS, get_unaligned_le16(&bytes[4]));
+ else
+ input_report_abs(input, ABS_GAS, 0);
+
+ if (connections & GIP_WHEEL_BRAKE_CONN)
+ input_report_abs(input, ABS_BRAKE, get_unaligned_le16(&bytes[6]));
+ else
+ input_report_abs(input, ABS_BRAKE, 0);
+
+ if (connections & GIP_WHEEL_CLUTCH_CONN)
+ input_report_abs(input, ABS_CLUTCH, get_unaligned_le16(&bytes[8]));
+ else
+ input_report_abs(input, ABS_CLUTCH, 0);
+
+ if (connections & GIP_WHEEL_HANDBRAKE_CONN)
+ input_report_abs(input, ABS_HANDBRAKE, bytes[10]);
+ else
+ input_report_abs(input, ABS_HANDBRAKE, 0);
+
+ if (info->shifter_type)
+ input_report_abs(input, ABS_SHIFTER, (int8_t)bytes[12]);
+
+ return 0;
+}
+
+static int gip_handle_wheel_ll_static_configuration(struct gip_attachment *attachment,
+ const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+ struct gip_wheel_info *info = attachment->driver_data;
+
+ if (!info)
+ return -ENODEV;
+
+ if (num_bytes < 11)
+ return -EINVAL;
+
+ info->max_angle = BIT(min(bytes[0], 16)) - 1;
+ info->max_throttle = BIT(min(bytes[1], 16)) - 1;
+ info->max_brake = BIT(min(bytes[2], 16)) - 1;
+ info->max_clutch = BIT(min(bytes[3], 16)) - 1;
+ info->max_handbrake = BIT(min(bytes[4], 8)) - 1;
+
+ gip_dbg(attachment,
+ "Racing wheel with precision: %u bits angle, %u bits throttle, %u bits brake,\n",
+ bytes[0], bytes[1], bytes[2]);
+ gip_dbg(attachment,
+ "%u bits clutch, %u bits handbrake\n", bytes[3], bytes[4]);
+
+ if (info->angle_setting && info->max_angle && info->value_retries <= 0)
+ return gip_setup_input_device(attachment);
+
+ return 0;
+}
+
+const struct gip_driver gip_driver_wheel = {
+ .types = (const char* const[]) {
+ "Windows.Xbox.Input.Wheel",
+ "Microsoft.Xbox.Input.Wheel",
+ NULL
+ },
+ .guid = GUID_INIT(0x646979cf, 0x6b71, 0x4e96, 0x8d, 0xf9,
+ 0x59, 0xe3, 0x98, 0xd7, 0x42, 0x0c),
+
+ .quirks = (const struct gip_quirks[]) {
+ /* Thrustmaster T128X GIP Racing Wheel */
+ { GIP_VID_THRUSTMASTER, GIP_PID_THRUSTMASTER_T128X, 0,
+ .quirks = GIP_QUIRK_FORCE_GAMEPAD_SB | GIP_QUIRK_WHEEL_FORCE_HANDBRAKE, },
+
+ {0},
+ },
+
+ .probe = gip_wheel_probe,
+ .remove = gip_wheel_remove,
+ .init = gip_wheel_init,
+ .setup_input = gip_setup_wheel_input,
+ .handle_input_report = gip_handle_wheel_report,
+ .vendor_handlers = {
+ [GIP_LL_INPUT_REPORT] = gip_handle_wheel_ll_input_report,
+ [GIP_LL_STATIC_CONFIGURATION] = gip_handle_wheel_ll_static_configuration,
+ },
+};
+
+struct gip_trueforce_wheel_state {
+ struct gip_wheel_info wheel_info; /* This field must be first for type punning */
+ int8_t dial;
+};
+
+static int gip_trueforce_wheel_probe(struct gip_attachment *attachment)
+{
+ struct gip_trueforce_wheel_state *state = kzalloc(sizeof(*state), GFP_KERNEL);
+
+ if (!state)
+ return -ENOMEM;
+ attachment->driver_data = state;
+ /* The shifter won't show in the first few input reports */
+ state->wheel_info.value_retries = 3;
+
+ return 0;
+}
+
+static void gip_trueforce_wheel_remove(struct gip_attachment *attachment)
+{
+ kfree(attachment->driver_data);
+ attachment->driver_data = NULL;
+}
+
+static int gip_setup_trueforce_wheel_input(struct gip_attachment *attachment,
+ struct input_dev *input)
+{
+ int rc = gip_driver_wheel.setup_input(attachment, input);
+
+ if (rc < 0)
+ return rc;
+
+ input_set_capability(input, EV_KEY, BTN_THUMBL);
+ input_set_capability(input, EV_KEY, BTN_THUMBR);
+ input_set_capability(input, EV_KEY, KEY_KPPLUS);
+ input_set_capability(input, EV_KEY, KEY_KPMINUS);
+ input_set_capability(input, EV_KEY, KEY_KPENTER);
+ input_set_capability(input, EV_REL, REL_DIAL);
+
+ return 0;
+}
+
+static int gip_handle_trueforce_wheel_report(struct gip_attachment *attachment,
+ struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+ int rc = gip_driver_wheel.handle_input_report(attachment, input, bytes, num_bytes);
+ struct gip_trueforce_wheel_state *state = attachment->driver_data;
+ int dial;
+
+ if (rc < 0)
+ return rc;
+
+ if (num_bytes < 18)
+ return -EINVAL;
+
+ dial = bytes[17] >> 5;
+
+ input_report_key(input, BTN_THUMBL, bytes[17] & BIT(0));
+ input_report_key(input, BTN_THUMBR, bytes[17] & BIT(1));
+ input_report_key(input, KEY_KPPLUS, bytes[17] & BIT(2));
+ input_report_key(input, KEY_KPMINUS, bytes[17] & BIT(3));
+ input_report_key(input, KEY_KPENTER, bytes[17] & BIT(4));
+ if (dial == 0 && state->dial == 7)
+ input_report_rel(input, REL_DIAL, -1);
+ else if (dial == 7 && state->dial == 0)
+ input_report_rel(input, REL_DIAL, 1);
+ else
+ input_report_rel(input, REL_DIAL,
+ state->dial - dial);
+ state->dial = dial;
+
+ return 0;
+}
+
+const struct gip_driver gip_driver_trueforce_wheel = {
+ .types = (const char *const[]) { "Logi.Xbox.Input.TrueForceWheel", NULL },
+ .guid = GUID_INIT(0x6ca319e5, 0x0bc0, 0x41be, 0x83, 0x19,
+ 0x6b, 0xb7, 0x10, 0x81, 0xec, 0x55),
+
+ .probe = gip_trueforce_wheel_probe,
+ .remove = gip_trueforce_wheel_remove,
+ .init = gip_wheel_init,
+ .setup_input = gip_setup_trueforce_wheel_input,
+ .handle_input_report = gip_handle_trueforce_wheel_report,
+ .vendor_handlers = {
+ [GIP_LL_INPUT_REPORT] = gip_handle_wheel_ll_input_report,
+ [GIP_LL_STATIC_CONFIGURATION] = gip_handle_wheel_ll_static_configuration,
+ },
+};
+
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
index 23c23982658e..06cc2b3c00c8 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -27,10 +27,13 @@
#define MAX_OUT_MESSAGES 8
#define MAX_OUT_FRAGMENTS 2
+#define GIP_VID_THRUSTMASTER 0x044f
#define GIP_VID_MICROSOFT 0x045e
#define GIP_VID_PDP 0x0e6f
#define GIP_VID_BDA 0x24c6
+#define GIP_PID_THRUSTMASTER_T128X 0xb69c
+
#define GIP_PID_XBOX_ONE_1573 0x02d1
#define GIP_PID_XBOX_ONE_1697 0x02dd
#define GIP_PID_XBOX_ELITE 0x02e3
@@ -49,6 +52,7 @@
#define GIP_QUIRK_NO_IMPULSE_VIBRATION BIT(1)
#define GIP_QUIRK_SWAP_LB_RB BIT(2)
#define GIP_QUIRK_SKIP_SECURITY BIT(3)
+#define GIP_QUIRK_FORCE_GAMEPAD_SB BIT(4)
/*
* Driver-specific quirks should start from 31 and go downwards to avoid
* conflicts with newly-added core quirks
@@ -414,4 +418,6 @@ extern const struct gip_driver gip_driver_navigation;
extern const struct gip_driver gip_driver_gamepad;
extern const struct gip_driver gip_driver_arcade_stick;
extern const struct gip_driver gip_driver_pdp_jaguar;
+extern const struct gip_driver gip_driver_wheel;
+extern const struct gip_driver gip_driver_trueforce_wheel;
#endif
--
2.54.0
next prev parent reply other threads:[~2026-08-22 2:13 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-22 2:11 [PATCH v4 00/12] Input: xbox_gip - Add new driver for Xbox GIP Vicki Pfau
2026-08-22 2:11 ` [PATCH v4 01/12] " Vicki Pfau
2026-08-22 2:38 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 02/12] Input: xpad - Remove Xbox One support Vicki Pfau
2026-08-22 2:41 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 03/12] Input: xbox_gip - Add controllable LED support Vicki Pfau
2026-08-22 2:37 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 04/12] Input: xbox_gip - Add HID relaying Vicki Pfau
2026-08-22 2:38 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 05/12] Input: xbox_gip - Add battery support Vicki Pfau
2026-08-22 2:38 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 06/12] Input: xbox_gip - Allow sending fragmented and ACME messages Vicki Pfau
2026-08-22 2:39 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 07/12] Input: xbox_gip - Add security implementation from xone Vicki Pfau
2026-08-22 2:35 ` sashiko-bot
2026-08-24 21:49 ` Eric Biggers
2026-08-28 1:40 ` Vicki Pfau
2026-08-28 1:50 ` Eric Biggers
2026-08-28 1:51 ` Vicki Pfau
2026-08-22 2:11 ` [PATCH v4 08/12] Input: xbox_gip - Add arcade stick support Vicki Pfau
2026-08-22 2:38 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 09/12] Input: xbox_gip - Add support for PDP guitar controllers Vicki Pfau
2026-08-22 2:30 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 10/12] Input: Add ABS_CLUTCH, HANDBRAKE, and SHIFTER Vicki Pfau
2026-08-22 2:38 ` sashiko-bot
2026-08-22 2:11 ` [PATCH v4 11/12] HID: Map more automobile simulation inputs Vicki Pfau
2026-08-22 2:11 ` Vicki Pfau [this message]
2026-08-22 2:54 ` [PATCH v4 12/12] Input: xbox_gip - Add wheel support sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260822021140.1149546-13-vi@endrift.com \
--to=vi@endrift.com \
--cc=dmitry.torokhov@gmail.com \
--cc=linux-input@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox