forked from unom/punktfunk
Four encoder faults, plus a note on a fifth that turned out not to be one. A centred stick did not encode as centre on the Y axes. The mapper inverted the already-quantised byte, and 0..255 has no exact midpoint: the forward map puts centre at 0x80, so mirroring the output lands on 0x7F — one below the 0x80 that DsState::neutral and the pad's own resting report use. Games idle-poll a centred stick constantly, so a DualSense, Edge or DS4 sat under a permanent sub-deadzone tilt. Inverting in i16 space instead maps centre to centre by construction and keeps both extremes exact; the only cost is i16::MIN and -32767 sharing a code, one LSB at the very end of the travel. Force-feedback ignored replay.delay. It was decoded on upload and never read: an effect started the moment it was played and ended replay.length later, so anything scheduling a delayed effect — DirectInput under Wine does this routinely — fired early AND finished early by the same amount. The delay now shifts the whole window, with length measured from the end of the delay rather than eaten into by it. Writing the test for that surfaced a second bug in the same path: a waiting effect was still a candidate for the abandoned-effect force-off, and since the play command is itself the last FF activity, an infinite effect with a delay longer than the idle window would be killed on its first contributing tick after sitting silent the whole time it waited. Being abandoned now requires the effect to have been audible for the window too. An empty serial panicked the service thread. The reply builder clamped the length to at least 1 and then sliced that many bytes out of the string, which asks a zero-byte slice for one byte. The kernel already has a graceful answer for a length it rejects, so report the true one and let it fall back. Deck triggers could not reach full pull. Scaling by 128 tops out at 32640 of a declared 32767, leaving the last 127 counts unreachable, so no game could ever see the axis bottom out. One multiply gets both ends exact. The idle watchdog is left alone. It does cut finite multi-second effects that the uinput path exempts, but only the uinput path is handed an explicit duration; the protocols behind the watchdog are level-triggered with no duration field anywhere in a report, so there is nothing at that layer to exempt. The choice is between cutting a long effect and letting an abandoned one drone forever, and only the latter has field evidence behind it. Recorded at the constant so the next reader sees the cost rather than rediscovering it.