Move input polling to its own thread; fixed-width fast-changing UI numbers
Input thread: controller polling, pad publishing, and rumble forwarding were driven by the render loop, so a low/synced frame rate throttled how often guest input reached the game. New InputWorker owns the InputSource and runs poll + IPC publish + rumble on a dedicated ~1 kHz thread, independent of rendering. The UI thread reads a copy-safe InputSnapshot for the Controllers panel and relays the Steam-Input request/active/failed state to/from the worker (Steam init/shutdown now happen on the worker thread). IpcServer gained a mutex so the worker's publish() / hook_status() can't race the UI thread starting/stopping the shared-memory channel (use-after-unmap); InjectionPanel::test_input_ is now atomic. ControllersPanel::draw takes an InputSnapshot instead of the live InputSource. Fixed-width numbers: fast-changing readouts (menu-bar FPS/ms, Video pipeline rates + latency + graph legend, controller poll rates + round-trip sticks, audio buffered ms + frames/s) printed with %.0f etc., so they shifted/blurred as values crossed digit thresholds (99 -> 100) each frame. Padded them to fixed field widths so they stay put. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
// this window via Windows Graphics Capture (Video mirror panel).
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
@@ -23,14 +22,12 @@
|
||||
#include "imgui_layer.hpp"
|
||||
#include "inject/mkb_forward.hpp"
|
||||
#include "injection_panel.hpp"
|
||||
#include "input/xinput_source.hpp"
|
||||
#include "input/input_worker.hpp"
|
||||
#include "log_panel.hpp"
|
||||
#include "ui/app_chrome.hpp"
|
||||
|
||||
#ifdef COOP_WITH_STEAM
|
||||
#include <string>
|
||||
|
||||
#include "input/steam_input_source.hpp"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
@@ -125,17 +122,6 @@ int run()
|
||||
return 1;
|
||||
}
|
||||
|
||||
// XInput is the default guest-input path: Remote Play Together delivers guest
|
||||
// pads as XInput, and it Just Works. Steam Input is opt-in (Controllers panel) --
|
||||
// merely initializing it activates Steam's in-process XInput interception, which
|
||||
// hides controllers from XInput unless they're bound to our action set for this
|
||||
// app, so making it the default can silently break input. We switch the active
|
||||
// backend at runtime to match the toggle.
|
||||
coop::XInputSource xinput;
|
||||
coop::InputSource* input = &xinput;
|
||||
#ifdef COOP_WITH_STEAM
|
||||
std::unique_ptr<coop::SteamInputSource> steam;
|
||||
#endif
|
||||
coop::ControllersPanel controllers;
|
||||
coop::InjectionPanel injection;
|
||||
coop::AudioPanel audio;
|
||||
@@ -148,6 +134,19 @@ int run()
|
||||
}
|
||||
capture.set_injection(&injection); // for the Present-hook (Hooked) video source
|
||||
|
||||
// Controller polling + forwarding runs on its own thread so the render frame rate
|
||||
// (which can drop, especially with frame-sync) never throttles input. XInput is the
|
||||
// default guest path (RPT delivers guest pads as XInput and it Just Works); Steam
|
||||
// Input is opt-in (Controllers panel) -- merely initializing it hijacks XInput and
|
||||
// hides controllers unless they're bound to our action set, so it can silently break
|
||||
// input. The worker reconciles the active backend with the toggle.
|
||||
coop::InputWorker input_worker;
|
||||
#ifdef COOP_WITH_STEAM
|
||||
input_worker.start(&injection, steam_manifest_path());
|
||||
#else
|
||||
input_worker.start(&injection, std::string());
|
||||
#endif
|
||||
|
||||
// The overlay can be hidden (F1) so the window is a clean mirror for Remote
|
||||
// Play Together; the pipelines keep running underneath either way.
|
||||
bool show_overlay = true;
|
||||
@@ -155,10 +154,6 @@ 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] = {};
|
||||
|
||||
// Frame-sync: the hook generation we last presented (so we wait for the next one).
|
||||
std::uint32_t last_synced_gen = 0;
|
||||
|
||||
@@ -174,51 +169,24 @@ int run()
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Input polling, pad publishing, and rumble all run on the input worker thread;
|
||||
// here we only relay UI requests to it and read back its snapshot for display.
|
||||
const coop::InputSnapshot input_snapshot = input_worker.snapshot();
|
||||
#ifdef COOP_WITH_STEAM
|
||||
// Switch the active input backend to match the Controllers-panel toggle.
|
||||
const bool want_steam = controllers.steam_input_requested();
|
||||
if (want_steam && steam == nullptr)
|
||||
input_worker.set_want_steam(controllers.steam_input_requested());
|
||||
if (input_worker.steam_failed())
|
||||
{
|
||||
steam = std::make_unique<coop::SteamInputSource>();
|
||||
if (steam->init(steam_manifest_path()))
|
||||
{
|
||||
input = steam.get();
|
||||
controllers.set_steam_active(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
steam.reset();
|
||||
controllers.on_steam_init_failed();
|
||||
}
|
||||
controllers.on_steam_init_failed(); // resets the toggle; worker falls back to XInput
|
||||
}
|
||||
else if (!want_steam && steam != nullptr)
|
||||
else
|
||||
{
|
||||
steam->shutdown();
|
||||
steam.reset();
|
||||
input = &xinput;
|
||||
controllers.set_steam_active(false);
|
||||
controllers.set_steam_active(input_snapshot.steam_active);
|
||||
}
|
||||
#endif
|
||||
input->poll();
|
||||
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);
|
||||
@@ -245,7 +213,7 @@ int run()
|
||||
coop::draw_main_menu_bar(ui, stats);
|
||||
if (ui.show_controllers)
|
||||
{
|
||||
controllers.draw(*input, injection.hook_status(), ui.debug_details);
|
||||
controllers.draw(input_snapshot, injection.hook_status(), ui.debug_details);
|
||||
}
|
||||
if (ui.show_injection)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user