feat: add Xbox One HID report layout and mapping

Phase 4 foundation, independent of the presentation mechanism:

- hid/hid_report.hpp: standard Xbox One report layout (64-byte input:
  buttons/guide/triggers/sticks; 64-byte output: motors), the HID
  report descriptor, and GIP state to report mapping.
- make_input_report(): remaps GIP button bits to the standard Xbox
  bitmask, scales 10-bit triggers to 8-bit, sticks pass through.
- parse_output_report(): extracts motor intensities from output
  reports for the rumble relay; malformed reports stop the motors.
- test_hid: unit tests for the mapping, scaling, and descriptor.

Report packing verified with a descriptor walker: input and output
are exactly 64 bytes each. Next step is presentation via DriverKit,
which needs a Developer ID with the DriverKit entitlement.

Co-Authored-By: qwen3.8-27b@q3_k_xl: implemented HID report layer
This commit is contained in:
portersky
2026-08-29 15:39:03 +02:00
parent 73c026218d
commit fafca4c265
7 changed files with 534 additions and 6 deletions
+5 -1
View File
@@ -7,10 +7,14 @@
// Preferred path: DriverKit extension implementing IOHIDDriver with an
// Xbox 360/One-compatible report descriptor (input reports from GIP, output
// reports for rumble relayed back via GIP).
//
// The standard Xbox One report layout and its HID report descriptor live in
// hid/hid_report.hpp; this module still needs the virtual device that
// presents them to macOS.
// ==============================================================================
namespace xone::hid {
// TODO(phase 4): report descriptor + virtual device presentation.
// TODO(phase 4): virtual device presentation (DriverKit IOHIDDriver).
} // namespace xone::hid
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
// ==============================================================================
// Standard Xbox One HID report layout
// ==============================================================================
// The virtual gamepad presents the standard Xbox One controller report
// layout so games that expect an Xbox controller (raw HID or via the
// GameController framework) recognize it natively.
//
// Input report (ID 1, 64 bytes):
// [0] report ID
// [1..2] buttons, little-endian u16:
// A,B,X,Y,LB,RB,LS,RS,Back,Start,DpadU,DpadD,DpadL,DpadR
// [3] guide button (0/1)
// [4] left trigger (8-bit, HID usage Z)
// [5] right trigger (8-bit, HID usage Rz)
// [6..7] left stick X (s16 LE, usage X)
// [8..9] left stick Y (s16 LE, usage Y)
// [10..11] right stick X (s16 LE, usage Rx)
// [12..13] right stick Y (s16 LE, usage Ry)
// [14..63] reserved (zero)
//
// Output report (ID 1, 64 bytes):
// [0] report ID
// [1] reserved
// [2] left motor intensity (8-bit)
// [3] right motor intensity (8-bit)
// [4..63] reserved (zero)
// ==============================================================================
namespace xone::hid {
constexpr std::uint8_t k_report_id = 1;
constexpr std::size_t k_input_report_size = 64;
constexpr std::size_t k_output_report_size = 64;
// Offsets within the input report.
constexpr std::size_t k_in_buttons = 1; // u16 LE
constexpr std::size_t k_in_guide = 3;
constexpr std::size_t k_in_trigger_left = 4;
constexpr std::size_t k_in_trigger_right = 5;
constexpr std::size_t k_in_stick_left_x = 6; // s16 LE
constexpr std::size_t k_in_stick_left_y = 8;
constexpr std::size_t k_in_stick_right_x = 10;
constexpr std::size_t k_in_stick_right_y = 12;
// Offsets within the output report.
constexpr std::size_t k_out_motor_left = 2;
constexpr std::size_t k_out_motor_right = 3;
// HID report descriptor matching the layout above (input + output).
auto xbox_one_report_descriptor() -> std::span<std::uint8_t const>;
// Normalized controller input, as tracked from GIP events. Stick Y is
// already oriented so up is positive; triggers are 10-bit values (0..1023)
// in the low bits of the u16. Buttons use the GIP bitmask (A=bit4 ...
// RS=bit15, Start=bit2, Select=bit3).
struct gamepad_input {
std::uint16_t buttons = 0;
bool guide_down = false;
std::uint16_t trigger_left = 0;
std::uint16_t trigger_right = 0;
std::int16_t stick_left_x = 0;
std::int16_t stick_left_y = 0;
std::int16_t stick_right_x = 0;
std::int16_t stick_right_y = 0;
};
// Build a standard Xbox One input report from normalized GIP state.
auto make_input_report(gamepad_input const& in)
-> std::array<std::uint8_t, k_input_report_size>;
// Motor intensities parsed from an output report (0..255 each).
struct motor_values {
std::uint8_t left = 0;
std::uint8_t right = 0;
};
// Parse a standard Xbox One output report. Returns zeroed motors (which
// stops the rumble) if the report ID or size does not match.
auto parse_output_report(std::span<std::uint8_t const> report)
-> motor_values;
} // namespace xone::hid