[GH-ISSUE #25] i386: PS/2 mouse is erratic under QEMU (backwards jumps, corner snaps, phantom clicks) #20

Closed
opened 2026-11-03 22:02:23 -07:00 by gitea-mirror · 3 comments
Owner

Originally created by @raynorpat on GitHub (Sep 25, 2026).
Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/25

Summary

On i386 under QEMU, the PS/2 mouse is erratic. Fast moves jump the cursor backwards or to the far corner of the screen, and it clicks when no button was pressed. It happens with our reconstructed drvPS2Mouse and with Apple's original driver, so the cause is shared code, not only the reconstruction.

Environment

  • QEMU 11.1.0, -M pc -cpu pentium -accel tcg, emulated i8042 PS/2 mouse
  • vm/work/eide-ne2k.img with our kernel and the EIDE and NE2K drivers
  • Rhapsody DR2 WindowServer, testing at the login window

Symptoms

  1. The cursor jumps backwards on fast moves. A quick flick in one direction briefly moves the cursor the other way.
  2. Phantom clicks during motion, with no button pressed.
  3. The cursor snaps to the far corner. A burst of up-left motion can leave the cursor at the bottom-right, or somewhere mid-screen on the bottom edge.
  4. Bursts lurch. Motion that arrives in a burst moves the cursor far more than the same motion sent slowly.

Measurements

These came from a headless QEMU copy driven over QMP with input-send-event. Screenshots located the cursor, and PS/2 tracing was on (-trace pckbd_kbd_read_data -trace ps2_mouse_send_packet).

  • Twenty +3 moves: 27 px when sent 150 ms apart, 273 px when sent back to back.
  • A burst of 12 × (-127, -127) (up-left) landed at (799, 599) starting from (100,100), and at (229, 599) starting from (57,57). Other runs pinned correctly at (0,0).
  • The trace for that burst was clean. QEMU queued 12 packets 18 81 7F and the guest read exactly those 36 bytes, in order. By this point the PS/2 layer was losing nothing, so the wrong-way jump happens after decoding.

Causes found

Several separate defects add up to the symptoms above. All of them are inherited from Apple's code.

  1. drvPS2Mouse: the running delta total wraps. Packets that arrive while an event is still being dispatched are added into summedEvent as raw bytes. PCPointerEvent's dx/dy are 8-bit signed bitfields, so a total past +127 wraps and reverses direction. Three packets of +60 arrive as -76. QEMU sends packets back to back, so they pile up on almost every fast move. Apple's kernel-7/bsd/dev/i386/PS2Mouse.m does the same adds.
  2. drvPS2Mouse: no packet framing check. Byte 0 of every packet has bit 3 set, and the driver never checks it. It only resyncs after 250 ms of silence, so one lost byte misframes every later packet. Movement bytes then land in the button byte, which gives the phantom clicks.
  3. drvPS2Keyboard (PS2Controller): keyboard paths read mouse bytes. keyboardDataPresent(), reallyGetKeyboardData() and the IRQ 1 handler test only output-buffer-full, never the 8042's auxiliary-data bit (0x20). The keyboard I/O-thread drain loop, NewStealKeyboardEvent() and setLEDs: can each consume a mouse byte as a scancode. That byte is lost to the mouse driver and the packets go out of step.
  4. driverkit EventInput.m: the pointer location wraps. -relativePointerEvent: adds each accelerated delta into pointerLoc, which is { short x, y; }. When the WindowServer holds the cursor lock, _setCursorPosition: puts off clipping, and later deltas keep adding into pointerLoc unclipped. A burst of heavily accelerated deltas passes ±32767 and wraps, so the cursor ends up on the far side or mid-screen on an edge. This explains the corner snaps in the clean trace.
  5. EventSrcPCPointer: bursts get maximum acceleration. Acceleration is computed from the time between events. Back-to-back packets have a near-zero interval, so each one gets the top scaling factor. This is the lurching, and it is not a decoding bug.

Real hardware rarely triggers 1, 4 or 5, because a physical mouse reports at about 100 Hz with small deltas. QEMU splits large host motion into back-to-back packets of up to ±127, and TCG slows the guest's I/O thread and WindowServer, so all three fire routinely.

Originally created by @raynorpat on GitHub (Sep 25, 2026). Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/25 ## Summary On i386 under QEMU, the PS/2 mouse is erratic. Fast moves jump the cursor backwards or to the far corner of the screen, and it clicks when no button was pressed. It happens with our reconstructed `drvPS2Mouse` and with Apple's original driver, so the cause is shared code, not only the reconstruction. ## Environment - QEMU 11.1.0, `-M pc -cpu pentium -accel tcg`, emulated i8042 PS/2 mouse - `vm/work/eide-ne2k.img` with our kernel and the EIDE and NE2K drivers - Rhapsody DR2 WindowServer, testing at the login window ## Symptoms 1. **The cursor jumps backwards on fast moves.** A quick flick in one direction briefly moves the cursor the other way. 2. **Phantom clicks** during motion, with no button pressed. 3. **The cursor snaps to the far corner.** A burst of up-left motion can leave the cursor at the bottom-right, or somewhere mid-screen on the bottom edge. 4. **Bursts lurch.** Motion that arrives in a burst moves the cursor far more than the same motion sent slowly. ## Measurements These came from a headless QEMU copy driven over QMP with `input-send-event`. Screenshots located the cursor, and PS/2 tracing was on (`-trace pckbd_kbd_read_data -trace ps2_mouse_send_packet`). - Twenty +3 moves: **27 px** when sent 150 ms apart, **273 px** when sent back to back. - A burst of 12 × (-127, -127) (up-left) landed at **(799, 599)** starting from (100,100), and at **(229, 599)** starting from (57,57). Other runs pinned correctly at (0,0). - The trace for that burst was clean. QEMU queued 12 packets `18 81 7F` and the guest read exactly those 36 bytes, in order. By this point the PS/2 layer was losing nothing, so the wrong-way jump happens after decoding. ## Causes found Several separate defects add up to the symptoms above. All of them are inherited from Apple's code. 1. **`drvPS2Mouse`: the running delta total wraps.** Packets that arrive while an event is still being dispatched are added into `summedEvent` as raw bytes. `PCPointerEvent`'s `dx`/`dy` are 8-bit signed bitfields, so a total past +127 wraps and reverses direction. Three packets of +60 arrive as -76. QEMU sends packets back to back, so they pile up on almost every fast move. Apple's `kernel-7/bsd/dev/i386/PS2Mouse.m` does the same adds. 2. **`drvPS2Mouse`: no packet framing check.** Byte 0 of every packet has bit 3 set, and the driver never checks it. It only resyncs after 250 ms of silence, so one lost byte misframes every later packet. Movement bytes then land in the button byte, which gives the phantom clicks. 3. **`drvPS2Keyboard` (`PS2Controller`): keyboard paths read mouse bytes.** `keyboardDataPresent()`, `reallyGetKeyboardData()` and the IRQ 1 handler test only output-buffer-full, never the 8042's auxiliary-data bit (0x20). The keyboard I/O-thread drain loop, `NewStealKeyboardEvent()` and `setLEDs:` can each consume a mouse byte as a scancode. That byte is lost to the mouse driver and the packets go out of step. 4. **driverkit `EventInput.m`: the pointer location wraps.** `-relativePointerEvent:` adds each accelerated delta into `pointerLoc`, which is `{ short x, y; }`. When the WindowServer holds the cursor lock, `_setCursorPosition:` puts off clipping, and later deltas keep adding into `pointerLoc` unclipped. A burst of heavily accelerated deltas passes ±32767 and wraps, so the cursor ends up on the far side or mid-screen on an edge. This explains the corner snaps in the clean trace. 5. **`EventSrcPCPointer`: bursts get maximum acceleration.** Acceleration is computed from the time between events. Back-to-back packets have a near-zero interval, so each one gets the top scaling factor. This is the lurching, and it is not a decoding bug. Real hardware rarely triggers 1, 4 or 5, because a physical mouse reports at about 100 Hz with small deltas. QEMU splits large host motion into back-to-back packets of up to ±127, and TCG slows the guest's I/O thread and WindowServer, so all three fire routinely.
gitea-mirror 2026-11-03 22:02:23 -07:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@raynorpat commented on GitHub (Sep 25, 2026):

Proposed fix

Branch claude/ps2-mouse-qemu-erratic-35b897. Each fix is a labelled divergence from Apple's code, and the reconstruction fixes are recorded in the drivers' reconstruction/divergences.md.

1. drvPS2Mouse: saturate summed deltas (8ec72be4a)

A new addDelta() adds two delta bytes as signed char and clamps the result to ±127. All six adds in PS2MouseIntHandler use it, so a summed burst stops at the cap instead of reversing. Very fast motion can now fall short of the full distance, but it never goes the wrong way.

2. drvPS2Mouse: drop bytes that can't start a packet (40c0c767c)

When the handler expects byte 0 of a packet and the byte doesn't have bit 3 set, it drops it. A misframed stream then realigns within a packet or two instead of waiting for 250 ms of silence. Linux's psmouse makes the same check. It can't catch every slip, because a data byte can have bit 3 set too, which is why fix 3 matters.

3. drvPS2Keyboard: leave mouse bytes for the mouse (a4640a130)

PS2Controller.m gets a new PS2_STATUS_AUX_DATA (0x20):

  • keyboardDataPresent() reports hardware data only when bit 0 is set and bit 5 is clear.
  • The IRQ 1 handler reads only under the same condition, leaving mouse bytes for the IRQ 12 handler.
  • reallyGetKeyboardData() reads and discards a mouse byte while it waits. Its callers hold the controller lock at spl 6, so the mouse handler can't take the byte, and waiting would hang. Fix 2 resyncs the mouse afterwards.

4. driverkit: saturate pointerLoc (4e58aa4c1)

-relativePointerEvent: in libDriver/Kernel/EventInput.m now adds in int and clamps each axis to the short range before storing into pointerLoc. A deferred burst then parks at the extreme, and _setCursorPosition: pins it to the screen edge in the right direction. This needs a kernel rebuild, since libDriver is linked into mach_kernel.

5. Burst acceleration: open, pending testing

Not changed yet. Options, if it still feels bad once 1–4 are in:

  • Give EventSrcPCPointer's acceleration a minimum time between events (for example about 8 ms, one report at 125 Hz), so back-to-back packets aren't treated as extreme velocity.
  • Or fold packets that arrive back to back in the driver into a single event.

The first is simpler and also covers any other pointer driver.

Status

  • drvPS2Keyboard and drvPS2Mouse build on the i386 guest, and both register at boot on eide-ne2k.img.
  • A trace confirms the PS/2 byte stream is now read cleanly.
  • The driverkit/kernel rebuild with fix 4 is in progress. It will be boot-tested under QEMU before deciding on 5.
<!-- gh-comment-id:5833545096 --> @raynorpat commented on GitHub (Sep 25, 2026): ## Proposed fix Branch `claude/ps2-mouse-qemu-erratic-35b897`. Each fix is a labelled divergence from Apple's code, and the reconstruction fixes are recorded in the drivers' `reconstruction/divergences.md`. ### 1. `drvPS2Mouse`: saturate summed deltas (8ec72be4a) A new `addDelta()` adds two delta bytes as `signed char` and clamps the result to ±127. All six adds in `PS2MouseIntHandler` use it, so a summed burst stops at the cap instead of reversing. Very fast motion can now fall short of the full distance, but it never goes the wrong way. ### 2. `drvPS2Mouse`: drop bytes that can't start a packet (40c0c767c) When the handler expects byte 0 of a packet and the byte doesn't have bit 3 set, it drops it. A misframed stream then realigns within a packet or two instead of waiting for 250 ms of silence. Linux's psmouse makes the same check. It can't catch every slip, because a data byte can have bit 3 set too, which is why fix 3 matters. ### 3. `drvPS2Keyboard`: leave mouse bytes for the mouse (a4640a130) `PS2Controller.m` gets a new `PS2_STATUS_AUX_DATA` (0x20): - `keyboardDataPresent()` reports hardware data only when bit 0 is set and bit 5 is clear. - The IRQ 1 handler reads only under the same condition, leaving mouse bytes for the IRQ 12 handler. - `reallyGetKeyboardData()` reads and discards a mouse byte while it waits. Its callers hold the controller lock at spl 6, so the mouse handler can't take the byte, and waiting would hang. Fix 2 resyncs the mouse afterwards. ### 4. driverkit: saturate `pointerLoc` (4e58aa4c1) `-relativePointerEvent:` in `libDriver/Kernel/EventInput.m` now adds in `int` and clamps each axis to the `short` range before storing into `pointerLoc`. A deferred burst then parks at the extreme, and `_setCursorPosition:` pins it to the screen edge in the right direction. This needs a kernel rebuild, since libDriver is linked into `mach_kernel`. ### 5. Burst acceleration: open, pending testing Not changed yet. Options, if it still feels bad once 1–4 are in: - Give `EventSrcPCPointer`'s acceleration a minimum time between events (for example about 8 ms, one report at 125 Hz), so back-to-back packets aren't treated as extreme velocity. - Or fold packets that arrive back to back in the driver into a single event. The first is simpler and also covers any other pointer driver. ### Status - `drvPS2Keyboard` and `drvPS2Mouse` build on the i386 guest, and both register at boot on `eide-ne2k.img`. - A trace confirms the PS/2 byte stream is now read cleanly. - The driverkit/kernel rebuild with fix 4 is in progress. It will be boot-tested under QEMU before deciding on 5.
Author
Owner

@raynorpat commented on GitHub (Sep 25, 2026):

Update: remaining fixes, and testing

Testing the first four fixes under QEMU turned up two more problems, plus a build-profile gap.

5. Burst acceleration: kernel EventSrcPCPointer (fee0ad761)

Adds a minimum time between events for pointer acceleration: MIN_EVENT_DELTA, 153 scaled ticks, about 10 ms (one report at the PS/2 default of 100 Hz). Packets that queue up and arrive back to back no longer each get the top scaling factor. Real hardware at 100 Hz or slower is unaffected.

6. drvPS2Mouse: resync only on a forward time gap (1a80dece9)

After fixes 1–5, some snapping remained. A QEMU trace of 602 packets showed the guest reading every byte exactly once, in order, so nothing was lost at the 8042. A diagnostic build of the driver then logged eight mid-packet resyncs over one set of sweeps:

  • Seven happened because IOGetTimestamp() stepped backwards between two bytes of one packet. The unsigned ns_time_t difference wrapped to an enormous value and tripped the 250 ms timeout.
  • One was a real 731 ms host stall.

Each resync restarted framing on a movement byte. That byte could pass the bit-3 check (0x08 has bit 3 set), so horizontal motion came out vertical. The timeout now fires only when newStamp > lastTimeStamp and the forward gap is over 250 ms.

The backwards clock itself (clock_get_counter(System) on i386) is a separate kernel bug and is being handled separately.

7. rbuild toolchain profiles: /usr/local/bin on the path (87cd2d6aa)

Kernel builds on the i386 QEMU build guest failed with relpath: not found. relpath comes from bootstrap-cmds at /usr/local/bin inside the build chroot, but the profiles' path= didn't include that directory. All three profiles now do.

Testing

The test image is eide-ne2k.img with the rebuilt kernel (driverkit fix 4 and kernel fix 5) and both rebuilt PS/2 drivers. Mouse input was driven over QMP at about 125 Hz in slow, medium and fast sweeps in all four directions, and the cursor was located from screendumps.

  • Before fix 6: 9–10 odd steps per run, with motion on the wrong axis and jumps to the corners.
  • After: 0 odd steps in two full runs. Every sweep stayed on its axis with steady step sizes.
  • Hands-on in a QEMU window: confirmed good; no snapping, and the lurching is gone.
<!-- gh-comment-id:5835827111 --> @raynorpat commented on GitHub (Sep 25, 2026): ## Update: remaining fixes, and testing Testing the first four fixes under QEMU turned up two more problems, plus a build-profile gap. ### 5. Burst acceleration: kernel `EventSrcPCPointer` (fee0ad761) Adds a minimum time between events for pointer acceleration: `MIN_EVENT_DELTA`, 153 scaled ticks, about 10 ms (one report at the PS/2 default of 100 Hz). Packets that queue up and arrive back to back no longer each get the top scaling factor. Real hardware at 100 Hz or slower is unaffected. ### 6. `drvPS2Mouse`: resync only on a forward time gap (1a80dece9) After fixes 1–5, some snapping remained. A QEMU trace of 602 packets showed the guest reading every byte exactly once, in order, so nothing was lost at the 8042. A diagnostic build of the driver then logged eight mid-packet resyncs over one set of sweeps: - **Seven** happened because `IOGetTimestamp()` stepped **backwards** between two bytes of one packet. The unsigned `ns_time_t` difference wrapped to an enormous value and tripped the 250 ms timeout. - **One** was a real 731 ms host stall. Each resync restarted framing on a movement byte. That byte could pass the bit-3 check (0x08 has bit 3 set), so horizontal motion came out vertical. The timeout now fires only when `newStamp > lastTimeStamp` and the forward gap is over 250 ms. The backwards clock itself (`clock_get_counter(System)` on i386) is a separate kernel bug and is being handled separately. ### 7. rbuild toolchain profiles: `/usr/local/bin` on the path (87cd2d6aa) Kernel builds on the i386 QEMU build guest failed with `relpath: not found`. `relpath` comes from `bootstrap-cmds` at `/usr/local/bin` inside the build chroot, but the profiles' `path=` didn't include that directory. All three profiles now do. ### Testing The test image is `eide-ne2k.img` with the rebuilt kernel (driverkit fix 4 and kernel fix 5) and both rebuilt PS/2 drivers. Mouse input was driven over QMP at about 125 Hz in slow, medium and fast sweeps in all four directions, and the cursor was located from screendumps. - **Before fix 6:** 9–10 odd steps per run, with motion on the wrong axis and jumps to the corners. - **After:** **0 odd steps in two full runs.** Every sweep stayed on its axis with steady step sizes. - **Hands-on in a QEMU window:** confirmed good; no snapping, and the lurching is gone.
Author
Owner

@raynorpat commented on GitHub (Sep 25, 2026):

Fixed on master in 04a699b5a (merge of the PS/2 mouse fixes above).

<!-- gh-comment-id:5835845394 --> @raynorpat commented on GitHub (Sep 25, 2026): Fixed on master in 04a699b5a (merge of the PS/2 mouse fixes above).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
github-starred/RhapsodiOS#20
No description provided.