[Pad] Track independent standard, special and remote port handles

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
This commit is contained in:
ParantezTech 2026-10-08 18:25:26 +03:00
parent d3f584a2f1
commit 0e55fa924c
1 changed files with 97 additions and 37 deletions

View File

@ -11,6 +11,7 @@ namespace SharpEmu.Libs.Pad;
public static class PadExports
{
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
private const int OrbisPadErrorAlreadyOpened = unchecked((int)0x80920004);
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
@ -19,8 +20,10 @@ public static class PadExports
// valid 0x10000000 user to scePadOpen and receive DEVICE_NOT_CONNECTED,
// leaving every later keyboard/gamepad read on an invalid handle.
private const int PrimaryUserId = 0x10000000;
private const int SystemUserId = 0xFF;
private const int StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int SpecialPortType = 2;
private const int RemotePortType = 16;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
private const float StandardGravity = 9.80665f;
@ -31,11 +34,32 @@ public static class PadExports
// user slots and treats every valid handle as a connected controller, so accepting
// 0 there reports phantom pads that repeat the primary pad's buttons.
private static int _padOpened;
private static readonly object PadHandleGate = new();
private static readonly Dictionary<PadOpenKey, int> PadHandleByKey = new();
private static readonly Dictionary<int, PadOpenKey> PadKeyByHandle = new();
private static int _nextPadHandle = 1;
private static bool IsPrimaryPadHandle(int handle) =>
handle == PrimaryPadHandle || (handle == 0 && Volatile.Read(ref _padOpened) == 0);
private readonly record struct PadOpenKey(int UserId, int Type, int Index);
internal static void ResetOpenedPadForTests() => Volatile.Write(ref _padOpened, 0);
private static bool IsOpenPadHandle(int handle)
{
lock (PadHandleGate)
{
return PadKeyByHandle.ContainsKey(handle) || (handle == 0 && _padOpened == 0);
}
}
internal static void ResetOpenedPadForTests()
{
lock (PadHandleGate)
{
PadHandleByKey.Clear();
PadKeyByHandle.Clear();
_nextPadHandle = 1;
_padOpened = 0;
_initialized = false;
}
}
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
[ThreadStatic]
@ -91,12 +115,6 @@ public static class PadExports
LibraryName = "libScePad")]
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
// scePadGetHandle(userId, type, index): returns the handle of an already-open
// pad without opening a new one. Dead Cells calls it every frame to poll
// input; leaving it unresolved returned a garbage handle so the input path
// (and the game loop that drives it) misbehaved. Same validation as
// scePadOpen — the one primary pad — returning its handle or a not-connected
// error, never opening or logging.
[SysAbiExport(
Nid = "u1GRHp+oWoY",
ExportName = "scePadGetHandle",
@ -112,22 +130,24 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorNotInitialized);
}
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
if (userId == -1)
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
return ctx.SetReturn(PrimaryPadHandle);
lock (PadHandleGate)
{
return PadHandleByKey.TryGetValue(new PadOpenKey(userId, type, index), out var handle)
? ctx.SetReturn(handle)
: ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
}
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
private static int PadOpenCore(CpuContext ctx, bool extended)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
var parameterAddress = ctx[CpuRegister.Rcx];
if (!_initialized)
{
return ctx.SetReturn(OrbisPadErrorNotInitialized);
@ -138,8 +158,7 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
if (!IsSupportedOpenTuple(userId, type, index, extended))
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
@ -153,8 +172,42 @@ public static class PadExports
: "[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 or Xbox controller will be used automatically when plugged in.");
}
Volatile.Write(ref _padOpened, 1);
return ctx.SetReturn(PrimaryPadHandle);
var key = new PadOpenKey(userId, type, index);
lock (PadHandleGate)
{
if (PadHandleByKey.ContainsKey(key))
{
return ctx.SetReturn(OrbisPadErrorAlreadyOpened);
}
var handle = _nextPadHandle++;
PadHandleByKey.Add(key, handle);
PadKeyByHandle.Add(handle, key);
_padOpened = 1;
return ctx.SetReturn(handle);
}
}
private static bool IsSupportedOpenTuple(int userId, int type, int index, bool extended)
{
if (index != 0)
{
return false;
}
if (userId == SystemUserId && type == RemotePortType)
{
return true;
}
if (userId != PrimaryUserId)
{
return false;
}
return extended
? type is 0 or 1 or 2
: type is StandardPortType or SpecialPortType;
}
[SysAbiExport(
@ -165,9 +218,16 @@ public static class PadExports
public static int PadClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
lock (PadHandleGate)
{
if (!PadKeyByHandle.Remove(handle, out var key))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
PadHandleByKey.Remove(key);
return ctx.SetReturn(0);
}
}
[SysAbiExport(
@ -178,7 +238,7 @@ public static class PadExports
public static int PadSetMotionSensorState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -195,7 +255,7 @@ public static class PadExports
public static int PadSetTiltCorrectionState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -208,7 +268,7 @@ public static class PadExports
public static int PadSetAngularVelocityDeadbandState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -222,7 +282,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -273,7 +333,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -305,7 +365,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -330,7 +390,7 @@ public static class PadExports
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -364,7 +424,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -402,7 +462,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var stateAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -590,7 +650,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -620,7 +680,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -649,7 +709,7 @@ public static class PadExports
public static int PadResetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -666,7 +726,7 @@ public static class PadExports
public static int PadResetOrientation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -679,7 +739,7 @@ public static class PadExports
public static int PadIsRemoteController(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}