The open source OpenXR runtime
1// Copyright 2025, Beyley Cardellio
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief Implementation of the Vive Pro 2 HID interface.
6 * @author Beyley Cardellio <ep1cm1n10n123@gmail.com>
7 * @ingroup drv_vp2
8 */
9
10#include "os/os_hid.h"
11#include "os/os_time.h"
12
13#include "util/u_logging.h"
14#include "util/u_debug.h"
15#include "util/u_misc.h"
16
17#include "xrt/xrt_byte_order.h"
18
19#include "vp2_hid.h"
20
21
22DEBUG_GET_ONCE_LOG_OPTION(vp2_log, "VP2_LOG", U_LOGGING_WARN)
23
24// NOTE: VP2_RESOLUTION_3680_1836_90_02 is the safest resolution that works without needing the kernel patches.
25DEBUG_GET_ONCE_NUM_OPTION(vp2_resolution, "VP2_RESOLUTION", VP2_RESOLUTION_3680_1836_90_02)
26
27#define VP2_TRACE(vp2, ...) U_LOG_IFL_T(vp2->log_level, __VA_ARGS__)
28#define VP2_DEBUG(vp2, ...) U_LOG_IFL_D(vp2->log_level, __VA_ARGS__)
29#define VP2_INFO(vp2, ...) U_LOG_IFL_I(vp2->log_level, __VA_ARGS__)
30#define VP2_WARN(vp2, ...) U_LOG_IFL_W(vp2->log_level, __VA_ARGS__)
31#define VP2_ERROR(vp2, ...) U_LOG_IFL_E(vp2->log_level, __VA_ARGS__)
32
33struct vp2_hid
34{
35 enum u_logging_level log_level;
36
37 struct os_hid_device *hid;
38
39 enum vp2_resolution resolution;
40};
41
42#define VP2_DATA_SIZE 64
43
44#pragma pack(push, 1)
45
46struct vp2_feature_header
47{
48 uint8_t id;
49 __le16 sub_id;
50 uint8_t size;
51};
52
53#pragma pack(pop)
54
55static int
56vp2_write(struct vp2_hid *vp2, uint8_t id, const uint8_t *data, size_t size)
57{
58 uint8_t buffer[VP2_DATA_SIZE] = {0};
59 buffer[0] = id;
60 if (size > sizeof(buffer) - 1) {
61 VP2_ERROR(vp2, "Data size too large to write.");
62 return -1;
63 }
64
65 memcpy(&buffer[1], data, size);
66 return os_hid_write(vp2->hid, buffer, sizeof(buffer));
67}
68
69static int
70vp2_write_feature(struct vp2_hid *vp2, uint8_t id, uint16_t sub_id, const uint8_t *data, size_t size)
71{
72 struct vp2_feature_header header = {
73 .id = id,
74 .sub_id = __cpu_to_le16(sub_id),
75 .size = (uint8_t)size,
76 };
77
78 uint8_t buffer[VP2_DATA_SIZE] = {0};
79 memcpy(buffer, &header, sizeof(header));
80 if (size > sizeof(buffer) - sizeof(header)) {
81 VP2_ERROR(vp2, "Data size too large to write.");
82 return -1;
83 }
84
85 memcpy(&buffer[sizeof(header)], data, size);
86 return os_hid_set_feature(vp2->hid, buffer, sizeof(buffer));
87}
88
89int
90vp2_set_resolution(struct vp2_hid *vp2, enum vp2_resolution resolution)
91{
92 const uint8_t *wireless_command = (const uint8_t *)"wireless,0";
93 int ret = vp2_write_feature(vp2, 0x04, 0x2970, wireless_command, strlen((const char *)wireless_command));
94 if (ret < 0) {
95 VP2_ERROR(vp2, "Failed to write no wireless command.");
96 return ret;
97 }
98
99 uint8_t resolution_command[16];
100 int command_length =
101 snprintf((char *)resolution_command, sizeof(resolution_command), "dtd,%d", (uint8_t)resolution);
102
103 ret = vp2_write_feature(vp2, 0x04, 0x2970, resolution_command, command_length);
104 if (ret < 0) {
105 VP2_ERROR(vp2, "Failed to write resolution command.");
106 return ret;
107 }
108
109 return 0;
110}
111
112static int
113vp2_read(struct vp2_hid *vp2, uint8_t id, const uint8_t *prefix, size_t prefix_size, uint8_t *out_data, size_t out_size)
114{
115 uint8_t buffer[VP2_DATA_SIZE] = {0};
116 int ret = os_hid_read(vp2->hid, buffer, sizeof(buffer), 500);
117 if (ret < 0) {
118 VP2_ERROR(vp2, "Failed to read from HID device.");
119 return ret;
120 }
121 if (ret == 0) {
122 VP2_WARN(vp2, "Timeout reading from HID device.");
123 return -1;
124 }
125
126 if (buffer[0] != id) {
127 VP2_ERROR(vp2, "Unexpected report ID: got %02x, expected %02x", buffer[0], id);
128 return -1;
129 }
130
131 if (prefix_size > 0 && memcmp(&buffer[1], prefix, prefix_size) != 0) {
132 VP2_ERROR(vp2, "Unexpected report prefix.");
133 U_LOG_IFL_D_HEX(vp2->log_level, &buffer[1], prefix_size);
134 U_LOG_IFL_D_HEX(vp2->log_level, prefix, prefix_size);
135 return -1;
136 }
137
138 uint8_t size = buffer[1 + prefix_size];
139 if (size > out_size) {
140 VP2_ERROR(vp2, "Output buffer too small: got %zu, need %u", out_size, size);
141 return -1;
142 }
143
144 memcpy(out_data, &buffer[2 + prefix_size], size);
145
146 return size;
147}
148
149static int
150vp2_read_int(struct vp2_hid *vp2, const char *command, int *out_value)
151{
152 int ret;
153
154 ret = vp2_write(vp2, 0x02, (const uint8_t *)command, strlen((const char *)command));
155 if (ret < 0) {
156 VP2_ERROR(vp2, "Failed to write IPD read command.");
157 return ret;
158 }
159
160 uint8_t response[VP2_DATA_SIZE] = {0};
161 ret = vp2_read(vp2, 0x02, NULL, 0, response, sizeof(response));
162 if (ret < 0) {
163 VP2_ERROR(vp2, "Failed to read IPD response.");
164 return ret;
165 }
166
167 // Null-terminate the response string
168 response[ret] = '\0';
169
170 *out_value = strtol((const char *)response, NULL, 10);
171
172 return 0;
173}
174
175static int
176vp2_read_ipd(struct vp2_hid *vp2, int *out, int *out_min, int *out_max, int *out_lap)
177{
178 int ret;
179
180 ret = vp2_read_int(vp2, "mfg-r-ipdadc", out);
181 if (ret < 0) {
182 VP2_ERROR(vp2, "Failed to read IPD.");
183 return ret;
184 }
185
186 ret = vp2_read_int(vp2, "mfg-r-ipdmin", out_min);
187 if (ret < 0) {
188 VP2_ERROR(vp2, "Failed to read IPD min.");
189 return ret;
190 }
191
192 ret = vp2_read_int(vp2, "mfg-r-ipdmax", out_max);
193 if (ret < 0) {
194 VP2_ERROR(vp2, "Failed to read IPD max.");
195 return ret;
196 }
197
198 ret = vp2_read_int(vp2, "mfg-r-ipdlap", out_lap);
199 if (ret < 0) {
200 VP2_ERROR(vp2, "Failed to read IPD lap.");
201 return ret;
202 }
203
204 return 0;
205}
206
207int
208vp2_hid_open(struct os_hid_device *hid_dev, struct vp2_hid **out_hid)
209{
210 int ret;
211
212 struct vp2_hid *vp2 = U_TYPED_CALLOC(struct vp2_hid);
213
214 vp2->log_level = debug_get_log_option_vp2_log();
215 vp2->hid = hid_dev;
216 vp2->resolution = (enum vp2_resolution)debug_get_num_option_vp2_resolution();
217
218 *out_hid = vp2;
219
220 VP2_INFO(vp2, "Opened Vive Pro 2 HID device.");
221
222 ret = vp2_set_resolution(vp2, vp2->resolution);
223 if (ret < 0) {
224 VP2_WARN(vp2, "Failed to set resolution.");
225 }
226 os_nanosleep(U_TIME_1S_IN_NS * 3llu); // wait 3s for headset to reset
227
228 // @todo: Figure out how to compute this into a mm value.
229 int ipd, ipd_min, ipd_max, ipd_lap;
230 ret = vp2_read_ipd(vp2, &ipd, &ipd_min, &ipd_max, &ipd_lap);
231 if (ret == 0) {
232 VP2_DEBUG(vp2, "IPD: %d (min: %d, max: %d, lap: %d)", ipd, ipd_min, ipd_max, ipd_lap);
233 } else {
234 VP2_WARN(vp2, "Failed to read IPD values.");
235 }
236
237 return 0;
238}
239
240enum vp2_resolution
241vp2_get_resolution(struct vp2_hid *vp2)
242{
243 assert(vp2 != NULL);
244
245 return vp2->resolution;
246}
247
248void
249vp2_hid_destroy(struct vp2_hid *vp2)
250{
251 if (vp2->hid != NULL) {
252 os_hid_destroy(vp2->hid);
253 vp2->hid = NULL;
254 }
255
256 VP2_INFO(vp2, "Destroyed Vive Pro 2 HID device.");
257
258 free(vp2);
259}