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:
Berk
2026-07-28 03:33:26 +03:00
committed by GitHub
parent b4cc5f88ca
commit 2b6bd5a532
111 changed files with 9846 additions and 4479 deletions
+152 -214
View File
@@ -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();
}