Skip to content

Commit e9e0bc1

Browse files
author
Aditya Srivastava
committed
feat(windows): back the virtual mouse with a real HID device
The Windows mouse backend synthesised input with SendInput, which travels the Win32 cursor pipeline. Applications that read mouse input through the Raw Input API observe nothing on that path, so relative-motion controls such as camera look and click-drag are unusable while the mouse still appears to move on screen. Create the mouse as a HID device through the UMDF control channel, the same way gamepads are created, and deliver relative motion, buttons and scrolling as HID input reports. Raw Input consumers then see the device as they would a physical mouse. - Add a five-button mouse report descriptor with 16-bit relative axes, a wheel and an AC Pan axis. - Accumulate sub-detent high-resolution scroll so precision is preserved across events. - Delegate absolute motion to the existing injection path, which has no relative HID equivalent. - Fall back to the previous SendInput mouse whenever the driver is unavailable, so mouse input keeps working without the driver package.
1 parent 15a37d3 commit e9e0bc1

1 file changed

Lines changed: 314 additions & 1 deletion

File tree

src/platform/windows/windows_backend.cpp

Lines changed: 314 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)