github Nonary/Vibepollo 2.0.2-nightly.d20261025.ed7f5b2d04cf

pre-releaseone hour ago

This is a nightly release of Vibepollo and contains several bug fixes and may contain new features. Check the commit log for more information.

Windows installer · Linux (Arch x86_64)

Nightly Windows installers are unsigned and include self-signed gamepad components. They may cause false-positive antivirus alerts.

Commit log

Source: ed7f5b2d04cf

Since the preceding successfully published nightly 2.0.2-nightly.d20261024.df3b45b4df14: compare exact sources.

4403d8221363

Hyprland KMS: launch Steam games (#669)

* test(linux): keep Hyprland broker assertions in release builds

RelWithDebInfo builds define NDEBUG, which compiled the fixture's assert()
checks away. Undefine it for this target, as other fixtures do.

* feat(linux): launch Steam and Lutris games under Hyprland KMS

Steam, Big Picture, steam-direct and Lutris were refused because the
Hyprland session record had no X11 display. The controller now records
UWSM's DISPLAY only when its socket, reached through the compositor's root,
is served by the bound compositor; Hyprland's Xwayland has no Xauthority.
The broker allows these launches only with that display.

18742d0a73c3

Steam Controller (2026) support over the VHF virtual gamepad (#629)

* Steam Controller (2026): protocol constants and control packets 0x5504/0x5505

Define the Moonlight extensions a Steam Controller (2026) client uses in one
header, src/steam_controller_protocol.h: the LI_CTYPE_STEAM arrival type, the
LI_CCAP_GRIP_SENSE / STICK_TOUCH / STEAM_HAPTIC / SC26_BUTTONS / STEAM_SETTINGS
capability bits, the grip, stick-touch, pad-click and trigger-click button
flags in the upper half of the button word, and the two control packet types.
Each value is a fallback define with a static_assert, so a moonlight-common-c
bump that carries the real names fails the build instead of masking a drift.

Two new gamepad feedback messages carry what the virtual controller's driver
hands back verbatim: a haptic output report (0x80 rumble, 0x81 pulse, 0x82
command, ...) and a settings write (SET_SETTINGS_VALUES triples or
LOAD_DEFAULT_SETTINGS). The control stream encodes them as packet types 0x5504
and 0x5505 (Vibepollo protocol extension), only ever sent to a client that
advertised the matching capability.

The same header defines the firmware attributes extension: the
LI_CCAP_STEAM_ATTRIBUTES capability bit (0x4000), the LI_SCATTR_* field and
transport bits, LI_STEAM_CONTROLLER_ATTRIBUTES and the
SS_STEAM_CONTROLLER_ATTRIBUTES_PACKET input packet (magic 0x55000008, 84 bytes,
u32 fields LE32) exactly as the client fork's Limelight.h/Input.h define them,
again as guarded fallbacks with static_asserts on every value and on the packed
layout, so a submodule bump that disagrees fails the build instead of masking
the fallback.

Input side: the controller touch event now carries the client's touchpadIndex
(the two Steam pads), controller-touch batching keeps the pads apart,
buttonFlags2 is masked to 16 bits before widening (a flag at bit 15 must not
sign-extend), and controller state batching preserves trigger press/release
and full-pull (0xF0) edges so a tap cannot be coalesced away before the
driver's trigger click sees it.

(cherry picked from commit e170d7fa5b7dda8a8adbb25acc993ecb715ccf16)
(cherry picked from commit 617b9897837829ad04c02c3fe0ae329af3c657be)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017y2gdXBFREitNzWvBmTrVr
(cherry picked from commit 6e94d9cea428d1bf4f9a3dacf88b8ecaf7622aa4)
(cherry picked from commit 61f314990d44ddc5611dba4d114e3c651b3ca2cb)

* Gamepad feedback queue: bounded classes, stop-key coalescing, short control wait

Steam's UI drives a Steam Controller's pads at a rate the 32-slot gamepad
feedback mailbox was never meant for: per-side haptic stops arrive 0.7 ms
apart, test-screen clicks every 100 ms, and a 20-triple settings write lands
as three events that must survive whole and in order. Overflowing the old
queue cleared every producer's pending output, so one haptic burst could drop
a rumble stop or half a settings write.

Give the queue three classes behind one entry point, raise_gamepad_feedback():
  * state keys (rumble, trigger rumble, LED, motion requests, adaptive
    triggers, periodic 0x80 rumble and the per-side haptic stops) coalesce in
    place of the pending key and are never evicted;
  * settings writes are a reserved class with its own bound (16) that ordinary
    events cannot evict;
  * everything else (haptic pulses and commands) is bounded at 32 and evicts
    its oldest sibling on overflow.
The mailbox is created with capacity 256 and no clear-on-overflow, so an
ordinary raise() on a full queue drops the newest message instead of flushing
the rest. safe::queue_t gains raise_latest_classed() for this; raise_latest()
is unchanged.

A session with a controller attached keeps the control loop on its 5 ms wait
(input::has_gamepad), so a haptic click or a rumble stop is not held for up to
150 ms behind the idle poll.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QUMtkvaR8iaFLhd98mBEA
(cherry picked from commit 7d3ef1de48ef8a0f123f5fb19864f8ea87d1113a)
(cherry picked from commit 2dfeb85a16b55f67276da4d15948067169c60e84)
(cherry picked from commit c2af27c7cb84e0ee1ab524a86c8afb1d03c46c82)

* VHF gamepad: Steam Controller (2026) profile, pad and motion mapping, haptic and settings forwarding

A client that arrives as LI_CTYPE_STEAM (or the new `vhf_steam` option) now
gets the driver's steam_controller profile when the driver offers it: Steam
sees a real Steam Controller with two trackpads, capacitive grips and stick
touch, motion sensors and a battery, instead of a DualSense approximation.
Profile selection is unchanged for a driver that lacks the profile (plain
`vhf` and `auto` retry with the driver's automatic profile; an explicit
`vhf_*` choice is never substituted), so the option is inert on an older
driver rather than a failure.

Input: the LI_CCAP_GRIP_SENSE, STICK_TOUCH and SC26_BUTTONS flags pass the
supported-button mask to the driver, and a client that advertised authoritative
clicks is marked with the driver's sc26_buttons_explicit metadata bit (derived
from the arrival capability, never from client input). A dual-touchpad client
addresses the pads by touchpadIndex; a single-touchpad client's halves map
onto the two pads (map_steam_touch), each pointer keeping the half it went
down on. Contacts are keyed by (touchpad, pointer) since both pads reuse
pointer id 0. Motion is requested at the IMU's native 250 Hz.

Feedback: the driver forwards Steam's haptic output reports and settings
writes verbatim. A client that advertised LI_CCAP_STEAM_HAPTIC receives every
haptic report (nothing is deduplicated: Steam re-sends clicks to keep a buzz
going); any other client gets rumble synthesized from the report
(synthesize_steam_rumble: 0x80 motor speeds with the real unit's safety
timeout, 0x81 pulse trains as duty cycle for the train's length, 0x82
ticks/clicks as timed rumble from their gain), with the 0x81/0x82 side
conventions measured on the real unit (0x81: side 1 left; 0x82: side 0 left;
side 2 both). Settings writes go only to a LI_CCAP_STEAM_SETTINGS client, as
events that are never coalesced. The poll drains up to four events per 8 ms
pass since Steam's per-side stops arrive 0.7 ms apart. When a slot is torn
down its driver-side feedback-drop counters are logged, and feedback still
queued for its client-relative index is discarded before the index is reused.

Two wake-ups of the feedback thread are now notified under its mutex; the
first controller's alloc could otherwise lose the notify between the
predicate and the untimed wait and leave the thread asleep for the session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QUMtkvaR8iaFLhd98mBEA
(cherry picked from commit 9dd993015ed6249a98498d2c25053e8ed452a4e4)
(cherry picked from commit ceace5c06cc34f566f5ef4f325bca875d8cf7415)
(cherry picked from commit e4fb7335336c6d3802440a81f75be527cc1d7f92)

* Add the vhf_steam gamepad option to the web UI and the configuration docs

The Steam Controller (2026) profile is selectable as `vhf_steam` next to the
other Vibepollo virtual gamepad profiles in both web UIs (the option list,
the platform filter and the English label). docs/configuration.md documents
the option, what plain `vhf` does for a client that reports a Steam, Switch or
PlayStation controller when the driver offers (or lacks) that profile, the
motion_as_ds4/touchpad_as_ds4 hints that also apply under `vhf`, and that
forward_rumble covers Steam Controller haptics.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QUMtkvaR8iaFLhd98mBEA
(cherry picked from commit 78d2348b9f1709bc6f644f4c8624521a641b3da0)
(cherry picked from commit 2193a783523912294aa4d53e8c8d73583e04807a)
(cherry picked from commit 396400b84c7df3577ffdd39fa7914a0c17ba63d0)

* docs: Steam Controller (2026) over the VHF virtual gamepad

Describe how a Steam Controller client is matched to the driver's profile
under auto, vhf and vhf_steam (and what happens on a driver without the
profile), what the host maps (clicks and capacitive touch, the two trackpads
for dual- and single-touchpad clients, 250 Hz motion, battery), the haptic
and settings feedback paths (verbatim to a client that advertised them as
packets 0x5504/0x5505, rumble synthesis otherwise, the side conventions), the
feedback-drop counters logged at slot teardown, the protocol constants and
their static_asserts, the driver dependency and the tests that cover it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QUMtkvaR8iaFLhd98mBEA
(cherry picked from commit c18dc88cd97473fda3a0a266dfa43b4654d4b27c)
(cherry picked from commit b0d8acbd2998eee1abf3a956ae4cfe36abc52288)
(cherry picked from commit 51110df30b54717f36feb2670fa66ad6b4083684)

* Steam Controller (2026): echo the real controller's firmware attributes and warn against firmware updates over the stream

The virtual controller answers Steam's attribute reads with defaults captured
from one wired unit, so Steam used to offer a "controller firmware update" for
the streamed controller whenever those defaults fell behind the real one. A
client that advertises LI_CCAP_STEAM_ATTRIBUTES reads the real controller's
attributes at link setup and sends them after the arrival event on the same
reliable channel as SS_STEAM_CONTROLLER_ATTRIBUTES_PACKET (0x55000008).

Decode and gate (src/input.cpp): the packet is admitted at its exact size and
needs the controller input permission like the other controller packets. The
little-endian fields are decoded, each string keeps at most 15 printable ASCII
characters (anything else becomes '?', always NUL terminated), and the new
platf::gamepad_attributes() is called only for a controller whose arrival
announced LI_CTYPE_STEAM with LI_CCAP_STEAM_ATTRIBUTES; the slot now remembers
the arrival's type and capabilities for that gate. A packet that arrives before
the slot exists is held and applied once the arrival says what the controller
is. Linux and macOS have no virtual Steam Controller and ignore the call.

Slot lifetime: a failed allocation (no free id, or platf::alloc_gamepad
refusing, on the arrival path and on the legacy state-packet path) marks the
slot alloc_failed and drops any held attributes; a packet that arrives after
a failed allocation is not held, since there is nothing to apply it to. The
legacy path stops retrying the allocation for a slot that already failed (one
warning instead of one per packet), and a slot that is only alloc_failed or
only holding pending attributes is reset like an allocated one when the
client's activeGamepadMask drops it, so neither state survives a removal.
A new arrival clears alloc_failed: it is a new attempt whatever an earlier one
did.

Relay (src/platform/windows): the VHF backend maps the packet to
lvg::client::set_steam_controller_attributes() for a slot running
profile::steam_controller whose client advertised LI_CCAP_STEAM_ATTRIBUTES.
The LI_SCATTR_* bits pass through as the driver's sc_attr_* flags (pinned
equal by static_assert) for the six fields the driver takes, a field whose bit
is clear is zeroed, and the radio/dongle firmware build times are logged only
since the virtual device is a wired unit. An installed driver that predates
the call answers ERROR_REVISION_MISMATCH: one info line

  VHF gamepad N (Steam Controller): the driver predates firmware attributes;
  Steam sees the virtual controller's defaults, not the real controller
  firmware build <UTC> (0x...), ...

