Forward rumble back to the guest controller (both backends)
The XInput hook used to swallow XInputSetState; now it records the requested left/right motor speeds into the status back-channel (protocol v8->v9: per-slot rumble_left/right in HookStatus). Each frame the host reads them and, only on change, drives the guest's actuator via the active backend: - XInput: XInputSetState on the guest's slot (the open question is whether Steam's RPT virtual pad accepts vibration and routes it to the guest -- needs live RPT); - Steam Input: SteamInput TriggerVibration on the slot's controller handle, with the XInput fallback for slots Steam isn't driving. InputSource gains a set_rumble(slot,left,right) hook (default no-op) implemented by both backends; SteamInputSource now tracks per-slot controller handles + which slots it drives. Verified: hook_selftest (x64 + x86) now asserts the hook records the rumble from XInputSetState into the status; full build x64 + x86 clean; ctest x64 9/9, x86 3/3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,11 @@ public:
|
||||
|
||||
// Latest snapshot of every slot (indexed 0..kMaxPads-1).
|
||||
[[nodiscard]] virtual const std::array<PadInfo, kMaxPads>& pads() const = 0;
|
||||
|
||||
// Drive the guest controller's rumble for `slot` (motor speeds 0..65535). The
|
||||
// game requests this via XInputSetState; the host forwards it here. Default
|
||||
// no-op; backends that can reach the guest's actuator override it.
|
||||
virtual void set_rumble(int /*slot*/, std::uint16_t /*left*/, std::uint16_t /*right*/) {}
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
|
||||
@@ -192,14 +192,35 @@ void SteamInputSource::poll()
|
||||
InputHandle_t handles[STEAM_INPUT_MAX_COUNT] = {};
|
||||
steam_count_ = SteamInput()->GetConnectedControllers(handles);
|
||||
|
||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
||||
{
|
||||
controllers_[i] = 0;
|
||||
steam_slot_[i] = false;
|
||||
}
|
||||
for (int i = 0; i < steam_count_ && i < static_cast<int>(kMaxPads); ++i)
|
||||
{
|
||||
controllers_[i] = handles[i];
|
||||
PadInfo steam_pad;
|
||||
if (read_steam_pad(handles[i], steam_pad))
|
||||
{
|
||||
pads_[i] = steam_pad; // Steam controller active on this slot -> use it
|
||||
steam_slot_[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SteamInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right)
|
||||
{
|
||||
if (slot < 0 || slot >= static_cast<int>(kMaxPads))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (steam_ready_ && steam_slot_[slot] && controllers_[slot] != 0)
|
||||
{
|
||||
SteamInput()->TriggerVibration(controllers_[slot], left, right);
|
||||
return;
|
||||
}
|
||||
xinput_.set_rumble(slot, left, right); // slot not Steam-driven -> XInput fallback
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
|
||||
@@ -41,6 +41,10 @@ public:
|
||||
return pads_;
|
||||
}
|
||||
|
||||
// Forward rumble to the guest: SteamInput TriggerVibration on the slot's
|
||||
// controller when it's Steam-active, else the XInput fallback.
|
||||
void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override;
|
||||
|
||||
[[nodiscard]] bool steam_active() const
|
||||
{
|
||||
return steam_ready_;
|
||||
@@ -57,6 +61,11 @@ private:
|
||||
XInputSource xinput_; // fallback for slots Steam Input doesn't fill
|
||||
std::array<PadInfo, kMaxPads> pads_;
|
||||
|
||||
// Per-slot Steam controller handle + whether Steam Input is driving that slot
|
||||
// (updated each poll), so set_rumble can target the right actuator.
|
||||
std::uint64_t controllers_[kMaxPads] = {};
|
||||
bool steam_slot_[kMaxPads] = {};
|
||||
|
||||
bool steam_ready_ = false;
|
||||
int steam_count_ = 0;
|
||||
const char* name_ = "XInput";
|
||||
|
||||
@@ -39,4 +39,14 @@ void XInputSource::poll()
|
||||
}
|
||||
}
|
||||
|
||||
void XInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right)
|
||||
{
|
||||
if (slot < 0 || slot >= static_cast<int>(kMaxPads))
|
||||
{
|
||||
return;
|
||||
}
|
||||
XINPUT_VIBRATION v{left, right};
|
||||
XInputSetState(static_cast<DWORD>(slot), &v);
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
|
||||
@@ -22,6 +22,9 @@ public:
|
||||
return pads_;
|
||||
}
|
||||
|
||||
// Forward rumble to the XInput device at `slot` (the guest's RPT virtual pad).
|
||||
void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override;
|
||||
|
||||
private:
|
||||
std::array<PadInfo, kMaxPads> pads_;
|
||||
};
|
||||
|
||||
@@ -85,6 +85,11 @@ HookStatusView IpcServer::hook_status() const
|
||||
{
|
||||
view.hook_entries[i] = s.hook_entries[i];
|
||||
}
|
||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
||||
{
|
||||
view.rumble_left[i] = s.rumble_left[i];
|
||||
view.rumble_right[i] = s.rumble_right[i];
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ struct HookStatusView
|
||||
// Installed-hooks registry (for the Injection panel's hook list).
|
||||
std::uint32_t hook_entry_count = 0;
|
||||
HookEntry hook_entries[kMaxHookEntries] = {};
|
||||
|
||||
// Per-slot rumble the game requested (host forwards it to the guest's pad).
|
||||
std::uint16_t rumble_left[kMaxPads] = {};
|
||||
std::uint16_t rumble_right[kMaxPads] = {};
|
||||
};
|
||||
|
||||
// Plain snapshot of the Present-hook video channel for the Video mirror panel.
|
||||
|
||||
@@ -117,6 +117,10 @@ int run()
|
||||
coop::UiState ui;
|
||||
coop::FrameStats stats;
|
||||
|
||||
// Last rumble forwarded per slot, so we only re-send on change.
|
||||
std::uint16_t last_rumble_l[coop::kMaxPads] = {};
|
||||
std::uint16_t last_rumble_r[coop::kMaxPads] = {};
|
||||
|
||||
while (window.pump_messages())
|
||||
{
|
||||
#ifdef COOP_WITH_STEAM
|
||||
@@ -148,6 +152,22 @@ int run()
|
||||
injection.set_test_input(controllers.test_input()); // toggle lives in the Controllers panel
|
||||
injection.publish(input->pads());
|
||||
injection.tick(); // refresh target liveness before the mirror panels read game_hwnd()
|
||||
|
||||
// Forward the rumble the game requested back to the guest's controller (only
|
||||
// when it changes, to avoid spamming XInputSetState / TriggerVibration).
|
||||
{
|
||||
const coop::HookStatusView hs = injection.hook_status();
|
||||
for (int i = 0; i < static_cast<int>(coop::kMaxPads); ++i)
|
||||
{
|
||||
if (hs.rumble_left[i] != last_rumble_l[i] || hs.rumble_right[i] != last_rumble_r[i])
|
||||
{
|
||||
input->set_rumble(i, hs.rumble_left[i], hs.rumble_right[i]);
|
||||
last_rumble_l[i] = hs.rumble_left[i];
|
||||
last_rumble_r[i] = hs.rumble_right[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HWND game = injection.game_hwnd();
|
||||
capture.set_target(game);
|
||||
audio.set_target(game);
|
||||
|
||||
Reference in New Issue
Block a user