Summary
When the source evdev device reports ff_effects_max = 1, InputPlumber uploads one
force feedback effect, never erases it, and then fails every subsequent upload with
ENOSPC. Each failed upload makes the xpad target wait 1 second for a response
that never arrives, and during that second the target device emits no input events
at all. Games that rumble repeatedly therefore produce chains of 1-second input
freezes: the client keeps the last controller state, sticks appear stuck, and buttons
stop responding.
I measured a 20-second freeze made of exactly 20 back-to-back 1-second timeouts.
Environment
|
|
| InputPlumber |
0.75.2-2 |
| Distro |
Pocknix Linux |
| Kernel |
7.1.5, aarch64 |
| Device |
Retroid Pocket 6 |
| Source device |
AYN Odin2 Gamepad — /dev/input/event3, EVIOCGEFFECTS returns 1 |
| Target |
xb360 + keyboard → virtual Microsoft X-Box 360 pad on /dev/input/event11 |
| FF consumer |
Moonlight 6.1.0 (Flatpak, SDL), streaming from Sunshine on Windows |
Symptom
All controller input freezes for a few seconds at a time. The controller does not
disconnect: the host keeps the last received state, so a held stick keeps the character
walking or turning. New inputs made during the freeze are lost. It recovers on its own.
Root cause
- The source device has exactly one FF effect slot.
- InputPlumber uploads source effect
0 early in the session and never erases it —
not when the client disconnects, not when the target device goes away. It stayed
occupied across multiple streaming sessions and application exits, for over an hour.
- I confirmed ownership by trying to erase it externally as root:
ioctl(fd, EVIOCRMFF, 0) returns EACCES, i.e. the effect belongs to
InputPlumber's own file descriptor. Nothing outside InputPlumber can free it.
- SDL then uses target effect id
1 for the actual rumble. InputPlumber tries to
allocate a second source effect, which fails with ENOSPC.
- The
xpad target blocks for 1 second per attempt waiting for an upload response.
Input forwarding is stalled for the whole second.
Evidence
Three independently captured logs, clocks NTP-synced, cross-referenced.
ff_effects_max and slot ownership, sampled once per second by uploading a
zero-magnitude FF_RUMBLE effect with id = -1 and erasing it immediately (the kernel
assigns the lowest free id, so a successful upload with id N means N effects are
already held):
16:28:02 ff_effects_max=1
16:28:02 -> 16:30:19 OCCUPIED (ENOSPC) leftover from a previous session
16:30:19.425 FREE previous stream disconnected
16:30:57.552 OCCUPIED first rumble of the new session
... still OCCUPIED hours later, across app exits and stream disconnects
The freeze itself, correlating the host, the target device, and the journal:
host 16:53:49.663 XInput packet number stops advancing (controller still connected)
journal 16:53:50.778 ERROR target::xpad Failed to receive FF upload response: Timeout
journal 16:53:51.778 ERROR target::xpad Failed to receive FF upload response: Timeout
journal 16:53:51.779 ERROR composite_device Error uploading effect to evdev://event3:
ServiceError("Failed to upload effect:
DeviceError(\"No space left on device (os error 28)\")")
journal 16:53:52.784 ERROR target::xpad Failed to receive FF upload response: Timeout
... 20 timeouts in total, exactly 1 s apart ...
journal 16:54:09.842 ERROR target::xpad Failed to receive FF upload response: Timeout
host 16:54:09.734 XInput resumes
The target device /dev/input/event11, read directly on the device, stopped emitting
events in exactly 1000 ms blocks, chained, over the same window:
16:53:51.782 gap 2005 ms
16:53:52.784 gap 1000 ms
16:53:53.786 gap 1001 ms
16:53:54.786 gap 1000 ms
16:53:56.801 gap 2015 ms
16:53:57.803 gap 1000 ms
... through 16:54:03.809, then the freeze continued
That is the smoking gun: the stall is inside InputPlumber, quantised to the 1-second
FF upload timeout, not in the client or the network.
Counts for one ~18 minute session:
|
|
Unable to find source effect id for effect 1 from evdev://event3 |
10,972 |
Failed to receive FF upload response: Timeout |
20 |
Error uploading effect ... No space left on device |
15 |
Control run: with the streamed game configured to emit no rumble at all, and force
feedback still enabled in InputPlumber, 22 minutes of play produced 0 FF
messages of any kind and 0 freezes. So the trigger is purely the rumble traffic.
Reproduction
- Use a source device whose driver reports
ff_effects_max = 1.
- Target
xb360.
- Run any FF client (Moonlight/SDL) and have it request rumble repeatedly with more
than one effect id.
- The first upload takes the only slot permanently; every later one blocks input for
1 second per attempt.
Probe used to observe the slot (run as root):
import fcntl, os, struct
EVIOCGEFFECTS, EVIOCSFF, EVIOCRMFF, FF_RUMBLE = 0x80044584, 0x40304580, 0x40044581, 0x50
fd = os.open('/dev/input/event3', os.O_RDWR)
print('ff_effects_max =', struct.unpack('i', fcntl.ioctl(fd, EVIOCGEFFECTS, struct.pack('i', 0)))[0])
eff = bytearray(48)
struct.pack_into('<HhH', eff, 0, FF_RUMBLE, -1, 0) # type, id=-1 (allocate), direction
struct.pack_into('<HH', eff, 10, 100, 0) # replay: length, delay
fcntl.ioctl(fd, EVIOCSFF, eff, True) # ENOSPC (28) => all slots held
print('assigned id =', struct.unpack_from('<h', eff, 2)[0])
Expected behaviour
Force feedback should never be able to stall normal input forwarding.
Suggested fixes, roughly in order of value:
- Do not block event forwarding on the FF upload round-trip. Move the upload
handshake off the path that forwards input events, so a slow or failed upload cannot
freeze sticks and buttons.
- Reuse the existing source effect instead of allocating a new one. evdev supports
updating an effect in place by passing an already-assigned id in EVIOCSFF. This
matters a lot when ff_effects_max is 1.
- Erase source effects when the corresponding target effect is erased, and when the
target device is destroyed or the client disconnects. Right now the slot leaks for
the lifetime of the daemon.
- Fail fast on
ENOSPC — drop the effect instead of retrying behind a 1-second
timeout.
Workaround
Disabling force feedback on the composite device avoids it entirely:
busctl set-property org.shadowblip.InputPlumber \
/org/shadowblip/InputPlumber/CompositeDevice0 \
org.shadowblip.Output.ForceFeedback Enabled b false
inputplumber-logs.zip
AI Disclosure: This issue was drafted with the assistance of an AI tool.
Summary
When the source evdev device reports
ff_effects_max = 1, InputPlumber uploads oneforce feedback effect, never erases it, and then fails every subsequent upload with
ENOSPC. Each failed upload makes thexpadtarget wait 1 second for a responsethat never arrives, and during that second the target device emits no input events
at all. Games that rumble repeatedly therefore produce chains of 1-second input
freezes: the client keeps the last controller state, sticks appear stuck, and buttons
stop responding.
I measured a 20-second freeze made of exactly 20 back-to-back 1-second timeouts.
Environment
AYN Odin2 Gamepad—/dev/input/event3,EVIOCGEFFECTSreturns 1xb360+keyboard→ virtualMicrosoft X-Box 360 padon/dev/input/event11Symptom
All controller input freezes for a few seconds at a time. The controller does not
disconnect: the host keeps the last received state, so a held stick keeps the character
walking or turning. New inputs made during the freeze are lost. It recovers on its own.
Root cause
0early in the session and never erases it —not when the client disconnects, not when the target device goes away. It stayed
occupied across multiple streaming sessions and application exits, for over an hour.
ioctl(fd, EVIOCRMFF, 0)returnsEACCES, i.e. the effect belongs toInputPlumber's own file descriptor. Nothing outside InputPlumber can free it.
1for the actual rumble. InputPlumber tries toallocate a second source effect, which fails with
ENOSPC.xpadtarget blocks for 1 second per attempt waiting for an upload response.Input forwarding is stalled for the whole second.
Evidence
Three independently captured logs, clocks NTP-synced, cross-referenced.
ff_effects_maxand slot ownership, sampled once per second by uploading azero-magnitude
FF_RUMBLEeffect withid = -1and erasing it immediately (the kernelassigns the lowest free id, so a successful upload with id
NmeansNeffects arealready held):
The freeze itself, correlating the host, the target device, and the journal:
The target device
/dev/input/event11, read directly on the device, stopped emittingevents in exactly 1000 ms blocks, chained, over the same window:
That is the smoking gun: the stall is inside InputPlumber, quantised to the 1-second
FF upload timeout, not in the client or the network.
Counts for one ~18 minute session:
Unable to find source effect id for effect 1 from evdev://event3Failed to receive FF upload response: TimeoutError uploading effect ... No space left on deviceControl run: with the streamed game configured to emit no rumble at all, and force
feedback still enabled in InputPlumber, 22 minutes of play produced 0 FF
messages of any kind and 0 freezes. So the trigger is purely the rumble traffic.
Reproduction
ff_effects_max = 1.xb360.than one effect id.
1 second per attempt.
Probe used to observe the slot (run as root):
Expected behaviour
Force feedback should never be able to stall normal input forwarding.
Suggested fixes, roughly in order of value:
handshake off the path that forwards input events, so a slow or failed upload cannot
freeze sticks and buttons.
updating an effect in place by passing an already-assigned
idinEVIOCSFF. Thismatters a lot when
ff_effects_maxis 1.target device is destroyed or the client disconnects. Right now the slot leaks for
the lifetime of the daemon.
ENOSPC— drop the effect instead of retrying behind a 1-secondtimeout.
Workaround
Disabling force feedback on the composite device avoids it entirely:
busctl set-property org.shadowblip.InputPlumber \ /org/shadowblip/InputPlumber/CompositeDevice0 \ org.shadowblip.Output.ForceFeedback Enabled b falseinputplumber-logs.zip
AI Disclosure: This issue was drafted with the assistance of an AI tool.