diff --git a/src/SharpEmu.Libs/Pad/DualSenseReader.cs b/src/SharpEmu.Libs/Pad/DualSenseReader.cs
new file mode 100644
index 0000000..92ad3ee
--- /dev/null
+++ b/src/SharpEmu.Libs/Pad/DualSenseReader.cs
@@ -0,0 +1,470 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using Microsoft.Win32.SafeHandles;
+
+namespace SharpEmu.Libs.Pad;
+
+///
+/// Snapshot of the DualSense state, already translated to ORBIS pad
+/// conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered;
+/// triggers 0..255).
+///
+internal readonly record struct DualSenseState(
+ bool Connected,
+ uint Buttons,
+ byte LeftX,
+ byte LeftY,
+ byte RightX,
+ byte RightY,
+ byte L2,
+ byte R2);
+
+///
+/// Reads a DualSense controller over raw HID on a background thread.
+/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
+/// activated by requesting feature report 0x05), with hot-plug retry.
+///
+internal static class DualSenseReader
+{
+ private const ushort SonyVendorId = 0x054C;
+ private const ushort DualSenseProductId = 0x0CE6;
+ private const ushort DualSenseEdgeProductId = 0x0DF2;
+
+ // SCE_PAD_BUTTON bit values.
+ private const uint ButtonL3 = 0x0002;
+ private const uint ButtonR3 = 0x0004;
+ private const uint ButtonOptions = 0x0008;
+ private const uint ButtonUp = 0x0010;
+ private const uint ButtonRight = 0x0020;
+ private const uint ButtonDown = 0x0040;
+ private const uint ButtonLeft = 0x0080;
+ private const uint ButtonL2 = 0x0100;
+ private const uint ButtonR2 = 0x0200;
+ private const uint ButtonL1 = 0x0400;
+ private const uint ButtonR1 = 0x0800;
+ private const uint ButtonTriangle = 0x1000;
+ private const uint ButtonCircle = 0x2000;
+ private const uint ButtonCross = 0x4000;
+ private const uint ButtonSquare = 0x8000;
+ private const uint ButtonTouchPad = 0x100000;
+
+ private static readonly object Gate = new();
+ private static DualSenseState _state;
+ private static bool _started;
+
+ // Output (rumble/lightbar) state, all guarded by Gate.
+ private static string? _devicePath;
+ private static bool _bluetooth;
+ private static bool _outputReady;
+ private static bool _lightbarSetupPending;
+ private static byte _outputSequence;
+ private static FileStream? _outputStream;
+ private static byte _motorLeft;
+ private static byte _motorRight;
+ private static byte _lightbarRed;
+ private static byte _lightbarGreen;
+ private static byte _lightbarBlue = 64; // PS-style blue default
+ private static byte _playerLeds = 0x04; // center LED = player 1
+
+ /// Starts the background reader once; safe to call repeatedly.
+ internal static void EnsureStarted()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return;
+ }
+
+ lock (Gate)
+ {
+ if (_started)
+ {
+ return;
+ }
+
+ _started = true;
+ var thread = new Thread(ReadLoop)
+ {
+ IsBackground = true,
+ Name = "DualSenseReader",
+ };
+ thread.Start();
+ }
+ }
+
+ internal static bool TryGetState(out DualSenseState state)
+ {
+ lock (Gate)
+ {
+ state = _state;
+ }
+
+ return state.Connected;
+ }
+
+ private static void SetState(in DualSenseState state)
+ {
+ lock (Gate)
+ {
+ _state = state;
+ }
+ }
+
+ /// Sets rumble; large = left/strong motor, small = right/weak.
+ internal static void SetRumble(byte largeMotor, byte smallMotor)
+ {
+ lock (Gate)
+ {
+ if (_motorLeft == largeMotor && _motorRight == smallMotor)
+ {
+ return;
+ }
+
+ _motorLeft = largeMotor;
+ _motorRight = smallMotor;
+ SendOutputLocked();
+ }
+ }
+
+ internal static void SetLightbar(byte red, byte green, byte blue)
+ {
+ lock (Gate)
+ {
+ if (_lightbarRed == red && _lightbarGreen == green && _lightbarBlue == blue)
+ {
+ return;
+ }
+
+ _lightbarRed = red;
+ _lightbarGreen = green;
+ _lightbarBlue = blue;
+ SendOutputLocked();
+ }
+ }
+
+ internal static void ResetLightbar() => SetLightbar(0, 0, 64);
+
+ private static void OnDeviceIdentified(string path, bool bluetooth)
+ {
+ lock (Gate)
+ {
+ _devicePath = path;
+ _bluetooth = bluetooth;
+ _outputReady = true;
+ _lightbarSetupPending = true;
+ // Announce ourselves on the hardware: default lightbar + player 1 LED.
+ SendOutputLocked();
+ }
+ }
+
+ private static void OnDeviceLost()
+ {
+ lock (Gate)
+ {
+ _devicePath = null;
+ _outputReady = false;
+ _motorLeft = 0;
+ _motorRight = 0;
+ _outputStream?.Dispose();
+ _outputStream = null;
+ }
+ }
+
+ private static void SendOutputLocked()
+ {
+ if (!_outputReady || _devicePath is null)
+ {
+ return; // flushed by OnDeviceIdentified once connected
+ }
+
+ try
+ {
+ if (_outputStream is null)
+ {
+ var handle = HidNative.CreateFile(
+ _devicePath,
+ HidNative.GenericRead | HidNative.GenericWrite,
+ HidNative.FileShareRead | HidNative.FileShareWrite,
+ 0, HidNative.OpenExisting, 0, 0);
+ if (handle.IsInvalid)
+ {
+ handle.Dispose();
+ return; // read-only device access: outputs unavailable
+ }
+
+ _outputStream = new FileStream(handle, FileAccess.Write, bufferSize: 1);
+ }
+
+ var report = BuildOutputReportLocked();
+ _outputStream.Write(report, 0, report.Length);
+ _outputStream.Flush();
+ }
+ catch (Exception)
+ {
+ _outputStream?.Dispose();
+ _outputStream = null;
+ }
+ }
+
+ private static byte[] BuildOutputReportLocked()
+ {
+ // Common 47-byte output payload (offsets per the DualSense output
+ // report layout, same as Linux hid-playstation).
+ Span common = stackalloc byte[47];
+ common[0] = 0x03; // valid_flag0: compatible vibration + haptics select
+ common[1] = 0x04 | 0x10; // valid_flag1: lightbar + player indicator
+ common[2] = _motorRight; // right (weak) motor
+ common[3] = _motorLeft; // left (strong) motor
+ if (_lightbarSetupPending)
+ {
+ common[38] |= 0x02; // valid_flag2: lightbar setup control enable
+ common[41] = 0x01; // lightbar_setup: light on
+ _lightbarSetupPending = false;
+ }
+
+ common[43] = _playerLeds;
+ common[44] = _lightbarRed;
+ common[45] = _lightbarGreen;
+ common[46] = _lightbarBlue;
+
+ if (!_bluetooth)
+ {
+ var usbReport = new byte[48];
+ usbReport[0] = 0x02;
+ common.CopyTo(usbReport.AsSpan(1));
+ return usbReport;
+ }
+
+ // Bluetooth: 0x31 wrapper with sequence tag and CRC32 over a 0xA2
+ // seed byte plus the first 74 report bytes.
+ var btReport = new byte[78];
+ btReport[0] = 0x31;
+ btReport[1] = (byte)((_outputSequence & 0x0F) << 4);
+ _outputSequence = (byte)((_outputSequence + 1) & 0x0F);
+ btReport[2] = 0x10;
+ common.CopyTo(btReport.AsSpan(3));
+ var crc = Crc32(0xA2, btReport.AsSpan(0, 74));
+ btReport[74] = (byte)crc;
+ btReport[75] = (byte)(crc >> 8);
+ btReport[76] = (byte)(crc >> 16);
+ btReport[77] = (byte)(crc >> 24);
+ return btReport;
+ }
+
+ private static uint Crc32(byte seed, ReadOnlySpan data)
+ {
+ var crc = Crc32Update(0xFFFFFFFFu, seed);
+ foreach (var value in data)
+ {
+ crc = Crc32Update(crc, value);
+ }
+
+ return ~crc;
+ }
+
+ private static uint Crc32Update(uint crc, byte value)
+ {
+ crc ^= value;
+ for (var bit = 0; bit < 8; bit++)
+ {
+ crc = (crc >> 1) ^ (0xEDB88320u & (uint)-(int)(crc & 1));
+ }
+
+ return crc;
+ }
+
+ private static void ReadLoop()
+ {
+ var announcedConnect = false;
+ while (true)
+ {
+ SafeFileHandle? handle = null;
+ try
+ {
+ handle = OpenDualSense(out var devicePath);
+ if (handle is null || devicePath is null)
+ {
+ SetState(default);
+ announcedConnect = false;
+ Thread.Sleep(1000);
+ continue;
+ }
+
+ // Bluetooth quirk: the DualSense sends a simplified report
+ // until feature report 0x05 is requested, which switches it
+ // to the full 0x31 input report. Harmless over USB.
+ var feature = new byte[41];
+ feature[0] = 0x05;
+ _ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
+
+ if (!announcedConnect)
+ {
+ Console.Error.WriteLine("[LOADER][INFO] DualSense controller connected.");
+ announcedConnect = true;
+ }
+
+ using var stream = new FileStream(handle, FileAccess.Read, bufferSize: 1);
+ handle = null; // stream owns it now
+ var buffer = new byte[256];
+ var transportKnown = false;
+ while (true)
+ {
+ var read = stream.Read(buffer, 0, buffer.Length);
+ if (read <= 0)
+ {
+ break;
+ }
+
+ if (TryParseReport(buffer.AsSpan(0, read), out var state))
+ {
+ if (!transportKnown)
+ {
+ // The first parsed report tells us the transport,
+ // which the output (rumble/lightbar) path needs.
+ transportKnown = true;
+ OnDeviceIdentified(devicePath, bluetooth: buffer[0] == 0x31);
+ }
+
+ SetState(state);
+ }
+ }
+ }
+ catch (Exception)
+ {
+ // Unplugged or read error: fall through and retry.
+ }
+ finally
+ {
+ handle?.Dispose();
+ }
+
+ if (announcedConnect)
+ {
+ Console.Error.WriteLine("[LOADER][INFO] DualSense controller disconnected.");
+ announcedConnect = false;
+ }
+
+ OnDeviceLost();
+ SetState(default);
+ Thread.Sleep(1000);
+ }
+ }
+
+ private static SafeFileHandle? OpenDualSense(out string? devicePath)
+ {
+ devicePath = null;
+ foreach (var path in HidNative.EnumerateHidDevicePaths())
+ {
+ // Open without access rights just to query VID/PID.
+ using var probe = HidNative.CreateFile(
+ path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
+ if (probe.IsInvalid)
+ {
+ continue;
+ }
+
+ var attributes = new HidNative.HiddAttributes { Size = 12 };
+ if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
+ attributes.VendorId != SonyVendorId ||
+ (attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
+ {
+ continue;
+ }
+
+ // Read+write so feature reports work; fall back to read-only.
+ var handle = HidNative.CreateFile(
+ path,
+ HidNative.GenericRead | HidNative.GenericWrite,
+ HidNative.FileShareRead | HidNative.FileShareWrite,
+ 0, HidNative.OpenExisting, 0, 0);
+ if (handle.IsInvalid)
+ {
+ handle.Dispose();
+ handle = HidNative.CreateFile(
+ path,
+ HidNative.GenericRead,
+ HidNative.FileShareRead | HidNative.FileShareWrite,
+ 0, HidNative.OpenExisting, 0, 0);
+ }
+
+ if (!handle.IsInvalid)
+ {
+ devicePath = path;
+ return handle;
+ }
+
+ handle.Dispose();
+ }
+
+ return null;
+ }
+
+ private static bool TryParseReport(ReadOnlySpan report, out DualSenseState state)
+ {
+ // USB: report id 0x01, payload starts at [1].
+ // Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
+ int offset;
+ if (report.Length >= 11 && report[0] == 0x01)
+ {
+ offset = 1;
+ }
+ else if (report.Length >= 12 && report[0] == 0x31)
+ {
+ offset = 2;
+ }
+ else
+ {
+ state = default;
+ return false;
+ }
+
+ var leftX = report[offset + 0];
+ var leftY = report[offset + 1];
+ var rightX = report[offset + 2];
+ var rightY = report[offset + 3];
+ var l2 = report[offset + 4];
+ var r2 = report[offset + 5];
+ var buttons0 = report[offset + 7];
+ var buttons1 = report[offset + 8];
+ var buttons2 = report[offset + 9];
+
+ uint buttons = 0;
+ buttons |= (buttons0 & 0x10) != 0 ? ButtonSquare : 0;
+ buttons |= (buttons0 & 0x20) != 0 ? ButtonCross : 0;
+ buttons |= (buttons0 & 0x40) != 0 ? ButtonCircle : 0;
+ buttons |= (buttons0 & 0x80) != 0 ? ButtonTriangle : 0;
+ buttons |= HatToButtons(buttons0 & 0x0F);
+ buttons |= (buttons1 & 0x01) != 0 ? ButtonL1 : 0;
+ buttons |= (buttons1 & 0x02) != 0 ? ButtonR1 : 0;
+ buttons |= (buttons1 & 0x04) != 0 ? ButtonL2 : 0;
+ buttons |= (buttons1 & 0x08) != 0 ? ButtonR2 : 0;
+ buttons |= (buttons1 & 0x20) != 0 ? ButtonOptions : 0;
+ buttons |= (buttons1 & 0x40) != 0 ? ButtonL3 : 0;
+ buttons |= (buttons1 & 0x80) != 0 ? ButtonR3 : 0;
+ buttons |= (buttons2 & 0x02) != 0 ? ButtonTouchPad : 0;
+
+ state = new DualSenseState(
+ Connected: true,
+ Buttons: buttons,
+ LeftX: leftX,
+ LeftY: leftY,
+ RightX: rightX,
+ RightY: rightY,
+ L2: l2,
+ R2: r2);
+ return true;
+ }
+
+ private static uint HatToButtons(int hat) => hat switch
+ {
+ 0 => ButtonUp,
+ 1 => ButtonUp | ButtonRight,
+ 2 => ButtonRight,
+ 3 => ButtonRight | ButtonDown,
+ 4 => ButtonDown,
+ 5 => ButtonDown | ButtonLeft,
+ 6 => ButtonLeft,
+ 7 => ButtonLeft | ButtonUp,
+ _ => 0,
+ };
+}
diff --git a/src/SharpEmu.Libs/Pad/HidNative.cs b/src/SharpEmu.Libs/Pad/HidNative.cs
new file mode 100644
index 0000000..b52a589
--- /dev/null
+++ b/src/SharpEmu.Libs/Pad/HidNative.cs
@@ -0,0 +1,136 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace SharpEmu.Libs.Pad;
+
+///
+/// Minimal Win32 HID interop used to talk to a DualSense controller
+/// directly, without any external input library.
+///
+internal static partial class HidNative
+{
+ internal const int DigcfPresent = 0x02;
+ internal const int DigcfDeviceInterface = 0x10;
+ internal const uint GenericRead = 0x80000000;
+ internal const uint GenericWrite = 0x40000000;
+ internal const uint FileShareRead = 0x1;
+ internal const uint FileShareWrite = 0x2;
+ internal const uint OpenExisting = 3;
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct SpDeviceInterfaceData
+ {
+ public int CbSize;
+ public Guid InterfaceClassGuid;
+ public int Flags;
+ public nint Reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HiddAttributes
+ {
+ public int Size;
+ public ushort VendorId;
+ public ushort ProductId;
+ public ushort VersionNumber;
+ }
+
+ [DllImport("hid.dll")]
+ internal static extern void HidD_GetHidGuid(out Guid hidGuid);
+
+ [DllImport("hid.dll")]
+ internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
+
+ [DllImport("hid.dll")]
+ internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
+
+ [DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
+ internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
+
+ [DllImport("setupapi.dll")]
+ internal static extern bool SetupDiEnumDeviceInterfaces(
+ nint deviceInfoSet,
+ nint deviceInfoData,
+ ref Guid interfaceClassGuid,
+ int memberIndex,
+ ref SpDeviceInterfaceData deviceInterfaceData);
+
+ [DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
+ internal static extern bool SetupDiGetDeviceInterfaceDetail(
+ nint deviceInfoSet,
+ ref SpDeviceInterfaceData deviceInterfaceData,
+ nint deviceInterfaceDetailData,
+ int deviceInterfaceDetailDataSize,
+ out int requiredSize,
+ nint deviceInfoData);
+
+ [DllImport("setupapi.dll")]
+ internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern SafeFileHandle CreateFile(
+ string fileName,
+ uint desiredAccess,
+ uint shareMode,
+ nint securityAttributes,
+ uint creationDisposition,
+ uint flagsAndAttributes,
+ nint templateFile);
+
+ ///
+ /// Enumerates the device paths of all present HID interfaces.
+ ///
+ internal static List EnumerateHidDevicePaths()
+ {
+ var paths = new List();
+ HidD_GetHidGuid(out var hidGuid);
+ var deviceInfoSet = SetupDiGetClassDevs(ref hidGuid, 0, 0, DigcfPresent | DigcfDeviceInterface);
+ if (deviceInfoSet == -1 || deviceInfoSet == 0)
+ {
+ return paths;
+ }
+
+ try
+ {
+ var interfaceData = new SpDeviceInterfaceData
+ {
+ CbSize = Marshal.SizeOf(),
+ };
+
+ for (var index = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, 0, ref hidGuid, index, ref interfaceData); index++)
+ {
+ SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, 0, 0, out var requiredSize, 0);
+ if (requiredSize <= 0)
+ {
+ continue;
+ }
+
+ var detailBuffer = Marshal.AllocHGlobal(requiredSize);
+ try
+ {
+ // SP_DEVICE_INTERFACE_DETAIL_DATA_W.cbSize is 8 on x64
+ // (DWORD + aligned WCHAR[1]); the path string follows it.
+ Marshal.WriteInt32(detailBuffer, 8);
+ if (SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, detailBuffer, requiredSize, out _, 0) &&
+ Marshal.PtrToStringUni(detailBuffer + 4) is { Length: > 0 } path)
+ {
+ paths.Add(path);
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(detailBuffer);
+ }
+ }
+ }
+ finally
+ {
+ SetupDiDestroyDeviceInfoList(deviceInfoSet);
+ }
+
+ return paths;
+ }
+}
diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs
index 1a10c67..dc9f3f8 100644
--- a/src/SharpEmu.Libs/Pad/PadExports.cs
+++ b/src/SharpEmu.Libs/Pad/PadExports.cs
@@ -30,6 +30,7 @@ public static class PadExports
public static int PadInit(CpuContext ctx)
{
_initialized = true;
+ DualSenseReader.EnsureStarted();
return ctx.SetReturn(0);
}
@@ -59,7 +60,10 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
- Console.Error.WriteLine("[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options");
+ DualSenseReader.EnsureStarted();
+ Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
+ ? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
+ : "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense will be used automatically when plugged in.");
return ctx.SetReturn(PrimaryPadHandle);
}
@@ -170,23 +174,116 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
+ [SysAbiExport(
+ Nid = "yFVnOdGxvZY",
+ ExportName = "scePadSetVibration",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadSetVibration(CpuContext ctx)
+ {
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ var parameterAddress = ctx[CpuRegister.Rsi];
+ if (handle != PrimaryPadHandle)
+ {
+ return ctx.SetReturn(OrbisPadErrorInvalidHandle);
+ }
+
+ if (parameterAddress == 0)
+ {
+ return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
+ }
+
+ // ScePadVibrationParam: { uint8_t largeMotor; uint8_t smallMotor; }
+ Span parameter = stackalloc byte[2];
+ if (!ctx.Memory.TryRead(parameterAddress, parameter))
+ {
+ return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+
+ DualSenseReader.SetRumble(parameter[0], parameter[1]);
+ return ctx.SetReturn(0);
+ }
+
+ [SysAbiExport(
+ Nid = "RR4novUEENY",
+ ExportName = "scePadSetLightBar",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadSetLightBar(CpuContext ctx)
+ {
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ var parameterAddress = ctx[CpuRegister.Rsi];
+ if (handle != PrimaryPadHandle)
+ {
+ return ctx.SetReturn(OrbisPadErrorInvalidHandle);
+ }
+
+ if (parameterAddress == 0)
+ {
+ return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
+ }
+
+ // ScePadColor: { uint8_t r; uint8_t g; uint8_t b; uint8_t reserved; }
+ Span color = stackalloc byte[4];
+ if (!ctx.Memory.TryRead(parameterAddress, color))
+ {
+ return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
+ }
+
+ DualSenseReader.SetLightbar(color[0], color[1], color[2]);
+ return ctx.SetReturn(0);
+ }
+
+ [SysAbiExport(
+ Nid = "DscD1i9HX1w",
+ ExportName = "scePadResetLightBar",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libScePad")]
+ public static int PadResetLightBar(CpuContext ctx)
+ {
+ var handle = unchecked((int)ctx[CpuRegister.Rdi]);
+ if (handle != PrimaryPadHandle)
+ {
+ return ctx.SetReturn(OrbisPadErrorInvalidHandle);
+ }
+
+ DualSenseReader.ResetLightbar();
+ return ctx.SetReturn(0);
+ }
+
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
{
Span data = stackalloc byte[PadDataSize];
data.Clear();
var acceptsKeyboardInput = IsEmulatorWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
- BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
+ var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
+ var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
+
+ if (DualSenseReader.TryGetState(out var pad))
+ {
+ buttons |= pad.Buttons;
+ // The controller stick wins whenever it is deflected past a
+ // small deadzone; otherwise any keyboard value stays.
+ leftX = MergeAxis(pad.LeftX, leftX);
+ leftY = MergeAxis(pad.LeftY, leftY);
+ rightX = MergeAxis(pad.RightX, rightX);
+ rightY = MergeAxis(pad.RightY, rightY);
+ l2 = Math.Max(l2, pad.L2);
+ r2 = Math.Max(r2, pad.R2);
+ }
+
+ BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
data[0x04] = leftX;
data[0x05] = leftY;
data[0x06] = rightX;
data[0x07] = rightY;
- data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
- data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
+ data[0x08] = l2;
+ data[0x09] = r2;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp();
@@ -254,4 +351,10 @@ public static class PadExports
if (positive && !negative) return 255;
return 128;
}
+
+ private static byte MergeAxis(byte controller, byte keyboard)
+ {
+ const int Deadzone = 10;
+ return Math.Abs(controller - 128) > Deadzone ? controller : keyboard;
+ }
}