Parse a HID report descriptor into a tablet layout.
Free of device state and of HIDAPI, so captured descriptor bytes can be parsed and asserted against with no hardware present.
250{
251
252 TabletLayout layout;
253
254 GlobalState global;
255 std::vector<GlobalState> global_stack;
256
257 std::vector<LocalUsage> local_usages;
258 uint32_t usage_min = 0;
259 uint32_t usage_max = 0;
260 bool has_usage_range = false;
261
262 std::unordered_map<uint8_t, uint32_t> bit_cursor;
263 bool digitizer_seen = false;
264
265 const auto clear_locals = [&]() {
266 local_usages.clear();
267 usage_min = 0;
268 usage_max = 0;
269 has_usage_range = false;
270 };
271
272 size_t i = 0;
273 while (i < descriptor.size()) {
274 const uint8_t prefix = descriptor[i];
275
276 if (prefix == 0xFE) {
277 if (i + 1 >= descriptor.size())
278 break;
279 i += 2U + descriptor[i + 1];
280 continue;
281 }
282
283 size_t size = prefix & 0x03U;
284 if (size == 3)
285 size = 4;
286 const uint8_t type = (prefix >> 2U) & 0x03U;
287 const uint8_t tag = (prefix >> 4U) & 0x0FU;
288
289 ++i;
290 if (i + size > descriptor.size())
291 break;
292
293 const uint8_t* payload = descriptor.data() + i;
294 i += size;
295
296 if (type == 1) {
297 switch (tag) {
298 case 0x0:
299 global.usage_page = static_cast<uint16_t>(item_unsigned(payload, size));
300 if (global.usage_page == k_page_digitizer)
301 digitizer_seen = true;
302 break;
303
304 case 0x1:
305 global.logical_min = item_signed(payload, size);
306 break;
307
308 case 0x2: {
309 const int32_t as_signed = item_signed(payload, size);
310 global.logical_max = (global.logical_min >= 0 && as_signed < 0)
311 ? static_cast<int32_t>(item_unsigned(payload, size))
312 : as_signed;
313 break;
314 }
315
316 case 0x7:
317 global.report_size = item_unsigned(payload, size);
318 break;
319
320 case 0x8:
321 global.report_id = static_cast<uint8_t>(item_unsigned(payload, size));
322 layout.uses_report_ids = true;
323 break;
324
325 case 0x9:
326 global.report_count = item_unsigned(payload, size);
327 break;
328
329 case 0xA:
330 global_stack.push_back(global);
331 break;
332
333 case 0xB:
334 if (!global_stack.empty()) {
335 global = global_stack.back();
336 global_stack.pop_back();
337 }
338 break;
339
340 default:
341 break;
342 }
343 continue;
344 }
345
346 if (type == 2) {
347 switch (tag) {
348 case 0x0:
349 if (size == 4) {
350 const uint32_t full = item_unsigned(payload, size);
351 const auto page =
static_cast<uint16_t
>(full >> 16U);
352 local_usages.push_back({ .page =
page, .usage =
static_cast<uint16_t
>(full & 0xFFFFU) });
353 if (
page == k_page_digitizer)
354 digitizer_seen = true;
355 } else {
356 local_usages.push_back({ .page = global.usage_page,
357 .usage = static_cast<uint16_t>(item_unsigned(payload, size)) });
358 }
359 break;
360
361 case 0x1:
362 usage_min = item_unsigned(payload, size);
363 has_usage_range = true;
364 break;
365
366 case 0x2:
367 usage_max = item_unsigned(payload, size);
368 has_usage_range = true;
369 break;
370
371 default:
372 break;
373 }
374 continue;
375 }
376
377 if (type != 0) {
378 continue;
379 }
380
381 if (tag != 0x8) {
382 clear_locals();
383 continue;
384 }
385
386 const uint32_t flags = item_unsigned(payload, size);
387 const bool is_constant = (flags & 0x01U) != 0U;
388
389 if (global.report_size == 0 || global.report_count == 0) {
390 clear_locals();
391 continue;
392 }
393
394 uint32_t& cursor = bit_cursor[global.report_id];
395
396 for (uint32_t index = 0; index < global.report_count; ++index) {
397 LocalUsage
current { .page = global.usage_page, .usage = 0 };
398
399 if (!is_constant) {
400 if (index < local_usages.size()) {
402 } else if (!local_usages.empty()) {
404 } else if (has_usage_range) {
405 current.usage =
static_cast<uint16_t
>(
406 std::min(usage_min + index, usage_max));
407 }
408 }
409
410 const size_t slot = is_constant
411 ? k_slot_none
413
414 if (slot != k_slot_none) {
415 TabletField field;
416 field.report_id = global.report_id;
417 field.usage_page =
current.page;
419 field.bit_offset = cursor;
420 field.bit_size = global.report_size;
421 field.logical_min = global.logical_min;
422 field.logical_max = global.logical_max;
423 field.slot = slot;
424 layout.fields.push_back(field);
425
427 layout.axes |= axis_for_slot(slot);
428 if (slot == k_pseudo_invert)
429 layout.has_invert = true;
430 if (slot == k_pseudo_serial)
431 layout.has_serial = true;
432 }
433
434 cursor += global.report_size;
435 }
436
437 clear_locals();
438 }
439
440 if (!digitizer_seen)
441 layout.fields.clear();
442
443 return layout;
444}
static constexpr size_t SLOT_COUNT