input: preserve relative mouse motion between polls (#2593)

## Changes

- Accumulate relative mouse movement until it is read and consumed by an
input mapping.
- Maintain an independent read position for each `InputRelative`
binding.
- Synchronize relative input positions when input bindings or mouse
capture state changes.
- Apply the same relative-motion handling to the Raw Input, NSEvent, and
Xlib mouse drivers.

## Motivation

In my testing, `InputManager::poll()` is called approximately once every
8ms on my system, with observed intervals generally no shorter than 5ms.
At 60Hz, each frame lasts approximately 16.67ms, so input is normally
polled twice per frame.

Each poll retrieves the mouse-axis movement accumulated during the
preceding approximately 8ms. However, emulator cores generally read
relative input only once per frame. This means that `InputManager` may
complete its second poll before the core reads the movement value.

The previous implementation retained only the mouse-axis movement
reported by the most recent poll. The second poll therefore overwrote
the result of the first, allowing the core to observe only the movement
from the latest approximately 8ms. Mouse movement from the earlier
approximately 8ms of the frame was lost.

This change accumulates all relative mouse movement produced between
consecutive core input reads. As a result, even when
`InputManager::poll()` runs multiple times within a single frame, the
core can still retrieve the complete mouse-axis movement accumulated
during that period.
This commit is contained in:
ZengGengSen
2026-07-30 20:29:09 +01:00
committed by GitHub
parent 53ba720c8d
commit b80f67d383
8 changed files with 143 additions and 51 deletions
+3 -3
View File
@@ -16,10 +16,10 @@ auto InputManager::createHotkeys() -> void {
hotkeys.push_back(InputHotkey("Toggle Mouse Capture").onPress([&] {
Program::Guard guard;
if(!emulator) return;
if(!ruby::input.acquired()) {
ruby::input.acquire();
if(!inputManager.acquired()) {
inputManager.acquire();
} else {
ruby::input.release();
inputManager.release();
}
}));
+70 -24
View File
@@ -179,7 +179,7 @@ auto InputDigital::value() -> s16 {
output = value != 0;
}
if(device->isMouse() && groupID == HID::Mouse::GroupID::Button && ruby::input.acquired()) {
if(device->isMouse() && groupID == HID::Mouse::GroupID::Button && inputManager.acquired()) {
output = value != 0;
}
@@ -223,7 +223,7 @@ auto InputHotkey::value() -> s16 {
output = value != 0;
}
if(device->isMouse() && groupID == HID::Mouse::GroupID::Button && ruby::input.acquired()) {
if(device->isMouse() && groupID == HID::Mouse::GroupID::Button && inputManager.acquired()) {
output = value != 0;
}
@@ -363,7 +363,7 @@ auto InputAbsolute::value() -> s16 {
if (device->isKeyboard() && program.keyboardCaptured) continue;
s16 value = device->group(groupID).input(inputID).value();
if(device->isMouse() && groupID == HID::Joypad::GroupID::Axis && ruby::input.acquired()) {
if(device->isMouse() && groupID == HID::Joypad::GroupID::Axis && inputManager.acquired()) {
result += value;
}
@@ -389,45 +389,51 @@ auto InputRelative::bind(u32 binding, std::shared_ptr<HID::Device> device, u32 g
return unbind(binding), true;
}
if(device->isMouse() && groupID == HID::Mouse::GroupID::Axis) {
return bind(binding, assignment), true;
bool accepted = device->isMouse() && groupID == HID::Mouse::GroupID::Axis;
if(device->isJoypad() && groupID == HID::Joypad::GroupID::Axis) {
if(oldValue >= -16384 && newValue < -16384) accepted = true;
if(oldValue <= +16384 && newValue > +16384) accepted = true;
}
if(!accepted) return false;
if(device->isJoypad() && groupID == HID::Joypad::GroupID::Axis
&& oldValue >= -16384 && newValue < -16384
) {
return bind(binding, assignment), true;
InputMapping::bind(binding, assignment);
synchronize();
return true;
}
auto InputRelative::synchronize() -> void {
lock_guard<recursive_mutex> inputLock(program.inputMutex);
for(u32 index : range(BindingLimit)) {
auto& binding = bindings[index];
if(!binding.device || !binding.device->isMouse()) continue;
if(binding.groupID != HID::Mouse::GroupID::Axis) continue;
auto& mouse = static_cast<HID::Mouse&>(*binding.device);
if(binding.inputID >= mouse.axes().size()) continue;
mouse.motion(binding.inputID).synchronize(previous[index]);
}
if(device->isJoypad() && groupID == HID::Joypad::GroupID::Axis
&& oldValue <= +16384 && newValue > +16384
) {
return bind(binding, assignment), true;
}
return false;
}
auto InputRelative::value() -> s16 {
lock_guard<recursive_mutex> inputLock(program.inputMutex);
s32 result = 0;
s64 result = 0;
for(auto& binding : bindings) {
for(u32 index : range(BindingLimit)) {
auto& binding = bindings[index];
if(!binding.device) continue; //unbound
auto& device = binding.device;
auto& groupID = binding.groupID;
auto& inputID = binding.inputID;
auto& qualifier = binding.qualifier;
if (device->isKeyboard() && program.keyboardCaptured) continue;
s16 value = device->group(groupID).input(inputID).value();
if(device->isMouse() && groupID == HID::Joypad::GroupID::Axis && ruby::input.acquired()) {
result += value;
if(device->isMouse() && groupID == HID::Mouse::GroupID::Axis && inputManager.acquired()) {
auto& mouse = static_cast<HID::Mouse&>(*device);
result += mouse.motion(inputID).read(previous[index]);
}
if(device->isJoypad() && groupID == HID::Joypad::GroupID::Axis) {
result += value;
result += device->group(groupID).input(inputID).value();
}
}
@@ -535,6 +541,46 @@ auto InputManager::bind() -> void {
}
}
for(auto& mapping : hotkeys) mapping.bind();
synchronize();
}
auto InputManager::acquired() -> bool {
return ruby::input.acquired();
}
auto InputManager::acquire() -> bool {
lock_guard<recursive_mutex> inputLock(program.inputMutex);
if(!ruby::input.acquire()) return false;
synchronize();
return true;
}
auto InputManager::release() -> bool {
lock_guard<recursive_mutex> inputLock(program.inputMutex);
if(!ruby::input.release()) return false;
synchronize();
return true;
}
auto InputManager::synchronize() -> void {
lock_guard<recursive_mutex> inputLock(program.inputMutex);
for(auto& port : virtualPorts) {
for(auto& input : port.mouse.inputs) {
if(input.type == InputNode::Type::Relative) static_cast<InputRelative&>(*input.mapping).synchronize();
}
}
for(auto& emulator : emulators) {
for(auto& port : emulator->ports) {
for(auto& device : port.devices) {
if(!device.hasDirectMappings()) continue;
for(auto& input : device.inputs) {
if(input.type == InputNode::Type::Relative) {
static_cast<InputRelative&>(input.configuredMapping()).synchronize();
}
}
}
}
}
}
auto InputManager::poll(bool force) -> void {
+8
View File
@@ -58,7 +58,11 @@ struct InputAbsolute : InputMapping {
struct InputRelative : InputMapping {
using InputMapping::bind;
auto bind(u32 binding, std::shared_ptr<HID::Device>, u32 groupID, u32 inputID, s16 oldValue, s16 newValue) -> bool override;
auto synchronize() -> void;
auto value() -> s16 override;
private:
s64 previous[BindingLimit] = {};
};
//specifies a target joypad for force feedback
@@ -299,6 +303,10 @@ struct VirtualPort {
struct InputManager {
auto create() -> void;
auto bind() -> void;
auto acquired() -> bool;
auto acquire() -> bool;
auto release() -> bool;
auto synchronize() -> void;
auto poll(bool force = false) -> void;
auto eventInput(std::shared_ptr<HID::Device>, u32 groupID, u32 inputID, s16 oldValue, s16 newValue) -> void;
+8 -8
View File
@@ -77,14 +77,14 @@ auto Program::videoFullScreenToggle() -> void {
ruby::video.clear();
if(!ruby::video.fullScreen()) {
ruby::video.setFullScreen(true);
if(!ruby::input.acquired()) {
if(!inputManager.acquired()) {
if(ruby::video.exclusive() || ruby::video.hasMonitors().size() == 1) {
ruby::input.acquire();
inputManager.acquire();
}
}
} else {
if(ruby::input.acquired()) {
ruby::input.release();
if(inputManager.acquired()) {
inputManager.release();
}
ruby::video.setFullScreen(false);
presentation.viewport.setFocused();
@@ -99,13 +99,13 @@ auto Program::videoPseudoFullScreenToggle() -> void {
if(!presentation.fullScreen()) {
presentation.setFullScreen(true);
presentation.menuBar.setVisible(false);
if(!ruby::input.acquired() && ruby::video.hasMonitors().size() == 1) {
ruby::input.acquire();
if(!inputManager.acquired() && ruby::video.hasMonitors().size() == 1) {
inputManager.acquire();
}
startPseudoFullScreen = true;
} else {
if(ruby::input.acquired()) {
ruby::input.release();
if(inputManager.acquired()) {
inputManager.release();
}
if(!kiosk) presentation.menuBar.setVisible(true);
presentation.setFullScreen(false);
+27
View File
@@ -117,10 +117,37 @@ struct Mouse : Device {
enum : u16 { GenericVendorID = 0x0000, GenericProductID = 0x0002 };
enum GroupID : u32 { Axis, Button };
struct Motion {
auto move(s16 distance) -> void {
position += distance;
}
auto synchronize(s64& previous) const -> void {
previous = position;
}
auto read(s64& previous) const -> s64 {
auto distance = position - previous;
previous = position;
return distance;
}
private:
s64 position = 0;
};
Mouse() : Device("Mouse") { append("Axis"), append("Button"); }
auto isMouse() const -> bool { return true; }
auto axes() -> Group& { return group(GroupID::Axis); }
auto buttons() -> Group& { return group(GroupID::Button); }
auto appendAxis(const string& name) -> void {
axes().append(name);
_motion.emplace_back();
}
auto motion(u32 id) -> Motion& { return _motion[id]; }
private:
std::vector<Motion> _motion;
};
struct Joypad : Device {
+9 -5
View File
@@ -13,6 +13,7 @@ struct InputMouseNS {
auto acquire() -> bool {
[NSCursor hide];
previousLocation = [NSEvent mouseLocation];
isAcquired = true;
return acquired();
}
@@ -25,9 +26,12 @@ struct InputMouseNS {
auto assign(u32 groupID, u32 inputID, s16 value) -> void {
auto& group = hid->group(groupID);
if(group.input(inputID).value() == value) return;
input.doChange(hid, groupID, inputID, group.input(inputID).value(), value);
group.input(inputID).setValue(value);
auto& target = group.input(inputID);
if(target.value() != value) {
input.doChange(hid, groupID, inputID, target.value(), value);
target.setValue(value);
}
if(groupID == HID::Mouse::GroupID::Axis) hid->motion(inputID).move(value);
}
auto poll(std::vector<std::shared_ptr<HID::Device>>& devices) -> void {
@@ -52,8 +56,8 @@ struct InputMouseNS {
hid->setProductID(HID::Mouse::GenericProductID);
hid->setPathID(0);
hid->axes().append("X");
hid->axes().append("Y");
hid->appendAxis("X");
hid->appendAxis("Y");
hid->buttons().append("Left");
hid->buttons().append("Middle");
+10 -6
View File
@@ -88,6 +88,9 @@ struct InputMouseRawInput {
auto extents = desktopExtents();
WaitForSingleObject(rawinput.mutex, INFINITE);
motion.refresh(extents);
ms.relativeX = 0;
ms.relativeY = 0;
ms.relativeZ = 0;
ReleaseMutex(rawinput.mutex);
}
@@ -151,9 +154,10 @@ struct InputMouseRawInput {
auto assign(u32 groupID, u32 inputID, s16 value) -> void {
auto& group = ms.hid->group(groupID);
if(group.input(inputID).value() == value) return;
input.doChange(ms.hid, groupID, inputID, group.input(inputID).value(), value);
group.input(inputID).setValue(value);
auto& target = group.input(inputID);
if(target.value() != value) input.doChange(ms.hid, groupID, inputID, target.value(), value);
target.setValue(value);
if(groupID == HID::Mouse::GroupID::Axis) ms.hid->motion(inputID).move(value);
}
auto poll(std::vector<std::shared_ptr<HID::Device>>& devices) -> void {
@@ -192,9 +196,9 @@ struct InputMouseRawInput {
ms.hid->setProductID(HID::Mouse::GenericProductID);
ms.hid->setPathID(0);
ms.hid->axes().append("X");
ms.hid->axes().append("Y");
ms.hid->axes().append("Z");
ms.hid->appendAxis("X");
ms.hid->appendAxis("Y");
ms.hid->appendAxis("Z");
ms.hid->buttons().append("Left");
ms.hid->buttons().append("Middle");
+8 -5
View File
@@ -59,9 +59,12 @@ struct InputMouseXlib {
auto assign(u32 groupID, u32 inputID, s16 value) -> void {
auto& group = hid->group(groupID);
if(group.input(inputID).value() == value) return;
input.doChange(hid, groupID, inputID, group.input(inputID).value(), value);
group.input(inputID).setValue(value);
auto& target = group.input(inputID);
if(target.value() != value) {
input.doChange(hid, groupID, inputID, target.value(), value);
target.setValue(value);
}
if(groupID == HID::Mouse::GroupID::Axis) hid->motion(inputID).move(value);
}
auto poll(std::vector<std::shared_ptr<HID::Device>>& devices) -> void {
@@ -150,8 +153,8 @@ struct InputMouseXlib {
hid->setProductID(HID::Mouse::GenericProductID);
hid->setPathID(0);
hid->axes().append("X");
hid->axes().append("Y");
hid->appendAxis("X");
hid->appendAxis("Y");
hid->buttons().append("Left");
hid->buttons().append("Middle");