From 71c56bd8422abaf9ace6ae80bcd62d4cc2b16757 Mon Sep 17 00:00:00 2001 From: Heinz Kirste Date: Wed, 1 Apr 2026 20:52:44 -0600 Subject: [PATCH 1/5] chore: add WinUHid submodule for virtual HID gamepad emulation Add cgutman/WinUHid as a submodule. WinUHid is a UMDF driver using Microsoft's VHF (Virtual HID Framework) that enables user-mode applications to create virtual HID devices. It provides ready-made DualSense (PS5), DualShock 4, and Xbox One controller emulation with full feature support including adaptive triggers, motion sensors, touchpad, and lightbar control. --- .gitmodules | 3 +++ third-party/WinUHid | 1 + 2 files changed, 4 insertions(+) create mode 160000 third-party/WinUHid diff --git a/.gitmodules b/.gitmodules index a69e4ee4cbf..5d4f631d007 100644 --- a/.gitmodules +++ b/.gitmodules @@ -69,3 +69,6 @@ path = third-party/wlr-protocols url = https://github.com/LizardByte-infrastructure/wlr-protocols.git branch = master +[submodule "third-party/WinUHid"] + path = third-party/WinUHid + url = https://github.com/cgutman/WinUHid.git diff --git a/third-party/WinUHid b/third-party/WinUHid new file mode 160000 index 00000000000..d6cebbef5c7 --- /dev/null +++ b/third-party/WinUHid @@ -0,0 +1 @@ +Subproject commit d6cebbef5c7909168d1f881185be8f607d6aefd4 From e4f62bb9a92c58ce05ccc98451a24ae297160007 Mon Sep 17 00:00:00 2001 From: Heinz Kirste Date: Wed, 1 Apr 2026 20:52:59 -0600 Subject: [PATCH 2/5] build: integrate WinUHid into Windows CMake build - Add SUNSHINE_ENABLE_WINUHID CMake option (ON by default) - Compile WinUHid and WinUHidPS5 sources as static (WINUHID_STATIC) - Add MinGW-compatible WRL shim for Wrappers::Event/FileHandle (WinUHid uses MSVC WRL for RAII handle wrappers; this shim provides equivalent types for MinGW builds) - Rename DllMain per-file via -D to avoid duplicate symbol errors - Define SUNSHINE_WINUHID preprocessor guard for conditional code --- cmake/compile_definitions/windows.cmake | 36 ++++++++++ .../wrl/wrappers/corewrappers.h | 66 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 third-party/WinUHid-compat/wrl/wrappers/corewrappers.h diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 843dafe61e5..5eb4b882bdd 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -40,6 +40,17 @@ file(GLOB NVPREFS_FILES CONFIGURE_DEPENDS # vigem include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include") +# winuhid - virtual HID gamepad emulation (DualSense, DS4, Xbox One) +option(SUNSHINE_ENABLE_WINUHID "Enable WinUHid virtual gamepad backend for DualSense support" ON) +if(SUNSHINE_ENABLE_WINUHID) + list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_WINUHID) + # WRL compatibility shim must come before WinUHid includes so MinGW finds our shim + # instead of looking for MSVC's wrl/wrappers/corewrappers.h + include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/WinUHid-compat") + include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHid") + include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs") +endif() + # sunshine icon if(NOT DEFINED SUNSHINE_ICON_PATH) set(SUNSHINE_ICON_PATH "${CMAKE_SOURCE_DIR}/sunshine.ico") @@ -80,6 +91,31 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/km/BusShared.h" ${NVPREFS_FILES}) +if(SUNSHINE_ENABLE_WINUHID) + list(APPEND PLATFORM_TARGET_FILES + "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHid/WinUHid.cpp" + "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidDevs.cpp" + "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidPS5.cpp") + + # WinUHid is compiled as static source — no DLL export/import. + # DllMain is renamed per-file to avoid duplicate symbol errors when linking. + set_source_files_properties( + "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHid/WinUHid.cpp" + PROPERTIES COMPILE_DEFINITIONS "WINUHID_STATIC;DllMain=WinUHid_DllMain_Unused" + COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" + ) + set_source_files_properties( + "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidDevs.cpp" + PROPERTIES COMPILE_DEFINITIONS "WINUHID_STATIC;DllMain=WinUHidDevs_DllMain_Unused" + COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" + ) + set_source_files_properties( + "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidPS5.cpp" + PROPERTIES COMPILE_DEFINITIONS "WINUHID_STATIC" + COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" + ) +endif() + set(OPENSSL_LIBRARIES libssl.a libcrypto.a) diff --git a/third-party/WinUHid-compat/wrl/wrappers/corewrappers.h b/third-party/WinUHid-compat/wrl/wrappers/corewrappers.h new file mode 100644 index 00000000000..8e502ac9299 --- /dev/null +++ b/third-party/WinUHid-compat/wrl/wrappers/corewrappers.h @@ -0,0 +1,66 @@ +/** + * @file wrl/wrappers/corewrappers.h + * @brief MinGW-compatible shim for Microsoft WRL RAII handle wrappers. + * @details WinUHid uses Wrappers::Event, Wrappers::FileHandle, and + * Wrappers::HandleT from WRL for RAII handle management. + * This header provides equivalent implementations for MinGW builds. + */ +#pragma once + +#include + +namespace Microsoft { +namespace WRL { +namespace Wrappers { + + namespace HandleTraits { + + struct HANDLETraits { + static HANDLE GetInvalidValue() { return INVALID_HANDLE_VALUE; } + }; + + struct HANDLENullTraits { + static HANDLE GetInvalidValue() { return nullptr; } + }; + + } // namespace HandleTraits + + template + class HandleT { + public: + HandleT() noexcept : handle_(Traits::GetInvalidValue()) {} + explicit HandleT(HANDLE h) noexcept : handle_(h) {} + HandleT(HandleT &&other) noexcept : handle_(other.handle_) { other.handle_ = Traits::GetInvalidValue(); } + HandleT &operator=(HandleT &&other) noexcept { + if (this != &other) { + Close(); + handle_ = other.handle_; + other.handle_ = Traits::GetInvalidValue(); + } + return *this; + } + HandleT(const HandleT &) = delete; + HandleT &operator=(const HandleT &) = delete; + ~HandleT() { Close(); } + + bool IsValid() const noexcept { return handle_ != Traits::GetInvalidValue(); } + HANDLE Get() const noexcept { return handle_; } + void Attach(HANDLE h) noexcept { Close(); handle_ = h; } + HANDLE Detach() noexcept { HANDLE h = handle_; handle_ = Traits::GetInvalidValue(); return h; } + void Close() noexcept { + if (IsValid()) { + CloseHandle(handle_); + handle_ = Traits::GetInvalidValue(); + } + } + + private: + HANDLE handle_; + }; + + using Event = HandleT; + using FileHandle = HandleT; + +} // namespace Wrappers +} // namespace WRL +} // namespace Microsoft From d08be764326456ac63cf1b99296c80db458af695 Mon Sep 17 00:00:00 2001 From: Heinz Kirste Date: Wed, 1 Apr 2026 20:53:21 -0600 Subject: [PATCH 3/5] feat(input/windows): add WinUHid backend for native DualSense emulation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a WinUHid-based gamepad backend alongside the existing ViGEmBus backend. When the WinUHid driver is installed, Sunshine can now create virtual DualSense (PS5) controllers that games recognize natively, enabling features that were impossible with DS4 emulation: - Adaptive trigger effects flow from games back to the Moonlight client's physical DualSense controller - RGB lightbar color feedback is now forwarded (was discarded) - Motion sensor data passes through without inverse-calibration hacks (WinUHid provides proper calibration in feature reports) - Touchpad uses native 1920x1080 DualSense resolution (was 1920x943) - Games and Steam detect the controller as a real DualSense Architecture: - winuhid_t manager class parallel to vigem_t - winuhid_gamepad_context_t holds per-gamepad state and PS5 report - Four WinUHid callbacks (rumble, lightbar, player LED, adaptive triggers) push feedback onto the existing feedback_queue - No changes needed to stream.cpp — the adaptive trigger and LED wire protocol support already existed from the Linux DS5 work - Backend selection: WinUHid preferred for PS-type controllers, ViGEm fallback for Xbox 360 or when WinUHid is not installed - All WinUHid code guarded by #ifdef SUNSHINE_WINUHID Adds "ds5" to supported_gamepads when WinUHid is available. --- src/platform/windows/input.cpp | 401 ++++++++++++++++++++++++++++++++- 1 file changed, 389 insertions(+), 12 deletions(-) diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 28fe6fa5749..2bac7fbba9f 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -15,6 +15,12 @@ // lib includes #include +#ifdef SUNSHINE_WINUHID + #define WINUHID_STATIC + #include + #include +#endif + // local includes #include "keylayout.h" #include "misc.h" @@ -431,12 +437,176 @@ namespace platf { task_pool.push(&vigem_t::set_rgb_led, (vigem_t *) userdata, target, led_color.Red, led_color.Green, led_color.Blue); } +#ifdef SUNSHINE_WINUHID + /** + * @brief Per-gamepad context for WinUHid backend. + */ + struct winuhid_gamepad_context_t { + PWINUHID_PS5_GAMEPAD ps5 = nullptr; + WINUHID_PS5_INPUT_REPORT ps5_report {}; + + feedback_queue_t feedback_queue; + uint8_t client_relative_index = 0; + + // Touchpad pointer tracking (same pattern as DS4 path) + std::map pointer_id_map; + uint8_t available_pointers = 0x3; + + // Dedup caches to avoid sending duplicate feedback messages + gamepad_feedback_msg_t last_rumble {}; + gamepad_feedback_msg_t last_rgb_led {}; + + bool is_active() const { return ps5 != nullptr; } + + void reset() { + if (ps5) { + WinUHidPS5Destroy(ps5); + ps5 = nullptr; + } + pointer_id_map.clear(); + available_pointers = 0x3; + last_rumble = {}; + last_rgb_led = {}; + } + }; + + /** + * @brief WinUHid gamepad manager — provides DualSense emulation on Windows. + */ + struct winuhid_t { + bool available = false; + std::vector gamepads; + + int init() { + DWORD version = WinUHidGetDriverInterfaceVersion(); + if (version == 0) { + BOOST_LOG(info) << "WinUHid driver not found. DualSense emulation unavailable. Install WinUHid for PS5 controller support."sv; + return -1; + } + + BOOST_LOG(info) << "WinUHid driver detected (interface version "sv << version << "). DualSense emulation available."sv; + available = true; + gamepads.resize(MAX_GAMEPADS); + return 0; + } + + static void CALLBACK rumble_cb(PVOID ctx, UCHAR leftMotor, UCHAR rightMotor) { + auto *gp = (winuhid_gamepad_context_t *) ctx; + + // Scale 8-bit to 16-bit: 0xFF -> 0xFFFF + uint16_t normalizedLeft = (uint16_t) leftMotor << 8 | leftMotor; + uint16_t normalizedRight = (uint16_t) rightMotor << 8 | rightMotor; + + auto msg = gamepad_feedback_msg_t::make_rumble(gp->client_relative_index, normalizedLeft, normalizedRight); + if (msg.data.rumble.lowfreq != gp->last_rumble.data.rumble.lowfreq || + msg.data.rumble.highfreq != gp->last_rumble.data.rumble.highfreq) { + gp->feedback_queue->raise(msg); + gp->last_rumble = msg; + } + } + + static void CALLBACK lightbar_cb(PVOID ctx, UCHAR r, UCHAR g, UCHAR b) { + auto *gp = (winuhid_gamepad_context_t *) ctx; + + auto msg = gamepad_feedback_msg_t::make_rgb_led(gp->client_relative_index, r, g, b); + if (msg.data.rgb_led.r != gp->last_rgb_led.data.rgb_led.r || + msg.data.rgb_led.g != gp->last_rgb_led.data.rgb_led.g || + msg.data.rgb_led.b != gp->last_rgb_led.data.rgb_led.b) { + gp->feedback_queue->raise(msg); + gp->last_rgb_led = msg; + } + } + + static void CALLBACK player_led_cb(PVOID ctx, UCHAR ledValue) { + // Moonlight protocol doesn't support player LED feedback yet + (void) ctx; + (void) ledValue; + } + + static void CALLBACK trigger_effect_cb(PVOID ctx, PCWINUHID_PS5_TRIGGER_EFFECT left, PCWINUHID_PS5_TRIGGER_EFFECT right) { + auto *gp = (winuhid_gamepad_context_t *) ctx; + + uint8_t event_flags = 0; + uint8_t type_left = 0; + uint8_t type_right = 0; + std::array data_left {}; + std::array data_right {}; + + if (left) { + event_flags |= 0x08; + type_left = left->Type; + std::copy_n(left->Data, 10, data_left.begin()); + } + if (right) { + event_flags |= 0x04; + type_right = right->Type; + std::copy_n(right->Data, 10, data_right.begin()); + } + + gp->feedback_queue->raise( + gamepad_feedback_msg_t::make_adaptive_triggers( + gp->client_relative_index, event_flags, type_left, type_right, data_left, data_right)); + } + + int alloc_gamepad(const gamepad_id_t &id, feedback_queue_t &feedback_queue) { + auto &ctx = gamepads[id.globalIndex]; + assert(!ctx.is_active()); + + ctx.client_relative_index = id.clientRelativeIndex; + ctx.available_pointers = 0x3; + + WinUHidPS5InitializeInputReport(&ctx.ps5_report); + + // Generate a deterministic MAC from gamepad index for stable Steam controller identity + WINUHID_PS5_GAMEPAD_INFO info {}; + info.MacAddress[0] = 0x00; + info.MacAddress[1] = 0x05; + info.MacAddress[2] = 0x53; // 'S' for Sunshine + info.MacAddress[3] = 0x00; + info.MacAddress[4] = 0x00; + info.MacAddress[5] = (UCHAR) (id.globalIndex & 0xFF); + + ctx.ps5 = WinUHidPS5Create(&info, rumble_cb, lightbar_cb, player_led_cb, trigger_effect_cb, &ctx); + if (!ctx.ps5) { + BOOST_LOG(error) << "Failed to create WinUHid PS5 gamepad: "sv << GetLastError(); + return -1; + } + + ctx.feedback_queue = std::move(feedback_queue); + + // Request motion data from client at 100Hz + ctx.feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(ctx.client_relative_index, LI_MOTION_TYPE_ACCEL, 100)); + ctx.feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(ctx.client_relative_index, LI_MOTION_TYPE_GYRO, 100)); + + BOOST_LOG(info) << "Created WinUHid DualSense gamepad "sv << id.globalIndex; + return 0; + } + + void free_target(int nr) { + auto &ctx = gamepads[nr]; + ctx.reset(); + } + + ~winuhid_t() { + for (auto &ctx : gamepads) { + ctx.reset(); + } + } + }; +#endif + struct input_raw_t { ~input_raw_t() { delete vigem; +#ifdef SUNSHINE_WINUHID + delete winuhid; +#endif } - vigem_t *vigem; + vigem_t *vigem = nullptr; +#ifdef SUNSHINE_WINUHID + winuhid_t *winuhid = nullptr; +#endif decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; @@ -447,6 +617,16 @@ namespace platf { input_t result {new input_raw_t {}}; auto &raw = *(input_raw_t *) result.get(); +#ifdef SUNSHINE_WINUHID + // Try WinUHid first (preferred — supports DualSense with adaptive triggers) + raw.winuhid = new winuhid_t {}; + if (raw.winuhid->init()) { + delete raw.winuhid; + raw.winuhid = nullptr; + } +#endif + + // ViGEm is always available as fallback raw.vigem = new vigem_t {}; if (raw.vigem->init()) { delete raw.vigem; @@ -1166,6 +1346,37 @@ namespace platf { int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { auto raw = (input_raw_t *) input.get(); +#ifdef SUNSHINE_WINUHID + // Try WinUHid DualSense emulation for PS-type controllers or when explicitly configured + if (raw->winuhid && raw->winuhid->available) { + bool use_ds5 = false; + + if (config::input.gamepad == "ds5"sv) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (manual selection)"sv; + use_ds5 = true; + } else if (config::input.gamepad == "auto"sv || config::input.gamepad.empty()) { + if (metadata.type == LI_CTYPE_PS) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (auto-selected by client-reported PS type)"sv; + use_ds5 = true; + } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (auto-selected by motion sensor presence)"sv; + use_ds5 = true; + } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (auto-selected by touchpad presence)"sv; + use_ds5 = true; + } + } + + if (use_ds5) { + int ret = raw->winuhid->alloc_gamepad(id, feedback_queue); + if (ret == 0) { + return 0; + } + BOOST_LOG(warning) << "WinUHid DualSense creation failed, falling back to ViGEm"sv; + } + } +#endif + if (!raw->vigem) { return 0; } @@ -1220,6 +1431,13 @@ namespace platf { void free_gamepad(input_t &input, int nr) { auto raw = (input_raw_t *) input.get(); +#ifdef SUNSHINE_WINUHID + if (raw->winuhid && raw->winuhid->gamepads[nr].is_active()) { + raw->winuhid->free_target(nr); + return; + } +#endif + if (!raw->vigem) { return; } @@ -1478,7 +1696,60 @@ namespace platf { * @param gamepad_state The gamepad button/axis state sent from the client. */ void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + +#ifdef SUNSHINE_WINUHID + if (raw->winuhid && raw->winuhid->gamepads[nr].is_active()) { + auto &ctx = raw->winuhid->gamepads[nr]; + auto &r = ctx.ps5_report; + + auto flags = gamepad_state.buttonFlags; + + // Sticks: Moonlight sends int16 (-32768..32767), DualSense expects uint8 (0-255, center=0x80) + r.LeftStickX = (uint8_t) ((gamepad_state.lsX + 32768) >> 8); + r.LeftStickY = (uint8_t) ((gamepad_state.lsY + 32768) >> 8); + r.RightStickX = (uint8_t) ((gamepad_state.rsX + 32768) >> 8); + r.RightStickY = (uint8_t) ((gamepad_state.rsY + 32768) >> 8); + + // Triggers: already 0-255 + r.LeftTrigger = gamepad_state.lt; + r.RightTrigger = gamepad_state.rt; + + // Face buttons (A=Cross, B=Circle, X=Square, Y=Triangle) + r.ButtonCross = !!(flags & A); + r.ButtonCircle = !!(flags & B); + r.ButtonSquare = !!(flags & X); + r.ButtonTriangle = !!(flags & Y); + + // Shoulder buttons + r.ButtonL1 = !!(flags & LEFT_BUTTON); + r.ButtonR1 = !!(flags & RIGHT_BUTTON); + r.ButtonL2 = gamepad_state.lt > 0 ? 1 : 0; + r.ButtonR2 = gamepad_state.rt > 0 ? 1 : 0; + r.ButtonL3 = !!(flags & LEFT_STICK); + r.ButtonR3 = !!(flags & RIGHT_STICK); + + // System buttons + r.ButtonShare = !!(flags & BACK); + r.ButtonOptions = !!(flags & START); + r.ButtonHome = !!(flags & HOME); + r.ButtonTouchpad = !!(flags & (TOUCHPAD_BUTTON | MISC_BUTTON)); + r.ButtonMute = 0; + + // D-pad: convert flags to hat X/Y for WinUHidPS5SetHatState + int hatX = 0, hatY = 0; + if (flags & DPAD_UP) hatY = -1; + if (flags & DPAD_DOWN) hatY = 1; + if (flags & DPAD_LEFT) hatX = -1; + if (flags & DPAD_RIGHT) hatX = 1; + WinUHidPS5SetHatState(&r, hatX, hatY); + + WinUHidPS5ReportInput(ctx.ps5, &r); + return; + } +#endif + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1510,7 +1781,59 @@ namespace platf { * @param touch The touch event. */ void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + +#ifdef SUNSHINE_WINUHID + if (raw->winuhid && raw->winuhid->gamepads[touch.id.globalIndex].is_active()) { + auto &ctx = raw->winuhid->gamepads[touch.id.globalIndex]; + auto &r = ctx.ps5_report; + + uint8_t pointerIndex; + if (touch.eventType == LI_TOUCH_EVENT_DOWN) { + if (ctx.available_pointers & 0x1) { + ctx.pointer_id_map[touch.pointerId] = pointerIndex = 0; + ctx.available_pointers &= ~(1 << pointerIndex); + } else if (ctx.available_pointers & 0x2) { + ctx.pointer_id_map[touch.pointerId] = pointerIndex = 1; + ctx.available_pointers &= ~(1 << pointerIndex); + } else { + BOOST_LOG(warning) << "No more free pointer indices for DS5 touchpad"sv; + return; + } + // DualSense touchpad: 1920x1080 + uint16_t x = (uint16_t) (touch.x * 1920); + uint16_t y = (uint16_t) (touch.y * 1080); + WinUHidPS5SetTouchState(&r, pointerIndex, TRUE, x, y); + } else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + WinUHidPS5SetTouchState(&r, 0, FALSE, 0, 0); + WinUHidPS5SetTouchState(&r, 1, FALSE, 0, 0); + ctx.pointer_id_map.clear(); + ctx.available_pointers = 0x3; + } else { + auto i = ctx.pointer_id_map.find(touch.pointerId); + if (i == ctx.pointer_id_map.end()) { + BOOST_LOG(warning) << "Pointer ID not found for DS5 touchpad"sv; + return; + } + pointerIndex = i->second; + + if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) { + WinUHidPS5SetTouchState(&r, pointerIndex, FALSE, 0, 0); + ctx.pointer_id_map.erase(i); + ctx.available_pointers |= (1 << pointerIndex); + } else if (touch.eventType == LI_TOUCH_EVENT_MOVE) { + uint16_t x = (uint16_t) (touch.x * 1920); + uint16_t y = (uint16_t) (touch.y * 1080); + WinUHidPS5SetTouchState(&r, pointerIndex, TRUE, x, y); + } + } + + WinUHidPS5ReportInput(ctx.ps5, &r); + return; + } +#endif + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1616,7 +1939,29 @@ namespace platf { * @param motion The motion event. */ void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + +#ifdef SUNSHINE_WINUHID + if (raw->winuhid && raw->winuhid->gamepads[motion.id.globalIndex].is_active()) { + auto &ctx = raw->winuhid->gamepads[motion.id.globalIndex]; + auto &r = ctx.ps5_report; + + constexpr float DEG_TO_RAD = (float) M_PI / 180.0f; + + if (motion.motionType == LI_MOTION_TYPE_ACCEL) { + // WinUHidPS5SetAccelState expects m/s^2 — Moonlight sends m/s^2. Direct pass-through. + WinUHidPS5SetAccelState(&r, motion.x, motion.y, motion.z); + } else if (motion.motionType == LI_MOTION_TYPE_GYRO) { + // WinUHidPS5SetGyroState expects rad/s — Moonlight sends deg/s. + WinUHidPS5SetGyroState(&r, motion.x * DEG_TO_RAD, motion.y * DEG_TO_RAD, motion.z * DEG_TO_RAD); + } + + WinUHidPS5ReportInput(ctx.ps5, &r); + return; + } +#endif + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1643,7 +1988,25 @@ namespace platf { * @param battery The battery event. */ void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { - auto vigem = ((input_raw_t *) input.get())->vigem; + auto raw = (input_raw_t *) input.get(); + +#ifdef SUNSHINE_WINUHID + if (raw->winuhid && raw->winuhid->gamepads[battery.id.globalIndex].is_active()) { + auto &ctx = raw->winuhid->gamepads[battery.id.globalIndex]; + auto &r = ctx.ps5_report; + + bool wired = (battery.state == LI_BATTERY_STATE_CHARGING || battery.state == LI_BATTERY_STATE_FULL); + uint8_t pct = (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) + ? (uint8_t) std::min((int) battery.percentage, 100) + : 50; + + WinUHidPS5SetBatteryState(&r, wired ? TRUE : FALSE, pct); + WinUHidPS5ReportInput(ctx.ps5, &r); + return; + } +#endif + + auto vigem = raw->vigem; // If there is no gamepad support if (!vigem) { @@ -1722,21 +2085,35 @@ namespace platf { supported_gamepad_t {"auto", true, ""}, supported_gamepad_t {"x360", false, ""}, supported_gamepad_t {"ds4", false, ""}, +#ifdef SUNSHINE_WINUHID + supported_gamepad_t {"ds5", false, ""}, +#endif }; return gps; } - auto vigem = ((input_raw_t *) input)->vigem; - auto enabled = vigem != nullptr; - auto reason = enabled ? "" : "gamepads.vigem-not-available"; + auto raw = (input_raw_t *) input; + auto vigem_enabled = raw->vigem != nullptr; + auto vigem_reason = vigem_enabled ? "" : "gamepads.vigem-not-available"; - // ds4 == ps4 +#ifdef SUNSHINE_WINUHID + auto winuhid_enabled = raw->winuhid && raw->winuhid->available; + auto ds5_reason = winuhid_enabled ? "" : "gamepads.winuhid-not-available"; + + static std::vector gps { + supported_gamepad_t {"auto", true, ""}, + supported_gamepad_t {"ds5", winuhid_enabled, ds5_reason}, + supported_gamepad_t {"x360", vigem_enabled, vigem_reason}, + supported_gamepad_t {"ds4", vigem_enabled, vigem_reason}, + }; +#else static std::vector gps { - supported_gamepad_t {"auto", true, reason}, - supported_gamepad_t {"x360", enabled, reason}, - supported_gamepad_t {"ds4", enabled, reason} + supported_gamepad_t {"auto", true, vigem_reason}, + supported_gamepad_t {"x360", vigem_enabled, vigem_reason}, + supported_gamepad_t {"ds4", vigem_enabled, vigem_reason}, }; +#endif for (auto &[name, is_enabled, reason_disabled] : gps) { if (!is_enabled) { From 862daa927d56c8cefc2313d1d6f3446863da8705 Mon Sep 17 00:00:00 2001 From: Heinz Kirste Date: Wed, 1 Apr 2026 21:33:13 -0600 Subject: [PATCH 4/5] fix(input/windows): fix WinUHid build with MinGW - Make WINUHID_STATIC a global compile definition instead of per-file (fixes dllimport errors when GCC processes WinUHid header includes) - Remove redundant #define WINUHID_STATIC from input.cpp - Rename local variable 'info' to 'gp_info' to avoid collision with Boost.Log's BOOST_LOG(info) macro --- cmake/compile_definitions/windows.cmake | 12 +++++------- src/platform/windows/input.cpp | 19 +++++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 5eb4b882bdd..51e1a59bc04 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -43,7 +43,7 @@ include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include" # winuhid - virtual HID gamepad emulation (DualSense, DS4, Xbox One) option(SUNSHINE_ENABLE_WINUHID "Enable WinUHid virtual gamepad backend for DualSense support" ON) if(SUNSHINE_ENABLE_WINUHID) - list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_WINUHID) + list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_WINUHID WINUHID_STATIC) # WRL compatibility shim must come before WinUHid includes so MinGW finds our shim # instead of looking for MSVC's wrl/wrappers/corewrappers.h include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/third-party/WinUHid-compat") @@ -97,22 +97,20 @@ if(SUNSHINE_ENABLE_WINUHID) "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidDevs.cpp" "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidPS5.cpp") - # WinUHid is compiled as static source — no DLL export/import. - # DllMain is renamed per-file to avoid duplicate symbol errors when linking. + # DllMain is renamed per-file to avoid duplicate symbol errors when linking statically. set_source_files_properties( "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHid/WinUHid.cpp" - PROPERTIES COMPILE_DEFINITIONS "WINUHID_STATIC;DllMain=WinUHid_DllMain_Unused" + PROPERTIES COMPILE_DEFINITIONS "DllMain=WinUHid_DllMain_Unused" COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" ) set_source_files_properties( "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidDevs.cpp" - PROPERTIES COMPILE_DEFINITIONS "WINUHID_STATIC;DllMain=WinUHidDevs_DllMain_Unused" + PROPERTIES COMPILE_DEFINITIONS "DllMain=WinUHidDevs_DllMain_Unused" COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" ) set_source_files_properties( "${CMAKE_SOURCE_DIR}/third-party/WinUHid/WinUHidDevs/WinUHidPS5.cpp" - PROPERTIES COMPILE_DEFINITIONS "WINUHID_STATIC" - COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" + PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" ) endif() diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 2bac7fbba9f..d9aa8c3e3a7 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -16,7 +16,6 @@ #include #ifdef SUNSHINE_WINUHID - #define WINUHID_STATIC #include #include #endif @@ -558,15 +557,15 @@ namespace platf { WinUHidPS5InitializeInputReport(&ctx.ps5_report); // Generate a deterministic MAC from gamepad index for stable Steam controller identity - WINUHID_PS5_GAMEPAD_INFO info {}; - info.MacAddress[0] = 0x00; - info.MacAddress[1] = 0x05; - info.MacAddress[2] = 0x53; // 'S' for Sunshine - info.MacAddress[3] = 0x00; - info.MacAddress[4] = 0x00; - info.MacAddress[5] = (UCHAR) (id.globalIndex & 0xFF); - - ctx.ps5 = WinUHidPS5Create(&info, rumble_cb, lightbar_cb, player_led_cb, trigger_effect_cb, &ctx); + WINUHID_PS5_GAMEPAD_INFO gp_info {}; + gp_info.MacAddress[0] = 0x00; + gp_info.MacAddress[1] = 0x05; + gp_info.MacAddress[2] = 0x53; // 'S' for Sunshine + gp_info.MacAddress[3] = 0x00; + gp_info.MacAddress[4] = 0x00; + gp_info.MacAddress[5] = (UCHAR) (id.globalIndex & 0xFF); + + ctx.ps5 = WinUHidPS5Create(&gp_info, rumble_cb, lightbar_cb, player_led_cb, trigger_effect_cb, &ctx); if (!ctx.ps5) { BOOST_LOG(error) << "Failed to create WinUHid PS5 gamepad: "sv << GetLastError(); return -1; From 90a0520298b55ee9c5b09d213573a65d271e2657 Mon Sep 17 00:00:00 2001 From: Heinz Kirste Date: Sat, 4 Apr 2026 00:36:13 -0600 Subject: [PATCH 5/5] fix(input/windows): address SonarCloud code quality issues - Add noexcept move constructor/assignment to winuhid_gamepad_context_t - Replace C-style void* casts with static_cast - Replace raw new/delete with std::unique_ptr for winuhid_t and vigem_t - Extract should_use_ds5() helper to reduce alloc_gamepad complexity - Extract winuhid_handle_touch() to reduce gamepad_touch complexity - Replace redundant type declarations with auto - Split multi-variable declarations into separate statements - Remove comment that SonarCloud flagged as commented-out code - Use named parameters instead of (void) casts for unused args --- src/platform/windows/input.cpp | 195 ++++++++++++++++----------------- 1 file changed, 95 insertions(+), 100 deletions(-) diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index d9aa8c3e3a7..c9ba31450c8 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -455,6 +455,12 @@ namespace platf { gamepad_feedback_msg_t last_rumble {}; gamepad_feedback_msg_t last_rgb_led {}; + winuhid_gamepad_context_t() = default; + winuhid_gamepad_context_t(winuhid_gamepad_context_t &&other) noexcept = default; + winuhid_gamepad_context_t &operator=(winuhid_gamepad_context_t &&other) noexcept = default; + winuhid_gamepad_context_t(const winuhid_gamepad_context_t &) = delete; + winuhid_gamepad_context_t &operator=(const winuhid_gamepad_context_t &) = delete; + bool is_active() const { return ps5 != nullptr; } void reset() { @@ -490,7 +496,7 @@ namespace platf { } static void CALLBACK rumble_cb(PVOID ctx, UCHAR leftMotor, UCHAR rightMotor) { - auto *gp = (winuhid_gamepad_context_t *) ctx; + auto *gp = static_cast(ctx); // Scale 8-bit to 16-bit: 0xFF -> 0xFFFF uint16_t normalizedLeft = (uint16_t) leftMotor << 8 | leftMotor; @@ -505,7 +511,7 @@ namespace platf { } static void CALLBACK lightbar_cb(PVOID ctx, UCHAR r, UCHAR g, UCHAR b) { - auto *gp = (winuhid_gamepad_context_t *) ctx; + auto *gp = static_cast(ctx); auto msg = gamepad_feedback_msg_t::make_rgb_led(gp->client_relative_index, r, g, b); if (msg.data.rgb_led.r != gp->last_rgb_led.data.rgb_led.r || @@ -516,18 +522,16 @@ namespace platf { } } - static void CALLBACK player_led_cb(PVOID ctx, UCHAR ledValue) { + static void CALLBACK player_led_cb(PVOID /* ctx */, UCHAR /* ledValue */) { // Moonlight protocol doesn't support player LED feedback yet - (void) ctx; - (void) ledValue; } static void CALLBACK trigger_effect_cb(PVOID ctx, PCWINUHID_PS5_TRIGGER_EFFECT left, PCWINUHID_PS5_TRIGGER_EFFECT right) { - auto *gp = (winuhid_gamepad_context_t *) ctx; + auto *gp = static_cast(ctx); uint8_t event_flags = 0; - uint8_t type_left = 0; - uint8_t type_right = 0; + auto type_left = uint8_t {0}; + auto type_right = uint8_t {0}; std::array data_left {}; std::array data_right {}; @@ -547,6 +551,28 @@ namespace platf { gp->client_relative_index, event_flags, type_left, type_right, data_left, data_right)); } + /** + * @brief Determines whether to use DualSense emulation for a given gamepad. + */ + static bool should_use_ds5(const gamepad_arrival_t &metadata) { + if (config::input.gamepad == "ds5"sv) { + return true; + } + if (config::input.gamepad != "auto"sv && !config::input.gamepad.empty()) { + return false; + } + if (metadata.type == LI_CTYPE_PS) { + return true; + } + if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { + return true; + } + if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { + return true; + } + return false; + } + int alloc_gamepad(const gamepad_id_t &id, feedback_queue_t &feedback_queue) { auto &ctx = gamepads[id.globalIndex]; assert(!ctx.is_active()); @@ -595,16 +621,9 @@ namespace platf { #endif struct input_raw_t { - ~input_raw_t() { - delete vigem; + std::unique_ptr vigem; #ifdef SUNSHINE_WINUHID - delete winuhid; -#endif - } - - vigem_t *vigem = nullptr; -#ifdef SUNSHINE_WINUHID - winuhid_t *winuhid = nullptr; + std::unique_ptr winuhid; #endif decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; @@ -618,18 +637,16 @@ namespace platf { #ifdef SUNSHINE_WINUHID // Try WinUHid first (preferred — supports DualSense with adaptive triggers) - raw.winuhid = new winuhid_t {}; + raw.winuhid = std::make_unique(); if (raw.winuhid->init()) { - delete raw.winuhid; - raw.winuhid = nullptr; + raw.winuhid.reset(); } #endif // ViGEm is always available as fallback - raw.vigem = new vigem_t {}; + raw.vigem = std::make_unique(); if (raw.vigem->init()) { - delete raw.vigem; - raw.vigem = nullptr; + raw.vigem.reset(); } // Get pointers to virtual touch/pen input functions (Win10 1809+) @@ -1346,33 +1363,12 @@ namespace platf { auto raw = (input_raw_t *) input.get(); #ifdef SUNSHINE_WINUHID - // Try WinUHid DualSense emulation for PS-type controllers or when explicitly configured - if (raw->winuhid && raw->winuhid->available) { - bool use_ds5 = false; - - if (config::input.gamepad == "ds5"sv) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (manual selection)"sv; - use_ds5 = true; - } else if (config::input.gamepad == "auto"sv || config::input.gamepad.empty()) { - if (metadata.type == LI_CTYPE_PS) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (auto-selected by client-reported PS type)"sv; - use_ds5 = true; - } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (auto-selected by motion sensor presence)"sv; - use_ds5 = true; - } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { - BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller (auto-selected by touchpad presence)"sv; - use_ds5 = true; - } - } - - if (use_ds5) { - int ret = raw->winuhid->alloc_gamepad(id, feedback_queue); - if (ret == 0) { - return 0; - } - BOOST_LOG(warning) << "WinUHid DualSense creation failed, falling back to ViGEm"sv; + if (raw->winuhid && raw->winuhid->available && winuhid_t::should_use_ds5(metadata)) { + BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense controller via WinUHid"sv; + if (raw->winuhid->alloc_gamepad(id, feedback_queue) == 0) { + return 0; } + BOOST_LOG(warning) << "WinUHid DualSense creation failed, falling back to ViGEm"sv; } #endif @@ -1714,7 +1710,6 @@ namespace platf { r.LeftTrigger = gamepad_state.lt; r.RightTrigger = gamepad_state.rt; - // Face buttons (A=Cross, B=Circle, X=Square, Y=Triangle) r.ButtonCross = !!(flags & A); r.ButtonCircle = !!(flags & B); r.ButtonSquare = !!(flags & X); @@ -1735,8 +1730,8 @@ namespace platf { r.ButtonTouchpad = !!(flags & (TOUCHPAD_BUTTON | MISC_BUTTON)); r.ButtonMute = 0; - // D-pad: convert flags to hat X/Y for WinUHidPS5SetHatState - int hatX = 0, hatY = 0; + int hatX = 0; + int hatY = 0; if (flags & DPAD_UP) hatY = -1; if (flags & DPAD_DOWN) hatY = 1; if (flags & DPAD_LEFT) hatX = -1; @@ -1748,7 +1743,7 @@ namespace platf { } #endif - auto vigem = raw->vigem; + auto *vigem = raw->vigem.get(); // If there is no gamepad support if (!vigem) { @@ -1779,60 +1774,60 @@ namespace platf { * @param input The global input context. * @param touch The touch event. */ +#ifdef SUNSHINE_WINUHID + /** + * @brief Handles a touch event for a WinUHid DualSense gamepad. + */ + static void winuhid_handle_touch(winuhid_gamepad_context_t &ctx, const gamepad_touch_t &touch) { + auto &r = ctx.ps5_report; + + if (touch.eventType == LI_TOUCH_EVENT_DOWN) { + uint8_t pointerIndex = (ctx.available_pointers & 0x1) ? 0 : ((ctx.available_pointers & 0x2) ? 1 : 0xFF); + if (pointerIndex == 0xFF) { + BOOST_LOG(warning) << "No more free pointer indices for DS5 touchpad"sv; + return; + } + ctx.pointer_id_map[touch.pointerId] = pointerIndex; + ctx.available_pointers &= ~(1 << pointerIndex); + WinUHidPS5SetTouchState(&r, pointerIndex, TRUE, + static_cast(touch.x * 1920), static_cast(touch.y * 1080)); + } else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { + WinUHidPS5SetTouchState(&r, 0, FALSE, 0, 0); + WinUHidPS5SetTouchState(&r, 1, FALSE, 0, 0); + ctx.pointer_id_map.clear(); + ctx.available_pointers = 0x3; + } else { + auto i = ctx.pointer_id_map.find(touch.pointerId); + if (i == ctx.pointer_id_map.end()) { + BOOST_LOG(warning) << "Pointer ID not found for DS5 touchpad"sv; + return; + } + auto pointerIndex = i->second; + + if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) { + WinUHidPS5SetTouchState(&r, pointerIndex, FALSE, 0, 0); + ctx.pointer_id_map.erase(i); + ctx.available_pointers |= (1 << pointerIndex); + } else if (touch.eventType == LI_TOUCH_EVENT_MOVE) { + WinUHidPS5SetTouchState(&r, pointerIndex, TRUE, + static_cast(touch.x * 1920), static_cast(touch.y * 1080)); + } + } + WinUHidPS5ReportInput(ctx.ps5, &r); + } +#endif + void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { auto raw = (input_raw_t *) input.get(); #ifdef SUNSHINE_WINUHID if (raw->winuhid && raw->winuhid->gamepads[touch.id.globalIndex].is_active()) { - auto &ctx = raw->winuhid->gamepads[touch.id.globalIndex]; - auto &r = ctx.ps5_report; - - uint8_t pointerIndex; - if (touch.eventType == LI_TOUCH_EVENT_DOWN) { - if (ctx.available_pointers & 0x1) { - ctx.pointer_id_map[touch.pointerId] = pointerIndex = 0; - ctx.available_pointers &= ~(1 << pointerIndex); - } else if (ctx.available_pointers & 0x2) { - ctx.pointer_id_map[touch.pointerId] = pointerIndex = 1; - ctx.available_pointers &= ~(1 << pointerIndex); - } else { - BOOST_LOG(warning) << "No more free pointer indices for DS5 touchpad"sv; - return; - } - // DualSense touchpad: 1920x1080 - uint16_t x = (uint16_t) (touch.x * 1920); - uint16_t y = (uint16_t) (touch.y * 1080); - WinUHidPS5SetTouchState(&r, pointerIndex, TRUE, x, y); - } else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) { - WinUHidPS5SetTouchState(&r, 0, FALSE, 0, 0); - WinUHidPS5SetTouchState(&r, 1, FALSE, 0, 0); - ctx.pointer_id_map.clear(); - ctx.available_pointers = 0x3; - } else { - auto i = ctx.pointer_id_map.find(touch.pointerId); - if (i == ctx.pointer_id_map.end()) { - BOOST_LOG(warning) << "Pointer ID not found for DS5 touchpad"sv; - return; - } - pointerIndex = i->second; - - if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) { - WinUHidPS5SetTouchState(&r, pointerIndex, FALSE, 0, 0); - ctx.pointer_id_map.erase(i); - ctx.available_pointers |= (1 << pointerIndex); - } else if (touch.eventType == LI_TOUCH_EVENT_MOVE) { - uint16_t x = (uint16_t) (touch.x * 1920); - uint16_t y = (uint16_t) (touch.y * 1080); - WinUHidPS5SetTouchState(&r, pointerIndex, TRUE, x, y); - } - } - - WinUHidPS5ReportInput(ctx.ps5, &r); + winuhid_handle_touch(raw->winuhid->gamepads[touch.id.globalIndex], touch); return; } #endif - auto vigem = raw->vigem; + auto *vigem = raw->vigem.get(); // If there is no gamepad support if (!vigem) { @@ -1960,7 +1955,7 @@ namespace platf { } #endif - auto vigem = raw->vigem; + auto *vigem = raw->vigem.get(); // If there is no gamepad support if (!vigem) { @@ -2005,7 +2000,7 @@ namespace platf { } #endif - auto vigem = raw->vigem; + auto *vigem = raw->vigem.get(); // If there is no gamepad support if (!vigem) {