@@ -924,6 +924,7 @@ namespace lvh::detail {
924924 private:
925925 friend class WindowsBackendContext ;
926926 friend class WindowsGamepad ;
927+ friend class WindowsHidMouse ;
927928
928929 std::mutex output_dispatch_mutex_;
929930 mutable std::mutex mutex_;
@@ -1039,6 +1040,18 @@ namespace lvh::detail {
10391040 return {OperationStatus::success (), std::move (gamepad)};
10401041 }
10411042
1043+ /* *
1044+ * @brief Create a driver-backed HID mouse.
1045+ *
1046+ * Defined out of line because it constructs `WindowsHidMouse`, which is
1047+ * declared after this class.
1048+ *
1049+ * @param id Client device identity.
1050+ * @param options Mouse creation options.
1051+ * @return Creation result; the device is null when the driver declines.
1052+ */
1053+ BackendMouseCreationResult create_hid_mouse (DeviceId id, const CreateMouseOptions &options);
1054+
10421055 OperationStatus submit_gamepad_report (
10431056 const std::shared_ptr<WindowsGamepadState> &state,
10441057 const std::vector<std::uint8_t > &report
@@ -1420,6 +1433,296 @@ namespace lvh::detail {
14201433 bool open_ = true ;
14211434 };
14221435
1436+ /* *
1437+ * @brief Report descriptor for the driver-backed relative mouse.
1438+ *
1439+ * Five buttons, 16-bit relative X/Y, an 8-bit wheel and an 8-bit AC Pan
1440+ * axis. No report ID is declared, matching the seven byte input report
1441+ * emitted by `WindowsHidMouse`.
1442+ *
1443+ * @return Report descriptor bytes.
1444+ */
1445+ std::vector<std::uint8_t > make_mouse_report_descriptor () {
1446+ return {
1447+ 0x05 , 0x01 , // Usage Page (Generic Desktop)
1448+ 0x09 , 0x02 , // Usage (Mouse)
1449+ 0xA1 , 0x01 , // Collection (Application)
1450+ 0x09 , 0x01 , // Usage (Pointer)
1451+ 0xA1 , 0x00 , // Collection (Physical)
1452+ 0x05 , 0x09 , // Usage Page (Button)
1453+ 0x19 , 0x01 , // Usage Minimum (Button 1)
1454+ 0x29 , 0x05 , // Usage Maximum (Button 5)
1455+ 0x15 , 0x00 , // Logical Minimum (0)
1456+ 0x25 , 0x01 , // Logical Maximum (1)
1457+ 0x75 , 0x01 , // Report Size (1)
1458+ 0x95 , 0x05 , // Report Count (5)
1459+ 0x81 , 0x02 , // Input (Data,Var,Abs)
1460+ 0x75 , 0x03 , // Report Size (3)
1461+ 0x95 , 0x01 , // Report Count (1)
1462+ 0x81 , 0x03 , // Input (Cnst,Var,Abs) - padding
1463+ 0x05 , 0x01 , // Usage Page (Generic Desktop)
1464+ 0x09 , 0x30 , // Usage (X)
1465+ 0x09 , 0x31 , // Usage (Y)
1466+ 0x16 , 0x00 , 0x80 , // Logical Minimum (-32768)
1467+ 0x26 , 0xFF , 0x7F , // Logical Maximum (32767)
1468+ 0x75 , 0x10 , // Report Size (16)
1469+ 0x95 , 0x02 , // Report Count (2)
1470+ 0x81 , 0x06 , // Input (Data,Var,Rel)
1471+ 0x09 , 0x38 , // Usage (Wheel)
1472+ 0x15 , 0x81 , // Logical Minimum (-127)
1473+ 0x25 , 0x7F , // Logical Maximum (127)
1474+ 0x75 , 0x08 , // Report Size (8)
1475+ 0x95 , 0x01 , // Report Count (1)
1476+ 0x81 , 0x06 , // Input (Data,Var,Rel)
1477+ 0x05 , 0x0C , // Usage Page (Consumer)
1478+ 0x0A , 0x38 , 0x02 , // Usage (AC Pan)
1479+ 0x15 , 0x81 , // Logical Minimum (-127)
1480+ 0x25 , 0x7F , // Logical Maximum (127)
1481+ 0x75 , 0x08 , // Report Size (8)
1482+ 0x95 , 0x01 , // Report Count (1)
1483+ 0x81 , 0x06 , // Input (Data,Var,Rel)
1484+ 0xC0 , // End Collection
1485+ 0xC0 , // End Collection
1486+ };
1487+ }
1488+
1489+ /* *
1490+ * @brief Size of the input report emitted by `WindowsHidMouse`.
1491+ */
1492+ constexpr std::size_t mouse_input_report_size = 7U ;
1493+
1494+ /* *
1495+ * @brief High-resolution scroll units that make up a single wheel detent.
1496+ */
1497+ constexpr int mouse_scroll_units_per_detent = 120 ;
1498+
1499+ /* *
1500+ * @brief Device profile describing the driver-backed mouse.
1501+ *
1502+ * @return Mouse device profile.
1503+ */
1504+ DeviceProfile make_hid_mouse_profile () {
1505+ DeviceProfile profile;
1506+ profile.device_type = DeviceType::mouse;
1507+ profile.gamepad_kind = GamepadProfileKind::generic;
1508+ profile.bus_type = BusType::usb;
1509+ profile.vendor_id = 0x1209 ;
1510+ profile.product_id = 0x0003 ;
1511+ profile.version = 0x0001 ;
1512+ profile.report_id = 0 ;
1513+ profile.name = " libvirtualhid Mouse" ;
1514+ profile.manufacturer = " LizardByte" ;
1515+ profile.report_descriptor = make_mouse_report_descriptor ();
1516+ profile.input_report_size = mouse_input_report_size;
1517+ profile.output_report_size = 0 ;
1518+ profile.capabilities = {};
1519+ return profile;
1520+ }
1521+
1522+ /* *
1523+ * @brief Map a mouse button onto its HID button bit.
1524+ *
1525+ * HID orders the primary buttons left, right, middle, which differs from
1526+ * the `MouseButton` declaration order.
1527+ *
1528+ * @param button Mouse button.
1529+ * @return Bit index, or `std::nullopt` when the button is unmapped.
1530+ */
1531+ std::optional<unsigned > hid_mouse_button_bit (MouseButton button) {
1532+ switch (button) {
1533+ using enum MouseButton;
1534+
1535+ case left:
1536+ return 0U ;
1537+ case right:
1538+ return 1U ;
1539+ case middle:
1540+ return 2U ;
1541+ case side:
1542+ return 3U ;
1543+ case extra:
1544+ return 4U ;
1545+ }
1546+
1547+ return std::nullopt ;
1548+ }
1549+
1550+ /* *
1551+ * @brief Mouse backed by a real HID device created through the UMDF driver.
1552+ *
1553+ * Relative motion, buttons and scrolling are delivered as HID input
1554+ * reports, so applications reading the Raw Input API observe them exactly
1555+ * as they would a physical mouse. Absolute motion has no relative HID
1556+ * equivalent and is delegated to the Win32 injection path.
1557+ */
1558+ class WindowsHidMouse final : public BackendMouse {
1559+ public:
1560+ WindowsHidMouse (
1561+ std::shared_ptr<WindowsBackendContext> context,
1562+ std::shared_ptr<WindowsGamepadState> state
1563+ ):
1564+ context_ {std::move (context)},
1565+ state_ {std::move (state)} {}
1566+
1567+ OperationStatus submit (const MouseEvent &event) override {
1568+ using enum ErrorCode;
1569+
1570+ if (!open_) {
1571+ return OperationStatus::failure (device_closed, " Windows HID mouse is closed" );
1572+ }
1573+
1574+ switch (event.kind ) {
1575+ using enum MouseEventKind;
1576+
1577+ case relative_motion:
1578+ return emit (clamp_axis (event.x ), clamp_axis (event.y ), 0 , 0 );
1579+ case absolute_motion:
1580+ // Relative HID reports cannot express absolute positioning.
1581+ return fallback_.submit (event);
1582+ case button:
1583+ return submit_button (event);
1584+ case vertical_scroll:
1585+ return emit (0 , 0 , accumulate_scroll (vertical_scroll_remainder_, event.high_resolution_scroll ), 0 );
1586+ case horizontal_scroll:
1587+ return emit (0 , 0 , 0 , accumulate_scroll (horizontal_scroll_remainder_, event.high_resolution_scroll ));
1588+ }
1589+
1590+ return OperationStatus::success ();
1591+ }
1592+
1593+ std::vector<DeviceNode> device_nodes () const override {
1594+ return {DeviceNode {.path = state_->path }};
1595+ }
1596+
1597+ OperationStatus close () override {
1598+ if (!open_) {
1599+ return OperationStatus::success ();
1600+ }
1601+
1602+ open_ = false ;
1603+ return context_->close_gamepad (state_);
1604+ }
1605+
1606+ private:
1607+ /* *
1608+ * @brief Clamp a motion delta into the descriptor's 16-bit range.
1609+ *
1610+ * @param value Incoming delta.
1611+ * @return Clamped delta.
1612+ */
1613+ static std::int16_t clamp_axis (std::int32_t value) {
1614+ return static_cast <std::int16_t >(
1615+ std::clamp (
1616+ value,
1617+ static_cast <std::int32_t >(std::numeric_limits<std::int16_t >::min ()),
1618+ static_cast <std::int32_t >(std::numeric_limits<std::int16_t >::max ())
1619+ )
1620+ );
1621+ }
1622+
1623+ /* *
1624+ * @brief Convert high-resolution scroll units into whole wheel detents.
1625+ *
1626+ * The descriptor exposes a detent-based wheel, so sub-detent movement is
1627+ * carried in @p remainder until it accumulates into a full step.
1628+ *
1629+ * @param remainder Running sub-detent remainder for this axis.
1630+ * @param high_resolution_scroll Incoming high-resolution distance.
1631+ * @return Whole detents to report, clamped to the descriptor range.
1632+ */
1633+ static std::int8_t accumulate_scroll (int &remainder, int high_resolution_scroll) {
1634+ remainder += high_resolution_scroll;
1635+ const auto detents = remainder / mouse_scroll_units_per_detent;
1636+ remainder -= detents * mouse_scroll_units_per_detent;
1637+ return static_cast <std::int8_t >(std::clamp (detents, -127 , 127 ));
1638+ }
1639+
1640+ /* *
1641+ * @brief Track a button transition and emit the updated button state.
1642+ *
1643+ * @param event Mouse event describing the transition.
1644+ * @return Submission status.
1645+ */
1646+ OperationStatus submit_button (const MouseEvent &event) {
1647+ const auto bit = hid_mouse_button_bit (event.button );
1648+ if (!bit) {
1649+ return OperationStatus::success ();
1650+ }
1651+
1652+ const auto mask = static_cast <std::uint8_t >(1U << *bit);
1653+ if (event.pressed ) {
1654+ buttons_ = static_cast <std::uint8_t >(buttons_ | mask);
1655+ } else {
1656+ buttons_ = static_cast <std::uint8_t >(buttons_ & ~mask);
1657+ }
1658+
1659+ return emit (0 , 0 , 0 , 0 );
1660+ }
1661+
1662+ /* *
1663+ * @brief Pack and submit a single HID input report.
1664+ *
1665+ * @param x Relative X delta.
1666+ * @param y Relative Y delta.
1667+ * @param wheel Vertical wheel detents.
1668+ * @param pan Horizontal wheel detents.
1669+ * @return Submission status.
1670+ */
1671+ OperationStatus emit (std::int16_t x, std::int16_t y, std::int8_t wheel, std::int8_t pan) {
1672+ std::vector<std::uint8_t > report (mouse_input_report_size, 0U );
1673+ report[0 ] = buttons_;
1674+ report[1 ] = static_cast <std::uint8_t >(static_cast <std::uint16_t >(x) & 0xFFU );
1675+ report[2 ] = static_cast <std::uint8_t >((static_cast <std::uint16_t >(x) >> 8U ) & 0xFFU );
1676+ report[3 ] = static_cast <std::uint8_t >(static_cast <std::uint16_t >(y) & 0xFFU );
1677+ report[4 ] = static_cast <std::uint8_t >((static_cast <std::uint16_t >(y) >> 8U ) & 0xFFU );
1678+ report[5 ] = static_cast <std::uint8_t >(wheel);
1679+ report[6 ] = static_cast <std::uint8_t >(pan);
1680+ return context_->submit_gamepad_report (state_, report);
1681+ }
1682+
1683+ std::shared_ptr<WindowsBackendContext> context_;
1684+ std::shared_ptr<WindowsGamepadState> state_;
1685+ WindowsMouse fallback_;
1686+ std::uint8_t buttons_ = 0U ;
1687+ int vertical_scroll_remainder_ = 0 ;
1688+ int horizontal_scroll_remainder_ = 0 ;
1689+ bool open_ = true ;
1690+ };
1691+
1692+ BackendMouseCreationResult WindowsBackendContext::create_hid_mouse (
1693+ DeviceId id,
1694+ const CreateMouseOptions &options
1695+ ) {
1696+ CreateGamepadOptions gamepad_options;
1697+ gamepad_options.profile = make_hid_mouse_profile ();
1698+ gamepad_options.metadata .stable_id = options.stable_id ;
1699+
1700+ auto request = windows::make_create_gamepad_request (id, gamepad_options);
1701+ LvhWindowsCreateGamepadResponse response {};
1702+ response.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION ;
1703+ response.size = sizeof (response);
1704+
1705+ if (const auto status = command_channel_->create_gamepad (request, response); !status.ok ()) {
1706+ return {status, nullptr };
1707+ }
1708+
1709+ auto state = std::make_shared<WindowsGamepadState>(
1710+ id,
1711+ response.driver_device_id ,
1712+ response.session_token ,
1713+ gamepad_options.profile ,
1714+ response.device_path [0 ] == ' \0 ' ? command_channel_->path () : std::string {response.device_path .data ()}
1715+ );
1716+
1717+ {
1718+ std::lock_guard lock {devices_mutex_};
1719+ gamepads_[state->driver_id ] = state;
1720+ }
1721+ notify_pid_timer ();
1722+
1723+ return {OperationStatus::success (), std::make_unique<WindowsHidMouse>(shared_from_this (), std::move (state))};
1724+ }
1725+
14231726 /* *
14241727 * @brief Shared lifecycle and refresh support for Windows synthetic pointer devices.
14251728 */
@@ -2033,11 +2336,21 @@ namespace lvh::detail {
20332336 return {OperationStatus::success (), std::make_unique<WindowsKeyboard>()};
20342337 }
20352338
2036- BackendMouseCreationResult create_mouse (DeviceId /* id */ , const CreateMouseOptions &options) override {
2339+ BackendMouseCreationResult create_mouse (DeviceId id , const CreateMouseOptions &options) override {
20372340 if (options.profile .device_type != DeviceType::mouse) {
20382341 return {unsupported_profile_status (" Windows mouse backend requires a mouse profile" ), nullptr };
20392342 }
20402343
2344+ // A driver-backed HID mouse is visible to the Raw Input API, which the
2345+ // Win32 injection fallback below is not. Fall back whenever the driver
2346+ // is unavailable so mouse input keeps working without it.
2347+ if (context_) {
2348+ auto hid = context_->create_hid_mouse (id, options);
2349+ if (hid.mouse ) {
2350+ return hid;
2351+ }
2352+ }
2353+
20412354 return {OperationStatus::success (), std::make_unique<WindowsMouse>()};
20422355 }
20432356
0 commit comments