All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support
@ 2026-10-27 19:42 soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 1/8] ui/input: add high-resolution scroll input event soumyajyotisarkar23
                   ` (7 more replies)
  0 siblings, 8 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

This series adds high-resolution vertical and horizontal wheel support
to QEMU's USB HID mouse.

As of now the USB mouse only reports whole wheel detents and has no
horizontal wheel. Modern touchpads, trackpoints and free-spinning wheels on
the host produce much finer and often simultaneous horizontal and
vertical, scroll movement, but the guest never sees it: the GTK and
SDL ui turn every smooth scroll event into a single wheel button
click and the HID report has no room for anything finer.

Since Windows Vista, Windows supports Microsoft's enhanced wheel
protocol: a Resolution Multiplier feature report per wheel axis and
horizontal scrolling through the Consumer AC Pan usage. Linux
supports the same multiplier through hid-input. This series
implements it end to end:

 - Patch 1 adds a 'scroll' input event carrying the distance in 1/120
   of a detent (WHEEL_DELTA units). Devices that do not handle it
   keep getting wheel button clicks from the input core, one per
   detent.
 - Patches 2 and 3 make the GTK and SDL ui forward smooth scroll
   deltas on both axes as scroll events instead of rounding them to
   clicks. SDL needs 2.0.18 or newer for preciseX/preciseY.
 - Patch 4 derives the report descriptor length with sizeof() (no
   functional change).
 - Patch 5 makes the HID mouse accept scroll events and track the
   wheel in 1/120 detent units. Guest visible behaviour is unchanged.
 - Patch 6 switches the mouse to a report-ID based descriptor with an
   AC Pan field. Boot protocol keeps the 4-byte report.
 - Patch 7 adds the Resolution Multiplier feature report (ID 2,
   physical maximum 120). Once the guest enables it, each wheel count
   is 1/120 of a detent.
 - Patch 8 clears the wheel state and mouse_grabbed on reset.

Sub-count remainders are carried in a residual per axis, so slow
scrolling adds up instead of being dropped. The new state lives in an
optional "HIDPointerDevice/wheel" vmstate subsection that is only sent
while wheel state is in use.

Testing was done with a Windows 10 guest and -device usb-mouse, with
both -display gtk and -display sdl, using the wheel event viewer at
https://domeventviewer.com/wheel-event-viewer.html:

 - Without the series, deltaY is always +/-100 px (one 120-unit
   detent). With the multiplier of 4 from v1 it was +/-25 px.
 - With the series, deltaX and deltaY arrive in steps of 0.833 px
   (one WHEEL_DELTA unit; Edge maps 120 units to 100 px), horizontal
   scrolling works and both axes can change in the same event.
 - Migration from a QEMU without the series to one with it completes.
   Migration back to an older QEMU only works while the guest has not
   enabled the Resolution Multiplier and no wheel movement is pending;
   otherwise the older QEMU rejects the unknown "HIDPointerDevice/wheel"
   subsection (see the open questions below).

Why the guest cannot match the host 1:1
---------------------------------------
The scroll path is now lossless on the QEMU side: every fraction of a
detent the host UI reports either reaches the guest or is carried over
to the next report. The guest still cannot reproduce the host's
pixel deltas exactly, because the smallest step a USB wheel can
express is one count and with the multiplier enabled one count is
one WHEEL_DELTA unit (1/120 detent). Windows delivers WM_MOUSEWHEEL
deltas as integers in the same unit and Chromium turns them into
pixels as

  deltaY = units * 100 / 120

so the guest sees deltas in steps of 0.833 px. A host movement that
does not land on a whole unit is split: the whole units are sent now
and the remainder later. For example, if the host scrolls by the
equivalent of 26 px:

  26 px * 120 / 100 = 31.2 units
  sent now: 31 units -> 25.833 px in the guest
  carried: 0.2 units -> sent with a later report

Per event the guest is therefore off by less than 0.833 px, but no
movement is lost over time. Movements that are a multiple of 5 px
(6 units) map exactly.

The remaining per-event differences between host and guest come from
outside QEMU: host and guest browsers apply different scroll scaling
and smoothing and several host events can fall into one USB polling
interval (8-10 ms) and reach the guest as a single report.

Changes since RFC:
 - Patch 4 (new): derive the descriptor length with sizeof() instead
   of hard-coding it.
 - Keep the report descriptor comments in their existing column
   instead of moving them.
 - Check "hs->protocol == 1" for report protocol and add boot/report
   protocol comments in hid_pointer_poll().
 - Replace the 1/4/8/16 divisor switch with multiplier scaling in
   hid_scroll_counts(); the guest-selected multiplier now increases
   the resolution instead of dividing it.
 - Drop the stray blank line in hid_pointer_poll().
 - Move the reset changes into their own patch.
 - Move the new migration state into an optional subsection and
   convert queued wheel movement from older QEMU versions.
 - New input-layer scroll event and GTK/SDL support, so the host's
   fine-grained scroll actually reaches the device.

Changes since v1:
 - Added proper suggested by.

Reference:
Enhanced Wheel Support in Windows
https://download.microsoft.com/download/b/d/1/bd1f7ef4-7d72-419e-bc5c-9f79ad7bb66e/wheel.docx

Open questions/known gaps:

 - The descriptor and packet size change is guest visible. A guest
   migrated from an older QEMU keeps the old 52-byte descriptor until
   it re-enumerates the device, but receives the new 6-byte reports.
   Should this be tied to a usb-mouse property that hw_compat_11_1
   turns off? The same property could keep the wheel subsection from
   being sent to older machine types, so that migrating back to an
   older QEMU keeps working after the guest enabled the multiplier.
 - Only the GTK and SDL UIs produce high-resolution scroll events so
   far. VNC, Spice and Cocoa still send wheel clicks.
 - Linux guests are not tested yet.

Feedback is very welcome!

Thank you,
Soumyajyotii Ssarkar

Soumyajyotii Ssarkar (8):
  ui/input: add high-resolution scroll input event
  ui/gtk: forward smooth scroll deltas as high-resolution scroll events
  ui/sdl2: forward precise wheel deltas as high-resolution scroll events
  hw/usb/dev-hid: derive the mouse report descriptor length with
    sizeof()
  hw/input/hid: accept high-resolution vertical scroll events
  hw/usb/dev-hid: report horizontal wheel through AC Pan
  hw/usb/dev-hid: support the wheel Resolution Multiplier feature report
  hw/input/hid: clear wheel state and mouse_grabbed on reset

 hw/input/hid.c         | 187 ++++++++++++++++++++++++++++++++----
 hw/usb/dev-hid.c       | 209 ++++++++++++++++++++++++++++++++---------
 include/hw/input/hid.h |   8 ++
 include/ui/input.h     |   6 ++
 qapi/ui.json           |  45 ++++++++-
 replay/replay-input.c  |   8 ++
 ui/gtk.c               |  34 ++++---
 ui/input.c             |  82 +++++++++++++++-
 ui/sdl2.c              |  28 ++++++
 ui/trace-events        |   1 +
 10 files changed, 531 insertions(+), 77 deletions(-)

-- 
2.56.0



^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2 1/8] ui/input: add high-resolution scroll input event
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  2026-10-28  6:26   ` Markus Armbruster
  2026-10-27 19:42 ` [PATCH v2 2/8] ui/gtk: forward smooth scroll deltas as high-resolution scroll events soumyajyotisarkar23
                   ` (6 subsequent siblings)
  7 siblings, 1 reply; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

The input layer can only describe wheel movement as WHEEL_UP/DOWN/
LEFT/RIGHT button clicks, one per detent. Host UIs that receive
smooth scroll deltas (touchpads, free-spinning or high-resolution
wheels) have to round them to whole clicks and the guest never sees
the finer movement.

Add a new 'scroll' input event kind carrying a scroll axis (vertical
or horizontal) and a distance in 1/120 of a wheel detent, the same
unit Windows uses for WHEEL_DELTA and Linux for REL_WHEEL_HI_RES.
Positive values scroll down or right.

Scroll events are routed to the handler that receives pointer button
events for the console. Handlers that do not set
INPUT_EVENT_MASK_SCROLL keep working unchanged: the input core
accumulates the distance and emits one wheel button click per full
detent.

Also wire the new event into input-send-event, tracing and
record/replay.

Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 include/ui/input.h    |  6 ++++
 qapi/ui.json          | 45 ++++++++++++++++++++++--
 replay/replay-input.c |  8 +++++
 ui/input.c            | 82 ++++++++++++++++++++++++++++++++++++++++++-
 ui/trace-events       |  1 +
 5 files changed, 139 insertions(+), 3 deletions(-)

diff --git a/include/ui/input.h b/include/ui/input.h
index a1560cac0f..daedb547ff 100644
--- a/include/ui/input.h
+++ b/include/ui/input.h
@@ -9,6 +9,9 @@
 #define INPUT_EVENT_MASK_REL   (1<<INPUT_EVENT_KIND_REL)
 #define INPUT_EVENT_MASK_ABS   (1<<INPUT_EVENT_KIND_ABS)
 #define INPUT_EVENT_MASK_MTT   (1<<INPUT_EVENT_KIND_MTT)
+#define INPUT_EVENT_MASK_SCROLL (1 << INPUT_EVENT_KIND_SCROLL)
+
+#define INPUT_EVENT_SCROLL_DETENT 120
 
 #define INPUT_EVENT_ABS_MIN    0x0000
 #define INPUT_EVENT_ABS_MAX    0x7FFF
@@ -31,6 +34,7 @@ typedef struct QemuInputEvent {
         InputMoveEvent rel;
         InputMoveEvent abs;
         InputMultiTouchEvent mtt;
+        InputScrollEvent scroll;
     };
 } QemuInputEvent;
 
@@ -80,6 +84,8 @@ int qemu_input_scale_axis(int value,
 void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value);
 void qemu_input_queue_abs(QemuConsole *src, InputAxis axis, int value,
                           int min_in, int max_in);
+void qemu_input_queue_scroll(QemuConsole *src, InputScrollAxis axis,
+                             int value);
 void qemu_input_queue_mtt(QemuConsole *src, InputMultiTouchType type, int slot,
                           int tracking_id);
 void qemu_input_queue_mtt_abs(QemuConsole *src, InputAxis axis, int value,
diff --git a/qapi/ui.json b/qapi/ui.json
index 9e3e80a3d7..e31fd74f87 100644
--- a/qapi/ui.json
+++ b/qapi/ui.json
@@ -1120,6 +1120,34 @@
               'axis'       : 'InputAxis',
               'value'      : 'int' } }
 