and the virtual controller keeps its defaults; any other error is a warning.
On success one info line names what Steam will now see:

  VHF gamepad N (Steam Controller): real controller firmware build <UTC>
  (0x...), bootloader <UTC> (0x...), board rev R, build id ..., serial ...,
  via usb|ble|puck

Warning: every Steam Controller slot creation logs

  Do not start a Steam Controller firmware update from Steam while streaming:
  the update would target this virtual controller, not the real one. Update
  with the controller connected directly to a PC.

The same sentence appears verbatim in the gamepad option help of both web UIs
(vhf_steam), in the vhf_steam row of docs/configuration.md and in
docs/steam_controller.md, which also gains a section on the packet, the driver
call and the old-driver fallback, names the call in the driver dependency and
lists the new policy cases and harness under Tests.

The libvirtualgamepad gitlink is not moved here; the driver release that
carries set_steam_controller_attributes is pinned separately.

(cherry picked from commit 799b6975c38ebce23593237cdde523dcce1d8e1c)
(cherry picked from commit ffbbf0121124c3213f32b0368431eb7c357ad01a)
(cherry picked from commit b44d4d371a35a883c0753b92d564c50bc99cb71a)
(cherry picked from commit 3715bf82083abffe5f291230af247fcb56523590, src/input.cpp)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017y2gdXBFREitNzWvBmTrVr
(cherry picked from commit 8fe3a44909bfff71f05c21f73b3ab80e968cbbd0)
(cherry picked from commit 972ac893b06a2c08b2bf0c1d0004107af25a66ec)

