using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; using RioJoy.Core.Calibration; using RioJoy.Core.Hid; using RioJoy.Core.Mapping; namespace RioJoy.Core.Output; /// /// that feeds the RioGamepad virtual HID driver: it /// opens the driver's device interface, maintains the current /// , and submits the whole report on every change via /// DeviceIoControl(IOCTL_RIO_SUBMIT_REPORT). Replaces /// once the driver is installed (Phase 1/3b). /// public sealed class HidFeederJoystickSink : IJoystickSink, IDisposable { // Must match driver/RioGamepad/Public.h. private static readonly Guid InterfaceGuid = new(0xb6a3f1c2, 0x7e84, 0x4d2a, 0x9c, 0x1f, 0x2a, 0x5e, 0x8d, 0x3b, 0x60, 0x71); // CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, 0x800, METHOD_BUFFERED=0, FILE_WRITE_ACCESS=0x2). private const uint IoctlSubmitReport = (0x22u << 16) | (0x2u << 14) | (0x800u << 2); private readonly SafeFileHandle _device; private readonly RioHidReport _report = new(); private readonly object _gate = new(); private HidFeederJoystickSink(SafeFileHandle device) => _device = device; /// /// Try to open the virtual joystick driver. Returns if /// the driver is not installed/present, so callers can fall back to /// . /// public static bool TryCreate(out HidFeederJoystickSink? sink) { sink = null; #if NET40 // Windows XP: RioGamepadXP is a HID minidriver whose sideband control // device can't carry a device interface (no PDO), so it's opened by its // fixed symbolic-link name rather than via SetupDi + interface GUID. // Contract is otherwise identical (see driver/RioGamepadXP/Public.h). string? path = @"\\.\RioGamepadXP"; #else string? path = FindDevicePath(); #endif if (path is null) return false; SafeFileHandle handle = CreateFileW( path, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero); if (handle.IsInvalid) { handle.Dispose(); return false; } sink = new HidFeederJoystickSink(handle); sink.Submit(); // push the centered rest state return true; } public void SetButton(int button, bool pressed) { lock (_gate) { _report.SetButton(button, pressed); Submit(); } } public void SetHat(RioHat position) { lock (_gate) { _report.SetHat(position); Submit(); } } public void SetAxis(JoyAxis axis, int value) { lock (_gate) { _report.SetAxis(axis, value); Submit(); } } private void Submit() { byte[] buffer = _report.ToArray(); if (!DeviceIoControl(_device, IoctlSubmitReport, buffer, (uint)buffer.Length, IntPtr.Zero, 0, out _, IntPtr.Zero)) { // Device may have been removed; surface for the caller's error handling. throw new InvalidOperationException( $"IOCTL_RIO_SUBMIT_REPORT failed (Win32 {Marshal.GetLastWin32Error()})."); } } public void Dispose() => _device.Dispose(); // --- SetupAPI device-interface resolution -------------------------------- private static string? FindDevicePath() { Guid guid = InterfaceGuid; IntPtr devInfo = SetupDiGetClassDevs(ref guid, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (devInfo == INVALID_HANDLE_VALUE) return null; try { var ifData = new SP_DEVICE_INTERFACE_DATA(); ifData.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA)); if (!SetupDiEnumDeviceInterfaces(devInfo, IntPtr.Zero, ref guid, 0, ref ifData)) return null; uint required = 0; SetupDiGetDeviceInterfaceDetail(devInfo, ref ifData, IntPtr.Zero, 0, ref required, IntPtr.Zero); if (required == 0) return null; IntPtr detail = Marshal.AllocHGlobal((int)required); try { // SP_DEVICE_INTERFACE_DETAIL_DATA.cbSize: 8 on x64, 6 on x86. Marshal.WriteInt32(detail, IntPtr.Size == 8 ? 8 : 6); if (!SetupDiGetDeviceInterfaceDetail(devInfo, ref ifData, detail, required, ref required, IntPtr.Zero)) return null; // DevicePath follows the cbSize DWORD. return Marshal.PtrToStringUni(IntPtr.Add(detail, 4)); } finally { Marshal.FreeHGlobal(detail); } } finally { SetupDiDestroyDeviceInfoList(devInfo); } } private const int DIGCF_PRESENT = 0x02; private const int DIGCF_DEVICEINTERFACE = 0x10; private const uint GENERIC_WRITE = 0x40000000; private const uint FILE_SHARE_READ = 0x01; private const uint FILE_SHARE_WRITE = 0x02; private const uint OPEN_EXISTING = 3; private static readonly IntPtr INVALID_HANDLE_VALUE = new(-1); [StructLayout(LayoutKind.Sequential)] private struct SP_DEVICE_INTERFACE_DATA { public int cbSize; public Guid InterfaceClassGuid; public int Flags; public IntPtr Reserved; } [DllImport("setupapi.dll", SetLastError = true)] private static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, IntPtr Enumerator, IntPtr hwndParent, int Flags); [DllImport("setupapi.dll", SetLastError = true)] private static extern bool SetupDiEnumDeviceInterfaces(IntPtr DeviceInfoSet, IntPtr DeviceInfoData, ref Guid InterfaceClassGuid, uint MemberIndex, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData); [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr DeviceInfoSet, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData, IntPtr DeviceInterfaceDetailData, uint DeviceInterfaceDetailDataSize, ref uint RequiredSize, IntPtr DeviceInfoData); [DllImport("setupapi.dll", SetLastError = true)] private static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern SafeFileHandle CreateFileW(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, byte[] lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); }