+##
+# @InputScrollAxis:
+#     Scroll axis of a pointer input device.
+#
+# @vertical: vertical wheel
+#
+# @horizontal: horizontal wheel (pan)
+#
+# Since: 11.2
+##
+{ 'enum'  : 'InputScrollAxis',
+  'data'  : [ 'vertical', 'horizontal' ] }
+
+##
+# @InputScrollEvent:
+#     High-resolution scroll input event.
+#
+# @axis: Which scroll axis is referenced by @value.
+#
+# @value: Scroll distance in 1/120 of a wheel detent.  Positive
+#     values scroll down (vertical) or right (horizontal).
+#
+# Since: 11.2
+##
+{ 'struct'  : 'InputScrollEvent',
+  'data'  : { 'axis'    : 'InputScrollAxis',
+              'value'   : 'int' } }
+
 ##
 # @InputEventKind:
 #
@@ -1133,10 +1161,12 @@
 #
 # @mtt: a multi-touch input event
 #
+# @scroll: a high-resolution scroll input event (since 11.2)
+#
 # Since: 2.0
 ##
 { 'enum': 'InputEventKind',
-  'data': [ 'key', 'btn', 'rel', 'abs', 'mtt' ] }
+  'data': [ 'key', 'btn', 'rel', 'abs', 'mtt', 'scroll' ] }
 
 ##
 # @InputKeyEventWrapper:
@@ -1178,6 +1208,16 @@
 { 'struct': 'InputMultiTouchEventWrapper',
   'data': { 'data': 'InputMultiTouchEvent' } }
 
+##
+# @InputScrollEventWrapper:
+#
+# @data: High-resolution scroll input event
+#
+# Since: 11.2
+##
+{ 'struct': 'InputScrollEventWrapper',
+  'data': { 'data': 'InputScrollEvent' } }
+
 ##
 # @InputEvent:
 #     Input event union.
@@ -1193,7 +1233,8 @@
               'btn'     : 'InputBtnEventWrapper',
               'rel'     : 'InputMoveEventWrapper',
               'abs'     : 'InputMoveEventWrapper',
-              'mtt'     : 'InputMultiTouchEventWrapper' } }
+              'mtt'     : 'InputMultiTouchEventWrapper',
+              'scroll'  : 'InputScrollEventWrapper' } }
 
 ##
 # @input-send-event:
diff --git a/replay/replay-input.c b/replay/replay-input.c
index c86e00af0e..188c43a137 100644
--- a/replay/replay-input.c
+++ b/replay/replay-input.c
@@ -45,6 +45,10 @@ void replay_save_input_event(QemuInputEvent *evt)
         replay_put_dword(evt->mtt.axis);
         replay_put_qword(evt->mtt.value);
         break;
+    case INPUT_EVENT_KIND_SCROLL:
+        replay_put_dword(evt->scroll.axis);
+        replay_put_qword(evt->scroll.value);
+        break;
     case INPUT_EVENT_KIND__MAX:
         /* keep gcc happy */
         break;
@@ -80,6 +84,10 @@ QemuInputEvent *replay_read_input_event(void)
         evt->mtt.axis = (InputAxis)replay_get_dword();
         evt->mtt.value = replay_get_qword();
         break;
+    case INPUT_EVENT_KIND_SCROLL:
+        evt->scroll.axis = (InputScrollAxis)replay_get_dword();
+        evt->scroll.value = replay_get_qword();
+        break;
     case INPUT_EVENT_KIND__MAX:
         /* keep gcc happy */
         break;
diff --git a/ui/input.c b/ui/input.c
index f40a988370..f9ae49e275 100644
--- a/ui/input.c
+++ b/ui/input.c
@@ -181,7 +181,12 @@ void qmp_input_send_event(const char *device,
     for (e = events; e != NULL; e = e->next) {
         InputEvent *event = e->value;
 
-        if (!qemu_input_find_handler(1 << event->type, con)) {
+        uint32_t mask = 1 << event->type;
+
+        if (event->type == INPUT_EVENT_KIND_SCROLL) {
+            mask |= INPUT_EVENT_MASK_BTN;
+        }
+        if (!qemu_input_find_handler(mask, con)) {
             error_setg(errp, "Input handler not found for "
                              "event type %s",
                             InputEventKind_str(event->type));
@@ -217,6 +222,10 @@ void qmp_input_send_event(const char *device,
             evt.mtt = *qapi->u.mtt.data;
             break;
 
+        case INPUT_EVENT_KIND_SCROLL:
+            evt.scroll = *qapi->u.scroll.data;
+            break;
+
         default:
             g_assert_not_reached();
         }
@@ -235,6 +244,7 @@ static void qemu_input_event_trace(QemuConsole *src, QemuInputEvent *evt)
     InputBtnEvent *btn;
     InputMoveEvent *move;
     InputMultiTouchEvent *mtt;
+    InputScrollEvent *scroll;
 
     if (src) {
         idx = qemu_console_get_index(src);
@@ -265,6 +275,12 @@ static void qemu_input_event_trace(QemuConsole *src, QemuInputEvent *evt)
         name = mtt->axis < INPUT_AXIS__MAX ? InputAxis_str(mtt->axis) : "invalid";
         trace_input_event_mtt(idx, name, mtt->value);
         break;
+    case INPUT_EVENT_KIND_SCROLL:
+        scroll = &evt->scroll;
+        name = scroll->axis < INPUT_SCROLL_AXIS__MAX ?
+            InputScrollAxis_str(scroll->axis) : "invalid";
+        trace_input_event_scroll(idx, name, scroll->value);
+        break;
     case INPUT_EVENT_KIND__MAX:
         /* keep gcc happy */
         break;
@@ -342,12 +358,62 @@ static void qemu_input_queue_sync(QemuInputEventQueueHead *queue)
     queue_count++;
 }
 
+static void qemu_input_scroll_to_btn(QemuInputHandlerState *s,
+                                     QemuConsole *src, QemuInputEvent *evt)
+{
+    static int residual[INPUT_SCROLL_AXIS__MAX];
+    QemuInputEvent btn = { .type = INPUT_EVENT_KIND_BTN };
+    InputScrollAxis axis = evt->scroll.axis;
+
+    if (axis >= INPUT_SCROLL_AXIS__MAX) {
+        return;
+    }
+
+    residual[axis] += evt->scroll.value;
+    while (abs(residual[axis]) >= INPUT_EVENT_SCROLL_DETENT) {
+        bool fwd = residual[axis] > 0;
+
+        if (axis == INPUT_SCROLL_AXIS_VERTICAL) {
+            btn.btn.button = fwd ? INPUT_BUTTON_WHEEL_DOWN
+                                 : INPUT_BUTTON_WHEEL_UP;
+        } else {
+            btn.btn.button = fwd ? INPUT_BUTTON_WHEEL_RIGHT
+                                 : INPUT_BUTTON_WHEEL_LEFT;
+        }
+        residual[axis] -= fwd ? INPUT_EVENT_SCROLL_DETENT
+                              : -INPUT_EVENT_SCROLL_DETENT;
+
+        btn.btn.down = true;
+        qemu_input_event_trace(src, &btn);
+        s->handler->event(s->dev, src, &btn);
+        btn.btn.down = false;
+        qemu_input_event_trace(src, &btn);
+        s->handler->event(s->dev, src, &btn);
+        s->events++;
+    }
+}
+
 void qemu_input_event_send_impl(QemuConsole *src, QemuInputEvent *evt)
 {
     QemuInputHandlerState *s;
 
     qemu_input_event_trace(src, evt);
 
+    if (evt->type == INPUT_EVENT_KIND_SCROLL) {
+        s = qemu_input_find_handler(INPUT_EVENT_MASK_BTN |
+                                    INPUT_EVENT_MASK_SCROLL, src);
+        if (!s) {
+            return;
+        }
+        if (s->handler->mask & INPUT_EVENT_MASK_SCROLL) {
+            s->handler->event(s->dev, src, evt);
+            s->events++;
+        } else {
+            qemu_input_scroll_to_btn(s, src, evt);
+        }
+        return;
+    }
+
     /* send event */
     s = qemu_input_find_handler(1 << evt->type, src);
     if (!s) {
@@ -518,6 +584,20 @@ void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value)
     qemu_input_event_send(src, &evt);
 }
 
+void qemu_input_queue_scroll(QemuConsole *src, InputScrollAxis axis,
+                             int value)
+{
+    QemuInputEvent evt = {
+        .type = INPUT_EVENT_KIND_SCROLL,
+        .scroll = {
+            .axis = axis,
+            .value = value,
+        },
+    };
+
+    qemu_input_event_send(src, &evt);
+}
+
 void qemu_input_queue_abs(QemuConsole *src, InputAxis axis, int value,
                           int min_in, int max_in)
 {
diff --git a/ui/trace-events b/ui/trace-events
index 339a61f341..638096607a 100644
--- a/ui/trace-events
+++ b/ui/trace-events
@@ -132,6 +132,7 @@ input_event_btn(int conidx, const char *btn, bool down) "con %d, button %s, down
 input_event_rel(int conidx, const char *axis, int value) "con %d, axis %s, value %d"
 input_event_abs(int conidx, const char *axis, int value) "con %d, axis %s, value 0x%x"
 input_event_mtt(int conidx, const char *axis, int value) "con %d, axis %s, value 0x%x"
+input_event_scroll(int conidx, const char *axis, int value) "con %d, axis %s, value %d"
 input_event_sync(void) ""
 
 # sdl2-input.c
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 2/8] ui/gtk: forward smooth scroll deltas as high-resolution scroll events
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 1/8] ui/input: add high-resolution scroll input event soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 3/8] ui/sdl2: forward precise wheel " soumyajyotisarkar23
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

For GDK_SCROLL_SMOOTH events gd_scroll_event() only looked at the sign
of the deltas and sent a single wheel button click, so any amount of
scrolling became one detent. It also dropped the horizontal delta
whenever there was vertical movement.

Convert the deltas, which GDK reports in detents, into 1/120 detent
units and send them as scroll events on both axes. Fractions are
carried over to the next event so slow scrolling is not lost.
Discrete GDK_SCROLL_UP/DOWN/LEFT/RIGHT events still send button
clicks.

Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 ui/gtk.c | 34 ++++++++++++++++++++--------------
 1 file changed, 20 insertions(+), 14 deletions(-)

diff --git a/ui/gtk.c b/ui/gtk.c
index 8d312d6a4b..9df83b6225 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -1152,27 +1152,33 @@ static gboolean gd_scroll_event(GtkWidget *widget, GdkEventScroll *scroll,
         btn_horizontal = INPUT_BUTTON_WHEEL_RIGHT;
         has_horizontal = true;
     } else if (scroll->direction == GDK_SCROLL_SMOOTH) {
+        static double residual_x, residual_y;
         gdouble delta_x, delta_y;
+        int units;
+
         if (!gdk_event_get_scroll_deltas((GdkEvent *)scroll,
                                          &delta_x, &delta_y)) {
             return TRUE;
         }
 
-        if (delta_y > 0) {
-            btn_vertical = INPUT_BUTTON_WHEEL_DOWN;
-            has_vertical = true;
-        } else if (delta_y < 0) {
-            btn_vertical = INPUT_BUTTON_WHEEL_UP;
-            has_vertical = true;
-        } else if (delta_x > 0) {
-            btn_horizontal = INPUT_BUTTON_WHEEL_RIGHT;
-            has_horizontal = true;
-        } else if (delta_x < 0) {
-            btn_horizontal = INPUT_BUTTON_WHEEL_LEFT;
-            has_horizontal = true;
-        } else {
-            return TRUE;
+        residual_y += delta_y * INPUT_EVENT_SCROLL_DETENT;
+        units = (int)residual_y;
+        residual_y -= units;
+        if (units) {
+            qemu_input_queue_scroll(vc->gfx.dcl.con,
+                                    INPUT_SCROLL_AXIS_VERTICAL, units);
+        }
+
+        residual_x += delta_x * INPUT_EVENT_SCROLL_DETENT;
+        units = (int)residual_x;
+        residual_x -= units;
+        if (units) {
+            qemu_input_queue_scroll(vc->gfx.dcl.con,
+                                    INPUT_SCROLL_AXIS_HORIZONTAL, units);
         }
+
+        qemu_input_event_sync();
+        return TRUE;
     } else {
         return TRUE;
     }
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 3/8] ui/sdl2: forward precise wheel deltas as high-resolution scroll events
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 1/8] ui/input: add high-resolution scroll input event soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 2/8] ui/gtk: forward smooth scroll deltas as high-resolution scroll events soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 4/8] hw/usb/dev-hid: derive the mouse report descriptor length with sizeof() soumyajyotisarkar23
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

