[Host] Abstract audio output and pad/keyboard input behind the host platform seam (#192)

* [Host] Abstract audio output behind IHostAudioOutput

Add IHostAudioOutput (opens streams, names the backend for diagnostics)
and IHostAudioStream (submit interleaved stereo 16-bit PCM, Dispose) to
the host seam, with the winmm waveOut implementation moving whole into
Host/Windows/WindowsWaveOutAudio — same device open, queueing,
32 KB backpressure wait, and buffer lifetime as WinMmAudioPort had. The
DllImports become source-generated LibraryImports in the move, matching
the other Windows backends.

The guest-format conversion (mono/stereo/7.1, s16/float32 -> stereo
PCM16) is platform policy, not device code, so it stays in Libs as
AudioPcmConversion; AudioOutOutput converts into a pooled buffer and
submits the result through the stream. Open failures still degrade to
the silent paced port with the same warning, and the port log line now
takes its backend name from the platform instead of a hardcoded string.

* [Host] Abstract pad and keyboard input behind IHostInput

Add IHostInput to the host seam: gamepad state snapshots, rumble /
trigger-rumble / lightbar sinks, and the keyboard-fallback queries
(window focus, key state). Gamepad state crosses the seam as the new
unmanaged HostGamepadState with HostGamepadButtons flags — named after
the PlayStation layout the guest API exposes but with the seam's own
values, so SCE_PAD_BUTTON bits never leak into host backends and the
per-frame poll can stackalloc its snapshot buffer.

The DualSense raw-HID reader, the XInput reader, and the Win32 HID
interop move whole into Host/Windows (report parsing, hot-plug loops,
rumble/lightbar output reports, and log strings unchanged), translating
to the neutral flags instead of ORBIS bits and converting their
DllImports to source-generated LibraryImports. WindowsHostInput
composes them plus the user32 keyboard queries; rumble still fans out
to both readers, trigger rumble stays XInput-only, lightbar stays
DualSense-only.

PadExports keeps all policy: the keyboard mapping (now via named
OrbisPadButton constants instead of raw hex), the controller-beats-
keyboard-past-deadzone merge, and the new host->ORBIS button
translation. The GUI's source-linked reader copies re-point to the
moved files (it still cannot reference SharpEmu.HLE wholesale), which
requires AllowUnsafeBlocks for the generated marshalling stubs; its
navigation code switches to the neutral flags.

* [Host] Move the timer-resolution request behind IHostThreading

IHostThreading gains RequestTimerResolution (idempotent, best-effort
~1 ms timed-wait granularity; a no-op wherever the platform default is
already fine). The winmm timeBeginPeriod call, its once-only latch, and
both warning strings move from the Libs-level HostTimerResolution
helper into WindowsHostThreading as a source-generated LibraryImport;
the vblank pump requests it through the platform instead.

HostSystemInfo in SharpEmu.Logging keeps its direct user32/kernel32
imports deliberately: Logging sits below HLE in the dependency chain so
it cannot see the host seam, every path is already OS-gated with
fallbacks, and it only runs once for the diagnostics banner.
This commit is contained in:
Gutemberg Ribeiro
2026-07-15 11:24:53 +01:00
committed by GitHub
parent 2ad9836d13
commit 72645cb373
21 changed files with 783 additions and 556 deletions
+61 -73
View File
@@ -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<HostGamepadState> 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()
/// <summary>Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.</summary>
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;
}