diff --git a/ControlApp/ControlApp.csproj b/ControlApp/ControlApp.csproj
index 4ddb04a7..248475d9 100644
--- a/ControlApp/ControlApp.csproj
+++ b/ControlApp/ControlApp.csproj
@@ -13,9 +13,8 @@
Nefarius.DsHidMini.ControlApp
true
3.0.0
- Kanuan, Nefarius
- Copyright (c) 2024 Nefarius Software Solutions e.U.
Debug;Release
+ false
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..6cb89ccc
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,22 @@
+
+
+
+
+ Kanuan, Benjamin Höglinger-Stelzer
+ Copyright © Nefarius Software Solutions e.U. 2022-2024
+
+
+
+ latest
+
+
+ $(NoWarn),0219,1587,1591,8600,8601,8602,8603,8604,8618,8619,8622,8629,8765,8767
+
+
+ true
+ true
+ true
+ snupkg
+ true
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 752e23a1..edf01012 100644
--- a/README.md
+++ b/README.md
@@ -197,6 +197,7 @@ The following awesome resources have made this project possible.
- [linux/drivers/hid/hid-sony.c](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-sony.c)
- [The HID Page](http://janaxelson.com/hidpage.htm)
- [CircumSpector/Research/Sony DualShock 3](https://github.com/CircumSpector/Research/tree/master/Sony%20DualShock%203)
+- [Memory-Mapped Files and Overlaid Structs](https://blog.stephencleary.com/2023/09/memory-mapped-files-overlaid-structs.html)
### DevOps
diff --git a/SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.Commands.cs b/SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.Commands.cs
new file mode 100644
index 00000000..e045a7b6
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.Commands.cs
@@ -0,0 +1,305 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.NetworkInformation;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Nefarius.DsHidMini.IPC.Exceptions;
+using Nefarius.DsHidMini.IPC.Models;
+using Nefarius.DsHidMini.IPC.Models.Public;
+
+namespace Nefarius.DsHidMini.IPC;
+
+public partial class DsHidMiniInterop
+{
+ ///
+ /// Attempts to read the from a given device instance.
+ ///
+ ///
+ /// If is null, this method returns the last known input report copy immediately. If
+ /// you use this call in a busy loop, you should set a timeout so this call becomes event-based, meaning the call will
+ /// only return when the driver signaled that new data is available, otherwise you will just burn through CPU for no
+ /// good reason. A new input report is typically available each average 5 milliseconds, depending on the connection
+ /// (wired or wireless) so a timeout of 20 milliseconds should be a good recommendation.
+ ///
+ /// The one-based device index.
+ /// The to populate.
+ /// Optional timeout to wait for a report update to arrive. Default invocation returns immediately.
+ ///
+ /// Driver process interaction failed due to missing permissions;
+ /// this operation requires elevated privileges.
+ ///
+ /// The driver returned unexpected or malformed data.
+ /// Handle duplication failed.
+ /// The driver didn't respond within an expected period.
+ /// A different thread is currently performing a data exchange.
+ ///
+ /// No driver instance is available. Make sure that at least one
+ /// device is connected and that the driver is installed and working properly. Call prior to
+ /// avoid this exception.
+ ///
+ ///
+ /// TRUE if got filled in or FALSE if the given is not
+ /// occupied.
+ ///
+ [SuppressMessage("ReSharper", "UnusedMember.Global")]
+ public unsafe bool GetRawInputReport(int deviceIndex, ref DS3_RAW_INPUT_REPORT report, TimeSpan? timeout = null)
+ {
+ if (_hidView is null)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+
+ ValidateDeviceIndex(deviceIndex);
+
+ ref IPC_HID_INPUT_REPORT_MESSAGE message = ref Unsafe.AsRef(_hidView);
+
+ if (timeout.HasValue)
+ {
+ _inputReportEvent ??= GetHidReportWaitHandle(deviceIndex);
+
+ _inputReportEvent.WaitOne(timeout.Value);
+ }
+
+ //
+ // Device is/got disconnected
+ //
+ if (message.SlotIndex == 0)
+ {
+ return false;
+ }
+
+ //
+ // Index mismatch is not supposed to happen
+ //
+ if (message.SlotIndex != deviceIndex)
+ {
+ throw new DsHidMiniInteropUnexpectedReplyException();
+ }
+
+ report = message.InputReport;
+
+ return true;
+ }
+
+ ///
+ /// Send a PING to the driver and awaits the reply.
+ ///
+ ///
+ /// Driver IPC unavailable, make sure that at least one compatible
+ /// controller is connected and operational.
+ ///
+ /// The driver didn't respond within an expected period.
+ /// The driver returned unexpected or malformed data.
+ [SuppressMessage("ReSharper", "UnusedMember.Global")]
+ public unsafe void SendPing()
+ {
+ if (_commandMutex is null || _cmdView is null)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+
+ AcquireCommandLock();
+
+ try
+ {
+ ref DSHM_IPC_MSG_HEADER message = ref Unsafe.AsRef(_cmdView);
+
+ message.Type = DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE;
+ message.Target = DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_DRIVER;
+ message.Command.Driver = DSHM_IPC_MSG_CMD_DRIVER.DSHM_IPC_MSG_CMD_DRIVER_PING;
+ message.TargetIndex = 0;
+ message.Size = (uint)Marshal.SizeOf();
+
+ if (!SendAndWait())
+ {
+ throw new DsHidMiniInteropReplyTimeoutException();
+ }
+
+ ref DSHM_IPC_MSG_HEADER reply = ref Unsafe.AsRef(_cmdView);
+
+ //
+ // Plausibility check
+ //
+ if (reply is
+ {
+ Type: DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_REQUEST_REPLY,
+ Target: DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_CLIENT,
+ Command.Driver: DSHM_IPC_MSG_CMD_DRIVER.DSHM_IPC_MSG_CMD_DRIVER_PING, TargetIndex: 0
+ }
+ && reply.Size == Marshal.SizeOf())
+ {
+ return;
+ }
+
+ throw new DsHidMiniInteropUnexpectedReplyException(ref reply);
+ }
+ finally
+ {
+ _commandMutex.ReleaseMutex();
+ }
+ }
+
+ ///
+ /// Writes a new host address to the given device.
+ ///
+ ///
+ /// Driver IPC unavailable, make sure that at least one compatible
+ /// controller is connected and operational.
+ ///
+ /// A containing success (or error) details.
+ /// This is synonymous with "pairing" to a new Bluetooth host.
+ /// The one-based device index.
+ /// The new host address.
+ ///
+ /// The was outside a valid
+ /// range.
+ ///
+ /// A different thread is currently performing a data exchange.
+ /// The driver didn't respond within an expected period.
+ /// The driver returned unexpected or malformed data.
+ [SuppressMessage("ReSharper", "UnusedMember.Global")]
+ public unsafe SetHostResult SetHostAddress(int deviceIndex, PhysicalAddress hostAddress)
+ {
+ if (_commandMutex is null || _cmdView is null)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+
+ ValidateDeviceIndex(deviceIndex);
+
+ AcquireCommandLock();
+
+ try
+ {
+ ref DSHM_IPC_MSG_PAIR_TO_REQUEST request = ref Unsafe.AsRef(_cmdView);
+
+ request.Header.Type = DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE;
+ request.Header.Target = DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_DEVICE;
+ request.Header.Command.Device = DSHM_IPC_MSG_CMD_DEVICE.DSHM_IPC_MSG_CMD_DEVICE_PAIR_TO;
+ request.Header.TargetIndex = (uint)deviceIndex;
+ request.Header.Size = (uint)Marshal.SizeOf();
+
+ fixed (byte* source = hostAddress.GetAddressBytes())
+ fixed (byte* address = request.Address)
+ {
+ if (source is not null)
+ {
+ Buffer.MemoryCopy(source, address, 6, 6);
+ }
+ else
+ {
+ // there might be previous values there we need to zero out
+ Unsafe.InitBlockUnaligned(address, 0, 6);
+ }
+ }
+
+ if (!SendAndWait())
+ {
+ throw new DsHidMiniInteropReplyTimeoutException();
+ }
+
+ ref DSHM_IPC_MSG_PAIR_TO_REPLY reply = ref Unsafe.AsRef(_cmdView);
+
+ //
+ // Plausibility check
+ //
+ if (reply.Header.Type == DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_REQUEST_REPLY
+ && reply.Header is
+ {
+ Target: DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_CLIENT,
+ Command.Device: DSHM_IPC_MSG_CMD_DEVICE.DSHM_IPC_MSG_CMD_DEVICE_PAIR_TO
+ }
+ && reply.Header.TargetIndex == deviceIndex
+ && reply.Header.Size == Marshal.SizeOf())
+ {
+ return new SetHostResult { WriteStatus = reply.WriteStatus, ReadStatus = reply.ReadStatus };
+ }
+
+ throw new DsHidMiniInteropUnexpectedReplyException(ref reply.Header);
+ }
+ finally
+ {
+ _commandMutex.ReleaseMutex();
+ }
+ }
+
+ ///
+ /// Overwrites the player slot indicator (player LEDs) of the given device.
+ ///
+ /// The one-based device index.
+ /// The player index to set to. Valid values include 1 to 7.
+ ///
+ /// Driver IPC unavailable, make sure that at least one compatible
+ /// controller is connected and operational.
+ ///
+ ///
+ ///
+ /// The or
+ /// were out of range.
+ ///
+ /// A different thread is currently performing a data exchange.
+ /// The driver didn't respond within an expected period.
+ /// The driver returned unexpected or malformed data.
+ [SuppressMessage("ReSharper", "UnusedMember.Global")]
+ public unsafe UInt32 SetPlayerIndex(int deviceIndex, byte playerIndex)
+ {
+ if (_commandMutex is null || _cmdView is null)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+
+ ValidateDeviceIndex(deviceIndex);
+
+ if (playerIndex is < 1 or > 7)
+ {
+ throw new ArgumentOutOfRangeException(nameof(playerIndex),
+ "Player index must be between (including) 1 and 7.");
+ }
+
+ AcquireCommandLock();
+
+ try
+ {
+ ref DSHM_IPC_MSG_SET_PLAYER_INDEX_REQUEST request =
+ ref Unsafe.AsRef(_cmdView);
+
+ request.Header.Type = DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE;
+ request.Header.Target = DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_DEVICE;
+ request.Header.Command.Device = DSHM_IPC_MSG_CMD_DEVICE.DSHM_IPC_MSG_CMD_DEVICE_SET_PLAYER_INDEX;
+ request.Header.TargetIndex = (uint)deviceIndex;
+ request.Header.Size = (uint)Marshal.SizeOf();
+
+ request.PlayerIndex = playerIndex;
+
+ if (!SendAndWait())
+ {
+ throw new DsHidMiniInteropReplyTimeoutException();
+ }
+
+ ref DSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY reply =
+ ref Unsafe.AsRef(_cmdView);
+
+ //
+ // Plausibility check
+ //
+ if (reply.Header is
+ {
+ Type: DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_REQUEST_REPLY,
+ Target: DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_CLIENT,
+ Command.Device: DSHM_IPC_MSG_CMD_DEVICE.DSHM_IPC_MSG_CMD_DEVICE_SET_PLAYER_INDEX
+ }
+ && reply.Header.TargetIndex == deviceIndex
+ && reply.Header.Size == Marshal.SizeOf())
+ {
+ return reply.NtStatus;
+ }
+
+ throw new DsHidMiniInteropUnexpectedReplyException(ref reply.Header);
+ }
+ finally
+ {
+ _commandMutex.ReleaseMutex();
+ }
+ }
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.cs b/SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.cs
new file mode 100644
index 00000000..24c8f9df
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/DsHidMiniInterop.cs
@@ -0,0 +1,393 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.IO.MemoryMappedFiles;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.System.Memory;
+using Windows.Win32.System.SystemInformation;
+using Windows.Win32.System.Threading;
+
+using Microsoft.Win32.SafeHandles;
+
+using Nefarius.DsHidMini.IPC.Exceptions;
+using Nefarius.DsHidMini.IPC.Models;
+using Nefarius.DsHidMini.IPC.Models.Drivers;
+using Nefarius.Utilities.DeviceManagement.PnP;
+
+namespace Nefarius.DsHidMini.IPC;
+
+///
+/// Connects to the drivers shared memory region and offers utility methods for data exchange.
+///
+public sealed partial class DsHidMiniInterop : IDisposable
+{
+ private const string FileMapName = "Global\\DsHidMiniSharedMemory";
+ private const string ReadEventName = "Global\\DsHidMiniReadEvent";
+ private const string WriteEventName = "Global\\DsHidMiniWriteEvent";
+ private const string MutexName = "Global\\DsHidMiniCommandMutex";
+
+ private readonly Dictionary _connectedDevices = new();
+ private readonly DeviceNotificationListener _deviceListener = new();
+ private MEMORY_MAPPED_VIEW_ADDRESS? _cmdView;
+
+ private Mutex? _commandMutex;
+
+ private SafeFileHandle? _fileMapping;
+ private MEMORY_MAPPED_VIEW_ADDRESS? _hidView;
+
+ private EventWaitHandle? _inputReportEvent;
+
+ private EventWaitHandle? _readEvent;
+ private EventWaitHandle? _writeEvent;
+
+ ///
+ /// Creates a new instance by connecting to the driver IPC mechanism.
+ ///
+ ///
+ /// No driver instance is available. Make sure that at least one
+ /// device is connected and that the driver is installed and working properly. Call prior to
+ /// avoid this exception.
+ ///
+ public DsHidMiniInterop()
+ {
+ Reconnect();
+
+ _deviceListener.RegisterDeviceArrived(DsHidMiniDeviceArrived, DsHidMiniDriver.DeviceInterfaceGuid);
+ _deviceListener.RegisterDeviceRemoved(DsHidMiniDeviceRemoved, DsHidMiniDriver.DeviceInterfaceGuid);
+
+ RefreshDevices();
+ }
+
+ ///
+ /// Gets whether driver IPC is available.
+ ///
+ [SuppressMessage("ReSharper", "UnusedMember.Global")]
+ public static bool IsAvailable
+ {
+ get
+ {
+ try
+ {
+ using MemoryMappedFile mmf = MemoryMappedFile.OpenExisting(FileMapName);
+
+ return true;
+ }
+ catch (FileNotFoundException)
+ {
+ return false;
+ }
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_cmdView.HasValue)
+ {
+ PInvoke.UnmapViewOfFile(_cmdView.Value);
+ }
+
+ if (_hidView.HasValue)
+ {
+ PInvoke.UnmapViewOfFile(_hidView.Value);
+ }
+
+ _fileMapping?.Dispose();
+
+ _readEvent?.Dispose();
+ _writeEvent?.Dispose();
+ _inputReportEvent?.Dispose();
+
+ _commandMutex?.Dispose();
+ }
+
+ ///
+ /// Attempt re-initialization of IPC after all devices got disconnected.
+ ///
+ ///
+ /// No driver instance is available. Make sure that at least one
+ /// device is connected and that the driver is installed and working properly. Call prior to
+ /// avoid this exception.
+ ///
+ public void Reconnect()
+ {
+ PInvoke.GetSystemInfo(out SYSTEM_INFO systemInfo);
+
+ try
+ {
+ _commandMutex = Mutex.OpenExisting(MutexName);
+ _readEvent = EventWaitHandle.OpenExisting(ReadEventName);
+ _writeEvent = EventWaitHandle.OpenExisting(WriteEventName);
+ }
+ catch (WaitHandleCannotBeOpenedException)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+
+ try
+ {
+ _fileMapping = PInvoke.OpenFileMapping(
+ (uint)(FILE_MAP.FILE_MAP_READ | FILE_MAP.FILE_MAP_WRITE),
+ new BOOL(false),
+ FileMapName
+ );
+
+ _cmdView = PInvoke.MapViewOfFile(
+ _fileMapping,
+ FILE_MAP.FILE_MAP_READ | FILE_MAP.FILE_MAP_WRITE,
+ 0,
+ 0,
+ systemInfo.dwAllocationGranularity
+ );
+
+ if (_cmdView.Value == 0)
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to access command view");
+ }
+
+ uint pageSize = systemInfo.dwAllocationGranularity;
+ long alignedOffset = systemInfo.dwAllocationGranularity / pageSize * pageSize;
+ long offsetWithinPage = systemInfo.dwAllocationGranularity % pageSize;
+
+ _hidView = PInvoke.MapViewOfFile(
+ _fileMapping,
+ FILE_MAP.FILE_MAP_READ,
+ 0,
+ (uint)alignedOffset,
+ (uint)(systemInfo.dwAllocationGranularity + offsetWithinPage)
+ );
+
+ if (_hidView.Value == 0)
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to access HID view");
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+ }
+
+ private void RefreshDevices()
+ {
+ _connectedDevices.Clear();
+
+ int instanceIndex = 0;
+
+ while (Devcon.FindByInterfaceGuid(DsHidMiniDriver.DeviceInterfaceGuid, out PnPDevice device, instanceIndex++))
+ {
+ _connectedDevices.Add(instanceIndex, device);
+ }
+
+ //
+ // We need to let go of all shared resources or the driver won't be able to re-create the named objects
+ //
+ if (_connectedDevices.Count == 0)
+ {
+ Dispose();
+ }
+ }
+
+ private void DsHidMiniDeviceRemoved(DeviceEventArgs obj)
+ {
+ PnPDevice? device = PnPDevice.GetDeviceByInterfaceId(obj.SymLink, DeviceLocationFlags.Phantom);
+
+ KeyValuePair item = _connectedDevices.Single(kvp => kvp.Value.Equals(device));
+
+ // TODO: react to removal
+
+ RefreshDevices();
+ }
+
+ private void DsHidMiniDeviceArrived(DeviceEventArgs obj)
+ {
+ if (_connectedDevices.Count == 0)
+ {
+ Reconnect();
+ }
+
+ RefreshDevices();
+ }
+
+ ///
+ /// Gets the input report wait handle from the driver and duplicates it into the current process.
+ ///
+ ///
+ /// Driver process interaction failed due to missing permissions;
+ /// this operation requires elevated privileges.
+ ///
+ /// The driver returned unexpected or malformed data.
+ /// Handle duplication failed.
+ /// The driver didn't respond within an expected period.
+ /// A different thread is currently performing a data exchange.
+ ///
+ /// No driver instance is available. Make sure that at least one
+ /// device is connected and that the driver is installed and working properly. Call prior to
+ /// avoid this exception.
+ ///
+ private unsafe EventWaitHandle GetHidReportWaitHandle(int deviceIndex)
+ {
+ if (_commandMutex is null || _cmdView is null)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+
+ ValidateDeviceIndex(deviceIndex);
+
+ AcquireCommandLock();
+
+ try
+ {
+ ref DSHM_IPC_MSG_HEADER request = ref Unsafe.AsRef(_cmdView);
+
+ request.Type = DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_RESPONSE_ONLY;
+ request.Target = DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_DEVICE;
+ request.Command.Device = DSHM_IPC_MSG_CMD_DEVICE.DSHM_IPC_MSG_CMD_DEVICE_GET_HID_WAIT_HANDLE;
+ request.TargetIndex = (uint)deviceIndex;
+ request.Size = (uint)Marshal.SizeOf();
+
+ if (!SendAndWait())
+ {
+ throw new DsHidMiniInteropReplyTimeoutException();
+ }
+
+ ref DSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE reply =
+ ref Unsafe.AsRef(_cmdView);
+
+ //
+ // Plausibility check
+ //
+ if (reply.Header is
+ {
+ Type: DSHM_IPC_MSG_TYPE.DSHM_IPC_MSG_TYPE_RESPONSE_ONLY,
+ Target: DSHM_IPC_MSG_TARGET.DSHM_IPC_MSG_TARGET_CLIENT,
+ Command.Device: DSHM_IPC_MSG_CMD_DEVICE.DSHM_IPC_MSG_CMD_DEVICE_GET_HID_WAIT_HANDLE
+ }
+ && reply.Header.TargetIndex == deviceIndex
+ && reply.Header.Size == Marshal.SizeOf())
+ {
+ HANDLE driverProcess = PInvoke.OpenProcess(
+ PROCESS_ACCESS_RIGHTS.PROCESS_DUP_HANDLE,
+ new BOOL(false),
+ reply.ProcessId
+ );
+
+ try
+ {
+ if (driverProcess.IsNull)
+ {
+ if (Marshal.GetLastWin32Error() == (int)WIN32_ERROR.ERROR_ACCESS_DENIED)
+ {
+ throw new DsHidMiniInteropAccessDeniedException();
+ }
+
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcess call failed.");
+ }
+
+ HANDLE dupHandle;
+
+ if (!PInvoke.DuplicateHandle(
+ driverProcess,
+ new HANDLE(reply.WaitHandle),
+ PInvoke.GetCurrentProcess(),
+ &dupHandle,
+ 0,
+ new BOOL(false),
+ DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS
+ ))
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "DuplicateHandle call failed.");
+ }
+
+ return new EventWaitHandle(false, EventResetMode.AutoReset)
+ {
+ SafeWaitHandle = new SafeWaitHandle(dupHandle, true)
+ };
+ }
+ finally
+ {
+ if (!driverProcess.IsNull)
+ {
+ PInvoke.CloseHandle(driverProcess);
+ }
+ }
+ }
+
+ throw new DsHidMiniInteropUnexpectedReplyException(ref reply.Header);
+ }
+ finally
+ {
+ _commandMutex.ReleaseMutex();
+ }
+ }
+
+ private void AcquireCommandLock()
+ {
+ if (_commandMutex is null)
+ {
+ throw new DsHidMiniInteropUnavailableException();
+ }
+
+ try
+ {
+ if (!_commandMutex.WaitOne(0))
+ {
+ throw new DsHidMiniInteropConcurrencyException();
+ }
+ }
+ catch (AbandonedMutexException) { }
+ }
+
+ ///
+ /// Ensures the target device index is in a valid range.
+ ///
+ ///
+ /// The was outside a valid
+ /// range.
+ ///
+ private static void ValidateDeviceIndex(int deviceIndex)
+ {
+ if (deviceIndex is <= 0 or > byte.MaxValue)
+ {
+ throw new DsHidMiniInteropInvalidDeviceIndexException(deviceIndex);
+ }
+ }
+
+ ///
+ /// Signal the driver that we are done modifying the shared region and are now awaiting an update from the driver.
+ ///
+ /// Timeout to wait for a reply. Defaults to 500ms.
+ /// TRUE if we got a reply in time, FALSE otherwise.
+ private bool SendAndWait(int timeoutMs = 500)
+ {
+ return SendAndWait(TimeSpan.FromMilliseconds(timeoutMs));
+ }
+
+ ///
+ /// Signal the driver that we are done modifying the shared region and are now awaiting an update from the driver.
+ ///
+ /// Timeout to wait for a reply.
+ /// TRUE if we got a reply in time, FALSE otherwise.
+ private bool SendAndWait(TimeSpan timeout)
+ {
+ SignalWriteFinished();
+ return _writeEvent!.WaitOne(timeout);
+ }
+
+ [SuppressMessage("ReSharper", "UnusedMember.Local")]
+ private void SignalReadFinished()
+ {
+ // we are done reading from the region, driver can now write
+ _writeEvent!.Set();
+ }
+
+ [SuppressMessage("ReSharper", "UnusedMember.Local")]
+ private void SignalWriteFinished()
+ {
+ // we are done writing to the region, driver can now read
+ _readEvent!.Set();
+ }
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Exceptions/Exceptions.cs b/SDK/Nefarius.DsHidMini.IPC/Exceptions/Exceptions.cs
new file mode 100644
index 00000000..b344f44f
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Exceptions/Exceptions.cs
@@ -0,0 +1,78 @@
+using Nefarius.DsHidMini.IPC.Models;
+
+namespace Nefarius.DsHidMini.IPC.Exceptions;
+
+///
+/// Driver IPC unavailable, make sure that at least one compatible controller is connected and operational.
+///
+public sealed class DsHidMiniInteropUnavailableException : Exception
+{
+ internal DsHidMiniInteropUnavailableException() : base(
+ "Driver IPC unavailable, make sure that at least one compatible controller is connected and operational.")
+ {
+ }
+}
+
+///
+/// Operation timed out while waiting for a request reply.
+///
+public sealed class DsHidMiniInteropReplyTimeoutException : Exception
+{
+ internal DsHidMiniInteropReplyTimeoutException() : base("Operation timed out while waiting for a request reply.")
+ {
+ }
+}
+
+///
+/// A request reply was malformed.
+///
+public sealed class DsHidMiniInteropUnexpectedReplyException : Exception
+{
+ private readonly DSHM_IPC_MSG_HEADER? _header;
+
+ internal DsHidMiniInteropUnexpectedReplyException() : base("A request reply was malformed.") { }
+
+ internal DsHidMiniInteropUnexpectedReplyException(ref DSHM_IPC_MSG_HEADER header) : this()
+ {
+ _header = header;
+ }
+
+ ///
+ public override string Message =>
+ _header.HasValue
+ ? $"{base.Message}, Type: {_header.Value.Type}, Target: {_header.Value.Target}, Command: {_header.Value.Command.Driver}, TargetIndex: {_header.Value.TargetIndex}, Size: {_header.Value.Size}"
+ : base.Message;
+}
+
+///
+/// Multiple threads tried to invoke IPC methods at the same time, which is not supported.
+///
+public sealed class DsHidMiniInteropConcurrencyException : Exception
+{
+ internal DsHidMiniInteropConcurrencyException() : base(
+ "Multiple threads tried to invoke IPC methods at the same time, which is not supported.")
+ {
+ }
+}
+
+///
+/// The provided device index was out of range; allowed values must remain between (including) 1 and 255.
+///
+public sealed class DsHidMiniInteropInvalidDeviceIndexException : Exception
+{
+ internal DsHidMiniInteropInvalidDeviceIndexException(int deviceIndex) : base(
+ $"The provided device index {deviceIndex} was out of range; allowed values must remain between (including) 1 and 255.")
+ {
+ }
+}
+
+///
+/// Driver process interaction failed due to missing permissions; this operation requires elevated privileges.
+///
+public sealed class DsHidMiniInteropAccessDeniedException : Exception
+{
+ internal DsHidMiniInteropAccessDeniedException() : base(
+ "Driver process interaction failed due to missing permissions; this operation requires elevated privileges.")
+ {
+ }
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Models/CmdStructs.cs b/SDK/Nefarius.DsHidMini.IPC/Models/CmdStructs.cs
new file mode 100644
index 00000000..199e8ba0
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Models/CmdStructs.cs
@@ -0,0 +1,148 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+using Nefarius.DsHidMini.IPC.Models.Public;
+
+namespace Nefarius.DsHidMini.IPC.Models;
+
+///
+/// What command is this message carrying
+///
+[StructLayout(LayoutKind.Explicit)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+internal struct DSHM_IPC_MSG_COMMAND
+{
+ ///
+ /// Driver global command
+ ///
+ [FieldOffset(0)]
+ public DSHM_IPC_MSG_CMD_DRIVER Driver;
+
+ ///
+ /// Device-specific command
+ ///
+ [FieldOffset(0)]
+ public DSHM_IPC_MSG_CMD_DEVICE Device;
+}
+
+///
+/// Prefix of every packet describing the message
+///
+[StructLayout(LayoutKind.Sequential)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal struct DSHM_IPC_MSG_HEADER
+{
+ ///
+ /// What request-behavior is expected (request, request-reply, ...)
+ ///
+ public DSHM_IPC_MSG_TYPE Type;
+
+ ///
+ /// What component is this message targeting (driver, device, ...)
+ ///
+ public DSHM_IPC_MSG_TARGET Target;
+
+ ///
+ /// What command is this message carrying
+ ///
+ public DSHM_IPC_MSG_COMMAND Command;
+
+ ///
+ /// One-based index of which device is this message for
+ ///
+ /// Set to 0 if driver is targeted
+ public uint TargetIndex;
+
+ ///
+ /// The size of the entire message (header + payload) in bytes
+ ///
+ /// A size of 0 is invalid
+ public uint Size;
+}
+
+///
+/// Updates a specified devices' host address
+///
+[StructLayout(LayoutKind.Sequential)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal unsafe struct DSHM_IPC_MSG_PAIR_TO_REQUEST
+{
+ public DSHM_IPC_MSG_HEADER Header;
+
+ public fixed byte Address[6];
+}
+
+///
+/// Reply to .
+///
+[StructLayout(LayoutKind.Sequential)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal struct DSHM_IPC_MSG_PAIR_TO_REPLY
+{
+ public DSHM_IPC_MSG_HEADER Header;
+
+ ///
+ /// NTSTATUS of the set address action
+ ///
+ public UInt32 WriteStatus;
+
+ ///
+ /// NTSTATUS of the get address action
+ ///
+ public UInt32 ReadStatus;
+}
+
+///
+/// Updates the player index of a given device
+///
+[StructLayout(LayoutKind.Sequential)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal struct DSHM_IPC_MSG_SET_PLAYER_INDEX_REQUEST
+{
+ public DSHM_IPC_MSG_HEADER Header;
+
+ ///
+ /// The new player index to set
+ ///
+ /// Valid values are 1 to 7
+ public byte PlayerIndex;
+}
+
+///
+/// Reply to .
+///
+[StructLayout(LayoutKind.Sequential)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal struct DSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY
+{
+ public DSHM_IPC_MSG_HEADER Header;
+
+ public UInt32 NtStatus;
+}
+
+///
+/// Requests the driver host process PID and a wait handle for new input reports
+///
+/// Post-processing this command requires elevated privileges.
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[StructLayout(LayoutKind.Sequential)]
+internal struct DSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE
+{
+ public DSHM_IPC_MSG_HEADER Header;
+
+ ///
+ /// The driver hosting process PID
+ ///
+ public UInt32 ProcessId;
+
+ ///
+ /// A handle to an auto-reset event
+ ///
+ /// The requester of this handle must duplicate it into the current process before it becomes usable.
+ public IntPtr WaitHandle;
+}
diff --git a/SDK/Nefarius.DsHidMini.IPC/Models/Drivers/DsHidMiniDriver.cs b/SDK/Nefarius.DsHidMini.IPC/Models/Drivers/DsHidMiniDriver.cs
new file mode 100644
index 00000000..a917065e
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Models/Drivers/DsHidMiniDriver.cs
@@ -0,0 +1,166 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+
+using Nefarius.DsHidMini.IPC.Util.Converters;
+using Nefarius.Utilities.DeviceManagement.PnP;
+
+namespace Nefarius.DsHidMini.IPC.Models.Drivers;
+
+///
+/// Interface and property information about the DsHidMini driver.
+///
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+public static class DsHidMiniDriver
+{
+ ///
+ /// Interface GUID common to all devices the DsHidMini driver supports.
+ ///
+ public static Guid DeviceInterfaceGuid => Guid.Parse("{399ED672-E0BD-4FB3-AB0C-4955B56FB86A}");
+
+ #region Read-only properties
+
+ ///
+ /// The last reported of the device.
+ ///
+ public static DevicePropertyKey BatteryStatusProperty => CustomDeviceProperty.CreateCustomDeviceProperty(
+ Guid.Parse("{3FECF510-CC94-4FBE-8839-738201F84D59}"), 2,
+ typeof(byte));
+
+ public static DevicePropertyKey LastPairingStatusProperty => CustomDeviceProperty.CreateCustomDeviceProperty(
+ Guid.Parse("{3FECF510-CC94-4FBE-8839-738201F84D59}"), 3,
+ typeof(int));
+
+ public static DevicePropertyKey LastHostRequestStatusProperty => CustomDeviceProperty.CreateCustomDeviceProperty(
+ Guid.Parse("{3FECF510-CC94-4FBE-8839-738201F84D59}"), 5,
+ typeof(int));
+
+ #endregion
+
+ #region Common device properties
+
+ ///
+ /// The currently active .
+ ///
+ public static DevicePropertyKey HidDeviceModeProperty => CustomDeviceProperty.CreateCustomDeviceProperty(
+ Guid.Parse("{6D293077-C3D6-4062-9597-BE4389404C02}"), 2,
+ typeof(byte));
+
+ ///
+ /// The Bluetooth MAC address the device is currently paired to.
+ ///
+ public static DevicePropertyKey HostAddressProperty => CustomDeviceProperty.CreateCustomDeviceProperty(
+ Guid.Parse("{0xa92f26ca, 0xeda7, 0x4b1d, {0x9d, 0xb2, 0x27, 0xb6, 0x8a, 0xa5, 0xa2, 0xeb}}"), 1,
+ typeof(ulong));
+
+ ///
+ /// The Bluetooth MAC address of the device itself.
+ ///
+ public static DevicePropertyKey DeviceAddressProperty => CustomDeviceProperty.CreateCustomDeviceProperty(
+ Guid.Parse("{0x2bd67d8b, 0x8beb, 0x48d5, {0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a}}"), 1,
+ typeof(string));
+
+ ///
+ /// Timestamp of last wireless connection.
+ ///
+ public static DevicePropertyKey BluetoothLastConnectedTimeProperty =>
+ CustomDeviceProperty.CreateCustomDeviceProperty(
+ Guid.Parse("{0x2bd67d8b, 0x8beb, 0x48d5, {0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a}}"), 11,
+ typeof(DateTimeOffset));
+
+ #endregion
+}
+
+///
+/// Battery status values.
+///
+[TypeConverter(typeof(EnumDescriptionTypeConverter))]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+public enum DsBatteryStatus : byte
+{
+ ///
+ /// Unknown/not yet reported.
+ ///
+ [Description("Unknown")]
+ Unknown = 0x00,
+
+ ///
+ /// Dying. Battery is so low the device is barely being kept on.
+ ///
+ [Description("Dying")]
+ Dying = 0x01,
+
+ ///
+ /// Low. Device should be charged soon.
+ ///
+ [Description("Low")]
+ Low = 0x02,
+
+ ///
+ /// Medium. Will last for a while but should be charged soon.
+ ///
+ [Description("Medium")]
+ Medium = 0x03,
+
+ ///
+ /// High. Will last for a few sessions.
+ ///
+ [Description("High")]
+ High = 0x04,
+
+ ///
+ /// Full. Status right after successful charging.
+ ///
+ [Description("Full")]
+ Full = 0x05,
+
+ ///
+ /// Charging. The default state while wired until is reached.
+ ///
+ [Description("Charging")]
+ Charging = 0xEE,
+
+ ///
+ /// Charged. While wired synonymous to while wireless.
+ ///
+ [Description("Charged")]
+ Charged = 0xEF
+}
+
+///
+/// HID device emulation modes.
+///
+[TypeConverter(typeof(EnumDescriptionTypeConverter))]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+public enum DsHidDeviceMode : byte
+{
+ ///
+ /// Single Device with Force Feedback mode.
+ ///
+ [Description("SDF (PCSX2 Non-Qt-Edition)")]
+ SDF = 0x01,
+
+ ///
+ /// Gamepad plus Joystick mode.
+ ///
+ [Description("GPJ (Separated pressure)")]
+ GPJ = 0x02,
+
+ ///
+ /// SIXAXIS.SYS mode.
+ ///
+ [Description("SXS (Steam, RPCS3, PCSX2 Qt-Edition)")]
+ SXS = 0x03,
+
+ ///
+ /// DS4Windows DualShock 4 emulation mode.
+ ///
+ [Description("DS4Windows")]
+ DS4W = 0x04,
+
+ ///
+ /// Xbox One Controller mode.
+ ///
+ [Description("XInput (Xbox One)")]
+ XInput = 0x05
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Models/Enums.cs b/SDK/Nefarius.DsHidMini.IPC/Models/Enums.cs
new file mode 100644
index 00000000..ed00a677
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Models/Enums.cs
@@ -0,0 +1,108 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Nefarius.DsHidMini.IPC.Models;
+
+///
+/// Describes the type of IPC message response behavior
+///
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal enum DSHM_IPC_MSG_TYPE : UInt32
+{
+ ///
+ /// Invalid/reserved, do not use
+ ///
+ DSHM_IPC_MSG_TYPE_INVALID = 0,
+
+ ///
+ /// Client-to-driver data incoming, no acknowledgment/reply requested
+ ///
+ DSHM_IPC_MSG_TYPE_REQUEST_ONLY,
+
+ ///
+ /// Client-to-driver data incoming, must be acknowledged by reply
+ ///
+ DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE,
+
+ ///
+ /// Client requested data, there is nothing to read for the driver
+ ///
+ DSHM_IPC_MSG_TYPE_RESPONSE_ONLY,
+
+ ///
+ /// Driver-to-client response to a previous DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE
+ ///
+ DSHM_IPC_MSG_TYPE_REQUEST_REPLY
+}
+
+///
+/// Describes the message receiver
+///
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal enum DSHM_IPC_MSG_TARGET : UInt32
+{
+ ///
+ /// Invalid/reserved, do not use
+ ///
+ DSHM_IPC_MSG_TARGET_INVALID = 0,
+
+ ///
+ /// The message is targeted at the driver
+ ///
+ DSHM_IPC_MSG_TARGET_DRIVER,
+
+ ///
+ /// The message is targeted at a device
+ ///
+ DSHM_IPC_MSG_TARGET_DEVICE,
+
+ ///
+ /// The message is targeted at the client/caller/app
+ ///
+ DSHM_IPC_MSG_TARGET_CLIENT
+}
+
+///
+/// Describes a per-driver command
+///
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal enum DSHM_IPC_MSG_CMD_DRIVER : UInt32
+{
+ ///
+ /// Invalid/reserved, do not use
+ ///
+ DSHM_IPC_MSG_CMD_DRIVER_INVALID = 0,
+
+ ///
+ /// Message without payload, useful to check for functionality
+ ///
+ DSHM_IPC_MSG_CMD_DRIVER_PING
+}
+
+// Describes a per-device command
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+[SuppressMessage("ReSharper", "UnusedMember.Global")]
+internal enum DSHM_IPC_MSG_CMD_DEVICE : UInt32
+{
+ ///
+ /// Invalid/reserved, do not use
+ ///
+ DSHM_IPC_MSG_CMD_DEVICE_INVALID = 0,
+
+ ///
+ /// Pair a given device to a new host
+ ///
+ DSHM_IPC_MSG_CMD_DEVICE_PAIR_TO,
+
+ ///
+ /// Requests a player index update (switch player LED etc.)
+ ///
+ DSHM_IPC_MSG_CMD_DEVICE_SET_PLAYER_INDEX,
+
+ ///
+ /// Requests a wait handle for input report state changes
+ ///
+ DSHM_IPC_MSG_CMD_DEVICE_GET_HID_WAIT_HANDLE
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Models/HidStructs.cs b/SDK/Nefarius.DsHidMini.IPC/Models/HidStructs.cs
new file mode 100644
index 00000000..e789dfd5
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Models/HidStructs.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+using Nefarius.DsHidMini.IPC.Models.Public;
+
+namespace Nefarius.DsHidMini.IPC.Models;
+
+///
+/// Represents the most current raw DS3 HID input report.
+///
+[StructLayout(LayoutKind.Sequential)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+internal struct IPC_HID_INPUT_REPORT_MESSAGE
+{
+ ///
+ /// The one-based device index this report belongs to.
+ ///
+ public UInt32 SlotIndex;
+
+ ///
+ /// The coming directly from the device with no transformations applied.
+ ///
+ public DS3_RAW_INPUT_REPORT InputReport;
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Models/Public/RawInputReport.cs b/SDK/Nefarius.DsHidMini.IPC/Models/Public/RawInputReport.cs
new file mode 100644
index 00000000..4395372b
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Models/Public/RawInputReport.cs
@@ -0,0 +1,276 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+namespace Nefarius.DsHidMini.IPC.Models.Public;
+
+///
+/// Raw input report as it is sent by the SIXAXIS/DualShock 3.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+public unsafe struct DS3_RAW_INPUT_REPORT
+{
+ ///
+ /// Report ID (always 1).
+ ///
+ public byte ReportId;
+
+ internal byte Reserved0;
+
+ ///
+ /// Button breakouts in various formats.
+ ///
+ [StructLayout(LayoutKind.Explicit)]
+ [SuppressMessage("ReSharper", "UnusedMember.Global")]
+ public struct ButtonUnion
+ {
+ [FieldOffset(0)]
+ internal uint lButtons;
+
+ [FieldOffset(0)]
+ internal fixed byte bButtons[4];
+
+ [FieldOffset(0)]
+ internal ushort packedButtons;
+
+ ///
+ /// Gets or sets the Select button state.
+ ///
+ public bool Select { get => GetBit(packedButtons, 0); set => SetBit(ref packedButtons, 0, value); }
+
+ ///
+ /// Gets or sets the Left Thumb button state.
+ ///
+ public bool L3 { get => GetBit(packedButtons, 1); set => SetBit(ref packedButtons, 1, value); }
+
+ ///
+ /// Gets or sets the Right Thumb button state.
+ ///
+ public bool R3 { get => GetBit(packedButtons, 2); set => SetBit(ref packedButtons, 2, value); }
+
+ ///
+ /// Gets or sets the Start button state.
+ ///
+ public bool Start { get => GetBit(packedButtons, 3); set => SetBit(ref packedButtons, 3, value); }
+
+ ///
+ /// Gets or sets the DPad Up button state.
+ ///
+ public bool Up { get => GetBit(packedButtons, 4); set => SetBit(ref packedButtons, 4, value); }
+
+ ///
+ /// Gets or sets the DPad Right button state.
+ ///
+ public bool Right { get => GetBit(packedButtons, 5); set => SetBit(ref packedButtons, 5, value); }
+
+ ///
+ /// Gets or sets the DPad Down button state.
+ ///
+ public bool Down { get => GetBit(packedButtons, 6); set => SetBit(ref packedButtons, 6, value); }
+
+ ///
+ /// Gets or sets the DPad Left button state.
+ ///
+ public bool Left { get => GetBit(packedButtons, 7); set => SetBit(ref packedButtons, 7, value); }
+
+ ///
+ /// Gets or sets the Left Trigger button state.
+ ///
+ public bool L2 { get => GetBit(packedButtons, 8); set => SetBit(ref packedButtons, 8, value); }
+
+ ///
+ /// Gets or sets the Right Trigger button state.
+ ///
+ public bool R2 { get => GetBit(packedButtons, 9); set => SetBit(ref packedButtons, 9, value); }
+
+ ///
+ /// Gets or sets the Left Shoulder button state.
+ ///
+ public bool L1 { get => GetBit(packedButtons, 10); set => SetBit(ref packedButtons, 10, value); }
+
+ ///
+ /// Gets or sets the Right Shoulder button state.
+ ///
+ public bool R1 { get => GetBit(packedButtons, 11); set => SetBit(ref packedButtons, 11, value); }
+
+ ///
+ /// Gets or sets the Triangle button state.
+ ///
+ public bool Triangle { get => GetBit(packedButtons, 12); set => SetBit(ref packedButtons, 12, value); }
+
+ ///
+ /// Gets or sets the Circle button state.
+ ///
+ public bool Circle { get => GetBit(packedButtons, 13); set => SetBit(ref packedButtons, 13, value); }
+
+ ///
+ /// Gets or sets the Cross button state.
+ ///
+ public bool Cross { get => GetBit(packedButtons, 14); set => SetBit(ref packedButtons, 14, value); }
+
+ ///
+ /// Gets or sets the Square button state.
+ ///
+ public bool Square { get => GetBit(packedButtons, 15); set => SetBit(ref packedButtons, 15, value); }
+
+ ///
+ /// Gets or sets the PS button state.
+ ///
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ public bool PS { get => GetBit(packedButtons, 16); set => SetBit(ref packedButtons, 16, value); }
+
+ // Helper methods to manipulate individual bits
+ private static bool GetBit(ushort value, int bitNumber)
+ {
+ return (value & (1 << bitNumber)) != 0;
+ }
+
+ private static void SetBit(ref ushort value, int bitNumber, bool bitValue)
+ {
+ if (bitValue)
+ {
+ value |= (ushort)(1 << bitNumber);
+ }
+ else
+ {
+ value &= (ushort)~(1 << bitNumber);
+ }
+ }
+ }
+
+ ///
+ /// The individual controller buttons.
+ ///
+ public ButtonUnion Buttons;
+
+ ///
+ /// Left Thumb X-axis.
+ ///
+ /// 0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
+ public byte LeftThumbX;
+
+ ///
+ /// Left Thumb Y-axis.
+ ///
+ /// 0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
+ public byte LeftThumbY;
+
+ ///
+ /// Right Thumb X-axis.
+ ///
+ /// 0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
+ public byte RightThumbX;
+
+ ///
+ /// Right Thumb Y-axis.
+ ///
+ /// 0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
+ public byte RightThumbY;
+
+ internal fixed byte Reserved1[4];
+
+ ///
+ /// Pressure value breakouts in various formats.
+ ///
+ [StructLayout(LayoutKind.Explicit)]
+ public struct PressureUnion
+ {
+ [FieldOffset(0)]
+ internal fixed byte bValues[12];
+
+ ///
+ /// The individual pressure values.
+ ///
+ [FieldOffset(0)]
+ public PressureValues Values;
+
+ ///
+ /// Pressure button values.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct PressureValues
+ {
+ ///
+ /// DPad Up pressure value.
+ ///
+ public byte Up;
+
+ ///
+ /// DPad Right pressure value.
+ ///
+ public byte Right;
+
+ ///
+ /// DPad Down pressure value.
+ ///
+ public byte Down;
+
+ ///
+ /// DPad Left pressure value.
+ ///
+ public byte Left;
+
+ ///
+ /// Left Trigger pressure value.
+ ///
+ public byte L2;
+
+ ///
+ /// Right Trigger pressure value.
+ ///
+ public byte R2;
+
+ ///
+ /// Left Shoulder pressure value.
+ ///
+ public byte L1;
+
+ ///
+ /// Right Shoulder pressure value.
+ ///
+ public byte R1;
+
+ ///
+ /// Triangle pressure value.
+ ///
+ public byte Triangle;
+
+ ///
+ /// Circle pressure value.
+ ///
+ public byte Circle;
+
+ ///
+ /// Cross pressure value.
+ ///
+ public byte Cross;
+
+ ///
+ /// Square pressure value.
+ ///
+ public byte Square;
+ }
+ }
+
+ ///
+ /// Pressure button values.
+ ///
+ public PressureUnion Pressure;
+
+ internal fixed byte Reserved2[4];
+
+ ///
+ /// Battery charge status.
+ ///
+ public byte BatteryStatus;
+
+ internal fixed byte Reserved3[10];
+
+ //
+ // Motion information
+ //
+ public ushort AccelerometerX;
+ public ushort AccelerometerY;
+ public ushort AccelerometerZ;
+ public ushort Gyroscope;
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Models/Public/SetHostResult.cs b/SDK/Nefarius.DsHidMini.IPC/Models/Public/SetHostResult.cs
new file mode 100644
index 00000000..912460f6
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Models/Public/SetHostResult.cs
@@ -0,0 +1,19 @@
+namespace Nefarius.DsHidMini.IPC.Models.Public;
+
+public struct SetHostResult
+{
+ ///
+ /// The NTSTATUS value of the "pairing" or address overwrite action.
+ ///
+ public UInt32 WriteStatus;
+
+ ///
+ /// The NTSTATUS value of the address read/query action to verify the new address.
+ ///
+ public UInt32 ReadStatus;
+
+ public override string ToString()
+ {
+ return $"Pairing result: 0x{WriteStatus:X}, query result: 0x{ReadStatus:X}";
+ }
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Models/Public/UsbSetupPacket.cs b/SDK/Nefarius.DsHidMini.IPC/Models/Public/UsbSetupPacket.cs
new file mode 100644
index 00000000..d0531537
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Models/Public/UsbSetupPacket.cs
@@ -0,0 +1,106 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+namespace Nefarius.DsHidMini.IPC.Models.Public;
+
+///
+/// Describes a USB control transfer setup packet.
+///
+[StructLayout(LayoutKind.Explicit)]
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+public struct WDF_USB_CONTROL_SETUP_PACKET
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct RequestStruct
+ {
+ private byte _byte;
+
+ public byte Recipient
+ {
+ get => (byte)(_byte & 0x03); // Mask for the lowest 2 bits
+ set => _byte = (byte)((_byte & ~0x03) | (value & 0x03));
+ }
+
+ public byte Reserved
+ {
+ get => (byte)((_byte >> 2) & 0x07); // Next 3 bits
+ set => _byte = (byte)((_byte & ~(0x07 << 2)) | ((value & 0x07) << 2));
+ }
+
+ public byte Type
+ {
+ get => (byte)((_byte >> 5) & 0x03); // Next 2 bits
+ set => _byte = (byte)((_byte & ~(0x03 << 5)) | ((value & 0x03) << 5));
+ }
+
+ public byte Dir
+ {
+ get => (byte)((_byte >> 7) & 0x01); // The highest bit
+ set => _byte = (byte)((_byte & ~(0x01 << 7)) | ((value & 0x01) << 7));
+ }
+
+ public byte Byte
+ {
+ get => _byte;
+ set => _byte = value;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct BytesStruct
+ {
+ public byte LowByte;
+ public byte HiByte;
+ }
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1)]
+ public struct PacketStruct
+ {
+ [FieldOffset(0)]
+ public RequestStruct bm;
+
+ [FieldOffset(1)]
+ public byte bRequest;
+
+ [FieldOffset(2)]
+ internal BytesStruct wValueBytes;
+
+ [FieldOffset(4)]
+ internal BytesStruct wIndexBytes;
+
+ [FieldOffset(6)]
+ public ushort wLength;
+
+ public ushort wValue
+ {
+ get => (ushort)((wValueBytes.HiByte << 8) | wValueBytes.LowByte);
+ set
+ {
+ wValueBytes.LowByte = (byte)(value & 0xFF);
+ wValueBytes.HiByte = (byte)((value >> 8) & 0xFF);
+ }
+ }
+
+ public ushort wIndex
+ {
+ get => (ushort)((wIndexBytes.HiByte << 8) | wIndexBytes.LowByte);
+ set
+ {
+ wIndexBytes.LowByte = (byte)(value & 0xFF);
+ wIndexBytes.HiByte = (byte)((value >> 8) & 0xFF);
+ }
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public unsafe struct GenericStruct
+ {
+ public fixed byte Bytes[8];
+ }
+
+ [FieldOffset(0)]
+ public PacketStruct Packet;
+
+ [FieldOffset(0)]
+ public GenericStruct Generic;
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/NativeMethods.txt b/SDK/Nefarius.DsHidMini.IPC/NativeMethods.txt
new file mode 100644
index 00000000..4aef14ee
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/NativeMethods.txt
@@ -0,0 +1,10 @@
+OpenProcess
+DuplicateHandle
+GetCurrentProcess
+DUPLICATE_HANDLE_OPTIONS
+OpenFileMapping
+MapViewOfFile
+UnmapViewOfFile
+FILE_MAP
+GetSystemInfo
+WIN32_ERROR
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/Nefarius.DsHidMini.IPC.csproj b/SDK/Nefarius.DsHidMini.IPC/Nefarius.DsHidMini.IPC.csproj
new file mode 100644
index 00000000..1107aa4c
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Nefarius.DsHidMini.IPC.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0-windows
+ enable
+ enable
+ true
+ 1591
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/SDK/Nefarius.DsHidMini.IPC/README.md b/SDK/Nefarius.DsHidMini.IPC/README.md
new file mode 100644
index 00000000..a75af670
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/README.md
@@ -0,0 +1,17 @@
+# Nefarius.DsHidMini.IPC
+
+![Requirements](https://img.shields.io/badge/Requires-.NET%208.0-blue.svg)
+[![NuGet Version](https://img.shields.io/nuget/v/Nefarius.DsHidMini.IPC)](https://www.nuget.org/packages/Nefarius.DsHidMini.IPC/)
+[![NuGet](https://img.shields.io/nuget/dt/Nefarius.DsHidMini.IPC)](https://www.nuget.org/packages/Nefarius.DsHidMini.IPC/)
+
+Interop SDK for DsHidMini IPC mechanism.
+
+## Documentation
+
+[Link to API docs](docs/index.md).
+
+### Generating documentation
+
+- `dotnet build -c:Release`
+- `dotnet tool install -g Nefarius.Tools.XMLDoc2Markdown`
+- `xmldoc2md .\bin\Release\net8.0-windows\Nefarius.DsHidMini.IPC.dll .\docs\`
diff --git a/SDK/Nefarius.DsHidMini.IPC/Util/Converters/EnumDescriptionTypeConverter.cs b/SDK/Nefarius.DsHidMini.IPC/Util/Converters/EnumDescriptionTypeConverter.cs
new file mode 100644
index 00000000..0af79b33
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/Util/Converters/EnumDescriptionTypeConverter.cs
@@ -0,0 +1,37 @@
+using System.ComponentModel;
+using System.Globalization;
+using System.Reflection;
+
+namespace Nefarius.DsHidMini.IPC.Util.Converters;
+
+public class EnumDescriptionTypeConverter : EnumConverter
+{
+ public EnumDescriptionTypeConverter(Type type)
+ : base(type)
+ {
+ }
+
+ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value,
+ Type destinationType)
+ {
+ if (destinationType == typeof(string))
+ {
+ if (value != null)
+ {
+ FieldInfo? fi = value.GetType().GetField(value.ToString());
+ if (fi != null)
+ {
+ DescriptionAttribute[] attributes =
+ (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
+ return attributes.Length > 0 && !string.IsNullOrEmpty(attributes[0].Description)
+ ? attributes[0].Description
+ : value.ToString();
+ }
+ }
+
+ return string.Empty;
+ }
+
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+}
\ No newline at end of file
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/index.md b/SDK/Nefarius.DsHidMini.IPC/docs/index.md
new file mode 100644
index 00000000..bc37b8bc
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/index.md
@@ -0,0 +1,35 @@
+# Assembly Nefarius.DsHidMini.IPC
+
+## Namespace Nefarius.DsHidMini.IPC
+
+- [DsHidMiniInterop](./nefarius.dshidmini.ipc.dshidminiinterop.md)
+
+## Namespace Nefarius.DsHidMini.IPC.Exceptions
+
+- [DsHidMiniInteropConcurrencyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropconcurrencyexception.md)
+
+- [DsHidMiniInteropInvalidDeviceIndexException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropinvaliddeviceindexexception.md)
+
+- [DsHidMiniInteropReplyTimeoutException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md)
+
+- [DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+
+- [DsHidMiniInteropUnexpectedReplyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md)
+
+## Namespace Nefarius.DsHidMini.IPC.Models.Drivers
+
+- [DsBatteryStatus](./nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md)
+
+- [DsHidDeviceMode](./nefarius.dshidmini.ipc.models.drivers.dshiddevicemode.md)
+
+- [DsHidMiniDriver](./nefarius.dshidmini.ipc.models.drivers.dshidminidriver.md)
+
+## Namespace Nefarius.DsHidMini.IPC.Models.Public
+
+- [Ds3RawInputReport](./nefarius.dshidmini.ipc.models.public.ds3rawinputreport.md)
+
+- [SetHostResult](./nefarius.dshidmini.ipc.models.public.sethostresult.md)
+
+## Namespace Nefarius.DsHidMini.IPC.Util.Converters
+
+- [EnumDescriptionTypeConverter](./nefarius.dshidmini.ipc.util.converters.enumdescriptiontypeconverter.md)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.dshidminiinterop.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.dshidminiinterop.md
new file mode 100644
index 00000000..d4a9f925
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.dshidminiinterop.md
@@ -0,0 +1,209 @@
+# DsHidMiniInterop
+
+Namespace: Nefarius.DsHidMini.IPC
+
+Connects to the drivers shared memory region and offers utility methods for data exchange.
+
+```csharp
+public sealed class DsHidMiniInterop : System.IDisposable
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [DsHidMiniInterop](./nefarius.dshidmini.ipc.dshidminiinterop.md)
+Implements [IDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.idisposable)
+
+## Properties
+
+### **IsAvailable**
+
+Gets whether driver IPC is available.
+
+```csharp
+public static bool IsAvailable { get; }
+```
+
+#### Property Value
+
+[Boolean](https://docs.microsoft.com/en-us/dotnet/api/system.boolean)
+
+## Constructors
+
+### **DsHidMiniInterop()**
+
+Creates a new [DsHidMiniInterop](./nefarius.dshidmini.ipc.dshidminiinterop.md) instance by connecting to the driver IPC mechanism.
+
+```csharp
+public DsHidMiniInterop()
+```
+
+#### Exceptions
+
+[DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+No driver instance is available. Make sure that at least one
+ device is connected and that the driver is installed and working properly. Call [DsHidMiniInterop.IsAvailable](./nefarius.dshidmini.ipc.dshidminiinterop.md#isavailable) prior to
+ avoid this exception.
+
+## Methods
+
+### **Dispose()**
+
+```csharp
+public void Dispose()
+```
+
+### **GetRawInputReport(Int32, ref Ds3RawInputReport, Nullable<TimeSpan>)**
+
+Attempts to read the [Ds3RawInputReport](./nefarius.dshidmini.ipc.models.public.ds3rawinputreport.md) from a given device instance.
+
+```csharp
+public bool GetRawInputReport(int deviceIndex, ref Ds3RawInputReport report, Nullable timeout)
+```
+
+#### Parameters
+
+`deviceIndex` [Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+The one-based device index.
+
+`report` [Ds3RawInputReport&](./nefarius.dshidmini.ipc.models.public.ds3rawinputreport&.md)
+The [Ds3RawInputReport](./nefarius.dshidmini.ipc.models.public.ds3rawinputreport.md) to populate.
+
+`timeout` [Nullable<TimeSpan>](https://docs.microsoft.com/en-us/dotnet/api/system.nullable-1)
+Optional timeout to wait for a report update to arrive. Default invocation returns immediately.
+
+#### Returns
+
+TRUE if `report` got filled in or FALSE if the given `deviceIndex` is not
+ occupied.
+
+#### Exceptions
+
+[DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+Driver IPC unavailable, make sure that at least one compatible
+ controller is connected and operational.
+
+[DsHidMiniInteropUnexpectedReplyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md)
+The driver returned unexpected or malformed data.
+
+**Remarks:**
+
+If `timeout` is null, this method returns the last known input report copy immediately. If
+ you use this call in a busy loop, you should set a timeout so this call becomes event-based, meaning the call will
+ only return when the driver signaled that new data is available, otherwise you will just burn through CPU for no
+ good reason. A new input report is typically available each average 5 milliseconds, depending on the connection
+ (wired or wireless) so a timeout of 20 milliseconds should be a good recommendation.
+
+### **Reconnect()**
+
+Attempt re-initialization of IPC after all devices got disconnected.
+
+```csharp
+public void Reconnect()
+```
+
+#### Exceptions
+
+[DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+No driver instance is available. Make sure that at least one
+ device is connected and that the driver is installed and working properly. Call [DsHidMiniInterop.IsAvailable](./nefarius.dshidmini.ipc.dshidminiinterop.md#isavailable) prior to
+ avoid this exception.
+
+### **SendPing()**
+
+Send a PING to the driver and awaits the reply.
+
+```csharp
+public void SendPing()
+```
+
+#### Exceptions
+
+[DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+Driver IPC unavailable, make sure that at least one compatible
+ controller is connected and operational.
+
+[DsHidMiniInteropReplyTimeoutException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md)
+The driver didn't respond within an expected period.
+
+[DsHidMiniInteropUnexpectedReplyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md)
+The driver returned unexpected or malformed data.
+
+### **SetHostAddress(Int32, PhysicalAddress)**
+
+Writes a new host address to the given device.
+
+```csharp
+public SetHostResult SetHostAddress(int deviceIndex, PhysicalAddress hostAddress)
+```
+
+#### Parameters
+
+`deviceIndex` [Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+The one-based device index.
+
+`hostAddress` PhysicalAddress
+The new host address.
+
+#### Returns
+
+A [SetHostResult](./nefarius.dshidmini.ipc.models.public.sethostresult.md) containing success (or error) details.
+
+#### Exceptions
+
+[DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+Driver IPC unavailable, make sure that at least one compatible
+ controller is connected and operational.
+
+[DsHidMiniInteropInvalidDeviceIndexException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropinvaliddeviceindexexception.md)
+The `deviceIndex` was outside a valid
+ range.
+
+[DsHidMiniInteropConcurrencyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropconcurrencyexception.md)
+A different thread is currently performing a data exchange.
+
+[DsHidMiniInteropReplyTimeoutException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md)
+The driver didn't respond within an expected period.
+
+[DsHidMiniInteropUnexpectedReplyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md)
+The driver returned unexpected or malformed data.
+
+**Remarks:**
+
+This is synonymous with "pairing" to a new Bluetooth host.
+
+### **SetPlayerIndex(Int32, Byte)**
+
+Overwrites the player slot indicator (player LEDs) of the given device.
+
+```csharp
+public uint SetPlayerIndex(int deviceIndex, byte playerIndex)
+```
+
+#### Parameters
+
+`deviceIndex` [Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+The one-based device index.
+
+`playerIndex` [Byte](https://docs.microsoft.com/en-us/dotnet/api/system.byte)
+The player index to set to. Valid values include 1 to 7.
+
+#### Returns
+
+[UInt32](https://docs.microsoft.com/en-us/dotnet/api/system.uint32)
+
+#### Exceptions
+
+[DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+Driver IPC unavailable, make sure that at least one compatible
+ controller is connected and operational.
+
+[ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception)
+The `deviceIndex` or `playerIndex`
+ were out of range.
+
+[DsHidMiniInteropConcurrencyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropconcurrencyexception.md)
+A different thread is currently performing a data exchange.
+
+[DsHidMiniInteropReplyTimeoutException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md)
+The driver didn't respond within an expected period.
+
+[DsHidMiniInteropUnexpectedReplyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md)
+The driver returned unexpected or malformed data.
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropconcurrencyexception.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropconcurrencyexception.md
new file mode 100644
index 00000000..9c69dab5
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropconcurrencyexception.md
@@ -0,0 +1,94 @@
+# DsHidMiniInteropConcurrencyException
+
+Namespace: Nefarius.DsHidMini.IPC.Exceptions
+
+Multiple threads tried to invoke IPC methods at the same time, which is not supported.
+
+```csharp
+public sealed class DsHidMiniInteropConcurrencyException : System.Exception, System.Runtime.Serialization.ISerializable
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception) → [DsHidMiniInteropConcurrencyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropconcurrencyexception.md)
+Implements [ISerializable](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.iserializable)
+
+## Properties
+
+### **Data**
+
+```csharp
+public IDictionary Data { get; }
+```
+
+#### Property Value
+
+[IDictionary](https://docs.microsoft.com/en-us/dotnet/api/system.collections.idictionary)
+
+### **HelpLink**
+
+```csharp
+public string HelpLink { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **HResult**
+
+```csharp
+public int HResult { get; set; }
+```
+
+#### Property Value
+
+[Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+
+### **InnerException**
+
+```csharp
+public Exception InnerException { get; }
+```
+
+#### Property Value
+
+[Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception)
+
+### **Message**
+
+```csharp
+public string Message { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **Source**
+
+```csharp
+public string Source { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **StackTrace**
+
+```csharp
+public string StackTrace { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **TargetSite**
+
+```csharp
+public MethodBase TargetSite { get; }
+```
+
+#### Property Value
+
+[MethodBase](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropexclusiveaccessexception.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropexclusiveaccessexception.md
new file mode 100644
index 00000000..41876dcd
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropexclusiveaccessexception.md
@@ -0,0 +1,94 @@
+# DsHidMiniInteropExclusiveAccessException
+
+Namespace: Nefarius.DsHidMini.IPC.Exceptions
+
+Another client is already connected to the driver, can't continue initialization.
+
+```csharp
+public sealed class DsHidMiniInteropExclusiveAccessException : System.Exception, System.Runtime.Serialization.ISerializable
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception) → [DsHidMiniInteropExclusiveAccessException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropexclusiveaccessexception.md)
+Implements [ISerializable](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.iserializable)
+
+## Properties
+
+### **Data**
+
+```csharp
+public IDictionary Data { get; }
+```
+
+#### Property Value
+
+[IDictionary](https://docs.microsoft.com/en-us/dotnet/api/system.collections.idictionary)
+
+### **HelpLink**
+
+```csharp
+public string HelpLink { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **HResult**
+
+```csharp
+public int HResult { get; set; }
+```
+
+#### Property Value
+
+[Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+
+### **InnerException**
+
+```csharp
+public Exception InnerException { get; }
+```
+
+#### Property Value
+
+[Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception)
+
+### **Message**
+
+```csharp
+public string Message { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **Source**
+
+```csharp
+public string Source { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **StackTrace**
+
+```csharp
+public string StackTrace { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **TargetSite**
+
+```csharp
+public MethodBase TargetSite { get; }
+```
+
+#### Property Value
+
+[MethodBase](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropinvaliddeviceindexexception.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropinvaliddeviceindexexception.md
new file mode 100644
index 00000000..88e73d40
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropinvaliddeviceindexexception.md
@@ -0,0 +1,94 @@
+# DsHidMiniInteropInvalidDeviceIndexException
+
+Namespace: Nefarius.DsHidMini.IPC.Exceptions
+
+The provided device index was out of range; allowed values must remain between (including) 1 and 255.
+
+```csharp
+public sealed class DsHidMiniInteropInvalidDeviceIndexException : System.Exception, System.Runtime.Serialization.ISerializable
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception) → [DsHidMiniInteropInvalidDeviceIndexException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropinvaliddeviceindexexception.md)
+Implements [ISerializable](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.iserializable)
+
+## Properties
+
+### **Data**
+
+```csharp
+public IDictionary Data { get; }
+```
+
+#### Property Value
+
+[IDictionary](https://docs.microsoft.com/en-us/dotnet/api/system.collections.idictionary)
+
+### **HelpLink**
+
+```csharp
+public string HelpLink { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **HResult**
+
+```csharp
+public int HResult { get; set; }
+```
+
+#### Property Value
+
+[Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+
+### **InnerException**
+
+```csharp
+public Exception InnerException { get; }
+```
+
+#### Property Value
+
+[Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception)
+
+### **Message**
+
+```csharp
+public string Message { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **Source**
+
+```csharp
+public string Source { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **StackTrace**
+
+```csharp
+public string StackTrace { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **TargetSite**
+
+```csharp
+public MethodBase TargetSite { get; }
+```
+
+#### Property Value
+
+[MethodBase](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md
new file mode 100644
index 00000000..2a70f9a8
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md
@@ -0,0 +1,94 @@
+# DsHidMiniInteropReplyTimeoutException
+
+Namespace: Nefarius.DsHidMini.IPC.Exceptions
+
+Operation timed out while waiting for a request reply.
+
+```csharp
+public sealed class DsHidMiniInteropReplyTimeoutException : System.Exception, System.Runtime.Serialization.ISerializable
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception) → [DsHidMiniInteropReplyTimeoutException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropreplytimeoutexception.md)
+Implements [ISerializable](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.iserializable)
+
+## Properties
+
+### **Data**
+
+```csharp
+public IDictionary Data { get; }
+```
+
+#### Property Value
+
+[IDictionary](https://docs.microsoft.com/en-us/dotnet/api/system.collections.idictionary)
+
+### **HelpLink**
+
+```csharp
+public string HelpLink { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **HResult**
+
+```csharp
+public int HResult { get; set; }
+```
+
+#### Property Value
+
+[Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+
+### **InnerException**
+
+```csharp
+public Exception InnerException { get; }
+```
+
+#### Property Value
+
+[Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception)
+
+### **Message**
+
+```csharp
+public string Message { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **Source**
+
+```csharp
+public string Source { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **StackTrace**
+
+```csharp
+public string StackTrace { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **TargetSite**
+
+```csharp
+public MethodBase TargetSite { get; }
+```
+
+#### Property Value
+
+[MethodBase](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md
new file mode 100644
index 00000000..5c46d8f7
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md
@@ -0,0 +1,94 @@
+# DsHidMiniInteropUnavailableException
+
+Namespace: Nefarius.DsHidMini.IPC.Exceptions
+
+Driver IPC unavailable, make sure that at least one compatible controller is connected and operational.
+
+```csharp
+public sealed class DsHidMiniInteropUnavailableException : System.Exception, System.Runtime.Serialization.ISerializable
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception) → [DsHidMiniInteropUnavailableException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunavailableexception.md)
+Implements [ISerializable](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.iserializable)
+
+## Properties
+
+### **Data**
+
+```csharp
+public IDictionary Data { get; }
+```
+
+#### Property Value
+
+[IDictionary](https://docs.microsoft.com/en-us/dotnet/api/system.collections.idictionary)
+
+### **HelpLink**
+
+```csharp
+public string HelpLink { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **HResult**
+
+```csharp
+public int HResult { get; set; }
+```
+
+#### Property Value
+
+[Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+
+### **InnerException**
+
+```csharp
+public Exception InnerException { get; }
+```
+
+#### Property Value
+
+[Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception)
+
+### **Message**
+
+```csharp
+public string Message { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **Source**
+
+```csharp
+public string Source { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **StackTrace**
+
+```csharp
+public string StackTrace { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **TargetSite**
+
+```csharp
+public MethodBase TargetSite { get; }
+```
+
+#### Property Value
+
+[MethodBase](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md
new file mode 100644
index 00000000..f40108e0
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md
@@ -0,0 +1,94 @@
+# DsHidMiniInteropUnexpectedReplyException
+
+Namespace: Nefarius.DsHidMini.IPC.Exceptions
+
+A request reply was malformed.
+
+```csharp
+public sealed class DsHidMiniInteropUnexpectedReplyException : System.Exception, System.Runtime.Serialization.ISerializable
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception) → [DsHidMiniInteropUnexpectedReplyException](./nefarius.dshidmini.ipc.exceptions.dshidminiinteropunexpectedreplyexception.md)
+Implements [ISerializable](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.iserializable)
+
+## Properties
+
+### **Data**
+
+```csharp
+public IDictionary Data { get; }
+```
+
+#### Property Value
+
+[IDictionary](https://docs.microsoft.com/en-us/dotnet/api/system.collections.idictionary)
+
+### **HelpLink**
+
+```csharp
+public string HelpLink { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **HResult**
+
+```csharp
+public int HResult { get; set; }
+```
+
+#### Property Value
+
+[Int32](https://docs.microsoft.com/en-us/dotnet/api/system.int32)
+
+### **InnerException**
+
+```csharp
+public Exception InnerException { get; }
+```
+
+#### Property Value
+
+[Exception](https://docs.microsoft.com/en-us/dotnet/api/system.exception)
+
+### **Message**
+
+```csharp
+public string Message { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **Source**
+
+```csharp
+public string Source { get; set; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **StackTrace**
+
+```csharp
+public string StackTrace { get; }
+```
+
+#### Property Value
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+
+### **TargetSite**
+
+```csharp
+public MethodBase TargetSite { get; }
+```
+
+#### Property Value
+
+[MethodBase](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodbase)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md
new file mode 100644
index 00000000..9fbafa1e
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md
@@ -0,0 +1,25 @@
+# DsBatteryStatus
+
+Namespace: Nefarius.DsHidMini.IPC.Models.Drivers
+
+Battery status values.
+
+```csharp
+public enum DsBatteryStatus
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [ValueType](https://docs.microsoft.com/en-us/dotnet/api/system.valuetype) → [Enum](https://docs.microsoft.com/en-us/dotnet/api/system.enum) → [DsBatteryStatus](./nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md)
+Implements [IComparable](https://docs.microsoft.com/en-us/dotnet/api/system.icomparable), [ISpanFormattable](https://docs.microsoft.com/en-us/dotnet/api/system.ispanformattable), [IFormattable](https://docs.microsoft.com/en-us/dotnet/api/system.iformattable), [IConvertible](https://docs.microsoft.com/en-us/dotnet/api/system.iconvertible)
+
+## Fields
+
+| Name | Value | Description |
+| --- | --: | --- |
+| Unknown | 0 | Unknown/not yet reported. |
+| Dying | 1 | Dying. Battery is so low the device is barely being kept on. |
+| Low | 2 | Low. Device should be charged soon. |
+| Medium | 3 | Medium. Will last for a while but should be charged soon. |
+| High | 4 | High. Will last for a few sessions. |
+| Full | 5 | Full. Status right after successful charging. |
+| Charging | 238 | Charging. The default state while wired until [DsBatteryStatus.Charged](./nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md#charged) is reached. |
+| Charged | 239 | Charged. While wired synonymous to [DsBatteryStatus.Full](./nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md#full) while wireless. |
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dshiddevicemode.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dshiddevicemode.md
new file mode 100644
index 00000000..75c178e7
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dshiddevicemode.md
@@ -0,0 +1,22 @@
+# DsHidDeviceMode
+
+Namespace: Nefarius.DsHidMini.IPC.Models.Drivers
+
+HID device emulation modes.
+
+```csharp
+public enum DsHidDeviceMode
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [ValueType](https://docs.microsoft.com/en-us/dotnet/api/system.valuetype) → [Enum](https://docs.microsoft.com/en-us/dotnet/api/system.enum) → [DsHidDeviceMode](./nefarius.dshidmini.ipc.models.drivers.dshiddevicemode.md)
+Implements [IComparable](https://docs.microsoft.com/en-us/dotnet/api/system.icomparable), [ISpanFormattable](https://docs.microsoft.com/en-us/dotnet/api/system.ispanformattable), [IFormattable](https://docs.microsoft.com/en-us/dotnet/api/system.iformattable), [IConvertible](https://docs.microsoft.com/en-us/dotnet/api/system.iconvertible)
+
+## Fields
+
+| Name | Value | Description |
+| --- | --: | --- |
+| SDF | 1 | Single Device with Force Feedback mode. |
+| GPJ | 2 | Gamepad plus Joystick mode. |
+| SXS | 3 | SIXAXIS.SYS mode. |
+| DS4W | 4 | DS4Windows DualShock 4 emulation mode. |
+| XInput | 5 | Xbox One Controller mode. |
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dshidminidriver.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dshidminidriver.md
new file mode 100644
index 00000000..426a7743
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.drivers.dshidminidriver.md
@@ -0,0 +1,105 @@
+# DsHidMiniDriver
+
+Namespace: Nefarius.DsHidMini.IPC.Models.Drivers
+
+Interface and property information about the DsHidMini driver.
+
+```csharp
+public static class DsHidMiniDriver
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [DsHidMiniDriver](./nefarius.dshidmini.ipc.models.drivers.dshidminidriver.md)
+
+## Properties
+
+### **BatteryStatusProperty**
+
+The last reported [DsBatteryStatus](./nefarius.dshidmini.ipc.models.drivers.dsbatterystatus.md) of the device.
+
+```csharp
+public static DevicePropertyKey BatteryStatusProperty { get; }
+```
+
+#### Property Value
+
+DevicePropertyKey
+
+### **BluetoothLastConnectedTimeProperty**
+
+Timestamp of last wireless connection.
+
+```csharp
+public static DevicePropertyKey BluetoothLastConnectedTimeProperty { get; }
+```
+
+#### Property Value
+
+DevicePropertyKey
+
+### **DeviceAddressProperty**
+
+The Bluetooth MAC address of the device itself.
+
+```csharp
+public static DevicePropertyKey DeviceAddressProperty { get; }
+```
+
+#### Property Value
+
+DevicePropertyKey
+
+### **DeviceInterfaceGuid**
+
+Interface GUID common to all devices the DsHidMini driver supports.
+
+```csharp
+public static Guid DeviceInterfaceGuid { get; }
+```
+
+#### Property Value
+
+[Guid](https://docs.microsoft.com/en-us/dotnet/api/system.guid)
+
+### **HidDeviceModeProperty**
+
+The currently active [DsHidDeviceMode](./nefarius.dshidmini.ipc.models.drivers.dshiddevicemode.md).
+
+```csharp
+public static DevicePropertyKey HidDeviceModeProperty { get; }
+```
+
+#### Property Value
+
+DevicePropertyKey
+
+### **HostAddressProperty**
+
+The Bluetooth MAC address the device is currently paired to.
+
+```csharp
+public static DevicePropertyKey HostAddressProperty { get; }
+```
+
+#### Property Value
+
+DevicePropertyKey
+
+### **LastHostRequestStatusProperty**
+
+```csharp
+public static DevicePropertyKey LastHostRequestStatusProperty { get; }
+```
+
+#### Property Value
+
+DevicePropertyKey
+
+### **LastPairingStatusProperty**
+
+```csharp
+public static DevicePropertyKey LastPairingStatusProperty { get; }
+```
+
+#### Property Value
+
+DevicePropertyKey
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.public.ds3rawinputreport.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.public.ds3rawinputreport.md
new file mode 100644
index 00000000..26fa5a09
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.public.ds3rawinputreport.md
@@ -0,0 +1,117 @@
+# Ds3RawInputReport
+
+Namespace: Nefarius.DsHidMini.IPC.Models.Public
+
+Raw input report as it is sent by the SIXAXIS/DualShock 3.
+
+```csharp
+public struct Ds3RawInputReport
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [ValueType](https://docs.microsoft.com/en-us/dotnet/api/system.valuetype) → [Ds3RawInputReport](./nefarius.dshidmini.ipc.models.public.ds3rawinputreport.md)
+
+## Fields
+
+### **AccelerometerX**
+
+```csharp
+public ushort AccelerometerX;
+```
+
+### **AccelerometerY**
+
+```csharp
+public ushort AccelerometerY;
+```
+
+### **AccelerometerZ**
+
+```csharp
+public ushort AccelerometerZ;
+```
+
+### **BatteryStatus**
+
+Battery charge status.
+
+```csharp
+public byte BatteryStatus;
+```
+
+### **Buttons**
+
+The individual controller buttons.
+
+```csharp
+public ButtonUnion Buttons;
+```
+
+### **Gyroscope**
+
+```csharp
+public ushort Gyroscope;
+```
+
+### **LeftThumbX**
+
+Left Thumb X-axis.
+
+```csharp
+public byte LeftThumbX;
+```
+
+**Remarks:**
+
+0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
+
+### **LeftThumbY**
+
+Left Thumb Y-axis.
+
+```csharp
+public byte LeftThumbY;
+```
+
+**Remarks:**
+
+0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
+
+### **Pressure**
+
+Pressure button values.
+
+```csharp
+public PressureUnion Pressure;
+```
+
+### **ReportId**
+
+Report ID (always 1).
+
+```csharp
+public byte ReportId;
+```
+
+### **RightThumbX**
+
+Right Thumb X-axis.
+
+```csharp
+public byte RightThumbX;
+```
+
+**Remarks:**
+
+0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
+
+### **RightThumbY**
+
+Right Thumb Y-axis.
+
+```csharp
+public byte RightThumbY;
+```
+
+**Remarks:**
+
+0x00 = left/bottom, 0x80 = centered, 0xFF = right/top
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.public.sethostresult.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.public.sethostresult.md
new file mode 100644
index 00000000..cc08f090
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.models.public.sethostresult.md
@@ -0,0 +1,39 @@
+# SetHostResult
+
+Namespace: Nefarius.DsHidMini.IPC.Models.Public
+
+```csharp
+public struct SetHostResult
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [ValueType](https://docs.microsoft.com/en-us/dotnet/api/system.valuetype) → [SetHostResult](./nefarius.dshidmini.ipc.models.public.sethostresult.md)
+
+## Fields
+
+### **ReadStatus**
+
+The NTSTATUS value of the address read/query action to verify the new address.
+
+```csharp
+public uint ReadStatus;
+```
+
+### **WriteStatus**
+
+The NTSTATUS value of the "pairing" or address overwrite action.
+
+```csharp
+public uint WriteStatus;
+```
+
+## Methods
+
+### **ToString()**
+
+```csharp
+string ToString()
+```
+
+#### Returns
+
+[String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
diff --git a/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.util.converters.enumdescriptiontypeconverter.md b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.util.converters.enumdescriptiontypeconverter.md
new file mode 100644
index 00000000..15300acd
--- /dev/null
+++ b/SDK/Nefarius.DsHidMini.IPC/docs/nefarius.dshidmini.ipc.util.converters.enumdescriptiontypeconverter.md
@@ -0,0 +1,43 @@
+# EnumDescriptionTypeConverter
+
+Namespace: Nefarius.DsHidMini.IPC.Util.Converters
+
+```csharp
+public class EnumDescriptionTypeConverter : System.ComponentModel.EnumConverter
+```
+
+Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → TypeConverter → EnumConverter → [EnumDescriptionTypeConverter](./nefarius.dshidmini.ipc.util.converters.enumdescriptiontypeconverter.md)
+
+## Constructors
+
+### **EnumDescriptionTypeConverter(Type)**
+
+```csharp
+public EnumDescriptionTypeConverter(Type type)
+```
+
+#### Parameters
+
+`type` [Type](https://docs.microsoft.com/en-us/dotnet/api/system.type)
+
+## Methods
+
+### **ConvertTo(ITypeDescriptorContext, CultureInfo, Object, Type)**
+
+```csharp
+public object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
+```
+
+#### Parameters
+
+`context` ITypeDescriptorContext
+
+`culture` [CultureInfo](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo)
+
+`value` [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object)
+
+`destinationType` [Type](https://docs.microsoft.com/en-us/dotnet/api/system.type)
+
+#### Returns
+
+[Object](https://docs.microsoft.com/en-us/dotnet/api/system.object)
diff --git a/appveyor.yml b/appveyor.yml
index e7bff559..1c990008 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 3.1.{build}.0
+version: 3.2.{build}.0
build_cloud: WIN-LKR467JS4GL
image: Windows
test: off
@@ -13,6 +13,9 @@ platform:
- x86
configuration:
- Release
+branches:
+ only:
+ - master
skip_commits:
files:
- '**/*.md'
@@ -27,6 +30,7 @@ install:
cd "C:\projects\DMF"
git pull > NUL
cd %appveyor_build_folder%
+- cmd: mklink /J "%APPVEYOR_BUILD_FOLDER%\XInputBridge\vcpkg_installed" "C:\tools\build-cache\dshidmini\XInputBridge\vcpkg_installed\%PLATFORM%"
dotnet_csproj:
patch: true
file: '**\*.csproj'
@@ -42,6 +46,7 @@ before_build:
- cmd: vpatch --stamp-version "%APPVEYOR_BUILD_VERSION%" --target-file ".\sys\dshidmini.rc" --resource.file-version --resource.product-version
- cmd: vpatch --stamp-version "%APPVEYOR_BUILD_VERSION%" --target-file ".\XInputBridge\XInputBridge.rc" --resource.file-version --resource.product-version
- cmd: dotnet restore .\ControlApp\
+- cmd: dotnet restore .\ipctest\
build_script:
- cmd: .\build.cmd
- cmd: if %PLATFORM%==x64 dotnet publish /p:PublishProfile=Properties\PublishProfiles\release-win-x64.pubxml .\ControlApp\
diff --git a/build/Build.cs b/build/Build.cs
index 6579b4a4..007af8a4 100644
--- a/build/Build.cs
+++ b/build/Build.cs
@@ -76,23 +76,6 @@ class Build : NukeBuild
.SetMaxCpuCount(Environment.ProcessorCount)
.SetNodeReuse(IsLocalBuild)
);
-
- bool enableTelemetry =
- bool.TryParse(Environment.GetEnvironmentVariable("SCPLIB_ENABLE_TELEMETRY"), out bool isSet) && isSet;
-
- if (enableTelemetry)
- {
- // OTEL build
- MSBuildTasks.MSBuild(s => s
- .SetTargetPath(Solution)
- .SetTargets("Rebuild")
- .SetConfiguration("Release_OTEL")
- .SetMaxCpuCount(Environment.ProcessorCount)
- .SetNodeReuse(IsLocalBuild)
- // enables building with OTEL in XInputBridge
- .SetProperty("SCPLIB_ENABLE_TELEMETRY", true)
- );
- }
});
/// Support plugins are available for:
diff --git a/build/_build.csproj b/build/_build.csproj
index 1810d5f5..75492e73 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -15,7 +15,8 @@
-
+
+
diff --git a/dshidmini.sln b/dshidmini.sln
index eb8172f8..97b9bab7 100644
--- a/dshidmini.sln
+++ b/dshidmini.sln
@@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.gitattributes = .gitattributes
.gitignore = .gitignore
appveyor.yml = appveyor.yml
+ Directory.Build.props = Directory.Build.props
global.json = global.json
LICENSE = LICENSE
README.md = README.md
@@ -39,6 +40,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DsHidMini.Installer", "setu
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlApp", "ControlApp\ControlApp.csproj", "{AD47E724-2038-46EA-ACF9-C28B53D39A9A}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ipctest", "ipctest\ipctest.csproj", "{45C7103C-2F57-45AD-84BA-1498BC9F21CC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK", "SDK", "{63280790-A828-4A50-B136-D7A4D0846808}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nefarius.DsHidMini.IPC", "SDK\Nefarius.DsHidMini.IPC\Nefarius.DsHidMini.IPC.csproj", "{52BDD811-0B9A-428D-96B6-496322D7398E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -151,6 +158,38 @@ Global
{AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|x64.Build.0 = Release|x64
{AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|x86.ActiveCfg = Release|Any CPU
{AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|x86.Build.0 = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|x64.Build.0 = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Debug|x86.Build.0 = Debug|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|ARM64.Build.0 = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|x64.ActiveCfg = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|x64.Build.0 = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|x86.ActiveCfg = Release|Any CPU
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC}.Release|x86.Build.0 = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|x64.Build.0 = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Debug|x86.Build.0 = Debug|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|ARM64.Build.0 = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|x64.ActiveCfg = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|x64.Build.0 = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|x86.ActiveCfg = Release|Any CPU
+ {52BDD811-0B9A-428D-96B6-496322D7398E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -162,6 +201,8 @@ Global
{FEB89FC0-BF9B-43F3-8467-B3496D61B76E} = {D5FFDFFE-55A3-4AF3-92A6-13F28598A5EE}
{7D07C49F-A5A8-44AD-83B5-1E5B1F3080AA} = {58E023F1-01BB-4D75-90A5-2E6049F94048}
{AD47E724-2038-46EA-ACF9-C28B53D39A9A} = {CE492389-7FB3-4DC4-9AFF-B7A04F70F891}
+ {45C7103C-2F57-45AD-84BA-1498BC9F21CC} = {CE492389-7FB3-4DC4-9AFF-B7A04F70F891}
+ {52BDD811-0B9A-428D-96B6-496322D7398E} = {63280790-A828-4A50-B136-D7A4D0846808}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5A4A580B-8A5D-4E6A-A75D-ADFDD46F6F37}
diff --git a/dshidmini.sln.DotSettings b/dshidmini.sln.DotSettings
index 15a23627..1daa2e6b 100644
--- a/dshidmini.sln.DotSettings
+++ b/dshidmini.sln.DotSettings
@@ -21,11 +21,13 @@
True
True
True
+ True
True
True
True
True
True
+ True
True
True
True
@@ -38,11 +40,14 @@
True
True
True
+ True
True
True
True
True
+ True
True
+ True
True
True
True
diff --git a/dshidmini_tvp.twx b/dshidmini_tvp.twx
new file mode 100644
index 00000000..a9bd454a
--- /dev/null
+++ b/dshidmini_tvp.twx
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/include/DsHidMini/dshmguid.h b/include/DsHidMini/dshmguid.h
index b883cb52..03f01326 100644
--- a/include/DsHidMini/dshmguid.h
+++ b/include/DsHidMini/dshmguid.h
@@ -31,6 +31,7 @@ DEFINE_DEVPROPKEY(DEVPKEY_DsHidMini_RO_BatteryStatus,
0x3fecf510, 0xcc94, 0x4fbe, 0x88, 0x39, 0x73, 0x82, 0x1, 0xf8, 0x4d, 0x59, 2); // DEVPROP_TYPE_BYTE
// {3FECF510-CC94-4FBE-8839-738201F84D59}
+__declspec(deprecated)
DEFINE_DEVPROPKEY(DEVPKEY_DsHidMini_RO_LastPairingStatus,
0x3fecf510, 0xcc94, 0x4fbe, 0x88, 0x39, 0x73, 0x82, 0x1, 0xf8, 0x4d, 0x59, 3); // DEVPROP_TYPE_NTSTATUS
diff --git a/ipctest/Program.cs b/ipctest/Program.cs
new file mode 100644
index 00000000..ba671d96
--- /dev/null
+++ b/ipctest/Program.cs
@@ -0,0 +1,56 @@
+// See https://aka.ms/new-console-template for more information
+
+using System.Diagnostics;
+
+using Nefarius.DsHidMini.IPC;
+#if INPUT_TEST
+using Nefarius.DsHidMini.IPC.Models.Public;
+#endif
+
+if (!DsHidMiniInterop.IsAvailable)
+{
+ Console.WriteLine("DsHidMini IPC not available, make sure that at least one controller is connected");
+ return;
+}
+
+using DsHidMiniInterop ipc = new();
+
+#if INPUT_TEST
+DS3_RAW_INPUT_REPORT report = new();
+#endif
+
+Stopwatch stopwatch = new();
+
+do
+{
+ while (!Console.KeyAvailable)
+ {
+ int executionCount = 0;
+
+ stopwatch.Restart();
+
+ while (stopwatch.ElapsedMilliseconds < 1000)
+ {
+#if INPUT_TEST
+ bool success = ipc.GetRawInputReport(1, ref report, TimeSpan.FromMilliseconds(50));
+
+ if (success && report.Buttons.Cross)
+ {
+ Console.WriteLine("Cross pressed");
+ }
+#else
+ ipc.SendPing();
+#endif
+
+ executionCount++;
+ }
+
+ stopwatch.Stop();
+
+#if INPUT_TEST
+ Console.WriteLine($"Read {executionCount} input reports in one second.");
+#else
+ Console.WriteLine($"Executed {executionCount} PINGs in one second.");
+#endif
+ }
+} while (Console.ReadKey(true).Key != ConsoleKey.Escape);
\ No newline at end of file
diff --git a/ipctest/ipctest.csproj b/ipctest/ipctest.csproj
new file mode 100644
index 00000000..0697d258
--- /dev/null
+++ b/ipctest/ipctest.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0-windows
+ enable
+ enable
+ Nefarius.DsHidMini.IPC
+ true
+ false
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 00000000..c3a4ad79
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sys/Device.c b/sys/Device.c
index 9529a318..cf28f5ee 100644
--- a/sys/Device.c
+++ b/sys/Device.c
@@ -9,7 +9,6 @@ EVT_DMF_DEVICE_MODULES_ADD DmfDeviceModulesAdd;
EVT_WDF_DEVICE_CONTEXT_CLEANUP DsHidMini_DeviceCleanup;
#pragma code_seg("PAGED")
-DMF_DEFAULT_DRIVERCLEANUP(dshidminiEvtDriverContextCleanup)
//
// Bootstrap device
@@ -216,6 +215,25 @@ void DsHidMini_DeviceCleanup(
PAGED_CODE();
+ const WDFDEVICE device = Object;
+ const PDEVICE_CONTEXT deviceContext = DeviceGetContext(device);
+ const WDFDRIVER driver = WdfGetDriver();
+ const PDSHM_DRIVER_CONTEXT driverContext = DriverGetContext(driver);
+
+ WdfWaitLockAcquire(driverContext->SlotsLock, NULL);
+ {
+ CLEAR_SLOT(driverContext, deviceContext->SlotIndex);
+ driverContext->IPC.DeviceDispatchers.Callbacks[deviceContext->SlotIndex] = NULL;
+ driverContext->IPC.DeviceDispatchers.Contexts[deviceContext->SlotIndex] = NULL;
+ }
+ WdfWaitLockRelease(driverContext->SlotsLock);
+
+ const size_t offset = (sizeof(IPC_HID_INPUT_REPORT_MESSAGE) * (deviceContext->SlotIndex - 1));
+ const PUCHAR pHIDBuffer = (driverContext->IPC.SharedRegions.HID.Buffer + offset);
+
+ // zero out the slot so potential readers get notified we're gone
+ RtlZeroMemory(pHIDBuffer, sizeof(IPC_HID_INPUT_REPORT_MESSAGE));
+
EventWriteUnloadEvent(Object);
FuncExitNoReturn(TRACE_DEVICE);
@@ -405,13 +423,50 @@ DsDevice_InitContext(
)
{
const PDEVICE_CONTEXT pDevCtx = DeviceGetContext(Device);
- NTSTATUS status = STATUS_SUCCESS;
+ const PDSHM_DRIVER_CONTEXT pDrvCtx = DriverGetContext(WdfGetDriver());
+ NTSTATUS status = STATUS_INSUFFICIENT_RESOURCES;
WDF_OBJECT_ATTRIBUTES attributes;
PUCHAR outReportBuffer = NULL;
WDF_TIMER_CONFIG timerCfg;
FuncEntry(TRACE_DEVICE);
+ WdfWaitLockAcquire(pDrvCtx->SlotsLock, NULL);
+ {
+ //
+ // Get next free slot
+ //
+ for (UINT32 slotIndex = 1; slotIndex <= DSHM_MAX_DEVICES; slotIndex++)
+ {
+ if (!TEST_SLOT(pDrvCtx, slotIndex))
+ {
+ SET_SLOT(pDrvCtx, slotIndex);
+ status = STATUS_SUCCESS;
+
+ TraceVerbose(
+ TRACE_DEVICE,
+ "Claimed device slot: %d",
+ slotIndex
+ );
+
+ pDevCtx->SlotIndex = slotIndex;
+ pDrvCtx->IPC.DeviceDispatchers.Callbacks[slotIndex] = DSHM_EvtDispatchDeviceMessage;
+ pDrvCtx->IPC.DeviceDispatchers.Contexts[slotIndex] = pDevCtx;
+ break;
+ }
+ }
+ }
+ WdfWaitLockRelease(pDrvCtx->SlotsLock);
+
+ if (!NT_SUCCESS(status))
+ {
+ FuncExit(TRACE_DEVICE, "status=%!STATUS!", status);
+
+ return status;
+ }
+
+ // ReSharper disable once CppIncompleteSwitchStatement
+ // ReSharper disable once CppDefaultCaseNotHandledInSwitchStatement
switch (pDevCtx->ConnectionType)
{
case DsDeviceConnectionTypeUsb:
@@ -645,6 +700,63 @@ DsDevice_InitContext(
break;
}
+#pragma region IPC
+
+ SECURITY_DESCRIPTOR sd = { 0 };
+
+ if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
+ {
+ TraceError(
+ TRACE_IPC,
+ "InitializeSecurityDescriptor failed with error: %!WINERROR!",
+ GetLastError()
+ );
+ EventWriteFailedWithWin32Error(__FUNCTION__, L"InitializeSecurityDescriptor", GetLastError());
+ break;
+ }
+
+ SECURITY_ATTRIBUTES sa = { 0 };
+ sa.nLength = sizeof(sa);
+ sa.bInheritHandle = TRUE;
+ sa.lpSecurityDescriptor = &sd;
+
+ CHAR* szSD = "D:" // Discretionary ACL
+ "(D;OICI;GA;;;BG)" // Deny access to Built-in Guests
+ "(D;OICI;GA;;;AN)" // Deny access to Anonymous Logon
+ "(A;OICI;GRGWGX;;;AU)" // Allow read/write/execute to Authenticated Users
+ "(A;OICI;GA;;;BA)"; // Allow full control to Administrators
+
+ if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(
+ szSD,
+ SDDL_REVISION_1,
+ &sa.lpSecurityDescriptor,
+ NULL
+ ))
+ {
+ TraceError(
+ TRACE_IPC,
+ "ConvertStringSecurityDescriptorToSecurityDescriptor failed with error: %!WINERROR!",
+ GetLastError()
+ );
+ EventWriteFailedWithWin32Error(__FUNCTION__, L"ConvertStringSecurityDescriptorToSecurityDescriptor", GetLastError());
+ break;
+ }
+
+ pDevCtx->IPC.InputReportWaitHandle = CreateEventA(&sa, FALSE, FALSE, NULL);
+
+ if (pDevCtx->IPC.InputReportWaitHandle == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "CreateEventA failed with error: %!WINERROR!",
+ GetLastError()
+ );
+ EventWriteFailedWithWin32Error(__FUNCTION__, L"CreateEventA", GetLastError());
+ break;
+ }
+
+#pragma endregion
+
} while (FALSE);
FuncExit(TRACE_DEVICE, "status=%!STATUS!", status);
diff --git a/sys/Device.h b/sys/Device.h
index f45b4c4e..4e14aea4 100644
--- a/sys/Device.h
+++ b/sys/Device.h
@@ -357,8 +357,33 @@ typedef struct _DEVICE_CONTEXT
} RumbleControlState;
+ UINT32 SlotIndex;
+
+ struct
+ {
+ HANDLE InputReportWaitHandle;
+ } IPC;
+
} DEVICE_CONTEXT, * PDEVICE_CONTEXT;
+#include
+//
+// Describes a raw input report packet shared via IPC
+//
+typedef struct _IPC_HID_INPUT_REPORT_MESSAGE
+{
+ //
+ // One-based device index
+ //
+ UINT32 SlotIndex;
+
+ //
+ // Input report copy
+ //
+ DS3_RAW_INPUT_REPORT InputReport;
+} IPC_HID_INPUT_REPORT_MESSAGE, *PIPC_HID_INPUT_REPORT_MESSAGE;
+#include
+
//
// This macro will generate an inline function called DeviceGetContext
// which will be used to get a pointer to the device context memory
@@ -416,6 +441,8 @@ EVT_WDF_TIMER DSHM_OutputReportDelayTimerElapsed;
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL DSHM_EvtWdfIoQueueIoDeviceControl;
+EVT_DSHM_IPC_DispatchDeviceMessage DSHM_EvtDispatchDeviceMessage;
+
NTSTATUS
DsDevice_ReadProperties(
WDFDEVICE Device
diff --git a/sys/Driver.c b/sys/Driver.c
index a946b139..0b984ad4 100644
--- a/sys/Driver.c
+++ b/sys/Driver.c
@@ -2,12 +2,83 @@
#include "Driver.tmh"
-/*WPP_INIT_TRACING(); (This comment is necessary for WPP Scanner.)*/
#pragma code_seg("INIT")
-DMF_DEFAULT_DRIVERENTRY(DriverEntry,
- dshidminiEvtDriverContextCleanup,
- dshidminiEvtDeviceAdd,
- L"DsHidMini")
+NTSTATUS
+DriverEntry(
+ _In_ PDRIVER_OBJECT DriverObject,
+ _In_ PUNICODE_STRING RegistryPath
+)
+{
+ WDF_DRIVER_CONFIG config;
+ WDF_OBJECT_ATTRIBUTES attributes;
+
+ //
+ // Initialize WPP Tracing
+ //
+ WPP_INIT_TRACING(DriverObject, RegistryPath);
+
+ TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
+
+ //
+ // Register a cleanup callback so that we can call WPP_CLEANUP when
+ // the framework driver object is deleted during driver unload.
+ //
+ WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, DSHM_DRIVER_CONTEXT);
+ attributes.EvtCleanupCallback = dshidminiEvtDriverContextCleanup;
+
+ WDF_DRIVER_CONFIG_INIT(
+ &config,
+ dshidminiEvtDeviceAdd
+ );
+
+ WDFDRIVER driver = NULL;
+
+ NTSTATUS status = WdfDriverCreate(
+ DriverObject,
+ RegistryPath,
+ &attributes,
+ &config,
+ &driver
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "WdfDriverCreate failed with status %!STATUS!", status);
+ WPP_CLEANUP(DriverObject);
+ return status;
+ }
+
+ if (!NT_SUCCESS(status = InitIPC()))
+ {
+ TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "InitIPC failed with status %!STATUS!", status);
+ WPP_CLEANUP(DriverObject);
+ return status;
+ }
+
+ const PDSHM_DRIVER_CONTEXT context = DriverGetContext(driver);
+
+ if (!NT_SUCCESS(status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &context->SlotsLock)))
+ {
+ TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "WdfWaitLockCreate failed with status %!STATUS!", status);
+ WPP_CLEANUP(DriverObject);
+ return status;
+ }
+
+ TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
+
+ return status;
+}
+#pragma code_seg()
+
+#pragma code_seg("PAGED")
+void dshidminiEvtDriverContextCleanup(WDFOBJECT DriverObject)
+{
+ UNREFERENCED_PARAMETER(DriverObject);
+
+ DestroyIPC();
+
+ WPP_CLEANUP(WdfDriverWdmGetDriverObject( (WDFDRIVER) DriverObject));
+}
#pragma code_seg()
//
diff --git a/sys/Driver.h b/sys/Driver.h
index 14e6dd96..08d9a0ca 100644
--- a/sys/Driver.h
+++ b/sys/Driver.h
@@ -23,7 +23,14 @@
#ifdef DSHM_FEATURE_FFB
#include "PID/PIDTypes.h"
#endif
+
+//
+// Artificial limit to make memory management easier (should be enough ;)
+//
+#define DSHM_MAX_DEVICES UCHAR_MAX
+
#include "Configuration.h"
+#include "IPC.h"
#include "Device.h"
#include
@@ -40,6 +47,140 @@
EXTERN_C_START
+typedef struct _DSHM_DRIVER_CONTEXT
+{
+ //
+ // IPC-specific fields
+ //
+ struct
+ {
+ //
+ // Handle of the memory-mapped file
+ //
+ HANDLE MapFile;
+
+ //
+ // Mutex handle to sync a connected client application
+ //
+ HANDLE ConnectMutex;
+
+ //
+ // Read ready event handle
+ //
+ HANDLE ReadEvent;
+
+ //
+ // Write ready event handle
+ //
+ HANDLE WriteEvent;
+
+ //
+ // Dispatch thread handle
+ //
+ HANDLE DispatchThread;
+
+ //
+ // Thread termination event
+ //
+ HANDLE DispatchThreadTermination;
+
+ //
+ // Shared memory regions details
+ //
+ struct
+ {
+ //
+ // Low frequency, low lifetime command exchange region
+ //
+ struct
+ {
+ //
+ // Pointer to shared memory buffer
+ //
+ PUCHAR Buffer;
+
+ //
+ // Total size of shared memory region
+ //
+ size_t BufferSize;
+ } Commands;
+
+ //
+ // High-frequency, high throughput read-only region
+ struct
+ {
+ //
+ // Pointer to shared memory buffer
+ //
+ PUCHAR Buffer;
+
+ //
+ // Total size of shared memory region
+ //
+ size_t BufferSize;
+ } HID;
+ } SharedRegions;
+
+ //
+ // Message dispatcher callback handlers
+ //
+ struct
+ {
+ PFN_DSHM_IPC_DispatchDeviceMessage Callbacks[DSHM_MAX_DEVICES];
+
+ PDEVICE_CONTEXT Contexts[DSHM_MAX_DEVICES];
+ } DeviceDispatchers;
+ } IPC;
+
+ //
+ // Index slots to associate connected devices in IPC
+ //
+ LONG Slots[8]; // 256 usable bits
+
+ //
+ // Lock protecting access to Slots
+ //
+ WDFWAITLOCK SlotsLock;
+} DSHM_DRIVER_CONTEXT, * PDSHM_DRIVER_CONTEXT;
+
+WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DSHM_DRIVER_CONTEXT, DriverGetContext)
+
+LONG
+FORCEINLINE
+SET_SLOT(
+ PDSHM_DRIVER_CONTEXT Context,
+ UINT32 SlotIndex
+)
+{
+ const UINT32 bits = sizeof(Context->Slots);
+
+ return InterlockedOr(&Context->Slots[SlotIndex / bits], 1 << (SlotIndex % bits));
+}
+
+BOOLEAN
+FORCEINLINE
+TEST_SLOT(
+ PDSHM_DRIVER_CONTEXT Context,
+ UINT32 SlotIndex
+)
+{
+ const UINT32 bits = sizeof(Context->Slots);
+
+ return (BOOLEAN)(Context->Slots[SlotIndex / bits] & (1 << (SlotIndex % bits)));
+}
+
+LONG
+FORCEINLINE
+CLEAR_SLOT(
+ PDSHM_DRIVER_CONTEXT Context,
+ UINT32 SlotIndex
+)
+{
+ const UINT32 bits = sizeof(Context->Slots);
+
+ return InterlockedAnd(&Context->Slots[SlotIndex / bits], ~(1 << (SlotIndex % bits)));
+}
+
//
// WDFDRIVER Events
//
diff --git a/sys/Ds3.c b/sys/Ds3.c
index f5e5094d..320770fd 100644
--- a/sys/Ds3.c
+++ b/sys/Ds3.c
@@ -411,7 +411,6 @@ NTSTATUS DS3_GetActiveRadioAddress(BD_ADDR* Address)
NTSTATUS DsUsb_Ds3PairToNewHost(WDFDEVICE Device)
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
- WDF_DEVICE_PROPERTY_DATA propertyData;
const PDEVICE_CONTEXT pDevCtx = DeviceGetContext(Device);
BD_ADDR newHostAddress = { 0 };
@@ -500,28 +499,6 @@ NTSTATUS DsUsb_Ds3PairToNewHost(WDFDEVICE Device)
} while (FALSE);
- WDF_DEVICE_PROPERTY_DATA_INIT(&propertyData, &DEVPKEY_DsHidMini_RO_LastPairingStatus);
- propertyData.Flags |= PLUGPLAY_PROPERTY_PERSISTENT;
- propertyData.Lcid = LOCALE_NEUTRAL;
-
- //
- // Store in property
- //
- if (!NT_SUCCESS(status = WdfDeviceAssignProperty(
- Device,
- &propertyData,
- DEVPROP_TYPE_NTSTATUS,
- sizeof(NTSTATUS),
- &status
- )))
- {
- TraceError(
- TRACE_DS3,
- "Setting DEVPKEY_DsHidMini_RO_LastPairingStatus failed with status %!STATUS!",
- status
- );
- }
-
FuncExit(TRACE_DS3, "status=%!STATUS!", status);
return status;
diff --git a/sys/IPC.Device.c b/sys/IPC.Device.c
new file mode 100644
index 00000000..5e82b346
--- /dev/null
+++ b/sys/IPC.Device.c
@@ -0,0 +1,87 @@
+#include "Driver.h"
+#include "IPC.Device.tmh"
+
+
+//
+// Processes incoming IPC messages targeted to this device instance
+//
+_Use_decl_annotations_
+NTSTATUS
+DSHM_EvtDispatchDeviceMessage(
+ _In_ PDEVICE_CONTEXT DeviceContext,
+ _In_ PDSHM_IPC_MSG_HEADER MessageHeader
+)
+{
+ FuncEntry(TRACE_IPC);
+
+ NTSTATUS status = STATUS_NOT_IMPLEMENTED;
+
+ if (MessageHeader->Command.Device == DSHM_IPC_MSG_CMD_DEVICE_PAIR_TO)
+ {
+ const PDSHM_IPC_MSG_PAIR_TO_REQUEST request = (PDSHM_IPC_MSG_PAIR_TO_REQUEST)MessageHeader;
+
+ TraceVerbose(
+ TRACE_IPC,
+ "Received pairing request, new host address: %02X:%02X:%02X:%02X:%02X:%02X",
+ request->Address.Address[0],
+ request->Address.Address[1],
+ request->Address.Address[2],
+ request->Address.Address[3],
+ request->Address.Address[4],
+ request->Address.Address[5]
+ );
+
+ // TODO: this changes state for runtime settings, improve
+ DeviceContext->Configuration.DevicePairingMode = DsDevicePairingModeCustom;
+ RtlCopyMemory(&DeviceContext->Configuration.CustomHostAddress, &request->Address, sizeof(BD_ADDR));
+
+ const WDFDEVICE device = WdfObjectContextGetObject(DeviceContext);
+
+ NTSTATUS writeStatus = DsUsb_Ds3PairToNewHost(device);
+ NTSTATUS readStatus = STATUS_UNSUCCESSFUL;
+ if (!NT_SUCCESS(readStatus = DsUsb_Ds3RequestHostAddress(device)))
+ {
+ TraceError(
+ TRACE_IPC,
+ "DsUsb_Ds3RequestHostAddress failed with status %!STATUS!",
+ readStatus
+ );
+ }
+
+ DSHM_IPC_MSG_PAIR_TO_RESPONSE_INIT(
+ (PDSHM_IPC_MSG_PAIR_TO_REPLY)MessageHeader,
+ MessageHeader->TargetIndex,
+ writeStatus,
+ readStatus
+ );
+
+ status = STATUS_SUCCESS;
+ }
+ else if (MessageHeader->Command.Device == DSHM_IPC_MSG_CMD_DEVICE_SET_PLAYER_INDEX)
+ {
+ // TODO: implement me!
+
+ DSHM_IPC_MSG_SET_PLAYER_INDEX_RESPONSE_INIT(
+ (PDSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY)MessageHeader,
+ MessageHeader->TargetIndex,
+ STATUS_NOT_IMPLEMENTED
+ );
+
+ status = STATUS_SUCCESS;
+ }
+ else if (MessageHeader->Command.Device == DSHM_IPC_MSG_CMD_DEVICE_GET_HID_WAIT_HANDLE)
+ {
+ DSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE_INIT(
+ (PDSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE)MessageHeader,
+ MessageHeader->TargetIndex,
+ GetCurrentProcessId(),
+ DeviceContext->IPC.InputReportWaitHandle
+ );
+
+ status = STATUS_SUCCESS;
+ }
+
+ FuncExit(TRACE_IPC, "status=%!STATUS!", status);
+
+ return status;
+}
diff --git a/sys/IPC.c b/sys/IPC.c
new file mode 100644
index 00000000..e51d9199
--- /dev/null
+++ b/sys/IPC.c
@@ -0,0 +1,456 @@
+#include "Driver.h"
+#include "IPC.tmh"
+
+
+static DWORD WINAPI DSHM_IPC_ClientDispatchProc(
+ _In_ LPVOID lpParameter
+);
+
+NTSTATUS InitIPC(void)
+{
+ FuncEntry(TRACE_IPC);
+
+ const WDFDRIVER driver = WdfGetDriver();
+ const PDSHM_DRIVER_CONTEXT context = DriverGetContext(driver);
+
+ SYSTEM_INFO sysInfo;
+ GetSystemInfo(&sysInfo);
+ DWORD pageSize = sysInfo.dwAllocationGranularity; // Usually 4096 bytes (4KB)
+
+ DWORD cmdRegionSize = pageSize;
+ DWORD hidRegionSize = pageSize;
+ DWORD totalRegionSize = cmdRegionSize + hidRegionSize;
+
+ TraceVerbose(
+ TRACE_IPC,
+ "pageSize = %d, cmdRegionSize = %d, hidRegionSize = %d, totalRegionSize = %d",
+ pageSize, cmdRegionSize, hidRegionSize, totalRegionSize
+ );
+
+ PUCHAR pCmdBuf = NULL;
+ PUCHAR pHIDBuf = NULL;
+ HANDLE hReadEvent = NULL;
+ HANDLE hWriteEvent = NULL;
+ HANDLE hMapFile = NULL;
+ HANDLE hMutex = NULL;
+ HANDLE hThread = NULL;
+ HANDLE hThreadTermination = NULL;
+
+ SECURITY_DESCRIPTOR sd = { 0 };
+
+ if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
+ {
+ TraceError(
+ TRACE_IPC,
+ "InitializeSecurityDescriptor failed with error: %!WINERROR!",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ SECURITY_ATTRIBUTES sa = { 0 };
+ sa.nLength = sizeof(sa);
+ sa.bInheritHandle = TRUE;
+ sa.lpSecurityDescriptor = &sd;
+
+ CHAR* szSD = "D:" // Discretionary ACL
+ "(D;OICI;GA;;;BG)" // Deny access to Built-in Guests
+ "(D;OICI;GA;;;AN)" // Deny access to Anonymous Logon
+ "(A;OICI;GRGWGX;;;AU)" // Allow read/write/execute to Authenticated Users
+ "(A;OICI;GA;;;BA)"; // Allow full control to Administrators
+
+ if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(
+ szSD,
+ SDDL_REVISION_1,
+ &sa.lpSecurityDescriptor,
+ NULL
+ ))
+ {
+ TraceError(
+ TRACE_IPC,
+ "ConvertStringSecurityDescriptorToSecurityDescriptor failed with error: %!WINERROR!",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ hMutex = CreateMutexA(&sa, FALSE, DSHM_IPC_MUTEX_NAME);
+ if (hMutex == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not create mutex (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ // Create a named event for signaling
+ hReadEvent = CreateEventA(&sa, FALSE, FALSE, DSHM_IPC_READ_EVENT_NAME);
+ if (hReadEvent == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not create READ event (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ hWriteEvent = CreateEventA(&sa, FALSE, FALSE, DSHM_IPC_WRITE_EVENT_NAME);
+ if (hWriteEvent == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not create WRITE event (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ hThreadTermination = CreateEventA(&sa, FALSE, FALSE, NULL);
+ if (hThreadTermination == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not create event (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ // Create a memory-mapped file
+ hMapFile = CreateFileMappingA(
+ INVALID_HANDLE_VALUE, // use paging file
+ &sa, // custom security
+ PAGE_READWRITE, // read/write access
+ 0, // maximum object size (high-order DWORD)
+ totalRegionSize, // maximum object size (low-order DWORD)
+ DSHM_IPC_FILE_MAP_NAME // name of mapping object
+ );
+
+ if (hMapFile == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not create file mapping object (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ // Map a view of the file in the calling process's address space
+ pCmdBuf = MapViewOfFile(
+ hMapFile, // handle to map object
+ FILE_MAP_ALL_ACCESS, // read/write permission
+ 0,
+ 0,
+ cmdRegionSize
+ );
+
+ if (pCmdBuf == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not map view of file CMD REGION (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ // Calculate the nearest page-aligned offset for the HID region
+ DWORD alignedOffset = (cmdRegionSize / pageSize) * pageSize; // Page-aligned offset
+ DWORD offsetWithinPage = cmdRegionSize % pageSize; // Offset within the mapped page
+
+ pHIDBuf = MapViewOfFile(
+ hMapFile, // handle to map object
+ FILE_MAP_ALL_ACCESS, // read/write permission
+ 0,
+ alignedOffset,
+ hidRegionSize + offsetWithinPage
+ );
+
+ if (pHIDBuf == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not map view of file HID REGION (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ context->IPC.DispatchThreadTermination = hThreadTermination;
+ context->IPC.MapFile = hMapFile;
+ context->IPC.ConnectMutex = hMutex;
+ context->IPC.ReadEvent = hReadEvent;
+ context->IPC.WriteEvent = hWriteEvent;
+
+ context->IPC.SharedRegions.Commands.Buffer = pCmdBuf;
+ context->IPC.SharedRegions.Commands.BufferSize = cmdRegionSize;
+
+ context->IPC.SharedRegions.HID.Buffer = pHIDBuf;
+ context->IPC.SharedRegions.HID.BufferSize = hidRegionSize;
+
+ //
+ // Start thread now that context is initialized at its minimum requirement
+ //
+ hThread = CreateThread(
+ NULL,
+ 0,
+ DSHM_IPC_ClientDispatchProc,
+ context,
+ 0,
+ NULL
+ );
+
+ if (hThread == NULL)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Could not create dispatch thread (%!WINERROR!).",
+ GetLastError()
+ );
+ goto exitFailure;
+ }
+
+ context->IPC.DispatchThread = hThread;
+
+ FuncExitNoReturn(TRACE_IPC);
+
+ return STATUS_SUCCESS;
+
+exitFailure:
+ if (pCmdBuf)
+ UnmapViewOfFile(pCmdBuf);
+
+ if (pHIDBuf)
+ UnmapViewOfFile(pHIDBuf);
+
+ if (hReadEvent)
+ CloseHandle(hReadEvent);
+
+ if (hWriteEvent)
+ CloseHandle(hWriteEvent);
+
+ if (hMapFile)
+ CloseHandle(hMapFile);
+
+ if (hMutex)
+ CloseHandle(hMutex);
+
+ if (hThread)
+ CloseHandle(hThread);
+
+ if (hThreadTermination)
+ CloseHandle(hThreadTermination);
+
+ const NTSTATUS status = NTSTATUS_FROM_WIN32(GetLastError());
+
+ FuncExit(TRACE_IPC, "status=%!STATUS!", status);
+
+ return status;
+}
+
+void DestroyIPC(void)
+{
+ FuncEntry(TRACE_IPC);
+
+ const WDFDRIVER driver = WdfGetDriver();
+ const PDSHM_DRIVER_CONTEXT context = DriverGetContext(driver);
+
+ //
+ // Thread running; signal termination, wait on exit, free resources
+ //
+ if (context->IPC.DispatchThread && context->IPC.DispatchThreadTermination)
+ {
+ SetEvent(context->IPC.DispatchThreadTermination);
+ WaitForSingleObject(context->IPC.DispatchThread, 500);
+ CloseHandle(context->IPC.DispatchThread);
+ CloseHandle(context->IPC.DispatchThreadTermination);
+ }
+
+ if (context->IPC.SharedRegions.Commands.Buffer)
+ UnmapViewOfFile(context->IPC.SharedRegions.Commands.Buffer);
+
+ if (context->IPC.SharedRegions.HID.Buffer)
+ UnmapViewOfFile(context->IPC.SharedRegions.HID.Buffer);
+
+ if (context->IPC.MapFile)
+ CloseHandle(context->IPC.MapFile);
+
+ if (context->IPC.ReadEvent)
+ CloseHandle(context->IPC.ReadEvent);
+
+ if (context->IPC.WriteEvent)
+ CloseHandle(context->IPC.WriteEvent);
+
+ if (context->IPC.ConnectMutex)
+ CloseHandle(context->IPC.ConnectMutex);
+
+ FuncExitNoReturn(TRACE_IPC);
+}
+
+static NTSTATUS DSHM_IPC_DispatchIncomingCommandMessage(
+ _In_ const PDSHM_DRIVER_CONTEXT Context,
+ _In_ const PDSHM_IPC_MSG_HEADER Message
+)
+{
+ FuncEntry(TRACE_IPC);
+
+ NTSTATUS status = STATUS_NOT_IMPLEMENTED;
+
+ //
+ // Sanity check
+ //
+ if (Message->Size < sizeof(DSHM_IPC_MSG_HEADER))
+ {
+ return STATUS_INVALID_USER_BUFFER;
+ }
+
+ //
+ // Message outside of region bounds
+ //
+ if (Message->Size > Context->IPC.SharedRegions.Commands.BufferSize)
+ {
+ return STATUS_BUFFER_OVERFLOW;
+ }
+
+ //
+ // Incoming PING message
+ //
+ if (DSHM_IPC_MSG_IS_PING(Message))
+ {
+ TraceVerbose(
+ TRACE_IPC,
+ "IPC: PING message received"
+ );
+
+ const PDSHM_IPC_MSG_HEADER header = (PDSHM_IPC_MSG_HEADER)Context->IPC.SharedRegions.Commands.Buffer;
+
+ DSHM_IPC_MSG_PING_RESPONSE_INIT(header);
+
+ DSHM_IPC_SIGNAL_WRITE_DONE(Context);
+
+ status = STATUS_SUCCESS;
+ }
+
+ //
+ // Message is for a device instance
+ //
+ if (DSHM_IPC_MSG_IS_FOR_DEVICE(Message))
+ {
+ const BOOLEAN expectsReply = DSHM_IPC_MSG_EXPECTS_REPLY(Message);
+ const PDEVICE_CONTEXT deviceContext = Context->IPC.DeviceDispatchers.Contexts[Message->TargetIndex];
+ const PFN_DSHM_IPC_DispatchDeviceMessage callback = Context->IPC.DeviceDispatchers.Callbacks[Message->TargetIndex];
+
+ //
+ // Proxy the message dispatching to the targeted device instance
+ //
+ if (callback && deviceContext)
+ {
+ status = callback(deviceContext, Message);
+ }
+ else
+ {
+ TraceWarning(
+ TRACE_IPC,
+ "Device with index %d has no valid callback or context assigned",
+ Message->TargetIndex
+ );
+ }
+
+ if (expectsReply)
+ {
+ DSHM_IPC_SIGNAL_WRITE_DONE(Context);
+ }
+ }
+
+ FuncExit(TRACE_IPC, "status=%!STATUS!", status);
+
+ return status;
+}
+
+//
+// Listens for client connection and processes data exchange
+//
+static DWORD WINAPI DSHM_IPC_ClientDispatchProc(
+ _In_ LPVOID lpParameter
+)
+{
+ FuncEntry(TRACE_IPC);
+
+ const PDSHM_DRIVER_CONTEXT context = lpParameter;
+
+ const HANDLE waits[] = {
+ // driver shutdown signals thread termination
+ context->IPC.DispatchThreadTermination,
+ // read is signaled when an outside app has finished writing
+ context->IPC.ReadEvent
+ };
+
+ do
+ {
+ DWORD waitResult = WaitForMultipleObjects(ARRAYSIZE(waits), waits, FALSE, 1000);
+
+ //
+ // Retry indefinitely until an event is signaled
+ //
+ if (waitResult == WAIT_TIMEOUT)
+ continue;
+
+ //
+ // Unexpected result
+ //
+ if (waitResult == WAIT_FAILED)
+ {
+ TraceError(
+ TRACE_IPC,
+ "Wait failed with error %!WINERROR!",
+ GetLastError()
+ );
+ break;
+ }
+
+ //
+ // Thread termination signaled, exiting
+ //
+ if (waitResult == WAIT_OBJECT_0)
+ {
+ break;
+ }
+
+ //
+ // New data is available in the shared region
+ //
+ if (waitResult == WAIT_OBJECT_0 + 1)
+ {
+ //
+ // Each valid message is expected to be prefixed with this header
+ //
+ const PDSHM_IPC_MSG_HEADER header = (PDSHM_IPC_MSG_HEADER)context->IPC.SharedRegions.Commands.Buffer;
+
+ TraceInformation(
+ TRACE_IPC,
+ "Got message type %d for target %d and command %d",
+ header->Type, header->Target, header->Command.Device
+ );
+
+ NTSTATUS status = DSHM_IPC_DispatchIncomingCommandMessage(context, header);
+
+ if (!NT_SUCCESS(status))
+ {
+ TraceError(
+ TRACE_IPC,
+ "DSHM_IPC_DispatchIncomingCommandMessage reported non-success status %!STATUS!",
+ status
+ );
+ }
+ }
+
+ } while (TRUE);
+
+ FuncExitNoReturn(TRACE_IPC);
+
+ return ERROR_SUCCESS;
+}
diff --git a/sys/IPC.h b/sys/IPC.h
new file mode 100644
index 00000000..db515455
--- /dev/null
+++ b/sys/IPC.h
@@ -0,0 +1,326 @@
+#pragma once
+
+#define DSHM_IPC_FILE_MAP_NAME "Global\\DsHidMiniSharedMemory"
+#define DSHM_IPC_MUTEX_NAME "Global\\DsHidMiniCommandMutex"
+#define DSHM_IPC_READ_EVENT_NAME "Global\\DsHidMiniReadEvent"
+#define DSHM_IPC_WRITE_EVENT_NAME "Global\\DsHidMiniWriteEvent"
+
+
+//
+// Describes the type of IPC message response behavior
+//
+typedef enum
+{
+ //
+ // Invalid/reserved, do not use
+ //
+ DSHM_IPC_MSG_TYPE_INVALID = 0,
+ //
+ // Client-to-driver data incoming, no acknowledgment/reply requested
+ //
+ DSHM_IPC_MSG_TYPE_REQUEST_ONLY,
+ //
+ // Client-to-driver data incoming, must be acknowledged by reply
+ //
+ DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE,
+ //
+ // Client requested data, there is nothing to read for the driver
+ //
+ DSHM_IPC_MSG_TYPE_RESPONSE_ONLY,
+ //
+ // Driver-to-client response to a previous DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE
+ //
+ DSHM_IPC_MSG_TYPE_REQUEST_REPLY
+} DSHM_IPC_MSG_TYPE;
+
+//
+// Describes the message receiver
+//
+typedef enum
+{
+ //
+ // Invalid/reserved, do not use
+ //
+ DSHM_IPC_MSG_TARGET_INVALID = 0,
+ //
+ // The message is targeted at the driver
+ //
+ DSHM_IPC_MSG_TARGET_DRIVER,
+ //
+ // The message is targeted at a device
+ //
+ DSHM_IPC_MSG_TARGET_DEVICE,
+ //
+ // The message is targeted at the client/caller/app
+ //
+ DSHM_IPC_MSG_TARGET_CLIENT
+} DSHM_IPC_MSG_TARGET;
+
+//
+// Describes a per-driver command
+//
+typedef enum
+{
+ //
+ // Invalid/reserved, do not use
+ //
+ DSHM_IPC_MSG_CMD_DRIVER_INVALID = 0,
+ //
+ // Message without payload, useful to check for functionality
+ //
+ DSHM_IPC_MSG_CMD_DRIVER_PING
+} DSHM_IPC_MSG_CMD_DRIVER;
+
+//
+// Describes a per-device command
+//
+typedef enum
+{
+ //
+ // Invalid/reserved, do not use
+ //
+ DSHM_IPC_MSG_CMD_DEVICE_INVALID = 0,
+ //
+ // Pair a given device to a new host
+ //
+ DSHM_IPC_MSG_CMD_DEVICE_PAIR_TO,
+ //
+ // Requests a player index update (switch player LED etc.)
+ //
+ DSHM_IPC_MSG_CMD_DEVICE_SET_PLAYER_INDEX,
+ //
+ // Requests a wait handle for input report state changes
+ //
+ DSHM_IPC_MSG_CMD_DEVICE_GET_HID_WAIT_HANDLE,
+} DSHM_IPC_MSG_CMD_DEVICE;
+
+//
+// Prefix of every packet describing the message
+//
+typedef struct _DSHM_IPC_MSG_HEADER
+{
+ //
+ // What request-behavior is expected (request, request-reply, ...)
+ //
+ DSHM_IPC_MSG_TYPE Type;
+
+ //
+ // What component is this message targeting (driver, device, ...)
+ //
+ DSHM_IPC_MSG_TARGET Target;
+
+ //
+ // What command is this message carrying
+ //
+ union
+ {
+ DSHM_IPC_MSG_CMD_DRIVER Driver;
+
+ DSHM_IPC_MSG_CMD_DEVICE Device;
+ } Command;
+
+ //
+ // One-based index of which device is this message for
+ // Set to 0 if driver is targeted
+ //
+ UINT32 TargetIndex;
+
+ //
+ // The size of the entire message (header + payload) in bytes
+ // A size of 0 is invalid
+ //
+ UINT32 Size;
+} DSHM_IPC_MSG_HEADER, * PDSHM_IPC_MSG_HEADER;
+
+//
+// Updates a specified devices' host address
+//
+typedef struct _DSHM_IPC_MSG_PAIR_TO_REQUEST
+{
+ DSHM_IPC_MSG_HEADER Header;
+
+ BD_ADDR Address;
+
+} DSHM_IPC_MSG_PAIR_TO_REQUEST, *PDSHM_IPC_MSG_PAIR_TO_REQUEST;
+
+//
+// Reply to struct _DSHM_IPC_MSG_PAIR_TO_REQUEST
+//
+typedef struct _DSHM_IPC_MSG_PAIR_TO_REPLY
+{
+ DSHM_IPC_MSG_HEADER Header;
+
+ //
+ // NTSTATUS of the set address action
+ //
+ NTSTATUS WriteStatus;
+
+ //
+ // NTSTATUS of the get address action
+ //
+ NTSTATUS ReadStatus;
+
+} DSHM_IPC_MSG_PAIR_TO_REPLY, *PDSHM_IPC_MSG_PAIR_TO_REPLY;
+
+//
+// Updates the player index of a given device
+//
+typedef struct _DSHM_IPC_MSG_SET_PLAYER_INDEX_REQUEST
+{
+ DSHM_IPC_MSG_HEADER Header;
+
+ //
+ // The new player index to set
+ // Valid values are 1 to 7
+ //
+ BYTE PlayerIndex;
+
+} DSHM_IPC_MSG_SET_PLAYER_INDEX_REQUEST, *PDSHM_IPC_MSG_SET_PLAYER_INDEX_REQUEST;
+
+//
+// Reply to struct _DSHM_IPC_MSG_SET_PLAYER_INDEX_REQUEST
+//
+typedef struct _DSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY
+{
+ DSHM_IPC_MSG_HEADER Header;
+
+ NTSTATUS NtStatus;
+
+} DSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY, *PDSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY;
+
+//
+// Requests the driver host process PID and a wait handle for new input reports
+//
+typedef struct _DSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE
+{
+ DSHM_IPC_MSG_HEADER Header;
+
+ //
+ // The driver hosting process PID
+ //
+ DWORD ProcessId;
+
+ //
+ // A handle to an auto-reset event
+ //
+ HANDLE WaitHandle;
+
+} DSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE, *PDSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE;
+
+typedef
+_Function_class_(EVT_DSHM_IPC_DispatchDeviceMessage)
+_IRQL_requires_same_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+NTSTATUS
+EVT_DSHM_IPC_DispatchDeviceMessage(
+ _In_ PDEVICE_CONTEXT DeviceContext,
+ _In_ PDSHM_IPC_MSG_HEADER MessageHeader
+);
+
+typedef EVT_DSHM_IPC_DispatchDeviceMessage *PFN_DSHM_IPC_DispatchDeviceMessage;
+
+#define DSHM_IPC_SIGNAL_WRITE_DONE(_ctx_) \
+ SetEvent((_ctx_)->IPC.WriteEvent)
+
+#define DSHM_IPC_MSG_EXPECTS_REPLY(_msg_) \
+ ((_msg_)->Type == DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE \
+ || (_msg_)->Type == DSHM_IPC_MSG_TYPE_RESPONSE_ONLY)
+
+#define DSHM_IPC_MSG_IS_PING(_msg_) \
+ ((_msg_)->Type == DSHM_IPC_MSG_TYPE_REQUEST_RESPONSE \
+ && (_msg_)->Target == DSHM_IPC_MSG_TARGET_DRIVER \
+ && (_msg_)->Command.Driver == DSHM_IPC_MSG_CMD_DRIVER_PING \
+ && (_msg_)->TargetIndex == 0 \
+ && (_msg_)->Size == sizeof(DSHM_IPC_MSG_HEADER))
+
+#define DSHM_IPC_MSG_IS_FOR_DEVICE(_msg_) \
+ ((_msg_)->Type != DSHM_IPC_MSG_TYPE_INVALID \
+ && (_msg_)->Target == DSHM_IPC_MSG_TARGET_DEVICE \
+ && (_msg_)->Command.Device != DSHM_IPC_MSG_CMD_DEVICE_INVALID \
+ && (_msg_)->TargetIndex > 0 \
+ && (_msg_)->TargetIndex < DSHM_MAX_DEVICES \
+ && (_msg_)->Size >= sizeof(DSHM_IPC_MSG_HEADER))
+
+VOID
+FORCEINLINE
+DSHM_IPC_MSG_PING_RESPONSE_INIT(
+ _Inout_ PDSHM_IPC_MSG_HEADER Message
+)
+{
+ RtlZeroMemory(Message, sizeof(DSHM_IPC_MSG_HEADER));
+
+ Message->Type = DSHM_IPC_MSG_TYPE_REQUEST_REPLY;
+ Message->Target = DSHM_IPC_MSG_TARGET_CLIENT;
+ Message->Command.Driver = DSHM_IPC_MSG_CMD_DRIVER_PING;
+ Message->TargetIndex = 0;
+ Message->Size = sizeof(DSHM_IPC_MSG_HEADER); // no payload
+}
+
+VOID
+FORCEINLINE
+DSHM_IPC_MSG_PAIR_TO_RESPONSE_INIT(
+ _Inout_ PDSHM_IPC_MSG_PAIR_TO_REPLY Message,
+ _In_ UINT32 DeviceIndex,
+ _In_ NTSTATUS WriteStatus,
+ _In_ NTSTATUS ReadStatus
+)
+{
+ const UINT32 size = sizeof(DSHM_IPC_MSG_PAIR_TO_REPLY);
+ RtlZeroMemory(Message, size);
+
+ Message->Header.Type = DSHM_IPC_MSG_TYPE_REQUEST_REPLY;
+ Message->Header.Target = DSHM_IPC_MSG_TARGET_CLIENT;
+ Message->Header.Command.Device = DSHM_IPC_MSG_CMD_DEVICE_PAIR_TO;
+ Message->Header.TargetIndex = DeviceIndex;
+ Message->Header.Size = size;
+
+ Message->WriteStatus = WriteStatus;
+ Message->ReadStatus = ReadStatus;
+}
+
+VOID
+FORCEINLINE
+DSHM_IPC_MSG_SET_PLAYER_INDEX_RESPONSE_INIT(
+ _Inout_ PDSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY Message,
+ _In_ UINT32 DeviceIndex,
+ _In_ NTSTATUS Status
+)
+{
+ const UINT32 size = sizeof(DSHM_IPC_MSG_SET_PLAYER_INDEX_REPLY);
+ RtlZeroMemory(Message, size);
+
+ Message->Header.Type = DSHM_IPC_MSG_TYPE_REQUEST_REPLY;
+ Message->Header.Target = DSHM_IPC_MSG_TARGET_CLIENT;
+ Message->Header.Command.Device = DSHM_IPC_MSG_CMD_DEVICE_PAIR_TO;
+ Message->Header.TargetIndex = DeviceIndex;
+ Message->Header.Size = size;
+
+ Message->NtStatus = Status;
+}
+
+VOID
+FORCEINLINE
+DSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE_INIT(
+ _Inout_ PDSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE Message,
+ _In_ UINT32 DeviceIndex,
+ _In_ DWORD ProcessId,
+ _In_opt_ HANDLE WaitHandle
+)
+{
+ const UINT32 size = sizeof(DSHM_IPC_MSG_GET_HID_WAIT_HANDLE_RESPONSE);
+ RtlZeroMemory(Message, size);
+
+ Message->Header.Type = DSHM_IPC_MSG_TYPE_RESPONSE_ONLY;
+ Message->Header.Target = DSHM_IPC_MSG_TARGET_CLIENT;
+ Message->Header.Command.Device = DSHM_IPC_MSG_CMD_DEVICE_GET_HID_WAIT_HANDLE;
+ Message->Header.TargetIndex = DeviceIndex;
+ Message->Header.Size = size;
+
+ Message->ProcessId = ProcessId;
+ Message->WaitHandle = WaitHandle;
+}
+
+
+NTSTATUS InitIPC(void);
+
+void DestroyIPC(void);
diff --git a/sys/InputReport.c b/sys/InputReport.c
index ef623c9d..856d241f 100644
--- a/sys/InputReport.c
+++ b/sys/InputReport.c
@@ -2,6 +2,9 @@
#include "InputReport.tmh"
+//
+// Protocol-agnostic function that transforms the raw input report to HID-mode-compatible ones
+//
_Use_decl_annotations_
void
DSHM_ParseInputReport(
@@ -12,6 +15,31 @@ DSHM_ParseInputReport(
{
FuncEntry(TRACE_DSHIDMINIDRV);
+#pragma region IPC Copy
+
+ const WDFDRIVER driver = WdfGetDriver();
+ const PDSHM_DRIVER_CONTEXT pDrvCtx = DriverGetContext(driver);
+ /*
+ * Offset calculation puts each devices' input report copy
+ * in their respective position in the memory region, like:
+ * 1st device: ((4 + 49) * (1 - 1)) = 0
+ * 2nd device: ((4 + 49) * (2 - 1)) = 53
+ * 3rd device: ((4 + 49) * (3 - 1)) = 106
+ * and so on
+ */
+ const size_t offset = (sizeof(IPC_HID_INPUT_REPORT_MESSAGE) * (DeviceContext->SlotIndex - 1));
+ const PIPC_HID_INPUT_REPORT_MESSAGE pHIDBuffer = (PIPC_HID_INPUT_REPORT_MESSAGE)(pDrvCtx->IPC.SharedRegions.HID.Buffer + offset);
+
+ // prefix each report with associated device index
+ pHIDBuffer->SlotIndex = DeviceContext->SlotIndex;
+ // skip index and copy unmodified raw report to the section
+ RtlCopyMemory(&pHIDBuffer->InputReport, Report, sizeof(DS3_RAW_INPUT_REPORT));
+
+ // signal any reader that there is new data available
+ SetEvent(DeviceContext->IPC.InputReportWaitHandle);
+
+#pragma endregion
+
#pragma region HID Input Report (SDF, GPJ ID 01) processing
switch (DeviceContext->Configuration.HidDeviceMode) // NOLINT(clang-diagnostic-switch-enum)
diff --git a/sys/Trace.h b/sys/Trace.h
index 93e708d8..eb4af983 100644
--- a/sys/Trace.h
+++ b/sys/Trace.h
@@ -14,6 +14,7 @@
WPP_DEFINE_BIT(TRACE_DS3) \
WPP_DEFINE_BIT(TRACE_DSBTH) \
WPP_DEFINE_BIT(TRACE_CONFIG) \
+ WPP_DEFINE_BIT(TRACE_IPC) \
) \
#define WPP_FLAG_LEVEL_LOGGER(flag, level) \
diff --git a/sys/dshidmini.inf b/sys/dshidmini.inf
index adf8b073..f7f2c2e5 100644
Binary files a/sys/dshidmini.inf and b/sys/dshidmini.inf differ
diff --git a/sys/dshidmini.vcxproj b/sys/dshidmini.vcxproj
index dc60ac4f..af66d7be 100644
--- a/sys/dshidmini.vcxproj
+++ b/sys/dshidmini.vcxproj
@@ -31,6 +31,8 @@
+
+
@@ -59,6 +61,7 @@
+
diff --git a/sys/dshidmini.vcxproj.filters b/sys/dshidmini.vcxproj.filters
index f3414f81..1edd3242 100644
--- a/sys/dshidmini.vcxproj.filters
+++ b/sys/dshidmini.vcxproj.filters
@@ -171,6 +171,9 @@
Header Files
+
+ Header Files
+
@@ -224,6 +227,12 @@
Source Files
+
+ Source Files
+
+
+ Source Files
+