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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.