handle_mousewheel() only looked at the sign of the integer wheel
deltas and sent a single wheel button click, so any amount of
scrolling became one detent and the horizontal axis was dropped
whenever there was vertical movement.

SDL 2.0.18 added preciseX and preciseY, which carry the wheel movement
in fractions of a detent. When building against SDL 2.0.18 or newer,
convert them into 1/120 detent units and send them as scroll events on
both axes, carrying fractions over to the next event. The direction
follows SDL's convention (positive x scrolls right, positive y scrolls
up) and honours SDL_MOUSEWHEEL_FLIPPED. Older SDL versions keep the
existing button behaviour.

Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 ui/sdl2.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/ui/sdl2.c b/ui/sdl2.c
index 1c97d23a47..0a052a8f2d 100644
--- a/ui/sdl2.c
+++ b/ui/sdl2.c
@@ -566,12 +566,39 @@ static void handle_mousewheel(SDL_Event *ev)
 {
     struct sdl2_console *scon = get_scon_from_window(ev->wheel.windowID);
     SDL_MouseWheelEvent *wev = &ev->wheel;
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+    static float residual_x, residual_y;
+    float flip;
+    int units;
+#else
     InputButton btn;
+#endif
 
     if (!scon || !qemu_console_is_graphic(scon->dcl.con)) {
         return;
     }
 
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+    flip = wev->direction == SDL_MOUSEWHEEL_FLIPPED ? -1.0f : 1.0f;
+
+    residual_y -= flip * wev->preciseY * INPUT_EVENT_SCROLL_DETENT;
+    units = (int)residual_y;
+    residual_y -= units;
+    if (units) {
+        qemu_input_queue_scroll(scon->dcl.con,
+                                INPUT_SCROLL_AXIS_VERTICAL, units);
+    }
+
+    residual_x += flip * wev->preciseX * INPUT_EVENT_SCROLL_DETENT;
+    units = (int)residual_x;
+    residual_x -= units;
+    if (units) {
+        qemu_input_queue_scroll(scon->dcl.con,
+                                INPUT_SCROLL_AXIS_HORIZONTAL, units);
+    }
+
+    qemu_input_event_sync();
+#else
     if (wev->y > 0) {
         btn = INPUT_BUTTON_WHEEL_UP;
     } else if (wev->y < 0) {
@@ -588,6 +615,7 @@ static void handle_mousewheel(SDL_Event *ev)
     qemu_input_event_sync();
     qemu_input_queue_btn(scon->dcl.con, btn, false);
     qemu_input_event_sync();
+#endif
 }
 
 static void handle_windowevent(SDL_Event *ev)
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 4/8] hw/usb/dev-hid: derive the mouse report descriptor length with sizeof()
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
                   ` (2 preceding siblings ...)
  2026-10-27 19:42 ` [PATCH v2 3/8] ui/sdl2: forward precise wheel " soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 5/8] hw/input/hid: accept high-resolution vertical scroll events soumyajyotisarkar23
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar,
	Helge Deller

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

The HID class descriptors of the mouse interfaces carry the length of
the report descriptor as a hard-coded 52. Any change to the report
descriptor then has to update both copies by hand, and Windows stops
recognising the mouse if the advertised length does not match the
descriptor exactly.

Move qemu_mouse_hid_report_descriptor above the interface descriptors
and compute the length with sizeof(). 

No functional change.

Suggested-by: Alex Bennée <alex.bennee@linaro.org>
Suggested-by: Helge Deller <deller@gmx.de>
Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 hw/usb/dev-hid.c | 74 +++++++++++++++++++++++++++---------------------
 1 file changed, 42 insertions(+), 32 deletions(-)

diff --git a/hw/usb/dev-hid.c b/hw/usb/dev-hid.c
index ae19d60203..2c10e88c85 100644
--- a/hw/usb/dev-hid.c
+++ b/hw/usb/dev-hid.c
@@ -76,6 +76,42 @@ static const USBDescStrings desc_strings = {
     [STR_SERIAL_KEYBOARD]  = "68284",
 };
 
+/*
+ * The interface descriptors below advertise the length of this report
+ * descriptor.
+ * Note: Windows stops recognising the mouse if it does not match
+ * exactly.
+ */
+static const uint8_t qemu_mouse_hid_report_descriptor[] = {
+    0x05, 0x01,         /* Usage Page (Generic Desktop) */
+    0x09, 0x02,         /* Usage (Mouse) */
+    0xa1, 0x01,         /* Collection (Application) */
+    0x09, 0x01,         /*   Usage (Pointer) */
+    0xa1, 0x00,         /*   Collection (Physical) */
+    0x05, 0x09,         /*     Usage Page (Button) */
+    0x19, 0x01,         /*     Usage Minimum (1) */
+    0x29, 0x05,         /*     Usage Maximum (5) */
+    0x15, 0x00,         /*     Logical Minimum (0) */
+    0x25, 0x01,         /*     Logical Maximum (1) */
+    0x95, 0x05,         /*     Report Count (5) */
+    0x75, 0x01,         /*     Report Size (1) */
+    0x81, 0x02,         /*     Input (Data, Variable, Absolute) */
+    0x95, 0x01,         /*     Report Count (1) */
+    0x75, 0x03,         /*     Report Size (3) */
+    0x81, 0x01,         /*     Input (Constant) */
+    0x05, 0x01,         /*     Usage Page (Generic Desktop) */
+    0x09, 0x30,         /*     Usage (X) */
+    0x09, 0x31,         /*     Usage (Y) */
+    0x09, 0x38,         /*     Usage (Wheel) */
+    0x15, 0x81,         /*     Logical Minimum (-0x7f) */
+    0x25, 0x7f,         /*     Logical Maximum (0x7f) */
+    0x75, 0x08,         /*     Report Size (8) */
+    0x95, 0x03,         /*     Report Count (3) */
+    0x81, 0x06,         /*     Input (Data, Variable, Relative) */
+    0xc0,               /*   End Collection */
+    0xc0,               /* End Collection */
+};
+
 static const USBDescIface desc_iface_mouse = {
     .bInterfaceNumber              = 0,
     .bNumEndpoints                 = 1,
@@ -93,7 +129,9 @@ static const USBDescIface desc_iface_mouse = {
                 0x00,          /*  u8  country_code */
                 0x01,          /*  u8  num_descriptors */
                 USB_DT_REPORT, /*  u8  type: Report */
-                52, 0,         /*  u16 len */
+                /*  u16 len */
+                sizeof(qemu_mouse_hid_report_descriptor) & 0xff,
+                sizeof(qemu_mouse_hid_report_descriptor) >> 8,
             },
         },
     },
@@ -124,7 +162,9 @@ static const USBDescIface desc_iface_mouse2 = {
                 0x00,          /*  u8  country_code */
                 0x01,          /*  u8  num_descriptors */
                 USB_DT_REPORT, /*  u8  type: Report */
-                52, 0,         /*  u16 len */
+                /*  u16 len */
+                sizeof(qemu_mouse_hid_report_descriptor) & 0xff,
+                sizeof(qemu_mouse_hid_report_descriptor) >> 8,
             },
         },
     },
@@ -453,36 +493,6 @@ static const USBDesc desc_keyboard2 = {
     .msos = &desc_msos_suspend,
 };
 
