Files
sharpemu/src/SharpEmu.Libs/Pad/PadExports.cs
T
Berk 2b6bd5a532 Sdl backend (#670)
* [audio] added sdl audio backend and in-tree atrac9 decoder

* [input] replaced per-platform pad readers with sdl gamepad input

* [video] added sdl window and host display plumbing

* [gui] added host display options and per-game render settings

* [bink] synced host movie playback to the guest audio clock

* [cpu] hooked windows write faults into guest image tracking

* [perf] added guest and render profiling, reserved host cpu lanes

* [kernel] fixed stale pthread mutex handle alias

* [host] wired the sdl session, save-data paths and project references

* [audio] hoisted ajm trace stackalloc out of its loop

* [video] Add guest image sync setting

* [build] Strip native symbols

* reuse
2026-07-28 03:33:26 +03:00

856 lines
32 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using System.Buffers.Binary;
using System.Diagnostics;
namespace SharpEmu.Libs.Pad;
public static class PadExports
{
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
// Keep the pad session on the same retail user id returned by
// libSceUserService. A mismatched emulator-local id makes games pass a
// 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 StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
// Real firmware hands out small non-negative handles; 0 is valid. Some titles
// (Monster Truck Championship) read pad state with handle 0, and rejecting it
// leaves their controller/FFB init path polling a never-valid state forever.
private static bool IsPrimaryPadHandle(int handle) => handle is 0 or PrimaryPadHandle;
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
[ThreadStatic]
private static long _lastInputSampleTicks;
[ThreadStatic]
private static PadState _cachedInputState;
private static bool _initialized;
private static int _motionSensorEnabled;
private static int _controlsAnnouncementLogged;
[SysAbiExport(
Nid = "hv1luiJrqQM",
ExportName = "scePadInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadInit(CpuContext ctx)
{
_initialized = true;
HostPlatform.Current.Input.EnsureStarted();
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "xk0AcarP3V4",
ExportName = "scePadOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadOpen(CpuContext ctx) => PadOpenCore(ctx, extended: false);
[SysAbiExport(
Nid = "WFIiSfXGUq8",
ExportName = "scePadOpenExt",
Target = Generation.Gen4 | Generation.Gen5,
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",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadGetHandle(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
if (!_initialized)
{
return ctx.SetReturn(OrbisPadErrorNotInitialized);
}
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
return ctx.SetReturn(PrimaryPadHandle);
}
// 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);
}
if (userId == -1)
{
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))
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
var input = HostPlatform.Current.Input;
input.EnsureStarted();
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
{
Console.Error.WriteLine(input.DescribeConnectedGamepad() is { } gamepadName
? $"[LOADER][INFO] Controls: {gamepadName} 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 or Xbox controller will be used automatically when plugged in.");
}
return ctx.SetReturn(PrimaryPadHandle);
}
[SysAbiExport(
Nid = "6ncge5+l5Qs",
ExportName = "scePadClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
[SysAbiExport(
Nid = "clVvL4ZDntw",
ExportName = "scePadSetMotionSensorState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetMotionSensorState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
Volatile.Write(ref _motionSensorEnabled, ctx[CpuRegister.Rsi] != 0 ? 1 : 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "vDLMoJLde8I",
ExportName = "scePadSetTiltCorrectionState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetTiltCorrectionState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
[SysAbiExport(
Nid = "gjP9-KQzoUk",
ExportName = "scePadGetControllerInformation",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadGetControllerInformation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (informationAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> information = stackalloc byte[ControllerInformationSize];
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
information[0x08] = 30;
information[0x09] = 30;
information[0x0A] = StandardPortType;
information[0x0B] = 1;
information[0x0C] = 1;
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
return ctx.Memory.TryWrite(informationAddress, information)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "hGbf2QTBmqc",
ExportName = "scePadGetExtControllerInformation",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadGetExtControllerInformation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (informationAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// Base ScePadControllerInformation + device-class/connection fields: report a connected
// DualSense so the guest's open -> get-ext-info -> close probe loop resolves.
Span<byte> information = stackalloc byte[0x40];
information.Clear();
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
information[0x08] = 30;
information[0x09] = 30;
information[0x0A] = StandardPortType;
information[0x0B] = 1; // connected count
information[0x0C] = 1; // connected
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense
information[0x1D] = 1; // connected (ext)
information[0x1E] = 0; // connectionType: local
return ctx.Memory.TryWrite(informationAddress, information)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "AcslpN1jHR8",
ExportName = "scePadDeviceClassGetExtendedInformation",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadDeviceClassGetExtendedInformation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (informationAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// ScePadDeviceClassExtendedInformation: deviceClass 0 = standard pad
// (DualSense). We emulate no special peripheral (guitar/drums/wheel), so
// the class-data union stays zeroed — the guest treats it as a plain
// controller with no extended capabilities.
Span<byte> information = stackalloc byte[0x20];
information.Clear();
BinaryPrimitives.WriteInt32LittleEndian(information[0x00..], 0);
return ctx.Memory.TryWrite(informationAddress, information)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "YndgXqQVV7c",
ExportName = "scePadReadState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadReadState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "q1cHNfGycLI",
ExportName = "scePadRead",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadRead(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0 || count < 1 || count > 64)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? ctx.SetReturn(1)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "W2G-yoyMF5U",
ExportName = "scePadSetVibrationMode",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetVibrationMode(CpuContext ctx)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "2JgFB2n9oUM",
ExportName = "scePadSetTriggerEffect",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetTriggerEffect(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (parameterAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> parameter = stackalloc byte[120];
if (!ctx.Memory.TryRead(parameterAddress, parameter))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var triggerMask = parameter[0];
HostPlatform.Current.Input.SetAdaptiveTriggerEffect(
(triggerMask & 0x01) != 0 ? DecodeTriggerEffect(parameter[8..64]) : null,
(triggerMask & 0x02) != 0 ? DecodeTriggerEffect(parameter[64..120]) : null);
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
private static HostAdaptiveTriggerEffect DecodeTriggerEffect(ReadOnlySpan<byte> command)
{
var mode = BinaryPrimitives.ReadUInt32LittleEndian(command);
var parameters = command[8..];
Span<byte> native = stackalloc byte[11];
native.Clear();
byte fallbackStrength = 0;
switch (mode)
{
case 1:
EncodeFeedback(native, parameters[0], parameters[1]);
fallbackStrength = ScaleTriggerStrength(parameters[1]);
break;
case 2:
EncodeWeapon(native, parameters[0], parameters[1], parameters[2]);
fallbackStrength = ScaleTriggerStrength(parameters[2]);
break;
case 3:
EncodeZonedEffect(native, 0x26, parameters[0], parameters[1], parameters[2]);
fallbackStrength = ScaleTriggerStrength(parameters[1]);
break;
case 4:
EncodeZonedStrengths(native, 0x21, parameters[..10], 0);
fallbackStrength = ScaleTriggerStrength(Max(parameters[..10]));
break;
case 5:
EncodeSlope(native, parameters[0], parameters[1], parameters[2], parameters[3]);
fallbackStrength = ScaleTriggerStrength(Math.Max(parameters[2], parameters[3]));
break;
case 6:
EncodeZonedStrengths(native, 0x26, parameters[1..11], parameters[0]);
fallbackStrength = parameters[0] == 0 ? (byte)0 : ScaleTriggerStrength(Max(parameters[1..11]));
break;
default:
native[0] = 0x05;
break;
}
return HostAdaptiveTriggerEffect.FromBytes(native, fallbackStrength);
}
private static void EncodeFeedback(Span<byte> destination, byte position, byte strength)
{
if (position > 9 || strength is 0 or > 8)
{
destination[0] = 0x05;
return;
}
Span<byte> strengths = stackalloc byte[10];
strengths[position..].Fill(strength);
EncodeZonedStrengths(destination, 0x21, strengths, 0);
}
private static void EncodeWeapon(Span<byte> destination, byte start, byte end, byte strength)
{
if (start is < 2 or > 7 || end <= start || end > 8 || strength is 0 or > 8)
{
destination[0] = 0x05;
return;
}
var zones = (ushort)((1 << start) | (1 << end));
destination[0] = 0x25;
BinaryPrimitives.WriteUInt16LittleEndian(destination[1..], zones);
destination[3] = (byte)(strength - 1);
}
private static void EncodeZonedEffect(
Span<byte> destination,
byte nativeMode,
byte position,
byte strength,
byte frequency)
{
if (position > 9 || strength is 0 or > 8 || frequency == 0)
{
destination[0] = 0x05;
return;
}
Span<byte> strengths = stackalloc byte[10];
strengths[position..].Fill(strength);
EncodeZonedStrengths(destination, nativeMode, strengths, frequency);
}
private static void EncodeSlope(
Span<byte> destination,
byte startPosition,
byte endPosition,
byte startStrength,
byte endStrength)
{
if (startPosition > 8 || endPosition <= startPosition || endPosition > 9 ||
startStrength is 0 or > 8 || endStrength is 0 or > 8)
{
destination[0] = 0x05;
return;
}
Span<byte> strengths = stackalloc byte[10];
var distance = endPosition - startPosition;
for (var index = startPosition; index < strengths.Length; index++)
{
strengths[index] = index <= endPosition
? (byte)Math.Round(startStrength + ((endStrength - startStrength) * (index - startPosition) / (double)distance))
: endStrength;
}
EncodeZonedStrengths(destination, 0x21, strengths, 0);
}
private static void EncodeZonedStrengths(
Span<byte> destination,
byte nativeMode,
ReadOnlySpan<byte> strengths,
byte frequency)
{
ushort activeZones = 0;
uint packedStrengths = 0;
for (var index = 0; index < Math.Min(strengths.Length, 10); index++)
{
var strength = strengths[index];
if (strength is 0 or > 8)
{
continue;
}
activeZones |= (ushort)(1 << index);
packedStrengths |= (uint)(strength - 1) << (index * 3);
}
if (activeZones == 0 || (nativeMode == 0x26 && frequency == 0))
{
destination[0] = 0x05;
return;
}
destination[0] = nativeMode;
BinaryPrimitives.WriteUInt16LittleEndian(destination[1..], activeZones);
BinaryPrimitives.WriteUInt32LittleEndian(destination[3..], packedStrengths);
destination[9] = frequency;
}
private static byte Max(ReadOnlySpan<byte> values)
{
byte result = 0;
foreach (var value in values)
{
result = Math.Max(result, value);
}
return result;
}
private static byte ScaleTriggerStrength(byte strength) =>
(byte)(Math.Min(strength, (byte)8) * 255 / 8);
[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 (!IsPrimaryPadHandle(handle))
{
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<byte> parameter = stackalloc byte[2];
if (!ctx.Memory.TryRead(parameterAddress, parameter))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
HostPlatform.Current.Input.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 (!IsPrimaryPadHandle(handle))
{
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<byte> color = stackalloc byte[4];
if (!ctx.Memory.TryRead(parameterAddress, color))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
HostPlatform.Current.Input.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 (!IsPrimaryPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
HostPlatform.Current.Input.ResetLightbar();
return ctx.SetReturn(0);
}
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
{
Span<byte> data = stackalloc byte[PadDataSize];
data.Clear();
var input = ReadHostInputState();
var buttons = input.Buttons;
var leftX = input.LeftX;
var leftY = input.LeftY;
var rightX = input.RightX;
var rightY = input.RightY;
var l2 = input.L2;
var r2 = input.R2;
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
data[0x04] = leftX;
data[0x05] = leftY;
data[0x06] = rightX;
data[0x07] = rightY;
data[0x08] = l2;
data[0x09] = r2;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
if (Volatile.Read(ref _motionSensorEnabled) != 0 && input.Motion.Available)
{
BinaryPrimitives.WriteSingleLittleEndian(data[0x1C..], input.Motion.AccelerationX);
BinaryPrimitives.WriteSingleLittleEndian(data[0x20..], input.Motion.AccelerationY);
BinaryPrimitives.WriteSingleLittleEndian(data[0x24..], input.Motion.AccelerationZ);
BinaryPrimitives.WriteSingleLittleEndian(data[0x28..], input.Motion.AngularVelocityX);
BinaryPrimitives.WriteSingleLittleEndian(data[0x2C..], input.Motion.AngularVelocityY);
BinaryPrimitives.WriteSingleLittleEndian(data[0x30..], input.Motion.AngularVelocityZ);
}
WriteTouchData(data, input.Touch);
data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp();
var timestampMicroseconds =
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
BinaryPrimitives.WriteUInt64LittleEndian(
data[0x50..],
timestampMicroseconds);
data[0x68] = 1;
return ctx.Memory.TryWrite(dataAddress, data);
}
private static PadState ReadHostInputState()
{
var now = Stopwatch.GetTimestamp();
if (_lastInputSampleTicks != 0 && now - _lastInputSampleTicks < InputSampleIntervalTicks)
{
return _cachedInputState;
}
var input = HostPlatform.Current.Input;
var acceptsKeyboardInput = input.IsHostWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons(input) : 0;
var leftX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x41), input.IsKeyDown(0x44)) : (byte)128;
var leftY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x57), input.IsKeyDown(0x53)) : (byte)128;
var rightX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x4A), input.IsKeyDown(0x4C)) : (byte)128;
var rightY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x49), input.IsKeyDown(0x4B)) : (byte)128;
var l2 = acceptsKeyboardInput && input.IsKeyDown(0x52) ? (byte)255 : (byte)0;
var r2 = acceptsKeyboardInput && input.IsKeyDown(0x46) ? (byte)255 : (byte)0;
var gamepadType = HostGamepadType.Generic;
var connection = HostGamepadConnection.Unknown;
var motion = default(HostMotionState);
var touch = default(HostTouchState);
Span<HostGamepadState> gamepads = stackalloc HostGamepadState[2];
var gamepadCount = input.GetGamepadStates(gamepads);
for (var index = 0; index < gamepadCount; index++)
{
var pad = gamepads[index];
buttons |= ToOrbisButtons(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.LeftTrigger);
r2 = Math.Max(r2, pad.RightTrigger);
if (index == 0)
{
gamepadType = pad.Type;
connection = pad.Connection;
motion = pad.Motion;
touch = pad.Touch;
}
}
if (IsAutoCrossActive())
{
buttons |= 0x4000;
}
_cachedInputState = new PadState(
Connected: true,
Buttons: buttons,
LeftX: leftX,
LeftY: leftY,
RightX: rightX,
RightY: rightY,
L2: l2,
R2: r2,
Type: gamepadType,
Connection: connection,
Motion: motion,
Touch: touch);
_lastInputSampleTicks = now;
return _cachedInputState;
}
private static readonly long PadStartTimestamp = Stopwatch.GetTimestamp();
private static readonly double[] AutoCrossTimes = ParseAutoCrossTimes();
private static double[] ParseAutoCrossTimes()
{
// SHARPEMU_AUTO_CROSS="40,52,64": presses Cross for 0.4s at each
// second offset from process start. Debug aid for unattended runs.
var raw = Environment.GetEnvironmentVariable("SHARPEMU_AUTO_CROSS");
if (string.IsNullOrWhiteSpace(raw))
{
return [];
}
var values = new List<double>();
foreach (var token in raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (double.TryParse(token, System.Globalization.CultureInfo.InvariantCulture, out var value))
{
values.Add(value);
}
}
return values.ToArray();
}
private static bool IsAutoCrossActive()
{
var times = AutoCrossTimes;
if (times.Length == 0)
{
return false;
}
var elapsed = (Stopwatch.GetTimestamp() - PadStartTimestamp) / (double)Stopwatch.Frequency;
foreach (var time in times)
{
if (elapsed >= time && elapsed < time + 0.4)
{
return true;
}
}
return false;
}
/// <summary>Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.</summary>
private static uint ToOrbisButtons(HostGamepadButtons buttons)
{
uint result = 0;
if ((buttons & HostGamepadButtons.Create) != 0) result |= OrbisPadButton.Share;
if ((buttons & HostGamepadButtons.Up) != 0) result |= OrbisPadButton.Up;
if ((buttons & HostGamepadButtons.Down) != 0) result |= OrbisPadButton.Down;
if ((buttons & HostGamepadButtons.Left) != 0) result |= OrbisPadButton.Left;
if ((buttons & HostGamepadButtons.Right) != 0) result |= OrbisPadButton.Right;
if ((buttons & HostGamepadButtons.Cross) != 0) result |= OrbisPadButton.Cross;
if ((buttons & HostGamepadButtons.Circle) != 0) result |= OrbisPadButton.Circle;
if ((buttons & HostGamepadButtons.Square) != 0) result |= OrbisPadButton.Square;
if ((buttons & HostGamepadButtons.Triangle) != 0) result |= OrbisPadButton.Triangle;
if ((buttons & HostGamepadButtons.L1) != 0) result |= OrbisPadButton.L1;
if ((buttons & HostGamepadButtons.R1) != 0) result |= OrbisPadButton.R1;
if ((buttons & HostGamepadButtons.L2) != 0) result |= OrbisPadButton.L2;
if ((buttons & HostGamepadButtons.R2) != 0) result |= OrbisPadButton.R2;
if ((buttons & HostGamepadButtons.L3) != 0) result |= OrbisPadButton.L3;
if ((buttons & HostGamepadButtons.R3) != 0) result |= OrbisPadButton.R3;
if ((buttons & HostGamepadButtons.Options) != 0) result |= OrbisPadButton.Options;
if ((buttons & HostGamepadButtons.TouchPad) != 0) result |= OrbisPadButton.TouchPad;
return result;
}
private static void WriteTouchData(Span<byte> data, HostTouchState touch)
{
Span<HostTouchPoint> active = stackalloc HostTouchPoint[2];
var count = 0;
if (touch.First.Active)
{
active[count++] = touch.First;
}
if (touch.Second.Active)
{
active[count++] = touch.Second;
}
data[0x34] = (byte)count;
for (var index = 0; index < count; index++)
{
var offset = 0x3C + (index * 8);
var point = active[index];
BinaryPrimitives.WriteUInt16LittleEndian(
data[offset..],
(ushort)Math.Round(Math.Clamp(point.X, 0, 1) * 1919));
BinaryPrimitives.WriteUInt16LittleEndian(
data[(offset + 2)..],
(ushort)Math.Round(Math.Clamp(point.Y, 0, 1) * 942));
data[offset + 4] = point.Id;
}
}
private static uint ReadKeyboardButtons(IHostInput input)
{
uint buttons = 0;
// D-pad
if (input.IsKeyDown(0x25)) buttons |= OrbisPadButton.Left;
if (input.IsKeyDown(0x27)) buttons |= OrbisPadButton.Right;
if (input.IsKeyDown(0x26)) buttons |= OrbisPadButton.Up;
if (input.IsKeyDown(0x28)) buttons |= OrbisPadButton.Down;
// Face buttons
if (input.IsKeyDown(0x5A) || input.IsKeyDown(0x0D)) buttons |= OrbisPadButton.Cross; // Z / Enter
if (input.IsKeyDown(0x58) || input.IsKeyDown(0x1B)) buttons |= OrbisPadButton.Circle; // X / Escape
if (input.IsKeyDown(0x43)) buttons |= OrbisPadButton.Square; // C
if (input.IsKeyDown(0x56)) buttons |= OrbisPadButton.Triangle; // V
// Shoulder buttons
if (input.IsKeyDown(0x51)) buttons |= OrbisPadButton.L1; // Q
if (input.IsKeyDown(0x45)) buttons |= OrbisPadButton.R1; // E
if (input.IsKeyDown(0x52)) buttons |= OrbisPadButton.L2; // R (digital)
if (input.IsKeyDown(0x46)) buttons |= OrbisPadButton.R2; // F (digital)
// Options (Start)
if (input.IsKeyDown(0x09) || input.IsKeyDown(0x08)) buttons |= OrbisPadButton.Options; // Tab / Backspace
return buttons;
}
private static byte ReadAnalogStick(bool negative, bool positive)
{
if (negative && !positive) return 0;
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;
}
}