./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
1394 / 1481
94.1%
Branches:
826 / 1027
80.4%
Functions:
169 / 170
99.4%

src/internal/input_intercept.cpp
Line Branch Exec Source
1 /**
2 * @file input_intercept.cpp
3 * @brief XInput route maintenance and thread-scoped wheel capture.
4 */
5
6 #include "input_intercept.hpp"
7 #include "internal/drain_backoff.hpp"
8 #include "internal/hook_backend_visit.hpp"
9 #include "internal/hook_patch_witness.hpp"
10 #include "internal/xinput_raw_scope.hpp"
11 #include "internal/xinput_route_probe.hpp"
12 #include "platform.hpp"
13 #include "DetourModKit/diagnostics.hpp"
14 #include "DetourModKit/logger.hpp"
15
16 #include <safetyhook.hpp>
17
18 #include <algorithm>
19 #include <array>
20 #include <atomic>
21 #include <chrono>
22 #include <charconv>
23 #include <cstdint>
24 #include <cstring>
25 #include <limits>
26 #include <memory>
27 #include <new>
28 #include <optional>
29 #include <span>
30 #include <string_view>
31 #include <thread>
32 #include <type_traits>
33 #include <utility>
34
35 namespace DetourModKit::detail
36 {
37 namespace
38 {
39 /// The game or runtime determines which DLL contains the XInput export.
40 constexpr const wchar_t *XINPUT_DLL_NAMES[] = {
41 L"xinput1_4.dll",
42 L"xinput1_3.dll",
43 L"xinput9_1_0.dll",
44 L"xinput1_2.dll",
45 L"xinput1_1.dll",
46 };
47
48 /// Identifies the undocumented ordinal that exports XInputGetStateEx and reports the Guide button.
49 constexpr WORD XINPUT_GET_STATE_EX_ORDINAL = 100;
50
51 /**
52 * @brief Names the position of each pair member across generations and DMK copies.
53 * @details A create claims a Vacant position with this identity. The names are part of the cross-copy
54 * protocol and must not change.
55 */
56 constexpr safetyhook::PositionClaim XINPUT_GET_STATE_POSITION =
57 safetyhook::PositionClaim::from_name("DetourModKit.XInputGetState");
58 constexpr safetyhook::PositionClaim XINPUT_GET_STATE_EX_POSITION =
59 safetyhook::PositionClaim::from_name("DetourModKit.XInputGetStateEx");
60
61 /**
62 * @brief Defines how long a published suppression mask stays valid without a refresh.
63 * @details Twice MAX_POLL_INTERVAL leaves a full poll interval for cycle work before expiry. A stalled poll
64 * thread still loses suppression after a bounded delay.
65 */
66 constexpr uint64_t SUPPRESS_TTL_MS = 2000;
67
68 // One owner per layer: hooks and keepalives are shared per linked DMK instance, and the token prevents
69 // superseded poller teardown of a newer installation. Static SRWLOCK storage has no destructor, so late
70 // process teardown cannot encounter a destroyed mutex.
71 SRWLOCK s_intercept_mutex = SRWLOCK_INIT;
72 // Makes the owner check and its authorized write one indivisible step. Separate locks let a revoked poller
73 // overwrite the new owner's state. Acquired after s_intercept_mutex wherever both are held. The detours take
74 // neither lock.
75 SRWLOCK s_data_plane_mutex = SRWLOCK_INIT;
76 std::atomic<std::uint64_t> s_intercept_owner{0};
77 std::atomic<std::uint64_t> s_xinput_hook_epoch{1};
78 bool s_xinput_raw_owner{false};
79 bool s_xinput_observed_pair{false};
80 bool s_xinput_primary_observed{false};
81 bool s_xinput_ex_observed{false};
82 bool s_xinput_route_warned{false};
83 std::atomic<std::uint64_t> s_next_intercept_owner{STANDALONE_INTERCEPT_OWNER + 1};
84 constexpr unsigned WHEEL_COUNT_BITS = 11;
85 constexpr std::uint64_t WHEEL_COUNT_MASK = (std::uint64_t{1} << WHEEL_COUNT_BITS) - 1;
86 constexpr std::uint64_t WHEEL_EPOCH_MAX =
87 (std::uint64_t{1} << (std::numeric_limits<std::uint64_t>::digits - WHEEL_COUNT_BITS)) - 1;
88 constexpr std::uint64_t WHEEL_CAPTURE_ENABLED = 1;
89 static_assert(MAX_WHEEL_NOTCHES <= WHEEL_COUNT_MASK);
90
91 333 [[nodiscard]] constexpr std::uint64_t wheel_capture_state(std::uint64_t epoch, bool enabled) noexcept
92 {
93
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 333 times.
333 return (epoch << 1) | (enabled ? WHEEL_CAPTURE_ENABLED : 0);
94 }
95
96 2903 [[nodiscard]] constexpr std::uint64_t wheel_capture_epoch(std::uint64_t state) noexcept
97 {
98 2903 return state >> 1;
99 }
100
101 2820 [[nodiscard]] constexpr std::uint64_t wheel_count_slot(std::uint64_t epoch, std::uint64_t count) noexcept
102 {
103 2820 return (epoch << WHEEL_COUNT_BITS) | count;
104 }
105
106 1600 [[nodiscard]] constexpr std::uint64_t wheel_slot_epoch(std::uint64_t slot) noexcept
107 {
108 1600 return slot >> WHEEL_COUNT_BITS;
109 }
110
111 // The per-axis signed sub-notch remainder includes the capture epoch and consume-ownership state. A fragment
112 // with a different (epoch, owned) tag resets the remainder to zero. A retired epoch fragment and an
113 // owned/unowned fragment pair cannot combine into one notch (WheelDeltaTest.*).
114 constexpr unsigned WHEEL_REMAINDER_BITS = 8;
115 constexpr int WHEEL_REMAINDER_BIAS = 128;
116 constexpr std::uint64_t WHEEL_REMAINDER_VALUE_MASK = (std::uint64_t{1} << WHEEL_REMAINDER_BITS) - 1;
117 constexpr std::uint64_t WHEEL_REMAINDER_OWNED_BIT = std::uint64_t{1} << WHEEL_REMAINDER_BITS;
118 constexpr unsigned WHEEL_REMAINDER_EPOCH_SHIFT = WHEEL_REMAINDER_BITS + 1;
119 static_assert(WHEEL_DELTA < WHEEL_REMAINDER_BIAS, "a sub-notch remainder must fit the biased value field");
120 static_assert(WHEEL_EPOCH_MAX <= (std::numeric_limits<std::uint64_t>::max() >> WHEEL_REMAINDER_EPOCH_SHIFT));
121
122 [[nodiscard]] constexpr std::uint64_t
123 1879 wheel_remainder_slot(std::uint64_t epoch, bool owned, int remainder) noexcept
124 {
125
2/2
✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 4 taken 1863 times.
1879 return (epoch << WHEEL_REMAINDER_EPOCH_SHIFT) | (owned ? WHEEL_REMAINDER_OWNED_BIT : 0) |
126 1879 static_cast<std::uint64_t>(remainder + WHEEL_REMAINDER_BIAS);
127 }
128
129 std::atomic<std::uint64_t> s_wheel_capture_state{wheel_capture_state(1, false)};
130 #if defined(DMK_ENABLE_TEST_SEAMS)
131 std::atomic<WheelCaptureEntrySeam> s_wheel_capture_entry_seam{nullptr};
132 std::atomic<WheelFinalizeEntrySeam> s_wheel_finalize_entry_seam{nullptr};
133 std::atomic<XInputRetentionAttributionSeam> s_xinput_retention_attribution_seam{nullptr};
134 std::atomic<std::uint64_t> s_wheel_drain_timeout_override_ms{0};
135 std::atomic<bool> s_force_message_unhook_failure{false};
136 std::atomic<std::int32_t> s_wheel_process_focus_override{-1};
137 #endif
138
139 /** @brief Owns exclusive access to the process-lifetime interception lock. */
140 class InterceptLockGuard
141 {
142 public:
143 16304 explicit InterceptLockGuard(SRWLOCK &mutex) noexcept : m_mutex(mutex) { AcquireSRWLockExclusive(&m_mutex); }
144
145 16304 ~InterceptLockGuard() noexcept
146 {
147
2/2
✓ Branch 2 → 3 taken 14433 times.
✓ Branch 2 → 4 taken 1871 times.
16304 if (m_locked)
148 {
149 14433 ReleaseSRWLockExclusive(&m_mutex);
150 }
151 16304 }
152
153 InterceptLockGuard(const InterceptLockGuard &) = delete;
154 InterceptLockGuard &operator=(const InterceptLockGuard &) = delete;
155 InterceptLockGuard(InterceptLockGuard &&) = delete;
156 InterceptLockGuard &operator=(InterceptLockGuard &&) = delete;
157
158 /// Releases the lock before this guard leaves scope.
159 3144 void unlock() noexcept
160 {
161
1/2
✓ Branch 2 → 3 taken 3144 times.
✗ Branch 2 → 5 not taken.
3144 if (m_locked)
162 {
163 3144 ReleaseSRWLockExclusive(&m_mutex);
164 3144 m_locked = false;
165 }
166 3144 }
167
168 /// Reacquires the lock after an upstream call returns.
169 1273 void relock() noexcept
170 {
171
1/2
✓ Branch 2 → 3 taken 1273 times.
✗ Branch 2 → 5 not taken.
1273 if (!m_locked)
172 {
173 1273 AcquireSRWLockExclusive(&m_mutex);
174 1273 m_locked = true;
175 }
176 1273 }
177
178 private:
179 SRWLOCK &m_mutex;
180 bool m_locked{true};
181 };
182
183 /**
184 * @brief Requires s_intercept_mutex. Reports whether @p owner can claim the layer.
185 * @details This claim predicate admits the unowned layer, which is invalid as write authorization. The
186 * data_plane_authorized() predicate requires an exact owner match.
187 */
188 2051 [[nodiscard]] bool owner_available(std::uint64_t owner) noexcept
189 {
190
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 2050 times.
2051 if (owner == 0)
191 {
192 1 return false;
193 }
194 2050 const std::uint64_t current = s_intercept_owner.load(std::memory_order_relaxed);
195
4/4
✓ Branch 11 → 12 taken 1718 times.
✓ Branch 11 → 13 taken 332 times.
✓ Branch 12 → 13 taken 1685 times.
✓ Branch 12 → 14 taken 33 times.
2050 return current == 0 || current == owner;
196 }
197
198 /// Requires s_data_plane_mutex. Reports whether @p owner can write the state that detours read.
199 7969 [[nodiscard]] bool data_plane_authorized(std::uint64_t owner) noexcept
200 {
201
3/4
✓ Branch 2 → 3 taken 7969 times.
✗ Branch 2 → 12 not taken.
✓ Branch 10 → 11 taken 3116 times.
✓ Branch 10 → 12 taken 4853 times.
15938 return owner != 0 && s_intercept_owner.load(std::memory_order_relaxed) == owner;
202 }
203
204 #if defined(DMK_ENABLE_TEST_SEAMS)
205 std::atomic<DataPlaneEntrySeam> s_data_plane_entry_seam{nullptr};
206 #endif
207
208 /// Runs the entry probe, if any, before a data-plane operation takes its lock.
209 7969 void run_data_plane_entry_seam() noexcept
210 {
211 #if defined(DMK_ENABLE_TEST_SEAMS)
212
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 7963 times.
7969 if (const DataPlaneEntrySeam seam = s_data_plane_entry_seam.load(std::memory_order_acquire);
213 seam != nullptr)
214 {
215 6 seam();
216 }
217 #endif
218 7969 }
219
220 /** @brief Owns exclusive access to the process-lifetime data-plane lock. */
221 class DataPlaneLockGuard
222 {
223 public:
224 11308 DataPlaneLockGuard() noexcept { AcquireSRWLockExclusive(&s_data_plane_mutex); }
225
226 11308 ~DataPlaneLockGuard() noexcept { ReleaseSRWLockExclusive(&s_data_plane_mutex); }
227
228 DataPlaneLockGuard(const DataPlaneLockGuard &) = delete;
229 DataPlaneLockGuard &operator=(const DataPlaneLockGuard &) = delete;
230 DataPlaneLockGuard(DataPlaneLockGuard &&) = delete;
231 DataPlaneLockGuard &operator=(DataPlaneLockGuard &&) = delete;
232 };
233
234 /// Requires s_data_plane_mutex. Clears every mask and rule that the prior owner armed.
235 void clear_data_plane_locked(std::uint64_t wheel_epoch) noexcept;
236
237 /**
238 * @brief Requires s_intercept_mutex. Reports whether wheel capture can open.
239 * @details True with no mounted wheel hook, where an open capture is inert, or on a Ready route. A hook that a
240 * failed migration or removal left on a Retryable or CleanupBlocked route keeps capture closed.
241 */
242 [[nodiscard]] bool wheel_capture_armable_locked() noexcept;
243
244 /**
245 * @brief Closes wheel capture and advances its epoch so an already-entered frame cannot write into a
246 * successor.
247 * @return The newly published disabled epoch.
248 */
249 333 [[nodiscard]] std::uint64_t close_wheel_capture_and_advance_epoch() noexcept
250 {
251 333 std::uint64_t state = s_wheel_capture_state.load(std::memory_order_seq_cst);
252 for (;;)
253 {
254 333 const std::uint64_t epoch = wheel_capture_epoch(state);
255
1/2
✓ Branch 11 → 12 taken 333 times.
✗ Branch 11 → 13 not taken.
333 const std::uint64_t next_epoch = epoch == WHEEL_EPOCH_MAX ? 1 : epoch + 1;
256 333 const std::uint64_t desired = wheel_capture_state(next_epoch, false);
257
1/2
✓ Branch 23 → 24 taken 333 times.
✗ Branch 23 → 25 not taken.
333 if (s_wheel_capture_state.compare_exchange_weak(state, desired, std::memory_order_seq_cst))
258 {
259 333 return next_epoch;
260 }
261 ✗ }
262 }
263
264 /**
265 * @brief Requires s_intercept_mutex. Publishes the owner after an installation is ready and re-opens wheel
266 * capture for it when wheel_capture_armable_locked() allows it.
267 * @details The arm action occurs here, not at each install site, so ownership itself controls the association.
268 * The Ready store in install_message_hook publishes again, so a route that later becomes Ready arms.
269 */
270 3006 void publish_owner(std::uint64_t owner) noexcept
271 {
272 3006 const bool arm = wheel_capture_armable_locked();
273 3006 const DataPlaneLockGuard data_lock;
274
2/2
✓ Branch 11 → 12 taken 324 times.
✓ Branch 11 → 15 taken 2682 times.
3006 if (s_intercept_owner.load(std::memory_order_relaxed) != owner)
275 {
276 s_xinput_hook_epoch.fetch_add(1, std::memory_order_seq_cst);
277 324 s_xinput_observed_pair = false;
278 324 s_xinput_primary_observed = false;
279 324 s_xinput_ex_observed = false;
280 324 s_xinput_route_warned = false;
281 }
282 s_intercept_owner.store(owner, std::memory_order_release);
283
2/2
✓ Branch 23 → 24 taken 3004 times.
✓ Branch 23 → 27 taken 2 times.
3006 if (arm)
284 {
285 s_wheel_capture_state.fetch_or(WHEEL_CAPTURE_ENABLED, std::memory_order_seq_cst);
286 }
287 3006 }
288
289 /**
290 * @brief Requires s_intercept_mutex. Revokes the layer and clears the data that the prior owner armed.
291 * @details One step ensures an unowned layer retains no live mask that lacks an owner with revoke authority. A
292 * wheel-capture epoch advance invalidates an active message-hook frame without a wait.
293 */
294 323 void revoke_owner_and_clear_data() noexcept
295 {
296 s_xinput_hook_epoch.fetch_add(1, std::memory_order_seq_cst);
297 323 close_xinput_route_probes();
298 323 s_xinput_observed_pair = false;
299 323 s_xinput_primary_observed = false;
300 323 s_xinput_ex_observed = false;
301 323 s_xinput_route_warned = false;
302 323 const std::uint64_t wheel_epoch = close_wheel_capture_and_advance_epoch();
303 323 const DataPlaneLockGuard data_lock;
304 323 clear_data_plane_locked(wheel_epoch);
305 s_intercept_owner.store(0, std::memory_order_release);
306 323 }
307
308 #if defined(DMK_ENABLE_TEST_SEAMS)
309 // This oracle precedes the hook cell, so its destructor runs after later automatic objects.
310 // The exit proof arms it with a patched target window.
311 constexpr std::size_t XINPUT_PROCESS_EXIT_WITNESS_BYTES = 16;
312 std::atomic<const std::uint8_t *> s_xinput_process_exit_target{nullptr};
313 std::uint8_t s_xinput_process_exit_patch[XINPUT_PROCESS_EXIT_WITNESS_BYTES]{};
314 class XInputProcessExitOracle
315 {
316 public:
317 ✗ ~XInputProcessExitOracle() noexcept
318 {
319 ✗ const std::uint8_t *const target = s_xinput_process_exit_target.load(std::memory_order_acquire);
320
0/2
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
✗ if (target == nullptr)
321 {
322 ✗ return;
323 }
324
0/2
✗ Branch 9 → 6 not taken.
✗ Branch 9 → 10 not taken.
✗ for (std::size_t i = 0; i < XINPUT_PROCESS_EXIT_WITNESS_BYTES; ++i)
325 {
326
0/2
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 8 not taken.
✗ if (target[i] != s_xinput_process_exit_patch[i])
327 {
328 ✗ ::RaiseFailFastException(nullptr, nullptr, 0);
329 }
330 }
331 }
332 };
333 XInputProcessExitOracle s_xinput_process_exit_oracle;
334 #endif
335 std::atomic<XInputGetStateFn> s_xinput_original{nullptr};
336 std::atomic<XInputGetStateFn> s_xinput_ex_original{nullptr};
337 std::atomic<bool> s_xinput_installed{false};
338 std::atomic<bool> s_xinput_pair_degraded{false};
339 // This flag is true after a timeout or unproved restore latches the XInput hooks in process-lifetime storage. A
340 // later Input start re-arms only through the retained primary entry, never over uncertain storage.
341 std::atomic<bool> s_xinput_permanent_detour{false};
342 // One-shot diagnostic latches prevent sink spam because install_xinput retries every poll cycle. uninstall()
343 // clears them so a later hot-reload re-arm can warn again.
344 std::atomic<bool> s_xinput_enable_warned{false};
345 std::atomic<bool> s_xinput_ex_enable_warned{false};
346 std::atomic<bool> s_xinput_capacity_warned{false};
347 // This raw cell owns all XInput hooks and keepalives for the process lifetime.
348 // A normal teardown resets its hooks and releases its module references.
349 // A veto leaves the cell intact, so the CRT runs no InlineHook destructor under the loader lock.
350 struct PermanentXInputHooks
351 {
352 safetyhook::InlineHook primary;
353 safetyhook::InlineHook ex;
354 /// The gateway entry of each claimed member. The claimed witness compares the target with a jump to it.
355 const std::uint8_t *primary_gateway{nullptr};
356 const std::uint8_t *ex_gateway{nullptr};
357 HMODULE self_ref{nullptr};
358 HMODULE target_ref{nullptr};
359 HMODULE ex_target_ref{nullptr};
360 };
361 alignas(PermanentXInputHooks) unsigned char s_xinput_permanent_cell[sizeof(PermanentXInputHooks)];
362 static_assert(
363 std::is_trivially_destructible_v<decltype(s_xinput_permanent_cell)>,
364 "the raw XInput placement cell must have no automatic destructor"
365 );
366 bool s_xinput_permanent_cell_ready{false};
367 PermanentXInputHooks *s_xinput_permanent_hooks{nullptr};
368 #if defined(DMK_ENABLE_TEST_SEAMS)
369 std::atomic<std::size_t> s_xinput_backend_toggle_exception_catches{0};
370 #endif
371
372 /**
373 * @brief Returns the reserved cell as a live object.
374 * @return Pointer to the object that ensure_permanent_cell() constructed.
375 * @note Requires s_intercept_mutex and a prior successful ensure_permanent_cell().
376 */
377 312 [[nodiscard]] PermanentXInputHooks *permanent_cell() noexcept
378 {
379 312 return std::launder(reinterpret_cast<PermanentXInputHooks *>(s_xinput_permanent_cell));
380 }
381
382 /**
383 * @brief Constructs the reserved cell before an XInput detour is published.
384 * @note Requires s_intercept_mutex. MSVC debug-container proxy setup can allocate, so this runs before the
385 * allocation-free teardown boundary.
386 */
387 212 [[nodiscard]] bool ensure_permanent_cell() noexcept
388 {
389
2/2
✓ Branch 2 → 3 taken 68 times.
✓ Branch 2 → 4 taken 144 times.
212 if (s_xinput_permanent_cell_ready)
390 {
391 68 return true;
392 }
393 try
394 {
395
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 144 times.
144 ::new (static_cast<void *>(s_xinput_permanent_cell)) PermanentXInputHooks{};
396 }
397 catch (...)
398 {
399 return false;
400 }
401 144 s_xinput_permanent_cell_ready = true;
402 144 s_xinput_permanent_hooks = permanent_cell();
403 144 return true;
404 }
405
406 /**
407 * @brief Identifies the evidence that resets the recovery deadline for an incomplete XInput pair.
408 * @details The delay grows to a cap and stays there (the poll loop asks every cycle, and the target really
409 * can come back). Changed evidence drops the accumulated delay, so recovery occurs before a stale
410 * backoff expires.
411 * @note Requires s_intercept_mutex.
412 */
413 struct XInputRecoveryEvidence
414 {
415 /// Identifies the module whose prologue the pair patches. A different pin identifies a different target.
416 1374 const void *target_module{nullptr};
417 /// Identifies the owner that requests recovery. A poller restart supplies fresh evidence.
418 406 std::uint64_t owner{0};
419 /// Identifies the absent member. Either member can be absent, so the recovery cadence keys on both.
420 406 bool primary_covered{false};
421 406 bool ex_covered{false};
422 /**
423 * @brief Records whether each target's current bytes permit the re-arm write.
424 * @details A guarded byte comparison detects when a concurrent writer returns an export. The comparison
425 * does not perform the protection change that the delay bounds.
426 */
427 405 bool primary_target_writable{false};
428 404 bool ex_target_writable{false};
429
430
10/12
✓ Branch 2 → 3 taken 968 times.
✓ Branch 2 → 4 taken 406 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 406 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 406 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 405 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 404 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 403 times.
1374 [[nodiscard]] bool operator==(const XInputRecoveryEvidence &) const noexcept = default;
431 };
432
433 /// Defines the first delay after a failed recovery transaction.
434 inline constexpr std::uint64_t XINPUT_RECOVERY_MIN_DELAY_MS = 32;
435 /// Defines the delay ceiling. Recovery retries at this cadence while the pair remains broken.
436 inline constexpr std::uint64_t XINPUT_RECOVERY_MAX_DELAY_MS = 2000;
437
438 XInputRecoveryEvidence s_xinput_recovery_evidence{};
439 std::uint64_t s_xinput_recovery_delay_ms{0};
440 std::uint64_t s_xinput_recovery_not_before_ms{0};
441 #if defined(DMK_ENABLE_TEST_SEAMS)
442 std::atomic<std::size_t> s_xinput_recovery_attempts{0};
443 #endif
444
445 /// Requires s_intercept_mutex. Reports whether a recovery transaction can run for @p evidence now.
446 1374 [[nodiscard]] bool xinput_recovery_due(const XInputRecoveryEvidence &evidence) noexcept
447 {
448
2/2
✓ Branch 3 → 4 taken 971 times.
✓ Branch 3 → 5 taken 403 times.
1374 if (evidence != s_xinput_recovery_evidence)
449 {
450 971 s_xinput_recovery_evidence = evidence;
451 971 s_xinput_recovery_delay_ms = 0;
452 971 s_xinput_recovery_not_before_ms = 0;
453 971 return true;
454 }
455 403 return GetTickCount64() >= s_xinput_recovery_not_before_ms;
456 }
457
458 /// Requires s_intercept_mutex. Grows the delay toward its cap after a recovery transaction did not complete.
459 975 void xinput_recovery_deferred() noexcept
460 {
461 975 s_xinput_recovery_delay_ms =
462 975 (s_xinput_recovery_delay_ms == 0)
463
2/2
✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 6 taken 965 times.
978 ? XINPUT_RECOVERY_MIN_DELAY_MS
464
2/2
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 5 taken 3 times.
10 : (s_xinput_recovery_delay_ms >= XINPUT_RECOVERY_MAX_DELAY_MS / 2 ? XINPUT_RECOVERY_MAX_DELAY_MS
465 7 : s_xinput_recovery_delay_ms * 2);
466 975 s_xinput_recovery_not_before_ms = GetTickCount64() + s_xinput_recovery_delay_ms;
467 975 }
468
469 /// Requires s_intercept_mutex. Clears the gate so the next incomplete pair starts from an immediate attempt.
470 1783 void xinput_recovery_reset() noexcept
471 {
472 1783 s_xinput_recovery_evidence = {};
473 1783 s_xinput_recovery_delay_ms = 0;
474 1783 s_xinput_recovery_not_before_ms = 0;
475 1783 }
476
477 /// Counts one recovery transaction for the proofs and passes its outcome straight through.
478 982 [[nodiscard]] bool record_xinput_recovery_attempt(bool armed) noexcept
479 {
480 #if defined(DMK_ENABLE_TEST_SEAMS)
481 s_xinput_recovery_attempts.fetch_add(1, std::memory_order_relaxed);
482 #endif
483 982 return armed;
484 }
485
486 std::atomic<int> s_bound_user_index{0};
487 std::atomic<uint16_t> s_suppress_mask{0};
488 std::atomic<uint64_t> s_suppress_deadline_ms{0};
489
490 // This counter tracks game threads inside an XInput detour body. Before hook destruction, uninstall() retires
491 // the published trampoline pointers and drains this counter. This complements SafetyHook's mid-prologue
492 // relocation.
493 std::atomic<int> s_xinput_inflight{0};
494
495 #if defined(DMK_ENABLE_TEST_SEAMS)
496 std::atomic<XInputDetourBodySeam> s_xinput_detour_body_seam{nullptr};
497 std::atomic<XInputArmSeam> s_xinput_arm_seam{nullptr};
498 std::atomic<XInputCleanReleaseSeam> s_xinput_clean_release_seam{nullptr};
499 std::atomic<XInputCreateSeam> s_xinput_create_seam{nullptr};
500 // When set, install_xinput resolves XInputGetState from this module so a test can drive the install against
501 // a synthetic proxy DLL. Set/cleared only on the test thread while no install runs.
502 HMODULE s_xinput_module_override{nullptr};
503 #endif
504
505 /// Runs the arm-boundary probe between the backend toggle and the witness read that judges it.
506 307 void run_xinput_arm_seam() noexcept
507 {
508 #if defined(DMK_ENABLE_TEST_SEAMS)
509
2/2
✓ Branch 3 → 4 taken 16 times.
✓ Branch 3 → 5 taken 291 times.
307 if (const XInputArmSeam seam = s_xinput_arm_seam.load(std::memory_order_acquire); seam != nullptr)
510 {
511 16 seam();
512 }
513 #endif
514 307 }
515
516 /// Names the code that a retired XInput chain can still execute. Each reachable provider keeps its reference.
517 enum class XInputChainReach : std::uint8_t
518 {
519 /// No chain stays, so every install reference goes.
520 Nothing,
521 /// A bypassed chain forwards to the XInput provider and reaches no code of this image.
522 Provider,
523 /// A retained chain can reach the provider and the detours of this image.
524 ProviderAndImage,
525 };
526
527 /// Maps a reset that retained its chain to the reach of that chain.
528 116 [[nodiscard]] constexpr XInputChainReach reset_chain_reach(bool retained) noexcept
529 {
530
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 110 times.
116 return retained ? XInputChainReach::ProviderAndImage : XInputChainReach::Nothing;
531 }
532
533 /// Releases the install references that @p reach does not need. A kept reference is never released.
534 203 void retire_xinput_module_refs(XInputChainReach reach = XInputChainReach::Nothing) noexcept
535 {
536
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 203 times.
203 if (s_xinput_permanent_hooks == nullptr)
537 {
538 ✗ return;
539 }
540
2/2
✓ Branch 4 → 5 taken 197 times.
✓ Branch 4 → 10 taken 6 times.
203 if (reach != XInputChainReach::ProviderAndImage)
541 {
542
2/2
✓ Branch 5 → 6 taken 196 times.
✓ Branch 5 → 8 taken 1 time.
197 if (s_xinput_raw_owner)
543 {
544 196 release_xinput_raw_scope_owner();
545 196 s_xinput_raw_owner = false;
546 }
547 197 close_xinput_route_probes();
548 197 (void)release_xinput_route_probe_if_idle();
549 }
550
2/2
✓ Branch 10 → 11 taken 170 times.
✓ Branch 10 → 13 taken 33 times.
203 if (reach == XInputChainReach::Nothing)
551 {
552 170 DetourModKit::detail::release_module_ref(
553 s_xinput_permanent_hooks->ex_target_ref,
554 diagnostics::ModulePinReason::XInputTarget
555 );
556 170 DetourModKit::detail::release_module_ref(
557 s_xinput_permanent_hooks->target_ref,
558 diagnostics::ModulePinReason::XInputTarget
559 );
560 }
561
2/2
✓ Branch 13 → 14 taken 197 times.
✓ Branch 13 → 15 taken 6 times.
203 if (reach != XInputChainReach::ProviderAndImage)
562 {
563 197 DetourModKit::detail::release_module_ref(
564 s_xinput_permanent_hooks->self_ref,
565 diagnostics::ModulePinReason::XInputKeepalive
566 );
567 }
568 203 s_xinput_permanent_hooks->ex_target_ref = nullptr;
569 203 s_xinput_permanent_hooks->target_ref = nullptr;
570 203 s_xinput_permanent_hooks->self_ref = nullptr;
571 }
572
573 /// Returns the cached gateway entry of the cell member @p hook.
574 6096 [[nodiscard]] const std::uint8_t *claimed_gateway(const safetyhook::InlineHook &hook) noexcept
575 {
576
2/2
✓ Branch 2 → 3 taken 3048 times.
✓ Branch 2 → 4 taken 3048 times.
6096 return &hook == &s_xinput_permanent_hooks->ex ? s_xinput_permanent_hooks->ex_gateway
577 6096 : s_xinput_permanent_hooks->primary_gateway;
578 }
579
580 /**
581 * @brief Classifies the target window of a claimed member.
582 * @details A routed position patches a `jmp rel32` to its gateway and fills the rest of the window with
583 * `nop`. Only the owner of the gateway jumps to it, so that exact encoding is the patch of this member.
584 */
585 6096 [[nodiscard]] PatchWitness claimed_xinput_witness(const safetyhook::InlineHook &hook) noexcept
586 {
587 6096 constexpr std::size_t JUMP_SIZE = 5;
588 6096 const auto &original = hook.original_bytes();
589 6096 const std::size_t count = original.size();
590 6096 std::array<std::uint8_t, BACKEND_MAX_STEAL_WINDOW> current{};
591
3/6
✓ Branch 4 → 5 taken 6096 times.
✗ Branch 4 → 13 not taken.
✓ Branch 7 → 8 taken 6096 times.
✗ Branch 7 → 13 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 6096 times.
18288 if (count < JUMP_SIZE || count > current.size() ||
592
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 6096 times.
6096 !guarded_read_bytes(hook.target_address(), current.data(), count))
593 {
594 ✗ return PatchWitness::Indeterminate;
595 }
596
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 6096 times.
6096 if (std::equal(original.begin(), original.end(), current.begin()))
597 {
598 ✗ return PatchWitness::Original;
599 }
600 6096 const std::uint8_t *const gateway = claimed_gateway(hook);
601 12192 const std::intptr_t displacement = reinterpret_cast<std::intptr_t>(gateway) -
602 6096 static_cast<std::intptr_t>(hook.target_address() + JUMP_SIZE);
603
3/6
✓ Branch 24 → 25 taken 6096 times.
✗ Branch 24 → 29 not taken.
✓ Branch 26 → 27 taken 6096 times.
✗ Branch 26 → 29 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 6096 times.
12192 if (gateway == nullptr || displacement < std::numeric_limits<std::int32_t>::min() ||
604
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 6096 times.
6096 displacement > std::numeric_limits<std::int32_t>::max())
605 {
606 ✗ return PatchWitness::Foreign;
607 }
608 6096 std::array<std::uint8_t, BACKEND_MAX_STEAL_WINDOW> jump{};
609 6096 jump.fill(0x90);
610 6096 jump[0] = 0xE9;
611 6096 const auto relative = static_cast<std::int32_t>(displacement);
612 6096 std::memcpy(jump.data() + 1, &relative, sizeof(relative));
613 6096 return std::equal(jump.begin(), jump.begin() + static_cast<std::ptrdiff_t>(count), current.begin())
614
2/2
✓ Branch 38 → 39 taken 3084 times.
✓ Branch 38 → 40 taken 3012 times.
6096 ? PatchWitness::OwnedPatch
615 6096 : PatchWitness::Foreign;
616 }
617
618 19848 [[nodiscard]] PatchWitness xinput_patch_witness(const safetyhook::InlineHook &hook) noexcept
619 {
620
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 19847 times.
19848 if (!hook)
621 {
622 1 return PatchWitness::Original;
623 }
624
2/2
✓ Branch 6 → 7 taken 6096 times.
✓ Branch 6 → 8 taken 13751 times.
19847 return hook.claimed() ? claimed_xinput_witness(hook) : witness_patch(hook);
625 }
626
627 631 [[nodiscard]] PatchWitness xinput_teardown_witness(const safetyhook::InlineHook &hook) noexcept
628 {
629 // A reconciled disabled backend has no target entry into its trampoline. Treat retained inactive storage
630 // as Original even if its old target was later repatched or unmapped.
631
4/4
✓ Branch 3 → 4 taken 609 times.
✓ Branch 3 → 7 taken 22 times.
✓ Branch 5 → 6 taken 607 times.
✓ Branch 5 → 7 taken 2 times.
631 return hook && hook.enabled() ? xinput_patch_witness(hook) : PatchWitness::Original;
632 }
633
634 421 [[nodiscard]] bool try_xinput_backend_enable(safetyhook::InlineHook &hook) noexcept
635 {
636 try
637 {
638
2/2
✓ Branch 2 → 3 taken 415 times.
✓ Branch 2 → 6 taken 6 times.
421 return hook.enable().has_value();
639 }
640 6 catch (...)
641 {
642 #if defined(DMK_ENABLE_TEST_SEAMS)
643 s_xinput_backend_toggle_exception_catches.fetch_add(1, std::memory_order_relaxed);
644 #endif
645 6 return false;
646 6 }
647 }
648
649 214 [[nodiscard]] bool try_xinput_backend_disable(safetyhook::InlineHook &hook) noexcept
650 {
651 try
652 {
653
2/2
✓ Branch 2 → 3 taken 208 times.
✓ Branch 2 → 6 taken 6 times.
214 return hook.disable().has_value();
654 }
655 6 catch (...)
656 {
657 #if defined(DMK_ENABLE_TEST_SEAMS)
658 s_xinput_backend_toggle_exception_catches.fetch_add(1, std::memory_order_relaxed);
659 #endif
660 6 return false;
661 6 }
662 }
663
664 /**
665 * @brief Classifies one raw-hook arm by whether a prologue mutation reached the target.
666 * @details Uncommitted is the only outcome whose storage the caller can destroy. CommittedUnreachable wrote
667 * the prologue and then lost it. A thread already inside can still reach the trampoline.
668 */
669 enum class XInputArmOutcome : std::uint8_t
670 {
671 Armed,
672 Uncommitted,
673 CommittedUnreachable
674 };
675
676 /** @brief Arms a freshly created raw hook and reconciles a failed transaction from its target bytes. */
677 422 [[nodiscard]] XInputArmOutcome arm_xinput_hook(
678 safetyhook::InlineHook &hook,
679 std::atomic<XInputGetStateFn> &original,
680 bool original_was_published,
681 std::atomic<bool> &warning_latch,
682 std::string_view warning
683 ) noexcept
684 {
685 422 const PatchWitness before = xinput_patch_witness(hook);
686
2/2
✓ Branch 4 → 5 taken 114 times.
✓ Branch 4 → 15 taken 308 times.
422 if (hook.claimed())
687 {
688 // A claimed member writes no target byte, so its switch store is the whole arm. Original bytes no
689 // longer reach the claimed position.
690
3/6
✓ Branch 5 → 6 taken 114 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 114 times.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 114 times.
✗ Branch 10 → 12 not taken.
114 if (before != PatchWitness::Original && try_xinput_backend_enable(hook))
691 {
692 114 return XInputArmOutcome::Armed;
693 }
694
0/2
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
✗ if (!original_was_published)
695 {
696 ✗ original.store(nullptr, std::memory_order_release);
697 }
698 ✗ return XInputArmOutcome::Uncommitted;
699 }
700 // An arm over the patch of another layer recaptures it and becomes the newest layer (`[B-110]`).
701
4/4
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 19 taken 306 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 1 time.
308 const bool layered = before == PatchWitness::Foreign && hook.window_holds_layer();
702
6/6
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 24 taken 306 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 30 taken 307 times.
308 if (!witness_permits_write(before) && !layered)
703 {
704 1 hook.reconcile_enabled(false);
705
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 1 time.
1 if (!original_was_published)
706 {
707 ✗ original.store(nullptr, std::memory_order_release);
708 }
709 1 return XInputArmOutcome::Uncommitted;
710 }
711
712 307 const bool backend_enabled = try_xinput_backend_enable(hook);
713 307 run_xinput_arm_seam();
714 307 const bool mutation_committed = hook.enabled();
715 307 const PatchWitness after = xinput_patch_witness(hook);
716
4/4
✓ Branch 34 → 35 taken 302 times.
✓ Branch 34 → 36 taken 5 times.
✓ Branch 35 → 36 taken 12 times.
✓ Branch 35 → 53 taken 290 times.
307 if (!mutation_committed || after == PatchWitness::Original)
717 {
718 17 hook.reconcile_enabled(false);
719
4/4
✓ Branch 37 → 38 taken 5 times.
✓ Branch 37 → 40 taken 12 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 38 → 40 taken 2 times.
17 if (!mutation_committed && !original_was_published)
720 {
721 3 original.store(nullptr, std::memory_order_release);
722 }
723
5/6
✓ Branch 40 → 41 taken 12 times.
✓ Branch 40 → 42 taken 5 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 45 taken 12 times.
✓ Branch 46 → 47 taken 5 times.
✓ Branch 46 → 49 taken 12 times.
22 if ((!backend_enabled || after != PatchWitness::Original) &&
724
1/2
✓ Branch 43 → 44 taken 5 times.
✗ Branch 43 → 45 not taken.
5 !warning_latch.exchange(true, std::memory_order_relaxed))
725 {
726 5 (void)log().log_noexcept(LogLevel::Warning, warning);
727 }
728 // A committed mutation routed callers through this trampoline before another writer restored the
729 // bytes. No new caller can arrive, but this code still must not free the storage.
730
2/2
✓ Branch 49 → 50 taken 12 times.
✓ Branch 49 → 51 taken 5 times.
17 return mutation_committed ? XInputArmOutcome::CommittedUnreachable : XInputArmOutcome::Uncommitted;
731 }
732
733 // OwnedPatch is exact reachability. Foreign and Indeterminate cannot disprove a newer chain through this
734 // trampoline, so they retain the same conservative enabled state.
735 290 hook.reconcile_enabled(true);
736
5/6
✓ Branch 54 → 55 taken 289 times.
✓ Branch 54 → 56 taken 1 time.
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 59 taken 289 times.
✓ Branch 60 → 61 taken 1 time.
✓ Branch 60 → 63 taken 289 times.
291 if ((!backend_enabled || after != PatchWitness::OwnedPatch) &&
737
1/2
✓ Branch 57 → 58 taken 1 time.
✗ Branch 57 → 59 not taken.
1 !warning_latch.exchange(true, std::memory_order_relaxed))
738 {
739 1 (void)log().log_noexcept(LogLevel::Warning, warning);
740 }
741 290 return XInputArmOutcome::Armed;
742 }
743
744 /// Returns the gateway entry of the process record that a claimed @p hook adopted, or nullptr.
745 114 [[nodiscard]] const std::uint8_t *claimed_gateway_entry(const safetyhook::InlineHook &hook) noexcept
746 {
747 114 const safetyhook::ProcessCoordinator coordinator;
748 114 std::array<safetyhook::ProcessRouteRecord, 32> records{};
749
1/2
✓ Branch 4 → 5 taken 114 times.
✗ Branch 4 → 8 not taken.
114 const std::size_t count = coordinator ? coordinator.records_at(hook.target(), records) : 0;
750
1/2
✓ Branch 17 → 10 taken 164 times.
✗ Branch 17 → 18 not taken.
164 for (std::size_t index = 0; index < count; ++index)
751 {
752
2/2
✓ Branch 13 → 14 taken 114 times.
✓ Branch 13 → 16 taken 50 times.
164 if (records[index].trampoline.begin == hook.trampoline().data())
753 {
754 114 return records[index].gateway.begin;
755 }
756 }
757 ✗ return nullptr;
758 114 }
759
760 /**
761 * @brief Creates one disabled raw hook and contains allocation exceptions from creation.
762 * @details Creation remains separate from the arm, so the whole pair exists before any prologue patch. A
763 * creation failure rolls back completely and publishes nothing. The create claims a Vacant
764 * @p position first and then stores the claimed gateway in @p gateway.
765 */
766 402 [[nodiscard]] bool create_disabled_xinput_hook(
767 safetyhook::RouteRetentionCredit &credit,
768 void *target,
769 void *detour,
770 safetyhook::PositionClaim position,
771 safetyhook::InlineHook &destination,
772 const std::uint8_t *&gateway
773 ) noexcept
774 {
775 try
776 {
777 // One fresh arena per pair member keeps the pre-reserved block worst case independent.
778
2/2
✓ Branch 2 → 3 taken 401 times.
✓ Branch 2 → 27 taken 1 time.
402 const std::shared_ptr<safetyhook::Allocator> allocator = safetyhook::Allocator::create();
779 #if defined(DMK_ENABLE_TEST_SEAMS)
780
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 400 times.
401 if (const XInputCreateSeam seam = s_xinput_create_seam.load(std::memory_order_acquire); seam != nullptr)
781 {
782 1 seam();
783 }
784 #endif
785 auto created = safetyhook::InlineHook::create(
786 allocator,
787 target,
788 detour,
789 static_cast<safetyhook::InlineHook::Flags>(
790 safetyhook::InlineHook::StartDisabled | safetyhook::InlineHook::RoutedExternal
791 ),
792 credit,
793 position
794
2/2
✓ Branch 6 → 7 taken 400 times.
✓ Branch 6 → 25 taken 1 time.
401 );
795
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 400 times.
400 if (!created)
796 {
797 ✗ return false;
798 }
799 // The move assigns onto an empty destination, so it frees nothing and takes no allocation.
800
1/2
✓ Branch 10 → 11 taken 400 times.
✗ Branch 10 → 23 not taken.
800 destination = std::move(created.value());
801
2/2
✓ Branch 15 → 16 taken 114 times.
✓ Branch 15 → 17 taken 286 times.
400 gateway = destination.claimed() ? claimed_gateway_entry(destination) : nullptr;
802 400 return true;
803 401 }
804 2 catch (...)
805 {
806 2 return false;
807 2 }
808 }
809
810 /** @brief Restores one raw hook only while its pre-write witness authorizes the backend mutation. */
811 237 [[nodiscard]] PatchWitness restore_xinput_hook(safetyhook::InlineHook &hook) noexcept
812 {
813
2/2
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 5 taken 215 times.
237 if (!hook)
814 {
815 22 return PatchWitness::Original;
816 }
817
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 214 times.
215 if (!hook.enabled())
818 {
819 // A disabled backend has no target entry and writes nothing. A byte check at its former target reports
820 // Foreign for an unrelated writer and forces needless permanent retention.
821 1 return PatchWitness::Original;
822 }
823 214 const PatchWitness before = xinput_teardown_witness(hook);
824
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 13 taken 214 times.
214 if (!witness_permits_write(before))
825 {
826 ✗ hook.reconcile_enabled(true);
827 ✗ return before;
828 }
829
830 214 (void)try_xinput_backend_disable(hook);
831 214 const PatchWitness after = xinput_patch_witness(hook);
832 214 hook.reconcile_enabled(after != PatchWitness::Original);
833 214 return after;
834 }
835
836 /// Bounds the wait for closed bypass calls that a target restore released.
837 constexpr auto XINPUT_BYPASS_DRAIN_TIMEOUT = std::chrono::seconds{1};
838
839 /**
840 * @brief Releases a hook after its target witnesses Original and its detour bodies drain.
841 * @details A closed bypass holds its route entry until the provider returns. Reset waits for those calls, so
842 * an ordinary completion does not retain the chain. After expiry, reset retains a counted chain.
843 * @return The retention reason, or null after a clean reset.
844 */
845 [[nodiscard]] const char *
846 232 reset_inactive_xinput_hook(safetyhook::InlineHook &hook, std::atomic<XInputGetStateFn> &original) noexcept
847 {
848 232 original.store(nullptr, std::memory_order_seq_cst);
849 232 (void)drain_until_zero(
850 659 [&hook]() noexcept { return hook.route_entries(); },
851 232 std::chrono::steady_clock::now() + XINPUT_BYPASS_DRAIN_TIMEOUT
852 );
853 232 hook.reset();
854 232 const bool retained = hook.route_retained();
855
2/2
✓ Branch 8 → 9 taken 6 times.
✓ Branch 8 → 10 taken 226 times.
232 if (retained)
856 {
857 6 diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Input);
858 }
859
2/2
✓ Branch 10 → 11 taken 6 times.
✓ Branch 10 → 12 taken 226 times.
232 return retained ? hook.route_retention_reason() : nullptr;
860 }
861
862 /**
863 * @brief Reports retained chains after the interception lock releases.
864 */
865 200 void emit_xinput_route_retention_logs(const char *primary, const char *ex) noexcept
866 {
867
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 6 taken 196 times.
200 if (primary)
868 {
869 4 (void)log().try_log(
870 LogLevel::Warning,
871 "XInput route retention: XInputGetState retained its executable chain after reset. Reason: {}.",
872 primary
873 );
874 }
875
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 10 taken 198 times.
200 if (ex)
876 {
877 2 (void)log().try_log(
878 LogLevel::Warning,
879 "XInput route retention: ordinal 100 retained its executable chain after reset. Reason: {}.",
880 ex
881 );
882 }
883 200 }
884
885 /**
886 * @brief Publishes a trampoline, then arms a hook whose prologue is not patched.
887 * @details Reuses the hook object and its trampoline, so nothing is created and no executable storage is
888 * freed or replaced. arm_xinput_hook's witnesses still gate the write and decide reachability.
889 */
890 422 [[nodiscard]] XInputArmOutcome arm_created_xinput_hook(
891 safetyhook::InlineHook &hook,
892 std::atomic<XInputGetStateFn> &original,
893 std::atomic<bool> &warning_latch,
894 std::string_view warning
895 ) noexcept
896 {
897
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 422 times.
422 if (!hook)
898 {
899 ✗ return XInputArmOutcome::Uncommitted;
900 }
901
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 422 times.
422 if (hook.enabled())
902 {
903 ✗ return XInputArmOutcome::Armed;
904 }
905 422 const bool original_was_published = original.load(std::memory_order_seq_cst) != nullptr;
906 422 original.store(hook.original<XInputGetStateFn>(), std::memory_order_seq_cst);
907 422 return arm_xinput_hook(hook, original, original_was_published, warning_latch, warning);
908 }
909
910 /// Reports whether rearm restores the forward path.
911 25 [[nodiscard]] bool rearm_xinput_hook(
912 safetyhook::InlineHook &hook,
913 std::atomic<XInputGetStateFn> &original,
914 std::atomic<bool> &warning_latch,
915 std::string_view warning
916 ) noexcept
917 {
918 25 return arm_created_xinput_hook(hook, original, warning_latch, warning) == XInputArmOutcome::Armed;
919 }
920
921 enum class XInputRetentionReason : std::uint8_t
922 {
923 InflightTimeout,
924 UnrestoredPatch,
925 UnprovedInstall
926 };
927
928 struct XInputPublishedChains
929 {
930 bool primary{false};
931 bool ex{false};
932 };
933
934 struct XInputRetentionLog
935 {
936 std::array<char, 256> attribution{};
937 std::size_t attribution_length{0};
938 XInputRetentionReason reason{XInputRetentionReason::InflightTimeout};
939 bool pending{false};
940 };
941
942 /// Appends @p text at @p length and truncates at the buffer end.
943 2009 void append_text(std::span<char> buffer, std::size_t &length, std::string_view text) noexcept
944 {
945 2009 const std::size_t room = buffer.size() - length;
946
1/2
✓ Branch 4 → 5 taken 2009 times.
✗ Branch 4 → 6 not taken.
2009 const std::size_t count = text.size() < room ? text.size() : room;
947 2009 std::memcpy(buffer.data() + length, text.data(), count);
948 2009 length += count;
949 2009 }
950
951 /// Appends "0x" and the fixed 16-digit hex form of @p value.
952 20 void append_hex_address(std::span<char> buffer, std::size_t &length, std::uintptr_t value) noexcept
953 {
954 20 append_text(buffer, length, "0x");
955 char digits[16];
956
2/2
✓ Branch 6 → 5 taken 320 times.
✓ Branch 6 → 7 taken 20 times.
340 for (int i = 15; i >= 0; --i)
957 {
958 320 digits[i] = "0123456789ABCDEF"[value & 0xFU];
959 320 value >>= 4U;
960 }
961 20 append_text(buffer, length, std::string_view{digits, sizeof(digits)});
962 20 }
963
964 /// Names the condition that forced the raw hook pair into permanent storage.
965 10 [[nodiscard]] constexpr std::string_view xinput_retention_message(XInputRetentionReason reason) noexcept
966 {
967
3/4
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 5 times.
✓ Branch 2 → 5 taken 1 time.
✗ Branch 2 → 6 not taken.
10 switch (reason)
968 {
969 4 case XInputRetentionReason::InflightTimeout:
970 return "XInput interception: a game thread was still inside a detour at the quiesce deadline; "
971 4 "retained the hook trampolines instead of freeing them.";
972 5 case XInputRetentionReason::UnrestoredPatch:
973 return "XInput interception: target bytes could not be proved restored; retained the raw hook chain "
974 5 "instead of overwriting or freeing it.";
975 1 case XInputRetentionReason::UnprovedInstall:
976 1 break;
977 }
978 return "XInput interception: the prologue reverted to its original bytes after the hook transaction "
979 1 "committed; retained the published trampoline instead of freeing it.";
980 }
981
982 /**
983 * @brief Latches the canonical raw hook pair and keepalives as permanently retained.
984 * @note Requires s_intercept_mutex and a constructed process-lifetime cell.
985 * A supplied publication snapshot must be exact.
986 * Omit the snapshot only when a successful drain preceded the retention decision.
987 * Forwarding state then selects the published chains on its own.
988 */
989 10 void retain_xinput_hooks(
990 PatchWitness primary_witness,
991 PatchWitness ex_witness,
992 XInputRetentionReason reason,
993 XInputRetentionLog &deferred_log,
994 XInputPublishedChains published_chains = {}
995 ) noexcept
996 {
997 10 PermanentXInputHooks *const permanent = permanent_cell();
998 10 const bool primary_valid = static_cast<bool>(permanent->primary);
999 10 const bool ex_valid = static_cast<bool>(permanent->ex);
1000
3/4
✓ Branch 5 → 6 taken 10 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 8 times.
✓ Branch 7 → 9 taken 2 times.
10 const bool primary_forwarding_required = primary_valid && permanent->primary.enabled();
1001
3/4
✓ Branch 10 → 11 taken 10 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 8 times.
✓ Branch 12 → 14 taken 2 times.
10 const bool ex_forwarding_required = ex_valid && permanent->ex.enabled();
1002 10 permanent->primary.reconcile_enabled(
1003
3/4
✓ Branch 15 → 16 taken 8 times.
✓ Branch 15 → 18 taken 2 times.
✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 18 not taken.
10 primary_forwarding_required && primary_witness != PatchWitness::Original
1004 );
1005
3/4
✓ Branch 20 → 21 taken 8 times.
✓ Branch 20 → 23 taken 2 times.
✓ Branch 21 → 22 taken 8 times.
✗ Branch 21 → 23 not taken.
10 permanent->ex.reconcile_enabled(ex_forwarding_required && ex_witness != PatchWitness::Original);
1006
1007 // Keep a chain published when the backend still forwards. Also keep it when pointer publication preceded
1008 // the retention decision. An admitted caller can still reach the detour's original-pointer load.
1009 10 const bool primary_chain_required =
1010
4/6
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 28 taken 8 times.
✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 29 not taken.
✓ Branch 27 → 28 taken 2 times.
✗ Branch 27 → 29 not taken.
10 primary_forwarding_required || (primary_valid && published_chains.primary);
1011
4/6
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 33 taken 8 times.
✓ Branch 31 → 32 taken 2 times.
✗ Branch 31 → 34 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 2 times.
10 const bool ex_chain_required = ex_forwarding_required || (ex_valid && published_chains.ex);
1012
1/2
✓ Branch 35 → 36 taken 10 times.
✗ Branch 35 → 37 not taken.
20 s_xinput_original.store(
1013 10 primary_chain_required ? permanent->primary.original<XInputGetStateFn>() : nullptr,
1014 std::memory_order_seq_cst
1015 );
1016
2/2
✓ Branch 39 → 40 taken 8 times.
✓ Branch 39 → 41 taken 2 times.
18 s_xinput_ex_original.store(
1017 8 ex_chain_required ? permanent->ex.original<XInputGetStateFn>() : nullptr,
1018 std::memory_order_seq_cst
1019 );
1020 10 s_xinput_permanent_detour.store(true, std::memory_order_release);
1021 10 s_xinput_installed.store(false, std::memory_order_release);
1022 10 DetourModKit::diagnostics::record_intentional_leak(DetourModKit::diagnostics::LeakSubsystem::Input);
1023
1024 // Build the loader attribution under the lock. The caller emits it after the critical section.
1025 10 deferred_log.reason = reason;
1026 10 append_text(
1027 10 deferred_log.attribution,
1028 10 deferred_log.attribution_length,
1029 10 "XInput retention attribution: XInputGetState target "
1030 );
1031
1/2
✓ Branch 50 → 51 taken 10 times.
✗ Branch 50 → 52 not taken.
20 append_hex_address(
1032 10 deferred_log.attribution,
1033 10 deferred_log.attribution_length,
1034 20 permanent->primary ? permanent->primary.target_address() : std::uintptr_t{0}
1035 );
1036 10 append_text(deferred_log.attribution, deferred_log.attribution_length, " (");
1037 20 append_text(
1038 10 deferred_log.attribution,
1039 10 deferred_log.attribution_length,
1040 witness_description(primary_witness)
1041 );
1042 10 append_text(deferred_log.attribution, deferred_log.attribution_length, ")");
1043
1/2
✓ Branch 65 → 66 taken 10 times.
✗ Branch 65 → 82 not taken.
10 if (permanent->ex)
1044 {
1045 10 append_text(deferred_log.attribution, deferred_log.attribution_length, ". XInputGetStateEx target ");
1046 20 append_hex_address(
1047 10 deferred_log.attribution,
1048 10 deferred_log.attribution_length,
1049 permanent->ex.target_address()
1050 );
1051 10 append_text(deferred_log.attribution, deferred_log.attribution_length, " (");
1052 10 append_text(deferred_log.attribution, deferred_log.attribution_length, witness_description(ex_witness));
1053 10 append_text(deferred_log.attribution, deferred_log.attribution_length, ")");
1054 }
1055 10 append_text(deferred_log.attribution, deferred_log.attribution_length, ".");
1056 10 deferred_log.pending = true;
1057
1058 10 s_xinput_pair_degraded.store(false, std::memory_order_release);
1059 10 xinput_recovery_reset();
1060 10 s_xinput_enable_warned.store(false, std::memory_order_relaxed);
1061 10 s_xinput_ex_enable_warned.store(false, std::memory_order_relaxed);
1062 10 s_xinput_capacity_warned.store(false, std::memory_order_relaxed);
1063 10 }
1064
1065 /** @brief Emits a retention report after its caller releases s_intercept_mutex. */
1066 94 void emit_xinput_retention_log(const XInputRetentionLog &deferred_log) noexcept
1067 {
1068
2/2
✓ Branch 2 → 3 taken 84 times.
✓ Branch 2 → 4 taken 10 times.
94 if (!deferred_log.pending)
1069 {
1070 84 return;
1071 }
1072 10 const std::string_view attribution{deferred_log.attribution.data(), deferred_log.attribution_length};
1073 #if defined(DMK_ENABLE_TEST_SEAMS)
1074
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 9 times.
10 if (const XInputRetentionAttributionSeam seam =
1075 10 s_xinput_retention_attribution_seam.load(std::memory_order_acquire);
1076 seam != nullptr)
1077 {
1078 1 seam(attribution);
1079 }
1080 #endif
1081 10 (void)log().log_noexcept(LogLevel::Warning, xinput_retention_message(deferred_log.reason));
1082 10 (void)log().log_noexcept(LogLevel::Warning, attribution);
1083 }
1084
1085 /** @brief Changes the controller and invalidates receipts for its prior binding under s_intercept_mutex. */
1086 2866 void bind_xinput_user(int user_index) noexcept
1087 {
1088
2/2
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 13 taken 2863 times.
2866 if (s_bound_user_index.load(std::memory_order_relaxed) != user_index)
1089 {
1090 s_xinput_hook_epoch.fetch_add(1, std::memory_order_seq_cst);
1091 3 s_xinput_observed_pair = false;
1092 3 s_xinput_primary_observed = false;
1093 3 s_xinput_ex_observed = false;
1094 3 s_xinput_route_warned = false;
1095 }
1096 s_bound_user_index.store(user_index, std::memory_order_relaxed);
1097 2866 }
1098
1099 /**
1100 * @brief Requires s_intercept_mutex. Commits a complete XInput pair and activates suppression.
1101 * @details Both detours read s_xinput_installed, so suppression can never be live for one member of the
1102 * pair and not the other.
1103 */
1104 1443 void publish_complete_xinput_pair(int user_index, std::uint64_t owner) noexcept
1105 {
1106 1443 bind_xinput_user(user_index);
1107 1443 xinput_recovery_reset();
1108 1443 s_xinput_pair_degraded.store(false, std::memory_order_release);
1109 1443 s_xinput_installed.store(true, std::memory_order_release);
1110 1443 publish_owner(owner);
1111 1443 s_xinput_route_warned = false;
1112 1443 }
1113
1114 /**
1115 * @brief Requires s_intercept_mutex. Reads one pair member's target bytes and reports whether it still covers.
1116 * @param required Whether this entry point needs coverage for a complete pair. An absent or aliased
1117 * ordinal-100 export needs no second member.
1118 */
1119 2997 [[nodiscard]] bool xinput_member_entry_witnessed(const safetyhook::InlineHook &hook, bool required) noexcept
1120 {
1121
2/2
✓ Branch 3 → 4 taken 12 times.
✓ Branch 3 → 5 taken 2985 times.
2997 if (!hook)
1122 {
1123 12 return !required;
1124 }
1125
2/2
✓ Branch 6 → 7 taken 162 times.
✓ Branch 6 → 8 taken 2823 times.
2985 if (!hook.enabled())
1126 {
1127 162 return false;
1128 }
1129 2823 return xinput_patch_witness(hook) == PatchWitness::OwnedPatch;
1130 }
1131
1132 /** @brief Reports exact owned coverage and reconciles Original to disabled under s_intercept_mutex. */
1133 5710 [[nodiscard]] bool xinput_member_covers_entry(safetyhook::InlineHook &hook, bool required) noexcept
1134 {
1135
2/2
✓ Branch 3 → 4 taken 20 times.
✓ Branch 3 → 5 taken 5690 times.
5710 if (!hook)
1136 {
1137 20 return !required;
1138 }
1139
2/2
✓ Branch 6 → 7 taken 165 times.
✓ Branch 6 → 8 taken 5525 times.
5690 if (!hook.enabled())
1140 {
1141 165 return false;
1142 }
1143 5525 const PatchWitness witness = xinput_patch_witness(hook);
1144
2/2
✓ Branch 9 → 10 taken 2460 times.
✓ Branch 9 → 11 taken 3065 times.
5525 if (witness == PatchWitness::OwnedPatch)
1145 {
1146 2460 return true;
1147 }
1148 // Exact Original can be re-armed through the same object. Foreign and Indeterminate can still be a
1149 // newer chain through our trampoline, so preserve their enabled state and report only coverage loss.
1150
2/2
✓ Branch 11 → 12 taken 6 times.
✓ Branch 11 → 13 taken 3059 times.
3065 if (witness == PatchWitness::Original)
1151 {
1152 6 hook.reconcile_enabled(false);
1153 }
1154 3065 return false;
1155 }
1156
1157 /**
1158 * @brief Publishes exact-patch coverage under s_intercept_mutex.
1159 * @details Foreign entries require a separate unlocked route check. An unavailable member disarms both detours.
1160 * @return true when coverage was published.
1161 */
1162 1873 [[nodiscard]] bool publish_xinput_pair_if_whole(
1163 safetyhook::InlineHook &primary,
1164 safetyhook::InlineHook &ex,
1165 int user_index,
1166 std::uint64_t owner
1167 ) noexcept
1168 {
1169 1873 const bool primary_covered = xinput_member_covers_entry(primary, true);
1170 1873 const bool ex_covered = xinput_member_covers_entry(ex, static_cast<bool>(ex));
1171
7/8
✓ Branch 5 → 6 taken 670 times.
✓ Branch 5 → 10 taken 1203 times.
✓ Branch 6 → 7 taken 450 times.
✓ Branch 6 → 10 taken 220 times.
✓ Branch 8 → 9 taken 450 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 450 times.
✓ Branch 11 → 14 taken 1423 times.
1873 if (primary_covered && ex_covered && xinput_consume_context_healthy())
1172 {
1173 450 s_xinput_observed_pair = false;
1174 450 s_xinput_primary_observed = false;
1175 450 s_xinput_ex_observed = false;
1176 450 publish_complete_xinput_pair(user_index, owner);
1177 450 return true;
1178 }
1179 1423 bind_xinput_user(user_index);
1180
2/2
✓ Branch 17 → 18 taken 25 times.
✓ Branch 17 → 19 taken 890 times.
915 if (!s_xinput_observed_pair ||
1181
6/8
✓ Branch 15 → 16 taken 915 times.
✓ Branch 15 → 24 taken 508 times.
✓ Branch 18 → 19 taken 25 times.
✗ Branch 18 → 24 not taken.
✓ Branch 20 → 21 taken 915 times.
✗ Branch 20 → 25 not taken.
✓ Branch 26 → 27 taken 508 times.
✓ Branch 26 → 29 taken 915 times.
3278 (xinput_patch_witness(primary) != PatchWitness::Foreign && !primary_covered) ||
1182
3/4
✓ Branch 22 → 23 taken 505 times.
✓ Branch 22 → 25 taken 410 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 505 times.
1830 (ex && xinput_patch_witness(ex) != PatchWitness::Foreign && !ex_covered))
1183 {
1184 508 s_xinput_installed.store(false, std::memory_order_release);
1185 508 s_xinput_pair_degraded.store(true, std::memory_order_release);
1186 }
1187 1423 publish_owner(owner);
1188 1423 return false;
1189 }
1190
1191 /**
1192 * @brief Requires s_intercept_mutex. Re-arms each member that lost entry-point coverage.
1193 * @details Each member uses its own hook object. Recovery of only the partner leaves the required export
1194 * permanently open.
1195 */
1196 982 [[nodiscard]] bool rearm_xinput_pair(safetyhook::InlineHook &primary, safetyhook::InlineHook &ex) noexcept
1197 {
1198 982 bool primary_covered = xinput_member_covers_entry(primary, true);
1199
6/6
✓ Branch 3 → 4 taken 940 times.
✓ Branch 3 → 7 taken 42 times.
✓ Branch 5 → 6 taken 8 times.
✓ Branch 5 → 7 taken 932 times.
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 12 taken 974 times.
982 if (!primary_covered && !primary.enabled())
1200 {
1201 8 (void)rearm_xinput_hook(
1202 primary,
1203 s_xinput_original,
1204 s_xinput_enable_warned,
1205 8 "InputIntercept: the XInputGetState re-arm did not complete cleanly, so XInput "
1206 "coverage stays degraded and both entries pass through."
1207 );
1208 8 primary_covered = xinput_member_entry_witnessed(primary, true);
1209 }
1210 982 bool ex_covered = xinput_member_covers_entry(ex, static_cast<bool>(ex));
1211
6/6
✓ Branch 14 → 15 taken 452 times.
✓ Branch 14 → 18 taken 530 times.
✓ Branch 16 → 17 taken 13 times.
✓ Branch 16 → 18 taken 439 times.
✓ Branch 19 → 20 taken 13 times.
✓ Branch 19 → 24 taken 969 times.
982 if (!ex_covered && !ex.enabled())
1212 {
1213 13 (void)rearm_xinput_hook(
1214 ex,
1215 s_xinput_ex_original,
1216 s_xinput_ex_enable_warned,
1217 13 "InputIntercept: the XInputGetStateEx re-arm did not complete cleanly, so "
1218 "XInput coverage stays degraded and both entries pass through."
1219 );
1220 13 ex_covered = xinput_member_entry_witnessed(ex, static_cast<bool>(ex));
1221 }
1222
4/4
✓ Branch 24 → 25 taken 48 times.
✓ Branch 24 → 27 taken 934 times.
✓ Branch 25 → 26 taken 7 times.
✓ Branch 25 → 27 taken 41 times.
982 return primary_covered && ex_covered;
1223 }
1224
1225 /**
1226 * @brief Requires s_intercept_mutex. Re-witnesses a pair and drives deadline-gated recovery of absent members.
1227 * @details One routine handles per-cycle health, live-pair recovery, and retained-pair recovery. It detects
1228 * loss of pair integrity after publication. Every publication uses the final pair witness.
1229 * @return true when complete coverage is published for @p owner.
1230 */
1231 1660 [[nodiscard]] bool maintain_xinput_pair(
1232 safetyhook::InlineHook &primary,
1233 safetyhook::InlineHook &ex,
1234 const void *target_module,
1235 int user_index,
1236 std::uint64_t owner
1237 ) noexcept
1238 {
1239
2/2
✓ Branch 3 → 4 taken 286 times.
✓ Branch 3 → 5 taken 1374 times.
1660 if (publish_xinput_pair_if_whole(primary, ex, user_index, owner))
1240 {
1241 286 return true;
1242 }
1243
1244 const XInputRecoveryEvidence evidence{
1245 target_module,
1246 owner,
1247 1374 xinput_member_entry_witnessed(primary, true),
1248 1374 xinput_member_entry_witnessed(ex, static_cast<bool>(ex)),
1249 1374 witness_permits_write(xinput_patch_witness(primary)),
1250 1374 witness_permits_write(xinput_patch_witness(ex))
1251 4122 };
1252
2/2
✓ Branch 13 → 14 taken 392 times.
✓ Branch 13 → 15 taken 982 times.
1374 if (!xinput_recovery_due(evidence))
1253 {
1254 392 return false;
1255 }
1256
5/6
✓ Branch 17 → 18 taken 7 times.
✓ Branch 17 → 21 taken 975 times.
✓ Branch 19 → 20 taken 7 times.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 7 times.
✓ Branch 22 → 24 taken 975 times.
989 if (record_xinput_recovery_attempt(rearm_xinput_pair(primary, ex)) &&
1257 7 publish_xinput_pair_if_whole(primary, ex, user_index, owner))
1258 {
1259 7 return true;
1260 }
1261 975 xinput_recovery_deferred();
1262 975 return false;
1263 }
1264
1265 /**
1266 * @brief Marks a game thread as active inside an XInput detour body.
1267 * @details This counter and the published trampoline pointer form a Dekker-style pair with the retire store in
1268 * retire_published_xinput_chains() and the drain load in quiesce_xinput_routes_until_deadline(). Both
1269 * sides use store then load. Acquire and release do not forbid this StoreLoad order change. The
1270 * increment, trampoline load, retire store, and drain load use seq_cst. The decrement stays release
1271 * and does not belong to the StoreLoad pair.
1272 */
1273 struct InflightGuard
1274 {
1275 16827 InflightGuard() noexcept { s_xinput_inflight.fetch_add(1, std::memory_order_seq_cst); }
1276 16685 ~InflightGuard() noexcept { s_xinput_inflight.fetch_sub(1, std::memory_order_release); }
1277 InflightGuard(const InflightGuard &) = delete;
1278 InflightGuard &operator=(const InflightGuard &) = delete;
1279 InflightGuard(InflightGuard &&) = delete;
1280 InflightGuard &operator=(InflightGuard &&) = delete;
1281 };
1282
1283 // Each detour-side consume rule occupies one atomic word, so readers never see a torn rule. A seqlock protects
1284 // the array and count. An even value is stable, and an odd value marks an update. Game XInput threads read
1285 // snapshots without a lock. The two writers, clear_data_plane_locked() and publish_gamepad_consume_rules(),
1286 // serialize through s_data_plane_mutex. InputPoller publication also holds InputPoller::m_bindings_rw_mutex.
1287 std::array<std::atomic<uint64_t>, MAX_GAMEPAD_CONSUME_RULES> s_consume_rules{};
1288 std::atomic<uint32_t> s_consume_rule_count{0};
1289 std::atomic<uint32_t> s_consume_rules_seq{0};
1290
1291 // The focus gate disables same-frame rules while their published table and deadline survive focus changes.
1292 std::atomic<bool> s_rule_suppress_enabled{false};
1293
1294 /**
1295 * @brief Packs a rule into one word: modifier (bits 0-15), forbidden (16-31), trigger (32-47).
1296 * @details Three 16-bit masks fit a uint64 with room to spare, so a rule is published and read as a single
1297 * atomic store/load.
1298 */
1299 271 constexpr uint64_t pack_consume_rule(const GamepadConsumeRule &rule) noexcept
1300 {
1301 271 return static_cast<uint64_t>(rule.modifier_mask) | (static_cast<uint64_t>(rule.forbidden_mask) << 16) |
1302 271 (static_cast<uint64_t>(rule.trigger_mask) << 32);
1303 }
1304
1305 /// Unpacks a consume rule.
1306 463 constexpr GamepadConsumeRule unpack_consume_rule(uint64_t packed) noexcept
1307 {
1308 return GamepadConsumeRule{
1309 static_cast<uint16_t>(packed & 0xFFFFu),
1310 463 static_cast<uint16_t>((packed >> 16) & 0xFFFFu),
1311 463 static_cast<uint16_t>((packed >> 32) & 0xFFFFu)
1312 463 };
1313 }
1314
1315 std::array<std::atomic<std::uint64_t>, 4> s_wheel_count{
1316 wheel_count_slot(1, 0),
1317 wheel_count_slot(1, 0),
1318 wheel_count_slot(1, 0),
1319 wheel_count_slot(1, 0)
1320 };
1321 // Index zero stores vertical distance, and index one stores horizontal distance. Each axis has one signed
1322 // remainder because a reversal cancels sub-notch distance while the axes remain independent.
1323 std::array<std::atomic<std::uint64_t>, 2> s_wheel_remainder{
1324 wheel_remainder_slot(1, false, 0),
1325 wheel_remainder_slot(1, false, 0)
1326 };
1327 // The per-direction wheel-swallow mask uses WheelDirection bits and pairs with a TTL. Each poll cycle refreshes
1328 // it, so a stalled poll thread stops the swallow action. "Ctrl+WheelUp" must not eat bare WheelDown or WheelUp.
1329 std::atomic<uint8_t> s_wheel_consume_mask{0};
1330 std::atomic<uint64_t> s_wheel_consume_deadline_ms{0};
1331 // When set, wheel count admission and consume finalization also require process foreground. Published by the
1332 // poll loop together with the consume mask, and cleared on revocation.
1333 std::atomic<bool> s_wheel_require_focus{false};
1334
1335 /// Retags the wheel counters and remainders under @p wheel_epoch and clears the consume mask.
1336 333 void reset_wheel_data_plane(std::uint64_t wheel_epoch) noexcept
1337 {
1338 s_wheel_consume_mask.store(0, std::memory_order_release);
1339
2/2
✓ Branch 21 → 11 taken 1332 times.
✓ Branch 21 → 22 taken 333 times.
1665 for (auto &count : s_wheel_count)
1340 {
1341 1332 count.store(wheel_count_slot(wheel_epoch, 0), std::memory_order_relaxed);
1342 }
1343
2/2
✓ Branch 33 → 23 taken 666 times.
✓ Branch 33 → 34 taken 333 times.
999 for (auto &remainder : s_wheel_remainder)
1344 {
1345 666 remainder.store(wheel_remainder_slot(wheel_epoch, false, 0), std::memory_order_relaxed);
1346 }
1347 333 }
1348
1349 323 void clear_data_plane_locked(std::uint64_t wheel_epoch) noexcept
1350 {
1351 // Single-atomic disarms occur first, so detour suppression stops before the multi-step rule update begins.
1352 s_suppress_mask.store(0, std::memory_order_release);
1353 323 s_rule_suppress_enabled.store(false, std::memory_order_relaxed);
1354 // A revoked layer carries no focus requirement, so the next owner starts from the default gate.
1355 323 s_wheel_require_focus.store(false, std::memory_order_relaxed);
1356 323 reset_wheel_data_plane(wheel_epoch);
1357
1358 // Safe only because every writer now holds this lock, so the bracket cannot interleave with a binding
1359 // mutation. The clear exists so a later owner cannot inherit the previous owner's chords.
1360 323 const uint32_t seq = s_consume_rules_seq.load(std::memory_order_relaxed);
1361 323 s_consume_rules_seq.store(seq + 1, std::memory_order_relaxed);
1362 std::atomic_thread_fence(std::memory_order_release);
1363 s_consume_rule_count.store(0, std::memory_order_relaxed);
1364 323 s_consume_rules_seq.store(seq + 2, std::memory_order_release);
1365 323 }
1366
1367 // Local message-hook wheel-capture source (the single-DLL default). It observes wheel messages through a
1368 // thread-scoped WH_GETMESSAGE hook and feeds the shared fold and drain machinery. The module reference is
1369 // permanent because a selected callback can run after UnhookWindowsHookEx.
1370 //
1371 // Route identity: the mounted target thread's numeric id, its SYNCHRONIZE handle, a mount generation, and a
1372 // typed route state. Readiness derives from the live handle and the mounted id, never from a sticky flag.
1373 // The handle, generation, and state transitions are guarded by s_intercept_mutex; the id and state atomics
1374 // give the hook callback and cheap queries a lock-free read.
1375 std::atomic<HHOOK> s_msg_hook{nullptr};
1376 std::atomic<bool> s_msg_hook_ref_taken{false};
1377 std::atomic<DWORD> s_msg_hook_thread_id{0};
1378 HANDLE s_msg_hook_thread = nullptr;
1379 std::uint64_t s_msg_hook_mount_generation = 0;
1380 std::atomic<std::uint8_t> s_msg_hook_route_state{static_cast<std::uint8_t>(WheelRouteState::TargetWait)};
1381
1382 3006 bool wheel_capture_armable_locked() noexcept
1383 {
1384
2/2
✓ Branch 3 → 4 taken 38 times.
✓ Branch 3 → 12 taken 2968 times.
3044 return s_msg_hook.load(std::memory_order_relaxed) == nullptr ||
1385
2/2
✓ Branch 11 → 12 taken 36 times.
✓ Branch 11 → 13 taken 2 times.
38 s_msg_hook_route_state.load(std::memory_order_relaxed) ==
1386 3006 static_cast<std::uint8_t>(WheelRouteState::Ready);
1387 }
1388
1389 // Counts callback frames inside a wheel admission phase (count admission or consume finalization). The
1390 // control plane drains it, bounded, after an epoch advance so no admitted decision reaches a successor
1391 // route. A frame parked inside a lower hook holds no phase.
1392 std::atomic<std::uint32_t> s_wheel_admitted_phases{0};
1393
1394 /// Bounds every control-plane wait for wheel admitted phases.
1395 constexpr std::uint64_t WHEEL_DRAIN_TIMEOUT_MS = 2000;
1396
1397 /// Takes a counted reference for the module at @p address and books it as an XInputTarget pin.
1398 406 [[nodiscard]] HMODULE acquire_module_ref_containing_address(const void *address) noexcept
1399 {
1400
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 406 times.
406 if (address == nullptr)
1401 {
1402 ✗ return nullptr;
1403 }
1404
1405 406 HMODULE module = nullptr;
1406
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 406 times.
406 if (!GetModuleHandleExW(
1407 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
1408 reinterpret_cast<LPCWSTR>(address),
1409 &module
1410 ))
1411 {
1412 ✗ return nullptr;
1413 }
1414 406 module_pin_observability::note_acquired(diagnostics::ModulePinReason::XInputTarget);
1415 406 return module;
1416 }
1417
1418 /**
1419 * @brief Adds @p notches to an epoch-tagged, per-direction wheel counter with saturation.
1420 * @details A revocation retags the slot. A writer with the retired epoch fails and cannot publish into the
1421 * successor backlog. Saturation bounds idle accretion after the last binding is gone.
1422 * @return true when the slot records the notches or is already saturated. Returns false when @p epoch is
1423 * retired.
1424 */
1425 [[nodiscard]] bool
1426 1200 bump_wheel_notch(std::atomic<std::uint64_t> &slot, std::uint64_t epoch, std::uint64_t notches = 1) noexcept
1427 {
1428 1200 std::uint64_t current = slot.load(std::memory_order_relaxed);
1429
1/2
✓ Branch 22 → 10 taken 1200 times.
✗ Branch 22 → 23 not taken.
1200 while (wheel_slot_epoch(current) == epoch)
1430 {
1431 1200 const std::uint64_t count = current & WHEEL_COUNT_MASK;
1432
2/2
✓ Branch 10 → 11 taken 128 times.
✓ Branch 10 → 12 taken 1072 times.
1200 if (count >= MAX_WHEEL_NOTCHES)
1433 {
1434 128 return true;
1435 }
1436 const std::uint64_t next =
1437 1072 std::min<std::uint64_t>(count + notches, static_cast<std::uint64_t>(MAX_WHEEL_NOTCHES));
1438
1/2
✓ Branch 19 → 20 taken 1072 times.
✗ Branch 19 → 21 not taken.
2144 if (slot.compare_exchange_weak(
1439 current,
1440 wheel_count_slot(epoch, next),
1441 std::memory_order_relaxed,
1442 std::memory_order_relaxed
1443 ))
1444 {
1445 1072 return true;
1446 }
1447 }
1448 ✗ return false;
1449 }
1450
1451 /**
1452 * @brief Folds one wheel message's signed delta into an axis remainder tagged by (epoch, owned).
1453 * @details A stored tag from another (epoch, owned) state contributes nothing, so ownership flips and epoch
1454 * advances restart accumulation. A retired-epoch slot refuses the fold entirely.
1455 * @return The admission verdict and, when admitted, the signed whole-notch count of the fold.
1456 */
1457 struct WheelFold
1458 {
1459 bool admitted;
1460 int notches;
1461 };
1462 1213 [[nodiscard]] WheelFold accumulate_wheel_remainder(
1463 std::atomic<std::uint64_t> &slot,
1464 std::uint64_t epoch,
1465 bool owned,
1466 int delta
1467 ) noexcept
1468 {
1469 1213 std::uint64_t current = slot.load(std::memory_order_relaxed);
1470
1/2
✓ Branch 20 → 10 taken 1213 times.
✗ Branch 20 → 21 not taken.
1213 while ((current >> WHEEL_REMAINDER_EPOCH_SHIFT) == epoch)
1471 {
1472 1213 int prior = 0;
1473
2/2
✓ Branch 10 → 11 taken 1199 times.
✓ Branch 10 → 12 taken 14 times.
1213 if (((current & WHEEL_REMAINDER_OWNED_BIT) != 0) == owned)
1474 {
1475 1199 prior = static_cast<int>(current & WHEEL_REMAINDER_VALUE_MASK) - WHEEL_REMAINDER_BIAS;
1476 }
1477 // delta is a signed short and |prior| < WHEEL_DELTA, so the total cannot overflow int. The quotient
1478 // truncates toward zero and keeps the remainder sign equal to the total sign.
1479 1213 const int total = prior + delta;
1480 1213 const int notches = total / WHEEL_DELTA;
1481 1213 const int remainder = total % WHEEL_DELTA;
1482
1/2
✓ Branch 18 → 19 taken 1213 times.
✗ Branch 18 → 20 not taken.
2426 if (slot.compare_exchange_weak(
1483 current,
1484 wheel_remainder_slot(epoch, owned, remainder),
1485 std::memory_order_relaxed,
1486 std::memory_order_relaxed
1487 ))
1488 {
1489 1213 return WheelFold{true, notches};
1490 }
1491 }
1492 ✗ return WheelFold{false, 0};
1493 }
1494
1495 /**
1496 * @brief Reports whether the detour swallows a wheel message of the given direction this instant.
1497 * @details The acquire load of the mask orders the relaxed deadline read (publish_wheel_consume writes the
1498 * deadline first). A lapsed deadline forwards, so the game is never latched out of its wheel.
1499 * @param direction_bit One WheelDirection bit for the message direction.
1500 */
1501 1228 bool wheel_direction_consumed(uint8_t direction_bit) noexcept
1502 {
1503
2/2
✓ Branch 9 → 10 taken 1197 times.
✓ Branch 9 → 11 taken 31 times.
1228 if ((s_wheel_consume_mask.load(std::memory_order_acquire) & direction_bit) == 0)
1504 {
1505 1197 return false;
1506 }
1507 62 return GetTickCount64() < s_wheel_consume_deadline_ms.load(std::memory_order_relaxed);
1508 }
1509
1510 /// Brackets one wheel admission phase. Allocation-free, nonblocking, and free of DMK locks.
1511 class WheelPhaseGuard
1512 {
1513 public:
1514 1233 WheelPhaseGuard() noexcept { s_wheel_admitted_phases.fetch_add(1, std::memory_order_seq_cst); }
1515 1233 ~WheelPhaseGuard() noexcept { s_wheel_admitted_phases.fetch_sub(1, std::memory_order_seq_cst); }
1516 WheelPhaseGuard(const WheelPhaseGuard &) = delete;
1517 WheelPhaseGuard &operator=(const WheelPhaseGuard &) = delete;
1518 };
1519
1520 /// Reports whether this process owns the foreground window.
1521 5 [[nodiscard]] bool process_owns_foreground() noexcept
1522 {
1523 #if defined(DMK_ENABLE_TEST_SEAMS)
1524
1/2
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 11 not taken.
5 if (const std::int32_t override_value = s_wheel_process_focus_override.load(std::memory_order_acquire);
1525 override_value >= 0)
1526 {
1527 5 return override_value != 0;
1528 }
1529 #endif
1530 ✗ const HWND foreground = GetForegroundWindow();
1531
0/2
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
✗ if (foreground == nullptr)
1532 {
1533 ✗ return false;
1534 }
1535 ✗ DWORD pid = 0;
1536 ✗ GetWindowThreadProcessId(foreground, &pid);
1537 ✗ return pid == GetCurrentProcessId();
1538 }
1539
1540 /// Reports whether the published focus gate admits wheel counting and consume right now.
1541 1230 [[nodiscard]] bool wheel_focus_admits() noexcept
1542 {
1543
4/4
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 6 taken 1225 times.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 2 times.
1230 return !s_wheel_require_focus.load(std::memory_order_relaxed) || process_owns_foreground();
1544 }
1545
1546 /**
1547 * @brief Waits, bounded, for every wheel admitted phase to leave.
1548 * @details Runs only on the control plane, after an epoch advance already invalidated the admissions it
1549 * waits on. A parked lower-hook frame holds no phase, so the wait covers only the short
1550 * allocation-free admission windows.
1551 */
1552 32 [[nodiscard]] bool drain_wheel_admitted_phases() noexcept
1553 {
1554 32 std::uint64_t timeout_ms = WHEEL_DRAIN_TIMEOUT_MS;
1555 #if defined(DMK_ENABLE_TEST_SEAMS)
1556
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 30 times.
32 if (const std::uint64_t override_ms = s_wheel_drain_timeout_override_ms.load(std::memory_order_acquire);
1557 override_ms != 0)
1558 {
1559 2 timeout_ms = override_ms;
1560 }
1561 #endif
1562 32 const std::uint64_t deadline_ms = GetTickCount64() + timeout_ms;
1563
2/2
✓ Branch 24 → 13 taken 292148 times.
✓ Branch 24 → 25 taken 31 times.
292211 while (s_wheel_admitted_phases.load(std::memory_order_seq_cst) != 0)
1564 {
1565
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 292147 times.
292148 if (GetTickCount64() >= deadline_ms)
1566 {
1567 1 return false;
1568 }
1569 292147 Sleep(0);
1570 }
1571 31 return true;
1572 }
1573
1574 /// Returns the WheelDirection bit for one signed delta on one axis.
1575 1228 [[nodiscard]] uint8_t wheel_delta_direction_bit(bool horizontal, int delta) noexcept
1576 {
1577 // A positive vertical delta scrolls up, and a negative delta scrolls down. A positive horizontal delta
1578 // tilts right, and a negative delta tilts left.
1579
4/4
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 6 taken 1220 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 6 times.
2448 const WheelDirection direction = horizontal ? (delta > 0 ? WheelDirection::Right : WheelDirection::Left)
1580
2/2
✓ Branch 6 → 7 taken 1212 times.
✓ Branch 6 → 8 taken 8 times.
1220 : (delta > 0 ? WheelDirection::Up : WheelDirection::Down);
1581 1228 return wheel_direction_bit(direction);
1582 }
1583
1584 /**
1585 * @brief Wheel count admission: accumulates sub-notch distance, publishes whole notches, and snapshots the
1586 * consume intent, all before CallNextHookEx and without a message mutation.
1587 * @details GET_WHEEL_DELTA_WPARAM is signed and need not be a WHEEL_DELTA multiple. The axis remainder folds
1588 * fragments into `abs(total) / WHEEL_DELTA` notches. A reversal cancels accumulated distance. The
1589 * (epoch, owned) tag separates owned and unowned fragments (WheelDeltaTest.*). The admitted phase
1590 * is counted so the control plane can drain it.
1591 * @param horizontal When true, selects Right/Left. When false, selects Up/Down.
1592 * @param delta Signed wheel delta from the message.
1593 * @param capture_state Atomic capture state sampled when the callback frame began.
1594 * @return true when the frame snapshots consume intent for this message's direction.
1595 */
1596 1219 [[nodiscard]] bool wheel_count_admission(bool horizontal, int delta, std::uint64_t capture_state) noexcept
1597 {
1598
3/4
✓ Branch 2 → 3 taken 1219 times.
✗ Branch 2 → 4 not taken.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 1217 times.
1219 if (delta == 0 || (capture_state & WHEEL_CAPTURE_ENABLED) == 0)
1599 {
1600 2 return false;
1601 }
1602 1217 const WheelPhaseGuard phase;
1603 #if defined(DMK_ENABLE_TEST_SEAMS)
1604
2/2
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 1214 times.
1217 if (const WheelCaptureEntrySeam seam = s_wheel_capture_entry_seam.load(std::memory_order_acquire);
1605 seam != nullptr)
1606 {
1607 3 seam();
1608 }
1609 #endif
1610 // Re-read the capture state inside the counted phase, so an epoch advanced by a concurrent control
1611 // transaction refuses this admission.
1612 1217 const std::uint64_t epoch = wheel_capture_epoch(capture_state);
1613 1217 const std::uint64_t recheck = s_wheel_capture_state.load(std::memory_order_seq_cst);
1614
5/6
✓ Branch 18 → 19 taken 1214 times.
✓ Branch 18 → 22 taken 3 times.
✓ Branch 19 → 20 taken 1214 times.
✗ Branch 19 → 22 not taken.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 26 taken 1213 times.
2431 if (wheel_capture_epoch(recheck) != epoch || (recheck & WHEEL_CAPTURE_ENABLED) == 0 ||
1615
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 1213 times.
1214 !wheel_focus_admits())
1616 {
1617 4 return false;
1618 }
1619 1213 const bool owned = wheel_direction_consumed(wheel_delta_direction_bit(horizontal, delta));
1620 const WheelFold fold =
1621
2/2
✓ Branch 28 → 29 taken 8 times.
✓ Branch 28 → 30 taken 1205 times.
1213 accumulate_wheel_remainder(s_wheel_remainder[horizontal ? 1 : 0], epoch, owned, delta);
1622
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 1213 times.
1213 if (!fold.admitted)
1623 {
1624 ✗ return false;
1625 }
1626
2/2
✓ Branch 35 → 36 taken 1189 times.
✓ Branch 35 → 41 taken 24 times.
1213 if (fold.notches > 0)
1627 {
1628
2/2
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 38 taken 1187 times.
1189 const std::size_t positive_dir = horizontal ? 3u : 0u;
1629 1189 (void)bump_wheel_notch(s_wheel_count[positive_dir], epoch, static_cast<std::uint64_t>(fold.notches));
1630 }
1631
2/2
✓ Branch 41 → 42 taken 11 times.
✓ Branch 41 → 47 taken 13 times.
24 else if (fold.notches < 0)
1632 {
1633
2/2
✓ Branch 42 → 43 taken 5 times.
✓ Branch 42 → 44 taken 6 times.
11 const std::size_t negative_dir = horizontal ? 2u : 1u;
1634 11 (void)bump_wheel_notch(s_wheel_count[negative_dir], epoch, static_cast<std::uint64_t>(-fold.notches));
1635 }
1636 1213 return owned;
1637 1217 }
1638
1639 /**
1640 * @brief Wheel consume finalization: revalidates a saved consume intent after CallNextHookEx returned.
1641 * @details The WM_NULL write is permitted only while the entry epoch, the consume mask, its TTL, and the
1642 * focus gate all remain current. The admitted phase is counted so the control plane can drain it.
1643 * @return true when the caller must rewrite the message to WM_NULL.
1644 */
1645 16 [[nodiscard]] bool wheel_consume_finalization(bool horizontal, int delta, std::uint64_t capture_state) noexcept
1646 {
1647 16 const WheelPhaseGuard phase;
1648 #if defined(DMK_ENABLE_TEST_SEAMS)
1649
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 15 times.
16 if (const WheelFinalizeEntrySeam seam = s_wheel_finalize_entry_seam.load(std::memory_order_acquire);
1650 seam != nullptr)
1651 {
1652 1 seam();
1653 }
1654 #endif
1655 16 const std::uint64_t epoch = wheel_capture_epoch(capture_state);
1656 16 const std::uint64_t recheck = s_wheel_capture_state.load(std::memory_order_seq_cst);
1657
4/6
✓ Branch 15 → 16 taken 16 times.
✗ Branch 15 → 19 not taken.
✓ Branch 16 → 17 taken 16 times.
✗ Branch 16 → 19 not taken.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 15 times.
32 if (wheel_capture_epoch(recheck) != epoch || (recheck & WHEEL_CAPTURE_ENABLED) == 0 ||
1658
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 15 times.
16 !wheel_focus_admits())
1659 {
1660 1 return false;
1661 }
1662 15 return wheel_direction_consumed(wheel_delta_direction_bit(horizontal, delta));
1663 16 }
1664
1665 /**
1666 * @brief Computes the returned buttons from the current state and this copy's consume rules.
1667 * @details dwPacketNumber and the success return stay untouched, so the game sees a connected controller with
1668 * packet progress. The cleared bits combine the reactive mask with the consume rules. A TTL guard
1669 * drops all suppression after poll refreshes stop.
1670 */
1671 [[nodiscard]] WORD
1672 506 suppressed_xinput_buttons(const XINPUT_STATE *state, DWORD user_index, WORD original_buttons) noexcept
1673 {
1674 // A retained primary route can remain physically reachable before recovery completes for its Ex partner.
1675 // Keep both routes fail-open until the complete logical installation is published.
1676
1/2
✓ Branch 5 → 6 taken 506 times.
✗ Branch 5 → 9 not taken.
1012 if (is_xinput_raw_call() || !xinput_consume_context_healthy() ||
1677
6/8
✓ Branch 3 → 4 taken 506 times.
✗ Branch 3 → 9 not taken.
✓ Branch 7 → 8 taken 482 times.
✓ Branch 7 → 9 taken 24 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 482 times.
✓ Branch 11 → 12 taken 24 times.
✓ Branch 11 → 16 taken 482 times.
1012 !s_xinput_installed.load(std::memory_order_acquire) || state == nullptr)
1678 {
1679
1/2
✓ Branch 12 → 13 taken 24 times.
✗ Branch 12 → 14 not taken.
24 return state != nullptr ? state->Gamepad.wButtons : 0;
1680 }
1681
5/6
✓ Branch 16 → 17 taken 482 times.
✗ Branch 16 → 25 not taken.
✓ Branch 24 → 25 taken 154 times.
✓ Branch 24 → 26 taken 328 times.
✓ Branch 27 → 28 taken 154 times.
✓ Branch 27 → 29 taken 328 times.
964 if (user_index > 3 || static_cast<int>(user_index) != s_bound_user_index.load(std::memory_order_relaxed))
1682 {
1683 154 return state->Gamepad.wButtons;
1684 }
1685 // The acquire load of the mask orders the relaxed deadline read below (the writer stores the deadline
1686 // first), even when the mask reads as 0.
1687 328 const uint16_t reactive = s_suppress_mask.load(std::memory_order_acquire);
1688
1689 // Original-button context preserves modifiers that another compatible copy already consumed.
1690 328 const uint16_t rule_mask = s_rule_suppress_enabled.load(std::memory_order_relaxed)
1691 232 ? evaluate_published_consume_rules(original_buttons)
1692
2/2
✓ Branch 37 → 38 taken 232 times.
✓ Branch 37 → 39 taken 96 times.
328 : 0;
1693 328 const uint16_t mask = static_cast<uint16_t>(reactive | rule_mask);
1694
2/2
✓ Branch 40 → 41 taken 22 times.
✓ Branch 40 → 42 taken 306 times.
328 if (mask == 0)
1695 {
1696 22 return state->Gamepad.wButtons;
1697 }
1698 // A stalled poll thread lets the deadline lapse and stops all suppression. The game regains its input
1699 // instead of a permanent latch.
1700
1/2
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 306 times.
612 if (GetTickCount64() >= s_suppress_deadline_ms.load(std::memory_order_relaxed))
1701 {
1702 ✗ return state->Gamepad.wButtons;
1703 }
1704 306 return static_cast<WORD>(state->Gamepad.wButtons & static_cast<WORD>(~mask));
1705 }
1706
1707 /** @brief Applies this copy's mask only after a successful original-button context restore. */
1708 16818 void finish_xinput_consume_scope(
1709 std::optional<XInputConsumeScope> &scope,
1710 XINPUT_STATE *state,
1711 DWORD user_index,
1712 DWORD result
1713 ) noexcept
1714 {
1715
2/2
✓ Branch 3 → 4 taken 9576 times.
✓ Branch 3 → 5 taken 7137 times.
16818 if (!scope)
1716 16242 return;
1717 7137 WORD original_buttons = 0;
1718
6/8
✓ Branch 5 → 6 taken 495 times.
✓ Branch 5 → 14 taken 6642 times.
✓ Branch 6 → 7 taken 495 times.
✗ Branch 6 → 14 not taken.
✓ Branch 9 → 10 taken 495 times.
✗ Branch 9 → 14 not taken.
✓ Branch 12 → 13 taken 494 times.
✓ Branch 12 → 14 taken 9 times.
7640 const bool sampled = result == ERROR_SUCCESS && state != nullptr && scope->admitted() &&
1719 495 scope->original_buttons(state->Gamepad.wButtons, original_buttons);
1720
2/2
✓ Branch 15 → 16 taken 6651 times.
✓ Branch 15 → 19 taken 494 times.
7145 if (!sampled)
1721 {
1722 6651 (void)scope->finish();
1723 6666 return;
1724 }
1725 494 const WORD returned_buttons = suppressed_xinput_buttons(state, user_index, original_buttons);
1726
3/6
✓ Branch 22 → 23 taken 494 times.
✗ Branch 22 → 26 not taken.
✓ Branch 24 → 25 taken 494 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 494 times.
✗ Branch 27 → 29 not taken.
494 if (scope->finish(returned_buttons) && xinput_consume_context_healthy())
1727 494 state->Gamepad.wButtons = returned_buttons;
1728 }
1729
1730 13419 DWORD WINAPI xinput_get_state_detour(DWORD user_index, XINPUT_STATE *state) noexcept
1731 {
1732 13419 const InflightGuard inflight;
1733 13452 std::optional<XInputConsumeScope> consume_scope;
1734
5/6
✓ Branch 3 → 4 taken 13469 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 6898 times.
✓ Branch 5 → 7 taken 6428 times.
✓ Branch 8 → 9 taken 6898 times.
✓ Branch 8 → 10 taken 6411 times.
13452 if (state != nullptr && !is_xinput_raw_call())
1735 6898 consume_scope.emplace(user_index, state);
1736 39980 xinput_route_probe_record({
1737 13329 .owner = s_intercept_owner.load(std::memory_order_acquire),
1738 13319 .hook_epoch = s_xinput_hook_epoch.load(std::memory_order_acquire),
1739 .member = XInputRouteMember::Primary,
1740 .user_index = user_index,
1741 .state_address = reinterpret_cast<std::uintptr_t>(state),
1742 });
1743 #if defined(DMK_ENABLE_TEST_SEAMS)
1744
2/2
✓ Branch 26 → 27 taken 28 times.
✓ Branch 26 → 28 taken 13321 times.
13332 if (auto *seam = s_xinput_detour_body_seam.load(std::memory_order_acquire))
1745 {
1746 28 seam();
1747 }
1748 #endif
1749 // This seq_cst load forms the detour side of the Dekker drain pair. See InflightGuard.
1750 13349 const XInputGetStateFn original = s_xinput_original.load(std::memory_order_seq_cst);
1751
1/2
✓ Branch 29 → 30 taken 13298 times.
✗ Branch 29 → 32 not taken.
13298 const DWORD result = (original != nullptr) ? original(user_index, state) : ERROR_DEVICE_NOT_CONNECTED;
1752 13383 finish_xinput_consume_scope(consume_scope, state, user_index, result);
1753 26772 return result;
1754 13317 }
1755
1756 3486 DWORD WINAPI xinput_get_state_ex_detour(DWORD user_index, XINPUT_STATE *state) noexcept
1757 {
1758 3486 const InflightGuard inflight;
1759 3509 std::optional<XInputConsumeScope> consume_scope;
1760
5/6
✓ Branch 3 → 4 taken 3515 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 247 times.
✓ Branch 5 → 7 taken 3231 times.
✓ Branch 8 → 9 taken 247 times.
✓ Branch 8 → 10 taken 3225 times.
3509 if (state != nullptr && !is_xinput_raw_call())
1761 247 consume_scope.emplace(user_index, state);
1762 10406 xinput_route_probe_record({
1763 3467 .owner = s_intercept_owner.load(std::memory_order_acquire),
1764 3472 .hook_epoch = s_xinput_hook_epoch.load(std::memory_order_acquire),
1765 .member = XInputRouteMember::Ex,
1766 .user_index = user_index,
1767 .state_address = reinterpret_cast<std::uintptr_t>(state),
1768 });
1769 // This seq_cst load serves the same Dekker-pair reason as xinput_get_state_detour above.
1770 3467 const XInputGetStateFn original = s_xinput_ex_original.load(std::memory_order_seq_cst);
1771
1/2
✓ Branch 26 → 27 taken 3471 times.
✗ Branch 26 → 29 not taken.
3471 const DWORD result = (original != nullptr) ? original(user_index, state) : ERROR_DEVICE_NOT_CONNECTED;
1772 3466 finish_xinput_consume_scope(consume_scope, state, user_index, result);
1773 6867 return result;
1774 3406 }
1775
1776 // The local wheel source is a thread-scoped WH_GETMESSAGE hook on the selected game UI thread. It folds and
1777 // counts on PM_REMOVE only, at most once per retrieval, and a PM_NOREMOVE peek passes through uncounted. Count
1778 // admission runs before CallNextHookEx with no message mutation, so older hooks see the original record.
1779 // CallNextHookEx runs exactly once, and consume finalization writes WM_NULL after it returns, only while every
1780 // admission condition still holds. A newer hook can rewrite the message after this hook returns. The consume
1781 // stays best effort.
1782 1192 LRESULT CALLBACK message_hook_proc(int code, WPARAM wparam, LPARAM lparam) noexcept
1783 {
1784
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 1192 times.
1192 if (code != HC_ACTION)
1785 {
1786 ✗ return CallNextHookEx(nullptr, code, wparam, lparam);
1787 }
1788 1192 MSG *message = reinterpret_cast<MSG *>(lparam);
1789
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 1192 times.
1192 if (message == nullptr)
1790 {
1791 ✗ return CallNextHookEx(nullptr, code, wparam, lparam);
1792 }
1793
3/4
✓ Branch 8 → 9 taken 5 times.
✓ Branch 8 → 10 taken 1187 times.
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 11 not taken.
1192 const bool is_wheel = message->message == WM_MOUSEWHEEL || message->message == WM_MOUSEHWHEEL;
1794
3/4
✓ Branch 12 → 13 taken 1192 times.
✗ Branch 12 → 14 not taken.
✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 16 taken 1187 times.
1192 if (!is_wheel || wparam != PM_REMOVE)
1795 {
1796 5 return CallNextHookEx(nullptr, code, wparam, lparam);
1797 }
1798 // Reject a callback whose current thread does not match the published route. A reused numeric id cannot
1799 // revive retired state: the route id is republished only by a mount transaction.
1800
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 1187 times.
2374 if (GetCurrentThreadId() != s_msg_hook_thread_id.load(std::memory_order_acquire))
1801 {
1802 ✗ return CallNextHookEx(nullptr, code, wparam, lparam);
1803 }
1804 1187 const std::uint64_t capture_state = s_wheel_capture_state.load(std::memory_order_seq_cst);
1805 1187 const bool horizontal = message->message == WM_MOUSEHWHEEL;
1806 1187 const int delta = GET_WHEEL_DELTA_WPARAM(message->wParam);
1807 1187 const bool consume_intent = wheel_count_admission(horizontal, delta, capture_state);
1808 1187 const LRESULT result = CallNextHookEx(nullptr, code, wparam, lparam);
1809
6/6
✓ Branch 36 → 37 taken 12 times.
✓ Branch 36 → 40 taken 1175 times.
✓ Branch 38 → 39 taken 11 times.
✓ Branch 38 → 40 taken 1 time.
✓ Branch 41 → 42 taken 11 times.
✓ Branch 41 → 43 taken 1176 times.
1187 if (consume_intent && wheel_consume_finalization(horizontal, delta, capture_state))
1810 {
1811 11 message->message = WM_NULL;
1812 11 message->wParam = 0;
1813 11 message->lParam = 0;
1814 }
1815 1187 return result;
1816 }
1817
1818 /// Requires s_intercept_mutex. Reports whether the mounted route's target thread exited.
1819 91 [[nodiscard]] bool msg_hook_target_thread_exited_locked() noexcept
1820 {
1821
3/4
✓ Branch 2 → 3 taken 91 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 86 times.
91 return s_msg_hook_thread != nullptr && WaitForSingleObject(s_msg_hook_thread, 0) == WAIT_OBJECT_0;
1822 }
1823
1824 /**
1825 * @brief Removes an OS hook, tolerating a handle the OS already reclaimed.
1826 * @details The poll thread installs the wheel hook, so the OS retires the thread-owned hook when that thread
1827 * exits. A later removal on the control thread then fails with an invalid-handle error, which is a
1828 * successful removal, not a live-thread cleanup failure. A genuine failure keeps the caller blocked.
1829 * @return true when the hook is gone (removed now, or already reclaimed).
1830 */
1831 31 [[nodiscard]] bool unhook_or_already_gone(HHOOK hook) noexcept
1832 {
1833 #if defined(DMK_ENABLE_TEST_SEAMS)
1834
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 29 times.
31 if (s_force_message_unhook_failure.load(std::memory_order_acquire))
1835 {
1836 2 SetLastError(0);
1837 }
1838 else
1839 #endif
1840 {
1841 29 SetLastError(0);
1842
2/2
✓ Branch 7 → 8 taken 21 times.
✓ Branch 7 → 9 taken 8 times.
29 if (UnhookWindowsHookEx(hook) != 0)
1843 {
1844 21 return true;
1845 }
1846 }
1847 10 const DWORD error = GetLastError();
1848 10 return error == ERROR_INVALID_HOOK_HANDLE;
1849 }
1850
1851 /**
1852 * @brief Requires s_intercept_mutex. Drops the mounted route after its target thread exited.
1853 * @details Thread exit is authoritative hook retirement: the OS hook died with its thread, so the unhook
1854 * call only releases the handle and its result carries no authority.
1855 */
1856 5 void retire_message_hook_route_locked() noexcept
1857 {
1858
1/2
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 6 not taken.
5 if (const HHOOK hook = s_msg_hook.load(std::memory_order_relaxed); hook != nullptr)
1859 {
1860 5 (void)UnhookWindowsHookEx(hook);
1861 5 s_msg_hook.store(nullptr, std::memory_order_release);
1862 }
1863 s_msg_hook_thread_id.store(0, std::memory_order_release);
1864
1/2
✓ Branch 14 → 15 taken 5 times.
✗ Branch 14 → 17 not taken.
5 if (s_msg_hook_thread != nullptr)
1865 {
1866 5 CloseHandle(s_msg_hook_thread);
1867 5 s_msg_hook_thread = nullptr;
1868 }
1869 5 const std::uint64_t wheel_epoch = close_wheel_capture_and_advance_epoch();
1870 {
1871 5 const DataPlaneLockGuard data_lock;
1872 5 reset_wheel_data_plane(wheel_epoch);
1873 5 }
1874 s_msg_hook_route_state.store(
1875 static_cast<std::uint8_t>(WheelRouteState::Retryable),
1876 std::memory_order_release
1877 );
1878 5 }
1879
1880 /**
1881 * @brief Requires s_intercept_mutex. Rechecks target liveness and settles the route state.
1882 * @details A dead target cannot remain ready, and a cleanup-blocked route whose old thread exited becomes
1883 * retryable.
1884 */
1885 9867 void settle_message_hook_route_locked() noexcept
1886 {
1887
6/6
✓ Branch 3 → 4 taken 60 times.
✓ Branch 3 → 7 taken 9807 times.
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 7 taken 55 times.
✓ Branch 8 → 9 taken 5 times.
✓ Branch 8 → 10 taken 9862 times.
9867 if (s_msg_hook.load(std::memory_order_relaxed) != nullptr && msg_hook_target_thread_exited_locked())
1888 {
1889 5 retire_message_hook_route_locked();
1890 }
1891 9867 }
1892
1893 290 void uninstall_message_hook() noexcept
1894 {
1895 290 const HHOOK hook = s_msg_hook.load(std::memory_order_acquire);
1896
2/2
✓ Branch 3 → 4 taken 263 times.
✓ Branch 3 → 5 taken 27 times.
290 if (hook == nullptr)
1897 {
1898 263 return;
1899 }
1900 // The caller's revocation already advanced the epoch, so admitted decisions are stale; the bounded
1901 // drain only waits out the short admission windows. The phase atomics are process-lifetime statics, so
1902 // a drain timeout is a correctness residual, not a memory hazard, and teardown still proceeds.
1903 27 (void)drain_wheel_admitted_phases();
1904
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 27 times.
27 if (msg_hook_target_thread_exited_locked())
1905 {
1906 ✗ retire_message_hook_route_locked();
1907 }
1908
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 20 taken 26 times.
27 else if (!unhook_or_already_gone(hook))
1909 {
1910 // Cleanup only. The permanent module reference is retained either way, because Microsoft permits a
1911 // selected callback to run after UnhookWindowsHookEx returns.
1912 s_msg_hook_route_state.store(
1913 static_cast<std::uint8_t>(WheelRouteState::CleanupBlocked),
1914 std::memory_order_release
1915 );
1916 1 return;
1917 }
1918 else
1919 {
1920 26 s_msg_hook.store(nullptr, std::memory_order_release);
1921 s_msg_hook_thread_id.store(0, std::memory_order_release);
1922
1/2
✓ Branch 29 → 30 taken 26 times.
✗ Branch 29 → 32 not taken.
26 if (s_msg_hook_thread != nullptr)
1923 {
1924 26 CloseHandle(s_msg_hook_thread);
1925 26 s_msg_hook_thread = nullptr;
1926 }
1927 s_msg_hook_route_state.store(
1928 static_cast<std::uint8_t>(WheelRouteState::TargetWait),
1929 std::memory_order_release
1930 );
1931 }
1932 // The caller's revocation already advanced the capture epoch and reset the data plane at that epoch. A
1933 // second advance here would leave the count and remainder slots tagged with a stale epoch, so the next
1934 // owner's mount would fold nothing. Do not advance again.
1935 }
1936 } // anonymous namespace
1937
1938 1370 uint8_t step_wheel_pulse(WheelPulseState &state) noexcept
1939 {
1940 1370 uint8_t mask = 0;
1941
2/2
✓ Branch 13 → 3 taken 5480 times.
✓ Branch 13 → 14 taken 1370 times.
6850 for (int dir = 0; dir < 4; ++dir)
1942 {
1943
2/2
✓ Branch 4 → 5 taken 26 times.
✓ Branch 4 → 7 taken 5454 times.
5480 if (state.pulsing[dir])
1944 {
1945 // Force one low cycle after a pulse so the edge detector re-arms.
1946 26 state.pulsing[dir] = false;
1947 }
1948
2/2
✓ Branch 8 → 9 taken 33 times.
✓ Branch 8 → 12 taken 5421 times.
5454 else if (state.pending[dir] > 0)
1949 {
1950 33 --state.pending[dir];
1951 33 mask = static_cast<uint8_t>(mask | (1u << dir));
1952 33 state.pulsing[dir] = true;
1953 }
1954 }
1955 1370 return mask;
1956 }
1957
1958 1431 void add_wheel_notches(WheelPulseState &state, const std::array<int, 4> &taken) noexcept
1959 {
1960
2/2
✓ Branch 15 → 3 taken 5724 times.
✓ Branch 15 → 16 taken 1431 times.
7155 for (size_t dir = 0; dir < 4; ++dir)
1961 {
1962
2/2
✓ Branch 4 → 5 taken 412 times.
✓ Branch 4 → 7 taken 5312 times.
5724 const int add = taken[dir] > 0 ? taken[dir] : 0;
1963 // pending is in [0, MAX_WHEEL_PENDING] by induction, so room is nonnegative. Compare against room before
1964 // the addition so a large burst saturates instead of an int overflow.
1965 5724 const int room = MAX_WHEEL_PENDING - state.pending[dir];
1966
2/2
✓ Branch 9 → 10 taken 5322 times.
✓ Branch 9 → 12 taken 402 times.
5724 state.pending[dir] = (add >= room) ? MAX_WHEEL_PENDING : state.pending[dir] + add;
1967 }
1968 1431 }
1969
1970 1051 uint16_t step_gamepad_suppress(
1971 GamepadSuppressState &state,
1972 uint16_t owned_now,
1973 uint16_t true_buttons,
1974 uint64_t now_ms,
1975 uint64_t grace_ms
1976 ) noexcept
1977 {
1978 // Sentinel deadline denotes "actively held, with no release underway."
1979 1051 constexpr uint64_t held_sentinel = UINT64_MAX;
1980
1981 1051 uint16_t mask = 0;
1982 1051 const uint16_t relevant = static_cast<uint16_t>(state.armed | owned_now);
1983
2/2
✓ Branch 21 → 3 taken 16816 times.
✓ Branch 21 → 22 taken 1051 times.
17867 for (int bit = 0; bit < 16; ++bit)
1984 {
1985 16816 const uint16_t bit_mask = static_cast<uint16_t>(1u << bit);
1986
2/2
✓ Branch 3 → 4 taken 15767 times.
✓ Branch 3 → 5 taken 1049 times.
16816 if ((relevant & bit_mask) == 0)
1987 {
1988 15767 continue;
1989 }
1990 1049 const bool phys_down = (true_buttons & bit_mask) != 0;
1991 1049 const bool owned = (owned_now & bit_mask) != 0;
1992
1993
5/6
✓ Branch 5 → 6 taken 38 times.
✓ Branch 5 → 8 taken 1011 times.
✓ Branch 6 → 7 taken 38 times.
✗ Branch 6 → 10 not taken.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 10 taken 36 times.
1049 if (owned || ((state.armed & bit_mask) != 0 && phys_down))
1994 {
1995 // A current chord or a still-held trigger keeps suppression active after modifier release. Cancel any
1996 // active release grace.
1997 1013 state.armed = static_cast<uint16_t>(state.armed | bit_mask);
1998 1013 state.deadline_ms[static_cast<size_t>(bit)] = held_sentinel;
1999 1013 mask = static_cast<uint16_t>(mask | bit_mask);
2000 }
2001
1/2
✓ Branch 10 → 11 taken 36 times.
✗ Branch 10 → 20 not taken.
36 else if ((state.armed & bit_mask) != 0)
2002 {
2003 // If the armed button is physically up, run the release grace so a final bare-trigger frame cannot
2004 // reach the game.
2005
2/2
✓ Branch 12 → 13 taken 22 times.
✓ Branch 12 → 15 taken 14 times.
36 if (state.deadline_ms[static_cast<size_t>(bit)] == held_sentinel)
2006 {
2007 22 state.deadline_ms[static_cast<size_t>(bit)] = now_ms + grace_ms;
2008 }
2009
2/2
✓ Branch 16 → 17 taken 33 times.
✓ Branch 16 → 18 taken 3 times.
36 if (now_ms < state.deadline_ms[static_cast<size_t>(bit)])
2010 {
2011 33 mask = static_cast<uint16_t>(mask | bit_mask);
2012 }
2013 else
2014 {
2015 3 state.armed = static_cast<uint16_t>(state.armed & static_cast<uint16_t>(~bit_mask));
2016 3 state.deadline_ms[static_cast<size_t>(bit)] = 0;
2017 }
2018 }
2019 }
2020 1051 return mask;
2021 }
2022
2023 296 uint16_t evaluate_consume_rules(uint16_t true_buttons, const GamepadConsumeRule *rules, std::size_t count) noexcept
2024 {
2025 296 uint16_t mask = 0;
2026
2/2
✓ Branch 7 → 3 taken 486 times.
✓ Branch 7 → 8 taken 296 times.
782 for (std::size_t i = 0; i < count; ++i)
2027 {
2028 486 const GamepadConsumeRule &rule = rules[i];
2029 // The rule requires every modifier bit and rejects every forbidden bit. This matches the poll loop's exact
2030 // decision against the snapshot that the game reads. A forbidden bit belongs to a different chord.
2031
4/4
✓ Branch 3 → 4 taken 340 times.
✓ Branch 3 → 6 taken 146 times.
✓ Branch 4 → 5 taken 332 times.
✓ Branch 4 → 6 taken 8 times.
486 if ((true_buttons & rule.modifier_mask) == rule.modifier_mask && (true_buttons & rule.forbidden_mask) == 0)
2032 {
2033 332 mask = static_cast<uint16_t>(mask | rule.trigger_mask);
2034 }
2035 }
2036 296 return mask;
2037 }
2038
2039 ConsumePublish
2040 4179 publish_gamepad_consume_rules(const GamepadConsumeRule *rules, std::size_t count, std::uint64_t owner) noexcept
2041 {
2042 4179 run_data_plane_entry_seam();
2043 4179 const DataPlaneLockGuard data_lock;
2044
2/2
✓ Branch 5 → 6 taken 3985 times.
✓ Branch 5 → 7 taken 194 times.
4179 if (!data_plane_authorized(owner))
2045 {
2046 // Refuse before the seqlock bracket opens, so an unauthorized caller leaves the sequence untouched and
2047 // even.
2048 3985 return {};
2049 }
2050
2051 // Keep the rules that fit and drop the rest. Each retained rule protects its chord. An empty list revokes all
2052 // initial-edge protection because one rule did not fit.
2053
2/2
✓ Branch 7 → 8 taken 189 times.
✓ Branch 7 → 9 taken 5 times.
194 const std::size_t published = count < MAX_GAMEPAD_CONSUME_RULES ? count : MAX_GAMEPAD_CONSUME_RULES;
2054 // This writer brackets the update with an odd sequence. The release fence keeps rule stores inside that
2055 // bracket. The even release store publishes the final list.
2056 194 const uint32_t seq = s_consume_rules_seq.load(std::memory_order_relaxed);
2057 194 s_consume_rules_seq.store(seq + 1, std::memory_order_relaxed);
2058 std::atomic_thread_fence(std::memory_order_release);
2059
2/2
✓ Branch 38 → 27 taken 271 times.
✓ Branch 38 → 39 taken 194 times.
465 for (std::size_t i = 0; i < published; ++i)
2060 {
2061 271 s_consume_rules[i].store(pack_consume_rule(rules[i]), std::memory_order_relaxed);
2062 }
2063 194 s_consume_rule_count.store(static_cast<uint32_t>(published), std::memory_order_relaxed);
2064 194 s_consume_rules_seq.store(seq + 2, std::memory_order_release);
2065 194 return {true, published};
2066 4179 }
2067
2068 280 uint16_t evaluate_published_consume_rules(uint16_t true_buttons) noexcept
2069 {
2070 // Seqlock read with one attempt and no spin. On an odd sequence or torn snapshot, skip rule suppression for
2071 // this frame. The reactive mask still applies. The next game poll gets the settled list.
2072 280 const uint32_t seq_before = s_consume_rules_seq.load(std::memory_order_acquire);
2073
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 280 times.
280 if ((seq_before & 1u) != 0)
2074 {
2075 ✗ return 0;
2076 }
2077 280 uint32_t count = s_consume_rule_count.load(std::memory_order_relaxed);
2078
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 280 times.
280 if (count > MAX_GAMEPAD_CONSUME_RULES)
2079 {
2080 ✗ count = MAX_GAMEPAD_CONSUME_RULES;
2081 }
2082 280 std::array<GamepadConsumeRule, MAX_GAMEPAD_CONSUME_RULES> snapshot{};
2083
2/2
✓ Branch 32 → 21 taken 463 times.
✓ Branch 32 → 33 taken 280 times.
743 for (uint32_t i = 0; i < count; ++i)
2084 {
2085 926 snapshot[i] = unpack_consume_rule(s_consume_rules[i].load(std::memory_order_relaxed));
2086 }
2087 // Order the rule loads before the sequence re-read, so a mid-copy writer is always detected.
2088 std::atomic_thread_fence(std::memory_order_acquire);
2089
1/2
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 280 times.
280 if (s_consume_rules_seq.load(std::memory_order_relaxed) != seq_before)
2090 {
2091 ✗ return 0;
2092 }
2093 560 return evaluate_consume_rules(true_buttons, snapshot.data(), count);
2094 }
2095
2096 1050 bool set_gamepad_rule_suppress_enabled(bool enabled, std::uint64_t owner) noexcept
2097 {
2098 1050 run_data_plane_entry_seam();
2099 1050 const DataPlaneLockGuard data_lock;
2100
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 1049 times.
1050 if (!data_plane_authorized(owner))
2101 {
2102 1 return false;
2103 }
2104 1049 s_rule_suppress_enabled.store(enabled, std::memory_order_relaxed);
2105 1049 return true;
2106 1050 }
2107
2108 615 std::uint64_t next_intercept_owner() noexcept
2109 {
2110 for (;;)
2111 {
2112 615 const std::uint64_t owner = s_next_intercept_owner.fetch_add(1, std::memory_order_relaxed);
2113
2/4
✓ Branch 5 → 6 taken 615 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 615 times.
✗ Branch 6 → 8 not taken.
615 if (owner != 0 && owner != STANDALONE_INTERCEPT_OWNER)
2114 {
2115 615 return owner;
2116 }
2117 ✗ }
2118 }
2119
2120 1733 bool intercept_owned_by(std::uint64_t owner) noexcept
2121 {
2122
4/4
✓ Branch 2 → 3 taken 1732 times.
✓ Branch 2 → 12 taken 1 time.
✓ Branch 10 → 11 taken 99 times.
✓ Branch 10 → 12 taken 1633 times.
3465 return owner != 0 && s_intercept_owner.load(std::memory_order_acquire) == owner;
2123 }
2124
2125 #if defined(DMK_ENABLE_TEST_SEAMS)
2126 51 bool acquire_standalone_lease_for_test() noexcept
2127 {
2128 51 const InterceptLockGuard lock{s_intercept_mutex};
2129
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 51 times.
51 if (!owner_available(STANDALONE_INTERCEPT_OWNER))
2130 {
2131 ✗ return false;
2132 }
2133 51 publish_owner(STANDALONE_INTERCEPT_OWNER);
2134 51 return true;
2135 51 }
2136
2137 38 bool xinput_pair_degraded_for_test() noexcept
2138 {
2139 38 return s_xinput_pair_degraded.load(std::memory_order_acquire);
2140 }
2141
2142 114 XInputPairCoverage xinput_pair_coverage_for_test() noexcept
2143 {
2144 114 const InterceptLockGuard lock{s_intercept_mutex};
2145
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 114 times.
114 if (s_xinput_permanent_hooks == nullptr)
2146 {
2147 ✗ return XInputPairCoverage{false, true};
2148 }
2149 return XInputPairCoverage{
2150 114 xinput_member_entry_witnessed(s_xinput_permanent_hooks->primary, true),
2151 114 xinput_member_entry_witnessed(s_xinput_permanent_hooks->ex, static_cast<bool>(s_xinput_permanent_hooks->ex))
2152 228 };
2153 114 }
2154
2155 95 XInputPairCoverage xinput_pair_observation_for_test() noexcept
2156 {
2157 95 const InterceptLockGuard lock{s_intercept_mutex};
2158 190 return XInputPairCoverage{s_xinput_primary_observed, s_xinput_ex_observed};
2159 95 }
2160
2161 9 std::size_t xinput_recovery_attempts_for_test() noexcept
2162 {
2163 9 return s_xinput_recovery_attempts.load(std::memory_order_relaxed);
2164 }
2165
2166 11 std::uint64_t expire_xinput_recovery_delay_for_test() noexcept
2167 {
2168 11 const InterceptLockGuard lock{s_intercept_mutex};
2169 11 const std::uint64_t delay_ms = s_xinput_recovery_delay_ms;
2170 11 s_xinput_recovery_not_before_ms = 0;
2171 22 return delay_ms;
2172 11 }
2173
2174 53 bool adopt_owner_for_test(std::uint64_t owner) noexcept
2175 {
2176 53 const InterceptLockGuard lock{s_intercept_mutex};
2177
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 53 times.
53 if (!owner_available(owner))
2178 {
2179 ✗ return false;
2180 }
2181 53 publish_owner(owner);
2182 53 return true;
2183 53 }
2184 51 void release_standalone_lease_for_test() noexcept
2185 {
2186 51 const InterceptLockGuard lock{s_intercept_mutex};
2187
2/2
✓ Branch 10 → 11 taken 18 times.
✓ Branch 10 → 12 taken 33 times.
51 if (s_intercept_owner.load(std::memory_order_relaxed) != STANDALONE_INTERCEPT_OWNER)
2188 {
2189 18 return;
2190 }
2191 33 revoke_owner_and_clear_data();
2192
2/2
✓ Branch 15 → 16 taken 33 times.
✓ Branch 15 → 18 taken 18 times.
51 }
2193 #endif
2194
2195 namespace
2196 {
2197 /**
2198 * @brief Runs the maintenance transaction over the permanently retained hook pair.
2199 * @note Requires s_intercept_mutex and a constructed process-lifetime cell.
2200 */
2201 74 [[nodiscard]] bool maintain_retained_xinput_pair(int user_index, std::uint64_t owner) noexcept
2202 {
2203 74 PermanentXInputHooks *const permanent = permanent_cell();
2204
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 74 times.
74 if (!permanent->primary)
2205 {
2206 // No primary storage remains. A fresh hook over the current export creates the uncertain-storage case
2207 // that retention avoids.
2208 ✗ return false;
2209 }
2210 // Recovery re-arms each absent member through its retained hook. It never creates a second hook over the
2211 // current prologue.
2212 74 const bool whole = maintain_xinput_pair(
2213 74 permanent->primary,
2214 74 permanent->ex,
2215
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 74 times.
74 permanent->ex_target_ref != nullptr ? permanent->ex_target_ref : permanent->target_ref,
2216 user_index,
2217 owner
2218 );
2219 74 return whole;
2220 }
2221
2222 /**
2223 * @brief Runs the maintenance transaction over the live pair.
2224 * @note Requires s_intercept_mutex and a live primary member.
2225 */
2226 1586 [[nodiscard]] bool maintain_live_xinput_pair(int user_index, std::uint64_t owner) noexcept
2227 {
2228 4758 return maintain_xinput_pair(
2229 1586 s_xinput_permanent_hooks->primary,
2230 1586 s_xinput_permanent_hooks->ex,
2231
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1586 times.
1586 s_xinput_permanent_hooks->ex_target_ref != nullptr ? s_xinput_permanent_hooks->ex_target_ref
2232 1586 : s_xinput_permanent_hooks->target_ref,
2233 user_index,
2234 owner
2235 1586 );
2236 }
2237
2238 /**
2239 * @brief Returns the loaded XInput module, or nullptr if no XInput DLL is loaded.
2240 * @note Requires s_intercept_mutex.
2241 */
2242 826 [[nodiscard]] HMODULE find_loaded_xinput_module() noexcept
2243 {
2244 826 HMODULE module = nullptr;
2245 #if defined(DMK_ENABLE_TEST_SEAMS)
2246
2/2
✓ Branch 2 → 3 taken 770 times.
✓ Branch 2 → 4 taken 56 times.
826 if (s_xinput_module_override != nullptr)
2247 {
2248 770 module = s_xinput_module_override;
2249 }
2250 else
2251 #endif
2252 {
2253
1/2
✓ Branch 9 → 5 taken 56 times.
✗ Branch 9 → 10 not taken.
56 for (const wchar_t *name : XINPUT_DLL_NAMES)
2254 {
2255 56 module = GetModuleHandleW(name);
2256
1/2
✓ Branch 6 → 7 taken 56 times.
✗ Branch 6 → 8 not taken.
56 if (module != nullptr)
2257 {
2258 56 break;
2259 }
2260 }
2261 }
2262 826 return module;
2263 }
2264
2265 /**
2266 * @brief Takes every keepalive that the hook pair needs.
2267 * @details The caller runs this before any prologue patch because the retention teardown has no allocator or
2268 * loader call. A refusal fails closed, and the poll loop retries.
2269 * @return False when a keepalive acquisition fails. A false return leaves no keepalive held.
2270 * @note Requires s_intercept_mutex and a constructed process-lifetime cell.
2271 */
2272 [[nodiscard]] bool
2273 212 acquire_xinput_keepalives(const void *get_state, const void *get_state_ex, bool ex_is_distinct_member) noexcept
2274 {
2275 424 s_xinput_permanent_hooks->self_ref =
2276 212 DetourModKit::detail::acquire_module_ref(diagnostics::ModulePinReason::XInputKeepalive);
2277
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 212 times.
212 if (s_xinput_permanent_hooks->self_ref == nullptr)
2278 {
2279 ✗ return false;
2280 }
2281 212 s_xinput_permanent_hooks->target_ref = acquire_module_ref_containing_address(get_state);
2282
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 9 taken 212 times.
212 if (s_xinput_permanent_hooks->target_ref == nullptr)
2283 {
2284 ✗ retire_xinput_module_refs();
2285 ✗ return false;
2286 }
2287
2/2
✓ Branch 9 → 10 taken 194 times.
✓ Branch 9 → 17 taken 18 times.
212 if (ex_is_distinct_member)
2288 {
2289 194 const HMODULE ex_target_ref = acquire_module_ref_containing_address(get_state_ex);
2290
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 194 times.
194 if (ex_target_ref == nullptr)
2291 {
2292 ✗ retire_xinput_module_refs();
2293 ✗ return false;
2294 }
2295
2/2
✓ Branch 14 → 15 taken 193 times.
✓ Branch 14 → 16 taken 1 time.
194 if (ex_target_ref == s_xinput_permanent_hooks->target_ref)
2296 {
2297 // The primary pin already covers this prologue. Balance the duplicate probe reference now.
2298 193 DetourModKit::detail::release_module_ref(ex_target_ref, diagnostics::ModulePinReason::XInputTarget);
2299 }
2300 else
2301 {
2302 1 s_xinput_permanent_hooks->ex_target_ref = ex_target_ref;
2303 }
2304 }
2305 212 return true;
2306 }
2307
2308 /**
2309 * @brief Warns once about the refused pair credit and releases the install keepalives.
2310 * @note Requires s_intercept_mutex.
2311 */
2312 1 void retire_xinput_install_after_credit_refusal() noexcept
2313 {
2314
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 8 not taken.
1 if (!s_xinput_capacity_warned.exchange(true, std::memory_order_relaxed))
2315 {
2316 2 (void)log().log_noexcept(
2317 LogLevel::Warning,
2318 1 "InputIntercept: the routed retention ceiling refused the XInput hook pair, "
2319 "so no XInput interception was installed and both entries remain open."
2320 );
2321 }
2322 1 retire_xinput_module_refs();
2323 1 }
2324
2325 /**
2326 * @brief Retains the created pair after the primary arm wrote its prologue and then lost it.
2327 * @note Requires s_intercept_mutex and a constructed process-lifetime cell.
2328 */
2329 1 void retain_unproved_xinput_install(XInputRetentionLog &deferred_log) noexcept
2330 {
2331 // A game thread can hold the primary trampoline, and nothing here can drain it. Retain the created,
2332 // disabled ordinal-100 member too. Recovery must arm that retained object rather than treat an empty slot
2333 // as the absent/alias exemption.
2334 const XInputPublishedChains published_chains{
2335 1 s_xinput_original.load(std::memory_order_seq_cst) != nullptr,
2336 2 s_xinput_ex_original.load(std::memory_order_seq_cst) != nullptr
2337 1 };
2338 1 retain_xinput_hooks(
2339 PatchWitness::Original,
2340 PatchWitness::Original,
2341 XInputRetentionReason::UnprovedInstall,
2342 deferred_log,
2343 published_chains
2344 );
2345 1 }
2346
2347 /**
2348 * @brief Arms the created Ex member, if any, and publishes the pair only if both members are whole.
2349 * @return True when this call published the pair.
2350 * @note Requires s_intercept_mutex and an armed primary member.
2351 */
2352 [[nodiscard]] bool
2353 206 arm_ex_member_and_publish_xinput_pair(bool ex_is_distinct_member, int user_index, std::uint64_t owner) noexcept
2354 {
2355
2/2
✓ Branch 2 → 3 taken 188 times.
✓ Branch 2 → 6 taken 18 times.
206 if (ex_is_distinct_member)
2356 {
2357 188 (void)arm_created_xinput_hook(
2358 188 s_xinput_permanent_hooks->ex,
2359 s_xinput_ex_original,
2360 s_xinput_ex_enable_warned,
2361 188 "InputIntercept: the XInputGetStateEx hook transaction did not complete cleanly, "
2362 "so XInput coverage stays degraded and both entries pass through."
2363 );
2364 }
2365
2366 // The final pair witness reads both prologues before the store that enables suppression. A member
2367 // restored by a rival writer degrades the pair and prevents publication of incomplete coverage.
2368
2/2
✓ Branch 7 → 8 taken 157 times.
✓ Branch 7 → 9 taken 49 times.
206 if (publish_xinput_pair_if_whole(
2369 206 s_xinput_permanent_hooks->primary,
2370 206 s_xinput_permanent_hooks->ex,
2371 user_index,
2372 owner
2373 ))
2374 {
2375 157 return true;
2376 }
2377 // Clear the gate rather than defer: the next poll cycle attempts the first recovery immediately.
2378 49 xinput_recovery_reset();
2379 49 return false;
2380 }
2381 } // anonymous namespace
2382
2383 namespace
2384 {
2385 [[nodiscard]] DWORD
2386 1652 invoke_xinput_export(XInputGetStateFn target, DWORD user_index, XINPUT_STATE &state) noexcept
2387 {
2388 try
2389 {
2390
1/2
✓ Branch 2 → 3 taken 1652 times.
✗ Branch 2 → 5 not taken.
1652 return target(user_index, &state);
2391 }
2392 ✗ catch (...)
2393 {
2394 ✗ return ERROR_INVALID_FUNCTION;
2395 ✗ }
2396 }
2397 } // anonymous namespace
2398
2399 namespace
2400 {
2401 struct XInputMemberSnapshot
2402 {
2403 2542 XInputGetStateFn target{nullptr};
2404 2542 XInputGetStateFn original{nullptr};
2405 2542 std::array<std::uint8_t, BACKEND_MAX_STEAL_WINDOW> bytes{};
2406 2541 std::size_t byte_count{0};
2407 2541 PatchWitness witness{PatchWitness::Original};
2408 2541 bool enabled{false};
2409 2541 bool readable{true};
2410
2411
8/14
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2542 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2542 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 2541 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2541 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2541 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 2541 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 2541 times.
2542 [[nodiscard]] bool operator==(const XInputMemberSnapshot &) const noexcept = default;
2412 };
2413
2414 5374 [[nodiscard]] XInputMemberSnapshot capture_xinput_member(const safetyhook::InlineHook &hook) noexcept
2415 {
2416 5374 XInputMemberSnapshot snapshot;
2417
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 5372 times.
5374 if (!hook)
2418 2 return snapshot;
2419 5372 snapshot.target = reinterpret_cast<XInputGetStateFn>(hook.target());
2420 5372 snapshot.original = hook.original<XInputGetStateFn>();
2421 5372 snapshot.byte_count = hook.original_bytes().size();
2422 5372 snapshot.enabled = hook.enabled();
2423 5372 snapshot.witness = xinput_patch_witness(hook);
2424
3/6
✓ Branch 11 → 12 taken 5372 times.
✗ Branch 11 → 21 not taken.
✓ Branch 14 → 15 taken 5372 times.
✗ Branch 14 → 21 not taken.
✓ Branch 19 → 20 taken 5372 times.
✗ Branch 19 → 21 not taken.
16116 snapshot.readable = snapshot.byte_count != 0 && snapshot.byte_count <= snapshot.bytes.size() &&
2425 10744 guarded_read_bytes(hook.target_address(), snapshot.bytes.data(), snapshot.byte_count);
2426 5372 return snapshot;
2427 }
2428
2429 2827 [[nodiscard]] bool observed_route_candidate(const XInputMemberSnapshot &member, bool required) noexcept
2430 {
2431
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 2826 times.
2827 if (member.target == nullptr)
2432 1 return !required;
2433
3/4
✓ Branch 4 → 5 taken 2684 times.
✓ Branch 4 → 9 taken 142 times.
✓ Branch 5 → 6 taken 2684 times.
✗ Branch 5 → 9 not taken.
5510 return member.enabled && member.readable &&
2434
3/4
✓ Branch 6 → 7 taken 1686 times.
✓ Branch 6 → 8 taken 998 times.
✓ Branch 7 → 8 taken 1686 times.
✗ Branch 7 → 9 not taken.
5510 (member.witness == PatchWitness::OwnedPatch || member.witness == PatchWitness::Foreign);
2435 }
2436
2437 enum class XInputCallOutcome : std::uint8_t
2438 {
2439 Absent,
2440 Uninvoked,
2441 Returned,
2442 Exception,
2443 };
2444
2445 struct XInputCallEvidence
2446 {
2447 XInputCallOutcome outcome{XInputCallOutcome::Uninvoked};
2448 DWORD result{0};
2449 bool authenticated{false};
2450 std::optional<bool> admitted{};
2451 };
2452
2453 struct XInputPairEvidence
2454 {
2455 XInputCallEvidence primary{};
2456 XInputCallEvidence ex{};
2457 std::optional<bool> admitted{};
2458 std::optional<bool> raw_admitted{};
2459 std::optional<bool> raw_restored{};
2460 std::optional<bool> unchanged{};
2461 std::optional<bool> healthy{};
2462 };
2463
2464 [[nodiscard]] XInputCallEvidence
2465 2541 observe_xinput_export(XInputGetStateFn target, DWORD user_index, XINPUT_STATE &state) noexcept
2466 {
2467 try
2468 {
2469 return {
2470 .outcome = XInputCallOutcome::Returned,
2471 5057 .result = target(user_index, &state),
2472
2/2
✓ Branch 2 → 3 taken 2516 times.
✓ Branch 2 → 5 taken 24 times.
2541 };
2473 }
2474 24 catch (...)
2475 {
2476 return {
2477 .outcome = XInputCallOutcome::Exception,
2478 24 };
2479 24 }
2480 }
2481
2482 141 void append_decimal(std::span<char> buffer, std::size_t &length, std::uint32_t value) noexcept
2483 {
2484 141 std::array<char, 10> digits{};
2485 423 const auto converted = std::to_chars(digits.data(), digits.data() + digits.size(), value);
2486 282 append_text(buffer, length, {digits.data(), static_cast<std::size_t>(converted.ptr - digits.data())});
2487 141 }
2488
2489 294 [[nodiscard]] std::string_view check_description(std::optional<bool> result) noexcept
2490 {
2491
4/4
✓ Branch 3 → 4 taken 273 times.
✓ Branch 3 → 8 taken 21 times.
✓ Branch 5 → 6 taken 264 times.
✓ Branch 5 → 7 taken 9 times.
294 return result ? (*result ? "passed" : "failed") : "unmeasured";
2492 }
2493
2494 84 void append_xinput_call(
2495 std::span<char> buffer,
2496 std::size_t &length,
2497 std::string_view name,
2498 const XInputMemberSnapshot &member,
2499 const XInputCallEvidence &call
2500 ) noexcept
2501 {
2502 84 append_text(buffer, length, name);
2503 84 append_text(buffer, length, ": witness=");
2504 84 append_text(buffer, length, witness_description(member.witness));
2505 84 append_text(buffer, length, ", enabled=");
2506
2/2
✓ Branch 9 → 10 taken 81 times.
✓ Branch 9 → 11 taken 3 times.
84 append_text(buffer, length, member.enabled ? "yes" : "no");
2507 84 append_text(buffer, length, ", readable=");
2508
1/2
✓ Branch 16 → 17 taken 84 times.
✗ Branch 16 → 18 not taken.
84 append_text(buffer, length, member.readable ? "yes" : "no");
2509 84 append_text(buffer, length, ", probe=");
2510 84 append_text(buffer, length, check_description(call.admitted));
2511 84 append_text(buffer, length, ", call=");
2512
4/5
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 31 taken 10 times.
✓ Branch 27 → 34 taken 3 times.
✓ Branch 27 → 37 taken 70 times.
✗ Branch 27 → 41 not taken.
84 switch (call.outcome)
2513 {
2514 1 case XInputCallOutcome::Absent:
2515 1 append_text(buffer, length, "absent");
2516 1 break;
2517 10 case XInputCallOutcome::Uninvoked:
2518 10 append_text(buffer, length, "uninvoked");
2519 10 break;
2520 3 case XInputCallOutcome::Exception:
2521 3 append_text(buffer, length, "exception");
2522 3 break;
2523 70 case XInputCallOutcome::Returned:
2524 70 append_text(buffer, length, "returned result=");
2525 70 append_decimal(buffer, length, call.result);
2526 70 break;
2527 }
2528 84 append_text(buffer, length, ", authentication=");
2529 84 append_text(
2530 buffer,
2531 length,
2532
2/2
✓ Branch 44 → 45 taken 73 times.
✓ Branch 44 → 49 taken 10 times.
83 call.outcome == XInputCallOutcome::Absent || call.outcome == XInputCallOutcome::Uninvoked
2533
2/2
✓ Branch 43 → 44 taken 83 times.
✓ Branch 43 → 49 taken 1 time.
240 ? "unmeasured"
2534
2/2
✓ Branch 45 → 46 taken 57 times.
✓ Branch 45 → 47 taken 16 times.
73 : (call.authenticated ? "matched" : "failed")
2535 );
2536 84 }
2537
2538 421 void degrade_observed_xinput_pair(
2539 bool was_complete,
2540 int user_index,
2541 const XInputMemberSnapshot &primary,
2542 const XInputMemberSnapshot &ex,
2543 const XInputPairEvidence &evidence,
2544 InterceptLockGuard &lock
2545 ) noexcept
2546 {
2547 421 s_xinput_observed_pair = false;
2548 421 s_xinput_primary_observed = false;
2549 421 s_xinput_ex_observed = false;
2550 421 s_xinput_installed.store(false, std::memory_order_release);
2551 421 s_xinput_pair_degraded.store(true, std::memory_order_release);
2552
4/4
✓ Branch 4 → 5 taken 43 times.
✓ Branch 4 → 7 taken 378 times.
✓ Branch 5 → 6 taken 42 times.
✓ Branch 5 → 7 taken 1 time.
421 const bool warn = was_complete && !s_xinput_route_warned;
2553 421 std::array<char, 1024> message{};
2554 421 std::size_t length = 0;
2555
2/2
✓ Branch 8 → 9 taken 42 times.
✓ Branch 8 → 60 taken 379 times.
421 if (warn)
2556 {
2557 42 s_xinput_route_warned = true;
2558 42 append_text(message, length, "InputIntercept: XInput paired suppression stopped: controller=");
2559 42 append_decimal(message, length, static_cast<std::uint32_t>(user_index));
2560 42 append_text(message, length, ", pair: admission=");
2561 42 append_text(message, length, check_description(evidence.admitted));
2562 42 append_text(message, length, ", raw_scope=");
2563 42 append_text(message, length, check_description(evidence.raw_admitted));
2564 42 append_text(message, length, ", raw_restore=");
2565 42 append_text(message, length, check_description(evidence.raw_restored));
2566 42 append_text(message, length, ", snapshot=");
2567 42 append_text(message, length, check_description(evidence.unchanged));
2568 42 append_text(message, length, ", context=");
2569 42 append_text(message, length, check_description(evidence.healthy));
2570 42 append_text(message, length, ". ");
2571 42 append_xinput_call(message, length, "XInputGetState", primary, evidence.primary);
2572 42 append_text(message, length, ". ");
2573 42 append_xinput_call(message, length, "XInputGetStateEx", ex, evidence.ex);
2574 42 append_text(message, length, ".");
2575 }
2576 421 lock.unlock();
2577
2/2
✓ Branch 61 → 62 taken 42 times.
✓ Branch 61 → 68 taken 379 times.
421 if (warn)
2578 {
2579 84 (void)log().log_noexcept(LogLevel::Warning, std::string_view{message.data(), length});
2580 }
2581 421 }
2582
2583 1416 [[nodiscard]] bool observe_xinput_pair(
2584 int user_index,
2585 std::uint64_t owner,
2586 bool was_complete,
2587 bool &recovered,
2588 InterceptLockGuard &lock
2589 ) noexcept
2590 {
2591 1416 const std::uint64_t epoch = s_xinput_hook_epoch.load(std::memory_order_seq_cst);
2592
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1416 times.
1416 if (s_intercept_owner.load(std::memory_order_acquire) != owner)
2593 ✗ return false;
2594 1416 const XInputMemberSnapshot primary = capture_xinput_member(s_xinput_permanent_hooks->primary);
2595 1416 const XInputMemberSnapshot ex = capture_xinput_member(s_xinput_permanent_hooks->ex);
2596 1416 XInputPairEvidence evidence;
2597
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 1415 times.
1416 if (ex.target == nullptr)
2598 1 evidence.ex.outcome = XInputCallOutcome::Absent;
2599
6/8
✓ Branch 22 → 23 taken 1416 times.
✗ Branch 22 → 26 not taken.
✓ Branch 23 → 24 taken 1416 times.
✗ Branch 23 → 26 not taken.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 1415 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 32 taken 1415 times.
1416 if (user_index < 0 || user_index >= XUSER_MAX_COUNT || !reserve_xinput_route_probe())
2600 {
2601 1 evidence.admitted = false;
2602 1 degrade_observed_xinput_pair(was_complete, user_index, primary, ex, evidence, lock);
2603 1 return false;
2604 }
2605 1415 const bool ex_required = ex.target != nullptr;
2606
6/6
✓ Branch 33 → 34 taken 1412 times.
✓ Branch 33 → 36 taken 3 times.
✓ Branch 35 → 36 taken 139 times.
✓ Branch 35 → 37 taken 1273 times.
✓ Branch 38 → 39 taken 142 times.
✓ Branch 38 → 41 taken 1273 times.
1415 if (!observed_route_candidate(primary, true) || !observed_route_candidate(ex, ex_required))
2607 {
2608 142 degrade_observed_xinput_pair(was_complete, user_index, primary, ex, evidence, lock);
2609 142 return false;
2610 }
2611
2612 1273 XINPUT_STATE primary_state{};
2613 1273 XINPUT_STATE ex_state{};
2614 1273 XInputRawScope raw_scope;
2615 XInputRouteProbe primary_probe({
2616 .owner = owner,
2617 .hook_epoch = epoch,
2618 .member = XInputRouteMember::Primary,
2619 1273 .user_index = static_cast<std::uint32_t>(user_index),
2620 .state_address = reinterpret_cast<std::uintptr_t>(&primary_state),
2621 1273 });
2622 1273 std::optional<XInputRouteProbe> ex_probe;
2623
2/2
✓ Branch 43 → 44 taken 1272 times.
✓ Branch 43 → 46 taken 1 time.
1273 if (ex_required)
2624 {
2625 1272 ex_probe.emplace(
2626 1272 XInputRouteProbeIdentity{
2627 .owner = owner,
2628 .hook_epoch = epoch,
2629 .member = XInputRouteMember::Ex,
2630 1272 .user_index = static_cast<std::uint32_t>(user_index),
2631 .state_address = reinterpret_cast<std::uintptr_t>(&ex_state),
2632 }
2633 );
2634 }
2635 const bool admitted =
2636
7/8
✓ Branch 47 → 48 taken 1272 times.
✓ Branch 47 → 55 taken 1 time.
✓ Branch 49 → 50 taken 1271 times.
✓ Branch 49 → 55 taken 1 time.
✓ Branch 50 → 51 taken 1270 times.
✓ Branch 50 → 54 taken 1 time.
✓ Branch 53 → 54 taken 1270 times.
✗ Branch 53 → 55 not taken.
1273 raw_scope.admitted() && primary_probe.admitted() && (!ex_required || ex_probe->admitted());
2637 1273 lock.unlock();
2638 1273 evidence.admitted = admitted;
2639
2/2
✓ Branch 58 → 59 taken 1272 times.
✓ Branch 58 → 65 taken 1 time.
1273 if (ex_required)
2640 {
2641
2/2
✓ Branch 59 → 60 taken 1270 times.
✓ Branch 59 → 62 taken 2 times.
1272 if (admitted)
2642 1270 evidence.ex = observe_xinput_export(ex.target, static_cast<DWORD>(user_index), ex_state);
2643 1272 evidence.ex.authenticated = ex_probe->finish();
2644 }
2645
2/2
✓ Branch 65 → 66 taken 1271 times.
✓ Branch 65 → 68 taken 2 times.
1273 if (admitted)
2646 1271 evidence.primary = observe_xinput_export(primary.target, static_cast<DWORD>(user_index), primary_state);
2647 1272 evidence.primary.authenticated = primary_probe.finish();
2648 1272 evidence.primary.admitted = primary_probe.admitted();
2649
2/2
✓ Branch 71 → 72 taken 1272 times.
✓ Branch 71 → 75 taken 1 time.
1273 evidence.ex.admitted = ex_required ? std::optional<bool>{ex_probe->admitted()} : std::nullopt;
2650 1273 evidence.raw_admitted = raw_scope.admitted();
2651 1273 evidence.raw_restored = raw_scope.finish();
2652 1273 lock.relock();
2653
2654 // A successor owns its publication. An old receipt cannot revoke or refresh that owner's state.
2655
4/4
✓ Branch 88 → 89 taken 1271 times.
✓ Branch 88 → 97 taken 2 times.
✓ Branch 99 → 100 taken 2 times.
✓ Branch 99 → 101 taken 1271 times.
2544 if (s_intercept_owner.load(std::memory_order_acquire) != owner ||
2656
1/2
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 98 taken 1271 times.
1271 s_xinput_hook_epoch.load(std::memory_order_seq_cst) != epoch)
2657 2 return false;
2658
1/2
✓ Branch 111 → 112 taken 1271 times.
✗ Branch 111 → 116 not taken.
1271 const bool unchanged = s_bound_user_index.load(std::memory_order_relaxed) == user_index &&
2659
3/4
✓ Branch 108 → 109 taken 1271 times.
✗ Branch 108 → 116 not taken.
✓ Branch 114 → 115 taken 1270 times.
✓ Branch 114 → 116 taken 1 time.
2542 primary == capture_xinput_member(s_xinput_permanent_hooks->primary) &&
2660 1271 ex == capture_xinput_member(s_xinput_permanent_hooks->ex);
2661 1271 evidence.unchanged = unchanged;
2662 1271 evidence.healthy = xinput_consume_context_healthy();
2663 2317 const auto successful = [](const XInputCallEvidence &call) noexcept -> bool
2664 {
2665
4/4
✓ Branch 2 → 3 taken 2293 times.
✓ Branch 2 → 6 taken 24 times.
✓ Branch 3 → 4 taken 2155 times.
✓ Branch 3 → 6 taken 138 times.
4472 return call.outcome == XInputCallOutcome::Returned && call.authenticated &&
2666
2/2
✓ Branch 4 → 5 taken 2044 times.
✓ Branch 4 → 6 taken 111 times.
4472 call.result == ERROR_SUCCESS;
2667 };
2668
5/6
✓ Branch 122 → 123 taken 1048 times.
✓ Branch 122 → 132 taken 221 times.
✓ Branch 123 → 124 taken 1048 times.
✗ Branch 123 → 126 not taken.
✓ Branch 125 → 126 taken 996 times.
✓ Branch 125 → 132 taken 52 times.
1269 if (admitted && successful(evidence.primary) && (!ex_required || successful(evidence.ex)) &&
2669
10/10
✓ Branch 120 → 121 taken 1269 times.
✓ Branch 120 → 132 taken 2 times.
✓ Branch 127 → 128 taken 995 times.
✓ Branch 127 → 132 taken 1 time.
✓ Branch 128 → 129 taken 994 times.
✓ Branch 128 → 132 taken 1 time.
✓ Branch 130 → 131 taken 993 times.
✓ Branch 130 → 132 taken 1 time.
✓ Branch 133 → 134 taken 993 times.
✓ Branch 133 → 136 taken 278 times.
2540 *evidence.raw_restored && unchanged && *evidence.healthy)
2670 {
2671 993 recovered = s_xinput_route_warned;
2672 993 publish_complete_xinput_pair(user_index, owner);
2673 993 s_xinput_observed_pair = true;
2674 993 s_xinput_primary_observed = true;
2675 993 s_xinput_ex_observed = ex_required;
2676 993 return true;
2677 }
2678 278 degrade_observed_xinput_pair(was_complete, user_index, primary, ex, evidence, lock);
2679 278 return false;
2680 1273 }
2681 } // anonymous namespace
2682
2683 1907 bool install_xinput(int user_index, std::uint64_t owner) noexcept
2684 {
2685 1907 InterceptLockGuard lock{s_intercept_mutex};
2686 1907 XInputRetentionLog deferred_log;
2687
2688
2/2
✓ Branch 4 → 5 taken 34 times.
✓ Branch 4 → 6 taken 1873 times.
1907 if (!owner_available(owner))
2689 {
2690 34 return false;
2691 }
2692
2693
4/4
✓ Branch 7 → 8 taken 1799 times.
✓ Branch 7 → 11 taken 74 times.
✓ Branch 13 → 14 taken 1660 times.
✓ Branch 13 → 96 taken 213 times.
3672 if (s_xinput_permanent_detour.load(std::memory_order_acquire) ||
2694
4/4
✓ Branch 8 → 9 taken 1654 times.
✓ Branch 8 → 12 taken 145 times.
✓ Branch 10 → 11 taken 1586 times.
✓ Branch 10 → 12 taken 68 times.
1799 (s_xinput_permanent_hooks != nullptr && static_cast<bool>(s_xinput_permanent_hooks->primary)))
2695 {
2696 1660 const bool was_complete = s_xinput_installed.load(std::memory_order_acquire);
2697 1660 const std::uint64_t episode_epoch = s_xinput_hook_epoch.load(std::memory_order_seq_cst);
2698 269 const bool reported_episode = s_xinput_route_warned &&
2699
3/4
✓ Branch 22 → 23 taken 269 times.
✓ Branch 22 → 40 taken 1391 times.
✓ Branch 30 → 31 taken 269 times.
✗ Branch 30 → 40 not taken.
2198 s_intercept_owner.load(std::memory_order_acquire) == owner &&
2700
2/2
✓ Branch 38 → 39 taken 268 times.
✓ Branch 38 → 40 taken 1 time.
269 s_bound_user_index.load(std::memory_order_relaxed) == user_index;
2701 1660 const bool exact = s_xinput_permanent_detour.load(std::memory_order_acquire)
2702
2/2
✓ Branch 42 → 43 taken 74 times.
✓ Branch 42 → 45 taken 1586 times.
1660 ? maintain_retained_xinput_pair(user_index, owner)
2703 1660 : maintain_live_xinput_pair(user_index, owner);
2704 1660 bool recovered = false;
2705
4/4
✓ Branch 47 → 48 taken 1367 times.
✓ Branch 47 → 50 taken 293 times.
✓ Branch 49 → 50 taken 947 times.
✓ Branch 49 → 51 taken 420 times.
1660 const bool complete = exact || observe_xinput_pair(user_index, owner, was_complete, recovered, lock);
2706
2/2
✓ Branch 52 → 53 taken 420 times.
✓ Branch 52 → 54 taken 1240 times.
1660 if (!complete)
2707 420 return false;
2708
2/2
✓ Branch 54 → 55 taken 293 times.
✓ Branch 54 → 75 taken 947 times.
1240 if (exact)
2709 {
2710
3/4
✓ Branch 55 → 56 taken 2 times.
✓ Branch 55 → 73 taken 291 times.
✓ Branch 63 → 64 taken 2 times.
✗ Branch 63 → 73 not taken.
297 recovered = reported_episode && s_xinput_hook_epoch.load(std::memory_order_seq_cst) == episode_epoch &&
2711
1/2
✓ Branch 71 → 72 taken 2 times.
✗ Branch 71 → 73 not taken.
2 s_intercept_owner.load(std::memory_order_acquire) == owner;
2712 }
2713 1240 lock.unlock();
2714
2/2
✓ Branch 76 → 77 taken 29 times.
✓ Branch 76 → 94 taken 1211 times.
1240 if (recovered)
2715 {
2716 29 std::array<char, 128> message{};
2717 29 std::size_t length = 0;
2718 29 append_text(message, length, "InputIntercept: XInput paired suppression recovered: controller=");
2719 29 append_decimal(message, length, static_cast<std::uint32_t>(user_index));
2720
2/2
✓ Branch 82 → 83 taken 2 times.
✓ Branch 82 → 84 taken 27 times.
29 append_text(message, length, exact ? ", authorization=exact." : ", authorization=observed.");
2721 58 (void)log().log_noexcept(LogLevel::Info, std::string_view{message.data(), length});
2722 }
2723 1240 return true;
2724 }
2725
2726 213 const HMODULE module = find_loaded_xinput_module();
2727
1/2
✗ Branch 97 → 98 not taken.
✓ Branch 97 → 99 taken 213 times.
213 if (module == nullptr)
2728 {
2729 ✗ return false; // XInput is not loaded yet. The poll loop retries.
2730 }
2731
2732 213 auto *get_state = reinterpret_cast<void *>(GetProcAddress(module, "XInputGetState"));
2733
2/2
✓ Branch 100 → 101 taken 1 time.
✓ Branch 100 → 102 taken 212 times.
213 if (get_state == nullptr)
2734 {
2735 1 return false;
2736 }
2737
2738 // XInputGetStateEx (ordinal 100) carries the Guide button. Without its hook, a game can bypass the mask.
2739 // An absent export needs no second hook. The primary route already covers an alias.
2740 auto *get_state_ex =
2741 212 reinterpret_cast<void *>(GetProcAddress(module, MAKEINTRESOURCEA(XINPUT_GET_STATE_EX_ORDINAL)));
2742
4/4
✓ Branch 103 → 104 taken 196 times.
✓ Branch 103 → 106 taken 16 times.
✓ Branch 104 → 105 taken 194 times.
✓ Branch 104 → 106 taken 2 times.
212 const bool ex_is_distinct_member = get_state_ex != nullptr && get_state_ex != get_state;
2743
2744 // Construct the canonical cell before any reference or target patch.
2745
1/2
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 110 taken 212 times.
212 if (!ensure_permanent_cell())
2746 {
2747 ✗ return false;
2748 }
2749
2750
1/2
✗ Branch 111 → 112 not taken.
✓ Branch 111 → 113 taken 212 times.
212 if (!acquire_xinput_keepalives(get_state, get_state_ex, ex_is_distinct_member))
2751 {
2752 ✗ return false;
2753 }
2754
2755
3/6
✓ Branch 114 → 115 taken 212 times.
✗ Branch 114 → 117 not taken.
✗ Branch 116 → 117 not taken.
✓ Branch 116 → 118 taken 212 times.
✗ Branch 119 → 120 not taken.
✓ Branch 119 → 122 taken 212 times.
212 if (!reserve_xinput_route_probe() || !acquire_xinput_raw_scope_owner())
2756 {
2757 ✗ retire_xinput_module_refs();
2758 ✗ return false;
2759 }
2760 212 s_xinput_raw_owner = true;
2761
2762 // Reserve the worst case for BOTH members before either creation. A primary charge followed by an Ex refusal
2763 // strands the primary-only coverage that this transaction prevents.
2764 212 safetyhook::RouteRetentionCredit pair_credit = safetyhook::RouteRetentionCredit::acquire(2);
2765
2/2
✓ Branch 124 → 125 taken 1 time.
✓ Branch 124 → 127 taken 211 times.
212 if (!pair_credit)
2766 {
2767 1 retire_xinput_install_after_credit_refusal();
2768 1 return false;
2769 }
2770
2771 // Create both members before either prologue patch. Before an arm runs, no thread enters a detour. A creation
2772 // failure rolls the whole transaction back and leaves both entries open.
2773
2/2
✓ Branch 128 → 129 taken 2 times.
✓ Branch 128 → 131 taken 209 times.
211 if (!create_disabled_xinput_hook(
2774 pair_credit,
2775 get_state,
2776 reinterpret_cast<void *>(&xinput_get_state_detour),
2777 XINPUT_GET_STATE_POSITION,
2778 211 s_xinput_permanent_hooks->primary,
2779 211 s_xinput_permanent_hooks->primary_gateway
2780 ))
2781 {
2782 2 retire_xinput_module_refs();
2783 2 return false;
2784 }
2785
4/6
✓ Branch 131 → 132 taken 191 times.
✓ Branch 131 → 135 taken 18 times.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 135 taken 191 times.
✗ Branch 136 → 137 not taken.
✓ Branch 136 → 143 taken 209 times.
400 if (ex_is_distinct_member && !create_disabled_xinput_hook(
2786 pair_credit,
2787 get_state_ex,
2788 reinterpret_cast<void *>(&xinput_get_state_ex_detour),
2789 XINPUT_GET_STATE_EX_POSITION,
2790 191 s_xinput_permanent_hooks->ex,
2791 191 s_xinput_permanent_hooks->ex_gateway
2792 ))
2793 {
2794 const char *const primary_retained =
2795 ✗ reset_inactive_xinput_hook(s_xinput_permanent_hooks->primary, s_xinput_original);
2796 ✗ retire_xinput_module_refs(reset_chain_reach(primary_retained != nullptr));
2797 ✗ lock.unlock();
2798 ✗ emit_xinput_route_retention_logs(primary_retained, nullptr);
2799 ✗ return false;
2800 }
2801
2802 418 const XInputArmOutcome primary_outcome = arm_created_xinput_hook(
2803 209 s_xinput_permanent_hooks->primary,
2804 s_xinput_original,
2805 s_xinput_enable_warned,
2806 209 "InputIntercept: XInputGetState hook transaction did not complete cleanly; state was "
2807 "reconciled from the target bytes."
2808 );
2809
2/2
✓ Branch 145 → 146 taken 1 time.
✓ Branch 145 → 150 taken 208 times.
209 if (primary_outcome == XInputArmOutcome::CommittedUnreachable)
2810 {
2811 1 retain_unproved_xinput_install(deferred_log);
2812 1 lock.unlock();
2813 1 emit_xinput_retention_log(deferred_log);
2814 1 return false;
2815 }
2816
2/2
✓ Branch 150 → 151 taken 2 times.
✓ Branch 150 → 162 taken 206 times.
208 if (primary_outcome != XInputArmOutcome::Armed)
2817 {
2818 const char *const ex_retained =
2819 2 reset_inactive_xinput_hook(s_xinput_permanent_hooks->ex, s_xinput_ex_original);
2820 const char *const primary_retained =
2821 2 reset_inactive_xinput_hook(s_xinput_permanent_hooks->primary, s_xinput_original);
2822
3/4
✓ Branch 153 → 154 taken 2 times.
✗ Branch 153 → 155 not taken.
✓ Branch 154 → 155 taken 1 time.
✓ Branch 154 → 156 taken 1 time.
2 retire_xinput_module_refs(reset_chain_reach(primary_retained || ex_retained));
2823 2 lock.unlock();
2824 2 emit_xinput_route_retention_logs(primary_retained, ex_retained);
2825 2 return false;
2826 }
2827
2828 206 bool recovered = false;
2829
4/4
✓ Branch 163 → 164 taken 49 times.
✓ Branch 163 → 166 taken 157 times.
✓ Branch 165 → 166 taken 46 times.
✓ Branch 165 → 167 taken 3 times.
255 return arm_ex_member_and_publish_xinput_pair(ex_is_distinct_member, user_index, owner) ||
2830 255 observe_xinput_pair(user_index, owner, false, recovered, lock);
2831 1907 }
2832
2833 10234 bool xinput_installed() noexcept
2834 {
2835
3/4
✓ Branch 3 → 4 taken 399 times.
✓ Branch 3 → 7 taken 9835 times.
✓ Branch 5 → 6 taken 399 times.
✗ Branch 5 → 7 not taken.
10234 return s_xinput_installed.load(std::memory_order_acquire) && xinput_consume_context_healthy();
2836 }
2837
2838 87 XInputGetStateFn xinput_trampoline() noexcept
2839 {
2840
4/4
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 7 taken 65 times.
✓ Branch 8 → 9 taken 16 times.
✓ Branch 8 → 10 taken 71 times.
109 if (!s_xinput_installed.load(std::memory_order_acquire) &&
2841
2/2
✓ Branch 5 → 6 taken 16 times.
✓ Branch 5 → 7 taken 6 times.
22 !s_xinput_pair_degraded.load(std::memory_order_acquire))
2842 {
2843 16 return nullptr;
2844 }
2845 71 return s_xinput_original.load(std::memory_order_acquire);
2846 }
2847
2848 1652 bool sample_xinput_raw_state(DWORD user_index, XINPUT_STATE &state, std::uint64_t owner) noexcept
2849 {
2850 1652 XInputRawScope raw_scope;
2851
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1652 times.
1652 if (!raw_scope.admitted())
2852 ✗ return false;
2853 1652 XInputGetStateFn target = nullptr;
2854 {
2855 1652 const InterceptLockGuard lock{s_intercept_mutex};
2856
5/6
✓ Branch 7 → 8 taken 1039 times.
✓ Branch 7 → 19 taken 613 times.
✓ Branch 9 → 10 taken 1039 times.
✗ Branch 9 → 19 not taken.
✓ Branch 20 → 21 taken 1039 times.
✓ Branch 20 → 23 taken 613 times.
2691 if (s_xinput_permanent_hooks != nullptr && s_xinput_permanent_hooks->primary &&
2857
1/2
✓ Branch 17 → 18 taken 1039 times.
✗ Branch 17 → 19 not taken.
1039 s_intercept_owner.load(std::memory_order_acquire) == owner)
2858 {
2859 1039 target = reinterpret_cast<XInputGetStateFn>(s_xinput_permanent_hooks->primary.target());
2860 }
2861
1/2
✓ Branch 24 → 25 taken 613 times.
✗ Branch 24 → 27 not taken.
613 else if (const HMODULE module = find_loaded_xinput_module(); module != nullptr)
2862 {
2863 target = reinterpret_cast<XInputGetStateFn>(
2864 613 reinterpret_cast<void (*)()>(GetProcAddress(module, "XInputGetState"))
2865 );
2866 }
2867 1652 }
2868
1/2
✓ Branch 28 → 29 taken 1652 times.
✗ Branch 28 → 30 not taken.
1652 const DWORD result = invoke_xinput_export(target != nullptr ? target : &XInputGetState, user_index, state);
2869 1652 const bool restored = raw_scope.finish();
2870
3/4
✓ Branch 33 → 34 taken 1646 times.
✓ Branch 33 → 36 taken 6 times.
✓ Branch 34 → 35 taken 1646 times.
✗ Branch 34 → 36 not taken.
1652 return result == ERROR_SUCCESS && restored;
2871 1652 }
2872
2873 1152 bool publish_gamepad_suppress(uint16_t suppress_bits, std::uint64_t owner) noexcept
2874 {
2875 1152 run_data_plane_entry_seam();
2876 1152 const DataPlaneLockGuard data_lock;
2877
2/2
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 1148 times.
1152 if (!data_plane_authorized(owner))
2878 {
2879 4 return false;
2880 }
2881 // Write the deadline before the release store on the mask, so a fresh mask is never paired with a stale
2882 // deadline.
2883 1148 s_suppress_deadline_ms.store(GetTickCount64() + SUPPRESS_TTL_MS, std::memory_order_relaxed);
2884 1148 s_suppress_mask.store(suppress_bits, std::memory_order_release);
2885 1148 return true;
2886 1152 }
2887
2888 namespace
2889 {
2890 /**
2891 * @brief Removes the mounted hook before the new hook mounts.
2892 * @details The transaction disables capture and consume, advances the epoch, and drains the wheel admitted
2893 * phases before it removes the old hook. Hooks never overlap.
2894 * @return False if the new mount must not proceed. A false return leaves the route state Retryable or
2895 * CleanupBlocked.
2896 * @note Requires s_intercept_mutex.
2897 */
2898 5 [[nodiscard]] bool unmount_message_hook_for_migration(HHOOK mounted) noexcept
2899 {
2900 5 const std::uint64_t wheel_epoch = close_wheel_capture_and_advance_epoch();
2901 {
2902 5 const DataPlaneLockGuard data_lock;
2903 5 reset_wheel_data_plane(wheel_epoch);
2904 5 }
2905
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 17 taken 4 times.
5 if (!drain_wheel_admitted_phases())
2906 {
2907 s_msg_hook_route_state.store(
2908 static_cast<std::uint8_t>(WheelRouteState::Retryable),
2909 std::memory_order_release
2910 );
2911 1 return false;
2912 }
2913
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 4 times.
4 if (msg_hook_target_thread_exited_locked())
2914 {
2915 ✗ retire_message_hook_route_locked();
2916 }
2917
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 31 taken 3 times.
4 else if (!unhook_or_already_gone(mounted))
2918 {
2919 // Old-hook removal failed on a live thread. Block the new mount until that thread exits.
2920 s_msg_hook_route_state.store(
2921 static_cast<std::uint8_t>(WheelRouteState::CleanupBlocked),
2922 std::memory_order_release
2923 );
2924 1 return false;
2925 }
2926 else
2927 {
2928 3 s_msg_hook.store(nullptr, std::memory_order_release);
2929 s_msg_hook_thread_id.store(0, std::memory_order_release);
2930
1/2
✓ Branch 40 → 41 taken 3 times.
✗ Branch 40 → 43 not taken.
3 if (s_msg_hook_thread != nullptr)
2931 {
2932 3 CloseHandle(s_msg_hook_thread);
2933 3 s_msg_hook_thread = nullptr;
2934 }
2935 }
2936 3 return true;
2937 }
2938 } // anonymous namespace
2939
2940 40 bool install_message_hook(std::uint64_t owner, std::uint32_t target_thread_id) noexcept
2941 {
2942 40 const InterceptLockGuard lock{s_intercept_mutex};
2943
3/6
✓ Branch 4 → 5 taken 40 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 40 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 40 times.
40 if (!owner_available(owner) || target_thread_id == 0)
2944 {
2945 ✗ return false;
2946 }
2947 40 settle_message_hook_route_locked();
2948
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 39 times.
40 if (s_msg_hook_route_state.load(std::memory_order_relaxed) ==
2949 static_cast<std::uint8_t>(WheelRouteState::CleanupBlocked))
2950 {
2951 // A prior removal failure still blocks new mounts. settle_message_hook_route_locked() clears this once
2952 // the blocked thread exits.
2953 1 return false;
2954 }
2955
2956 39 const HHOOK mounted = s_msg_hook.load(std::memory_order_relaxed);
2957
2/2
✓ Branch 21 → 22 taken 7 times.
✓ Branch 21 → 46 taken 32 times.
39 if (mounted != nullptr)
2958 {
2959
4/4
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 39 taken 5 times.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 43 taken 5 times.
9 if (s_msg_hook_thread_id.load(std::memory_order_relaxed) == target_thread_id &&
2960
1/2
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 39 not taken.
2 s_msg_hook_route_state.load(std::memory_order_relaxed) ==
2961 static_cast<std::uint8_t>(WheelRouteState::Ready))
2962 {
2963 // A same-thread mount is idempotent. A window change on the same thread needs no republish.
2964 2 publish_owner(owner);
2965 2 return true;
2966 }
2967
2/2
✓ Branch 44 → 45 taken 2 times.
✓ Branch 44 → 46 taken 3 times.
5 if (!unmount_message_hook_for_migration(mounted))
2968 {
2969 2 return false;
2970 }
2971 }
2972 // Validate the target: it must belong to this process and be alive.
2973 35 HANDLE thread = OpenThread(SYNCHRONIZE | THREAD_QUERY_LIMITED_INFORMATION, FALSE, target_thread_id);
2974
2/2
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 57 taken 34 times.
35 if (thread == nullptr)
2975 {
2976 s_msg_hook_route_state.store(
2977 static_cast<std::uint8_t>(WheelRouteState::Retryable),
2978 std::memory_order_release
2979 );
2980 1 return false;
2981 }
2982
3/6
✓ Branch 59 → 60 taken 34 times.
✗ Branch 59 → 62 not taken.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 34 times.
✗ Branch 64 → 65 not taken.
✓ Branch 64 → 75 taken 34 times.
34 if (GetProcessIdOfThread(thread) != GetCurrentProcessId() || WaitForSingleObject(thread, 0) == WAIT_OBJECT_0)
2983 {
2984 ✗ CloseHandle(thread);
2985 s_msg_hook_route_state.store(
2986 static_cast<std::uint8_t>(WheelRouteState::Retryable),
2987 std::memory_order_release
2988 );
2989 ✗ return false;
2990 }
2991
2992 // Take the keepalive before the hook is published. A successful publication makes it permanent because a
2993 // selected callback can run after UnhookWindowsHookEx returns. A failed publication releases it.
2994 34 HMODULE new_ref = nullptr;
2995
2/2
✓ Branch 76 → 77 taken 26 times.
✓ Branch 76 → 81 taken 8 times.
34 if (!s_msg_hook_ref_taken.load(std::memory_order_relaxed))
2996 {
2997 26 new_ref = acquire_module_ref(diagnostics::ModulePinReason::MessageHookKeepalive);
2998
1/2
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 81 taken 26 times.
26 if (new_ref == nullptr)
2999 {
3000 ✗ CloseHandle(thread);
3001 ✗ return false;
3002 }
3003 }
3004
3005 34 const HHOOK hook = SetWindowsHookExW(WH_GETMESSAGE, &message_hook_proc, nullptr, target_thread_id);
3006
1/2
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 94 taken 34 times.
34 if (hook == nullptr)
3007 {
3008 ✗ release_module_ref(new_ref, diagnostics::ModulePinReason::MessageHookKeepalive);
3009 ✗ CloseHandle(thread);
3010 s_msg_hook_route_state.store(
3011 static_cast<std::uint8_t>(WheelRouteState::Retryable),
3012 std::memory_order_release
3013 );
3014 ✗ return false;
3015 }
3016
2/2
✓ Branch 94 → 95 taken 26 times.
✓ Branch 94 → 97 taken 8 times.
34 if (new_ref != nullptr)
3017 {
3018 26 s_msg_hook_ref_taken.store(true, std::memory_order_relaxed);
3019 26 DetourModKit::diagnostics::record_intentional_leak(DetourModKit::diagnostics::LeakSubsystem::Input);
3020 }
3021 34 s_msg_hook_thread = thread;
3022 // A callback that races route publication passes through until this id becomes visible.
3023 s_msg_hook_thread_id.store(target_thread_id, std::memory_order_release);
3024 34 s_msg_hook.store(hook, std::memory_order_release);
3025
1/2
✓ Branch 106 → 107 taken 34 times.
✗ Branch 106 → 108 not taken.
34 s_msg_hook_mount_generation = s_msg_hook_mount_generation + 1 == 0 ? 1 : s_msg_hook_mount_generation + 1;
3026 s_msg_hook_route_state.store(static_cast<std::uint8_t>(WheelRouteState::Ready), std::memory_order_release);
3027 34 publish_owner(owner);
3028 34 return true;
3029 40 }
3030
3031 9779 bool message_hook_installed() noexcept
3032 {
3033 9779 return message_hook_route_state() == WheelRouteState::Ready;
3034 }
3035
3036 9827 WheelRouteState message_hook_route_state() noexcept
3037 {
3038 9827 const InterceptLockGuard lock{s_intercept_mutex};
3039 9827 settle_message_hook_route_locked();
3040 9827 return static_cast<WheelRouteState>(s_msg_hook_route_state.load(std::memory_order_relaxed));
3041 9827 }
3042
3043 29 std::uint32_t message_hook_thread_id() noexcept
3044 {
3045 29 return s_msg_hook_thread_id.load(std::memory_order_acquire);
3046 }
3047
3048 6 std::uint64_t message_hook_mount_generation() noexcept
3049 {
3050 6 const InterceptLockGuard lock{s_intercept_mutex};
3051 12 return s_msg_hook_mount_generation;
3052 6 }
3053
3054 112 std::array<int, 4> take_wheel_counts(std::uint64_t owner) noexcept
3055 {
3056 112 run_data_plane_entry_seam();
3057 112 const DataPlaneLockGuard data_lock;
3058 112 std::array<int, 4> out{};
3059
2/2
✓ Branch 5 → 6 taken 12 times.
✓ Branch 5 → 7 taken 100 times.
112 if (!data_plane_authorized(owner))
3060 {
3061 12 return out;
3062 }
3063 100 const std::uint64_t wheel_epoch = wheel_capture_epoch(s_wheel_capture_state.load(std::memory_order_seq_cst));
3064
2/2
✓ Branch 25 → 16 taken 400 times.
✓ Branch 25 → 26 taken 100 times.
500 for (int dir = 0; dir < 4; ++dir)
3065 {
3066 400 const std::uint64_t packed = s_wheel_count[static_cast<size_t>(dir)].exchange(
3067 wheel_count_slot(wheel_epoch, 0),
3068 std::memory_order_relaxed
3069 );
3070
1/2
✓ Branch 21 → 22 taken 400 times.
✗ Branch 21 → 24 not taken.
400 if (wheel_slot_epoch(packed) == wheel_epoch)
3071 {
3072 400 out[static_cast<size_t>(dir)] = static_cast<int>(packed & WHEEL_COUNT_MASK);
3073 }
3074 }
3075 100 return out;
3076 112 }
3077
3078 #if defined(DMK_ENABLE_TEST_SEAMS)
3079 4 void seed_wheel_notches_for_test(const std::array<int, 4> &notches) noexcept
3080 {
3081 4 const std::uint64_t wheel_epoch = wheel_capture_epoch(s_wheel_capture_state.load(std::memory_order_seq_cst));
3082
2/2
✓ Branch 29 → 11 taken 16 times.
✓ Branch 29 → 30 taken 4 times.
40 for (size_t dir = 0; dir < s_wheel_count.size(); ++dir)
3083 {
3084 // Saturate to bump_wheel_notch's ceiling so a seeded backlog stays a producible state.
3085 16 int n = notches[dir];
3086
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 16 times.
16 if (n < 0)
3087 {
3088 ✗ n = 0;
3089 }
3090
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 16 times.
16 else if (n > MAX_WHEEL_NOTCHES)
3091 {
3092 ✗ n = MAX_WHEEL_NOTCHES;
3093 }
3094 16 s_wheel_count[dir].store(
3095 wheel_count_slot(wheel_epoch, static_cast<std::uint64_t>(n)),
3096 std::memory_order_relaxed
3097 );
3098 }
3099 4 }
3100 #endif
3101
3102 1476 bool publish_wheel_consume(uint8_t direction_mask, bool require_focus, std::uint64_t owner) noexcept
3103 {
3104 1476 run_data_plane_entry_seam();
3105 1476 const DataPlaneLockGuard data_lock;
3106
2/2
✓ Branch 5 → 6 taken 851 times.
✓ Branch 5 → 7 taken 625 times.
1476 if (!data_plane_authorized(owner))
3107 {
3108 851 return false;
3109 }
3110
2/2
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 16 taken 622 times.
625 if ((s_wheel_capture_state.load(std::memory_order_seq_cst) & WHEEL_CAPTURE_ENABLED) == 0)
3111 {
3112 3 return false;
3113 }
3114 622 s_wheel_require_focus.store(require_focus, std::memory_order_relaxed);
3115 // Refresh the deadline before the mask release store, but only for a nonzero arm. This order ensures a set
3116 // direction bit never appears with a stale deadline. A zero mask needs no deadline.
3117
2/2
✓ Branch 17 → 18 taken 22 times.
✓ Branch 17 → 28 taken 600 times.
622 if (direction_mask != 0)
3118 {
3119 22 s_wheel_consume_deadline_ms.store(GetTickCount64() + SUPPRESS_TTL_MS, std::memory_order_relaxed);
3120 }
3121 622 s_wheel_consume_mask.store(direction_mask, std::memory_order_release);
3122 622 return true;
3123 1476 }
3124
3125 #if defined(DMK_ENABLE_TEST_SEAMS)
3126 12 void set_xinput_arm_seam(XInputArmSeam seam) noexcept
3127 {
3128 12 s_xinput_arm_seam.store(seam, std::memory_order_release);
3129 12 }
3130
3131 19 void set_xinput_detour_body_seam(XInputDetourBodySeam seam) noexcept
3132 {
3133 19 s_xinput_detour_body_seam.store(seam, std::memory_order_release);
3134 19 }
3135
3136 4 void set_xinput_route_entry_hold_for_test(bool hold) noexcept
3137 {
3138
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 2 times.
4 safetyhook::set_route_park_for_test(
3139 hold ? safetyhook::RouteParkStage::BEFORE_DESTINATION : safetyhook::RouteParkStage::NONE
3140 );
3141 4 }
3142
3143 2077 bool xinput_route_entry_reached_for_test() noexcept
3144 {
3145 2077 return safetyhook::route_park_reached_for_test();
3146 }
3147
3148 18 void set_xinput_clean_release_seam(XInputCleanReleaseSeam seam) noexcept
3149 {
3150 18 s_xinput_clean_release_seam.store(seam, std::memory_order_release);
3151 18 }
3152
3153 2 void set_xinput_retention_attribution_seam(XInputRetentionAttributionSeam seam) noexcept
3154 {
3155 2 s_xinput_retention_attribution_seam.store(seam, std::memory_order_release);
3156 2 }
3157
3158 2 void set_xinput_create_seam(XInputCreateSeam seam) noexcept
3159 {
3160 2 s_xinput_create_seam.store(seam, std::memory_order_release);
3161 2 }
3162
3163 30 void set_xinput_backend_toggle_exception_for_test(void *target, bool after_mutation) noexcept
3164 {
3165
2/2
✓ Branch 2 → 3 taken 18 times.
✓ Branch 2 → 6 taken 12 times.
30 if (target == nullptr)
3166 {
3167 18 safetyhook::g_trap_exception_stage_override.store(
3168 safetyhook::TrapExceptionStage::NONE,
3169 std::memory_order_release
3170 );
3171 18 safetyhook::g_trap_exception_target_override.store(nullptr, std::memory_order_relaxed);
3172 18 return;
3173 }
3174
3175 s_xinput_backend_toggle_exception_catches.store(0, std::memory_order_relaxed);
3176 12 safetyhook::g_trap_exception_target_override.store(
3177 static_cast<std::uint8_t *>(target),
3178 std::memory_order_relaxed
3179 );
3180
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 10 times.
12 safetyhook::g_trap_exception_stage_override.store(
3181 after_mutation ? safetyhook::TrapExceptionStage::AFTER_MUTATION
3182 : safetyhook::TrapExceptionStage::BEFORE_MUTATION,
3183 std::memory_order_release
3184 );
3185 }
3186
3187 8 std::size_t xinput_backend_toggle_exception_catches_for_test() noexcept
3188 {
3189 8 return s_xinput_backend_toggle_exception_catches.load(std::memory_order_relaxed);
3190 }
3191
3192 12 bool xinput_permanent_primary_retained() noexcept
3193 {
3194
4/6
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 8 taken 7 times.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 8 not taken.
17 return s_xinput_permanent_detour.load(std::memory_order_acquire) && s_xinput_permanent_hooks != nullptr &&
3195 17 static_cast<bool>(s_xinput_permanent_hooks->primary);
3196 }
3197
3198 127 int xinput_module_refs_held() noexcept
3199 {
3200
4/4
✓ Branch 2 → 3 taken 126 times.
✓ Branch 2 → 13 taken 1 time.
✓ Branch 3 → 4 taken 33 times.
✓ Branch 3 → 5 taken 93 times.
127 return s_xinput_permanent_hooks != nullptr ? (s_xinput_permanent_hooks->self_ref != nullptr ? 1 : 0) +
3201
2/2
✓ Branch 6 → 7 taken 33 times.
✓ Branch 6 → 8 taken 93 times.
126 (s_xinput_permanent_hooks->target_ref != nullptr ? 1 : 0) +
3202
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 125 times.
126 (s_xinput_permanent_hooks->ex_target_ref != nullptr ? 1 : 0)
3203 127 : 0;
3204 }
3205
3206 1 void arm_xinput_process_exit_oracle_for_test(const std::uint8_t *target) noexcept
3207 {
3208
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (target == nullptr)
3209 {
3210 ✗ return;
3211 }
3212
2/2
✓ Branch 6 → 5 taken 16 times.
✓ Branch 6 → 7 taken 1 time.
17 for (std::size_t i = 0; i < XINPUT_PROCESS_EXIT_WITNESS_BYTES; ++i)
3213 {
3214 16 s_xinput_process_exit_patch[i] = target[i];
3215 }
3216 1 s_xinput_process_exit_target.store(target, std::memory_order_release);
3217 }
3218
3219 1735 void set_xinput_module_override_for_test(HMODULE module) noexcept
3220 {
3221 1735 const InterceptLockGuard lock{s_intercept_mutex};
3222 1735 s_xinput_module_override = module;
3223 1735 }
3224
3225 62 XInputGetStateFn xinput_ex_trampoline() noexcept
3226 {
3227 62 return s_xinput_ex_original.load(std::memory_order_acquire);
3228 }
3229
3230 12 void apply_xinput_suppress_for_test(XINPUT_STATE *state, DWORD user_index) noexcept
3231 {
3232
1/2
✓ Branch 2 → 3 taken 12 times.
✗ Branch 2 → 5 not taken.
12 if (state != nullptr)
3233 12 state->Gamepad.wButtons = suppressed_xinput_buttons(state, user_index, state->Gamepad.wButtons);
3234 12 }
3235
3236 4 int xinput_bound_user_index() noexcept
3237 {
3238 4 return s_bound_user_index.load(std::memory_order_relaxed);
3239 }
3240
3241 12 void set_data_plane_entry_seam(DataPlaneEntrySeam seam) noexcept
3242 {
3243 12 s_data_plane_entry_seam.store(seam, std::memory_order_release);
3244 12 }
3245
3246 7 void set_wheel_capture_entry_seam(WheelCaptureEntrySeam seam) noexcept
3247 {
3248 7 s_wheel_capture_entry_seam.store(seam, std::memory_order_release);
3249 7 }
3250
3251 36 void set_wheel_finalize_entry_seam(WheelFinalizeEntrySeam seam) noexcept
3252 {
3253 36 s_wheel_finalize_entry_seam.store(seam, std::memory_order_release);
3254 36 }
3255
3256 5 void set_wheel_drain_timeout_for_test(std::uint64_t timeout_ms) noexcept
3257 {
3258 s_wheel_drain_timeout_override_ms.store(timeout_ms, std::memory_order_release);
3259 5 }
3260
3261 38 void set_message_unhook_failure_for_test(bool fail) noexcept
3262 {
3263 38 s_force_message_unhook_failure.store(fail, std::memory_order_release);
3264 38 }
3265
3266 38 void set_wheel_process_focus_for_test(std::int32_t focused) noexcept
3267 {
3268 s_wheel_process_focus_override.store(focused, std::memory_order_release);
3269 38 }
3270
3271 2 std::uint32_t wheel_admitted_phases_for_test() noexcept
3272 {
3273 2 return s_wheel_admitted_phases.load(std::memory_order_seq_cst);
3274 }
3275
3276 32 bool process_wheel_message_for_test(bool horizontal, int delta) noexcept
3277 {
3278 32 const std::uint64_t capture_state = s_wheel_capture_state.load(std::memory_order_seq_cst);
3279 32 const bool consume_intent = wheel_count_admission(horizontal, delta, capture_state);
3280
3/4
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 14 taken 28 times.
✓ Branch 12 → 13 taken 4 times.
✗ Branch 12 → 14 not taken.
32 return consume_intent && wheel_consume_finalization(horizontal, delta, capture_state);
3281 }
3282
3283 2 std::uint32_t consume_rules_sequence() noexcept
3284 {
3285 2 return s_consume_rules_seq.load(std::memory_order_acquire);
3286 }
3287
3288 1 std::uint16_t gamepad_suppress_mask_for_test() noexcept
3289 {
3290 1 return s_suppress_mask.load(std::memory_order_acquire);
3291 }
3292
3293 1 bool gamepad_rule_suppress_enabled_for_test() noexcept
3294 {
3295 1 return s_rule_suppress_enabled.load(std::memory_order_acquire);
3296 }
3297
3298 2 std::uint8_t wheel_consume_mask_for_test() noexcept
3299 {
3300 2 return s_wheel_consume_mask.load(std::memory_order_acquire);
3301 }
3302 #endif
3303
3304 namespace
3305 {
3306 /**
3307 * @brief Disarms the logical XInput layer and resets its warning latches.
3308 * @note Requires s_intercept_mutex.
3309 */
3310 207 void clear_xinput_install_state() noexcept
3311 {
3312 207 s_xinput_installed.store(false, std::memory_order_release);
3313 207 s_xinput_pair_degraded.store(false, std::memory_order_release);
3314 207 xinput_recovery_reset();
3315 207 s_xinput_enable_warned.store(false, std::memory_order_relaxed);
3316 207 s_xinput_ex_enable_warned.store(false, std::memory_order_relaxed);
3317 207 s_xinput_capacity_warned.store(false, std::memory_order_relaxed);
3318 207 }
3319
3320 /**
3321 * @brief Closes backend admission and retires the published trampoline pointers.
3322 * @return The chains published before the retirement.
3323 * @note Requires s_intercept_mutex and a constructed process-lifetime cell.
3324 */
3325 207 [[nodiscard]] XInputPublishedChains retire_published_xinput_chains() noexcept
3326 {
3327 // Close backend admission before pointer retirement. This covers the interval before InflightGuard and the
3328 // body it counts.
3329 207 s_xinput_permanent_hooks->ex.begin_route_rundown();
3330 207 s_xinput_permanent_hooks->primary.begin_route_rundown();
3331
3332 // Retire the published trampoline pointers before the drain in quiesce_xinput_routes_until_deadline(). A
3333 // late entrant sees nullptr instead of a pointer into a hook near destruction. seq_cst places these stores
3334 // and the drain load in the detour total order.
3335 const XInputPublishedChains published_chains{
3336 207 s_xinput_original.load(std::memory_order_seq_cst) != nullptr,
3337 414 s_xinput_ex_original.load(std::memory_order_seq_cst) != nullptr
3338 207 };
3339 207 s_xinput_ex_original.store(nullptr, std::memory_order_seq_cst);
3340 207 s_xinput_original.store(nullptr, std::memory_order_seq_cst);
3341 207 return published_chains;
3342 }
3343
3344 /**
3345 * @brief Waits a bounded time for detours that already copied a trampoline.
3346 * @return True if a detour body or route entry is in flight after the bounded wait.
3347 * @note Requires s_intercept_mutex and a constructed process-lifetime cell.
3348 */
3349 207 [[nodiscard]] bool quiesce_xinput_routes_until_deadline() noexcept
3350 {
3351 // Use a wall-clock bound, not a yield count. A hot game thread can enter after pointer retirement, and
3352 // teardown must still progress.
3353 207 constexpr uint64_t xinput_quiesce_timeout_ms = 10;
3354 207 const uint64_t quiesce_deadline_ms = GetTickCount64() + xinput_quiesce_timeout_ms;
3355
3/4
✓ Branch 14 → 15 taken 3916 times.
✓ Branch 14 → 21 taken 108802 times.
✓ Branch 16 → 17 taken 3916 times.
✗ Branch 16 → 21 not taken.
116841 while ((s_xinput_inflight.load(std::memory_order_seq_cst) != 0 || xinput_route_probe_inflight() != 0 ||
3356
3/4
✓ Branch 18 → 19 taken 203 times.
✓ Branch 18 → 21 taken 3713 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 203 times.
8035 active_xinput_raw_scopes() != 0 || s_xinput_permanent_hooks->primary.route_entries() != 0 ||
3357
4/4
✓ Branch 12 → 13 taken 112718 times.
✓ Branch 12 → 21 taken 19565 times.
✓ Branch 25 → 4 taken 132076 times.
✓ Branch 25 → 26 taken 207 times.
264769 s_xinput_permanent_hooks->ex.route_entries() != 0) &&
3358
2/2
✓ Branch 22 → 23 taken 132076 times.
✓ Branch 22 → 24 taken 4 times.
132080 GetTickCount64() < quiesce_deadline_ms)
3359 {
3360 132076 std::this_thread::yield();
3361 }
3362
3363
3/4
✓ Branch 35 → 36 taken 204 times.
✓ Branch 35 → 42 taken 2 times.
✓ Branch 37 → 38 taken 204 times.
✗ Branch 37 → 42 not taken.
410 return s_xinput_inflight.load(std::memory_order_seq_cst) != 0 || xinput_route_probe_inflight() != 0 ||
3364
5/6
✓ Branch 33 → 34 taken 206 times.
✓ Branch 33 → 42 taken 1 time.
✓ Branch 39 → 40 taken 203 times.
✓ Branch 39 → 42 taken 1 time.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 203 times.
820 active_xinput_raw_scopes() != 0 || s_xinput_permanent_hooks->primary.route_entries() != 0 ||
3365 410 s_xinput_permanent_hooks->ex.route_entries() != 0;
3366 }
3367
3368 /**
3369 * @brief Re-arms the restored Ex member after a refused primary restore.
3370 * @return The Ex teardown witness, or PatchWitness::Original if the re-arm did not restore the forward path.
3371 * @note Requires s_intercept_mutex and a constructed process-lifetime cell.
3372 */
3373 4 [[nodiscard]] PatchWitness compensate_xinput_ex_restore() noexcept
3374 {
3375 4 return rearm_xinput_hook(
3376 4 s_xinput_permanent_hooks->ex,
3377 s_xinput_ex_original,
3378 s_xinput_ex_enable_warned,
3379 4 "InputIntercept: the XInputGetStateEx re-arm that compensates a refused primary "
3380 "restore did not complete cleanly; Ex state was reconciled from the target bytes."
3381 )
3382
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 1 time.
4 ? xinput_teardown_witness(s_xinput_permanent_hooks->ex)
3383 1 : PatchWitness::Original;
3384 }
3385
3386 /// Returns the first pair witness that refuses a restore, or PatchWitness::Original when both permit one.
3387 203 [[nodiscard]] constexpr PatchWitness restore_refusal(PatchWitness primary, PatchWitness ex) noexcept
3388 {
3389
2/2
✓ Branch 3 → 4 taken 70 times.
✓ Branch 3 → 5 taken 133 times.
203 if (!witness_permits_write(primary))
3390 {
3391 70 return primary;
3392 }
3393
2/2
✓ Branch 6 → 7 taken 131 times.
✓ Branch 6 → 8 taken 2 times.
133 return witness_permits_write(ex) ? PatchWitness::Original : ex;
3394 }
3395
3396 /// Records a route-bypass retirement for the report after s_intercept_mutex releases.
3397 struct XInputBypassLog
3398 {
3399 /// The witness that refused the restore, or PatchWitness::Original when only claimed positions retired.
3400 PatchWitness refusal{PatchWitness::Original};
3401 const char *primary_retained{nullptr};
3402 const char *ex_retained{nullptr};
3403 bool pending{false};
3404 };
3405
3406 /**
3407 * @brief Retires the drained pair by route bypass and gives each position to a later generation.
3408 * @details No step writes a target byte. Each closed route forwards to its trampoline and reaches no code of
3409 * this image, so the self-reference goes. A fresh position books HookResidue and keeps the target
3410 * references for good. A claimed position relies on the references of the generation that first
3411 * vacated it. The primary vacates first, so after a refused Ex vacate no primary hook remains that a
3412 * later maintenance can publish without its partner.
3413 * @return False when the coordinator is unavailable, a fresh position exceeds the residue budget, or a vacate
3414 * refused. A member that did not vacate keeps its state, and the caller retains it.
3415 * @note Requires s_intercept_mutex, a constructed process-lifetime cell, and routes that drained while CLOSING.
3416 */
3417 84 [[nodiscard]] bool retire_xinput_pair_by_bypass(PatchWitness refusal, XInputBypassLog &deferred_log) noexcept
3418 {
3419 84 PermanentXInputHooks *const permanent = permanent_cell();
3420 const std::array<safetyhook::InlineHook *, 2> members{
3421 84 &permanent->primary,
3422 84 &permanent->ex,
3423 84 };
3424 // A fresh member without a committed patch has no target entry, so it resets as on the clean path.
3425 336 const auto vacates = [](const safetyhook::InlineHook &member) noexcept -> bool
3426
4/6
✓ Branch 3 → 4 taken 336 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 108 times.
✓ Branch 5 → 8 taken 228 times.
✓ Branch 7 → 8 taken 108 times.
✗ Branch 7 → 9 not taken.
336 { return member && (member.claimed() || member.enabled()); };
3427 84 bool fresh_vacated = false;
3428 {
3429 // Each vacate acquires the held coordinator again without a wait.
3430 84 const safetyhook::ProcessCoordinator coordinator;
3431
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 84 times.
84 if (!coordinator)
3432 {
3433 ✗ return false;
3434 }
3435
2/2
✓ Branch 22 → 9 taken 168 times.
✓ Branch 22 → 23 taken 84 times.
252 for (const safetyhook::InlineHook *const member : members)
3436 {
3437
4/6
✓ Branch 10 → 11 taken 168 times.
✗ Branch 10 → 18 not taken.
✓ Branch 12 → 13 taken 54 times.
✓ Branch 12 → 18 taken 114 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 168 times.
222 if (vacates(*member) && !member->claimed() &&
3438
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 54 times.
54 !residue_fits_budget(coordinator, member->trampoline().data()))
3439 {
3440 ✗ return false;
3441 }
3442 }
3443
2/2
✓ Branch 43 → 25 taken 168 times.
✓ Branch 43 → 44 taken 84 times.
252 for (safetyhook::InlineHook *const member : members)
3444 {
3445
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 168 times.
168 if (!vacates(*member))
3446 {
3447 ✗ continue;
3448 }
3449 168 const bool fresh = !member->claimed();
3450 // A claimed member reports bypassed only after bypass() clears its enabled state.
3451
5/6
✓ Branch 29 → 30 taken 114 times.
✓ Branch 29 → 32 taken 54 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 114 times.
✓ Branch 34 → 35 taken 54 times.
✓ Branch 34 → 36 taken 114 times.
168 if (fresh || !member->bypass())
3452 {
3453 54 member->finish_route_rundown();
3454 }
3455
1/2
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 168 times.
168 if (!member->vacate())
3456 {
3457 ✗ return false;
3458 }
3459
2/2
✓ Branch 39 → 40 taken 54 times.
✓ Branch 39 → 42 taken 114 times.
168 if (fresh)
3460 {
3461 54 diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::HookResidue);
3462 54 fresh_vacated = true;
3463 }
3464 }
3465
1/2
✓ Branch 46 → 47 taken 84 times.
✗ Branch 46 → 50 not taken.
84 }
3466
3467 84 deferred_log.ex_retained =
3468
1/2
✗ Branch 49 → 51 not taken.
✓ Branch 49 → 52 taken 84 times.
84 permanent->ex ? reset_inactive_xinput_hook(permanent->ex, s_xinput_ex_original) : nullptr;
3469 84 deferred_log.primary_retained =
3470
1/2
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 84 times.
84 permanent->primary ? reset_inactive_xinput_hook(permanent->primary, s_xinput_original) : nullptr;
3471
2/4
✓ Branch 57 → 58 taken 84 times.
✗ Branch 57 → 59 not taken.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 60 taken 84 times.
84 if (deferred_log.primary_retained != nullptr || deferred_log.ex_retained != nullptr)
3472 {
3473 ✗ retire_xinput_module_refs(XInputChainReach::ProviderAndImage);
3474 }
3475 else
3476 {
3477
2/2
✓ Branch 60 → 61 taken 27 times.
✓ Branch 60 → 62 taken 57 times.
84 retire_xinput_module_refs(fresh_vacated ? XInputChainReach::Provider : XInputChainReach::Nothing);
3478 }
3479 84 clear_xinput_install_state();
3480 84 deferred_log.refusal = refusal;
3481 84 deferred_log.pending = true;
3482 84 return true;
3483 }
3484
3485 /** @brief Reports a route-bypass retirement after its caller releases s_intercept_mutex. */
3486 84 void emit_xinput_bypass_log(const XInputBypassLog &deferred_log) noexcept
3487 {
3488
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 84 times.
84 if (!deferred_log.pending)
3489 {
3490 ✗ return;
3491 }
3492
2/2
✓ Branch 4 → 5 taken 12 times.
✓ Branch 4 → 9 taken 72 times.
84 if (deferred_log.refusal == PatchWitness::Original)
3493 {
3494 24 (void)log().log_noexcept(
3495 LogLevel::Info,
3496 12 "InputIntercept: the XInput pair retired its claimed positions."
3497 );
3498 }
3499 else
3500 {
3501 144 (void)log().try_log(
3502 LogLevel::Info,
3503 "InputIntercept: the XInput pair retired by route bypass under a newer layer: {}. Its positions "
3504 "stay for a later generation.",
3505 144 witness_description(deferred_log.refusal)
3506 );
3507 }
3508 84 emit_xinput_route_retention_logs(deferred_log.primary_retained, deferred_log.ex_retained);
3509 }
3510 } // anonymous namespace
3511
3512 762 void uninstall(std::uint64_t owner) noexcept
3513 {
3514 762 InterceptLockGuard lock{s_intercept_mutex};
3515 762 XInputRetentionLog deferred_log;
3516
5/6
✓ Branch 3 → 4 taken 762 times.
✗ Branch 3 → 12 not taken.
✓ Branch 11 → 12 taken 472 times.
✓ Branch 11 → 13 taken 290 times.
✓ Branch 14 → 15 taken 472 times.
✓ Branch 14 → 16 taken 290 times.
1524 if (owner == 0 || s_intercept_owner.load(std::memory_order_relaxed) != owner)
3517 {
3518 472 return;
3519 }
3520
3521 // revoke_owner_and_clear_data() atomically revokes the layer and clears its data before teardown. The active
3522 // InterceptLockGuard serializes teardown. Data-plane writers use s_data_plane_mutex, so a revoked owner cannot
3523 // publish live state.
3524 290 revoke_owner_and_clear_data();
3525
3526 290 uninstall_message_hook();
3527
3528
2/2
✓ Branch 19 → 20 taken 9 times.
✓ Branch 19 → 22 taken 281 times.
290 if (s_xinput_permanent_detour.load(std::memory_order_acquire))
3529 {
3530 // A prior timeout or uncertain restore made the canonical hooks permanent.
3531 // This call only disarms the logical layer. A reachable retained entry continues to forward.
3532 9 clear_xinput_install_state();
3533 9 return;
3534 }
3535
3536
2/2
✓ Branch 22 → 23 taken 74 times.
✓ Branch 22 → 27 taken 207 times.
281 if (s_xinput_permanent_hooks == nullptr)
3537 {
3538 74 s_xinput_installed.store(false, std::memory_order_release);
3539 74 s_xinput_pair_degraded.store(false, std::memory_order_release);
3540 74 xinput_recovery_reset();
3541 74 return;
3542 }
3543
3544 // The exit structure resolves both routes from their byte witnesses. Retention exits use retain_xinput_hooks.
3545 // The bypass exit uses retire_xinput_pair_by_bypass. The clean exit uses reset_inactive_xinput_hook.
3546 207 const XInputPublishedChains published_chains = retire_published_xinput_chains();
3547 207 const bool route_still_inflight = quiesce_xinput_routes_until_deadline();
3548
2/2
✓ Branch 29 → 30 taken 4 times.
✓ Branch 29 → 36 taken 203 times.
207 if (route_still_inflight)
3549 {
3550 4 retain_xinput_hooks(
3551 4 xinput_teardown_witness(s_xinput_permanent_hooks->primary),
3552 4 xinput_teardown_witness(s_xinput_permanent_hooks->ex),
3553 XInputRetentionReason::InflightTimeout,
3554 deferred_log,
3555 published_chains
3556 );
3557 4 lock.unlock();
3558 4 emit_xinput_retention_log(deferred_log);
3559 4 return;
3560 }
3561
3562 // Classify both targets before either backend restore. A newer layer on either prologue or a claimed position
3563 // retires the whole pair by route bypass, so no partial teardown overwrites that layer. Retention remains
3564 // only for the refusals that retire_xinput_pair_by_bypass names.
3565 203 const PatchWitness primary_before = xinput_teardown_witness(s_xinput_permanent_hooks->primary);
3566 203 const PatchWitness ex_before = xinput_teardown_witness(s_xinput_permanent_hooks->ex);
3567 203 const PatchWitness refusal = restore_refusal(primary_before, ex_before);
3568
7/8
✓ Branch 39 → 40 taken 131 times.
✓ Branch 39 → 44 taken 72 times.
✓ Branch 41 → 42 taken 119 times.
✓ Branch 41 → 44 taken 12 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 119 times.
✓ Branch 46 → 47 taken 84 times.
✓ Branch 46 → 56 taken 119 times.
322 if (refusal != PatchWitness::Original || s_xinput_permanent_hooks->primary.claimed() ||
3569 119 s_xinput_permanent_hooks->ex.claimed())
3570 {
3571 84 XInputBypassLog bypass_log;
3572
1/2
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 52 taken 84 times.
84 if (!retire_xinput_pair_by_bypass(refusal, bypass_log))
3573 {
3574 ✗ retain_xinput_hooks(
3575 ✗ xinput_teardown_witness(s_xinput_permanent_hooks->primary),
3576 ✗ xinput_teardown_witness(s_xinput_permanent_hooks->ex),
3577 XInputRetentionReason::UnrestoredPatch,
3578 deferred_log
3579 );
3580 }
3581 84 lock.unlock();
3582 84 emit_xinput_bypass_log(bypass_log);
3583 84 emit_xinput_retention_log(deferred_log);
3584 84 return;
3585 }
3586
3587 // Restore Ex first, then primary. A caught exception can occur before or after either mutation, so the byte
3588 // witness after each call is authoritative. Only Original permits the hook object and keepalives to be freed.
3589 119 const PatchWitness ex_after = restore_xinput_hook(s_xinput_permanent_hooks->ex);
3590
2/2
✓ Branch 57 → 58 taken 1 time.
✓ Branch 57 → 62 taken 118 times.
119 if (ex_after != PatchWitness::Original)
3591 {
3592 1 retain_xinput_hooks(primary_before, ex_after, XInputRetentionReason::UnrestoredPatch, deferred_log);
3593 1 lock.unlock();
3594 1 emit_xinput_retention_log(deferred_log);
3595 1 return;
3596 }
3597 118 const PatchWitness primary_after = restore_xinput_hook(s_xinput_permanent_hooks->primary);
3598
2/2
✓ Branch 63 → 64 taken 4 times.
✓ Branch 63 → 69 taken 114 times.
118 if (primary_after != PatchWitness::Original)
3599 {
3600 // The pair is one transaction and this half did not commit. Put the Ex member back first. Immediate
3601 // retention publishes a primary-only chain and permanently drops the covered ordinal-100 entry point.
3602 // Compensation reuses the current hook, and its own witness gate declines rather than fight a newer
3603 // writer.
3604 4 const PatchWitness ex_compensated = compensate_xinput_ex_restore();
3605 4 retain_xinput_hooks(primary_after, ex_compensated, XInputRetentionReason::UnrestoredPatch, deferred_log);
3606 4 lock.unlock();
3607 4 emit_xinput_retention_log(deferred_log);
3608 4 return;
3609 }
3610
3611 #if defined(DMK_ENABLE_TEST_SEAMS)
3612
2/2
✓ Branch 70 → 71 taken 4 times.
✓ Branch 70 → 72 taken 110 times.
114 if (const XInputCleanReleaseSeam seam = s_xinput_clean_release_seam.load(std::memory_order_acquire);
3613 seam != nullptr)
3614 {
3615 4 seam();
3616 }
3617 #endif
3618
3619 114 const char *const ex_retained = reset_inactive_xinput_hook(s_xinput_permanent_hooks->ex, s_xinput_ex_original);
3620 const char *const primary_retained =
3621 114 reset_inactive_xinput_hook(s_xinput_permanent_hooks->primary, s_xinput_original);
3622
3623 // A retained chain can still reach a code provider after its detour body drains.
3624
4/4
✓ Branch 74 → 75 taken 110 times.
✓ Branch 74 → 76 taken 4 times.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 75 → 77 taken 109 times.
114 retire_xinput_module_refs(reset_chain_reach(primary_retained || ex_retained));
3625
3626 114 clear_xinput_install_state();
3627 114 lock.unlock();
3628 114 emit_xinput_route_retention_logs(primary_retained, ex_retained);
3629
2/2
✓ Branch 85 → 86 taken 114 times.
✓ Branch 85 → 88 taken 648 times.
762 }
3630
3631 } // namespace DetourModKit::detail
3632