* tests: Steam Controller policy cases, host harnesses and a PR validation workflow

test_vhf_gamepad_policy.cpp gains the Steam Controller cases: profile
selection for LI_CTYPE_STEAM and `vhf_steam`, the grip/stick-touch/click bits
through the supported-button mask, the sc26_buttons_explicit metadata bit
gated on capability and profile, split-pad touch ownership (map_steam_touch),
verbatim haptic and settings decoding with malformed shapes refused, rumble
synthesis for 0x80/0x81/0x82 including the measured side conventions, the
touch-event sentinel, and the feedback-drop counters. Seven firmware-attribute
cases follow: the packet keeps the client fork's layout (84/76 bytes, driver
mask 0xCF); attributes are accepted only by a steam_controller slot whose
client advertised LI_CCAP_STEAM_ATTRIBUTES; the driver request carries the
fields and flags through; every field the client did not read is zeroed (a
flagged but empty serial reaches the driver empty); strings keep fifteen
printable ASCII characters; Unix build times format as UTC without a locale;
the log description names what Steam will see for full, partial and empty
masks and for each transport.

tests/input gains six harnesses that compile the production code with fake
driver boundaries under ASan/UBSan (documented in tests/input/README.md):
test_haptic_queue.py (raise_steam_haptic and the mailbox classes),
test_steam_settings_forward.py (raise_steam_settings, chunk ordering, the
reserved class), test_vhf_feedback_retirement.py (vhf_gamepad_t::free
discarding a retired slot's queued output), test_vhf_fallback.py (the Windows
allocation path: Steam clients prefer VHF under auto, client-derived profiles
retry the automatic profile, explicit profiles are never substituted,
controller touch is advertised wherever a touchpad can result),
test_sc26_buttons.py (the host adapter against the driver's real Steam
Controller encoder from the libvirtualgamepad submodule) and
test_steam_attributes_forward.py (the production size table, decode, arrival
gate, pending-packet hold and VHF relay against a fake driver client, slot
table and log: field decode and string sanitising, the six fields and flags
reaching the driver, the success info line, re-application on Steam's
re-polls, ERROR_REVISION_MISMATCH as one info line and any other error as a
warning, packets dropped for non-Steam arrivals and for Steam clients without
the capability, the VHF slot's own gate, a packet before the arrival held and
applied once a Steam arrival allocates the slot or dropped for a non-Steam
one, out-of-range numbers refused, nothing decoded with controller input
disabled, and the firmware-update warning sentence present in the backend,
both UIs and both docs).

