Files
riojoy/src/RioJoy.Core/Output/HidFeederJoystickSink.cs
T
CydandClaude Opus 4.8 fe87c79f55 net48 port (test branch): retarget all projects to .NET Framework 4.8
Retargets RioJoy.Core/Overlay/Tray + tests from net8.0-windows to net48 so
the app can be tested as a framework-dependent build (relies on the in-box
.NET Framework 4.8 on Windows 10/11). Builds clean; all 241 tests pass.

Polyfills (no behavior change):
- PolySharp source generator for init/records/Index/Range/required members.
- System.Memory, System.Text.Json, Microsoft.Bcl.HashCode,
  System.Threading.Channels (tests) NuGet packages.
- Compat/Net48Polyfills.cs: GetValueOrDefault, KeyValuePair.Deconstruct,
  Math.Clamp; tests/TestPolyfills.cs: Task.WaitAsync.

Source adjustments for APIs absent on net48:
- ArgumentNullException/ArgumentException.ThrowIf* inlined to manual guards.
- Convert.ToHexString, Encoding.Latin1, Environment.ProcessPath,
  ApplicationConfiguration.Initialize, Enum.GetNames<T>/GetValues<T>,
  string.StartsWith(char), string.Split(char, opts), TextBox.PlaceholderText,
  PeriodicTimer, Memory-based Stream Read/WriteAsync, array range-slicing.
- Dropped [SupportedOSPlatform] hints (net48 is single-platform).

deploy/build-package.ps1: publish framework-dependent (no self-contained).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:34:47 -05:00

190 lines
6.7 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;
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);
}