The XP flavor of our own virtual joystick (no third-party driver), built with WDK 7.1.0 to x86/subsystem-5.01. Exposes the identical Public.h contract as the modern KMDF+VHF driver — same GUID, IOCTL_RIO_SUBMIT_REPORT, 25-byte report, VID/PID — so HidFeederJoystickSink drives both. Design (driver/RioGamepadXP/rioxp.c): - WDM HID minidriver via HidRegisterMinidriver presenting the 6-axis/hat/ 96-button joystick. POLLED mode (DevicesArePolled=TRUE): hidclass paces IOCTL_HID_READ_REPORT and we complete each synchronously from a cached report — no pending-IRP or cancel-routine machinery (the usual crash surface in a virtual HID driver). - Named sideband control device (\Device\RioGamepadXP + \DosDevices symlink) takes IOCTL_RIO_SUBMIT_REPORT and updates the cache under a spinlock. - hidclass overwrites our CREATE/CLOSE/DEVICE_CONTROL during registration; we save its pointers and reinstall wrappers that route the control device's IRPs to us and forward the HID FDO's to hidclass. Packaging/install: - Root-enumerated, so install uses devcon (built from the same WDK) — InstallHinfSection can't create the devnode. install-core.bat runs "devcon install RioGamepadXP.inf root\RioGamepadXP"; unsigned is fine (XP x86 enforces no kernel signing). Bundled into vendor\xp\. - net40 feeder opens the driver by name (\.\RioGamepadXP); XP can't put a device interface on a bare control device (no PDO) — the one nuance. Static build only: compiles clean but runtime bring-up (joy.cpl enumeration, report flow) needs a real XP target — that's 8E. 275 tests still green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
198 lines
7.1 KiB
C#
198 lines
7.1 KiB
C#
using System.Runtime.InteropServices;
|
|
using Microsoft.Win32.SafeHandles;
|
|
using RioJoy.Core.Calibration;
|
|
using RioJoy.Core.Hid;
|
|
using RioJoy.Core.Mapping;
|
|
|
|
namespace RioJoy.Core.Output;
|
|
|
|
/// <summary>
|
|
/// <see cref="IJoystickSink"/> that feeds the RioGamepad virtual HID driver: it
|
|
/// opens the driver's device interface, maintains the current
|
|
/// <see cref="RioHidReport"/>, and submits the whole report on every change via
|
|
/// <c>DeviceIoControl(IOCTL_RIO_SUBMIT_REPORT)</c>. Replaces
|
|
/// <see cref="NullJoystickSink"/> once the driver is installed (Phase 1/3b).
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Try to open the virtual joystick driver. Returns <see langword="false"/> if
|
|
/// the driver is not installed/present, so callers can fall back to
|
|
/// <see cref="NullJoystickSink"/>.
|
|
/// </summary>
|
|
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);
|
|
}
|