-static const uint8_t qemu_mouse_hid_report_descriptor[] = {
-    0x05, 0x01,		/* Usage Page (Generic Desktop) */
-    0x09, 0x02,		/* Usage (Mouse) */
-    0xa1, 0x01,		/* Collection (Application) */
-    0x09, 0x01,		/*   Usage (Pointer) */
-    0xa1, 0x00,		/*   Collection (Physical) */
-    0x05, 0x09,		/*     Usage Page (Button) */
-    0x19, 0x01,		/*     Usage Minimum (1) */
-    0x29, 0x05,		/*     Usage Maximum (5) */
-    0x15, 0x00,		/*     Logical Minimum (0) */
-    0x25, 0x01,		/*     Logical Maximum (1) */
-    0x95, 0x05,		/*     Report Count (5) */
-    0x75, 0x01,		/*     Report Size (1) */
-    0x81, 0x02,		/*     Input (Data, Variable, Absolute) */
-    0x95, 0x01,		/*     Report Count (1) */
-    0x75, 0x03,		/*     Report Size (3) */
-    0x81, 0x01,		/*     Input (Constant) */
-    0x05, 0x01,		/*     Usage Page (Generic Desktop) */
-    0x09, 0x30,		/*     Usage (X) */
-    0x09, 0x31,		/*     Usage (Y) */
-    0x09, 0x38,		/*     Usage (Wheel) */
-    0x15, 0x81,		/*     Logical Minimum (-0x7f) */
-    0x25, 0x7f,		/*     Logical Maximum (0x7f) */
-    0x75, 0x08,		/*     Report Size (8) */
-    0x95, 0x03,		/*     Report Count (3) */
-    0x81, 0x06,		/*     Input (Data, Variable, Relative) */
-    0xc0,		/*   End Collection */
-    0xc0,		/* End Collection */
-};
-
 static const uint8_t qemu_tablet_hid_report_descriptor[] = {
     0x05, 0x01,		/* Usage Page (Generic Desktop) */
     0x09, 0x02,		/* Usage (Mouse) */
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 5/8] hw/input/hid: accept high-resolution vertical scroll events
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
                   ` (3 preceding siblings ...)
  2026-10-27 19:42 ` [PATCH v2 4/8] hw/usb/dev-hid: derive the mouse report descriptor length with sizeof() soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 6/8] hw/usb/dev-hid: report horizontal wheel through AC Pan soumyajyotisarkar23
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

Keep the pending vertical wheel movement in 1/120 detent units instead
of whole detents and let the HID mouse receive scroll events directly
by setting INPUT_EVENT_MASK_SCROLL. Wheel button clicks now add a full
detent (120 units).

hid_pointer_poll() still reports whole detents, so the guest-visible
behaviour does not change yet. The sub-detent remainder is kept in a
per-device residual so that slow scrolling adds up instead of being
dropped and whole detents that do not fit into the 8-bit report field
stay queued for the next poll as before.

The residual is migrated in a new optional subsection, which is only
sent while wheel movement is pending. Because the unit of the queued
dz values changes an incoming stream without the subsection (from an
older QEMU, or with no wheel movement pending) has its queued dz values
converted from detents to 1/120 units in post_load.

Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 hw/input/hid.c         | 97 +++++++++++++++++++++++++++++++++++++++---
 include/hw/input/hid.h |  2 +
 2 files changed, 93 insertions(+), 6 deletions(-)

diff --git a/hw/input/hid.c b/hw/input/hid.c
index 0f28827554..2855f6a3d9 100644
--- a/hw/input/hid.c
+++ b/hw/input/hid.c
@@ -145,15 +145,21 @@ static void hid_pointer_event(DeviceState *dev, QemuConsole *src,
         if (evt->btn.down) {
             e->buttons_state |= bmap[evt->btn.button];
             if (evt->btn.button == INPUT_BUTTON_WHEEL_UP) {
-                e->dz--;
+                e->dz -= INPUT_EVENT_SCROLL_DETENT;
             } else if (evt->btn.button == INPUT_BUTTON_WHEEL_DOWN) {
-                e->dz++;
+                e->dz += INPUT_EVENT_SCROLL_DETENT;
             }
         } else {
             e->buttons_state &= ~bmap[evt->btn.button];
         }
         break;
 
+    case INPUT_EVENT_KIND_SCROLL:
+        if (evt->scroll.axis == INPUT_SCROLL_AXIS_VERTICAL) {
+            e->dz += evt->scroll.value;
+        }
+        break;
+
     default:
         /* keep gcc happy */
         break;
@@ -349,6 +355,18 @@ void hid_pointer_activate(HIDState *hs)
     }
 }
 
+static int hid_scroll_counts(int32_t *pending, int32_t *residual, int mult)
+{
+    int unit = INPUT_EVENT_SCROLL_DETENT / mult;
+    int32_t acc = *pending + *residual;
+    int counts = int_clamp(acc / unit, -127, 127);
+
+    acc -= counts * unit;
+    *residual = acc % unit;
+    *pending = acc - *residual;
+    return counts;
+}
+
 int hid_pointer_poll(HIDState *hs, uint8_t *buf, int len)
 {
     int dx, dy, dz, l;
@@ -373,8 +391,7 @@ int hid_pointer_poll(HIDState *hs, uint8_t *buf, int len)
         dx = e->xdx;
         dy = e->ydy;
     }
