7#ifdef MAYAFLUX_PLATFORM_MACOS
8#include <hidapi_darwin.h>
15 constexpr uint16_t k_page_generic = 0x01;
16 constexpr uint16_t k_page_digitizer = 0x0D;
18 constexpr uint16_t k_usage_x = 0x30;
19 constexpr uint16_t k_usage_y = 0x31;
20 constexpr uint16_t k_usage_z = 0x32;
21 constexpr uint16_t k_usage_wheel = 0x38;
23 constexpr uint16_t k_usage_digitizer = 0x01;
24 constexpr uint16_t k_usage_pen = 0x02;
25 constexpr uint16_t k_usage_tip_pressure = 0x30;
26 constexpr uint16_t k_usage_barrel_pressure = 0x31;
27 constexpr uint16_t k_usage_in_range = 0x32;
28 constexpr uint16_t k_usage_invert = 0x3C;
29 constexpr uint16_t k_usage_tilt_x = 0x3D;
30 constexpr uint16_t k_usage_tilt_y = 0x3E;
31 constexpr uint16_t k_usage_twist = 0x41;
32 constexpr uint16_t k_usage_tip_switch = 0x42;
33 constexpr uint16_t k_usage_barrel_switch = 0x44;
34 constexpr uint16_t k_usage_eraser = 0x45;
35 constexpr uint16_t k_usage_barrel_switch_2 = 0x5A;
36 constexpr uint16_t k_usage_serial = 0x5B;
53 size_t slot_for_usage(uint16_t
page, uint16_t
usage)
noexcept
55 if (
page == k_page_generic) {
70 if (
page != k_page_digitizer)
74 case k_usage_tip_pressure:
76 case k_usage_barrel_pressure:
84 case k_usage_in_range:
85 return k_pseudo_in_range;
86 case k_usage_tip_switch:
89 return k_pseudo_invert;
90 case k_usage_barrel_switch:
91 return k_pseudo_barrel;
92 case k_usage_barrel_switch_2:
93 return k_pseudo_barrel_2;
95 return k_pseudo_eraser;
97 return k_pseudo_serial;
103 TabletAxes axis_for_slot(
size_t slot)
noexcept
127 int32_t item_signed(
const uint8_t* data,
size_t bytes)
noexcept
130 for (
size_t i = 0; i < bytes; ++i)
131 value |=
static_cast<int32_t
>(data[i]) << (8U * i);
133 if (bytes > 0 && bytes < 4) {
134 const auto sign_bit =
static_cast<int32_t
>(1U << (8U * bytes - 1U));
135 if ((
value & sign_bit) != 0)
136 value -= (sign_bit << 1);
141 uint32_t item_unsigned(
const uint8_t* data,
size_t bytes)
noexcept
144 for (
size_t i = 0; i < bytes; ++i)
145 value |=
static_cast<uint32_t
>(data[i]) << (8U * i);
152 int32_t extract_field(std::span<const uint8_t> report,
const TabletField& field)
noexcept
155 for (uint32_t i = 0; i < field.bit_size; ++i) {
156 const uint32_t bit = field.bit_offset + i;
157 const size_t byte = bit / 8U;
158 if (
byte >= report.size())
160 if (((report[
byte] >> (bit % 8U)) & 1U) != 0U)
164 if (field.logical_min < 0 && field.bit_size > 0 && field.bit_size < 32) {
165 const auto sign_bit = 1U << (field.bit_size - 1U);
166 if ((raw & sign_bit) != 0
U)
167 return static_cast<int32_t
>(raw) -
static_cast<int32_t
>(sign_bit << 1U);
169 return static_cast<int32_t
>(raw);
172 double normalise(int32_t raw, int32_t lo, int32_t hi)
noexcept
176 const double span =
static_cast<double>(hi) -
static_cast<double>(lo);
177 const double v = (
static_cast<double>(raw) -
static_cast<double>(lo)) / span;
178 return std::clamp(v, 0.0, 1.0);
181 double to_degrees(int32_t raw, int32_t lo, int32_t hi,
double limit)
noexcept
185 return (normalise(raw, lo, hi) * 2.0 - 1.0) * limit;
188 void set_state_bit(std::array<double, TabletFrame::SLOT_COUNT>& slots,
193 current |=
static_cast<uint32_t
>(bit);
195 current &= ~static_cast<uint32_t>(bit);
200 void set_button_bit(std::array<double, TabletFrame::SLOT_COUNT>& slots,
201 uint32_t bit,
bool on)
noexcept
207 mask &= ~(1U << bit);
221 return text.empty() ? std::string(
"no detail from HIDAPI") : text;
259 std::vector<GlobalState> global_stack;
261 std::vector<LocalUsage> local_usages;
262 uint32_t usage_min = 0;
263 uint32_t usage_max = 0;
264 bool has_usage_range =
false;
266 std::unordered_map<uint8_t, uint32_t> bit_cursor;
267 bool digitizer_seen =
false;
269 const auto clear_locals = [&]() {
270 local_usages.clear();
273 has_usage_range =
false;
277 while (i < descriptor.size()) {
278 const uint8_t prefix = descriptor[i];
280 if (prefix == 0xFE) {
281 if (i + 1 >= descriptor.size())
283 i += 2U + descriptor[i + 1];
287 size_t size = prefix & 0x03U;
290 const uint8_t
type = (prefix >> 2U) & 0x03U;
291 const uint8_t tag = (prefix >> 4U) & 0x0FU;
294 if (i +
size > descriptor.size())
297 const uint8_t*
payload = descriptor.data() + i;
303 global.usage_page =
static_cast<uint16_t
>(item_unsigned(
payload,
size));
304 if (global.usage_page == k_page_digitizer)
305 digitizer_seen =
true;
313 const int32_t as_signed = item_signed(
payload,
size);
314 global.logical_max = (global.logical_min >= 0 && as_signed < 0)
315 ?
static_cast<int32_t
>(item_unsigned(
payload,
size))
325 global.report_id =
static_cast<uint8_t
>(item_unsigned(
payload,
size));
330 global.report_count = item_unsigned(
payload,
size);
334 global_stack.push_back(global);
338 if (!global_stack.empty()) {
339 global = global_stack.back();
340 global_stack.pop_back();
354 const uint32_t full = item_unsigned(
payload,
size);
355 const auto page =
static_cast<uint16_t
>(full >> 16U);
356 local_usages.push_back({ .page =
page, .usage =
static_cast<uint16_t
>(full & 0xFFFFU) });
357 if (
page == k_page_digitizer)
358 digitizer_seen =
true;
360 local_usages.push_back({ .page = global.usage_page,
361 .usage =
static_cast<uint16_t
>(item_unsigned(
payload,
size)) });
367 has_usage_range =
true;
372 has_usage_range =
true;
390 const uint32_t flags = item_unsigned(
payload,
size);
391 const bool is_constant = (flags & 0x01U) != 0U;
393 if (global.report_size == 0 || global.report_count == 0) {
398 uint32_t& cursor = bit_cursor[global.report_id];
401 LocalUsage
current { .page = global.usage_page, .usage = 0 };
404 if (
index < local_usages.size()) {
406 }
else if (!local_usages.empty()) {
408 }
else if (has_usage_range) {
409 current.usage =
static_cast<uint16_t
>(
410 std::min(usage_min +
index, usage_max));
414 const size_t slot = is_constant
418 if (slot != k_slot_none) {
424 field.
bit_size = global.report_size;
428 layout.
fields.push_back(field);
431 layout.
axes |= axis_for_slot(slot);
432 if (slot == k_pseudo_invert)
434 if (slot == k_pseudo_serial)
438 cursor += global.report_size;
481 if (hid_init() != 0) {
483 "Failed to initialize HIDAPI for tablet backend: {}",
484 hid_failure_reason(
nullptr));
488#ifdef MAYAFLUX_PLATFORM_MACOS
489 hid_darwin_set_open_exclusive(0);
497 "TabletBackend initialized, {} tool(s)", found);
506 "Cannot start TabletBackend: not initialized");
519 "TabletBackend started");
549 hid_close(
device->handle);
562 "TabletBackend shutdown complete");
575 hid_device_info* devs = hid_enumerate(0x0, 0x0);
577 for (hid_device_info* cur = devs; cur !=
nullptr; cur = cur->next) {
579 && cur->usage_page != k_page_digitizer
580 && cur->usage_page != 0)
583 std::string path(cur->path);
598 hid_free_enumeration(devs);
605 uint16_t pid, std::string
name)
607 hid_device* handle = hid_open_path(path.c_str());
610 "Could not open HID device {} ({}): {}",
611 name, path, hid_failure_reason(
nullptr));
615 std::vector<uint8_t> descriptor(4096);
616 const int written = hid_get_report_descriptor(handle,
617 descriptor.data(), descriptor.size());
621 "Could not read report descriptor of {} ({}): {}",
622 name, path, hid_failure_reason(handle));
626 descriptor.resize(
static_cast<size_t>(written));
629 if (layout.
fields.empty()) {
634 hid_set_nonblocking(handle, 0);
636 auto device = std::make_shared<TabletDevice>();
642 device->layout = std::move(layout);
644 device->active.store(
true);
646 std::vector<TabletToolInfo> announced;
666 announced.push_back(pen);
677 announced.push_back(eraser);
686 "Tablet adopted: {} (VID:{:04X} PID:{:04X}, {} field(s))",
689 for (
const auto& info : announced) {
703 std::vector<std::shared_ptr<TabletDevice>> snapshot;
710 snapshot.push_back(
device);
714 if (snapshot.empty()) {
715 std::this_thread::sleep_for(std::chrono::milliseconds(20));
719 for (
auto&
device : snapshot) {
727 const int bytes = hid_read_timeout(
device.handle,
733 std::span<const uint8_t>(
device.read_buffer.data(),
734 static_cast<size_t>(bytes)));
740 "Tablet read error on {}: {}",
device.name,
741 hid_failure_reason(
device.handle));
742 device.active.store(
false);
756 std::span<const uint8_t> body = report;
758 if (
device.layout.uses_report_ids) {
760 body = report.subspan(1);
763 bool matched =
false;
764 bool eraser_flag =
false;
765 bool invert_seen =
false;
767 for (
const auto& field :
device.layout.fields) {
772 const int32_t raw = extract_field(body, field);
774 switch (field.slot) {
779 device.slots[field.slot] = normalise(raw, field.logical_min, field.logical_max);
784 device.slots[field.slot] = to_degrees(raw, field.logical_min, field.logical_max, 90.0);
788 device.slots[field.slot] = to_degrees(raw, field.logical_min, field.logical_max, 180.0);
792 device.slots[field.slot] = normalise(raw, field.logical_min, field.logical_max) * 2.0 - 1.0;
800 case k_pseudo_in_range:
808 case k_pseudo_invert:
810 eraser_flag = eraser_flag || (raw != 0);
813 case k_pseudo_eraser:
814 eraser_flag = eraser_flag || (raw != 0);
815 set_button_bit(
device.slots, 2, raw != 0);
818 case k_pseudo_barrel:
819 set_button_bit(
device.slots, 0, raw != 0);
822 case k_pseudo_barrel_2:
823 set_button_bit(
device.slots, 1, raw != 0);
826 case k_pseudo_serial: {
830 it->second.hardware_serial =
static_cast<uint64_t
>(
831 static_cast<uint32_t
>(raw));
845 device.inverted = eraser_flag;
858 value.timestamp_ns =
static_cast<uint64_t
>(
859 std::chrono::steady_clock::now().time_since_epoch().count());
877 std::vector<InputDeviceInfo> result;
878 result.reserve(
m_tools.size());
879 for (
const auto& [
id, info] :
m_tools) {
880 result.push_back(info);
893 auto dev_it =
m_devices.find(path_it->second);
895 dev_it->second->active.store(
true);
902 std::shared_ptr<TabletDevice>
device;
910 path = path_it->second;
918 device->active.store(
false);
928 return dev !=
m_devices.end() && dev->second->active.load();
935 std::vector<uint32_t> result;
936 result.reserve(
m_tools.size());
937 for (
const auto& [
id, info] :
m_tools) {
938 result.push_back(
id);
946 auto it =
m_tools.find(device_id);
960 auto dev_it =
m_devices.find(path_it->second);
964 return dev_it->second->layout;
969 const hid_api_version* ver = hid_version();
971 return "HIDAPI unknown";
972 return "HIDAPI " + std::to_string(ver->major) +
"."
973 + 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,...)
#define MF_DEBUG(comp, ctx,...)
vk::PhysicalDevice device
vk::PhysicalDeviceType type
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.
MAYAFLUX_API std::string wide_to_utf8(const wchar_t *ws)
Encode a null-terminated wide string as UTF-8.
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.
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.