11 constexpr uint16_t k_page_generic = 0x01;
12 constexpr uint16_t k_page_digitizer = 0x0D;
14 constexpr uint16_t k_usage_x = 0x30;
15 constexpr uint16_t k_usage_y = 0x31;
16 constexpr uint16_t k_usage_z = 0x32;
17 constexpr uint16_t k_usage_wheel = 0x38;
19 constexpr uint16_t k_usage_digitizer = 0x01;
20 constexpr uint16_t k_usage_pen = 0x02;
21 constexpr uint16_t k_usage_tip_pressure = 0x30;
22 constexpr uint16_t k_usage_barrel_pressure = 0x31;
23 constexpr uint16_t k_usage_in_range = 0x32;
24 constexpr uint16_t k_usage_invert = 0x3C;
25 constexpr uint16_t k_usage_tilt_x = 0x3D;
26 constexpr uint16_t k_usage_tilt_y = 0x3E;
27 constexpr uint16_t k_usage_twist = 0x41;
28 constexpr uint16_t k_usage_tip_switch = 0x42;
29 constexpr uint16_t k_usage_barrel_switch = 0x44;
30 constexpr uint16_t k_usage_eraser = 0x45;
31 constexpr uint16_t k_usage_barrel_switch_2 = 0x5A;
32 constexpr uint16_t k_usage_serial = 0x5B;
49 size_t slot_for_usage(uint16_t
page, uint16_t
usage)
noexcept
51 if (
page == k_page_generic) {
66 if (
page != k_page_digitizer)
70 case k_usage_tip_pressure:
72 case k_usage_barrel_pressure:
80 case k_usage_in_range:
81 return k_pseudo_in_range;
82 case k_usage_tip_switch:
85 return k_pseudo_invert;
86 case k_usage_barrel_switch:
87 return k_pseudo_barrel;
88 case k_usage_barrel_switch_2:
89 return k_pseudo_barrel_2;
91 return k_pseudo_eraser;
93 return k_pseudo_serial;
123 int32_t item_signed(
const uint8_t* data,
size_t bytes)
noexcept
126 for (
size_t i = 0; i < bytes; ++i)
127 value |=
static_cast<int32_t
>(data[i]) << (8U * i);
129 if (bytes > 0 && bytes < 4) {
130 const auto sign_bit =
static_cast<int32_t
>(1U << (8U * bytes - 1U));
131 if ((
value & sign_bit) != 0)
132 value -= (sign_bit << 1);
137 uint32_t item_unsigned(
const uint8_t* data,
size_t bytes)
noexcept
140 for (
size_t i = 0; i < bytes; ++i)
141 value |=
static_cast<uint32_t
>(data[i]) << (8U * i);
148 int32_t extract_field(std::span<const uint8_t> report,
const TabletField& field)
noexcept
151 for (uint32_t i = 0; i < field.bit_size; ++i) {
152 const uint32_t bit = field.bit_offset + i;
153 const size_t byte = bit / 8U;
154 if (
byte >= report.size())
156 if (((report[
byte] >> (bit % 8U)) & 1U) != 0U)
160 if (field.logical_min < 0 && field.bit_size > 0 && field.bit_size < 32) {
161 const auto sign_bit = 1U << (field.bit_size - 1U);
162 if ((raw & sign_bit) != 0
U)
163 return static_cast<int32_t
>(raw) -
static_cast<int32_t
>(sign_bit << 1U);
165 return static_cast<int32_t
>(raw);
168 double normalise(int32_t raw, int32_t
lo, int32_t
hi)
noexcept
172 const double span =
static_cast<double>(
hi) -
static_cast<double>(
lo);
173 const double v = (
static_cast<double>(raw) -
static_cast<double>(
lo)) / span;
174 return std::clamp(v, 0.0, 1.0);
177 double to_degrees(int32_t raw, int32_t
lo, int32_t
hi,
double limit)
noexcept
181 return (normalise(raw,
lo,
hi) * 2.0 - 1.0) * limit;
184 void set_state_bit(std::array<double, TabletFrame::SLOT_COUNT>& slots,
189 current |=
static_cast<uint32_t
>(bit);
191 current &= ~static_cast<uint32_t>(bit);
196 void set_button_bit(std::array<double, TabletFrame::SLOT_COUNT>& slots,
197 uint32_t bit,
bool on)
noexcept
203 mask &= ~(1U << bit);
208 std::string widen_to_narrow(
const wchar_t* ws)
214 result.resize(
source.size());
215 std::ranges::transform(source, result.begin(),
216 [](
wchar_t c) { return static_cast<char>(c); });
255 std::vector<GlobalState> global_stack;
257 std::vector<LocalUsage> local_usages;
258 uint32_t usage_min = 0;
259 uint32_t usage_max = 0;
260 bool has_usage_range =
false;
262 std::unordered_map<uint8_t, uint32_t> bit_cursor;
263 bool digitizer_seen =
false;
265 const auto clear_locals = [&]() {
266 local_usages.clear();
269 has_usage_range =
false;
273 while (i < descriptor.size()) {
274 const uint8_t prefix = descriptor[i];
276 if (prefix == 0xFE) {
277 if (i + 1 >= descriptor.size())
279 i += 2U + descriptor[i + 1];
283 size_t size = prefix & 0x03U;
286 const uint8_t type = (prefix >> 2U) & 0x03U;
287 const uint8_t tag = (prefix >> 4U) & 0x0FU;
290 if (i + size > descriptor.size())
293 const uint8_t* payload = descriptor.data() + i;
299 global.usage_page =
static_cast<uint16_t
>(item_unsigned(payload, size));
300 if (global.usage_page == k_page_digitizer)
301 digitizer_seen =
true;
305 global.logical_min = item_signed(payload, size);
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))
317 global.report_size = item_unsigned(payload, size);
321 global.report_id =
static_cast<uint8_t
>(item_unsigned(payload, size));
326 global.report_count = item_unsigned(payload, size);
330 global_stack.push_back(global);
334 if (!global_stack.empty()) {
335 global = global_stack.back();
336 global_stack.pop_back();
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;
356 local_usages.push_back({ .page = global.usage_page,
357 .usage =
static_cast<uint16_t
>(item_unsigned(payload, size)) });
362 usage_min = item_unsigned(payload, size);
363 has_usage_range =
true;
367 usage_max = item_unsigned(payload, size);
368 has_usage_range =
true;
386 const uint32_t flags = item_unsigned(payload, size);
387 const bool is_constant = (flags & 0x01U) != 0U;
389 if (global.report_size == 0 || global.report_count == 0) {
394 uint32_t& cursor = bit_cursor[global.report_id];
396 for (uint32_t index = 0; index < global.report_count; ++index) {
397 LocalUsage
current { .page = global.usage_page, .usage = 0 };
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));
410 const size_t slot = is_constant
414 if (slot != k_slot_none) {
420 field.
bit_size = global.report_size;
424 layout.
fields.push_back(field);
427 layout.
axes |= axis_for_slot(slot);
428 if (slot == k_pseudo_invert)
430 if (slot == k_pseudo_serial)
434 cursor += global.report_size;
477 if (hid_init() != 0) {
479 "Failed to initialize HIDAPI for tablet backend");
488 "TabletBackend initialized, {} tool(s)", found);
497 "Cannot start TabletBackend: not initialized");
510 "TabletBackend started");
539 if (device->handle) {
540 hid_close(device->handle);
541 device->handle =
nullptr;
553 "TabletBackend shutdown complete");
566 hid_device_info* devs = hid_enumerate(0x0, 0x0);
568 for (hid_device_info* cur = devs; cur !=
nullptr; cur = cur->next) {
570 && cur->usage_page != k_page_digitizer
571 && cur->usage_page != 0)
574 std::string path(cur->path);
582 std::string name = widen_to_narrow(cur->product_string);
586 adopt_device(path, cur->vendor_id, cur->product_id, std::move(name));
589 hid_free_enumeration(devs);
596 uint16_t pid, std::string name)
598 hid_device* handle = hid_open_path(path.c_str());
603 std::vector<uint8_t> descriptor(4096);
604 const int written = hid_get_report_descriptor(handle,
605 descriptor.data(), descriptor.size());
611 descriptor.resize(
static_cast<size_t>(written));
614 if (layout.
fields.empty()) {
619 hid_set_nonblocking(handle, 0);
621 auto device = std::make_shared<TabletDevice>();
622 device->handle = handle;
624 device->name = std::move(name);
625 device->vendor_id = vid;
626 device->product_id = pid;
627 device->layout = std::move(layout);
629 device->active.store(
true);
631 std::vector<TabletToolInfo> announced;
644 pen.
name = device->name +
" Pen";
646 pen.
axes = device->layout.axes;
648 device->pen_id = pen.
id;
651 announced.push_back(pen);
656 eraser.
name = device->name +
" Eraser";
659 device->eraser_id = eraser.
id;
662 announced.push_back(eraser);
664 device->eraser_id = device->pen_id;
671 "Tablet adopted: {} (VID:{:04X} PID:{:04X}, {} field(s))",
672 device->name, vid, pid, device->layout.fields.size());
674 for (
const auto& info : announced) {
688 std::vector<std::shared_ptr<TabletDevice>> snapshot;
694 if (device->active.load() && device->handle)
695 snapshot.push_back(device);
699 if (snapshot.empty()) {
700 std::this_thread::sleep_for(std::chrono::milliseconds(20));
704 for (
auto& device : snapshot) {
712 const int bytes = hid_read_timeout(device.
handle,
718 std::span<const uint8_t>(device.
read_buffer.data(),
719 static_cast<size_t>(bytes)));
725 "Tablet read error on {}", device.
name);
726 device.
active.store(
false);
740 std::span<const uint8_t> body = report;
744 body = report.subspan(1);
747 bool matched =
false;
748 bool eraser_flag =
false;
749 bool invert_seen =
false;
756 const int32_t raw = extract_field(body, field);
758 switch (field.slot) {
763 device.
slots[field.slot] = normalise(raw, field.logical_min, field.logical_max);
768 device.
slots[field.slot] = to_degrees(raw, field.logical_min, field.logical_max, 90.0);
772 device.
slots[field.slot] = to_degrees(raw, field.logical_min, field.logical_max, 180.0);
776 device.
slots[field.slot] = normalise(raw, field.logical_min, field.logical_max) * 2.0 - 1.0;
784 case k_pseudo_in_range:
792 case k_pseudo_invert:
794 eraser_flag = eraser_flag || (raw != 0);
797 case k_pseudo_eraser:
798 eraser_flag = eraser_flag || (raw != 0);
799 set_button_bit(device.
slots, 2, raw != 0);
802 case k_pseudo_barrel:
803 set_button_bit(device.
slots, 0, raw != 0);
806 case k_pseudo_barrel_2:
807 set_button_bit(device.
slots, 1, raw != 0);
810 case k_pseudo_serial: {
814 it->second.hardware_serial =
static_cast<uint64_t
>(
815 static_cast<uint32_t
>(raw));
841 value.data = std::vector<double>(device.
slots.begin(), device.
slots.end());
842 value.timestamp_ns =
static_cast<uint64_t
>(
843 std::chrono::steady_clock::now().time_since_epoch().count());
861 std::vector<InputDeviceInfo> result;
862 result.reserve(
m_tools.size());
863 for (
const auto& [
id, info] :
m_tools) {
864 result.push_back(info);
877 std::shared_ptr<TabletDevice> device;
885 path = path_it->second;
890 device = dev_it->second;
893 device->active.store(
false);
903 return dev !=
m_devices.end() && dev->second->active.load();
910 std::vector<uint32_t> result;
911 result.reserve(
m_tools.size());
912 for (
const auto& [
id, info] :
m_tools) {
913 result.push_back(
id);
921 auto it =
m_tools.find(device_id);
935 auto dev_it =
m_devices.find(path_it->second);
939 return dev_it->second->layout;
944 const hid_api_version* ver = hid_version();
946 return "HIDAPI unknown";
947 return "HIDAPI " + std::to_string(ver->major) +
"."
948 + std::to_string(ver->minor) +
"." + std::to_string(ver->patch);
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
void stop() override
Stop listening for input events.
std::thread m_poll_thread
std::string get_version() const override
Get backend version string.
std::atomic< bool > m_initialized
bool is_device_open(uint32_t device_id) const override
Check if a device is currently open.
void shutdown() override
Shutdown and release all resources.
DeviceCallback m_device_callback
std::mutex m_callback_mutex
std::atomic< bool > m_stop_requested
InputCallback m_input_callback
std::optional< TabletLayout > get_layout(uint32_t device_id) const
Parsed report layout for the device backing a tool.
void set_input_callback(InputCallback callback) override
Register callback for input values.
uint32_t m_next_device_id
void notify_device_change(const InputDeviceInfo &info, bool connected)
std::optional< TabletToolInfo > get_tool_info(uint32_t device_id) const
Extended information for a tool.
void set_device_callback(DeviceCallback callback) override
Register callback for device connect/disconnect events.
std::vector< uint32_t > get_open_devices() const override
Get list of currently open device IDs.
~TabletBackend() override
static TabletLayout parse_descriptor(std::span< const uint8_t > descriptor)
Parse a HID report descriptor into a tablet layout.
void poll_device(TabletDevice &device)
void notify_input(const InputValue &value)
bool open_device(uint32_t device_id) override
Open a device for input.
bool initialize() override
Initialize the input backend.
std::unordered_map< uint32_t, std::string > m_tool_paths
void emit_frame(TabletDevice &device)
size_t refresh_devices() override
Refresh the device list.
void start() override
Start listening for input events.
std::vector< InputDeviceInfo > get_devices() const override
Get list of available devices.
void unpack_report(TabletDevice &device, std::span< const uint8_t > report)
std::unordered_map< std::string, std::shared_ptr< TabletDevice > > m_devices
std::mutex m_devices_mutex
std::unordered_map< uint32_t, TabletToolInfo > m_tools
void close_device(uint32_t device_id) override
Close a previously opened device.
std::atomic< bool > m_running
bool adopt_device(const std::string &path, uint16_t vid, uint16_t pid, std::string name)
Cross-platform tablet and stylus backend over raw HID.
@ TABLET
Digitizers and styluses (pressure, tilt, rotation)
std::function< void(const InputValue &)> InputCallback
Callback signature for input events.
std::function< void(const InputDeviceInfo &, bool connected)> DeviceCallback
Callback signature for device connection/disconnection events.
TabletState
State bits packed into the STATE slot.
TabletAxes
Axes a tool actually reports.
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
bool split_eraser
Report the eraser end as its own tool.
int poll_timeout_ms
Timeout for hid_read_timeout.
size_t read_buffer_size
Per-device read buffer.
bool probe_all_devices
Read every HID descriptor rather than trusting enumeration.
Configuration for the tablet backend.
std::atomic< bool > active
std::vector< uint8_t > read_buffer
std::array< double, TabletFrame::SLOT_COUNT > slots
One physical HID device and the tools it presents.
size_t slot
SLOT_COUNT means not a slot field.
One field located in a report by descriptor parsing.
@ DISTANCE
0.0 to 1.0, hover height
@ ROTATION
-180.0 to 180.0 degrees
@ X
0.0 to 1.0 across the active area
@ WHEEL_CLICKS
Integral detents this sample.
@ STATE
TabletState bitmask.
@ TILT_Y
-90.0 to 90.0 degrees
@ WHEEL
Degrees of wheel rotation this sample.
@ SLIDER
-1.0 to 1.0, barrel pressure or finger wheel
@ Y
0.0 to 1.0 across the active area
@ TILT_X
-90.0 to 90.0 degrees
@ BUTTONS
Bit 0 barrel, bit 1 secondary barrel, bit 2 eraser.
static constexpr size_t SLOT_COUNT
std::vector< TabletField > fields
Parsed layout of one device's input reports.