The allocation-lifetime cases: test_steam_attributes_forward.py checks that a
failed allocation drops held attributes and that a packet after a failed
allocation is not held; test_disconnect_release.py checks that a slot holding
only pending attributes is retired when another controller's activeGamepadMask
drops it, that exhausted ids on the legacy path mark the slot failed and
retire its attributes until the client removes it, and that a failed legacy
allocation is not retried per packet. The two existing harnesses learn the new
input_t members (allocated_gamepads from the feedback-queue commit, the arrival
type/capabilities, alloc_failed and pending_steam_attributes) and the packet
magic, since they compile the production snippets against their own input_t.

.github/workflows/pr-build.yml runs those harnesses on pull requests and
builds the Windows server with tests (build_only, build_tests) through the
existing ci-windows.yml, so the VHF policy gtest runs on every PR.

(cherry picked from commit b1b05cc95350cbeb494ce71c4067243772ba488c)
(cherry picked from commit fd7248469aa38a4e87ae1dedf5d0c68aa25ca563)
(cherry picked from commit 6b6cd20bae816216f3f27ffc2a6075826ac84117)
(cherry picked from commit 3715bf82083abffe5f291230af247fcb56523590, tests/input)

Upstream's tests/unit/test_stream_disconnect_lifecycle.py splices the control packet type table out of
src/stream.cpp; with the Steam haptic and settings packet types in that table the spliced snippet needs
their defines, so the harness now reads STEAM_HAPTIC_PACKET_TYPE / STEAM_SETTINGS_PACKET_TYPE from
src/steam_controller_protocol.h and prefixes them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017y2gdXBFREitNzWvBmTrVr
(cherry picked from commit 12b4d88e8b0af3110692b7e464daed484d12142c)
(cherry picked from commit 3c0adc017d4bd7c10cc95ed4f68867adba2241b6)