-    dz = int_clamp(e->dz, -127, 127);
-    e->dz -= dz;
+    dz = hid_scroll_counts(&e->dz, &hs->ptr.wheel_residual, 1);
 
     if (hs->n &&
         !e->dz &&
@@ -513,7 +530,8 @@ static const QemuInputHandler hid_keyboard_handler = {
 
 static const QemuInputHandler hid_mouse_handler = {
     .name  = "QEMU HID Mouse",
-    .mask  = INPUT_EVENT_MASK_BTN | INPUT_EVENT_MASK_REL,
+    .mask  = INPUT_EVENT_MASK_BTN | INPUT_EVENT_MASK_REL |
+             INPUT_EVENT_MASK_SCROLL,
     .event = hid_pointer_event,
     .sync  = hid_pointer_sync,
 };
@@ -585,11 +603,74 @@ static const VMStateDescription vmstate_hid_ptr_queue = {
     }
 };
 
+/*
+ * Wheel movement used to be queued in whole detents and is now kept in
+ * 1/120 detent units.  Without this subsection the source either was an
+ * older QEMU or had no wheel state pending, so rescaling dz is safe.
+ */
+static bool hid_ptr_wheel_needed(void *opaque)
+{
+    HIDState *s = opaque;
+    int i;
+
+    if (s->ptr.wheel_residual) {
+        return true;
+    }
+    for (i = 0; i < QUEUE_LENGTH; i++) {
+        if (s->ptr.queue[i].dz) {
+            return true;
+        }
+    }
+    return false;
+}
+
+static int hid_ptr_pre_load(void *opaque)
+{
+    HIDState *s = opaque;
+
+    s->ptr.wheel_state_loaded = false;
+    return 0;
+}
+
+static int hid_ptr_wheel_post_load(void *opaque, int version_id)
+{
+    HIDState *s = opaque;
+
+    s->ptr.wheel_state_loaded = true;
+    return 0;
+}
+
+static int hid_ptr_post_load(void *opaque, int version_id)
+{
+    HIDState *s = opaque;
+    int i;
+
+    if (!s->ptr.wheel_state_loaded) {
+        for (i = 0; i < QUEUE_LENGTH; i++) {
+            s->ptr.queue[i].dz *= INPUT_EVENT_SCROLL_DETENT;
+        }
+    }
+    return hid_post_load(opaque, version_id);
+}
+
+static const VMStateDescription vmstate_hid_ptr_wheel = {
+    .name = "HIDPointerDevice/wheel",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .needed = hid_ptr_wheel_needed,
+    .post_load = hid_ptr_wheel_post_load,
+    .fields = (const VMStateField[]) {
+        VMSTATE_INT32(ptr.wheel_residual, HIDState),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
 const VMStateDescription vmstate_hid_ptr_device = {
     .name = "HIDPointerDevice",
     .version_id = 1,
     .minimum_version_id = 1,
-    .post_load = hid_post_load,
+    .pre_load = hid_ptr_pre_load,
+    .post_load = hid_ptr_post_load,
     .fields = (const VMStateField[]) {
         VMSTATE_STRUCT_ARRAY(ptr.queue, HIDState, QUEUE_LENGTH, 0,
                              vmstate_hid_ptr_queue, HIDPointerEvent),
@@ -598,6 +679,10 @@ const VMStateDescription vmstate_hid_ptr_device = {
         VMSTATE_INT32(protocol, HIDState),
         VMSTATE_UINT8(idle, HIDState),
         VMSTATE_END_OF_LIST(),
+    },
+    .subsections = (const VMStateDescription * const []) {
+        &vmstate_hid_ptr_wheel,
+        NULL
     }
 };
 
diff --git a/include/hw/input/hid.h b/include/hw/input/hid.h
index 6a9d7bf466..1e29f4ada4 100644
--- a/include/hw/input/hid.h
+++ b/include/hw/input/hid.h
@@ -22,6 +22,8 @@ typedef void (*HIDEventFunc)(HIDState *s);
 typedef struct HIDMouseState {
     HIDPointerEvent queue[QUEUE_LENGTH];
     int mouse_grabbed;
+    int32_t wheel_residual;
+    bool wheel_state_loaded;
 } HIDMouseState;
 
 typedef struct HIDKeyboardState {
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 6/8] hw/usb/dev-hid: report horizontal wheel through AC Pan
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
                   ` (4 preceding siblings ...)
  2026-10-27 19:42 ` [PATCH v2 5/8] hw/input/hid: accept high-resolution vertical scroll events soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 7/8] hw/usb/dev-hid: support the wheel Resolution Multiplier feature report soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 8/8] hw/input/hid: clear wheel state and mouse_grabbed on reset soumyajyotisarkar23
  7 siblings, 0 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

The USB mouse report descriptor only describes buttons, X, Y and a
vertical wheel, so horizontal scrolling from the host never reaches
the guest.

Rework the descriptor following the layout in Microsoft's "Enhanced
Wheel Support in Windows" paper: the pointer fields move into input
report 1 and the vertical Wheel and the Consumer AC Pan usages each
get their own logical collection. The report descriptor grows from 52
to 90 bytes and the interrupt endpoint packet size from 4 to 6 bytes.

On the HID side, track horizontal movement in a new 'pan' field of the
pointer event queue, fed from WHEEL_LEFT/RIGHT button clicks and from
horizontal scroll events, with the same residual handling as the
vertical wheel. In report protocol hid_pointer_poll() now emits a
6-byte report: report ID, buttons, X, Y, wheel and one pan byte. Boot
protocol keeps the 4-byte layout without report ID and pan.

The pan value is not inverted: a positive AC Pan value means scrolling
right, which matches the sign of the input layer's horizontal scroll.

The queued pan values and the pan residual are migrated in the
optional wheel subsection.

Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 hw/input/hid.c         | 75 ++++++++++++++++++++++++++++-------
 hw/usb/dev-hid.c       | 88 +++++++++++++++++++++++++++++-------------
 include/hw/input/hid.h |  2 +
 3 files changed, 124 insertions(+), 41 deletions(-)

diff --git a/hw/input/hid.c b/hw/input/hid.c
index 2855f6a3d9..9f26850112 100644
--- a/hw/input/hid.c
+++ b/hw/input/hid.c
@@ -148,6 +148,10 @@ static void hid_pointer_event(DeviceState *dev, QemuConsole *src,
                 e->dz -= INPUT_EVENT_SCROLL_DETENT;
             } else if (evt->btn.button == INPUT_BUTTON_WHEEL_DOWN) {
                 e->dz += INPUT_EVENT_SCROLL_DETENT;
+            } else if (evt->btn.button == INPUT_BUTTON_WHEEL_LEFT) {
+                e->pan -= INPUT_EVENT_SCROLL_DETENT;
+            } else if (evt->btn.button == INPUT_BUTTON_WHEEL_RIGHT) {
+                e->pan += INPUT_EVENT_SCROLL_DETENT;
             }
         } else {
             e->buttons_state &= ~bmap[evt->btn.button];
@@ -157,6 +161,8 @@ static void hid_pointer_event(DeviceState *dev, QemuConsole *src,
     case INPUT_EVENT_KIND_SCROLL:
         if (evt->scroll.axis == INPUT_SCROLL_AXIS_VERTICAL) {
             e->dz += evt->scroll.value;
+        } else if (evt->scroll.axis == INPUT_SCROLL_AXIS_HORIZONTAL) {
+            e->pan += evt->scroll.value;
         }
         break;
 
@@ -210,6 +216,8 @@ static void hid_pointer_sync(DeviceState *dev)
         }
         prev->dz += curr->dz;
         curr->dz = 0;
+        prev->pan += curr->pan;
+        curr->pan = 0;
     } else {
         /* prepare next (clear rel, copy abs + btns) */
         if (hs->kind == HID_MOUSE) {
@@ -220,6 +228,7 @@ static void hid_pointer_sync(DeviceState *dev)
             next->ydy = curr->ydy;
         }
         next->dz = 0;
+        next->pan = 0;
         next->buttons_state = curr->buttons_state;
         /* make current guest visible, notify guest */
         hs->n++;
@@ -369,7 +378,7 @@ static int hid_scroll_counts(int32_t *pending, int32_t *residual, int mult)
 
 int hid_pointer_poll(HIDState *hs, uint8_t *buf, int len)
 {
-    int dx, dy, dz, l;
+    int dx, dy, dz, pan, l;
     int index;
     HIDPointerEvent *e;
 
@@ -392,9 +401,11 @@ int hid_pointer_poll(HIDState *hs, uint8_t *buf, int len)
         dy = e->ydy;
     }
     dz = hid_scroll_counts(&e->dz, &hs->ptr.wheel_residual, 1);
+    pan = hid_scroll_counts(&e->pan, &hs->ptr.pan_residual, 1);
 
     if (hs->n &&
         !e->dz &&
+        !e->pan &&
         (hs->kind == HID_TABLET || (!e->xdx && !e->ydy))) {
         /* that deals with this event */
         QUEUE_INCR(hs->head);
@@ -406,17 +417,40 @@ int hid_pointer_poll(HIDState *hs, uint8_t *buf, int len)
     l = 0;
     switch (hs->kind) {
     case HID_MOUSE:
-        if (len > l) {
-            buf[l++] = e->buttons_state;
-        }
-        if (len > l) {
-            buf[l++] = dx;
-        }
-        if (len > l) {
-            buf[l++] = dy;
-        }
-        if (len > l) {
-            buf[l++] = dz;
+        if (hs->protocol == 0) {
+            /* boot protocol (0) */
+            if (len > l) {
+                buf[l++] = e->buttons_state;
+            }
+            if (len > l) {
+                buf[l++] = dx;
+            }
+            if (len > l) {
+                buf[l++] = dy;
+            }
+            if (len > l) {
+                buf[l++] = dz;
+            }
+        } else {
+            /* report protocol (1) */
+            if (len > l) {
+                buf[l++] = 0x01;
+            }
+            if (len > l) {
+                buf[l++] = e->buttons_state;
+            }
+            if (len > l) {
+                buf[l++] = dx;
+            }
+            if (len > l) {
+                buf[l++] = dy;
+            }
+            if (len > l) {
+                buf[l++] = dz;
+            }
+            if (len > l) {
+                buf[l++] = pan;
+            }
         }
         break;
 
@@ -603,6 +637,16 @@ static const VMStateDescription vmstate_hid_ptr_queue = {
     }
 };
 
+static const VMStateDescription vmstate_hid_ptr_queue_pan = {
+    .name = "HIDPointerEventQueue/pan",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_INT32(pan, HIDPointerEvent),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
 /*
  * Wheel movement used to be queued in whole detents and is now kept in
  * 1/120 detent units.  Without this subsection the source either was an
@@ -613,11 +657,11 @@ static bool hid_ptr_wheel_needed(void *opaque)
     HIDState *s = opaque;
     int i;
 
-    if (s->ptr.wheel_residual) {
+    if (s->ptr.wheel_residual || s->ptr.pan_residual) {
         return true;
     }
     for (i = 0; i < QUEUE_LENGTH; i++) {
-        if (s->ptr.queue[i].dz) {
+        if (s->ptr.queue[i].dz || s->ptr.queue[i].pan) {
             return true;
         }
     }
@@ -660,7 +704,10 @@ static const VMStateDescription vmstate_hid_ptr_wheel = {
     .needed = hid_ptr_wheel_needed,
     .post_load = hid_ptr_wheel_post_load,
     .fields = (const VMStateField[]) {
+        VMSTATE_STRUCT_ARRAY(ptr.queue, HIDState, QUEUE_LENGTH, 0,
+                             vmstate_hid_ptr_queue_pan, HIDPointerEvent),
         VMSTATE_INT32(ptr.wheel_residual, HIDState),
+        VMSTATE_INT32(ptr.pan_residual, HIDState),
         VMSTATE_END_OF_LIST()
     }
 };
diff --git a/hw/usb/dev-hid.c b/hw/usb/dev-hid.c
index 2c10e88c85..804119006b 100644
--- a/hw/usb/dev-hid.c
+++ b/hw/usb/dev-hid.c
@@ -85,31 +85,65 @@ static const USBDescStrings desc_strings = {
 static const uint8_t qemu_mouse_hid_report_descriptor[] = {
     0x05, 0x01,         /* Usage Page (Generic Desktop) */
     0x09, 0x02,         /* Usage (Mouse) */
-    0xa1, 0x01,         /* Collection (Application) */
-    0x09, 0x01,         /*   Usage (Pointer) */
-    0xa1, 0x00,         /*   Collection (Physical) */
-    0x05, 0x09,         /*     Usage Page (Button) */
-    0x19, 0x01,         /*     Usage Minimum (1) */
-    0x29, 0x05,         /*     Usage Maximum (5) */
-    0x15, 0x00,         /*     Logical Minimum (0) */
-    0x25, 0x01,         /*     Logical Maximum (1) */
-    0x95, 0x05,         /*     Report Count (5) */
-    0x75, 0x01,         /*     Report Size (1) */
-    0x81, 0x02,         /*     Input (Data, Variable, Absolute) */
-    0x95, 0x01,         /*     Report Count (1) */
-    0x75, 0x03,         /*     Report Size (3) */
-    0x81, 0x01,         /*     Input (Constant) */
-    0x05, 0x01,         /*     Usage Page (Generic Desktop) */
-    0x09, 0x30,         /*     Usage (X) */
-    0x09, 0x31,         /*     Usage (Y) */
-    0x09, 0x38,         /*     Usage (Wheel) */
-    0x15, 0x81,         /*     Logical Minimum (-0x7f) */
-    0x25, 0x7f,         /*     Logical Maximum (0x7f) */
-    0x75, 0x08,         /*     Report Size (8) */
-    0x95, 0x03,         /*     Report Count (3) */
-    0x81, 0x06,         /*     Input (Data, Variable, Relative) */
-    0xc0,               /*   End Collection */
-    0xc0,               /* End Collection */
+    0xA1, 0x01,         /* Collection (Application) */
+
+    0x09, 0x02,         /* Usage (Mouse) */
+    0xA1, 0x02,         /* Collection (Logical) */
+
+    0x85, 0x01,         /* Report ID (0x01) */
+    0x09, 0x01,         /* Usage (Pointer) */
+    0xA1, 0x00,         /* Collection (Physical) */
+
+    0x05, 0x09,         /* Usage Page (Button) */
+    0x19, 0x01,         /* Usage Minimum (1) */
+    0x29, 0x05,         /* Usage Maximum (5) */
+    0x15, 0x00,         /* Logical Minimum (0) */
+    0x25, 0x01,         /* Logical Maximum (1) */
+    0x95, 0x05,         /* Report Count (5) */
+    0x75, 0x01,         /* Report Size (1) */
+    0x81, 0x02,         /* Input (Data,Var,Abs) */
+
+    0x95, 0x01,         /*  Report Count (1) */
+    0x75, 0x03,         /*  Report Size (3) */
+    0x81, 0x01,         /*  Input (Const) */
+
+    0x05, 0x01,         /*  Usage Page (Generic Desktop) */
+    0x09, 0x30,         /*  Usage (X) */
+    0x09, 0x31,         /*  Usage (Y) */
+    0x95, 0x02,         /*  Report Count (2) */
+    0x75, 0x08,         /*  Report Size (8) */
+    0x15, 0x81,         /*  Logical Minimum (-127) */
+    0x25, 0x7F,         /*  Logical Maximum (127) */
+    0x81, 0x06,         /*  Input (Data,Var,Rel) */
+
+    /* Vertical wheel logical collection */
+    0xA1, 0x02,         /*  Collection (Logical) */
+
+    0x09, 0x38,         /*  Usage (Wheel) */
+    0x15, 0x81,         /*  Logical Minimum (-0x7f) */
+    0x25, 0x7F,         /*  Logical Maximum (0x7f) */
+    0x75, 0x08,         /*  Report Size (8) */
+    0x95, 0x01,         /*  Report Count (1) */
+    0x81, 0x06,         /*  Input (Data,Var,Rel) */
+
+    0xC0,               /*  End Collection */
+
+    /* Horizontal wheel logical collection */
+    0xA1, 0x02,         /*  Collection (Logical) */
+
+    0x05, 0x0C,         /*  Usage Page (Consumer Devices) */
+    0x0A, 0x38, 0x02,   /*  Usage (AC Pan) */
+    0x15, 0x81,         /*  Logical Minimum (-0x7f) */
+    0x25, 0x7F,         /*  Logical Maximum (0x7f) */
+    0x75, 0x08,         /*  Report Size (8) */
+    0x95, 0x01,         /*  Report Count (1) */
+    0x81, 0x06,         /*  Input (Data,Var,Rel) */
+
+    0xC0,               /*  End Collection */
+
+    0xC0,               /*  End Collection (Physical) */
+    0xC0,               /*  End Collection (Logical) */
+    0xC0                /*  End Collection (Application) */
 };
 
 static const USBDescIface desc_iface_mouse = {
@@ -139,7 +173,7 @@ static const USBDescIface desc_iface_mouse = {
         {
             .bEndpointAddress      = USB_DIR_IN | 0x01,
             .bmAttributes          = USB_ENDPOINT_XFER_INT,
-            .wMaxPacketSize        = 4,
+            .wMaxPacketSize        = 6,
             .bInterval             = 0x0a,
         },
     },
@@ -172,7 +206,7 @@ static const USBDescIface desc_iface_mouse2 = {
         {
             .bEndpointAddress      = USB_DIR_IN | 0x01,
             .bmAttributes          = USB_ENDPOINT_XFER_INT,
-            .wMaxPacketSize        = 4,
+            .wMaxPacketSize        = 6,
             .bInterval             = 7, /* 2 ^ (8-1) * 125 usecs = 8 ms */
         },
     },
diff --git a/include/hw/input/hid.h b/include/hw/input/hid.h
index 1e29f4ada4..b7cdeb54c2 100644
--- a/include/hw/input/hid.h
+++ b/include/hw/input/hid.h
@@ -10,6 +10,7 @@
 typedef struct HIDPointerEvent {
     int32_t xdx, ydy; /* relative iff it's a mouse, otherwise absolute */
     int32_t dz, buttons_state;
+    int32_t pan;
 } HIDPointerEvent;
 
 #define QUEUE_LENGTH    16 /* should be enough for a triple-click */
@@ -23,6 +24,7 @@ typedef struct HIDMouseState {
     HIDPointerEvent queue[QUEUE_LENGTH];
     int mouse_grabbed;
     int32_t wheel_residual;
+    int32_t pan_residual;
     bool wheel_state_loaded;
 } HIDMouseState;
 
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 7/8] hw/usb/dev-hid: support the wheel Resolution Multiplier feature report
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
                   ` (5 preceding siblings ...)
  2026-10-27 19:42 ` [PATCH v2 6/8] hw/usb/dev-hid: report horizontal wheel through AC Pan soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  2026-10-27 19:42 ` [PATCH v2 8/8] hw/input/hid: clear wheel state and mouse_grabbed on reset soumyajyotisarkar23
  7 siblings, 0 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

Even with high-resolution scroll events reaching the HID mouse, each
wheel report count is still a whole detent, so guests see the same
coarse steps as before.

Add a Resolution Multiplier feature (report ID 2) to the vertical
wheel and AC Pan logical collections, as described in Microsoft's
"Enhanced Wheel Support in Windows" docs. The feature byte holds the
wheel multiplier in bits [1:0] and the pan multiplier in bits [3:2],
each with logical range 0..1 and physical range 1..120. The report
descriptor grows to 144 bytes.

Handle GET_REPORT and SET_REPORT for the feature report, accepting the
payload with or without the leading report ID byte. Windows (Vista and
later) and Linux set each multiplier to its logical maximum. While a
multiplier is enabled in report protocol, hid_pointer_poll() reports
that axis in counts of 1/120 detent, so one count reaches applications
as one WHEEL_DELTA unit. Boot protocol and guests that never enable
the multiplier keep getting whole detents.

The multiplier state is migrated in the optional wheel subsection.

Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 hw/input/hid.c         | 22 ++++++++--
 hw/usb/dev-hid.c       | 97 ++++++++++++++++++++++++++++++++++++++----
 include/hw/input/hid.h |  4 ++
 3 files changed, 111 insertions(+), 12 deletions(-)

diff --git a/hw/input/hid.c b/hw/input/hid.c
index 9f26850112..bc84e99dbc 100644
--- a/hw/input/hid.c
+++ b/hw/input/hid.c
@@ -378,7 +378,7 @@ static int hid_scroll_counts(int32_t *pending, int32_t *residual, int mult)
 
 int hid_pointer_poll(HIDState *hs, uint8_t *buf, int len)
 {
-    int dx, dy, dz, pan, l;
+    int dx, dy, dz, pan, l, mult;
     int index;
     HIDPointerEvent *e;
 
@@ -400,8 +400,19 @@ int hid_pointer_poll(HIDState *hs, uint8_t *buf, int len)
         dx = e->xdx;
         dy = e->ydy;
     }
-    dz = hid_scroll_counts(&e->dz, &hs->ptr.wheel_residual, 1);
-    pan = hid_scroll_counts(&e->pan, &hs->ptr.pan_residual, 1);
+    mult = 1;
+    if (hs->kind == HID_MOUSE && hs->protocol == 1 &&
+        hs->ptr.wheel_multiplier) {
+        mult = HID_WHEEL_RESOLUTION_MULTIPLIER;
+    }
+    dz = hid_scroll_counts(&e->dz, &hs->ptr.wheel_residual, mult);
+
+    mult = 1;
+    if (hs->kind == HID_MOUSE && hs->protocol == 1 &&
+        hs->ptr.pan_multiplier) {
+        mult = HID_WHEEL_RESOLUTION_MULTIPLIER;
+    }
+    pan = hid_scroll_counts(&e->pan, &hs->ptr.pan_residual, mult);
 
     if (hs->n &&
         !e->dz &&
@@ -657,7 +668,8 @@ static bool hid_ptr_wheel_needed(void *opaque)
     HIDState *s = opaque;
     int i;
 
-    if (s->ptr.wheel_residual || s->ptr.pan_residual) {
+    if (s->ptr.wheel_multiplier || s->ptr.pan_multiplier ||
+        s->ptr.wheel_residual || s->ptr.pan_residual) {
         return true;
     }
     for (i = 0; i < QUEUE_LENGTH; i++) {
@@ -706,6 +718,8 @@ static const VMStateDescription vmstate_hid_ptr_wheel = {
     .fields = (const VMStateField[]) {
         VMSTATE_STRUCT_ARRAY(ptr.queue, HIDState, QUEUE_LENGTH, 0,
                              vmstate_hid_ptr_queue_pan, HIDPointerEvent),
+        VMSTATE_UINT8(ptr.wheel_multiplier, HIDState),
+        VMSTATE_UINT8(ptr.pan_multiplier, HIDState),
         VMSTATE_INT32(ptr.wheel_residual, HIDState),
         VMSTATE_INT32(ptr.pan_residual, HIDState),
         VMSTATE_END_OF_LIST()
diff --git a/hw/usb/dev-hid.c b/hw/usb/dev-hid.c
index 804119006b..ffe07ff68b 100644
--- a/hw/usb/dev-hid.c
+++ b/hw/usb/dev-hid.c
@@ -81,6 +81,8 @@ static const USBDescStrings desc_strings = {
  * descriptor.
  * Note: Windows stops recognising the mouse if it does not match
  * exactly.
+ * The enhanced wheel support grew the descriptor from 52 to 144
+ * bytes.
  */
 static const uint8_t qemu_mouse_hid_report_descriptor[] = {
     0x05, 0x01,         /* Usage Page (Generic Desktop) */
@@ -119,7 +121,20 @@ static const uint8_t qemu_mouse_hid_report_descriptor[] = {
     /* Vertical wheel logical collection */
     0xA1, 0x02,         /*  Collection (Logical) */
 
+    0x85, 0x02,         /*  Report ID (0x02) */
+    0x09, 0x48,         /*  Usage (Resolution Multiplier) */
+    0x15, 0x00,         /*  Logical Minimum (0) */
+    0x25, 0x01,         /*  Logical Maximum (1) */
+    0x35, 0x01,         /*  Physical Minimum (1) */
+    0x45, 0x78,         /*  Physical Maximum (120) */
+    0x75, 0x02,         /*  Report Size (2) */
+    0x95, 0x01,         /*  Report Count (1) */
+    0xB1, 0x02,         /*  Feature (Data,Var,Abs) */
+
+    0x85, 0x01,         /*  Report ID (0x01) */
     0x09, 0x38,         /*  Usage (Wheel) */
+    0x35, 0x00,         /*  Physical Minimum (0) */
+    0x45, 0x00,         /*  Physical Maximum (0) */
     0x15, 0x81,         /*  Logical Minimum (-0x7f) */
     0x25, 0x7F,         /*  Logical Maximum (0x7f) */
     0x75, 0x08,         /*  Report Size (8) */
@@ -131,6 +146,23 @@ static const uint8_t qemu_mouse_hid_report_descriptor[] = {
     /* Horizontal wheel logical collection */
     0xA1, 0x02,         /*  Collection (Logical) */
 
+    0x85, 0x02,         /*  Report ID (0x02) */
+    0x09, 0x48,         /*  Usage (Resolution Multiplier) */
+    0x15, 0x00,         /*  Logical Minimum (0) */
+    0x25, 0x01,         /*  Logical Maximum (1) */
+    0x35, 0x01,         /*  Physical Minimum (1) */
+    0x45, 0x78,         /*  Physical Maximum (120) */
+    0x75, 0x02,         /*  Report Size (2) */
+    0x95, 0x01,         /*  Report Count (1) */
+    0xB1, 0x02,         /*  Feature (Data,Var,Abs) */
+
+    0x35, 0x00,         /*  Physical Minimum (0) */
+    0x45, 0x00,         /*  Physical Maximum (0) */
+    0x75, 0x04,         /*  Report Size (4) */
+    0x95, 0x01,         /*  Report Count (1) */
+    0xB1, 0x01,         /*  Feature (Const) */
+
+    0x85, 0x01,         /*  Report ID (0x01) */
     0x05, 0x0C,         /*  Usage Page (Consumer Devices) */
     0x0A, 0x38, 0x02,   /*  Usage (AC Pan) */
     0x15, 0x81,         /*  Logical Minimum (-0x7f) */
@@ -652,17 +684,66 @@ static void usb_hid_handle_control(USBDevice *dev, USBPacket *p,
         }
         break;
     case HID_GET_REPORT:
-        if (hs->kind == HID_MOUSE || hs->kind == HID_TABLET) {
-            p->actual_length = hid_pointer_poll(hs, data, length);
-        } else if (hs->kind == HID_KEYBOARD) {
-            p->actual_length = hid_keyboard_poll(hs, data, length);
+        {
+            uint8_t mult;
+            uint8_t report_type = (value >> 8) & 0xFF;
+            uint8_t report_id = value & 0xFF;
+
+            if (report_type == 0x03 && report_id == 0x02 &&
+                hs->kind == HID_MOUSE) {
+
+                if (length < 1) {
+                    goto fail;
+                }
+
+                mult = ((hs->ptr.pan_multiplier & 0x03) << 2) |
+                       (hs->ptr.wheel_multiplier & 0x03);
+                if (length >= 2) {
+                    data[0] = 0x02;
+                    data[1] = mult;
+                    p->actual_length = 2;
+                } else {
+                    data[0] = mult;
+                    p->actual_length = 1;
+                }
+
+            } else if (hs->kind == HID_MOUSE || hs->kind == HID_TABLET) {
+                p->actual_length = hid_pointer_poll(hs, data, length);
+            } else if (hs->kind == HID_KEYBOARD) {
+                p->actual_length = hid_keyboard_poll(hs, data, length);
+            } else {
+                goto fail;
+            }
         }
         break;
     case HID_SET_REPORT:
-        if (hs->kind == HID_KEYBOARD) {
-            p->actual_length = hid_keyboard_write(hs, data, length);
-        } else {
-            goto fail;
+        {
+            uint8_t mult;
+            uint8_t report_type = (value >> 8) & 0xFF;
+            uint8_t report_id = value & 0xFF;
+
+            if (report_type == 0x03 && report_id == 0x02 &&
+                hs->kind == HID_MOUSE) {
+
+                if (length < 1) {
+                    goto fail;
+                }
+
+                if (length >= 2 && data[0] == 0x02) {
+                    mult = data[1];
+                } else {
+                    mult = data[0];
+                }
+
+                hs->ptr.wheel_multiplier = mult & 0x03;
+                hs->ptr.pan_multiplier = (mult >> 2) & 0x03;
+                p->actual_length = length;
+
+            } else if (hs->kind == HID_KEYBOARD) {
+                p->actual_length = hid_keyboard_write(hs, data, length);
+            } else {
+                goto fail;
+            }
         }
         break;
     case HID_GET_PROTOCOL:
diff --git a/include/hw/input/hid.h b/include/hw/input/hid.h
index b7cdeb54c2..458aa07cae 100644
--- a/include/hw/input/hid.h
+++ b/include/hw/input/hid.h
@@ -7,6 +7,8 @@
 #define HID_TABLET    2
 #define HID_KEYBOARD  3
 
+#define HID_WHEEL_RESOLUTION_MULTIPLIER 120
+
 typedef struct HIDPointerEvent {
     int32_t xdx, ydy; /* relative iff it's a mouse, otherwise absolute */
     int32_t dz, buttons_state;
@@ -23,6 +25,8 @@ typedef void (*HIDEventFunc)(HIDState *s);
 typedef struct HIDMouseState {
     HIDPointerEvent queue[QUEUE_LENGTH];
     int mouse_grabbed;
+    uint8_t wheel_multiplier;
+    uint8_t pan_multiplier;
     int32_t wheel_residual;
     int32_t pan_residual;
     bool wheel_state_loaded;
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 8/8] hw/input/hid: clear wheel state and mouse_grabbed on reset
  2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
                   ` (6 preceding siblings ...)
  2026-10-27 19:42 ` [PATCH v2 7/8] hw/usb/dev-hid: support the wheel Resolution Multiplier feature report soumyajyotisarkar23
@ 2026-10-27 19:42 ` soumyajyotisarkar23
  7 siblings, 0 replies; 10+ messages in thread
From: soumyajyotisarkar23 @ 2026-10-27 19:42 UTC (permalink / raw)
  To: sarkarsoumyajyoti23, qemu-devel
  Cc: Alex Bennée, Helge Deller, Eric Blake,
	Marc-André Lureau, Markus Armbruster, Soumyajyotii Ssarkar,
	Helge Deller

From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>

hid_reset() clears the pointer event queue but leaves the rest of the
mouse state alone. After a guest reboot the wheel and pan multipliers
the previous guest enabled would stay in effect and leftover residuals
would leak into the first reports of the new boot.

Reset the multipliers and residuals together with the queue. Also
clear mouse_grabbed, so that hid_pointer_activate() activates the input
handler again when the rebooted guest first polls the device, instead
of keeping the state from before the reset.

Suggested-by: Helge Deller <deller@gmx.de>
Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
---
 hw/input/hid.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/hw/input/hid.c b/hw/input/hid.c
index bc84e99dbc..403cac127e 100644
--- a/hw/input/hid.c
+++ b/hw/input/hid.c
@@ -551,6 +551,11 @@ void hid_reset(HIDState *hs)
     case HID_MOUSE:
     case HID_TABLET:
         memset(hs->ptr.queue, 0, sizeof(hs->ptr.queue));
+        hs->ptr.mouse_grabbed = 0;
+        hs->ptr.wheel_multiplier = 0;
+        hs->ptr.pan_multiplier = 0;
+        hs->ptr.wheel_residual = 0;
+        hs->ptr.pan_residual = 0;
         break;
     }
     hs->head = 0;
-- 
2.56.0



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 1/8] ui/input: add high-resolution scroll input event
  2026-10-27 19:42 ` [PATCH v2 1/8] ui/input: add high-resolution scroll input event soumyajyotisarkar23
@ 2026-10-28  6:26   ` Markus Armbruster
  0 siblings, 0 replies; 10+ messages in thread
From: Markus Armbruster @ 2026-10-28  6:26 UTC (permalink / raw)
  To: soumyajyotisarkar23
  Cc: sarkarsoumyajyoti23, qemu-devel, Alex Bennée, Helge Deller,
	Eric Blake, Marc-André Lureau

soumyajyotisarkar23@gmail.com writes:

> From: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
>
> The input layer can only describe wheel movement as WHEEL_UP/DOWN/
> LEFT/RIGHT button clicks, one per detent. Host UIs that receive
> smooth scroll deltas (touchpads, free-spinning or high-resolution
> wheels) have to round them to whole clicks and the guest never sees
> the finer movement.
>
> Add a new 'scroll' input event kind carrying a scroll axis (vertical
> or horizontal) and a distance in 1/120 of a wheel detent, the same
> unit Windows uses for WHEEL_DELTA and Linux for REL_WHEEL_HI_RES.
> Positive values scroll down or right.
>
> Scroll events are routed to the handler that receives pointer button
> events for the console. Handlers that do not set
> INPUT_EVENT_MASK_SCROLL keep working unchanged: the input core
> accumulates the distance and emits one wheel button click per full
> detent.

See [*] below.

> Also wire the new event into input-send-event, tracing and
> record/replay.
>
> Signed-off-by: Soumyajyotii Ssarkar <soumyajyotisarkar23@gmail.com>
> ---
>  include/ui/input.h    |  6 ++++
>  qapi/ui.json          | 45 ++++++++++++++++++++++--
>  replay/replay-input.c |  8 +++++
>  ui/input.c            | 82 ++++++++++++++++++++++++++++++++++++++++++-
>  ui/trace-events       |  1 +
>  5 files changed, 139 insertions(+), 3 deletions(-)
>
> diff --git a/include/ui/input.h b/include/ui/input.h
> index a1560cac0f..daedb547ff 100644
> --- a/include/ui/input.h
> +++ b/include/ui/input.h
> @@ -9,6 +9,9 @@
>  #define INPUT_EVENT_MASK_REL   (1<<INPUT_EVENT_KIND_REL)
>  #define INPUT_EVENT_MASK_ABS   (1<<INPUT_EVENT_KIND_ABS)
>  #define INPUT_EVENT_MASK_MTT   (1<<INPUT_EVENT_KIND_MTT)
> +#define INPUT_EVENT_MASK_SCROLL (1 << INPUT_EVENT_KIND_SCROLL)
> +
> +#define INPUT_EVENT_SCROLL_DETENT 120
>  
>  #define INPUT_EVENT_ABS_MIN    0x0000
>  #define INPUT_EVENT_ABS_MAX    0x7FFF
> @@ -31,6 +34,7 @@ typedef struct QemuInputEvent {
>          InputMoveEvent rel;
>          InputMoveEvent abs;
>          InputMultiTouchEvent mtt;
> +        InputScrollEvent scroll;
>      };
>  } QemuInputEvent;
>  
> @@ -80,6 +84,8 @@ int qemu_input_scale_axis(int value,
>  void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value);
>  void qemu_input_queue_abs(QemuConsole *src, InputAxis axis, int value,
>                            int min_in, int max_in);
> +void qemu_input_queue_scroll(QemuConsole *src, InputScrollAxis axis,
> +                             int value);
>  void qemu_input_queue_mtt(QemuConsole *src, InputMultiTouchType type, int slot,
>                            int tracking_id);
>  void qemu_input_queue_mtt_abs(QemuConsole *src, InputAxis axis, int value,
> diff --git a/qapi/ui.json b/qapi/ui.json
> index 9e3e80a3d7..e31fd74f87 100644
> --- a/qapi/ui.json
> +++ b/qapi/ui.json
> @@ -1120,6 +1120,34 @@
>                'axis'       : 'InputAxis',
>                'value'      : 'int' } }
>  
> +##
> +# @InputScrollAxis:
> +#     Scroll axis of a pointer input device.
> +#
> +# @vertical: vertical wheel
> +#
> +# @horizontal: horizontal wheel (pan)
> +#
> +# Since: 11.2
> +##
> +{ 'enum'  : 'InputScrollAxis',
> +  'data'  : [ 'vertical', 'horizontal' ] }

