diff --git a/Extension/UI/Overlay/input_capture.cpp b/Extension/UI/Overlay/input_capture.cpp index 79c687a8..6a19a268 100644 --- a/Extension/UI/Overlay/input_capture.cpp +++ b/Extension/UI/Overlay/input_capture.cpp @@ -77,6 +77,24 @@ BOOLEAN __stdcall captured_hid_attributes(HANDLE file, PHIDD_ATTRIBUTES attribut SetLastError(error); return result; } +std::atomic& gamepad_filter() { + static std::atomic value{}; + return value; +} +// Full Bluetooth reports have a CRC over the 0xA1 input prefix and payload. +// A stale checksum makes the native reader discard a changed report. +void refresh_bluetooth_crc(std::uint8_t* bytes, DWORD size) noexcept { + if (size != 78 || (bytes[0] != 0x31 && bytes[0] != 0x11)) return; + std::uint32_t crc = 0xffffffffu; + const auto feed = [&](std::uint8_t byte) { + crc ^= byte; + for (unsigned bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u))); + }; + feed(0xa1); + for (DWORD i = 0; i < size - 4; ++i) feed(bytes[i]); + crc = ~crc; + std::memcpy(bytes + size - 4, &crc, sizeof(crc)); +} void neutral_hid(const InputCaptureHooks::HidRead& read, DWORD size) { if (!block_polled_input() || !read.buffer || size > read.capacity) return; auto* bytes = static_cast(read.buffer); @@ -100,25 +118,62 @@ void neutral_hid(const InputCaptureHooks::HidRead& read, DWORD size) { data[4] = 8; data[5] = 0; data[6] &= 0xfc; // retain DS4 sequence bits data[7] = data[8] = 0; } - // Full Bluetooth reports have a CRC over the 0xA1 input prefix and payload. - // A stale checksum makes the native reader discard the neutral report. - if (size == 78 && (bytes[0] == 0x31 || bytes[0] == 0x11)) { - std::uint32_t crc = 0xffffffffu; - const auto feed = [&](std::uint8_t byte) { - crc ^= byte; - for (unsigned bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u))); - }; - feed(0xa1); - for (DWORD i = 0; i < size - 4; ++i) feed(bytes[i]); - crc = ~crc; - std::memcpy(bytes + size - 4, &crc, sizeof(crc)); - } + refresh_bluetooth_crc(bytes, size); static std::atomic reported{}; if (!reported.exchange(true)) logging::printf(logging::Level::info, logging::Channel::input, "Native PlayStation HID report neutralized; overlay retains its independent input stream. (%s over %s, report 0x%02x of %lu bytes, " "the game's buffer %lu.)", read.kind == PlayStationPad::dualsense ? "DualSense" : "DualShock 4", bluetooth ? "Bluetooth" : "USB", bytes[0], size, read.capacity); } +// The gamepad filter (DingoSDKOverlaySetGamepadFilter) on the game's own native PlayStation +// reads, as filter_pad applies it to XInput: a DualShock 4 or DualSense read without Steam Input +// never goes through XInput. The report is parsed into XInput's layout, filtered, and what the +// filter changed (triggers, left stick X) is written back in the report's own layout. +inline constexpr unsigned hid_filter_slot = 3; // the filter's pad slot for native PlayStation pads +void filter_hid(const InputCaptureHooks::HidRead& read, DWORD size) noexcept { + const auto filter = gamepad_filter().load(std::memory_order_acquire); + if (!filter || !read.buffer || !size || size > read.capacity) return; + auto* bytes = static_cast(read.buffer); + const bool bluetooth = playstation_bluetooth(size); + const auto parsed = parse_playstation_report(read.kind, bluetooth, bytes, size); + if (!parsed) return; + const dingosdk::overlay::GamepadValues before{parsed->buttons, parsed->left_trigger, parsed->right_trigger, + parsed->left_x, parsed->left_y, parsed->right_x, parsed->right_y}; + auto pad = before; + filter(hid_filter_slot, pad); + if (pad.left_trigger == before.left_trigger && pad.right_trigger == before.right_trigger && pad.left_x == before.left_x) return; + // Where parse_playstation_report found them: sticks (left X first), the shoulder byte (L2 0x04, + // R2 0x08 digital bits) and the two analog triggers. + std::uint8_t *sticks{}, *shoulder{}, *left{}, *right{}; + const auto id = bytes[0]; + if (read.kind == PlayStationPad::dualsense && ((id == 0x01 && !bluetooth) || id == 0x31)) { + auto* data = bytes + (id == 0x31 ? 2 : 1); + sticks = data; left = data + 4; right = data + 5; shoulder = data + 8; + } else if (id == 0x01) { + sticks = bytes + 1; shoulder = bytes + 6; left = bytes + 8; right = bytes + 9; + } else if (read.kind == PlayStationPad::dualshock4 && id == 0x11) { + sticks = bytes + 3; shoulder = bytes + 8; left = bytes + 10; right = bytes + 11; + } else { + return; + } + if (pad.left_trigger != before.left_trigger) { + *left = pad.left_trigger; + if (!pad.left_trigger) *shoulder = static_cast(*shoulder & ~0x04u); + } + if (pad.right_trigger != before.right_trigger) { + *right = pad.right_trigger; + if (!pad.right_trigger) *shoulder = static_cast(*shoulder & ~0x08u); + } + if (pad.left_x != before.left_x) + sticks[0] = static_cast(std::clamp(static_cast(pad.left_x) / 258 + 128, 0, 255)); + refresh_bluetooth_crc(bytes, size); +} +// A finished native PlayStation read of the game's: neutral while a menu holds the input, else +// through the gamepad filter. +void handle_hid(const InputCaptureHooks::HidRead& read, DWORD size) noexcept { + if (block_polled_input()) neutral_hid(read, size); + else filter_hid(read, size); +} void complete_hid(OVERLAPPED* request, DWORD bytes, bool success, bool finished) { auto& h = input_hooks(); InputCaptureHooks::HidRead read{}; @@ -129,7 +184,7 @@ void complete_hid(OVERLAPPED* request, DWORD bytes, bool success, bool finished) read = found->second; if (finished) h.hid_reads.erase(found); } - if (success) neutral_hid(read, bytes); + if (success) handle_hid(read, bytes); } BOOL WINAPI captured_read_file(HANDLE file, LPVOID buffer, DWORD size, LPDWORD bytes, LPOVERLAPPED request) { auto& h = input_hooks(); @@ -163,7 +218,7 @@ BOOL WINAPI captured_read_file(HANDLE file, LPVOID buffer, DWORD size, LPDWORD b const auto result = original(h.read_file)(file, buffer, size, bytes, request); const auto error = GetLastError(); if (read.kind != PlayStationPad::none) { - if (result) neutral_hid(read, bytes ? *bytes : request ? static_cast(request->InternalHigh) : 0); + if (result) handle_hid(read, bytes ? *bytes : request ? static_cast(request->InternalHigh) : 0); if (!result && error != ERROR_IO_PENDING && request) complete_hid(request, 0, false, true); } SetLastError(error); @@ -266,10 +321,6 @@ std::string xinput_entry_owners() { return text.empty() ? "none" : text; } -std::atomic& gamepad_filter() { - static std::atomic value{}; - return value; -} // The game's own read only: not ReSkate's (overlay_input_access) and not an inner layer of a // read already being answered (Steam's hook going on to XInput runs inside OverlayInputAccess). void filter_pad(bool outer, DWORD user, XINPUT_STATE* output) noexcept {