* STAGING ONLY: pin the VHF driver to jlobue10/libvirtualgamepad v0.1.0-beta.154

Not for upstream. The host sources in this branch need the Steam Controller protocol additions (the profile, the feedback types and the set_steam_controller_attributes call) that only the fork's driver releases carry until the companion Nonary/libvirtualgamepad PR lands; this commit lets the fork's CI build the branch. Replace it with a pin to the Nonary release that includes the companion PR.

The third-party/libvirtualgamepad gitlink moves from Nonary's cc8ee144d47c2a9944f65017fa4ca0f4d6023942 (v0.1.0-beta.8) to jlobue10/libvirtualgamepad 57de074169e5d65d06775edc6cff7ab4ec9305f1, the feat/steam-controller-profile merge commit that beta.153 was released from; the five pin files are regenerated from the release lock.

source revision 57de074169e5d65d06775edc6cff7ab4ec9305f1
DriverVer 10/16/2026,0.1.0.227 (from the release lock)
archive sha256 c81e857be8921cc0005839b5159141ae603a44234be08890c553f291a6845fb4
protocol version 2 (unchanged)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017y2gdXBFREitNzWvBmTrVr
(cherry picked from commit a23e17d6aa88f96461bfce4846a8bc61a5091590)

* vhf: the Steam Controller profile only for a Steam Controller client controller

LI_CTYPE_STEAM alone selected the Steam Controller (2026) profile, and a
configured vhf_steam applied it to every controller a client forwards. Both
make a second Steam Controller out of something that is not one: a keyboard's
joystick interface or another pad next to the real controller (two Steam
Controllers in Steam, haptics and firmware prompts to the wrong one), or a
Steam Deck's built-in controls, which moonlight-qt reports as the Steam type
without any of the Steam capabilities.

is_steam_controller_client() requires the Steam type together with at least
one Steam extension (grip sense, pad haptic relay, explicit pad/trigger
clicks), which only the Steam Controller client drivers report. The automatic
path and the auto-mode VHF preference use it; a configured vhf_steam keeps the
profile for such controllers and lets any other controller fall through to
what its own metadata earns, with an info line saying so.

Tests: gtest policy cases for the gate (each extension bit, the SDL Steam
Deck shape, forced vhf_steam with Xbox/PS/unknown/plain-Steam clients); the
fallback harness compiles the helper and gives its Steam client the
extensions; configuration.md says which controllers the profile applies to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017y2gdXBFREitNzWvBmTrVr

* tests(input): follow upstream's speaker bindings in the disconnect harness

The merge commit's message already describes these: the fake input_t
gains a speaker_bindings stand-in (production passthrough now takes a
speaker owner on arrival), forward_steam_attributes is stubbed against
the fake gamepad_t, and the braced vector initializer inside assert()
is parenthesized so the macro sees one argument. All 40 cases pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142jvYrnZDdtd5D44FB8Wox

* Pin third-party/libvirtualgamepad back to the released driver build

The Windows validation job fetches the signed VHF driver package for the
commit the submodule pins (VHF_SOURCE_REVISION = 57de074), so the pin has
to name a released build of the fork's feat/steam-controller-profile line,
not the upstream-PR branch. 57de074 carries the Steam Controller profile
on top of upstream main at 7b000a2, the same base master pins today. The
pointer moves to Nonary/libvirtualgamepad main once #6 merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142jvYrnZDdtd5D44FB8Wox

* tests(input): follow upstream's speaker PCM and recovery in the CI harnesses

