diff --git a/src/SharpEmu.GUI/MainWindow.axaml.cs b/src/SharpEmu.GUI/MainWindow.axaml.cs
index 9e30df80..2b93c1dd 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml.cs
+++ b/src/SharpEmu.GUI/MainWindow.axaml.cs
@@ -12,7 +12,8 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
-using SharpEmu.Libs.Pad;
+using SharpEmu.HLE.Host;
+using SharpEmu.HLE.Host.Windows;
using SharpEmu.Logging;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
@@ -62,7 +63,7 @@ public partial class MainWindow : Window
// Controller navigation state.
private readonly DispatcherTimer _gamepadTimer;
- private uint _previousPadButtons;
+ private HostGamepadButtons _previousPadButtons;
private long _navLeftNextAt;
private long _navRightNextAt;
private long _navUpNextAt;
@@ -151,8 +152,8 @@ public partial class MainWindow : Window
Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => OnWindowClosing();
- DualSenseReader.EnsureStarted();
- XInputReader.EnsureStarted();
+ WindowsDualSenseReader.EnsureStarted();
+ WindowsXInputReader.EnsureStarted();
_gamepadTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(50),
@@ -224,9 +225,9 @@ public partial class MainWindow : Window
private void PollGamepad()
{
// DualSense wins when both are connected; XInput covers Xbox pads.
- if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad))
+ if (!WindowsDualSenseReader.TryGetState(out var pad) && !WindowsXInputReader.TryGetState(out pad))
{
- _previousPadButtons = 0;
+ _previousPadButtons = HostGamepadButtons.None;
return;
}
@@ -239,12 +240,12 @@ public partial class MainWindow : Window
}
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
- if ((shoulderPressed & OrbisPadButton.L1) != 0)
+ if ((shoulderPressed & HostGamepadButtons.L1) != 0)
{
SetActivePage(0);
}
- if ((shoulderPressed & OrbisPadButton.R1) != 0)
+ if ((shoulderPressed & HostGamepadButtons.R1) != 0)
{
SetActivePage(1);
}
@@ -256,10 +257,10 @@ public partial class MainWindow : Window
}
var now = Environment.TickCount64;
- var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
- var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
- var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
- var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
+ var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
+ var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
+ var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
+ var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
if (ShouldNavigate(left, ref _navLeftNextAt, now))
{
@@ -282,12 +283,12 @@ public partial class MainWindow : Window
}
var pressed = pad.Buttons & ~_previousPadButtons;
- if ((pressed & 0x4000) != 0) // Cross
+ if ((pressed & HostGamepadButtons.Cross) != 0)
{
LaunchSelected();
}
- if ((pressed & 0x2000) != 0) // Circle
+ if ((pressed & HostGamepadButtons.Circle) != 0)
{
StopEmulator();
}
diff --git a/src/SharpEmu.GUI/SharpEmu.GUI.csproj b/src/SharpEmu.GUI/SharpEmu.GUI.csproj
index 9eff14d6..86415a5a 100644
--- a/src/SharpEmu.GUI/SharpEmu.GUI.csproj
+++ b/src/SharpEmu.GUI/SharpEmu.GUI.csproj
@@ -10,6 +10,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
false
0.0.1
+
+ true
+ with the emulator's host input backend. They are dependency-free, so
+ they are compiled in directly rather than pulling a reference to all
+ of SharpEmu.HLE into the launcher. -->
-
-
-
-
+
+
+
+
diff --git a/src/SharpEmu.HLE/Host/HostGamepadState.cs b/src/SharpEmu.HLE/Host/HostGamepadState.cs
new file mode 100644
index 00000000..79760dca
--- /dev/null
+++ b/src/SharpEmu.HLE/Host/HostGamepadState.cs
@@ -0,0 +1,46 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.HLE.Host;
+
+///
+/// Host-neutral gamepad button flags. Named after the PlayStation layout the guest API
+/// exposes, but the numeric values are the seam's own — the HLE pad exports translate
+/// them to SCE_PAD_BUTTON bits, so guest ABI values never leak into host backends.
+///
+[Flags]
+public enum HostGamepadButtons : uint
+{
+ None = 0,
+ Up = 1 << 0,
+ Down = 1 << 1,
+ Left = 1 << 2,
+ Right = 1 << 3,
+ Cross = 1 << 4,
+ Circle = 1 << 5,
+ Square = 1 << 6,
+ Triangle = 1 << 7,
+ L1 = 1 << 8,
+ R1 = 1 << 9,
+ L2 = 1 << 10,
+ R2 = 1 << 11,
+ L3 = 1 << 12,
+ R3 = 1 << 13,
+ Options = 1 << 14,
+ TouchPad = 1 << 15,
+}
+
+///
+/// Snapshot of one host gamepad: sticks are 0..255 with 128 centered and Y growing
+/// downward; triggers 0..255. Unmanaged on purpose so per-frame polls can stackalloc
+/// snapshot buffers.
+///
+public readonly record struct HostGamepadState(
+ bool Connected,
+ HostGamepadButtons Buttons,
+ byte LeftX,
+ byte LeftY,
+ byte RightX,
+ byte RightY,
+ byte LeftTrigger,
+ byte RightTrigger);
diff --git a/src/SharpEmu.HLE/Host/IHostAudioOutput.cs b/src/SharpEmu.HLE/Host/IHostAudioOutput.cs
new file mode 100644
index 00000000..2f84fe57
--- /dev/null
+++ b/src/SharpEmu.HLE/Host/IHostAudioOutput.cs
@@ -0,0 +1,23 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.HLE.Host;
+
+///
+/// Host audio-output device access. The HLE audio exports convert guest submissions to
+/// interleaved stereo 16-bit PCM (the format every backend accepts) and feed them through
+/// streams opened here; everything device-specific — queueing, backpressure, native
+/// buffer lifetime — lives behind .
+///
+public interface IHostAudioOutput
+{
+ /// Backend identifier for diagnostics (e.g. "winmm").
+ string BackendName { get; }
+
+ ///
+ /// Opens an interleaved stereo 16-bit PCM output stream at the given sample rate.
+ /// Throws when the host has no usable output device; callers degrade to a silent
+ /// port and pace the guest instead.
+ ///
+ IHostAudioStream OpenStereoPcm16Stream(uint sampleRate);
+}
diff --git a/src/SharpEmu.HLE/Host/IHostAudioStream.cs b/src/SharpEmu.HLE/Host/IHostAudioStream.cs
new file mode 100644
index 00000000..2db99abb
--- /dev/null
+++ b/src/SharpEmu.HLE/Host/IHostAudioStream.cs
@@ -0,0 +1,18 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.HLE.Host;
+
+///
+/// One open host audio output stream. Submissions are interleaved stereo 16-bit PCM at
+/// the sample rate the stream was opened with.
+///
+public interface IHostAudioStream : IDisposable
+{
+ ///
+ /// Submits one buffer. May block briefly while the device drains its queue (this is
+ /// what paces the guest's audio loop); returns false when the stream cannot accept
+ /// audio, in which case the caller paces the guest itself.
+ ///
+ bool Submit(ReadOnlySpan stereoPcm16);
+}
diff --git a/src/SharpEmu.HLE/Host/IHostInput.cs b/src/SharpEmu.HLE/Host/IHostInput.cs
new file mode 100644
index 00000000..52779bcf
--- /dev/null
+++ b/src/SharpEmu.HLE/Host/IHostInput.cs
@@ -0,0 +1,44 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.HLE.Host;
+
+///
+/// Host input devices: gamepad state snapshots, force-feedback/lightbar sinks, and the
+/// keyboard-fallback queries. Which physical readers exist (DualSense over raw HID,
+/// XInput, evdev, ...) is a backend detail; merge policy between devices and the
+/// keyboard lives in the HLE pad exports.
+///
+public interface IHostInput
+{
+ /// Starts the background device readers once; safe to call repeatedly.
+ void EnsureStarted();
+
+ ///
+ /// Fills with snapshots of currently connected
+ /// gamepads and returns how many were written (0 when none are connected).
+ ///
+ int GetGamepadStates(Span destination);
+
+ /// Human-readable name of the first connected gamepad, or null.
+ string? DescribeConnectedGamepad();
+
+ /// Sets rumble on all connected gamepads; large = strong/left motor.
+ void SetRumble(byte largeMotor, byte smallMotor);
+
+ ///
+ /// Approximates per-trigger vibration on gamepads without independent trigger
+ /// actuators; null leaves that trigger's current value unchanged.
+ ///
+ void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger);
+
+ void SetLightbar(byte red, byte green, byte blue);
+
+ void ResetLightbar();
+
+ /// True when a window of this process has keyboard focus.
+ bool IsHostWindowFocused();
+
+ /// Windows virtual-key code semantics; other backends translate.
+ bool IsKeyDown(int virtualKey);
+}
diff --git a/src/SharpEmu.HLE/Host/IHostPlatform.cs b/src/SharpEmu.HLE/Host/IHostPlatform.cs
index 3dfeac60..05575c96 100644
--- a/src/SharpEmu.HLE/Host/IHostPlatform.cs
+++ b/src/SharpEmu.HLE/Host/IHostPlatform.cs
@@ -16,4 +16,8 @@ public interface IHostPlatform
IHostThreading Threading { get; }
IHostSymbolResolver Symbols { get; }
+
+ IHostAudioOutput Audio { get; }
+
+ IHostInput Input { get; }
}
diff --git a/src/SharpEmu.HLE/Host/IHostThreading.cs b/src/SharpEmu.HLE/Host/IHostThreading.cs
index 85cc0c9e..57a4b8e1 100644
--- a/src/SharpEmu.HLE/Host/IHostThreading.cs
+++ b/src/SharpEmu.HLE/Host/IHostThreading.cs
@@ -24,6 +24,12 @@ public interface IHostThreading
bool TrySetCurrentThreadAffinity(nuint affinityMask);
+ ///
+ /// Asks the OS for ~1 ms timed-wait granularity for the life of the process
+ /// (idempotent; best-effort). No-op on platforms whose default is already fine.
+ ///
+ void RequestTimerResolution();
+
///
/// Creates a raw OS thread executing native code at with
/// of reserved (not committed) stack.
diff --git a/src/SharpEmu.Libs/Pad/DualSenseReader.cs b/src/SharpEmu.HLE/Host/Windows/WindowsDualSenseReader.cs
similarity index 78%
rename from src/SharpEmu.Libs/Pad/DualSenseReader.cs
rename to src/SharpEmu.HLE/Host/Windows/WindowsDualSenseReader.cs
index 7094cdcc..5b49d545 100644
--- a/src/SharpEmu.Libs/Pad/DualSenseReader.cs
+++ b/src/SharpEmu.HLE/Host/Windows/WindowsDualSenseReader.cs
@@ -3,21 +3,21 @@
using Microsoft.Win32.SafeHandles;
-namespace SharpEmu.Libs.Pad;
+namespace SharpEmu.HLE.Host.Windows;
///
/// 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
+internal static class WindowsDualSenseReader
{
private const ushort SonyVendorId = 0x054C;
private const ushort DualSenseProductId = 0x0CE6;
private const ushort DualSenseEdgeProductId = 0x0DF2;
private static readonly object Gate = new();
- private static PadState _state;
+ private static HostGamepadState _state;
private static bool _started;
// Output (rumble/lightbar) state, all guarded by Gate.
@@ -37,6 +37,8 @@ internal static class DualSenseReader
/// Starts the background reader once; safe to call repeatedly.
internal static void EnsureStarted()
{
+ // The GUI source-links this reader and calls it directly, without the
+ // host-platform resolution that otherwise guarantees Windows.
if (!OperatingSystem.IsWindows())
{
return;
@@ -59,7 +61,7 @@ internal static class DualSenseReader
}
}
- internal static bool TryGetState(out PadState state)
+ internal static bool TryGetState(out HostGamepadState state)
{
lock (Gate)
{
@@ -69,7 +71,7 @@ internal static class DualSenseReader
return state.Connected;
}
- private static void SetState(in PadState state)
+ private static void SetState(in HostGamepadState state)
{
lock (Gate)
{
@@ -148,11 +150,11 @@ internal static class DualSenseReader
{
if (_outputStream is null)
{
- var handle = HidNative.CreateFile(
+ var handle = WindowsHidNative.CreateFile(
_devicePath,
- HidNative.GenericRead | HidNative.GenericWrite,
- HidNative.FileShareRead | HidNative.FileShareWrite,
- 0, HidNative.OpenExisting, 0, 0);
+ WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
+ WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
+ 0, WindowsHidNative.OpenExisting, 0, 0);
if (handle.IsInvalid)
{
handle.Dispose();
@@ -262,7 +264,7 @@ internal static class DualSenseReader
// to the full 0x31 input report. Harmless over USB.
var feature = new byte[41];
feature[0] = 0x05;
- _ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
+ _ = WindowsHidNative.HidD_GetFeature(handle, feature, feature.Length);
if (!announcedConnect)
{
@@ -320,18 +322,18 @@ internal static class DualSenseReader
private static SafeFileHandle? OpenDualSense(out string? devicePath)
{
devicePath = null;
- foreach (var path in HidNative.EnumerateHidDevicePaths())
+ foreach (var path in WindowsHidNative.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);
+ using var probe = WindowsHidNative.CreateFile(
+ path, 0, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite, 0, WindowsHidNative.OpenExisting, 0, 0);
if (probe.IsInvalid)
{
continue;
}
- var attributes = new HidNative.HiddAttributes { Size = 12 };
- if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
+ var attributes = new WindowsHidNative.HiddAttributes { Size = 12 };
+ if (!WindowsHidNative.HidD_GetAttributes(probe, ref attributes) ||
attributes.VendorId != SonyVendorId ||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
{
@@ -339,19 +341,19 @@ internal static class DualSenseReader
}
// Read+write so feature reports work; fall back to read-only.
- var handle = HidNative.CreateFile(
+ var handle = WindowsHidNative.CreateFile(
path,
- HidNative.GenericRead | HidNative.GenericWrite,
- HidNative.FileShareRead | HidNative.FileShareWrite,
- 0, HidNative.OpenExisting, 0, 0);
+ WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
+ WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
+ 0, WindowsHidNative.OpenExisting, 0, 0);
if (handle.IsInvalid)
{
handle.Dispose();
- handle = HidNative.CreateFile(
+ handle = WindowsHidNative.CreateFile(
path,
- HidNative.GenericRead,
- HidNative.FileShareRead | HidNative.FileShareWrite,
- 0, HidNative.OpenExisting, 0, 0);
+ WindowsHidNative.GenericRead,
+ WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
+ 0, WindowsHidNative.OpenExisting, 0, 0);
}
if (!handle.IsInvalid)
@@ -366,7 +368,7 @@ internal static class DualSenseReader
return null;
}
- private static bool TryParseReport(ReadOnlySpan report, out PadState state)
+ private static bool TryParseReport(ReadOnlySpan report, out HostGamepadState state)
{
// USB: report id 0x01, payload starts at [1].
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
@@ -395,43 +397,43 @@ internal static class DualSenseReader
var buttons1 = report[offset + 8];
var buttons2 = report[offset + 9];
- uint buttons = 0;
- buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
- buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
- buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
- buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
+ var buttons = HostGamepadButtons.None;
+ buttons |= (buttons0 & 0x10) != 0 ? HostGamepadButtons.Square : 0;
+ buttons |= (buttons0 & 0x20) != 0 ? HostGamepadButtons.Cross : 0;
+ buttons |= (buttons0 & 0x40) != 0 ? HostGamepadButtons.Circle : 0;
+ buttons |= (buttons0 & 0x80) != 0 ? HostGamepadButtons.Triangle : 0;
buttons |= HatToButtons(buttons0 & 0x0F);
- buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
- buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
- buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
- buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
- buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
- buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
- buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
- buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
+ buttons |= (buttons1 & 0x01) != 0 ? HostGamepadButtons.L1 : 0;
+ buttons |= (buttons1 & 0x02) != 0 ? HostGamepadButtons.R1 : 0;
+ buttons |= (buttons1 & 0x04) != 0 ? HostGamepadButtons.L2 : 0;
+ buttons |= (buttons1 & 0x08) != 0 ? HostGamepadButtons.R2 : 0;
+ buttons |= (buttons1 & 0x20) != 0 ? HostGamepadButtons.Options : 0;
+ buttons |= (buttons1 & 0x40) != 0 ? HostGamepadButtons.L3 : 0;
+ buttons |= (buttons1 & 0x80) != 0 ? HostGamepadButtons.R3 : 0;
+ buttons |= (buttons2 & 0x02) != 0 ? HostGamepadButtons.TouchPad : 0;
- state = new PadState(
+ state = new HostGamepadState(
Connected: true,
Buttons: buttons,
LeftX: leftX,
LeftY: leftY,
RightX: rightX,
RightY: rightY,
- L2: l2,
- R2: r2);
+ LeftTrigger: l2,
+ RightTrigger: r2);
return true;
}
- private static uint HatToButtons(int hat) => hat switch
+ private static HostGamepadButtons HatToButtons(int hat) => hat switch
{
- 0 => OrbisPadButton.Up,
- 1 => OrbisPadButton.Up | OrbisPadButton.Right,
- 2 => OrbisPadButton.Right,
- 3 => OrbisPadButton.Right | OrbisPadButton.Down,
- 4 => OrbisPadButton.Down,
- 5 => OrbisPadButton.Down | OrbisPadButton.Left,
- 6 => OrbisPadButton.Left,
- 7 => OrbisPadButton.Left | OrbisPadButton.Up,
+ 0 => HostGamepadButtons.Up,
+ 1 => HostGamepadButtons.Up | HostGamepadButtons.Right,
+ 2 => HostGamepadButtons.Right,
+ 3 => HostGamepadButtons.Right | HostGamepadButtons.Down,
+ 4 => HostGamepadButtons.Down,
+ 5 => HostGamepadButtons.Down | HostGamepadButtons.Left,
+ 6 => HostGamepadButtons.Left,
+ 7 => HostGamepadButtons.Left | HostGamepadButtons.Up,
_ => 0,
};
}
diff --git a/src/SharpEmu.Libs/Pad/HidNative.cs b/src/SharpEmu.HLE/Host/Windows/WindowsHidNative.cs
similarity index 71%
rename from src/SharpEmu.Libs/Pad/HidNative.cs
rename to src/SharpEmu.HLE/Host/Windows/WindowsHidNative.cs
index b52a5897..22e8afbb 100644
--- a/src/SharpEmu.Libs/Pad/HidNative.cs
+++ b/src/SharpEmu.HLE/Host/Windows/WindowsHidNative.cs
@@ -4,13 +4,13 @@
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
-namespace SharpEmu.Libs.Pad;
+namespace SharpEmu.HLE.Host.Windows;
///
/// Minimal Win32 HID interop used to talk to a DualSense controller
/// directly, without any external input library.
///
-internal static partial class HidNative
+internal static partial class WindowsHidNative
{
internal const int DigcfPresent = 0x02;
internal const int DigcfDeviceInterface = 0x10;
@@ -38,28 +38,32 @@ internal static partial class HidNative
public ushort VersionNumber;
}
- [DllImport("hid.dll")]
- internal static extern void HidD_GetHidGuid(out Guid hidGuid);
+ [LibraryImport("hid.dll")]
+ internal static partial void HidD_GetHidGuid(out Guid hidGuid);
- [DllImport("hid.dll")]
- internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
+ [LibraryImport("hid.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
- [DllImport("hid.dll")]
- internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
+ [LibraryImport("hid.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool HidD_GetFeature(SafeFileHandle hidDeviceObject, [In, Out] byte[] reportBuffer, int reportBufferLength);
- [DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
- internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
+ [LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetClassDevsW")]
+ internal static partial nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
- [DllImport("setupapi.dll")]
- internal static extern bool SetupDiEnumDeviceInterfaces(
+ [LibraryImport("setupapi.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial 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(
+ [LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetDeviceInterfaceDetailW")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool SetupDiGetDeviceInterfaceDetail(
nint deviceInfoSet,
ref SpDeviceInterfaceData deviceInterfaceData,
nint deviceInterfaceDetailData,
@@ -67,11 +71,12 @@ internal static partial class HidNative
out int requiredSize,
nint deviceInfoData);
- [DllImport("setupapi.dll")]
- internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
+ [LibraryImport("setupapi.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
- internal static extern SafeFileHandle CreateFile(
+ [LibraryImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
+ internal static partial SafeFileHandle CreateFile(
string fileName,
uint desiredAccess,
uint shareMode,
diff --git a/src/SharpEmu.HLE/Host/Windows/WindowsHostInput.cs b/src/SharpEmu.HLE/Host/Windows/WindowsHostInput.cs
new file mode 100644
index 00000000..c2abe1f4
--- /dev/null
+++ b/src/SharpEmu.HLE/Host/Windows/WindowsHostInput.cs
@@ -0,0 +1,84 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Runtime.InteropServices;
+
+namespace SharpEmu.HLE.Host.Windows;
+
+///
+/// Windows input backend: DualSense over raw HID plus XInput controllers for gamepads,
+/// user32 for the keyboard-fallback queries. Rumble fans out to every reader; lightbar
+/// only exists on the DualSense.
+///
+internal sealed partial class WindowsHostInput : IHostInput
+{
+ public void EnsureStarted()
+ {
+ WindowsDualSenseReader.EnsureStarted();
+ WindowsXInputReader.EnsureStarted();
+ }
+
+ public int GetGamepadStates(Span destination)
+ {
+ var count = 0;
+ if (count < destination.Length && WindowsDualSenseReader.TryGetState(out var dualSense))
+ {
+ destination[count++] = dualSense;
+ }
+
+ if (count < destination.Length && WindowsXInputReader.TryGetState(out var xinput))
+ {
+ destination[count++] = xinput;
+ }
+
+ return count;
+ }
+
+ public string? DescribeConnectedGamepad()
+ {
+ if (WindowsDualSenseReader.TryGetState(out _))
+ {
+ return "DualSense";
+ }
+
+ return WindowsXInputReader.TryGetState(out _) ? "Xbox controller" : null;
+ }
+
+ public void SetRumble(byte largeMotor, byte smallMotor)
+ {
+ WindowsDualSenseReader.SetRumble(largeMotor, smallMotor);
+ WindowsXInputReader.SetRumble(largeMotor, smallMotor);
+ }
+
+ public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger) =>
+ WindowsXInputReader.SetTriggerRumble(leftTrigger, rightTrigger);
+
+ public void SetLightbar(byte red, byte green, byte blue) =>
+ WindowsDualSenseReader.SetLightbar(red, green, blue);
+
+ public void ResetLightbar() => WindowsDualSenseReader.ResetLightbar();
+
+ public bool IsHostWindowFocused()
+ {
+ var foregroundWindow = GetForegroundWindow();
+ if (foregroundWindow == 0)
+ {
+ return false;
+ }
+
+ GetWindowThreadProcessId(foregroundWindow, out var processId);
+ return processId == (uint)Environment.ProcessId;
+ }
+
+ public bool IsKeyDown(int virtualKey) =>
+ (GetAsyncKeyState(virtualKey) & 0x8000) != 0;
+
+ [LibraryImport("user32.dll")]
+ private static partial short GetAsyncKeyState(int vKey);
+
+ [LibraryImport("user32.dll")]
+ private static partial nint GetForegroundWindow();
+
+ [LibraryImport("user32.dll")]
+ private static partial uint GetWindowThreadProcessId(nint hWnd, out uint processId);
+}
diff --git a/src/SharpEmu.HLE/Host/Windows/WindowsHostPlatform.cs b/src/SharpEmu.HLE/Host/Windows/WindowsHostPlatform.cs
index d8fdf28d..0e5ab7e2 100644
--- a/src/SharpEmu.HLE/Host/Windows/WindowsHostPlatform.cs
+++ b/src/SharpEmu.HLE/Host/Windows/WindowsHostPlatform.cs
@@ -10,4 +10,8 @@ internal sealed class WindowsHostPlatform : IHostPlatform
public IHostThreading Threading { get; } = new WindowsHostThreading();
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
+
+ public IHostAudioOutput Audio { get; } = new WindowsWaveOutAudio();
+
+ public IHostInput Input { get; } = new WindowsHostInput();
}
diff --git a/src/SharpEmu.HLE/Host/Windows/WindowsHostThreading.cs b/src/SharpEmu.HLE/Host/Windows/WindowsHostThreading.cs
index 44b36059..8f87b198 100644
--- a/src/SharpEmu.HLE/Host/Windows/WindowsHostThreading.cs
+++ b/src/SharpEmu.HLE/Host/Windows/WindowsHostThreading.cs
@@ -23,6 +23,31 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
private const int CtxRbp = 160;
private const int CtxRip = 248;
+ private static int _timerResolutionRequested;
+
+ public void RequestTimerResolution()
+ {
+ if (Interlocked.Exchange(ref _timerResolutionRequested, 1) != 0)
+ {
+ return;
+ }
+
+ try
+ {
+ if (TimeBeginPeriod(1) != 0)
+ {
+ Console.Error.WriteLine(
+ "[LOADER][WARN] Host timer resolution request rejected; " +
+ "timed waits keep the default ~15.6 ms granularity.");
+ }
+ }
+ catch (DllNotFoundException exception)
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
+ }
+ }
+
public uint AllocateTlsSlot() => TlsAlloc();
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
@@ -162,4 +187,7 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(nint hObject);
+
+ [LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
+ private static partial uint TimeBeginPeriod(uint uPeriod);
}
diff --git a/src/SharpEmu.HLE/Host/Windows/WindowsWaveOutAudio.cs b/src/SharpEmu.HLE/Host/Windows/WindowsWaveOutAudio.cs
new file mode 100644
index 00000000..30ce38c3
--- /dev/null
+++ b/src/SharpEmu.HLE/Host/Windows/WindowsWaveOutAudio.cs
@@ -0,0 +1,243 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Runtime.InteropServices;
+
+namespace SharpEmu.HLE.Host.Windows;
+
+internal sealed partial class WindowsWaveOutAudio : IHostAudioOutput
+{
+ public string BackendName => "winmm";
+
+ public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate) => new WaveOutStream(sampleRate);
+
+ private sealed partial class WaveOutStream : IHostAudioStream
+ {
+ private const uint WaveMapper = uint.MaxValue;
+ private const uint CallbackEvent = 0x0005_0000;
+ private const ushort WaveFormatPcm = 1;
+ private const uint WaveHeaderDone = 0x0000_0001;
+ private const int MaximumQueuedPcmBytes = 32 * 1024;
+
+ private readonly object _gate = new();
+ private readonly AutoResetEvent _completion = new(false);
+ private readonly Queue _buffers = new();
+ private IntPtr _device;
+ private int _queuedPcmBytes;
+ private bool _disposed;
+
+ public WaveOutStream(uint sampleRate)
+ {
+ var format = new WaveFormat
+ {
+ FormatTag = WaveFormatPcm,
+ Channels = 2,
+ SamplesPerSecond = sampleRate,
+ AverageBytesPerSecond = checked(sampleRate * 4),
+ BlockAlign = 4,
+ BitsPerSample = 16,
+ ExtraSize = 0,
+ };
+ var result = WaveOutOpen(
+ out _device,
+ WaveMapper,
+ ref format,
+ _completion.SafeWaitHandle.DangerousGetHandle(),
+ IntPtr.Zero,
+ CallbackEvent);
+ if (result != 0)
+ {
+ throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
+ }
+ }
+
+ public bool Submit(ReadOnlySpan stereoPcm16)
+ {
+ lock (_gate)
+ {
+ if (_disposed)
+ {
+ return false;
+ }
+
+ ReapCompletedBuffers();
+ while (_queuedPcmBytes != 0 &&
+ _queuedPcmBytes + stereoPcm16.Length > MaximumQueuedPcmBytes)
+ {
+ if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
+ {
+ return false;
+ }
+
+ ReapCompletedBuffers();
+ }
+
+ return QueueBuffer(stereoPcm16);
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_gate)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ if (_device != IntPtr.Zero)
+ {
+ WaveOutReset(_device);
+ while (_buffers.TryDequeue(out var buffer))
+ {
+ ReleaseBuffer(buffer);
+ }
+
+ WaveOutClose(_device);
+ _device = IntPtr.Zero;
+ }
+
+ _completion.Dispose();
+ }
+ }
+
+ private bool QueueBuffer(ReadOnlySpan data)
+ {
+ var dataAddress = Marshal.AllocHGlobal(data.Length);
+ var headerAddress = IntPtr.Zero;
+ try
+ {
+ unsafe
+ {
+ data.CopyTo(new Span((void*)dataAddress, data.Length));
+ }
+
+ var header = new WaveHeader
+ {
+ Data = dataAddress,
+ BufferLength = checked((uint)data.Length),
+ };
+ headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf());
+ Marshal.StructureToPtr(header, headerAddress, false);
+
+ var result = WaveOutPrepareHeader(
+ _device,
+ headerAddress,
+ checked((uint)Marshal.SizeOf()));
+ if (result != 0)
+ {
+ return false;
+ }
+
+ result = WaveOutWrite(
+ _device,
+ headerAddress,
+ checked((uint)Marshal.SizeOf()));
+ if (result != 0)
+ {
+ WaveOutUnprepareHeader(
+ _device,
+ headerAddress,
+ checked((uint)Marshal.SizeOf()));
+ return false;
+ }
+
+ _buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
+ _queuedPcmBytes += data.Length;
+ dataAddress = IntPtr.Zero;
+ headerAddress = IntPtr.Zero;
+ return true;
+ }
+ finally
+ {
+ if (headerAddress != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(headerAddress);
+ }
+
+ if (dataAddress != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(dataAddress);
+ }
+ }
+ }
+
+ private void ReapCompletedBuffers()
+ {
+ while (_buffers.TryPeek(out var buffer))
+ {
+ var header = Marshal.PtrToStructure(buffer.Header);
+ if ((header.Flags & WaveHeaderDone) == 0)
+ {
+ return;
+ }
+
+ _buffers.Dequeue();
+ ReleaseBuffer(buffer);
+ }
+ }
+
+ private void ReleaseBuffer(NativeBuffer buffer)
+ {
+ WaveOutUnprepareHeader(
+ _device,
+ buffer.Header,
+ checked((uint)Marshal.SizeOf()));
+ _queuedPcmBytes -= buffer.Length;
+ Marshal.FreeHGlobal(buffer.Header);
+ Marshal.FreeHGlobal(buffer.Data);
+ }
+
+ private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
+
+ [StructLayout(LayoutKind.Sequential, Pack = 2)]
+ private struct WaveFormat
+ {
+ public ushort FormatTag;
+ public ushort Channels;
+ public uint SamplesPerSecond;
+ public uint AverageBytesPerSecond;
+ public ushort BlockAlign;
+ public ushort BitsPerSample;
+ public ushort ExtraSize;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct WaveHeader
+ {
+ public IntPtr Data;
+ public uint BufferLength;
+ public uint BytesRecorded;
+ public nuint User;
+ public uint Flags;
+ public uint Loops;
+ public IntPtr Next;
+ public nuint Reserved;
+ }
+
+ [LibraryImport("winmm.dll", EntryPoint = "waveOutOpen")]
+ private static partial uint WaveOutOpen(
+ out IntPtr device,
+ uint deviceId,
+ ref WaveFormat format,
+ IntPtr callback,
+ IntPtr instance,
+ uint flags);
+
+ [LibraryImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
+ private static partial uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
+
+ [LibraryImport("winmm.dll", EntryPoint = "waveOutWrite")]
+ private static partial uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
+
+ [LibraryImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
+ private static partial uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
+
+ [LibraryImport("winmm.dll", EntryPoint = "waveOutReset")]
+ private static partial uint WaveOutReset(IntPtr device);
+
+ [LibraryImport("winmm.dll", EntryPoint = "waveOutClose")]
+ private static partial uint WaveOutClose(IntPtr device);
+ }
+}
diff --git a/src/SharpEmu.Libs/Pad/XInputReader.cs b/src/SharpEmu.HLE/Host/Windows/WindowsXInputReader.cs
similarity index 72%
rename from src/SharpEmu.Libs/Pad/XInputReader.cs
rename to src/SharpEmu.HLE/Host/Windows/WindowsXInputReader.cs
index 1cbc3779..6e1bc2b7 100644
--- a/src/SharpEmu.Libs/Pad/XInputReader.cs
+++ b/src/SharpEmu.HLE/Host/Windows/WindowsXInputReader.cs
@@ -3,15 +3,15 @@
using System.Runtime.InteropServices;
-namespace SharpEmu.Libs.Pad;
+namespace SharpEmu.HLE.Host.Windows;
///
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
-/// the Windows XInput API on a background thread, translated to the same
-/// ORBIS pad conventions as . Supports rumble
-/// and hot-plug retry; the first connected slot (of four) is used.
+/// the Windows XInput API on a background thread, translated to
+/// conventions. Supports rumble and hot-plug
+/// retry; the first connected slot (of four) is used.
///
-internal static class XInputReader
+internal static partial class WindowsXInputReader
{
private const uint ErrorSuccess = 0;
private const int SlotCount = 4;
@@ -34,7 +34,7 @@ internal static class XInputReader
private const ushort XinputY = 0x8000;
private static readonly object Gate = new();
- private static PadState _state;
+ private static HostGamepadState _state;
private static bool _started;
private static int _slot = -1; // connected XInput user index, -1 when none
private static byte _motorLeft;
@@ -45,6 +45,8 @@ internal static class XInputReader
/// Starts the background reader once; safe to call repeatedly.
internal static void EnsureStarted()
{
+ // The GUI source-links this reader and calls it directly, without the
+ // host-platform resolution that otherwise guarantees Windows.
if (!OperatingSystem.IsWindows())
{
return;
@@ -67,7 +69,7 @@ internal static class XInputReader
}
}
- internal static bool TryGetState(out PadState state)
+ internal static bool TryGetState(out HostGamepadState state)
{
lock (Gate)
{
@@ -77,7 +79,7 @@ internal static class XInputReader
return state.Connected;
}
- private static void SetState(in PadState state)
+ private static void SetState(in HostGamepadState state)
{
lock (Gate)
{
@@ -204,40 +206,40 @@ internal static class XInputReader
return -1;
}
- private static PadState Translate(in XInputGamepad pad)
+ private static HostGamepadState Translate(in XInputGamepad pad)
{
- uint buttons = 0;
- buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
- buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
- buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
- buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
- buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
- buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
- buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
- buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
- buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
- buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
- buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
- buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
- buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
- buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
- buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
- buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
+ var buttons = HostGamepadButtons.None;
+ buttons |= (pad.Buttons & XinputDpadUp) != 0 ? HostGamepadButtons.Up : 0;
+ buttons |= (pad.Buttons & XinputDpadDown) != 0 ? HostGamepadButtons.Down : 0;
+ buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? HostGamepadButtons.Left : 0;
+ buttons |= (pad.Buttons & XinputDpadRight) != 0 ? HostGamepadButtons.Right : 0;
+ buttons |= (pad.Buttons & XinputStart) != 0 ? HostGamepadButtons.Options : 0;
+ buttons |= (pad.Buttons & XinputBack) != 0 ? HostGamepadButtons.TouchPad : 0;
+ buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? HostGamepadButtons.L3 : 0;
+ buttons |= (pad.Buttons & XinputRightThumb) != 0 ? HostGamepadButtons.R3 : 0;
+ buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? HostGamepadButtons.L1 : 0;
+ buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? HostGamepadButtons.R1 : 0;
+ buttons |= (pad.Buttons & XinputA) != 0 ? HostGamepadButtons.Cross : 0;
+ buttons |= (pad.Buttons & XinputB) != 0 ? HostGamepadButtons.Circle : 0;
+ buttons |= (pad.Buttons & XinputX) != 0 ? HostGamepadButtons.Square : 0;
+ buttons |= (pad.Buttons & XinputY) != 0 ? HostGamepadButtons.Triangle : 0;
+ buttons |= pad.LeftTrigger > TriggerThreshold ? HostGamepadButtons.L2 : 0;
+ buttons |= pad.RightTrigger > TriggerThreshold ? HostGamepadButtons.R2 : 0;
- return new PadState(
+ return new HostGamepadState(
Connected: true,
Buttons: buttons,
LeftX: AxisToByte(pad.ThumbLX),
LeftY: AxisToByteInverted(pad.ThumbLY),
RightX: AxisToByte(pad.ThumbRX),
RightY: AxisToByteInverted(pad.ThumbRY),
- L2: pad.LeftTrigger,
- R2: pad.RightTrigger);
+ LeftTrigger: pad.LeftTrigger,
+ RightTrigger: pad.RightTrigger);
}
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
- // XInput Y grows upward, ORBIS pads report Y growing downward.
+ // XInput Y grows upward, host pad conventions report Y growing downward.
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
[StructLayout(LayoutKind.Sequential)]
@@ -267,9 +269,9 @@ internal static class XInputReader
}
// xinput1_4.dll ships with Windows 8 and later.
- [DllImport("xinput1_4.dll")]
- private static extern uint XInputGetState(uint userIndex, out XInputState state);
+ [LibraryImport("xinput1_4.dll")]
+ private static partial uint XInputGetState(uint userIndex, out XInputState state);
- [DllImport("xinput1_4.dll")]
- private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
+ [LibraryImport("xinput1_4.dll")]
+ private static partial uint XInputSetState(uint userIndex, ref XInputVibration vibration);
}
diff --git a/src/SharpEmu.Libs/Audio/AudioOutExports.cs b/src/SharpEmu.Libs/Audio/AudioOutExports.cs
index b2b3f307..a044e584 100644
--- a/src/SharpEmu.Libs/Audio/AudioOutExports.cs
+++ b/src/SharpEmu.Libs/Audio/AudioOutExports.cs
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
+using SharpEmu.HLE.Host;
using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -27,7 +28,7 @@ public static class AudioOutExports
int channels,
int bytesPerSample,
bool isFloat,
- WinMmAudioPort? backend)
+ IHostAudioStream? backend)
{
UserId = userId;
Type = type;
@@ -48,7 +49,7 @@ public static class AudioOutExports
public int Channels { get; }
public int BytesPerSample { get; }
public bool IsFloat { get; }
- public WinMmAudioPort? Backend { get; }
+ public IHostAudioStream? Backend { get; }
public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample);
@@ -103,12 +104,13 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
- WinMmAudioPort? backend = null;
+ IHostAudioStream? backend = null;
string backendName;
try
{
- backend = new WinMmAudioPort(frequency);
- backendName = "winmm";
+ var audio = HostPlatform.Current.Audio;
+ backend = audio.OpenStereoPcm16Stream(frequency);
+ backendName = audio.BackendName;
}
catch (Exception exception)
{
@@ -180,15 +182,31 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
- if (port.Backend is null ||
- !port.Backend.Submit(
- source,
- port.BufferLength,
- port.Channels,
- port.BytesPerSample,
- port.IsFloat))
+ if (port.Backend is null)
{
port.PaceSilence();
+ return ctx.SetReturn(0);
+ }
+
+ var outputLength = checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
+ var output = ArrayPool.Shared.Rent(outputLength);
+ try
+ {
+ AudioPcmConversion.ConvertToStereoPcm16(
+ source,
+ output.AsSpan(0, outputLength),
+ checked((int)port.BufferLength),
+ port.Channels,
+ port.BytesPerSample,
+ port.IsFloat);
+ if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
+ {
+ port.PaceSilence();
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(output);
}
return ctx.SetReturn(0);
diff --git a/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs b/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs
new file mode 100644
index 00000000..a15ce19c
--- /dev/null
+++ b/src/SharpEmu.Libs/Audio/AudioPcmConversion.cs
@@ -0,0 +1,55 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Buffers.Binary;
+
+namespace SharpEmu.Libs.Audio;
+
+///
+/// Converts guest AudioOut submissions (mono/stereo/7.1, s16 or float32) into the
+/// interleaved stereo 16-bit PCM that host audio streams accept. Platform-neutral —
+/// device specifics live behind IHostAudioStream.
+///
+internal static class AudioPcmConversion
+{
+ /// Bytes per output frame: two 16-bit channels.
+ public const int OutputFrameSize = 4;
+
+ public static void ConvertToStereoPcm16(
+ ReadOnlySpan source,
+ Span destination,
+ int frames,
+ int channels,
+ int bytesPerSample,
+ bool isFloat)
+ {
+ var sourceFrameSize = checked(channels * bytesPerSample);
+ for (var frame = 0; frame < frames; frame++)
+ {
+ var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
+ var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
+ var right = channels == 1
+ ? left
+ : ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
+ BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
+ BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
+ }
+ }
+
+ private static short ReadSample(
+ ReadOnlySpan frame,
+ int channel,
+ int bytesPerSample,
+ bool isFloat)
+ {
+ var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
+ if (!isFloat)
+ {
+ return BinaryPrimitives.ReadInt16LittleEndian(sample);
+ }
+
+ var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
+ var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
+ return checked((short)MathF.Round(value * short.MaxValue));
+ }
+}
diff --git a/src/SharpEmu.Libs/Audio/WinMmAudioPort.cs b/src/SharpEmu.Libs/Audio/WinMmAudioPort.cs
deleted file mode 100644
index 7322774a..00000000
--- a/src/SharpEmu.Libs/Audio/WinMmAudioPort.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-// Copyright (C) 2026 SharpEmu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-using System.Buffers;
-using System.Buffers.Binary;
-using System.Runtime.InteropServices;
-
-namespace SharpEmu.Libs.Audio;
-
-internal sealed class WinMmAudioPort : IDisposable
-{
- private const uint WaveMapper = uint.MaxValue;
- private const uint CallbackEvent = 0x0005_0000;
- private const ushort WaveFormatPcm = 1;
- private const uint WaveHeaderDone = 0x0000_0001;
- private const int MaximumQueuedPcmBytes = 32 * 1024;
-
- private readonly object _gate = new();
- private readonly AutoResetEvent _completion = new(false);
- private readonly Queue _buffers = new();
- private IntPtr _device;
- private int _queuedPcmBytes;
- private bool _disposed;
-
- public WinMmAudioPort(uint sampleRate)
- {
- if (!OperatingSystem.IsWindows())
- {
- throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
- }
-
- var format = new WaveFormat
- {
- FormatTag = WaveFormatPcm,
- Channels = 2,
- SamplesPerSecond = sampleRate,
- AverageBytesPerSecond = checked(sampleRate * 4),
- BlockAlign = 4,
- BitsPerSample = 16,
- ExtraSize = 0,
- };
- var result = WaveOutOpen(
- out _device,
- WaveMapper,
- ref format,
- _completion.SafeWaitHandle.DangerousGetHandle(),
- IntPtr.Zero,
- CallbackEvent);
- if (result != 0)
- {
- throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
- }
- }
-
- public bool Submit(
- ReadOnlySpan source,
- uint frames,
- int channels,
- int bytesPerSample,
- bool isFloat)
- {
- lock (_gate)
- {
- if (_disposed)
- {
- return false;
- }
-
- var outputLength = checked((int)frames * 4);
- ReapCompletedBuffers();
- while (_queuedPcmBytes != 0 &&
- _queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
- {
- if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
- {
- return false;
- }
-
- ReapCompletedBuffers();
- }
-
- var output = ArrayPool.Shared.Rent(outputLength);
- try
- {
- ConvertToStereoPcm16(
- source,
- output.AsSpan(0, outputLength),
- checked((int)frames),
- channels,
- bytesPerSample,
- isFloat);
- return QueueBuffer(output.AsSpan(0, outputLength));
- }
- finally
- {
- ArrayPool.Shared.Return(output);
- }
- }
- }
-
- public void Dispose()
- {
- lock (_gate)
- {
- if (_disposed)
- {
- return;
- }
-
- _disposed = true;
- if (_device != IntPtr.Zero)
- {
- WaveOutReset(_device);
- while (_buffers.TryDequeue(out var buffer))
- {
- ReleaseBuffer(buffer);
- }
-
- WaveOutClose(_device);
- _device = IntPtr.Zero;
- }
-
- _completion.Dispose();
- }
- }
-
- private bool QueueBuffer(ReadOnlySpan data)
- {
- var dataAddress = Marshal.AllocHGlobal(data.Length);
- var headerAddress = IntPtr.Zero;
- try
- {
- unsafe
- {
- data.CopyTo(new Span((void*)dataAddress, data.Length));
- }
-
- var header = new WaveHeader
- {
- Data = dataAddress,
- BufferLength = checked((uint)data.Length),
- };
- headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf());
- Marshal.StructureToPtr(header, headerAddress, false);
-
- var result = WaveOutPrepareHeader(
- _device,
- headerAddress,
- checked((uint)Marshal.SizeOf()));
- if (result != 0)
- {
- return false;
- }
-
- result = WaveOutWrite(
- _device,
- headerAddress,
- checked((uint)Marshal.SizeOf()));
- if (result != 0)
- {
- WaveOutUnprepareHeader(
- _device,
- headerAddress,
- checked((uint)Marshal.SizeOf()));
- return false;
- }
-
- _buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
- _queuedPcmBytes += data.Length;
- dataAddress = IntPtr.Zero;
- headerAddress = IntPtr.Zero;
- return true;
- }
- finally
- {
- if (headerAddress != IntPtr.Zero)
- {
- Marshal.FreeHGlobal(headerAddress);
- }
-
- if (dataAddress != IntPtr.Zero)
- {
- Marshal.FreeHGlobal(dataAddress);
- }
- }
- }
-
- private void ReapCompletedBuffers()
- {
- while (_buffers.TryPeek(out var buffer))
- {
- var header = Marshal.PtrToStructure(buffer.Header);
- if ((header.Flags & WaveHeaderDone) == 0)
- {
- return;
- }
-
- _buffers.Dequeue();
- ReleaseBuffer(buffer);
- }
- }
-
- private void ReleaseBuffer(NativeBuffer buffer)
- {
- WaveOutUnprepareHeader(
- _device,
- buffer.Header,
- checked((uint)Marshal.SizeOf()));
- _queuedPcmBytes -= buffer.Length;
- Marshal.FreeHGlobal(buffer.Header);
- Marshal.FreeHGlobal(buffer.Data);
- }
-
- private static void ConvertToStereoPcm16(
- ReadOnlySpan source,
- Span destination,
- int frames,
- int channels,
- int bytesPerSample,
- bool isFloat)
- {
- var sourceFrameSize = checked(channels * bytesPerSample);
- for (var frame = 0; frame < frames; frame++)
- {
- var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
- var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
- var right = channels == 1
- ? left
- : ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
- BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
- BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
- }
- }
-
- private static short ReadSample(
- ReadOnlySpan frame,
- int channel,
- int bytesPerSample,
- bool isFloat)
- {
- var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
- if (!isFloat)
- {
- return BinaryPrimitives.ReadInt16LittleEndian(sample);
- }
-
- var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
- var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
- return checked((short)MathF.Round(value * short.MaxValue));
- }
-
- private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
-
- [StructLayout(LayoutKind.Sequential, Pack = 2)]
- private struct WaveFormat
- {
- public ushort FormatTag;
- public ushort Channels;
- public uint SamplesPerSecond;
- public uint AverageBytesPerSecond;
- public ushort BlockAlign;
- public ushort BitsPerSample;
- public ushort ExtraSize;
- }
-
- [StructLayout(LayoutKind.Sequential)]
- private struct WaveHeader
- {
- public IntPtr Data;
- public uint BufferLength;
- public uint BytesRecorded;
- public nuint User;
- public uint Flags;
- public uint Loops;
- public IntPtr Next;
- public nuint Reserved;
- }
-
- [DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
- private static extern uint WaveOutOpen(
- out IntPtr device,
- uint deviceId,
- ref WaveFormat format,
- IntPtr callback,
- IntPtr instance,
- uint flags);
-
- [DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
- private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
-
- [DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
- private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
-
- [DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
- private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
-
- [DllImport("winmm.dll", EntryPoint = "waveOutReset")]
- private static extern uint WaveOutReset(IntPtr device);
-
- [DllImport("winmm.dll", EntryPoint = "waveOutClose")]
- private static extern uint WaveOutClose(IntPtr device);
-}
diff --git a/src/SharpEmu.Libs/HostTimerResolution.cs b/src/SharpEmu.Libs/HostTimerResolution.cs
deleted file mode 100644
index cb2651a5..00000000
--- a/src/SharpEmu.Libs/HostTimerResolution.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2026 SharpEmu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
-
-namespace SharpEmu.Libs;
-
-public static class HostTimerResolution
-{
- private const uint TargetPeriodMilliseconds = 1;
-
- private static int _requested;
-
- public static void Request()
- {
- if (Interlocked.Exchange(ref _requested, 1) != 0)
- {
- return;
- }
-
- if (!OperatingSystem.IsWindows())
- {
- return;
- }
-
- try
- {
- if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
- {
- Console.Error.WriteLine(
- "[LOADER][WARN] Host timer resolution request rejected; " +
- "timed waits keep the default ~15.6 ms granularity.");
- }
- }
- catch (DllNotFoundException exception)
- {
- Console.Error.WriteLine(
- $"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
- }
- }
-
- [SupportedOSPlatform("windows")]
- [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
- private static extern uint TimeBeginPeriod(uint uPeriod);
-}
diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs
index 83a9a140..72205c5f 100644
--- a/src/SharpEmu.Libs/Pad/PadExports.cs
+++ b/src/SharpEmu.Libs/Pad/PadExports.cs
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
+using SharpEmu.HLE.Host;
using System.Buffers.Binary;
using System.Diagnostics;
-using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad;
@@ -38,8 +38,7 @@ public static class PadExports
public static int PadInit(CpuContext ctx)
{
_initialized = true;
- DualSenseReader.EnsureStarted();
- XInputReader.EnsureStarted();
+ HostPlatform.Current.Input.EnsureStarted();
return ctx.SetReturn(0);
}
@@ -81,15 +80,13 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
- DualSenseReader.EnsureStarted();
- XInputReader.EnsureStarted();
+ var input = HostPlatform.Current.Input;
+ input.EnsureStarted();
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
{
- Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
- ? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
- : XInputReader.TryGetState(out _)
- ? "[LOADER][INFO] Controls: Xbox controller 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.");
+ 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);
@@ -282,7 +279,7 @@ public static class PadExports
}
var triggerMask = parameter[0];
- XInputReader.SetTriggerRumble(
+ HostPlatform.Current.Input.SetTriggerRumble(
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
@@ -326,8 +323,7 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
- DualSenseReader.SetRumble(parameter[0], parameter[1]);
- XInputReader.SetRumble(parameter[0], parameter[1]);
+ HostPlatform.Current.Input.SetRumble(parameter[0], parameter[1]);
return ctx.SetReturn(0);
}
@@ -357,7 +353,7 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
- DualSenseReader.SetLightbar(color[0], color[1], color[2]);
+ HostPlatform.Current.Input.SetLightbar(color[0], color[1], color[2]);
return ctx.SetReturn(0);
}
@@ -374,7 +370,7 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
- DualSenseReader.ResetLightbar();
+ HostPlatform.Current.Input.ResetLightbar();
return ctx.SetReturn(0);
}
@@ -420,37 +416,30 @@ public static class PadExports
return _cachedInputState;
}
- var acceptsKeyboardInput = IsEmulatorWindowFocused();
- var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
- 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;
+ 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;
- if (DualSenseReader.TryGetState(out var pad))
+ Span gamepads = stackalloc HostGamepadState[2];
+ var gamepadCount = input.GetGamepadStates(gamepads);
+ for (var index = 0; index < gamepadCount; index++)
{
- buttons |= pad.Buttons;
+ 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.L2);
- r2 = Math.Max(r2, pad.R2);
- }
-
- if (XInputReader.TryGetState(out var xpad))
- {
- buttons |= xpad.Buttons;
- leftX = MergeAxis(xpad.LeftX, leftX);
- leftY = MergeAxis(xpad.LeftY, leftY);
- rightX = MergeAxis(xpad.RightX, rightX);
- rightY = MergeAxis(xpad.RightY, rightY);
- l2 = Math.Max(l2, xpad.L2);
- r2 = Math.Max(r2, xpad.R2);
+ l2 = Math.Max(l2, pad.LeftTrigger);
+ r2 = Math.Max(r2, pad.RightTrigger);
}
_cachedInputState = new PadState(
@@ -466,50 +455,49 @@ public static class PadExports
return _cachedInputState;
}
- [DllImport("user32.dll")]
- private static extern short GetAsyncKeyState(int vKey);
-
- [DllImport("user32.dll")]
- private static extern nint GetForegroundWindow();
-
- [DllImport("user32.dll")]
- private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
-
- private static bool IsKeyDown(int vk) =>
- (GetAsyncKeyState(vk) & 0x8000) != 0;
-
- private static bool IsEmulatorWindowFocused()
+ /// Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.
+ private static uint ToOrbisButtons(HostGamepadButtons buttons)
{
- var foregroundWindow = GetForegroundWindow();
- if (foregroundWindow == 0)
- {
- return false;
- }
-
- GetWindowThreadProcessId(foregroundWindow, out var processId);
- return processId == (uint)Environment.ProcessId;
+ uint result = 0;
+ 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 uint ReadKeyboardButtons()
+ private static uint ReadKeyboardButtons(IHostInput input)
{
uint buttons = 0;
// D-pad
- if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
- if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
- if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
- if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
+ 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 (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
- if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
- if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
- if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
+ 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 (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
- if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
- if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
- if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
+ 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 (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options
+ if (input.IsKeyDown(0x09) || input.IsKeyDown(0x08)) buttons |= OrbisPadButton.Options; // Tab / Backspace
return buttons;
}
diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
index 91f7e150..dca2dd88 100644
--- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
+++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
+using SharpEmu.HLE.Host;
using SharpEmu.Libs.Audio;
using SharpEmu.Libs.Kernel;
using SharpEmu.Logging;
@@ -68,7 +69,7 @@ public static class VideoOutExports
return;
}
- HostTimerResolution.Request();
+ HostPlatform.Current.Threading.RequestTimerResolution();
_vblankPumpThread = new Thread(VblankPumpLoop)
{