What's the difference to existing InputAxis?

> +
> +##
> +# @InputScrollEvent:
> +#     High-resolution scroll input event.
> +#
> +# @axis: Which scroll axis is referenced by @value.
> +#
> +# @value: Scroll distance in 1/120 of a wheel detent.  Positive
> +#     values scroll down (vertical) or right (horizontal).
> +#
> +# Since: 11.2
> +##
> +{ 'struct'  : 'InputScrollEvent',
> +  'data'  : { 'axis'    : 'InputScrollAxis',
> +              'value'   : 'int' } }
> +
>  ##
>  # @InputEventKind:
>  #
> @@ -1133,10 +1161,12 @@
>  #
>  # @mtt: a multi-touch input event
>  #
> +# @scroll: a high-resolution scroll input event (since 11.2)
> +#
>  # Since: 2.0
>  ##
>  { 'enum': 'InputEventKind',
> -  'data': [ 'key', 'btn', 'rel', 'abs', 'mtt' ] }
> +  'data': [ 'key', 'btn', 'rel', 'abs', 'mtt', 'scroll' ] }
>  
>  ##
>  # @InputKeyEventWrapper:
> @@ -1178,6 +1208,16 @@
>  { 'struct': 'InputMultiTouchEventWrapper',
>    'data': { 'data': 'InputMultiTouchEvent' } }
>  
> +##
> +# @InputScrollEventWrapper:
> +#
> +# @data: High-resolution scroll input event
> +#
> +# Since: 11.2
> +##
> +{ 'struct': 'InputScrollEventWrapper',
> +  'data': { 'data': 'InputScrollEvent' } }
> +
>  ##
>  # @InputEvent:
>  #     Input event union.
> @@ -1193,7 +1233,8 @@
>                'btn'     : 'InputBtnEventWrapper',
>                'rel'     : 'InputMoveEventWrapper',
>                'abs'     : 'InputMoveEventWrapper',
> -              'mtt'     : 'InputMultiTouchEventWrapper' } }
> +              'mtt'     : 'InputMultiTouchEventWrapper',
> +              'scroll'  : 'InputScrollEventWrapper' } }
>  
>  ##
>  # @input-send-event:
> diff --git a/replay/replay-input.c b/replay/replay-input.c
> index c86e00af0e..188c43a137 100644
> --- a/replay/replay-input.c
> +++ b/replay/replay-input.c
> @@ -45,6 +45,10 @@ void replay_save_input_event(QemuInputEvent *evt)
>          replay_put_dword(evt->mtt.axis);
>          replay_put_qword(evt->mtt.value);
>          break;
> +    case INPUT_EVENT_KIND_SCROLL:
> +        replay_put_dword(evt->scroll.axis);
> +        replay_put_qword(evt->scroll.value);
> +        break;
>      case INPUT_EVENT_KIND__MAX:
>          /* keep gcc happy */
>          break;
> @@ -80,6 +84,10 @@ QemuInputEvent *replay_read_input_event(void)
>          evt->mtt.axis = (InputAxis)replay_get_dword();
>          evt->mtt.value = replay_get_qword();
>          break;
> +    case INPUT_EVENT_KIND_SCROLL:
> +        evt->scroll.axis = (InputScrollAxis)replay_get_dword();
> +        evt->scroll.value = replay_get_qword();
> +        break;
>      case INPUT_EVENT_KIND__MAX:
>          /* keep gcc happy */
>          break;
> diff --git a/ui/input.c b/ui/input.c
> index f40a988370..f9ae49e275 100644
> --- a/ui/input.c
> +++ b/ui/input.c
> @@ -181,7 +181,12 @@ void qmp_input_send_event(const char *device,
>      for (e = events; e != NULL; e = e->next) {
>          InputEvent *event = e->value;
>  
> -        if (!qemu_input_find_handler(1 << event->type, con)) {
> +        uint32_t mask = 1 << event->type;
> +
> +        if (event->type == INPUT_EVENT_KIND_SCROLL) {
> +            mask |= INPUT_EVENT_MASK_BTN;
> +        }
> +        if (!qemu_input_find_handler(mask, con)) {
>              error_setg(errp, "Input handler not found for "
>                               "event type %s",
>                              InputEventKind_str(event->type));
> @@ -217,6 +222,10 @@ void qmp_input_send_event(const char *device,
>              evt.mtt = *qapi->u.mtt.data;
>              break;
>  
> +        case INPUT_EVENT_KIND_SCROLL:
> +            evt.scroll = *qapi->u.scroll.data;
> +            break;
> +
>          default:
>              g_assert_not_reached();
>          }
> @@ -235,6 +244,7 @@ static void qemu_input_event_trace(QemuConsole *src, QemuInputEvent *evt)
>      InputBtnEvent *btn;
>      InputMoveEvent *move;
>      InputMultiTouchEvent *mtt;
> +    InputScrollEvent *scroll;
>  
>      if (src) {
>          idx = qemu_console_get_index(src);
> @@ -265,6 +275,12 @@ static void qemu_input_event_trace(QemuConsole *src, QemuInputEvent *evt)
>          name = mtt->axis < INPUT_AXIS__MAX ? InputAxis_str(mtt->axis) : "invalid";
>          trace_input_event_mtt(idx, name, mtt->value);
>          break;
> +    case INPUT_EVENT_KIND_SCROLL:
> +        scroll = &evt->scroll;
> +        name = scroll->axis < INPUT_SCROLL_AXIS__MAX ?
> +            InputScrollAxis_str(scroll->axis) : "invalid";
> +        trace_input_event_scroll(idx, name, scroll->value);
> +        break;
>      case INPUT_EVENT_KIND__MAX:
>          /* keep gcc happy */
>          break;
> @@ -342,12 +358,62 @@ static void qemu_input_queue_sync(QemuInputEventQueueHead *queue)
>      queue_count++;
>  }
>  
> +static void qemu_input_scroll_to_btn(QemuInputHandlerState *s,
> +                                     QemuConsole *src, QemuInputEvent *evt)
> +{
> +    static int residual[INPUT_SCROLL_AXIS__MAX];
> +    QemuInputEvent btn = { .type = INPUT_EVENT_KIND_BTN };
> +    InputScrollAxis axis = evt->scroll.axis;
> +
> +    if (axis >= INPUT_SCROLL_AXIS__MAX) {
> +        return;
> +    }
> +
> +    residual[axis] += evt->scroll.value;
> +    while (abs(residual[axis]) >= INPUT_EVENT_SCROLL_DETENT) {
> +        bool fwd = residual[axis] > 0;
> +
> +        if (axis == INPUT_SCROLL_AXIS_VERTICAL) {
> +            btn.btn.button = fwd ? INPUT_BUTTON_WHEEL_DOWN
> +                                 : INPUT_BUTTON_WHEEL_UP;
> +        } else {
> +            btn.btn.button = fwd ? INPUT_BUTTON_WHEEL_RIGHT
> +                                 : INPUT_BUTTON_WHEEL_LEFT;
> +        }
> +        residual[axis] -= fwd ? INPUT_EVENT_SCROLL_DETENT
> +                              : -INPUT_EVENT_SCROLL_DETENT;
> +
> +        btn.btn.down = true;
> +        qemu_input_event_trace(src, &btn);
> +        s->handler->event(s->dev, src, &btn);
> +        btn.btn.down = false;
> +        qemu_input_event_trace(src, &btn);
> +        s->handler->event(s->dev, src, &btn);
> +        s->events++;

Theoretical problem: counter overflow.  Existing code ignores it, so
ignoring it some more feels okay.

You send two events, but increment the event counter just once.
Checking its use...  looks like it's used like a bool.  Meh.  Not this
patch's problem.

Incrementing it twice could avoid triggering the reader's spider sense
for bugs.

> +    }
> +}
> +
>  void qemu_input_event_send_impl(QemuConsole *src, QemuInputEvent *evt)
>  {
>      QemuInputHandlerState *s;
>  
>      qemu_input_event_trace(src, evt);
>

Special case "scroll event":

> +    if (evt->type == INPUT_EVENT_KIND_SCROLL) {
> +        s = qemu_input_find_handler(INPUT_EVENT_MASK_BTN |
> +                                    INPUT_EVENT_MASK_SCROLL, src);

Difference to the normal case: find handler for button or scroll, not
just scroll.

> +        if (!s) {
> +            return;
> +        }
> +        if (s->handler->mask & INPUT_EVENT_MASK_SCROLL) {

The handler can do scroll.  Send the event normally:

> +            s->handler->event(s->dev, src, evt);
> +            s->events++;
> +        } else {

[*] 

The handler can only do button.  Send suitable button events:

> +            qemu_input_scroll_to_btn(s, src, evt);

The function name suggests a conversion, but it's actually conversion
plus send.  Suggest something like
qemu_input_event_send_scroll_as_btn().

> +        }
> +        return;
> +    }
> +

Normal case: 

>      /* send event */

The comment is now stale.  Suggest to change it to

       /* Send event other than scroll */

and add

       /* Send scroll event */

right before your new code above.

>      s = qemu_input_find_handler(1 << evt->type, src);
>      if (!s) {
           return;
       }
       s->handler->event(s->dev, src, evt);
       s->events++;
   }
> @@ -518,6 +584,20 @@ void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value)
>      qemu_input_event_send(src, &evt);
>  }
>  
> +void qemu_input_queue_scroll(QemuConsole *src, InputScrollAxis axis,
> +                             int value)
> +{
> +    QemuInputEvent evt = {
> +        .type = INPUT_EVENT_KIND_SCROLL,
> +        .scroll = {
> +            .axis = axis,
> +            .value = value,
> +        },
> +    };
> +
> +    qemu_input_event_send(src, &evt);
> +}
> +
>  void qemu_input_queue_abs(QemuConsole *src, InputAxis axis, int value,
>                            int min_in, int max_in)
>  {
> diff --git a/ui/trace-events b/ui/trace-events
> index 339a61f341..638096607a 100644
> --- a/ui/trace-events
> +++ b/ui/trace-events
> @@ -132,6 +132,7 @@ input_event_btn(int conidx, const char *btn, bool down) "con %d, button %s, down
>  input_event_rel(int conidx, const char *axis, int value) "con %d, axis %s, value %d"
>  input_event_abs(int conidx, const char *axis, int value) "con %d, axis %s, value 0x%x"
>  input_event_mtt(int conidx, const char *axis, int value) "con %d, axis %s, value 0x%x"
> +input_event_scroll(int conidx, const char *axis, int value) "con %d, axis %s, value %d"
>  input_event_sync(void) ""
>  
>  # sdl2-input.c



^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-10-28  6:27 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-27 19:42 [PATCH v2 0/8] usb-mouse: high-resolution vertical and horizontal wheel support soumyajyotisarkar23
2026-10-27 19:42 ` [PATCH v2 1/8] ui/input: add high-resolution scroll input event soumyajyotisarkar23
2026-10-28  6:26   ` Markus Armbruster
2026-10-27 19:42 ` [PATCH v2 2/8] ui/gtk: forward smooth scroll deltas as high-resolution scroll events soumyajyotisarkar23
2026-10-27 19:42 ` [PATCH v2 3/8] ui/sdl2: forward precise wheel " soumyajyotisarkar23
2026-10-27 19:42 ` [PATCH v2 4/8] hw/usb/dev-hid: derive the mouse report descriptor length with sizeof() soumyajyotisarkar23
2026-10-27 19:42 ` [PATCH v2 5/8] hw/input/hid: accept high-resolution vertical scroll events soumyajyotisarkar23
2026-10-27 19:42 ` [PATCH v2 6/8] hw/usb/dev-hid: report horizontal wheel through AC Pan soumyajyotisarkar23
2026-10-27 19:42 ` [PATCH v2 7/8] hw/usb/dev-hid: support the wheel Resolution Multiplier feature report soumyajyotisarkar23
2026-10-27 19:42 ` [PATCH v2 8/8] hw/input/hid: clear wheel state and mouse_grabbed on reset soumyajyotisarkar23

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.