The master merge changed the code these harnesses extract:

- gamepad_feedback_msg_t now carries a controller_speaker_pcm epoch and a
  speaker block, so the feedback-queue, settings-forward and retirement
  harnesses include controller_speaker_pcm.h ahead of the extracted types.
- vhf_gamepad_t::free() and ::attributes() consult the device-removal
  recovery state and report every driver status through observe_status();
  the fake impl_t in both harnesses gains those members, with recovery
  never pending, and the attributes fake slot keeps steam_attributes.
- The attributes harness's fake input_t gains the speaker_bindings stand-in
  that the arrival passthrough now takes a binding from.

All six scripts wired into the Validate Windows workflow pass:
haptic_queue 5, steam_settings_forward 13, vhf_feedback_retirement 13,
vhf_fallback 26, sc26_buttons 2, steam_attributes_forward 29 checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142jvYrnZDdtd5D44FB8Wox

* fix(stream): dequeue gamepad feedback atomically in the control loop

The control thread drained the session feedback mailbox with peek()
followed by a blocking pop(). vhf_gamepad_t::free() retires a
controller's queued output from the input thread with discard_if(),
which erases without waking consumers, so a retirement landing between
the two calls left the control loop waiting forever on an item that was
gone (every session on that server stalled). A queue stop() in the same
window made pop() return an empty optional that the loop dereferenced.

Add safe::queue_t::try_pop(), a locked nonblocking dequeue, and drain
with it: the loop ends when the mailbox is empty or stopped.

Test: tests/input/test_vhf_feedback_drain.py compiles the production
drain, queue and slot retirement with a barrier between the readiness
check and the dequeue; it covers retirement of the only queued
controller and of the last controller in that window, a stop in that
window, and FIFO delivery to the surviving controller. Against HEAD
(--baseline) the retirement cases never return and the stop case aborts.
Wired into the Validate Windows host-regressions job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(vhf): admit polled rumble through the protected feedback policy

dispatch_rumble_poll() submitted the decoded ordinary rumble with the
mailbox's plain raise(), which drops the newest message once the
256-slot stream mailbox is full, and reported success regardless. The
feedback thread then recorded the zero-motor stop as delivered, so a
client whose mailbox was saturated (240 protected state keys plus 16
reserved settings chunks) could be left rumbling with no retry.

Give the dispatcher an admission callable and return its verdict.
Production passes raise_gamepad_feedback(), so the stop competes as a
protected state key, replacing the stale motor state for that
controller instead of being dropped. raise_feedback() now records
last_feedback only when every message of the poll was admitted, so a
rejected message is retried by the next poll instead of being lost.

Tests: tests/input/test_vhf_rumble_admission.py fills the production
mailbox to capacity, shows the plain raise is refused there, and checks
the active-to-zero stop is admitted in place of the stale state (fails
against HEAD with --baseline). The policy gtest gains
RumblePollStopSurvivesAFullFeedbackMailbox and now expects a stopped
queue to report rejection. Wired into the Validate Windows
host-regressions job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(windows): migrate MinGW audio DLL CRT link fix

(cherry picked from commit 187cd896b9c01438d20b758d490b6e37a8c5b573)

* fix(input): route legacy feedback producers through coalesced admission

The ViGEm (input.cpp) and inputtino (inputtino_gamepad.cpp) backends
still handed rumble, LED, trigger-effect and motion-request messages to
the client mailbox with plain raise(), which drops the newest message
when the non-flushing 256-slot mailbox is full, and updated their
duplicate caches unconditionally. A final zero could be rejected yet
cached as delivered, so repeated zeros were suppressed; and the
uncoalesced duplicates could exhaust the protected slots and reject
another controller's synthesized rumble expiry stop.

Add raise_gamepad_feedback_deduped(): the message takes the coalesced
state-key admission and the cache is updated only once the mailbox
accepted it, so a refused message is retried by the next callback.
Every shared state producer now uses it (or raise_gamepad_feedback for
uncached requests). With all producers coalesced each (controller, key)
is pending at most once and 240 keys + 16 reserved chunks fit the
mailbox, so a state message is only refused by a stopped mailbox.

