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/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 843dafe61e5..51e1a59bc04 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 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") + 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,29 @@ 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") + + # 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 "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 "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_OPTIONS "-Wno-missing-field-initializers;-Wno-unused-parameter;-Wno-sign-compare" + ) +endif() + set(OPENSSL_LIBRARIES libssl.a libcrypto.a) diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index 28fe6fa5749..c9ba31450c8 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -15,6 +15,11 @@ // lib includes #include +#ifdef SUNSHINE_WINUHID + #include + #include +#endif + // local includes #include "keylayout.h" #include "misc.h" @@ -431,12 +436,195 @@ namespace platf { task_pool.push(&vigem_t::set_rgb_led, (vigem_t *) userdata, target, led_color.Red, led_color.Green, led_color.Blue); } - struct input_raw_t { - ~input_raw_t() { - delete vigem; +#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 {}; + + 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() { + 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 = static_cast(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 = 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 || + 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 + } + + static void CALLBACK trigger_effect_cb(PVOID ctx, PCWINUHID_PS5_TRIGGER_EFFECT left, PCWINUHID_PS5_TRIGGER_EFFECT right) { + auto *gp = static_cast(ctx); + + uint8_t event_flags = 0; + auto type_left = uint8_t {0}; + auto type_right = uint8_t {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)); + } + + /** + * @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()); + + 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 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; + } + + 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 - vigem_t *vigem; + struct input_raw_t { + std::unique_ptr vigem; +#ifdef SUNSHINE_WINUHID + std::unique_ptr winuhid; +#endif decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; @@ -447,10 +635,18 @@ namespace platf { input_t result {new input_raw_t {}}; auto &raw = *(input_raw_t *) result.get(); - raw.vigem = new vigem_t {}; +#ifdef SUNSHINE_WINUHID + // Try WinUHid first (preferred — supports DualSense with adaptive triggers) + raw.winuhid = std::make_unique(); + if (raw.winuhid->init()) { + raw.winuhid.reset(); + } +#endif + + // ViGEm is always available as fallback + 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+) @@ -1166,6 +1362,16 @@ 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 + 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 + if (!raw->vigem) { return 0; } @@ -1220,6 +1426,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 +1691,59 @@ 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; + + 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; + + int hatX = 0; + int 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.get(); // If there is no gamepad support if (!vigem) { @@ -1509,8 +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 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()) { + winuhid_handle_touch(raw->winuhid->gamepads[touch.id.globalIndex], touch); + return; + } +#endif + + auto *vigem = raw->vigem.get(); // If there is no gamepad support if (!vigem) { @@ -1616,7 +1933,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.get(); // If there is no gamepad support if (!vigem) { @@ -1643,7 +1982,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.get(); // If there is no gamepad support if (!vigem) { @@ -1722,21 +2079,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) { 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 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