Skip to content

Input forwarding blocks for 1s per FF upload retry when source device has ff_effects_max=1 (effect never erased → ENOSPC) #704

Description

@MountainFlayer

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

  1. The source device has exactly one FF effect slot.
  2. 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.
  3. 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.
  4. SDL then uses target effect id 1 for the actual rumble. InputPlumber tries to
    allocate a second source effect, which fails with ENOSPC.
  5. 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

  1. Use a source device whose driver reports ff_effects_max = 1.
  2. Target xb360.
  3. Run any FF client (Moonlight/SDL) and have it request rumble repeatedly with more
    than one effect id.
  4. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions