Phase 1 (3b): driver signing/install scripts + C# HID feeder sink
Make the virtual gamepad deployable on owned cabinets and wire the real user-mode feeder: - driver/sign.ps1 (non-admin): create a self-signed code-signing cert, build the catalog with inf2cat, and SHA-256-sign RioGamepad.sys + .cat (embed-sign the sys before cataloguing so the .cat matches). Exports RIOJoyTest.cer. Verified end-to-end against the EWDK (signability + catalog clean; both files signed). - driver/install.ps1 (admin, two-phase): trust the cert (LocalMachine Root + TrustedPublisher), stage the package (pnputil /add-driver), enable test signing; after reboot, -CreateDevice runs devgen to create root\RioGamepad so PnP installs it. uninstall.ps1 reverses it. - RioJoy.Core.Output.HidFeederJoystickSink: opens the driver by GUID_DEVINTERFACE_RIOGAMEPAD (SetupAPI), maintains a RioHidReport, and submits it via DeviceIoControl(IOCTL_RIO_SUBMIT_REPORT) on each axis/button/hat change. RioCoordinator now uses it when the driver is present and falls back to the no-op sink otherwise (status shows "[no joystick driver]"). - gitignore the signing outputs (driver/package/, *.cat); driver/README.md gets the full build → sign → install → joy.cpl workflow. Remaining = the actual elevated install + reboot + joy.cpl verification on the cabinet (admin steps), and on-hardware confirmation of the feeder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
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>
|
||||
[SupportedOSPlatform("windows")]
|
||||
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;
|
||||
string? path = FindDevicePath();
|
||||
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<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);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public sealed class RioCoordinator : IDisposable
|
||||
private RioSerialLink? _link;
|
||||
private RioRuntime? _runtime;
|
||||
private CancellationTokenSource? _cts;
|
||||
private IDisposable? _joystick;
|
||||
private string? _activeProfileName;
|
||||
|
||||
public RioCoordinator(Func<AppConfig> config, Func<string, IRioTransport>? transportFactory = null)
|
||||
@@ -104,20 +105,37 @@ public sealed class RioCoordinator : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
// Use the real virtual-joystick driver if installed; otherwise fall back
|
||||
// to a no-op sink (keyboard/mouse still work) and note it in the status.
|
||||
IJoystickSink joystick;
|
||||
string joystickNote;
|
||||
if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
|
||||
{
|
||||
joystick = feeder!;
|
||||
_joystick = feeder;
|
||||
joystickNote = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
joystick = new NullJoystickSink();
|
||||
_joystick = null;
|
||||
joystickNote = " [no joystick driver]";
|
||||
}
|
||||
|
||||
_transport = _transportFactory(port);
|
||||
_link = new RioSerialLink(_transport);
|
||||
_runtime = new RioRuntime(
|
||||
_link,
|
||||
profile.ToInputMap(),
|
||||
new SendInputSink(),
|
||||
new NullJoystickSink(),
|
||||
joystick,
|
||||
new AxisCalibrator(profile.Calibration));
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = _link.RunAsync(_cts.Token);
|
||||
_runtime.Start();
|
||||
_activeProfileName = profile.Name;
|
||||
SetStatus($"Active: {profile.Name} ({_link.Description})");
|
||||
SetStatus($"Active: {profile.Name} ({_link.Description}){joystickNote}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -143,6 +161,9 @@ public sealed class RioCoordinator : IDisposable
|
||||
|
||||
_link = null;
|
||||
|
||||
_joystick?.Dispose(); // closes the HID feeder handle
|
||||
_joystick = null;
|
||||
|
||||
_transport?.Dispose(); // releases the COM port
|
||||
_transport = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user