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 v3 09/10] Input: xbox_gip - Add wheel support
Date: Mon, 9 Mar 2026 22:20:03 -0700 [thread overview]
Message-ID: <20260310052017.1289494-10-vi@endrift.com> (raw)
In-Reply-To: <20260310052017.1289494-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 | 2 +-
drivers/input/joystick/gip/gip-core.c | 4 +-
drivers/input/joystick/gip/gip-drivers.c | 10 +
drivers/input/joystick/gip/gip-wheel.c | 348 +++++++++++++++++++++++
drivers/input/joystick/gip/gip.h | 4 +
5 files changed, 366 insertions(+), 2 deletions(-)
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 94ce6462d7ab0..db6c9079c7e18 100644
--- a/drivers/input/joystick/gip/Makefile
+++ b/drivers/input/joystick/gip/Makefile
@@ -1,3 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-or-later
obj-$(CONFIG_JOYSTICK_XBOX_GIP) += xbox-gip.o
-xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o
+xbox-gip-y := gip-arcade-stick.o gip-core.o gip-drivers.o gip-wheel.o
diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
index 1164b689856e7..773d7705b7be8 100644
--- a/drivers/input/joystick/gip/gip-core.c
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -10,7 +10,7 @@
* - Event logging
* - Sending fragmented messages
* - Raw character device
- * - Wheel support
+ * - Wheel force feedback
* - Flight stick support
* - More arcade stick testing
* - Arcade stick extra buttons
@@ -317,6 +317,8 @@ static const struct gip_driver* base_drivers[] = {
&gip_driver_navigation,
&gip_driver_gamepad,
&gip_driver_arcade_stick,
+ &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 f5507e6215a94..e15f580d371d8 100644
--- a/drivers/input/joystick/gip/gip-drivers.c
+++ b/drivers/input/joystick/gip/gip-drivers.c
@@ -140,6 +140,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);
@@ -191,6 +196,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 0000000000000..34e7795f16534
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-wheel.c
@@ -0,0 +1,348 @@
+// 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 "gip.h"
+
+/* 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 = bytes[13];
+ info->angle_setting |= bytes[14] << 8;
+
+ 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)
+{
+ int32_t axis;
+ 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;
+
+ axis = bytes[2];
+ axis |= bytes[3] << 8;
+ input_report_abs(input, ABS_WHEEL, axis - 0x8000);
+
+ connections = bytes[16];
+
+ if (attachment->quirks & GIP_QUIRK_WHEEL_FORCE_HANDBRAKE)
+ connections |= GIP_WHEEL_HANDBRAKE_CONN;
+
+ if (connections & GIP_WHEEL_THROTTLE_CONN) {
+ axis = bytes[4];
+ axis |= bytes[5] << 8;
+ input_report_abs(input, ABS_GAS, axis);
+ } else {
+ input_report_abs(input, ABS_GAS, 0);
+ }
+
+ if (connections & GIP_WHEEL_BRAKE_CONN) {
+ axis = bytes[6];
+ axis |= bytes[7] << 8;
+ input_report_abs(input, ABS_BRAKE, axis);
+ } else {
+ input_report_abs(input, ABS_BRAKE, 0);
+ }
+
+ if (connections & GIP_WHEEL_CLUTCH_CONN) {
+ axis = bytes[8];
+ axis |= bytes[9] << 8;
+ input_report_abs(input, ABS_CLUTCH, axis);
+ } 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(bytes[0]) - 1;
+ info->max_throttle = BIT(bytes[1]) - 1;
+ info->max_brake = BIT(bytes[2]) - 1;
+ info->max_clutch = BIT(bytes[3]) - 1;
+ info->max_handbrake = BIT(bytes[4]) - 1;
+
+ 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 */
+ { 0x044f, 0xb69c, 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 up until the second input report */
+ state->wheel_info.value_retries = 1;
+
+ 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 6e5ab8f357aaa..3b5cab96dc0b7 100644
--- a/drivers/input/joystick/gip/gip.h
+++ b/drivers/input/joystick/gip/gip.h
@@ -31,6 +31,8 @@
#define GIP_QUIRK_NO_HELLO BIT(0)
#define GIP_QUIRK_NO_IMPULSE_VIBRATION BIT(1)
#define GIP_QUIRK_SWAP_LB_RB BIT(2)
+#define GIP_QUIRK_FORCE_GAMEPAD_SB BIT(3)
+#define GIP_QUIRK_WHEEL_FORCE_HANDBRAKE BIT(31)
#define GIP_FEATURE_CONTROLLER BIT(0)
#define GIP_FEATURE_CONSOLE_FUNCTION_MAP BIT(1)
@@ -319,4 +321,6 @@ int gip_send_vendor_message(struct gip_attachment *attachment,
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_wheel;
+extern const struct gip_driver gip_driver_trueforce_wheel;
#endif
--
2.53.0
next prev parent reply other threads:[~2026-03-10 5:20 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-10 5:19 [PATCH v3 00/10] Input: xbox_gip - Add new driver for Xbox GIP Vicki Pfau
2026-03-10 5:19 ` [PATCH v3 01/10] " Vicki Pfau
2026-03-11 0:41 ` Vicki Pfau
2026-03-10 5:19 ` [PATCH v3 02/10] Input: xpad - Remove Xbox One support Vicki Pfau
2026-03-10 5:19 ` [PATCH v3 03/10] Input: xbox_gip - Add controllable LED support Vicki Pfau
2026-03-10 5:19 ` [PATCH v3 04/10] Input: xbox_gip - Add HID relaying Vicki Pfau
2026-03-10 5:19 ` [PATCH v3 05/10] Input: xbox_gip - Add battery support Vicki Pfau
2026-03-10 5:20 ` [PATCH v3 06/10] Input: xbox_gip - Add arcade stick support Vicki Pfau
2026-03-10 5:20 ` [PATCH v3 07/10] Input: Add ABS_CLUTCH, HANDBRAKE, and SHIFTER Vicki Pfau
2026-03-10 5:20 ` [PATCH v3 08/10] HID: Map more automobile simulation inputs Vicki Pfau
2026-03-10 5:20 ` Vicki Pfau [this message]
2026-03-10 5:20 ` [PATCH v3 10/10] Input: xbox_gip - Add flight stick support Vicki Pfau
2026-03-10 5:23 ` Vicki Pfau
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=20260310052017.1289494-10-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