Test: tests/input/test_legacy_feedback_admission.py compiles the
production vigem_t::rumble/set_rgb_led and the inputtino rumble/LED
lambdas against the real mailbox: sixteen churning legacy controllers
coalesce under settings and haptic pressure, a ViGEm and an inputtino
zero stop are admitted at capacity with the cache following acceptance,
a Steam slot's synthesized expiry stop is admitted under the same
pressure, duplicates stay suppressed, and a refused stop leaves the
cache stale for retry. Against HEAD (--baseline) 5 of 8 checks fail.
Wired into the Validate Windows host-regressions job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxkWBMb8aUVkm5BbFm9ubx

* fix(input): coalesce DualSense USB/IP trigger programs in the mailbox

The USB/IP backend appended every changed adaptive-trigger program with
try_raise, so with a stalled consumer hundreds of same-key programs
could occupy the mailbox's protected slots and reject another
controller's synthesized rumble expiry stop (whose start was already
consumed, so there was nothing to coalesce with and nothing to retry).

Route the program through raise_gamepad_feedback(): the state key is the
enable set (left, right, both), so a later right-only program never
replaces an unsent left-only one, a newer program for the same enables
replaces its pending predecessor, and a stalled consumer holds at most
three programs per controller. Programs refused by a stopped mailbox
keep their enables merged in pending_triggers, as before.

Test: tests/input/test_usbip_trigger_admission.py compiles the
production flush_output/publish_latest members against an undrained
mailbox: independent left/right programs both stay queued, same-side
replacement, 300 changed programs coalescing to three, a Steam slot's
consumed-start expiry stop admitted afterwards, rumble latest-wins, and
a refused program keeping its enable pending. Against HEAD (--baseline)
4 of 6 checks fail. Wired into the Validate Windows host-regressions job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxkWBMb8aUVkm5BbFm9ubx

* ci(windows): pin libvirtualgamepad v0.1.0-beta.155 (non-power-managed queue)

The pinned producer release beta.154 (source 57de074) still set the
upper filter's sequential IOCTL queue to PowerManaged = WdfTrue above
VHF, the power-policy owner. The correction (e68c4e7, "keep the
upper-filter queue non-power-managed") landed with the merged
Nonary/libvirtualgamepad#6 but was not in a published package.

v0.1.0-beta.155 is that merged head (09e635cc) published through the
fork's release workflow. Tag, archive SHA-256, DriverVer, source
revision and the third-party/libvirtualgamepad gitlink move together, as
the consumer verifier requires; the protocol version is unchanged (2).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxkWBMb8aUVkm5BbFm9ubx

* ci(windows): move the remaining VHF producer pin carriers to beta.155

240593c2 updated the two download/verify blocks and the gitlink but
left the pin's other carriers at beta.154: the CMake contract cache
(cmake/packaging/windows_virtual_gamepad_contract.cmake), the
-DSUNSHINE_VHF_GAMEPAD_* configure arguments, the packaging refresh and
MSI verification steps in ci-windows.yml, and the installed
install.ps1's expected producer identity. The package refresh therefore
compared the beta.155 manifest against the beta.154 expectation and
failed ("Manifest does not match the pinned producer source, DriverVer,
or protocol").

Every occurrence of the tag, asset name, archive SHA-256, source
revision and DriverVer now reads beta.155 / 09e635cc /
56be2b6c... / 10/24/2026,0.1.0.86. Verified locally: the release
contract (cmake -P) and manifest channel contract tests pass, and the
CI download step plus refresh_driver_package.ps1 succeed against the
published beta.155 archive with these values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxkWBMb8aUVkm5BbFm9ubx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Chase Payne <chase_payne@outlook.com>

badf0efe5a33

ci(nightly): Select the audited driver for the exact release source

Keep scheduled control workflows identical to Canary without forcing
master's legacy source to consume Steam Controller driver pins.
Validate each source gitlink and packaging contract against the audited
producer tuple before download, packaging and signature verification.
Keep Vibepollo nightly independent of Vibeshine migration readiness.

Co-authored-by: Codex <noreply@openai.com>

ed7f5b2d04cf

test(vhf): Model Steam dependencies in the replay fixture

The accepted Steam adapter adds types and feedback retirement callbacks
that the inert replay boundary did not declare, failing Linux qualification.
Add only the required fake declarations and preserve all replay, rollback,
throttle, battery and slot-reuse assertions. Production code is unchanged.

Co-authored-by: Codex <noreply@openai.com>

Don't miss a new Vibepollo release

NewReleases is sending notifications on new releases.