mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-07 10:29:46 +08:00
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
This commit is contained in:
@@ -2,138 +2,135 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
/// <summary>
|
||||
/// Keyboard and gamepad state sampled from the presenter's window, feeding
|
||||
/// the POSIX host input seam (macOS/Linux have no user32/XInput/raw-HID
|
||||
/// readers). The presenter attaches the window's input context once the
|
||||
/// window exists; input events arrive on the window thread and pad reads
|
||||
/// happen on guest threads, so all state is guarded.
|
||||
/// </summary>
|
||||
/// <summary>Cross-platform input state supplied by the SDL game window.</summary>
|
||||
public static class HostWindowInput
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static readonly HashSet<Key> Pressed = new();
|
||||
private static volatile bool _connected;
|
||||
|
||||
// Latest window-gamepad snapshot in the host seam's conventions.
|
||||
private static readonly HashSet<int> PressedKeys = new();
|
||||
private static bool _focused;
|
||||
private static bool _gamepadConnected;
|
||||
private static string? _gamepadName;
|
||||
private static HostGamepadButtons _gamepadButtons;
|
||||
private static byte _gamepadLeftX = 128;
|
||||
private static byte _gamepadLeftY = 128;
|
||||
private static byte _gamepadRightX = 128;
|
||||
private static byte _gamepadRightY = 128;
|
||||
private static byte _gamepadL2;
|
||||
private static byte _gamepadR2;
|
||||
private static HostGamepadState _gamepadState;
|
||||
private static IHostGamepadOutput? _gamepadOutput;
|
||||
private static readonly WindowInputSource Source = new();
|
||||
|
||||
/// <summary>True once a window keyboard is delivering events.</summary>
|
||||
public static bool IsConnected => _connected;
|
||||
|
||||
public static void Attach(IInputContext input)
|
||||
{
|
||||
foreach (var keyboard in input.Keyboards)
|
||||
{
|
||||
keyboard.KeyDown += (_, key, _) =>
|
||||
{
|
||||
if (key == Key.F1)
|
||||
{
|
||||
VideoOut.PerfOverlay.Toggle();
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
Pressed.Add(key);
|
||||
}
|
||||
};
|
||||
keyboard.KeyUp += (_, key, _) =>
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
Pressed.Remove(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (input.Keyboards.Count > 0)
|
||||
{
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
foreach (var gamepad in input.Gamepads)
|
||||
{
|
||||
AttachGamepad(gamepad);
|
||||
}
|
||||
|
||||
input.ConnectionChanged += (device, connected) =>
|
||||
{
|
||||
if (device is not IGamepad gamepad)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (connected)
|
||||
{
|
||||
AttachGamepad(gamepad);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadConnected = false;
|
||||
_gamepadName = null;
|
||||
_gamepadButtons = HostGamepadButtons.None;
|
||||
_gamepadLeftX = 128;
|
||||
_gamepadLeftY = 128;
|
||||
_gamepadRightX = 128;
|
||||
_gamepadRightY = 128;
|
||||
_gamepadL2 = 0;
|
||||
_gamepadR2 = 0;
|
||||
}
|
||||
};
|
||||
|
||||
PosixHostInput.SetSource(new WindowInputSource());
|
||||
}
|
||||
|
||||
public static bool IsKeyDown(Key key)
|
||||
public static void Connect(IHostGamepadOutput? gamepadOutput = null)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Pressed.Contains(key);
|
||||
_focused = true;
|
||||
_gamepadOutput = gamepadOutput;
|
||||
PressedKeys.Clear();
|
||||
}
|
||||
|
||||
HostWindowInputSource.Set(Source);
|
||||
}
|
||||
|
||||
public static void Disconnect()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_focused = false;
|
||||
_gamepadConnected = false;
|
||||
_gamepadName = null;
|
||||
_gamepadState = default;
|
||||
_gamepadOutput = null;
|
||||
PressedKeys.Clear();
|
||||
}
|
||||
|
||||
HostWindowInputSource.Clear(Source);
|
||||
}
|
||||
|
||||
public static void SetFocused(bool focused)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_focused = focused;
|
||||
if (!focused)
|
||||
{
|
||||
PressedKeys.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WindowInputSource : IPosixWindowInputSource
|
||||
public static void SetKey(int virtualKey, bool down)
|
||||
{
|
||||
public bool HasKeyboardFocus => _connected;
|
||||
lock (Gate)
|
||||
{
|
||||
if (down)
|
||||
{
|
||||
PressedKeys.Add(virtualKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
PressedKeys.Remove(virtualKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetGamepad(string? name, HostGamepadState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadConnected = state.Connected;
|
||||
_gamepadName = name;
|
||||
_gamepadState = state;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearGamepad()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadConnected = false;
|
||||
_gamepadName = null;
|
||||
_gamepadState = default;
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte ToStickByte(short value)
|
||||
{
|
||||
var normalized = value + 32768;
|
||||
return (byte)Math.Clamp((normalized * 255 + 32767) / 65535, 0, 255);
|
||||
}
|
||||
|
||||
internal static byte ToTriggerByte(short value) =>
|
||||
(byte)Math.Clamp(value * 255 / 32767, 0, 255);
|
||||
|
||||
private sealed class WindowInputSource : IHostWindowInputSource
|
||||
{
|
||||
public bool HasKeyboardFocus
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return _focused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsKeyDown(int virtualKey)
|
||||
{
|
||||
return TryMapVirtualKey(virtualKey, out var key) && HostWindowInput.IsKeyDown(key);
|
||||
lock (Gate)
|
||||
{
|
||||
return PressedKeys.Contains(virtualKey);
|
||||
}
|
||||
}
|
||||
|
||||
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (!_gamepadConnected || destination.Length == 0)
|
||||
if (!_gamepadConnected || destination.IsEmpty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
destination[0] = new HostGamepadState(
|
||||
Connected: true,
|
||||
Buttons: _gamepadButtons,
|
||||
LeftX: _gamepadLeftX,
|
||||
LeftY: _gamepadLeftY,
|
||||
RightX: _gamepadRightX,
|
||||
RightY: _gamepadRightY,
|
||||
LeftTrigger: _gamepadL2,
|
||||
RightTrigger: _gamepadR2);
|
||||
destination[0] = _gamepadState;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -142,139 +139,80 @@ public static class HostWindowInput
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return _gamepadConnected ? _gamepadName ?? "GLFW gamepad" : null;
|
||||
return _gamepadConnected ? _gamepadName ?? "SDL gamepad" : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMapVirtualKey(int vk, out Key key)
|
||||
{
|
||||
key = vk switch
|
||||
public void SetRumble(byte largeMotor, byte smallMotor)
|
||||
{
|
||||
0x08 => Key.Backspace,
|
||||
0x09 => Key.Tab,
|
||||
0x0D => Key.Enter,
|
||||
0x1B => Key.Escape,
|
||||
0x25 => Key.Left,
|
||||
0x26 => Key.Up,
|
||||
0x27 => Key.Right,
|
||||
0x28 => Key.Down,
|
||||
>= 0x41 and <= 0x5A => Key.A + (vk - 0x41),
|
||||
_ => Key.Unknown,
|
||||
};
|
||||
return key != Key.Unknown;
|
||||
}
|
||||
IHostGamepadOutput? output;
|
||||
lock (Gate)
|
||||
{
|
||||
output = _gamepadOutput;
|
||||
}
|
||||
|
||||
private static void AttachGamepad(IGamepad gamepad)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadConnected = true;
|
||||
_gamepadName = gamepad.Name;
|
||||
output?.SetRumble(largeMotor, smallMotor);
|
||||
}
|
||||
|
||||
gamepad.ButtonDown += (_, button) =>
|
||||
public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger)
|
||||
{
|
||||
var bit = MapButton(button.Name);
|
||||
if (bit == HostGamepadButtons.None)
|
||||
IHostGamepadOutput? output;
|
||||
lock (Gate)
|
||||
{
|
||||
return;
|
||||
output = _gamepadOutput;
|
||||
}
|
||||
|
||||
output?.SetTriggerRumble(leftTrigger, rightTrigger);
|
||||
}
|
||||
|
||||
public void SetAdaptiveTriggerEffect(
|
||||
HostAdaptiveTriggerEffect? leftTrigger,
|
||||
HostAdaptiveTriggerEffect? rightTrigger)
|
||||
{
|
||||
IHostGamepadOutput? output;
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadButtons |= bit;
|
||||
}
|
||||
};
|
||||
gamepad.ButtonUp += (_, button) =>
|
||||
{
|
||||
var bit = MapButton(button.Name);
|
||||
if (bit == HostGamepadButtons.None)
|
||||
{
|
||||
return;
|
||||
output = _gamepadOutput;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
_gamepadButtons &= ~bit;
|
||||
}
|
||||
};
|
||||
gamepad.ThumbstickMoved += (_, thumbstick) =>
|
||||
output?.SetAdaptiveTriggerEffect(leftTrigger, rightTrigger);
|
||||
}
|
||||
|
||||
public void SetLightbar(byte red, byte green, byte blue)
|
||||
{
|
||||
// Silk's GLFW backend reports sticks -1..1 with +Y pointing down,
|
||||
// matching the seam's 0..255 down-growing convention after biasing.
|
||||
var x = ToStickByte(thumbstick.X);
|
||||
var y = ToStickByte(thumbstick.Y);
|
||||
IHostGamepadOutput? output;
|
||||
lock (Gate)
|
||||
{
|
||||
if (thumbstick.Index == 0)
|
||||
{
|
||||
_gamepadLeftX = x;
|
||||
_gamepadLeftY = y;
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadRightX = x;
|
||||
_gamepadRightY = y;
|
||||
}
|
||||
output = _gamepadOutput;
|
||||
}
|
||||
};
|
||||
gamepad.TriggerMoved += (_, trigger) =>
|
||||
|
||||
output?.SetLightbar(red, green, blue);
|
||||
}
|
||||
|
||||
public void ResetLightbar()
|
||||
{
|
||||
// GLFW gamepad triggers rest at -1 and saturate at +1.
|
||||
var value = (byte)Math.Clamp((int)((trigger.Position + 1.0f) * 0.5f * 255.0f), 0, 255);
|
||||
IHostGamepadOutput? output;
|
||||
lock (Gate)
|
||||
{
|
||||
if (trigger.Index == 0)
|
||||
{
|
||||
_gamepadL2 = value;
|
||||
if (value > 64)
|
||||
{
|
||||
_gamepadButtons |= HostGamepadButtons.L2;
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadButtons &= ~HostGamepadButtons.L2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadR2 = value;
|
||||
if (value > 64)
|
||||
{
|
||||
_gamepadButtons |= HostGamepadButtons.R2;
|
||||
}
|
||||
else
|
||||
{
|
||||
_gamepadButtons &= ~HostGamepadButtons.R2;
|
||||
}
|
||||
}
|
||||
output = _gamepadOutput;
|
||||
}
|
||||
};
|
||||
|
||||
output?.ResetLightbar();
|
||||
}
|
||||
}
|
||||
|
||||
internal static byte ToStickByte(float value)
|
||||
{
|
||||
return (byte)Math.Clamp((int)MathF.Round((value + 1.0f) * 127.5f), 0, 255);
|
||||
}
|
||||
|
||||
private static HostGamepadButtons MapButton(ButtonName name) => name switch
|
||||
{
|
||||
// GLFW reports the Xbox layout: A=Cross, B=Circle, X=Square, Y=Triangle.
|
||||
ButtonName.A => HostGamepadButtons.Cross,
|
||||
ButtonName.B => HostGamepadButtons.Circle,
|
||||
ButtonName.X => HostGamepadButtons.Square,
|
||||
ButtonName.Y => HostGamepadButtons.Triangle,
|
||||
ButtonName.LeftBumper => HostGamepadButtons.L1,
|
||||
ButtonName.RightBumper => HostGamepadButtons.R1,
|
||||
ButtonName.Back => HostGamepadButtons.TouchPad,
|
||||
ButtonName.Start => HostGamepadButtons.Options,
|
||||
ButtonName.LeftStick => HostGamepadButtons.L3,
|
||||
ButtonName.RightStick => HostGamepadButtons.R3,
|
||||
ButtonName.DPadUp => HostGamepadButtons.Up,
|
||||
ButtonName.DPadRight => HostGamepadButtons.Right,
|
||||
ButtonName.DPadDown => HostGamepadButtons.Down,
|
||||
ButtonName.DPadLeft => HostGamepadButtons.Left,
|
||||
_ => HostGamepadButtons.None,
|
||||
};
|
||||
}
|
||||
|
||||
public interface IHostGamepadOutput
|
||||
{
|
||||
void SetRumble(byte largeMotor, byte smallMotor);
|
||||
|
||||
void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger);
|
||||
|
||||
void SetAdaptiveTriggerEffect(
|
||||
HostAdaptiveTriggerEffect? leftTrigger,
|
||||
HostAdaptiveTriggerEffect? rightTrigger);
|
||||
|
||||
void SetLightbar(byte red, byte green, byte blue);
|
||||
|
||||
void ResetLightbar();
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ public static class PadExports
|
||||
private static PadState _cachedInputState;
|
||||
|
||||
private static bool _initialized;
|
||||
private static int _motionSensorEnabled;
|
||||
private static int _controlsAnnouncementLogged;
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -151,9 +152,13 @@ public static class PadExports
|
||||
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
Volatile.Write(ref _motionSensorEnabled, ctx[CpuRegister.Rsi] != 0 ? 1 : 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -362,24 +367,170 @@ public static class PadExports
|
||||
}
|
||||
|
||||
var triggerMask = parameter[0];
|
||||
HostPlatform.Current.Input.SetTriggerRumble(
|
||||
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
||||
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
||||
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 byte DecodeTriggerVibration(ReadOnlySpan<byte> command)
|
||||
private static HostAdaptiveTriggerEffect DecodeTriggerEffect(ReadOnlySpan<byte> command)
|
||||
{
|
||||
var mode = BinaryPrimitives.ReadUInt32LittleEndian(command);
|
||||
var amplitude = mode switch
|
||||
var parameters = command[8..];
|
||||
Span<byte> native = stackalloc byte[11];
|
||||
native.Clear();
|
||||
byte fallbackStrength = 0;
|
||||
switch (mode)
|
||||
{
|
||||
3 when command[10] != 0 => command[9],
|
||||
6 when command[8] != 0 => command[9..19].ToArray().Max(),
|
||||
_ => (byte)0,
|
||||
};
|
||||
return (byte)(Math.Min(amplitude, (byte)8) * 255 / 8);
|
||||
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",
|
||||
@@ -478,6 +629,17 @@ public static class PadExports
|
||||
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 =
|
||||
@@ -508,6 +670,10 @@ public static class PadExports
|
||||
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);
|
||||
@@ -523,6 +689,13 @@ public static class PadExports
|
||||
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())
|
||||
@@ -538,7 +711,11 @@ public static class PadExports
|
||||
RightX: rightX,
|
||||
RightY: rightY,
|
||||
L2: l2,
|
||||
R2: r2);
|
||||
R2: r2,
|
||||
Type: gamepadType,
|
||||
Connection: connection,
|
||||
Motion: motion,
|
||||
Touch: touch);
|
||||
_lastInputSampleTicks = now;
|
||||
return _cachedInputState;
|
||||
}
|
||||
@@ -592,6 +769,7 @@ public static class PadExports
|
||||
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;
|
||||
@@ -611,6 +789,34 @@ public static class PadExports
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.Host;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,11 +18,16 @@ internal readonly record struct PadState(
|
||||
byte RightX,
|
||||
byte RightY,
|
||||
byte L2,
|
||||
byte R2);
|
||||
byte R2,
|
||||
HostGamepadType Type = HostGamepadType.Generic,
|
||||
HostGamepadConnection Connection = HostGamepadConnection.Unknown,
|
||||
HostMotionState Motion = default,
|
||||
HostTouchState Touch = default);
|
||||
|
||||
/// <summary>SCE_PAD_BUTTON bit values.</summary>
|
||||
internal static class OrbisPadButton
|
||||
{
|
||||
internal const uint Share = 0x0001;
|
||||
internal const uint L3 = 0x0002;
|
||||
internal const uint R3 = 0x0004;
|
||||
internal const uint Options = 0x0008;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.Host;
|
||||
using SDL;
|
||||
using static SDL.SDL3;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
internal static unsafe class SdlGamepadStateReader
|
||||
{
|
||||
public static void EnableSonyHidApi()
|
||||
{
|
||||
fixed (byte* enabled = "1"u8)
|
||||
fixed (byte* ps4 = SDL_HINT_JOYSTICK_HIDAPI_PS4)
|
||||
fixed (byte* ps5 = SDL_HINT_JOYSTICK_HIDAPI_PS5)
|
||||
{
|
||||
SDL_SetHint(ps4, enabled);
|
||||
SDL_SetHint(ps5, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public static SDL_Gamepad* OpenPreferredGamepad()
|
||||
{
|
||||
using var gamepads = SDL_GetGamepads();
|
||||
if (gamepads is null || gamepads.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var selected = gamepads[0];
|
||||
for (var index = 0; index < gamepads.Count; index++)
|
||||
{
|
||||
var type = SDL_GetRealGamepadTypeForID(gamepads[index]);
|
||||
if (type is SDL_GamepadType.SDL_GAMEPAD_TYPE_PS5 or SDL_GamepadType.SDL_GAMEPAD_TYPE_PS4)
|
||||
{
|
||||
selected = gamepads[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return SDL_OpenGamepad(selected);
|
||||
}
|
||||
|
||||
public static HostGamepadState Read(SDL_Gamepad* gamepad)
|
||||
{
|
||||
var buttons = HostGamepadButtons.None;
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH, HostGamepadButtons.Cross, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST, HostGamepadButtons.Circle, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST, HostGamepadButtons.Square, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH, HostGamepadButtons.Triangle, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, HostGamepadButtons.L1, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, HostGamepadButtons.R1, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK, HostGamepadButtons.L3, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_STICK, HostGamepadButtons.R3, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_BACK, HostGamepadButtons.Create, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_GUIDE, HostGamepadButtons.Ps, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START, HostGamepadButtons.Options, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_MISC1, HostGamepadButtons.Mic, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_TOUCHPAD, HostGamepadButtons.TouchPad, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_UP, HostGamepadButtons.Up, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_RIGHT, HostGamepadButtons.Right, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_DOWN, HostGamepadButtons.Down, ref buttons);
|
||||
AddButton(gamepad, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_LEFT, HostGamepadButtons.Left, ref buttons);
|
||||
|
||||
var leftTrigger = HostWindowInput.ToTriggerByte(
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFT_TRIGGER));
|
||||
var rightTrigger = HostWindowInput.ToTriggerByte(
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
|
||||
if (leftTrigger > 64)
|
||||
{
|
||||
buttons |= HostGamepadButtons.L2;
|
||||
}
|
||||
if (rightTrigger > 64)
|
||||
{
|
||||
buttons |= HostGamepadButtons.R2;
|
||||
}
|
||||
|
||||
var battery = 0;
|
||||
SDL_GetGamepadPowerInfo(gamepad, &battery);
|
||||
return new HostGamepadState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
LeftX: HostWindowInput.ToStickByte(SDL_GetGamepadAxis(gamepad, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTX)),
|
||||
LeftY: HostWindowInput.ToStickByte(SDL_GetGamepadAxis(gamepad, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTY)),
|
||||
RightX: HostWindowInput.ToStickByte(SDL_GetGamepadAxis(gamepad, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTX)),
|
||||
RightY: HostWindowInput.ToStickByte(SDL_GetGamepadAxis(gamepad, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTY)),
|
||||
LeftTrigger: leftTrigger,
|
||||
RightTrigger: rightTrigger,
|
||||
Type: MapGamepadType(SDL_GetRealGamepadType(gamepad)),
|
||||
Connection: GetConnection(gamepad),
|
||||
BatteryPercent: (byte)Math.Clamp(battery, 0, 100));
|
||||
}
|
||||
|
||||
public static HostGamepadType MapGamepadType(SDL_GamepadType type) => type switch
|
||||
{
|
||||
SDL_GamepadType.SDL_GAMEPAD_TYPE_PS4 => HostGamepadType.DualShock4,
|
||||
SDL_GamepadType.SDL_GAMEPAD_TYPE_PS5 => HostGamepadType.DualSense,
|
||||
_ => HostGamepadType.Generic,
|
||||
};
|
||||
|
||||
public static HostGamepadConnection GetConnection(SDL_Gamepad* gamepad) =>
|
||||
SDL_GetGamepadConnectionState(gamepad) switch
|
||||
{
|
||||
SDL_JoystickConnectionState.SDL_JOYSTICK_CONNECTION_WIRED => HostGamepadConnection.Wired,
|
||||
SDL_JoystickConnectionState.SDL_JOYSTICK_CONNECTION_WIRELESS => HostGamepadConnection.Wireless,
|
||||
_ => HostGamepadConnection.Unknown,
|
||||
};
|
||||
|
||||
private static void AddButton(
|
||||
SDL_Gamepad* gamepad,
|
||||
SDL_GamepadButton source,
|
||||
HostGamepadButtons target,
|
||||
ref HostGamepadButtons buttons)
|
||||
{
|
||||
if (SDL_GetGamepadButton(gamepad, source))
|
||||
{
|
||||
buttons |= target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE.Host;
|
||||
using SDL;
|
||||
using static SDL.SDL3;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
/// <summary>Cross-platform SDL gamepad polling for launcher navigation.</summary>
|
||||
public static unsafe class SdlLauncherGamepad
|
||||
{
|
||||
private const SDL_InitFlags InitFlags = SDL_InitFlags.SDL_INIT_GAMEPAD;
|
||||
private static SDL_Gamepad* _gamepad;
|
||||
private static bool _initialized;
|
||||
|
||||
public static void EnsureStarted()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SdlGamepadStateReader.EnableSonyHidApi();
|
||||
if (!SDL_InitSubSystem(InitFlags))
|
||||
{
|
||||
Console.Error.WriteLine("[GUI][WARN] SDL gamepad initialization failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
OpenFirstGamepad();
|
||||
}
|
||||
|
||||
public static bool TryGetState(out HostGamepadState state)
|
||||
{
|
||||
state = default;
|
||||
if (!_initialized)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_UpdateGamepads();
|
||||
if (_gamepad is not null && !SDL_GamepadConnected(_gamepad))
|
||||
{
|
||||
SDL_CloseGamepad(_gamepad);
|
||||
_gamepad = null;
|
||||
}
|
||||
|
||||
if (_gamepad is null)
|
||||
{
|
||||
OpenFirstGamepad();
|
||||
}
|
||||
|
||||
if (_gamepad is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
state = SdlGamepadStateReader.Read(_gamepad);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gamepad is not null)
|
||||
{
|
||||
SDL_CloseGamepad(_gamepad);
|
||||
_gamepad = null;
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(InitFlags);
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
private static void OpenFirstGamepad()
|
||||
{
|
||||
_gamepad = SdlGamepadStateReader.OpenPreferredGamepad();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user