0
0
Fork 0

Miria: The Death of OpenTK 3 (#2194)

* openal: Update to OpenTK 4

* Ryujinx.Graphics.OpenGL: Update to OpenTK 4

* Entirely removed OpenTK 3, still wip

* Use SPB for context creation and handling

Still need to test on GLX and readd input support

* Start implementing a new input system

So far only gamepad are supported, no configuration possible via UI but detected via hotplug/removal

Button mapping backend is implemented

TODO: front end, configuration handling and configuration migration
TODO: keyboard support

* Enforce RGB only framebuffer on the GLWidget

Fix possible transparent window

* Implement UI gamepad frontend

Also fix bad mapping of minus button and ensure gamepad config is updated in real time

* Handle controller being disconnected and reconnected again

* Revert "Enforce RGB only framebuffer on the GLWidget"

This reverts commit 0949715d1a03ec793e35e37f7b610cbff2d63965.

* Fix first color clear

* Filter SDL2 events a bit

* Start working on the keyboard detail

- Rework configuration classes a bit to be more clean.
- Integrate fully the keyboard configuration to the front end (TODO: assigner)
- Start skeleton for the GTK3 keyboard driver

* Add KeyboardStateSnapshot and its integration

* Implement keyboard assigner and GTK3 key mapping

TODO: controller configuration mapping and IGamepad implementation for keyboard

* Add missing SR and SL definitions

* Fix copy pasta mistake on config for previous commit

* Implement IGamepad interface for GTK3 keyboard

* Fix some implementation still being commented in the controller ui for keyboard

* Port screen handle code

* Remove all configuration management code and move HidNew to Hid

* Rename InputConfigNew to InputConfig

* Add a version field to the input config

* Prepare serialization and deserialization of new input config and migrate profile loading and saving

* Support input configuration saving to config and bump config version to 23.

* Clean up in ConfigurationState

* Reference SPB via a nuget package

* Move new input system to Ryujinx.Input project and SDL2 detail to Ryujinx.Input.SDL2

* move GTK3 input to the right directory

* Fix triggers on SDL2

* Update to SDL2 2.0.14 via our own fork

* Update buttons definition for SDL2 2.0.14 and report gamepad features

* Implement motion support again with SDL2

TODO: cemu hooks integration

* Switch to latest of nightly SDL2

* SDL2: Fix bugs in gamepad id matching allowing different gamepad to match on the same device index

* Ensure values are set in UI when the gamepad get hot plugged

* Avoid trying to add controllers in the Update method and don't open SDL2 gamepad instance before checking ids

This fixes permanent rumble of pro controller in some hotplug scenario

* Fix more UI bugs

* Move legcay motion code around before reintegration

* gamecontroller UI tweaks here and there

* Hide Motion on non motion configurations

* Update the TODO grave

Some TODO were fixed long time ago or are quite oudated...

* Integrate cemu hooks motion configuration

* Integrate cemu hooks configuration options to the UI again

* cemuhooks => cemuhooks

* Add cemu hook support again

* Fix regression on normal motion and fix some very nasty bugs around

* Fix for XCB multithreads issue on Linux

* Enable motion by default

* Block inputs in the main view when in the controller configuration window

* Some fixes for the controller ui again

* Add joycon support and fixes other hints

* Bug fixes and clean up

- Invert default mapping if not a Nintendo controller
- Keep alive the controller being selected on the controller window (allow to avoid big delay for controller needing time to init when doing button assignment)
- Clean up hints in use
- Remove debug logs around
- Fixes potential double free with SDL2Gamepad

* Move the button assigner and motion logic to the Ryujinx.Input project

* Reimplement raw keyboard hle input

Also move out the logic of the hotkeys

* Move all remaining Input manager stuffs to the Ryujinx.Input project

* Increment configuration version yet again because of master changes

* Ensure input config isn't null when not present

* Fixes for VS not being nice

* Fix broken gamepad caching logic causing crashes on ui

* Ensure the background context is destroyed

* Update dependencies

* Readd retrocompat with old format of the config to avoid parsing and crashes on those versions

Also updated the debug Config.json

* Document new input APIs

* Isolate SDL2Driver to the project and remove external export of it

* Add support for external gamepad db mappings on SDL2

* Last clean up before PR

* Addresses first part of comments

* Address gdkchan's comments

* Do not use JsonException

* Last comment fixes
This commit is contained in:
Mary 2021-04-14 12:28:43 +02:00 committed by GitHub
parent 978b69b706
commit 6cb22c9d38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 4516 additions and 2048 deletions

View file

@ -1,9 +1,10 @@
using OpenTK.Audio; using OpenTK.Audio.OpenAL;
using Ryujinx.Audio.Common; using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration; using Ryujinx.Audio.Integration;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
@ -12,8 +13,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
{ {
private object _lock = new object(); private object _lock = new object();
private ALDevice _device;
private AudioContext _context; private ALContext _context;
private ManualResetEvent _updateRequiredEvent; private ManualResetEvent _updateRequiredEvent;
private List<OpenALHardwareDeviceSession> _sessions; private List<OpenALHardwareDeviceSession> _sessions;
private bool _stillRunning; private bool _stillRunning;
@ -21,7 +22,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
public OpenALHardwareDeviceDriver() public OpenALHardwareDeviceDriver()
{ {
_context = new AudioContext(); _device = ALC.OpenDevice("");
_context = ALC.CreateContext(_device, new ALContextAttributes());
_updateRequiredEvent = new ManualResetEvent(false); _updateRequiredEvent = new ManualResetEvent(false);
_sessions = new List<OpenALHardwareDeviceSession>(); _sessions = new List<OpenALHardwareDeviceSession>();
@ -40,7 +42,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
{ {
try try
{ {
return AudioContext.AvailableDevices.Count > 0; return ALC.GetStringList(GetEnumerationStringList.DeviceSpecifier).Any();
} }
catch catch
{ {
@ -95,6 +97,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
private void Update() private void Update()
{ {
ALC.MakeContextCurrent(_context);
while (_stillRunning) while (_stillRunning)
{ {
bool updateRequired = false; bool updateRequired = false;
@ -143,7 +147,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
} }
} }
_context.Dispose(); ALC.DestroyContext(_context);
ALC.CloseDevice(_device);
} }
} }

View file

@ -71,11 +71,11 @@ namespace Ryujinx.Audio.Backends.OpenAL
OpenALAudioBuffer driverBuffer = new OpenALAudioBuffer OpenALAudioBuffer driverBuffer = new OpenALAudioBuffer
{ {
DriverIdentifier = buffer.DataPointer, DriverIdentifier = buffer.DataPointer,
BufferId = AL.GenBuffer(), BufferId = AL.GenBuffer(),
SampleCount = GetSampleCount(buffer) SampleCount = GetSampleCount(buffer)
}; };
AL.BufferData(driverBuffer.BufferId, _targetFormat, buffer.Data, (int)buffer.DataSize, (int)RequestedSampleRate); AL.BufferData(driverBuffer.BufferId, _targetFormat, buffer.Data, (int)RequestedSampleRate);
_queuedBuffers.Enqueue(driverBuffer); _queuedBuffers.Enqueue(driverBuffer);
@ -125,7 +125,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
} }
} }
public override void UnregisterBuffer(AudioBuffer buffer) {} public override void UnregisterBuffer(AudioBuffer buffer) { }
public override bool WasBufferFullyConsumed(AudioBuffer buffer) public override bool WasBufferFullyConsumed(AudioBuffer buffer)
{ {

View file

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="OpenTK.NetStandard" Version="1.0.5.32" /> <PackageReference Include="OpenTK.OpenAL" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 23; public const int CurrentVersion = 24;
public int Version { get; set; } public int Version { get; set; }
@ -224,14 +224,23 @@ namespace Ryujinx.Configuration
public KeyboardHotkeys Hotkeys { get; set; } public KeyboardHotkeys Hotkeys { get; set; }
/// <summary> /// <summary>
/// Keyboard control bindings /// Legacy keyboard control bindings
/// </summary> /// </summary>
public List<KeyboardConfig> KeyboardConfig { get; set; } /// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
/// TODO: Remove this when those older versions aren't in use anymore.
public List<object> KeyboardConfig { get; set; }
/// <summary> /// <summary>
/// Controller control bindings /// Legacy controller control bindings
/// </summary> /// </summary>
public List<ControllerConfig> ControllerConfig { get; set; } /// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
/// TODO: Remove this when those older versions aren't in use anymore.
public List<object> ControllerConfig { get; set; }
/// <summary>
/// Input configurations
/// </summary>
public List<InputConfig> InputConfig { get; set; }
/// <summary> /// <summary>
/// Loads a configuration file from disk /// Loads a configuration file from disk

View file

@ -1,8 +1,8 @@
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Configuration.Hid;
using Ryujinx.Configuration.System; using Ryujinx.Configuration.System;
using Ryujinx.Configuration.Ui; using Ryujinx.Configuration.Ui;
using System; using System;
@ -405,21 +405,6 @@ namespace Ryujinx.Configuration
public ConfigurationFileFormat ToFileFormat() public ConfigurationFileFormat ToFileFormat()
{ {
List<ControllerConfig> controllerConfigList = new List<ControllerConfig>();
List<KeyboardConfig> keyboardConfigList = new List<KeyboardConfig>();
foreach (InputConfig inputConfig in Hid.InputConfig.Value)
{
if (inputConfig is ControllerConfig controllerConfig)
{
controllerConfigList.Add(controllerConfig);
}
else if (inputConfig is KeyboardConfig keyboardConfig)
{
keyboardConfigList.Add(keyboardConfig);
}
}
ConfigurationFileFormat configurationFile = new ConfigurationFileFormat ConfigurationFileFormat configurationFile = new ConfigurationFileFormat
{ {
Version = ConfigurationFileFormat.CurrentVersion, Version = ConfigurationFileFormat.CurrentVersion,
@ -479,8 +464,9 @@ namespace Ryujinx.Configuration
StartFullscreen = Ui.StartFullscreen, StartFullscreen = Ui.StartFullscreen,
EnableKeyboard = Hid.EnableKeyboard, EnableKeyboard = Hid.EnableKeyboard,
Hotkeys = Hid.Hotkeys, Hotkeys = Hid.Hotkeys,
KeyboardConfig = keyboardConfigList, KeyboardConfig = new List<object>(),
ControllerConfig = controllerConfigList ControllerConfig = new List<object>(),
InputConfig = Hid.InputConfig,
}; };
return configurationFile; return configurationFile;
@ -543,54 +529,57 @@ namespace Ryujinx.Configuration
}; };
Hid.InputConfig.Value = new List<InputConfig> Hid.InputConfig.Value = new List<InputConfig>
{ {
new KeyboardConfig new StandardKeyboardInputConfig
{ {
Index = 0, Version = InputConfig.CurrentVersion,
ControllerType = ControllerType.JoyconPair, Backend = InputBackendType.WindowKeyboard,
PlayerIndex = PlayerIndex.Player1, Id = "0",
LeftJoycon = new NpadKeyboardLeft PlayerIndex = PlayerIndex.Player1,
{ ControllerType = ControllerType.JoyconPair,
StickUp = Key.W, LeftJoycon = new LeftJoyconCommonConfig<Key>
StickDown = Key.S, {
StickLeft = Key.A, DpadUp = Key.Up,
StickRight = Key.D, DpadDown = Key.Down,
StickButton = Key.F, DpadLeft = Key.Left,
DPadUp = Key.Up, DpadRight = Key.Right,
DPadDown = Key.Down, ButtonMinus = Key.Minus,
DPadLeft = Key.Left, ButtonL = Key.E,
DPadRight = Key.Right, ButtonZl = Key.Q,
ButtonMinus = Key.Minus, ButtonSl = Key.Unbound,
ButtonL = Key.E, ButtonSr = Key.Unbound
ButtonZl = Key.Q, },
ButtonSl = Key.Home,
ButtonSr = Key.End LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
}, {
RightJoycon = new NpadKeyboardRight StickUp = Key.W,
{ StickDown = Key.S,
StickUp = Key.I, StickLeft = Key.A,
StickDown = Key.K, StickRight = Key.D,
StickLeft = Key.J, StickButton = Key.F,
StickRight = Key.L, },
StickButton = Key.H,
ButtonA = Key.Z, RightJoycon = new RightJoyconCommonConfig<Key>
ButtonB = Key.X, {
ButtonX = Key.C, ButtonA = Key.Z,
ButtonY = Key.V, ButtonB = Key.X,
ButtonPlus = Key.Plus, ButtonX = Key.C,
ButtonR = Key.U, ButtonY = Key.V,
ButtonZr = Key.O, ButtonPlus = Key.Plus,
ButtonSl = Key.PageUp, ButtonR = Key.U,
ButtonSr = Key.PageDown ButtonZr = Key.O,
}, ButtonSl = Key.Unbound,
EnableMotion = false, ButtonSr = Key.Unbound
MirrorInput = false, },
Slot = 0,
AltSlot = 0, RightJoyconStick = new JoyconConfigKeyboardStick<Key>
Sensitivity = 100, {
GyroDeadzone = 1, StickUp = Key.I,
DsuServerHost = "127.0.0.1", StickDown = Key.K,
DsuServerPort = 26760 StickLeft = Key.J,
} StickRight = Key.L,
StickButton = Key.H,
}
}
}; };
} }
@ -643,80 +632,6 @@ namespace Ryujinx.Configuration
configurationFileUpdated = true; configurationFileUpdated = true;
} }
if (configurationFileFormat.Version < 6)
{
Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 6.");
configurationFileFormat.ControllerConfig = new List<ControllerConfig>();
configurationFileFormat.KeyboardConfig = new List<KeyboardConfig>
{
new KeyboardConfig
{
Index = 0,
ControllerType = ControllerType.JoyconPair,
PlayerIndex = PlayerIndex.Player1,
LeftJoycon = new NpadKeyboardLeft
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
DPadUp = Key.Up,
DPadDown = Key.Down,
DPadLeft = Key.Left,
DPadRight = Key.Right,
ButtonMinus = Key.Minus,
ButtonL = Key.E,
ButtonZl = Key.Q,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound
},
RightJoycon = new NpadKeyboardRight
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
ButtonA = Key.Z,
ButtonB = Key.X,
ButtonX = Key.C,
ButtonY = Key.V,
ButtonPlus = Key.Plus,
ButtonR = Key.U,
ButtonZr = Key.O,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound
},
EnableMotion = false,
MirrorInput = false,
Slot = 0,
AltSlot = 0,
Sensitivity = 100,
GyroDeadzone = 1,
DsuServerHost = "127.0.0.1",
DsuServerPort = 26760
}
};
configurationFileUpdated = true;
}
// Only needed for version 6 configurations.
if (configurationFileFormat.Version == 6)
{
Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 7.");
for (int i = 0; i < configurationFileFormat.KeyboardConfig.Count; i++)
{
if (configurationFileFormat.KeyboardConfig[i].Index != KeyboardConfig.AllKeyboardsIndex)
{
configurationFileFormat.KeyboardConfig[i].Index++;
}
}
}
if (configurationFileFormat.Version < 8) if (configurationFileFormat.Version < 8)
{ {
Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8.");
@ -826,9 +741,67 @@ namespace Ryujinx.Configuration
configurationFileUpdated = true; configurationFileUpdated = true;
} }
List<InputConfig> inputConfig = new List<InputConfig>(); if (configurationFileFormat.Version < 24)
inputConfig.AddRange(configurationFileFormat.ControllerConfig); {
inputConfig.AddRange(configurationFileFormat.KeyboardConfig); Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24.");
configurationFileFormat.InputConfig = new List<InputConfig>
{
new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = "0",
PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.JoyconPair,
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
DpadUp = Key.Up,
DpadDown = Key.Down,
DpadLeft = Key.Left,
DpadRight = Key.Right,
ButtonMinus = Key.Minus,
ButtonL = Key.E,
ButtonZl = Key.Q,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound
},
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
{
ButtonA = Key.Z,
ButtonB = Key.X,
ButtonX = Key.C,
ButtonY = Key.V,
ButtonPlus = Key.Plus,
ButtonR = Key.U,
ButtonZr = Key.O,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound
},
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
}
}
};
configurationFileUpdated = true;
}
Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
@ -880,7 +853,12 @@ namespace Ryujinx.Configuration
Ui.StartFullscreen.Value = configurationFileFormat.StartFullscreen; Ui.StartFullscreen.Value = configurationFileFormat.StartFullscreen;
Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard;
Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; Hid.Hotkeys.Value = configurationFileFormat.Hotkeys;
Hid.InputConfig.Value = inputConfig; Hid.InputConfig.Value = configurationFileFormat.InputConfig;
if (Hid.InputConfig.Value == null)
{
Hid.InputConfig.Value = new List<InputConfig>();
}
if (configurationFileUpdated) if (configurationFileUpdated)
{ {

View file

@ -0,0 +1,54 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public enum GamepadInputId : byte
{
Unbound,
A,
B,
X,
Y,
LeftStick,
RightStick,
LeftShoulder,
RightShoulder,
// Likely axis
LeftTrigger,
// Likely axis
RightTrigger,
DpadUp,
DpadDown,
DpadLeft,
DpadRight,
// Special buttons
Minus,
Plus,
Back = Minus,
Start = Plus,
Guide,
Misc1,
// Xbox Elite paddle
Paddle1,
Paddle2,
Paddle3,
Paddle4,
// PS5 touchpad button
Touchpad,
// Virtual buttons for single joycon
SingleLeftTrigger0,
SingleRightTrigger0,
SingleLeftTrigger1,
SingleRightTrigger1,
Count
}
}

View file

@ -0,0 +1,37 @@
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public class GenericControllerInputConfig<Button, Stick> : GenericInputConfigurationCommon<Button> where Button : unmanaged where Stick : unmanaged
{
/// <summary>
/// Left JoyCon Controller Stick Bindings
/// </summary>
public JoyconConfigControllerStick<Button, Stick> LeftJoyconStick { get; set; }
/// <summary>
/// Right JoyCon Controller Stick Bindings
/// </summary>
public JoyconConfigControllerStick<Button, Stick> RightJoyconStick { get; set; }
/// <summary>
/// Controller Left Analog Stick Deadzone
/// </summary>
public float DeadzoneLeft { get; set; }
/// <summary>
/// Controller Right Analog Stick Deadzone
/// </summary>
public float DeadzoneRight { get; set; }
/// <summary>
/// Controller Trigger Threshold
/// </summary>
public float TriggerThreshold { get; set; }
/// <summary>
/// Controller Motion Settings
/// </summary>
public MotionConfigController Motion { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public class JoyconConfigControllerStick<Button, Stick> where Button: unmanaged where Stick: unmanaged
{
public Stick Joystick { get; set; }
public bool InvertStickX { get; set; }
public bool InvertStickY { get; set; }
public Button StickButton { get; set; }
}
}

View file

@ -0,0 +1,30 @@
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{
public class CemuHookMotionConfigController : MotionConfigController
{
/// <summary>
/// Motion Controller Slot
/// </summary>
public int Slot { get; set; }
/// <summary>
/// Motion Controller Alternative Slot, for RightJoyCon in Pair mode
/// </summary>
public int AltSlot { get; set; }
/// <summary>
/// Mirror motion input in Pair mode
/// </summary>
public bool MirrorInput { get; set; }
/// <summary>
/// Host address of the DSU Server
/// </summary>
public string DsuServerHost { get; set; }
/// <summary>
/// Port of the DSU Server
/// </summary>
public int DsuServerPort { get; set; }
}
}

View file

@ -0,0 +1,76 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{
class JsonMotionConfigControllerConverter : JsonConverter<MotionConfigController>
{
private static MotionInputBackendType GetMotionInputBackendType(ref Utf8JsonReader reader)
{
// Temporary reader to get the backend type
Utf8JsonReader tempReader = reader;
MotionInputBackendType result = MotionInputBackendType.Invalid;
while (tempReader.Read())
{
// NOTE: We scan all properties ignoring the depth entirely on purpose.
// The reason behind this is that we cannot track in a reliable way the depth of the object because Utf8JsonReader never emit the first TokenType == StartObject if the json start with an object.
// As such, this code will try to parse very field named "motion_backend" to the correct enum.
if (tempReader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = tempReader.GetString();
if (propertyName.Equals("motion_backend"))
{
tempReader.Read();
if (tempReader.TokenType == JsonTokenType.String)
{
string backendTypeRaw = tempReader.GetString();
if (!Enum.TryParse(backendTypeRaw, out result))
{
result = MotionInputBackendType.Invalid;
}
else
{
break;
}
}
}
}
}
return result;
}
public override MotionConfigController Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
MotionInputBackendType motionBackendType = GetMotionInputBackendType(ref reader);
return motionBackendType switch
{
MotionInputBackendType.GamepadDriver => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(StandardMotionConfigController), options),
MotionInputBackendType.CemuHook => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(CemuHookMotionConfigController), options),
_ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"),
};
}
public override void Write(Utf8JsonWriter writer, MotionConfigController value, JsonSerializerOptions options)
{
switch (value.MotionBackend)
{
case MotionInputBackendType.GamepadDriver:
JsonSerializer.Serialize(writer, value as StandardMotionConfigController, options);
break;
case MotionInputBackendType.CemuHook:
JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, options);
break;
default:
throw new ArgumentException($"Unknown motion backend type {value.MotionBackend}");
}
}
}
}

View file

@ -0,0 +1,22 @@
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{
public class MotionConfigController
{
public MotionInputBackendType MotionBackend { get; set; }
/// <summary>
/// Gyro Sensitivity
/// </summary>
public int Sensitivity { get; set; }
/// <summary>
/// Gyro Deadzone
/// </summary>
public double GyroDeadzone { get; set; }
/// <summary>
/// Enable Motion Controls
/// </summary>
public bool EnableMotion { get; set; }
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{
public enum MotionInputBackendType : byte
{
Invalid,
GamepadDriver,
CemuHook
}
}

View file

@ -0,0 +1,4 @@
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{
public class StandardMotionConfigController : MotionConfigController { }
}

View file

@ -0,0 +1,4 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public class StandardControllerInputConfig : GenericControllerInputConfig<GamepadInputId, StickInputId> { }
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public enum StickInputId : byte
{
Unbound,
Left,
Right,
Count
}
}

View file

@ -1,30 +0,0 @@
namespace Ryujinx.Common.Configuration.Hid
{
public class ControllerConfig : InputConfig
{
/// <summary>
/// Controller Left Analog Stick Deadzone
/// </summary>
public float DeadzoneLeft { get; set; }
/// <summary>
/// Controller Right Analog Stick Deadzone
/// </summary>
public float DeadzoneRight { get; set; }
/// <summary>
/// Controller Trigger Threshold
/// </summary>
public float TriggerThreshold { get; set; }
/// <summary>
/// Left JoyCon Controller Bindings
/// </summary>
public NpadControllerLeft LeftJoycon { get; set; }
/// <summary>
/// Right JoyCon Controller Bindings
/// </summary>
public NpadControllerRight RightJoycon { get; set; }
}
}

View file

@ -1,46 +0,0 @@
namespace Ryujinx.Common.Configuration.Hid
{
public enum ControllerInputId
{
Button0,
Button1,
Button2,
Button3,
Button4,
Button5,
Button6,
Button7,
Button8,
Button9,
Button10,
Button11,
Button12,
Button13,
Button14,
Button15,
Button16,
Button17,
Button18,
Button19,
Button20,
Axis0,
Axis1,
Axis2,
Axis3,
Axis4,
Axis5,
Hat0Up,
Hat0Down,
Hat0Left,
Hat0Right,
Hat1Up,
Hat1Down,
Hat1Left,
Hat1Right,
Hat2Up,
Hat2Down,
Hat2Left,
Hat2Right,
Unbound
}
}

View file

@ -0,0 +1,15 @@
namespace Ryujinx.Common.Configuration.Hid
{
public class GenericInputConfigurationCommon<Button> : InputConfig where Button : unmanaged
{
/// <summary>
/// Left JoyCon Controller Bindings
/// </summary>
public LeftJoyconCommonConfig<Button> LeftJoycon { get; set; }
/// <summary>
/// Right JoyCon Controller Bindings
/// </summary>
public RightJoyconCommonConfig<Button> RightJoycon { get; set; }
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Common.Configuration.Hid
{
public enum InputBackendType
{
Invalid,
WindowKeyboard,
GamepadSDL2,
}
}

View file

@ -1,11 +1,20 @@
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
public class InputConfig public class InputConfig
{ {
/// <summary> /// <summary>
/// Controller Device Index /// The current version of the input file format
/// </summary> /// </summary>
public int Index { get; set; } public const int CurrentVersion = 1;
public int Version { get; set; }
public InputBackendType Backend { get; set; }
/// <summary>
/// Controller id
/// </summary>
public string Id { get; set; }
/// <summary> /// <summary>
/// Controller's Type /// Controller's Type
@ -16,45 +25,5 @@ namespace Ryujinx.Common.Configuration.Hid
/// Player's Index for the controller /// Player's Index for the controller
/// </summary> /// </summary>
public PlayerIndex PlayerIndex { get; set; } public PlayerIndex PlayerIndex { get; set; }
/// <summary>
/// Motion Controller Slot
/// </summary>
public int Slot { get; set; }
/// <summary>
/// Motion Controller Alternative Slot, for RightJoyCon in Pair mode
/// </summary>
public int AltSlot { get; set; }
/// <summary>
/// Mirror motion input in Pair mode
/// </summary>
public bool MirrorInput { get; set; }
/// <summary>
/// Host address of the DSU Server
/// </summary>
public string DsuServerHost { get; set; }
/// <summary>
/// Port of the DSU Server
/// </summary>
public int DsuServerPort { get; set; }
/// <summary>
/// Gyro Sensitivity
/// </summary>
public int Sensitivity { get; set; }
/// <summary>
/// Gyro Deadzone
/// </summary>
public double GyroDeadzone { get; set; }
/// <summary>
/// Enable Motion Controls
/// </summary>
public bool EnableMotion { get; set; }
} }
} }

View file

@ -0,0 +1,78 @@
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid
{
class JsonInputConfigConverter : JsonConverter<InputConfig>
{
private static InputBackendType GetInputBackendType(ref Utf8JsonReader reader)
{
// Temporary reader to get the backend type
Utf8JsonReader tempReader = reader;
InputBackendType result = InputBackendType.Invalid;
while (tempReader.Read())
{
// NOTE: We scan all properties ignoring the depth entirely on purpose.
// The reason behind this is that we cannot track in a reliable way the depth of the object because Utf8JsonReader never emit the first TokenType == StartObject if the json start with an object.
// As such, this code will try to parse very field named "backend" to the correct enum.
if (tempReader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = tempReader.GetString();
if (propertyName.Equals("backend"))
{
tempReader.Read();
if (tempReader.TokenType == JsonTokenType.String)
{
string backendTypeRaw = tempReader.GetString();
if (!Enum.TryParse(backendTypeRaw, out result))
{
result = InputBackendType.Invalid;
}
else
{
break;
}
}
}
}
}
return result;
}
public override InputConfig Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
InputBackendType backendType = GetInputBackendType(ref reader);
return backendType switch
{
InputBackendType.WindowKeyboard => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardKeyboardInputConfig), options),
InputBackendType.GamepadSDL2 => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardControllerInputConfig), options),
_ => throw new InvalidOperationException($"Unknown backend type {backendType}"),
};
}
public override void Write(Utf8JsonWriter writer, InputConfig value, JsonSerializerOptions options)
{
switch (value.Backend)
{
case InputBackendType.WindowKeyboard:
JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, options);
break;
case InputBackendType.GamepadSDL2:
JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, options);
break;
default:
throw new ArgumentException($"Unknown backend type {value.Backend}");
}
}
}
}

View file

@ -1,154 +1,139 @@
namespace Ryujinx.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
public enum Key public enum Key
{ {
Unknown = 0, Unknown,
ShiftLeft = 1, ShiftLeft,
LShift = 1, ShiftRight,
ShiftRight = 2, ControlLeft,
RShift = 2, ControlRight,
ControlLeft = 3, AltLeft,
LControl = 3, AltRight,
ControlRight = 4, WinLeft,
RControl = 4, WinRight,
AltLeft = 5, Menu,
LAlt = 5, F1,
AltRight = 6, F2,
RAlt = 6, F3,
WinLeft = 7, F4,
LWin = 7, F5,
WinRight = 8, F6,
RWin = 8, F7,
Menu = 9, F8,
F1 = 10, F9,
F2 = 11, F10,
F3 = 12, F11,
F4 = 13, F12,
F5 = 14, F13,
F6 = 15, F14,
F7 = 16, F15,
F8 = 17, F16,
F9 = 18, F17,
F10 = 19, F18,
F11 = 20, F19,
F12 = 21, F20,
F13 = 22, F21,
F14 = 23, F22,
F15 = 24, F23,
F16 = 25, F24,
F17 = 26, F25,
F18 = 27, F26,
F19 = 28, F27,
F20 = 29, F28,
F21 = 30, F29,
F22 = 31, F30,
F23 = 32, F31,
F24 = 33, F32,
F25 = 34, F33,
F26 = 35, F34,
F27 = 36, F35,
F28 = 37, Up,
F29 = 38, Down,
F30 = 39, Left,
F31 = 40, Right,
F32 = 41, Enter,
F33 = 42, Escape,
F34 = 43, Space,
F35 = 44, Tab,
Up = 45, BackSpace,
Down = 46, Insert,
Left = 47, Delete,
Right = 48, PageUp,
Enter = 49, PageDown,
Escape = 50, Home,
Space = 51, End,
Tab = 52, CapsLock,
BackSpace = 53, ScrollLock,
Back = 53, PrintScreen,
Insert = 54, Pause,
Delete = 55, NumLock,
PageUp = 56, Clear,
PageDown = 57, Keypad0,
Home = 58, Keypad1,
End = 59, Keypad2,
CapsLock = 60, Keypad3,
ScrollLock = 61, Keypad4,
PrintScreen = 62, Keypad5,
Pause = 63, Keypad6,
NumLock = 64, Keypad7,
Clear = 65, Keypad8,
Sleep = 66, Keypad9,
Keypad0 = 67, KeypadDivide,
Keypad1 = 68, KeypadMultiply,
Keypad2 = 69, KeypadSubtract,
Keypad3 = 70, KeypadAdd,
Keypad4 = 71, KeypadDecimal,
Keypad5 = 72, KeypadEnter,
Keypad6 = 73, A,
Keypad7 = 74, B,
Keypad8 = 75, C,
Keypad9 = 76, D,
KeypadDivide = 77, E,
KeypadMultiply = 78, F,
KeypadSubtract = 79, G,
KeypadMinus = 79, H,
KeypadAdd = 80, I,
KeypadPlus = 80, J,
KeypadDecimal = 81, K,
KeypadPeriod = 81, L,
KeypadEnter = 82, M,
A = 83, N,
B = 84, O,
C = 85, P,
D = 86, Q,
E = 87, R,
F = 88, S,
G = 89, T,
H = 90, U,
I = 91, V,
J = 92, W,
K = 93, X,
L = 94, Y,
M = 95, Z,
N = 96, Number0,
O = 97, Number1,
P = 98, Number2,
Q = 99, Number3,
R = 100, Number4,
S = 101, Number5,
T = 102, Number6,
U = 103, Number7,
V = 104, Number8,
W = 105, Number9,
X = 106, Tilde,
Y = 107, Grave,
Z = 108, Minus,
Number0 = 109, Plus,
Number1 = 110, BracketLeft,
Number2 = 111, BracketRight,
Number3 = 112, Semicolon,
Number4 = 113, Quote,
Number5 = 114, Comma,
Number6 = 115, Period,
Number7 = 116, Slash,
Number8 = 117, BackSlash,
Number9 = 118, Unbound,
Tilde = 119,
Grave = 119, Count
Minus = 120,
Plus = 121,
BracketLeft = 122,
LBracket = 122,
BracketRight = 123,
RBracket = 123,
Semicolon = 124,
Quote = 125,
Comma = 126,
Period = 127,
Slash = 128,
BackSlash = 129,
NonUSBackSlash = 130,
LastKey = 131,
Unbound
} }
} }

View file

@ -0,0 +1,15 @@
namespace Ryujinx.Common.Configuration.Hid.Keyboard
{
public class GenericKeyboardInputConfig<Key> : GenericInputConfigurationCommon<Key> where Key : unmanaged
{
/// <summary>
/// Left JoyCon Controller Stick Bindings
/// </summary>
public JoyconConfigKeyboardStick<Key> LeftJoyconStick { get; set; }
/// <summary>
/// Right JoyCon Controller Stick Bindings
/// </summary>
public JoyconConfigKeyboardStick<Key> RightJoyconStick { get; set; }
}
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.Common.Configuration.Hid.Keyboard
{
public class JoyconConfigKeyboardStick<Key> where Key: unmanaged
{
public Key StickUp { get; set; }
public Key StickDown { get; set; }
public Key StickLeft { get; set; }
public Key StickRight { get; set; }
public Key StickButton { get; set; }
}
}

View file

@ -0,0 +1,4 @@
namespace Ryujinx.Common.Configuration.Hid.Keyboard
{
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { }
}

View file

@ -1,18 +0,0 @@
namespace Ryujinx.Common.Configuration.Hid
{
public class KeyboardConfig : InputConfig
{
// DO NOT MODIFY
public const uint AllKeyboardsIndex = 0;
/// <summary>
/// Left JoyCon Keyboard Bindings
/// </summary>
public NpadKeyboardLeft LeftJoycon { get; set; }
/// <summary>
/// Right JoyCon Keyboard Bindings
/// </summary>
public NpadKeyboardRight RightJoycon { get; set; }
}
}

View file

@ -1,6 +1,4 @@
using Ryujinx.Configuration.Hid; namespace Ryujinx.Common.Configuration.Hid
namespace Ryujinx.Common.Configuration.Hid
{ {
public struct KeyboardHotkeys public struct KeyboardHotkeys
{ {

View file

@ -0,0 +1,15 @@
namespace Ryujinx.Common.Configuration.Hid
{
public class LeftJoyconCommonConfig<Button>
{
public Button ButtonMinus { get; set; }
public Button ButtonL { get; set; }
public Button ButtonZl { get; set; }
public Button ButtonSl { get; set; }
public Button ButtonSr { get; set; }
public Button DpadUp { get; set; }
public Button DpadDown { get; set; }
public Button DpadLeft { get; set; }
public Button DpadRight { get; set; }
}
}

View file

@ -1,20 +0,0 @@
namespace Ryujinx.Common.Configuration.Hid
{
public struct NpadControllerLeft
{
public ControllerInputId StickX { get; set; }
public bool InvertStickX { get; set; }
public ControllerInputId StickY { get; set; }
public bool InvertStickY { get; set; }
public ControllerInputId StickButton { get; set; }
public ControllerInputId ButtonMinus { get; set; }
public ControllerInputId ButtonL { get; set; }
public ControllerInputId ButtonZl { get; set; }
public ControllerInputId ButtonSl { get; set; }
public ControllerInputId ButtonSr { get; set; }
public ControllerInputId DPadUp { get; set; }
public ControllerInputId DPadDown { get; set; }
public ControllerInputId DPadLeft { get; set; }
public ControllerInputId DPadRight { get; set; }
}
}

View file

@ -1,20 +0,0 @@
namespace Ryujinx.Common.Configuration.Hid
{
public struct NpadControllerRight
{
public ControllerInputId StickX { get; set; }
public bool InvertStickX { get; set; }
public ControllerInputId StickY { get; set; }
public bool InvertStickY { get; set; }
public ControllerInputId StickButton { get; set; }
public ControllerInputId ButtonA { get; set; }
public ControllerInputId ButtonB { get; set; }
public ControllerInputId ButtonX { get; set; }
public ControllerInputId ButtonY { get; set; }
public ControllerInputId ButtonPlus { get; set; }
public ControllerInputId ButtonR { get; set; }
public ControllerInputId ButtonZr { get; set; }
public ControllerInputId ButtonSl { get; set; }
public ControllerInputId ButtonSr { get; set; }
}
}

View file

@ -1,22 +0,0 @@
using Ryujinx.Configuration.Hid;
namespace Ryujinx.Common.Configuration.Hid
{
public struct NpadKeyboardLeft
{
public Key StickUp { get; set; }
public Key StickDown { get; set; }
public Key StickLeft { get; set; }
public Key StickRight { get; set; }
public Key StickButton { get; set; }
public Key DPadUp { get; set; }
public Key DPadDown { get; set; }
public Key DPadLeft { get; set; }
public Key DPadRight { get; set; }
public Key ButtonMinus { get; set; }
public Key ButtonL { get; set; }
public Key ButtonZl { get; set; }
public Key ButtonSl { get; set; }
public Key ButtonSr { get; set; }
}
}

View file

@ -1,22 +0,0 @@
using Ryujinx.Configuration.Hid;
namespace Ryujinx.Common.Configuration.Hid
{
public struct NpadKeyboardRight
{
public Key StickUp { get; set; }
public Key StickDown { get; set; }
public Key StickLeft { get; set; }
public Key StickRight { get; set; }
public Key StickButton { get; set; }
public Key ButtonA { get; set; }
public Key ButtonB { get; set; }
public Key ButtonX { get; set; }
public Key ButtonY { get; set; }
public Key ButtonPlus { get; set; }
public Key ButtonR { get; set; }
public Key ButtonZr { get; set; }
public Key ButtonSl { get; set; }
public Key ButtonSr { get; set; }
}
}

View file

@ -0,0 +1,15 @@
namespace Ryujinx.Common.Configuration.Hid
{
public class RightJoyconCommonConfig<Button>
{
public Button ButtonPlus { get; set; }
public Button ButtonR { get; set; }
public Button ButtonZr { get; set; }
public Button ButtonSl { get; set; }
public Button ButtonSr { get; set; }
public Button ButtonX { get; set; }
public Button ButtonB { get; set; }
public Button ButtonY { get; set; }
public Button ButtonA { get; set; }
}
}

View file

@ -1,4 +1,6 @@
using System.IO; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -63,6 +65,8 @@ namespace Ryujinx.Common.Utilities
}; };
options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonInputConfigConverter());
options.Converters.Add(new JsonMotionConfigControllerConverter());
return options; return options;
} }

View file

@ -1,37 +1,24 @@
using OpenTK; using Ryujinx.Common;
using OpenTK.Graphics;
using Ryujinx.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
namespace Ryujinx.Graphics.OpenGL namespace Ryujinx.Graphics.OpenGL
{ {
class BackgroundContextWorker : IDisposable unsafe class BackgroundContextWorker : IDisposable
{ {
[ThreadStatic] [ThreadStatic]
public static bool InBackground; public static bool InBackground;
private GameWindow _window;
private GraphicsContext _context;
private Thread _thread; private Thread _thread;
private bool _running; private bool _running;
private AutoResetEvent _signal; private AutoResetEvent _signal;
private Queue<Action> _work; private Queue<Action> _work;
private ObjectPool<ManualResetEventSlim> _invokePool; private ObjectPool<ManualResetEventSlim> _invokePool;
private readonly IOpenGLContext _backgroundContext;
public BackgroundContextWorker(IGraphicsContext baseContext) public BackgroundContextWorker(IOpenGLContext backgroundContext)
{ {
_window = new GameWindow(
100, 100, GraphicsMode.Default,
"Background Window", OpenTK.GameWindowFlags.FixedWindow, OpenTK.DisplayDevice.Default,
3, 3, GraphicsContextFlags.ForwardCompatible, baseContext, false);
_window.Visible = false;
_context = (GraphicsContext)_window.Context;
_context.MakeCurrent(null);
_running = true; _running = true;
_signal = new AutoResetEvent(false); _signal = new AutoResetEvent(false);
@ -40,12 +27,14 @@ namespace Ryujinx.Graphics.OpenGL
_thread = new Thread(Run); _thread = new Thread(Run);
_thread.Start(); _thread.Start();
_backgroundContext = backgroundContext;
} }
private void Run() private void Run()
{ {
InBackground = true; InBackground = true;
_context.MakeCurrent(_window.WindowInfo);
_backgroundContext.MakeCurrent();
while (_running) while (_running)
{ {
@ -66,7 +55,7 @@ namespace Ryujinx.Graphics.OpenGL
} }
} }
_window.Dispose(); _backgroundContext.Dispose();
} }
public void Invoke(Action action) public void Invoke(Action action)
@ -99,4 +88,4 @@ namespace Ryujinx.Graphics.OpenGL
_signal.Dispose(); _signal.Dispose();
} }
} }
} }

View file

@ -0,0 +1,36 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Graphics.OpenGL.Helper
{
[SupportedOSPlatform("linux")]
internal static class GLXHelper
{
private const string LibraryName = "glx.dll";
static GLXHelper()
{
NativeLibrary.SetDllImportResolver(typeof(GLXHelper).Assembly, (name, assembly, path) =>
{
if (name != LibraryName)
{
return IntPtr.Zero;
}
if (!NativeLibrary.TryLoad("libGL.so.1", assembly, path, out IntPtr result))
{
if (!NativeLibrary.TryLoad("libGL.so", assembly, path, out result))
{
return IntPtr.Zero;
}
}
return result;
});
}
[DllImport(LibraryName, EntryPoint = "glXGetCurrentContext")]
public static extern IntPtr GetCurrentContext();
}
}

View file

@ -0,0 +1,15 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.Graphics.OpenGL.Helper
{
[SupportedOSPlatform("windows")]
internal static class WGLHelper
{
private const string LibraryName = "OPENGL32.DLL";
[DllImport(LibraryName, EntryPoint = "wglGetCurrentContext")]
public extern static IntPtr GetCurrentContext();
}
}

View file

@ -0,0 +1,27 @@
using Ryujinx.Graphics.OpenGL.Helper;
using System;
namespace Ryujinx.Graphics.OpenGL
{
public interface IOpenGLContext : IDisposable
{
void MakeCurrent();
// TODO: Support more APIs per platform.
static bool HasContext()
{
if (OperatingSystem.IsWindows())
{
return WGLHelper.GetCurrentContext() != IntPtr.Zero;
}
else if (OperatingSystem.IsLinux())
{
return GLXHelper.GetCurrentContext() != IntPtr.Zero;
}
else
{
return false;
}
}
}
}

View file

@ -149,7 +149,7 @@ namespace Ryujinx.Graphics.OpenGL
public void BackgroundContextAction(Action action) public void BackgroundContextAction(Action action)
{ {
if (GraphicsContext.CurrentContext != null) if (IOpenGLContext.HasContext())
{ {
action(); // We have a context already - use that (assuming it is the main one). action(); // We have a context already - use that (assuming it is the main one).
} }
@ -159,7 +159,7 @@ namespace Ryujinx.Graphics.OpenGL
} }
} }
public void InitializeBackgroundContext(IGraphicsContext baseContext) public void InitializeBackgroundContext(IOpenGLContext baseContext)
{ {
_window.InitializeBackgroundContext(baseContext); _window.InitializeBackgroundContext(baseContext);
} }

View file

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="OpenTK.NetStandard" Version="1.0.5.32" /> <PackageReference Include="OpenTK.Graphics" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -125,7 +125,7 @@ namespace Ryujinx.Graphics.OpenGL
// Remove Alpha channel // Remove Alpha channel
GL.ColorMask(drawFramebuffer, false, false, false, true); GL.ColorMask(drawFramebuffer, false, false, false, true);
GL.ClearBuffer(ClearBuffer.Color, 0, new float[] { 0.0f, 0.0f, 0.0f, 1.0f }); GL.ClearBuffer(ClearBuffer.Color, drawFramebuffer, new float[] { 0.0f, 0.0f, 0.0f, 1.0f });
GL.ColorMask(drawFramebuffer, GL.ColorMask(drawFramebuffer,
oldFramebufferColorWritemask[0], oldFramebufferColorWritemask[0],
oldFramebufferColorWritemask[1], oldFramebufferColorWritemask[1],
@ -158,7 +158,7 @@ namespace Ryujinx.Graphics.OpenGL
return handle; return handle;
} }
public void InitializeBackgroundContext(IGraphicsContext baseContext) public void InitializeBackgroundContext(IOpenGLContext baseContext)
{ {
BackgroundContext = new BackgroundContextWorker(baseContext); BackgroundContext = new BackgroundContextWorker(baseContext);
} }

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.SDL2-CS" Version="1.0.225-alpha" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,170 @@
using Ryujinx.Common.Logging;
using System;
using System.IO;
using System.Threading;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
class SDL2Driver : IDisposable
{
private static SDL2Driver _instance;
public static bool IsInitialized => _instance != null;
public static SDL2Driver Instance
{
get
{
if (_instance == null)
{
_instance = new SDL2Driver();
}
return _instance;
}
}
private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_SENSOR;
private bool _isRunning;
private uint _refereceCount;
private Thread _worker;
public event Action<int, int> OnJoyStickConnected;
public event Action<int> OnJoystickDisconnected;
private object _lock = new object();
private SDL2Driver() {}
public void Initialize()
{
lock (_lock)
{
_refereceCount++;
if (_isRunning)
{
return;
}
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// TODO: Add this in nuget package once SDL2 2.0.15 hit stable release.
SDL_SetHint("SDL_JOYSTICK_HIDAPI_SWITCH_HOME_LED", "0");
SDL_SetHint("SDL_JOYSTICK_HIDAPI_JOY_CONS", "1");
if (SDL_Init(SdlInitFlags) != 0)
{
string errorMessage = $"SDL2 initlaization failed with error \"{SDL_GetError()}\"";
Logger.Error?.Print(LogClass.Application, errorMessage);
throw new Exception(errorMessage);
}
// First ensure that we only enable joystick events (for connected/disconnected).
SDL_GameControllerEventState(SDL_DISABLE);
SDL_JoystickEventState(SDL_ENABLE);
// Disable all joysticks information, we don't need them no need to flood the event queue for that.
SDL_EventState(SDL_EventType.SDL_JOYAXISMOTION, SDL_DISABLE);
SDL_EventState(SDL_EventType.SDL_JOYBALLMOTION, SDL_DISABLE);
SDL_EventState(SDL_EventType.SDL_JOYHATMOTION, SDL_DISABLE);
SDL_EventState(SDL_EventType.SDL_JOYBUTTONDOWN, SDL_DISABLE);
SDL_EventState(SDL_EventType.SDL_JOYBUTTONUP, SDL_DISABLE);
SDL_EventState(SDL_EventType.SDL_CONTROLLERSENSORUPDATE, SDL_DISABLE);
string gamepadDbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SDL_GameControllerDB.txt");
if (File.Exists(gamepadDbPath))
{
SDL_GameControllerAddMappingsFromFile(gamepadDbPath);
}
_worker = new Thread(EventWorker);
_isRunning = true;
_worker.Start();
}
}
private void HandleSDLEvent(ref SDL_Event evnt)
{
if (evnt.type == SDL_EventType.SDL_JOYDEVICEADDED)
{
int deviceId = evnt.cbutton.which;
// SDL2 loves to be inconsistent here by providing the device id instead of the instance id (like on removed event), as such we just grab it and send it inside our system.
int instanceId = SDL_JoystickGetDeviceInstanceID(deviceId);
if (instanceId == -1)
{
return;
}
Logger.Debug?.Print(LogClass.Application, $"Added joystick instance id {instanceId}");
OnJoyStickConnected?.Invoke(deviceId, instanceId);
}
else if (evnt.type == SDL_EventType.SDL_JOYDEVICEREMOVED)
{
Logger.Debug?.Print(LogClass.Application, $"Removed joystick instance id {evnt.cbutton.which}");
OnJoystickDisconnected?.Invoke(evnt.cbutton.which);
}
}
private void EventWorker()
{
const int WaitTimeMs = 10;
using ManualResetEventSlim waitHandle = new ManualResetEventSlim(false);
while (_isRunning)
{
while (SDL_PollEvent(out SDL_Event evnt) != 0)
{
HandleSDLEvent(ref evnt);
}
waitHandle.Wait(WaitTimeMs);
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
lock (_lock)
{
if (_isRunning)
{
_refereceCount--;
if (_refereceCount == 0)
{
_isRunning = false;
_worker?.Join();
SDL_Quit();
OnJoyStickConnected = null;
OnJoystickDisconnected = null;
}
}
}
}
public void Dispose()
{
Dispose(true);
}
}
}

View file

@ -0,0 +1,366 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using System;
using System.Collections.Generic;
using System.Numerics;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
class SDL2Gamepad : IGamepad
{
private bool HasConfiguration => _configuration != null;
private class ButtonMappingEntry
{
public readonly GamepadButtonInputId To;
public readonly GamepadButtonInputId From;
public ButtonMappingEntry(GamepadButtonInputId to, GamepadButtonInputId from)
{
To = to;
From = from;
}
}
private StandardControllerInputConfig _configuration;
private static readonly SDL_GameControllerButton[] _buttonsDriverMapping = new SDL_GameControllerButton[(int)GamepadButtonInputId.Count]
{
// Unbound, ignored.
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSTICK,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
// NOTE: The left and right trigger are axis, we handle those differently
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_UP,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_DOWN,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_LEFT,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_BACK,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_GUIDE,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_MISC1,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE1,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE2,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE3,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE4,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_TOUCHPAD,
// Virtual buttons are invalid, ignored.
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
};
private object _userMappingLock = new object();
private List<ButtonMappingEntry> _buttonsUserMapping;
private StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
{
StickInputId.Unbound,
StickInputId.Left,
StickInputId.Right
};
public GamepadFeaturesFlag Features { get; }
private IntPtr _gamepadHandle;
private float _triggerThreshold;
public SDL2Gamepad(IntPtr gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_buttonsUserMapping = new List<ButtonMappingEntry>();
Name = SDL_GameControllerName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
_triggerThreshold = 0.0f;
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, SDL_bool.SDL_TRUE);
SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, SDL_bool.SDL_TRUE);
}
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) == SDL_bool.SDL_TRUE &&
SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Motion;
}
int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);
if (error == 0)
{
result |= GamepadFeaturesFlag.Rumble;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GameControllerGetAttached(_gamepadHandle) == SDL_bool.SDL_TRUE;
protected virtual void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != IntPtr.Zero)
{
SDL_GameControllerClose(_gamepadHandle);
_gamepadHandle = IntPtr.Zero;
}
}
public void Dispose()
{
Dispose(true);
}
public void SetTriggerThreshold(float triggerThreshold)
{
_triggerThreshold = triggerThreshold;
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (Features.HasFlag(GamepadFeaturesFlag.Rumble))
{
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs);
}
}
public Vector3 GetMotionData(MotionInputId inputId)
{
SDL_SensorType sensorType = SDL_SensorType.SDL_SENSOR_INVALID;
if (inputId == MotionInputId.Accelerometer)
{
sensorType = SDL_SensorType.SDL_SENSOR_ACCEL;
}
else if (inputId == MotionInputId.Gyroscope)
{
sensorType = SDL_SensorType.SDL_SENSOR_GYRO;
}
if (Features.HasFlag(GamepadFeaturesFlag.Motion) && sensorType != SDL_SensorType.SDL_SENSOR_INVALID)
{
const int ElementCount = 3;
unsafe
{
float* values = stackalloc float[ElementCount];
int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (IntPtr)values, ElementCount);
if (result == 0)
{
Vector3 value = new Vector3(values[0], values[1], values[2]);
if (inputId == MotionInputId.Gyroscope)
{
return RadToDegree(value);
}
else if (inputId == MotionInputId.Accelerometer)
{
return GsToMs2(value);
}
return value;
}
}
}
return Vector3.Zero;
}
private static Vector3 RadToDegree(Vector3 rad)
{
return rad * (180 / MathF.PI);
}
private static Vector3 GsToMs2(Vector3 gs)
{
return gs / SDL_STANDARD_GRAVITY;
}
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
_buttonsUserMapping.Clear();
// First update sticks
_stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
_stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
// Then left joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
// Finally right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
SetTriggerThreshold(_configuration.TriggerThreshold);
}
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
GamepadStateSnapshot rawState = GetStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (_buttonsUserMapping.Count == 0)
{
return rawState;
}
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (entry.From == GamepadButtonInputId.Unbound || entry.To == GamepadButtonInputId.Unbound)
{
continue;
}
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
(float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
result.SetStick(StickInputId.Left, leftStickX, leftStickY);
result.SetStick(StickInputId.Right, rightStickX, rightStickY);
}
return result;
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
public (float, float) GetStick(StickInputId inputId)
{
if (inputId == StickInputId.Unbound)
{
return (0.0f, 0.0f);
}
short stickX;
short stickY;
if (inputId == StickInputId.Left)
{
stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX);
stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY);
}
else if (inputId == StickInputId.Right)
{
stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTX);
stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTY);
}
else
{
throw new NotSupportedException($"Unsupported stick {inputId}");
}
float resultX = ConvertRawStickValue(stickX);
float resultY = -ConvertRawStickValue(stickY);
if (HasConfiguration)
{
if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickX) ||
(inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickX))
{
resultX = -resultX;
}
if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickY) ||
(inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickY))
{
resultY = -resultY;
}
}
return (resultX, resultY);
}
public bool IsPressed(GamepadButtonInputId inputId)
{
if (inputId == GamepadButtonInputId.LeftTrigger)
{
return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) > _triggerThreshold;
}
else if (inputId == GamepadButtonInputId.RightTrigger)
{
return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > _triggerThreshold;
}
else if (_buttonsDriverMapping[(int)inputId] == SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID)
{
return false;
}
else
{
return SDL_GameControllerGetButton(_gamepadHandle, _buttonsDriverMapping[(int)inputId]) == 1;
}
}
}
}

View file

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
public class SDL2GamepadDriver : IGamepadDriver
{
private Dictionary<int, string> _gamepadsInstanceIdsMapping;
private List<string> _gamepadsIds;
public ReadOnlySpan<string> GamepadsIds => _gamepadsIds.ToArray();
public string DriverName => "SDL2";
public event Action<string> OnGamepadConnected;
public event Action<string> OnGamepadDisconnected;
public SDL2GamepadDriver()
{
_gamepadsInstanceIdsMapping = new Dictionary<int, string>();
_gamepadsIds = new List<string>();
SDL2Driver.Instance.Initialize();
SDL2Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
SDL2Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
// Add already connected gamepads
for (int joystickIndex = 0; joystickIndex < SDL_NumJoysticks(); joystickIndex++)
{
HandleJoyStickConnected(joystickIndex, SDL_JoystickGetDeviceInstanceID(joystickIndex));
}
}
private string GenerateGamepadId(int joystickIndex)
{
Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex);
if (guid == Guid.Empty)
{
return null;
}
return joystickIndex + "-" + guid.ToString();
}
private int GetJoystickIndexByGamepadId(string id)
{
string[] data = id.Split("-");
if (data.Length != 6 || !int.TryParse(data[0], out int joystickIndex))
{
return -1;
}
return joystickIndex;
}
private void HandleJoyStickDisconnected(int joystickInstanceId)
{
if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id))
{
_gamepadsInstanceIdsMapping.Remove(joystickInstanceId);
_gamepadsIds.Remove(id);
OnGamepadDisconnected?.Invoke(id);
}
}
private void HandleJoyStickConnected(int joystickDeviceId, int joystickInstanceId)
{
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
{
string id = GenerateGamepadId(joystickDeviceId);
if (id == null)
{
return;
}
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
{
_gamepadsIds.Add(id);
OnGamepadConnected?.Invoke(id);
}
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
// Simulate a full disconnect when disposing
foreach (string id in _gamepadsIds)
{
OnGamepadDisconnected?.Invoke(id);
}
_gamepadsIds.Clear();
SDL2Driver.Instance.Dispose();
}
}
public void Dispose()
{
Dispose(true);
}
public IGamepad GetGamepad(string id)
{
int joystickIndex = GetJoystickIndexByGamepadId(id);
if (joystickIndex == -1)
{
return null;
}
if (id != GenerateGamepadId(joystickIndex))
{
return null;
}
IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex);
if (gamepadHandle == IntPtr.Zero)
{
return null;
}
return new SDL2Gamepad(gamepadHandle, id);
}
}
}

View file

@ -0,0 +1,198 @@
using System.Collections.Generic;
using System;
using System.IO;
using System.Linq;
namespace Ryujinx.Input.Assigner
{
/// <summary>
/// <see cref="IButtonAssigner"/> implementation for regular <see cref="IGamepad"/>.
/// </summary>
public class GamepadButtonAssigner : IButtonAssigner
{
private IGamepad _gamepad;
private GamepadStateSnapshot _currState;
private GamepadStateSnapshot _prevState;
private JoystickButtonDetector _detector;
private bool _forStick;
public GamepadButtonAssigner(IGamepad gamepad, float triggerThreshold, bool forStick)
{
_gamepad = gamepad;
_detector = new JoystickButtonDetector();
_forStick = forStick;
_gamepad?.SetTriggerThreshold(triggerThreshold);
}
public void Initialize()
{
if (_gamepad != null)
{
_currState = _gamepad.GetStateSnapshot();
_prevState = _currState;
}
}
public void ReadInput()
{
if (_gamepad != null)
{
_prevState = _currState;
_currState = _gamepad.GetStateSnapshot();
}
CollectButtonStats();
}
public bool HasAnyButtonPressed()
{
return _detector.HasAnyButtonPressed();
}
public bool ShouldCancel()
{
return _gamepad == null || !_gamepad.IsConnected;
}
public string GetPressedButton()
{
IEnumerable<GamepadButtonInputId> pressedButtons = _detector.GetPressedButtons();
if (pressedButtons.Any())
{
return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString();
}
return "";
}
private void CollectButtonStats()
{
if (_forStick)
{
for (StickInputId inputId = StickInputId.Left; inputId < StickInputId.Count; inputId++)
{
(float x, float y) = _currState.GetStick(inputId);
float value;
if (x != 0.0f)
{
value = x;
}
else if (y != 0.0f)
{
value = y;
}
else
{
continue;
}
_detector.AddInput((GamepadButtonInputId)inputId, value);
}
}
else
{
for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
{
if (_currState.IsPressed(inputId) && !_prevState.IsPressed(inputId))
{
_detector.AddInput(inputId, 1);
}
if (!_currState.IsPressed(inputId) && _prevState.IsPressed(inputId))
{
_detector.AddInput(inputId, -1);
}
}
}
}
private class JoystickButtonDetector
{
private Dictionary<GamepadButtonInputId, InputSummary> _stats;
public JoystickButtonDetector()
{
_stats = new Dictionary<GamepadButtonInputId, InputSummary>();
}
public bool HasAnyButtonPressed()
{
return _stats.Values.Any(CheckButtonPressed);
}
public IEnumerable<GamepadButtonInputId> GetPressedButtons()
{
return _stats.Where(kvp => CheckButtonPressed(kvp.Value)).Select(kvp => kvp.Key);
}
public void AddInput(GamepadButtonInputId button, float value)
{
InputSummary inputSummary;
if (!_stats.TryGetValue(button, out inputSummary))
{
inputSummary = new InputSummary();
_stats.Add(button, inputSummary);
}
inputSummary.AddInput(value);
}
public override string ToString()
{
StringWriter writer = new StringWriter();
foreach (var kvp in _stats)
{
writer.WriteLine($"Button {kvp.Key} -> {kvp.Value}");
}
return writer.ToString();
}
private bool CheckButtonPressed(InputSummary sequence)
{
float distance = Math.Abs(sequence.Min - sequence.Avg) + Math.Abs(sequence.Max - sequence.Avg);
return distance > 1.5; // distance range [0, 2]
}
}
private class InputSummary
{
public float Min, Max, Sum, Avg;
public int NumSamples;
public InputSummary()
{
Min = float.MaxValue;
Max = float.MinValue;
Sum = 0;
NumSamples = 0;
Avg = 0;
}
public void AddInput(float value)
{
Min = Math.Min(Min, value);
Max = Math.Max(Max, value);
Sum += value;
NumSamples += 1;
Avg = Sum / NumSamples;
}
public override string ToString()
{
return $"Avg: {Avg} Min: {Min} Max: {Max} Sum: {Sum} NumSamples: {NumSamples}";
}
}
}
}

View file

@ -0,0 +1,36 @@
namespace Ryujinx.Input.Assigner
{
/// <summary>
/// An interface that allows to gather the driver input info to assign to a button on the UI.
/// </summary>
public interface IButtonAssigner
{
/// <summary>
/// Initialize the button assigner.
/// </summary>
void Initialize();
/// <summary>
/// Read input.
/// </summary>
void ReadInput();
/// <summary>
/// Check if a button was pressed.
/// </summary>
/// <returns>True if a button was pressed</returns>
bool HasAnyButtonPressed();
/// <summary>
/// Indicate if the user of this API should cancel operations. This is triggered for example when a gamepad get disconnected or when a user cancel assignation operations.
/// </summary>
/// <returns>True if the user of this API should cancel operations</returns>
bool ShouldCancel();
/// <summary>
/// Get the pressed button that was read in <see cref="ReadInput"/> by the button assigner.
/// </summary>
/// <returns>The pressed button that was read</returns>
string GetPressedButton();
}
}

View file

@ -0,0 +1,50 @@
namespace Ryujinx.Input.Assigner
{
/// <summary>
/// <see cref="IButtonAssigner"/> implementation for <see cref="IKeyboard"/>.
/// </summary>
public class KeyboardKeyAssigner : IButtonAssigner
{
private IKeyboard _keyboard;
private KeyboardStateSnapshot _keyboardState;
public KeyboardKeyAssigner(IKeyboard keyboard)
{
_keyboard = keyboard;
}
public void Initialize() { }
public void ReadInput()
{
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
}
public bool HasAnyButtonPressed()
{
return GetPressedButton().Length != 0;
}
public bool ShouldCancel()
{
return _keyboardState.IsPressed(Key.Escape);
}
public string GetPressedButton()
{
string keyPressed = "";
for (Key key = Key.Unknown; key < Key.Count; key++)
{
if (_keyboardState.IsPressed(key))
{
keyPressed = key.ToString();
break;
}
}
return !ShouldCancel() ? keyPressed : "";
}
}
}

View file

@ -0,0 +1,57 @@
namespace Ryujinx.Input
{
/// <summary>
/// Represent a button from a gamepad.
/// </summary>
public enum GamepadButtonInputId : byte
{
Unbound,
A,
B,
X,
Y,
LeftStick,
RightStick,
LeftShoulder,
RightShoulder,
// Likely axis
LeftTrigger,
// Likely axis
RightTrigger,
DpadUp,
DpadDown,
DpadLeft,
DpadRight,
// Special buttons
Minus,
Plus,
Back = Minus,
Start = Plus,
Guide,
Misc1,
// Xbox Elite paddle
Paddle1,
Paddle2,
Paddle3,
Paddle4,
// PS5 touchpad button
Touchpad,
// Virtual buttons for single joycon
SingleLeftTrigger0,
SingleRightTrigger0,
SingleLeftTrigger1,
SingleRightTrigger1,
Count
}
}

View file

@ -0,0 +1,28 @@
using System;
namespace Ryujinx.Input
{
/// <summary>
/// Represent features supported by a <see cref="IGamepad"/>.
/// </summary>
[Flags]
public enum GamepadFeaturesFlag
{
/// <summary>
/// No features are supported
/// </summary>
None,
/// <summary>
/// Rumble
/// </summary>
/// <remarks>Also named haptic</remarks>
Rumble,
/// <summary>
/// Motion
/// <remarks>Also named sixaxis</remarks>
/// </summary>
Motion
}
}

View file

@ -0,0 +1,70 @@
using Ryujinx.Common.Memory;
using System.Runtime.CompilerServices;
namespace Ryujinx.Input
{
/// <summary>
/// A snapshot of a <see cref="IGamepad"/>.
/// </summary>
public struct GamepadStateSnapshot
{
// NOTE: Update Array size if JoystickInputId is changed.
private Array3<Array2<float>> _joysticksState;
// NOTE: Update Array size if GamepadInputId is changed.
private Array28<bool> _buttonsState;
/// <summary>
/// Create a new instance of <see cref="GamepadStateSnapshot"/>.
/// </summary>
/// <param name="joysticksState">The joysticks state</param>
/// <param name="buttonsState">The buttons state</param>
public GamepadStateSnapshot(Array3<Array2<float>> joysticksState, Array28<bool> buttonsState)
{
_joysticksState = joysticksState;
_buttonsState = buttonsState;
}
/// <summary>
/// Check if a given input button is pressed.
/// </summary>
/// <param name="inputId">The button id</param>
/// <returns>True if the given button is pressed</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(GamepadButtonInputId inputId) => _buttonsState[(int)inputId];
/// <summary>
/// Set the state of a given button.
/// </summary>
/// <param name="inputId">The button id</param>
/// <param name="value">The state to assign for the given button.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetPressed(GamepadButtonInputId inputId, bool value) => _buttonsState[(int)inputId] = value;
/// <summary>
/// Get the values of a given input joystick.
/// </summary>
/// <param name="inputId">The stick id</param>
/// <returns>The values of the given input joystick</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public (float, float) GetStick(StickInputId inputId)
{
var result = _joysticksState[(int)inputId];
return (result[0], result[1]);
}
/// <summary>
/// Set the values of a given input joystick.
/// </summary>
/// <param name="inputId">The stick id</param>
/// <param name="x">The x axis value</param>
/// <param name="y">The y axis value</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetStick(StickInputId inputId, float x, float y)
{
_joysticksState[(int)inputId][0] = x;
_joysticksState[(int)inputId][1] = y;
}
}
}

View file

@ -0,0 +1,35 @@
using System;
namespace Ryujinx.Input.HLE
{
public class InputManager : IDisposable
{
public IGamepadDriver KeyboardDriver { get; private set; }
public IGamepadDriver GamepadDriver { get; private set; }
public InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver)
{
KeyboardDriver = keyboardDriver;
GamepadDriver = gamepadDriver;
}
public NpadManager CreateNpadManager()
{
return new NpadManager(KeyboardDriver, GamepadDriver);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
KeyboardDriver?.Dispose();
GamepadDriver?.Dispose();
}
}
public void Dispose()
{
Dispose(true);
}
}
}

View file

@ -0,0 +1,496 @@
using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
using ConfigControllerType = Ryujinx.Common.Configuration.Hid.ControllerType;
namespace Ryujinx.Input.HLE
{
public class NpadController : IDisposable
{
private class HLEButtonMappingEntry
{
public readonly GamepadButtonInputId DriverInputId;
public readonly ControllerKeys HLEInput;
public HLEButtonMappingEntry(GamepadButtonInputId driverInputId, ControllerKeys hleInput)
{
DriverInputId = driverInputId;
HLEInput = hleInput;
}
}
private static readonly HLEButtonMappingEntry[] _hleButtonMapping = new HLEButtonMappingEntry[]
{
new HLEButtonMappingEntry(GamepadButtonInputId.A, ControllerKeys.A),
new HLEButtonMappingEntry(GamepadButtonInputId.B, ControllerKeys.B),
new HLEButtonMappingEntry(GamepadButtonInputId.X, ControllerKeys.X),
new HLEButtonMappingEntry(GamepadButtonInputId.Y, ControllerKeys.Y),
new HLEButtonMappingEntry(GamepadButtonInputId.LeftStick, ControllerKeys.LStick),
new HLEButtonMappingEntry(GamepadButtonInputId.RightStick, ControllerKeys.RStick),
new HLEButtonMappingEntry(GamepadButtonInputId.LeftShoulder, ControllerKeys.L),
new HLEButtonMappingEntry(GamepadButtonInputId.RightShoulder, ControllerKeys.R),
new HLEButtonMappingEntry(GamepadButtonInputId.LeftTrigger, ControllerKeys.Zl),
new HLEButtonMappingEntry(GamepadButtonInputId.RightTrigger, ControllerKeys.Zr),
new HLEButtonMappingEntry(GamepadButtonInputId.DpadUp, ControllerKeys.DpadUp),
new HLEButtonMappingEntry(GamepadButtonInputId.DpadDown, ControllerKeys.DpadDown),
new HLEButtonMappingEntry(GamepadButtonInputId.DpadLeft, ControllerKeys.DpadLeft),
new HLEButtonMappingEntry(GamepadButtonInputId.DpadRight, ControllerKeys.DpadRight),
new HLEButtonMappingEntry(GamepadButtonInputId.Minus, ControllerKeys.Minus),
new HLEButtonMappingEntry(GamepadButtonInputId.Plus, ControllerKeys.Plus),
new HLEButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, ControllerKeys.SlLeft),
new HLEButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, ControllerKeys.SrLeft),
new HLEButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, ControllerKeys.SlRight),
new HLEButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, ControllerKeys.SrRight),
};
private class HLEKeyboardMappingEntry
{
public readonly Key TargetKey;
public readonly byte Target;
public HLEKeyboardMappingEntry(Key targetKey, byte target)
{
TargetKey = targetKey;
Target = target;
}
}
private static readonly HLEKeyboardMappingEntry[] KeyMapping = new HLEKeyboardMappingEntry[]
{
new HLEKeyboardMappingEntry(Key.A, 0x4),
new HLEKeyboardMappingEntry(Key.B, 0x5),
new HLEKeyboardMappingEntry(Key.C, 0x6),
new HLEKeyboardMappingEntry(Key.D, 0x7),
new HLEKeyboardMappingEntry(Key.E, 0x8),
new HLEKeyboardMappingEntry(Key.F, 0x9),
new HLEKeyboardMappingEntry(Key.G, 0xA),
new HLEKeyboardMappingEntry(Key.H, 0xB),
new HLEKeyboardMappingEntry(Key.I, 0xC),
new HLEKeyboardMappingEntry(Key.J, 0xD),
new HLEKeyboardMappingEntry(Key.K, 0xE),
new HLEKeyboardMappingEntry(Key.L, 0xF),
new HLEKeyboardMappingEntry(Key.M, 0x10),
new HLEKeyboardMappingEntry(Key.N, 0x11),
new HLEKeyboardMappingEntry(Key.O, 0x12),
new HLEKeyboardMappingEntry(Key.P, 0x13),
new HLEKeyboardMappingEntry(Key.Q, 0x14),
new HLEKeyboardMappingEntry(Key.R, 0x15),
new HLEKeyboardMappingEntry(Key.S, 0x16),
new HLEKeyboardMappingEntry(Key.T, 0x17),
new HLEKeyboardMappingEntry(Key.U, 0x18),
new HLEKeyboardMappingEntry(Key.V, 0x19),
new HLEKeyboardMappingEntry(Key.W, 0x1A),
new HLEKeyboardMappingEntry(Key.X, 0x1B),
new HLEKeyboardMappingEntry(Key.Y, 0x1C),
new HLEKeyboardMappingEntry(Key.Z, 0x1D),
new HLEKeyboardMappingEntry(Key.Number1, 0x1E),
new HLEKeyboardMappingEntry(Key.Number2, 0x1F),
new HLEKeyboardMappingEntry(Key.Number3, 0x20),
new HLEKeyboardMappingEntry(Key.Number4, 0x21),
new HLEKeyboardMappingEntry(Key.Number5, 0x22),
new HLEKeyboardMappingEntry(Key.Number6, 0x23),
new HLEKeyboardMappingEntry(Key.Number7, 0x24),
new HLEKeyboardMappingEntry(Key.Number8, 0x25),
new HLEKeyboardMappingEntry(Key.Number9, 0x26),
new HLEKeyboardMappingEntry(Key.Number0, 0x27),
new HLEKeyboardMappingEntry(Key.Enter, 0x28),
new HLEKeyboardMappingEntry(Key.Escape, 0x29),
new HLEKeyboardMappingEntry(Key.BackSpace, 0x2A),
new HLEKeyboardMappingEntry(Key.Tab, 0x2B),
new HLEKeyboardMappingEntry(Key.Space, 0x2C),
new HLEKeyboardMappingEntry(Key.Minus, 0x2D),
new HLEKeyboardMappingEntry(Key.Plus, 0x2E),
new HLEKeyboardMappingEntry(Key.BracketLeft, 0x2F),
new HLEKeyboardMappingEntry(Key.BracketRight, 0x30),
new HLEKeyboardMappingEntry(Key.BackSlash, 0x31),
new HLEKeyboardMappingEntry(Key.Tilde, 0x32),
new HLEKeyboardMappingEntry(Key.Semicolon, 0x33),
new HLEKeyboardMappingEntry(Key.Quote, 0x34),
new HLEKeyboardMappingEntry(Key.Grave, 0x35),
new HLEKeyboardMappingEntry(Key.Comma, 0x36),
new HLEKeyboardMappingEntry(Key.Period, 0x37),
new HLEKeyboardMappingEntry(Key.Slash, 0x38),
new HLEKeyboardMappingEntry(Key.CapsLock, 0x39),
new HLEKeyboardMappingEntry(Key.F1, 0x3a),
new HLEKeyboardMappingEntry(Key.F2, 0x3b),
new HLEKeyboardMappingEntry(Key.F3, 0x3c),
new HLEKeyboardMappingEntry(Key.F4, 0x3d),
new HLEKeyboardMappingEntry(Key.F5, 0x3e),
new HLEKeyboardMappingEntry(Key.F6, 0x3f),
new HLEKeyboardMappingEntry(Key.F7, 0x40),
new HLEKeyboardMappingEntry(Key.F8, 0x41),
new HLEKeyboardMappingEntry(Key.F9, 0x42),
new HLEKeyboardMappingEntry(Key.F10, 0x43),
new HLEKeyboardMappingEntry(Key.F11, 0x44),
new HLEKeyboardMappingEntry(Key.F12, 0x45),
new HLEKeyboardMappingEntry(Key.PrintScreen, 0x46),
new HLEKeyboardMappingEntry(Key.ScrollLock, 0x47),
new HLEKeyboardMappingEntry(Key.Pause, 0x48),
new HLEKeyboardMappingEntry(Key.Insert, 0x49),
new HLEKeyboardMappingEntry(Key.Home, 0x4A),
new HLEKeyboardMappingEntry(Key.PageUp, 0x4B),
new HLEKeyboardMappingEntry(Key.Delete, 0x4C),
new HLEKeyboardMappingEntry(Key.End, 0x4D),
new HLEKeyboardMappingEntry(Key.PageDown, 0x4E),
new HLEKeyboardMappingEntry(Key.Right, 0x4F),
new HLEKeyboardMappingEntry(Key.Left, 0x50),
new HLEKeyboardMappingEntry(Key.Down, 0x51),
new HLEKeyboardMappingEntry(Key.Up, 0x52),
new HLEKeyboardMappingEntry(Key.NumLock, 0x53),
new HLEKeyboardMappingEntry(Key.KeypadDivide, 0x54),
new HLEKeyboardMappingEntry(Key.KeypadMultiply, 0x55),
new HLEKeyboardMappingEntry(Key.KeypadSubtract, 0x56),
new HLEKeyboardMappingEntry(Key.KeypadAdd, 0x57),
new HLEKeyboardMappingEntry(Key.KeypadEnter, 0x58),
new HLEKeyboardMappingEntry(Key.Keypad1, 0x59),
new HLEKeyboardMappingEntry(Key.Keypad2, 0x5A),
new HLEKeyboardMappingEntry(Key.Keypad3, 0x5B),
new HLEKeyboardMappingEntry(Key.Keypad4, 0x5C),
new HLEKeyboardMappingEntry(Key.Keypad5, 0x5D),
new HLEKeyboardMappingEntry(Key.Keypad6, 0x5E),
new HLEKeyboardMappingEntry(Key.Keypad7, 0x5F),
new HLEKeyboardMappingEntry(Key.Keypad8, 0x60),
new HLEKeyboardMappingEntry(Key.Keypad9, 0x61),
new HLEKeyboardMappingEntry(Key.Keypad0, 0x62),
new HLEKeyboardMappingEntry(Key.KeypadDecimal, 0x63),
new HLEKeyboardMappingEntry(Key.F13, 0x68),
new HLEKeyboardMappingEntry(Key.F14, 0x69),
new HLEKeyboardMappingEntry(Key.F15, 0x6A),
new HLEKeyboardMappingEntry(Key.F16, 0x6B),
new HLEKeyboardMappingEntry(Key.F17, 0x6C),
new HLEKeyboardMappingEntry(Key.F18, 0x6D),
new HLEKeyboardMappingEntry(Key.F19, 0x6E),
new HLEKeyboardMappingEntry(Key.F20, 0x6F),
new HLEKeyboardMappingEntry(Key.F21, 0x70),
new HLEKeyboardMappingEntry(Key.F22, 0x71),
new HLEKeyboardMappingEntry(Key.F23, 0x72),
new HLEKeyboardMappingEntry(Key.F24, 0x73),
new HLEKeyboardMappingEntry(Key.ControlLeft, 0xE0),
new HLEKeyboardMappingEntry(Key.ShiftLeft, 0xE1),
new HLEKeyboardMappingEntry(Key.AltLeft, 0xE2),
new HLEKeyboardMappingEntry(Key.WinLeft, 0xE3),
new HLEKeyboardMappingEntry(Key.ControlRight, 0xE4),
new HLEKeyboardMappingEntry(Key.ShiftRight, 0xE5),
new HLEKeyboardMappingEntry(Key.AltRight, 0xE6),
new HLEKeyboardMappingEntry(Key.WinRight, 0xE7),
};
private static readonly HLEKeyboardMappingEntry[] KeyModifierMapping = new HLEKeyboardMappingEntry[]
{
new HLEKeyboardMappingEntry(Key.ControlLeft, 0),
new HLEKeyboardMappingEntry(Key.ShiftLeft, 1),
new HLEKeyboardMappingEntry(Key.AltLeft, 2),
new HLEKeyboardMappingEntry(Key.WinLeft, 3),
new HLEKeyboardMappingEntry(Key.ControlRight, 4),
new HLEKeyboardMappingEntry(Key.ShiftRight, 5),
new HLEKeyboardMappingEntry(Key.AltRight, 6),
new HLEKeyboardMappingEntry(Key.WinRight, 7),
new HLEKeyboardMappingEntry(Key.CapsLock, 8),
new HLEKeyboardMappingEntry(Key.ScrollLock, 9),
new HLEKeyboardMappingEntry(Key.NumLock, 10),
};
private bool _isValid;
private string _id;
private MotionInput _motionInput;
private IGamepad _gamepad;
private InputConfig _config;
public IGamepadDriver GamepadDriver { get; private set; }
public GamepadStateSnapshot State { get; private set; }
public string Id => _id;
private CemuHookClient _cemuHookClient;
public NpadController(CemuHookClient cemuHookClient)
{
State = default;
_id = null;
_isValid = false;
_cemuHookClient = cemuHookClient;
}
public bool UpdateDriverConfiguration(IGamepadDriver gamepadDriver, InputConfig config)
{
GamepadDriver = gamepadDriver;
_gamepad?.Dispose();
_id = config.Id;
_gamepad = GamepadDriver.GetGamepad(_id);
_isValid = _gamepad != null;
UpdateUserConfiguration(config);
return _isValid;
}
public void UpdateUserConfiguration(InputConfig config)
{
if (config is StandardControllerInputConfig controllerConfig)
{
bool needsMotionInputUpdate = _config == null || (_config is StandardControllerInputConfig oldControllerConfig &&
(oldControllerConfig.Motion.EnableMotion != controllerConfig.Motion.EnableMotion) &&
(oldControllerConfig.Motion.MotionBackend != controllerConfig.Motion.MotionBackend));
if (needsMotionInputUpdate)
{
UpdateMotionInput(controllerConfig.Motion);
}
}
else
{
// Non-controller doesn't have motions.
_motionInput = null;
}
_config = config;
if (_isValid)
{
_gamepad.SetConfiguration(config);
}
}
private void UpdateMotionInput(MotionConfigController motionConfig)
{
if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
{
_motionInput = new MotionInput();
}
else
{
_motionInput = null;
}
}
public void Update()
{
if (_isValid && GamepadDriver != null)
{
State = _gamepad.GetMappedStateSnapshot();
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion)
{
if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver)
{
if (_gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion))
{
Vector3 accelerometer = _gamepad.GetMotionData(MotionInputId.Accelerometer);
Vector3 gyroscope = _gamepad.GetMotionData(MotionInputId.Gyroscope);
accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y);
gyroscope = new Vector3(gyroscope.X, gyroscope.Z, gyroscope.Y);
_motionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
}
}
else if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook && controllerConfig.Motion is CemuHookMotionConfigController cemuControllerConfig)
{
int clientId = (int)controllerConfig.PlayerIndex;
// First of all ensure we are registered
_cemuHookClient.RegisterClient(clientId, cemuControllerConfig.DsuServerHost, cemuControllerConfig.DsuServerPort);
// Then request data
_cemuHookClient.RequestData(clientId, cemuControllerConfig.Slot);
if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair && !cemuControllerConfig.MirrorInput)
{
_cemuHookClient.RequestData(clientId, cemuControllerConfig.AltSlot);
}
// Finally, get motion input data
_cemuHookClient.TryGetData(clientId, cemuControllerConfig.Slot, out _motionInput);
}
}
}
else
{
// Reset states
State = default;
_motionInput = null;
}
}
private static short ClampAxis(float value)
{
if (value <= -short.MaxValue)
{
return -short.MaxValue;
}
else
{
return (short)(value * short.MaxValue);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static JoystickPosition ApplyDeadzone(float x, float y, float deadzone)
{
return new JoystickPosition
{
Dx = ClampAxis(MathF.Abs(x) > deadzone ? x : 0.0f),
Dy = ClampAxis(MathF.Abs(y) > deadzone ? y : 0.0f)
};
}
public GamepadInput GetHLEInputState()
{
GamepadInput state = new GamepadInput();
// First update all buttons
foreach (HLEButtonMappingEntry entry in _hleButtonMapping)
{
if (State.IsPressed(entry.DriverInputId))
{
state.Buttons |= entry.HLEInput;
}
}
if (_gamepad is IKeyboard)
{
(float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
(float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
state.LStick = new JoystickPosition
{
Dx = ClampAxis(leftAxisX),
Dy = ClampAxis(leftAxisY)
};
state.RStick = new JoystickPosition
{
Dx = ClampAxis(rightAxisX),
Dy = ClampAxis(rightAxisY)
};
}
else if (_config is StandardControllerInputConfig controllerConfig)
{
(float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
(float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
state.LStick = ApplyDeadzone(leftAxisX, leftAxisY, controllerConfig.DeadzoneLeft);
state.RStick = ApplyDeadzone(rightAxisX, rightAxisY, controllerConfig.DeadzoneRight);
}
return state;
}
public SixAxisInput GetHLEMotionState()
{
float[] orientationForHLE = new float[9];
Vector3 gyroscope;
Vector3 accelerometer;
Vector3 rotation;
if (_motionInput != null)
{
gyroscope = Truncate(_motionInput.Gyroscrope * 0.0027f, 3);
accelerometer = Truncate(_motionInput.Accelerometer, 3);
rotation = Truncate(_motionInput.Rotation * 0.0027f, 3);
Matrix4x4 orientation = _motionInput.GetOrientation();
orientationForHLE[0] = Math.Clamp(orientation.M11, -1f, 1f);
orientationForHLE[1] = Math.Clamp(orientation.M12, -1f, 1f);
orientationForHLE[2] = Math.Clamp(orientation.M13, -1f, 1f);
orientationForHLE[3] = Math.Clamp(orientation.M21, -1f, 1f);
orientationForHLE[4] = Math.Clamp(orientation.M22, -1f, 1f);
orientationForHLE[5] = Math.Clamp(orientation.M23, -1f, 1f);
orientationForHLE[6] = Math.Clamp(orientation.M31, -1f, 1f);
orientationForHLE[7] = Math.Clamp(orientation.M32, -1f, 1f);
orientationForHLE[8] = Math.Clamp(orientation.M33, -1f, 1f);
}
else
{
gyroscope = new Vector3();
accelerometer = new Vector3();
rotation = new Vector3();
}
return new SixAxisInput()
{
Accelerometer = accelerometer,
Gyroscope = gyroscope,
Rotation = rotation,
Orientation = orientationForHLE
};
}
private static Vector3 Truncate(Vector3 value, int decimals)
{
float power = MathF.Pow(10, decimals);
value.X = float.IsNegative(value.X) ? MathF.Ceiling(value.X * power) / power : MathF.Floor(value.X * power) / power;
value.Y = float.IsNegative(value.Y) ? MathF.Ceiling(value.Y * power) / power : MathF.Floor(value.Y * power) / power;
value.Z = float.IsNegative(value.Z) ? MathF.Ceiling(value.Z * power) / power : MathF.Floor(value.Z * power) / power;
return value;
}
public KeyboardInput? GetHLEKeyboardInput()
{
if (_gamepad is IKeyboard keyboard)
{
KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot();
KeyboardInput hidKeyboard = new KeyboardInput
{
Modifier = 0,
Keys = new int[0x8]
};
foreach (HLEKeyboardMappingEntry entry in KeyMapping)
{
int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0;
hidKeyboard.Keys[entry.Target / 0x20] |= (value << (entry.Target % 0x20));
}
foreach (HLEKeyboardMappingEntry entry in KeyModifierMapping)
{
int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0;
hidKeyboard.Modifier |= value << entry.Target;
}
return hidKeyboard;
}
return null;
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_gamepad?.Dispose();
}
}
public void Dispose()
{
Dispose(true);
}
}
}

View file

@ -0,0 +1,222 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Configuration;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
namespace Ryujinx.Input.HLE
{
public class NpadManager : IDisposable
{
private CemuHookClient _cemuHookClient;
private object _lock = new object();
private bool _blockInputUpdates;
private const int MaxControllers = 9;
private NpadController[] _controllers;
private readonly IGamepadDriver _keyboardDriver;
private readonly IGamepadDriver _gamepadDriver;
private bool _isDisposed;
private List<InputConfig> _inputConfig;
public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver)
{
_controllers = new NpadController[MaxControllers];
_cemuHookClient = new CemuHookClient();
_keyboardDriver = keyboardDriver;
_gamepadDriver = gamepadDriver;
_inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value;
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
}
private void HandleOnGamepadDisconnected(string obj)
{
// Force input reload
ReloadConfiguration(ConfigurationState.Instance.Hid.InputConfig.Value);
}
private void HandleOnGamepadConnected(string id)
{
// Force input reload
ReloadConfiguration(ConfigurationState.Instance.Hid.InputConfig.Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
{
IGamepadDriver targetDriver = _gamepadDriver;
if (config is StandardControllerInputConfig)
{
targetDriver = _gamepadDriver;
}
else if (config is StandardKeyboardInputConfig)
{
targetDriver = _keyboardDriver;
}
Debug.Assert(targetDriver != null, "Unknown input configuration!");
if (controller.GamepadDriver != targetDriver || controller.Id != config.Id)
{
return controller.UpdateDriverConfiguration(targetDriver, config);
}
else
{
return controller.GamepadDriver != null;
}
}
public void ReloadConfiguration(List<InputConfig> inputConfig)
{
lock (_lock)
{
for (int i = 0; i < _controllers.Length; i++)
{
_controllers[i]?.Dispose();
_controllers[i] = null;
}
foreach (InputConfig inputConfigEntry in inputConfig)
{
NpadController controller = new NpadController(_cemuHookClient);
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
if (!isValid)
{
controller.Dispose();
}
else
{
_controllers[(int)inputConfigEntry.PlayerIndex] = controller;
}
}
_inputConfig = inputConfig;
// Enforce an update of the property that will be updated by HLE.
// TODO: Move that in the input manager maybe?
ConfigurationState.Instance.Hid.InputConfig.Value = inputConfig;
}
}
public void UnblockInputUpdates()
{
lock (_lock)
{
_blockInputUpdates = false;
}
}
public void BlockInputUpdates()
{
lock (_lock)
{
_blockInputUpdates = true;
}
}
public void Update(Hid hleHid, TamperMachine tamperMachine)
{
lock (_lock)
{
List<GamepadInput> hleInputStates = new List<GamepadInput>();
List<SixAxisInput> hleMotionStates = new List<SixAxisInput>(NpadDevices.MaxControllers);
foreach (InputConfig inputConfig in _inputConfig)
{
GamepadInput inputState = default;
SixAxisInput motionState = default;
NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
// Do we allow input updates and is a controller connected?
if (!_blockInputUpdates && controller != null)
{
DriverConfigurationUpdate(ref controller, inputConfig);
controller.UpdateUserConfiguration(inputConfig);
controller.Update();
inputState = controller.GetHLEInputState();
inputState.Buttons |= hleHid.UpdateStickButtons(inputState.LStick, inputState.RStick);
motionState = controller.GetHLEMotionState();
}
else
{
// Ensure that orientation isn't null
motionState.Orientation = new float[9];
}
inputState.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
motionState.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
hleInputStates.Add(inputState);
hleMotionStates.Add(motionState);
if (ConfigurationState.Instance.Hid.EnableKeyboard)
{
KeyboardInput? hleKeyboardInput = controller.GetHLEKeyboardInput();
if (hleKeyboardInput.HasValue)
{
hleHid.Keyboard.Update(hleKeyboardInput.Value);
}
}
}
hleHid.Npads.Update(hleInputStates);
hleHid.Npads.UpdateSixAxis(hleMotionStates);
tamperMachine.UpdateInput(hleInputStates);
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_lock)
{
if (!_isDisposed)
{
_cemuHookClient.Dispose();
_gamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
for (int i = 0; i < _controllers.Length; i++)
{
_controllers[i]?.Dispose();
}
_isDisposed = true;
}
}
}
}
public void Dispose()
{
Dispose(true);
}
}
}

122
Ryujinx.Input/IGamepad.cs Normal file
View file

@ -0,0 +1,122 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Memory;
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Ryujinx.Input
{
/// <summary>
/// Represent an emulated gamepad.
/// </summary>
public interface IGamepad : IDisposable
{
/// <summary>
/// Features supported by the gamepad.
/// </summary>
GamepadFeaturesFlag Features { get; }
/// <summary>
/// Unique Id of the gamepad.
/// </summary>
string Id { get; }
/// <summary>
/// The name of the gamepad.
/// </summary>
string Name { get; }
/// <summary>
/// True if the gamepad is connected.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Check if a given input button is pressed on the gamepad.
/// </summary>
/// <param name="inputId">The button id</param>
/// <returns>True if the given button is pressed on the gamepad</returns>
bool IsPressed(GamepadButtonInputId inputId);
/// <summary>
/// Get the values of a given input joystick on the gamepad.
/// </summary>
/// <param name="inputId">The stick id</param>
/// <returns>The values of the given input joystick on the gamepad</returns>
(float, float) GetStick(StickInputId inputId);
/// <summary>
/// Get the values of a given motion sensors on the gamepad.
/// </summary>
/// <param name="inputId">The motion id</param>
/// <returns> The values of the given motion sensors on the gamepad.</returns>
Vector3 GetMotionData(MotionInputId inputId);
/// <summary>
/// Configure the threshold of the triggers on the gamepad.
/// </summary>
/// <param name="triggerThreshold">The threshold value for the triggers on the gamepad</param>
void SetTriggerThreshold(float triggerThreshold);
/// <summary>
/// Set the configuration of the gamepad.
/// </summary>
/// <remarks>This expect config to be in the format expected by the driver</remarks>
/// <param name="configuration">The configuration of the gamepad</param>
void SetConfiguration(InputConfig configuration);
/// <summary>
/// Starts a rumble effect on the gampead.
/// </summary>
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
/// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
void Rumble(float lowFrequency, float highFrequency, uint durationMs);
/// <summary>
/// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
/// </summary>
/// <returns>A remapped snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetMappedStateSnapshot();
/// <summary>
/// Get a snaphost of the state of the gamepad.
/// </summary>
/// <returns>A snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetStateSnapshot();
/// <summary>
/// Get a snaphost of the state of a gamepad.
/// </summary>
/// <param name="gamepad">The gamepad to do a snapshot of</param>
/// <returns>A snaphost of the state of the gamepad.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static GamepadStateSnapshot GetStateSnapshot(IGamepad gamepad)
{
// NOTE: Update Array size if JoystickInputId is changed.
Array3<Array2<float>> joysticksState = default;
for (StickInputId inputId = StickInputId.Left; inputId < StickInputId.Count; inputId++)
{
(float state0, float state1) = gamepad.GetStick(inputId);
Array2<float> state = default;
state[0] = state0;
state[1] = state1;
joysticksState[(int)inputId] = state;
}
// NOTE: Update Array size if GamepadInputId is changed.
Array28<bool> buttonsState = default;
for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
{
buttonsState[(int)inputId] = gamepad.IsPressed(inputId);
}
return new GamepadStateSnapshot(joysticksState, buttonsState);
}
}
}

View file

@ -0,0 +1,37 @@
using System;
namespace Ryujinx.Input
{
/// <summary>
/// Represent an emulated gamepad driver used to provide input in the emulator.
/// </summary>
public interface IGamepadDriver : IDisposable
{
/// <summary>
/// The name of the driver
/// </summary>
string DriverName { get; }
/// <summary>
/// The unique ids of the gamepads connected.
/// </summary>
ReadOnlySpan<string> GamepadsIds { get; }
/// <summary>
/// Event triggered when a gamepad is connected.
/// </summary>
event Action<string> OnGamepadConnected;
/// <summary>
/// Event triggered when a gamepad is disconnected.
/// </summary>
event Action<string> OnGamepadDisconnected;
/// <summary>
/// Open a gampad by its unique id.
/// </summary>
/// <param name="id">The unique id of the gamepad</param>
/// <returns>An instance of <see cref="IGamepad"/> associated to the gamepad id given or null if not found</returns>
IGamepad GetGamepad(string id);
}
}

View file

@ -0,0 +1,41 @@
using System.Runtime.CompilerServices;
namespace Ryujinx.Input
{
/// <summary>
/// Represent an emulated keyboard.
/// </summary>
public interface IKeyboard : IGamepad
{
/// <summary>
/// Check if a given key is pressed on the keyboard.
/// </summary>
/// <param name="key">The key</param>
/// <returns>True if the given key is pressed on the keyboard</returns>
bool IsPressed(Key key);
/// <summary>
/// Get a snaphost of the state of the keyboard.
/// </summary>
/// <returns>A snaphost of the state of the keyboard.</returns>
KeyboardStateSnapshot GetKeyboardStateSnapshot();
/// <summary>
/// Get a snaphost of the state of a keyboard.
/// </summary>
/// <param name="keyboard">The keyboard to do a snapshot of</param>
/// <returns>A snaphost of the state of the keyboard.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard)
{
bool[] keysState = new bool[(int)Key.Count];
for (Key key = 0; key < Key.Count; key++)
{
keysState[(int)key] = keyboard.IsPressed(key);
}
return new KeyboardStateSnapshot(keysState);
}
}
}

142
Ryujinx.Input/Key.cs Normal file
View file

@ -0,0 +1,142 @@
namespace Ryujinx.Input
{
/// <summary>
/// Represent a key from a keyboard.
/// </summary>
public enum Key
{
Unknown,
ShiftLeft,
ShiftRight,
ControlLeft,
ControlRight,
AltLeft,
AltRight,
WinLeft,
WinRight,
Menu,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
F25,
F26,
F27,
F28,
F29,
F30,
F31,
F32,
F33,
F34,
F35,
Up,
Down,
Left,
Right,
Enter,
Escape,
Space,
Tab,
BackSpace,
Insert,
Delete,
PageUp,
PageDown,
Home,
End,
CapsLock,
ScrollLock,
PrintScreen,
Pause,
NumLock,
Clear,
Keypad0,
Keypad1,
Keypad2,
Keypad3,
Keypad4,
Keypad5,
Keypad6,
Keypad7,
Keypad8,
Keypad9,
KeypadDivide,
KeypadMultiply,
KeypadSubtract,
KeypadAdd,
KeypadDecimal,
KeypadEnter,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
Number0,
Number1,
Number2,
Number3,
Number4,
Number5,
Number6,
Number7,
Number8,
Number9,
Tilde,
Grave,
Minus,
Plus,
BracketLeft,
BracketRight,
Semicolon,
Quote,
Comma,
Period,
Slash,
BackSlash,
Unbound,
Count
}
}

View file

@ -0,0 +1,29 @@
using System.Runtime.CompilerServices;
namespace Ryujinx.Input
{
/// <summary>
/// A snapshot of a <see cref="IKeyboard"/>.
/// </summary>
public class KeyboardStateSnapshot
{
private bool[] _keysState;
/// <summary>
/// Create a new <see cref="KeyboardStateSnapshot"/>.
/// </summary>
/// <param name="keysState">The keys state</param>
public KeyboardStateSnapshot(bool[] keysState)
{
_keysState = keysState;
}
/// <summary>
/// Check if a given key is pressed.
/// </summary>
/// <param name="key">The key</param>
/// <returns>True if the given key is pressed</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(Key key) => _keysState[(int)key];
}
}

View file

@ -1,8 +1,11 @@
using Force.Crc32; using Force.Crc32;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Configuration; using Ryujinx.Configuration;
using Ryujinx.Input.Motion.CemuHook.Protocol;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -11,7 +14,7 @@ using System.Net.Sockets;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input.Motion.CemuHook
{ {
public class Client : IDisposable public class Client : IDisposable
{ {
@ -324,32 +327,43 @@ namespace Ryujinx.Modules.Motion
lock (_motionData) lock (_motionData)
{ {
int slot = inputData.Shared.Slot; // Sanity check the configuration state and remove client if needed if needed.
if (config is StandardControllerInputConfig controllerConfig &&
if (_motionData.ContainsKey(clientId)) controllerConfig.Motion.EnableMotion &&
controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook &&
controllerConfig.Motion is CemuHookMotionConfigController cemuHookConfig)
{ {
if (_motionData[clientId].ContainsKey(slot)) int slot = inputData.Shared.Slot;
{
MotionInput previousData = _motionData[clientId][slot];
previousData.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); if (_motionData.ContainsKey(clientId))
{
if (_motionData[clientId].ContainsKey(slot))
{
MotionInput previousData = _motionData[clientId][slot];
previousData.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
}
else
{
MotionInput input = new MotionInput();
input.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
_motionData[clientId].Add(slot, input);
}
} }
else else
{ {
MotionInput input = new MotionInput(); MotionInput input = new MotionInput();
input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); input.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
_motionData[clientId].Add(slot, input); _motionData.Add(clientId, new Dictionary<int, MotionInput>() { { slot, input } });
} }
} }
else else
{ {
MotionInput input = new MotionInput(); RemoveClient(clientId);
input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
_motionData.Add(clientId, new Dictionary<int, MotionInput>() { { slot, input } });
} }
} }
break; break;

View file

@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ControllerDataRequest struct ControllerDataRequest

View file

@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ControllerInfoResponse public struct ControllerInfoResponse

View file

@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Header public struct Header

View file

@ -1,4 +1,4 @@
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
public enum MessageType : uint public enum MessageType : uint
{ {

View file

@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SharedResponse public struct SharedResponse

View file

@ -1,7 +1,8 @@
using System; using Ryujinx.Input.Motion;
using System;
using System.Numerics; using System.Numerics;
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input
{ {
public class MotionInput public class MotionInput
{ {

View file

@ -1,11 +1,11 @@
using System.Numerics; using System.Numerics;
namespace Ryujinx.Modules.Motion namespace Ryujinx.Input.Motion
{ {
// MahonyAHRS class. Madgwick's implementation of Mayhony's AHRS algorithm. // MahonyAHRS class. Madgwick's implementation of Mayhony's AHRS algorithm.
// See: https://x-io.co.uk/open-source-imu-and-ahrs-algorithms/ // See: https://x-io.co.uk/open-source-imu-and-ahrs-algorithms/
// Based on: https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs // Based on: https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs
public class MotionSensorFilter class MotionSensorFilter
{ {
/// <summary> /// <summary>
/// Sample rate coefficient. /// Sample rate coefficient.

View file

@ -0,0 +1,25 @@
namespace Ryujinx.Input
{
/// <summary>
/// Represent a motion sensor on a gamepad.
/// </summary>
public enum MotionInputId : byte
{
/// <summary>
/// Invalid.
/// </summary>
Invalid,
/// <summary>
/// Accelerometer.
/// </summary>
/// <remarks>Values are in m/s^2</remarks>
Accelerometer,
/// <summary>
/// Gyroscope.
/// </summary>
/// <remarks>Values are in degrees</remarks>
Gyroscope
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,14 @@
namespace Ryujinx.Input
{
/// <summary>
/// Represent a joystick from a gamepad.
/// </summary>
public enum StickInputId : byte
{
Unbound,
Left,
Right,
Count
}
}

View file

@ -59,6 +59,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Open
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SoundIo", "Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj", "{716364DE-B988-41A6-BAB4-327964266ECC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SoundIo", "Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj", "{716364DE-B988-41A6-BAB4-327964266ECC}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input", "Ryujinx.Input\Ryujinx.Input.csproj", "{C16F112F-38C3-40BC-9F5F-4791112063D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input.SDL2", "Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj", "{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -165,6 +169,14 @@ Global
{716364DE-B988-41A6-BAB4-327964266ECC}.Debug|Any CPU.Build.0 = Debug|Any CPU {716364DE-B988-41A6-BAB4-327964266ECC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{716364DE-B988-41A6-BAB4-327964266ECC}.Release|Any CPU.ActiveCfg = Release|Any CPU {716364DE-B988-41A6-BAB4-327964266ECC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{716364DE-B988-41A6-BAB4-327964266ECC}.Release|Any CPU.Build.0 = Release|Any CPU {716364DE-B988-41A6-BAB4-327964266ECC}.Release|Any CPU.Build.0 = Release|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|Any CPU.Build.0 = Release|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -1,5 +1,5 @@
{ {
"version": 22, "version": 24,
"res_scale": 1, "res_scale": 1,
"res_scale_custom": 1, "res_scale_custom": 1,
"max_anisotropy": -1, "max_anisotropy": -1,
@ -13,6 +13,7 @@
"logging_enable_guest": true, "logging_enable_guest": true,
"logging_enable_fs_access_log": false, "logging_enable_fs_access_log": false,
"logging_filtered_classes": [], "logging_filtered_classes": [],
"logging_graphics_debug_level": "None",
"enable_file_log": true, "enable_file_log": true,
"system_language": "AmericanEnglish", "system_language": "AmericanEnglish",
"system_region": "USA", "system_region": "USA",
@ -25,10 +26,12 @@
"hide_cursor_on_idle": false, "hide_cursor_on_idle": false,
"enable_vsync": true, "enable_vsync": true,
"enable_shader_cache": true, "enable_shader_cache": true,
"enable_multicore_scheduling": false,
"enable_ptc": true, "enable_ptc": true,
"enable_fs_integrity_checks": true, "enable_fs_integrity_checks": true,
"fs_global_access_log_mode": 0, "fs_global_access_log_mode": 0,
"audio_backend": "OpenAl", "audio_backend": "OpenAl",
"expand_ram": false,
"ignore_missing_services": false, "ignore_missing_services": false,
"gui_columns": { "gui_columns": {
"fav_column": true, "fav_column": true,
@ -54,51 +57,51 @@
"hotkeys": { "hotkeys": {
"toggle_vsync": "Tab" "toggle_vsync": "Tab"
}, },
"keyboard_config": [ "keyboard_config": [],
"controller_config": [],
"input_config": [
{ {
"index": 0, "left_joycon_stick": {
"controller_type": "JoyconPair",
"player_index": "Player1",
"left_joycon": {
"stick_up": "W", "stick_up": "W",
"stick_down": "S", "stick_down": "S",
"stick_left": "A", "stick_left": "A",
"stick_right": "D", "stick_right": "D",
"stick_button": "F", "stick_button": "F"
"dpad_up": "Up",
"dpad_down": "Down",
"dpad_left": "Left",
"dpad_right": "Right",
"button_minus": "Minus",
"button_l": "E",
"button_zl": "Q",
"button_sl": "Unbound",
"button_sr": "Unbound"
}, },
"right_joycon": { "right_joycon_stick": {
"stick_up": "I", "stick_up": "I",
"stick_down": "K", "stick_down": "K",
"stick_left": "J", "stick_left": "J",
"stick_right": "L", "stick_right": "L",
"stick_button": "H", "stick_button": "H"
"button_a": "Z", },
"button_b": "X", "left_joycon": {
"button_x": "C", "button_minus": "Minus",
"button_y": "V", "button_l": "E",
"button_zl": "Q",
"button_sl": "Unbound",
"button_sr": "Unbound",
"dpad_up": "Up",
"dpad_down": "Down",
"dpad_left": "Left",
"dpad_right": "Right"
},
"right_joycon": {
"button_plus": "Plus", "button_plus": "Plus",
"button_r": "U", "button_r": "U",
"button_zr": "O", "button_zr": "O",
"button_sl": "Unbound", "button_sl": "Unbound",
"button_sr": "Unbound" "button_sr": "Unbound",
"button_x": "C",
"button_b": "X",
"button_y": "V",
"button_a": "Z"
}, },
"slot": 0, "version": 1,
"alt_slot": 0, "backend": "WindowKeyboard",
"mirror_input": false, "id": "0",
"dsu_server_host": "127.0.0.1", "controller_type": "JoyconPair",
"dsu_server_port": 26760, "player_index": "Player1"
"sensitivity": 100,
"enable_motion": false
} }
], ]
"controller_config": [] }
}

View file

@ -0,0 +1,204 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System;
using System.Collections.Generic;
using System.Numerics;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Input.GTK3
{
public class GTK3Keyboard : IKeyboard
{
private class ButtonMappingEntry
{
public readonly GamepadButtonInputId To;
public readonly Key From;
public ButtonMappingEntry(GamepadButtonInputId to, Key from)
{
To = to;
From = from;
}
}
private object _userMappingLock = new object();
private readonly GTK3KeyboardDriver _driver;
private StandardKeyboardInputConfig _configuration;
private List<ButtonMappingEntry> _buttonsUserMapping;
public GTK3Keyboard(GTK3KeyboardDriver driver, string id, string name)
{
_driver = driver;
Id = id;
Name = name;
_buttonsUserMapping = new List<ButtonMappingEntry>();
}
private bool HasConfiguration => _configuration != null;
public string Id { get; }
public string Name { get; }
public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
public void Dispose()
{
// No operations
}
public KeyboardStateSnapshot GetKeyboardStateSnapshot()
{
return IKeyboard.GetStateSnapshot(this);
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed((Key)stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
{
stickX -= 1;
}
OpenTK.Mathematics.Vector2 stick = new OpenTK.Mathematics.Vector2(stickX, stickY);
stick.NormalizeFast();
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (!HasConfiguration)
{
return result;
}
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
{
continue;
}
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
}
return result;
}
public GamepadStateSnapshot GetStateSnapshot()
{
throw new NotSupportedException();
}
public (float, float) GetStick(StickInputId inputId)
{
throw new NotSupportedException();
}
public bool IsPressed(GamepadButtonInputId inputId)
{
throw new NotSupportedException();
}
public bool IsPressed(Key key)
{
return _driver.IsPressed(key);
}
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardKeyboardInputConfig)configuration;
_buttonsUserMapping.Clear();
// Then left joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
// Finally right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
}
}
public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
// No operations
}
public Vector3 GetMotionData(MotionInputId inputId)
{
// No operations
return Vector3.Zero;
}
}
}

View file

@ -0,0 +1,93 @@
using Gdk;
using Gtk;
using System;
using System.Collections.Generic;
using GtkKey = Gdk.Key;
namespace Ryujinx.Input.GTK3
{
public class GTK3KeyboardDriver : IGamepadDriver
{
private readonly Widget _widget;
private HashSet<GtkKey> _pressedKeys;
public GTK3KeyboardDriver(Widget widget)
{
_widget = widget;
_pressedKeys = new HashSet<GtkKey>();
_widget.KeyPressEvent += OnKeyPress;
_widget.KeyReleaseEvent += OnKeyRelease;
}
public string DriverName => "GTK3";
private static readonly string[] _keyboardIdentifers = new string[1] { "0" };
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public event Action<string> OnGamepadConnected
{
add { }
remove { }
}
public event Action<string> OnGamepadDisconnected
{
add { }
remove { }
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_widget.KeyPressEvent -= OnKeyPress;
_widget.KeyReleaseEvent -= OnKeyRelease;
}
}
public void Dispose()
{
Dispose(true);
}
[GLib.ConnectBefore]
protected void OnKeyPress(object sender, KeyPressEventArgs args)
{
GtkKey key = (GtkKey)Keyval.ToLower((uint)args.Event.Key);
_pressedKeys.Add(key);
}
[GLib.ConnectBefore]
protected void OnKeyRelease(object sender, KeyReleaseEventArgs args)
{
GtkKey key = (GtkKey)Keyval.ToLower((uint)args.Event.Key);
_pressedKeys.Remove(key);
}
internal bool IsPressed(Key key)
{
if (key == Key.Unbound || key == Key.Unknown)
{
return false;
}
GtkKey nativeKey = GTK3MappingHelper.ToGtkKey(key);
return _pressedKeys.Contains(nativeKey);
}
public IGamepad GetGamepad(string id)
{
if (!_keyboardIdentifers[0].Equals(id))
{
return null;
}
return new GTK3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
}
}
}

View file

@ -0,0 +1,154 @@
using System.Runtime.CompilerServices;
using GtkKey = Gdk.Key;
namespace Ryujinx.Input.GTK3
{
public static class GTK3MappingHelper
{
private static readonly GtkKey[] _keyMapping = new GtkKey[(int)Key.Count]
{
// NOTE: invalid
GtkKey.blank,
GtkKey.Shift_L,
GtkKey.Shift_R,
GtkKey.Control_L,
GtkKey.Control_R,
GtkKey.Alt_L,
GtkKey.Alt_R,
GtkKey.Super_L,
GtkKey.Super_R,
GtkKey.Menu,
GtkKey.F1,
GtkKey.F2,
GtkKey.F3,
GtkKey.F4,
GtkKey.F5,
GtkKey.F6,
GtkKey.F7,
GtkKey.F8,
GtkKey.F9,
GtkKey.F10,
GtkKey.F11,
GtkKey.F12,
GtkKey.F13,
GtkKey.F14,
GtkKey.F15,
GtkKey.F16,
GtkKey.F17,
GtkKey.F18,
GtkKey.F19,
GtkKey.F20,
GtkKey.F21,
GtkKey.F22,
GtkKey.F23,
GtkKey.F24,
GtkKey.F25,
GtkKey.F26,
GtkKey.F27,
GtkKey.F28,
GtkKey.F29,
GtkKey.F29,
GtkKey.F31,
GtkKey.F32,
GtkKey.F33,
GtkKey.F34,
GtkKey.F35,
GtkKey.Up,
GtkKey.Down,
GtkKey.Left,
GtkKey.Right,
GtkKey.Return,
GtkKey.Escape,
GtkKey.space,
GtkKey.Tab,
GtkKey.BackSpace,
GtkKey.Insert,
GtkKey.Delete,
GtkKey.Page_Up,
GtkKey.Page_Down,
GtkKey.Home,
GtkKey.End,
GtkKey.Caps_Lock,
GtkKey.Scroll_Lock,
GtkKey.Print,
GtkKey.Pause,
GtkKey.Num_Lock,
GtkKey.Clear,
GtkKey.KP_0,
GtkKey.KP_1,
GtkKey.KP_2,
GtkKey.KP_3,
GtkKey.KP_4,
GtkKey.KP_5,
GtkKey.KP_6,
GtkKey.KP_7,
GtkKey.KP_8,
GtkKey.KP_9,
GtkKey.KP_Divide,
GtkKey.KP_Multiply,
GtkKey.KP_Subtract,
GtkKey.KP_Add,
GtkKey.KP_Decimal,
GtkKey.KP_Enter,
GtkKey.a,
GtkKey.b,
GtkKey.c,
GtkKey.d,
GtkKey.e,
GtkKey.f,
GtkKey.g,
GtkKey.h,
GtkKey.i,
GtkKey.j,
GtkKey.k,
GtkKey.l,
GtkKey.m,
GtkKey.n,
GtkKey.o,
GtkKey.p,
GtkKey.q,
GtkKey.r,
GtkKey.s,
GtkKey.t,
GtkKey.u,
GtkKey.v,
GtkKey.w,
GtkKey.x,
GtkKey.y,
GtkKey.z,
GtkKey.Key_0,
GtkKey.Key_1,
GtkKey.Key_2,
GtkKey.Key_3,
GtkKey.Key_4,
GtkKey.Key_5,
GtkKey.Key_6,
GtkKey.Key_7,
GtkKey.Key_8,
GtkKey.Key_9,
GtkKey.grave,
GtkKey.minus,
GtkKey.plus,
GtkKey.bracketleft,
GtkKey.bracketright,
GtkKey.semicolon,
GtkKey.quotedbl,
GtkKey.comma,
GtkKey.period,
GtkKey.slash,
GtkKey.backslash,
GtkKey.backslash,
// NOTE: invalid
GtkKey.blank,
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GtkKey ToGtkKey(Key key)
{
return _keyMapping[(int)key];
}
}
}

View file

@ -1,81 +0,0 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Configuration;
using System;
using System.Numerics;
namespace Ryujinx.Modules.Motion
{
public class MotionDevice
{
public Vector3 Gyroscope { get; private set; }
public Vector3 Accelerometer { get; private set; }
public Vector3 Rotation { get; private set; }
public float[] Orientation { get; private set; }
private readonly Client _motionSource;
public MotionDevice(Client motionSource)
{
_motionSource = motionSource;
}
public void RegisterController(PlayerIndex player)
{
InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == player);
if (config != null && config.EnableMotion)
{
string host = config.DsuServerHost;
int port = config.DsuServerPort;
_motionSource.RegisterClient((int)player, host, port);
_motionSource.RequestData((int)player, config.Slot);
if (config.ControllerType == ControllerType.JoyconPair && !config.MirrorInput)
{
_motionSource.RequestData((int)player, config.AltSlot);
}
}
}
public void Poll(InputConfig config, int slot)
{
Orientation = new float[9];
if (!config.EnableMotion || !_motionSource.TryGetData((int)config.PlayerIndex, slot, out MotionInput input))
{
Accelerometer = new Vector3();
Gyroscope = new Vector3();
return;
}
Gyroscope = Truncate(input.Gyroscrope * 0.0027f, 3);
Accelerometer = Truncate(input.Accelerometer, 3);
Rotation = Truncate(input.Rotation * 0.0027f, 3);
Matrix4x4 orientation = input.GetOrientation();
Orientation[0] = Math.Clamp(orientation.M11, -1f, 1f);
Orientation[1] = Math.Clamp(orientation.M12, -1f, 1f);
Orientation[2] = Math.Clamp(orientation.M13, -1f, 1f);
Orientation[3] = Math.Clamp(orientation.M21, -1f, 1f);
Orientation[4] = Math.Clamp(orientation.M22, -1f, 1f);
Orientation[5] = Math.Clamp(orientation.M23, -1f, 1f);
Orientation[6] = Math.Clamp(orientation.M31, -1f, 1f);
Orientation[7] = Math.Clamp(orientation.M32, -1f, 1f);
Orientation[8] = Math.Clamp(orientation.M33, -1f, 1f);
}
private static Vector3 Truncate(Vector3 value, int decimals)
{
float power = MathF.Pow(10, decimals);
value.X = float.IsNegative(value.X) ? MathF.Ceiling(value.X * power) / power : MathF.Floor(value.X * power) / power;
value.Y = float.IsNegative(value.Y) ? MathF.Ceiling(value.Y * power) / power : MathF.Floor(value.Y * power) / power;
value.Z = float.IsNegative(value.Z) ? MathF.Ceiling(value.Z * power) / power : MathF.Floor(value.Z * power) / power;
return value;
}
}
}

View file

@ -1,6 +1,5 @@
using ARMeilleure.Translation.PTC; using ARMeilleure.Translation.PTC;
using Gtk; using Gtk;
using OpenTK;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.System; using Ryujinx.Common.System;
@ -12,6 +11,7 @@ using Ryujinx.Ui.Widgets;
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx namespace Ryujinx
@ -24,6 +24,9 @@ namespace Ryujinx
public static string ConfigurationPath { get; set; } public static string ConfigurationPath { get; set; }
[DllImport("libX11")]
private extern static int XInitThreads();
static void Main(string[] args) static void Main(string[] args)
{ {
// Parse Arguments. // Parse Arguments.
@ -63,15 +66,17 @@ namespace Ryujinx
// Delete backup files after updating. // Delete backup files after updating.
Task.Run(Updater.CleanupUpdate); Task.Run(Updater.CleanupUpdate);
Toolkit.Init(new ToolkitOptions
{
Backend = PlatformBackend.PreferNative
});
Version = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion; Version = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
Console.Title = $"Ryujinx Console {Version}"; Console.Title = $"Ryujinx Console {Version}";
// NOTE: GTK3 doesn't init X11 in a multi threaded way.
// This ends up causing race condition and abort of XCB when a context is created by SPB (even if SPB do call XInitThreads).
if (OperatingSystem.IsLinux())
{
XInitThreads();
}
string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine); string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine);
Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}"); Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}");

View file

@ -12,18 +12,19 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
<PackageReference Include="DiscordRichPresence" Version="1.0.166" /> <PackageReference Include="DiscordRichPresence" Version="1.0.166" />
<PackageReference Include="GLWidget" Version="1.0.2" />
<PackageReference Include="GtkSharp" Version="3.22.25.128" /> <PackageReference Include="GtkSharp" Version="3.22.25.128" />
<PackageReference Include="GtkSharp.Dependencies" Version="1.1.0" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" /> <PackageReference Include="GtkSharp.Dependencies" Version="1.1.0" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="4.3.0" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" /> <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="4.3.0" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" /> <PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="OpenTK.NetStandard" Version="1.0.5.32" /> <PackageReference Include="OpenTK.Graphics" Version="4.5.0" />
<PackageReference Include="SPB" Version="0.0.2" />
<PackageReference Include="SharpZipLib" Version="1.3.0" /> <PackageReference Include="SharpZipLib" Version="1.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" /> <ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" /> <ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />

View file

@ -1,39 +1,37 @@
using ARMeilleure.Translation; using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC; using ARMeilleure.Translation.PTC;
using Gdk; using Gdk;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
using OpenTK.Input;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging;
using Ryujinx.Configuration; using Ryujinx.Configuration;
using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Modules.Motion; using Ryujinx.Input;
using Ryujinx.Input.HLE;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading; using System.Threading;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ui namespace Ryujinx.Ui
{ {
using Switch = HLE.Switch; using Switch = HLE.Switch;
public class GlRenderer : GLWidget public class GlRenderer : GLWidget
{ {
static GlRenderer()
{
OpenTK.Graphics.GraphicsContext.ShareContexts = true;
}
private const int SwitchPanelWidth = 1280; private const int SwitchPanelWidth = 1280;
private const int SwitchPanelHeight = 720; private const int SwitchPanelHeight = 720;
private const int TargetFps = 60; private const int TargetFps = 60;
public ManualResetEvent WaitEvent { get; set; } public ManualResetEvent WaitEvent { get; set; }
public NpadManager NpadManager { get; }
public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent; public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
@ -58,9 +56,7 @@ namespace Ryujinx.Ui
private Renderer _renderer; private Renderer _renderer;
private HotkeyButtons _prevHotkeyButtons; private KeyboardHotkeyState _prevHotkeyState;
private Client _dsuClient;
private GraphicsDebugLevel _glLogLevel; private GraphicsDebugLevel _glLogLevel;
@ -71,14 +67,22 @@ namespace Ryujinx.Ui
private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor); private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
private long _lastCursorMoveTime; private long _lastCursorMoveTime;
private bool _hideCursorOnIdle; private bool _hideCursorOnIdle;
private InputManager _inputManager;
private IKeyboard _keyboardInterface;
public GlRenderer(Switch device, GraphicsDebugLevel glLogLevel) public GlRenderer(Switch device, InputManager inputManager, GraphicsDebugLevel glLogLevel)
: base (GetGraphicsMode(), : base (GetGraphicsMode(),
3, 3, 3, 3,
glLogLevel == GraphicsDebugLevel.None glLogLevel == GraphicsDebugLevel.None
? GraphicsContextFlags.ForwardCompatible ? OpenGLContextFlags.Compat
: GraphicsContextFlags.ForwardCompatible | GraphicsContextFlags.Debug) : OpenGLContextFlags.Compat | OpenGLContextFlags.Debug)
{ {
_inputManager = inputManager;
NpadManager = _inputManager.CreateNpadManager();
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
NpadManager.ReloadConfiguration(ConfigurationState.Instance.Hid.InputConfig.Value.ToList());
WaitEvent = new ManualResetEvent(false); WaitEvent = new ManualResetEvent(false);
_device = device; _device = device;
@ -101,8 +105,6 @@ namespace Ryujinx.Ui
Shown += Renderer_Shown; Shown += Renderer_Shown;
_dsuClient = new Client();
_glLogLevel = glLogLevel; _glLogLevel = glLogLevel;
_exitEvent = new ManualResetEvent(false); _exitEvent = new ManualResetEvent(false);
@ -130,15 +132,15 @@ namespace Ryujinx.Ui
}); });
} }
private static GraphicsMode GetGraphicsMode() private static FramebufferFormat GetGraphicsMode()
{ {
return Environment.OSVersion.Platform == PlatformID.Unix ? new GraphicsMode(new ColorFormat(24)) : new GraphicsMode(new ColorFormat()); return Environment.OSVersion.Platform == PlatformID.Unix ? new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false) : FramebufferFormat.Default;
} }
private void GLRenderer_ShuttingDown(object sender, EventArgs args) private void GLRenderer_ShuttingDown(object sender, EventArgs args)
{ {
_device.DisposeGpu(); _device.DisposeGpu();
_dsuClient?.Dispose(); NpadManager.Dispose();
} }
private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args) private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args)
@ -155,7 +157,7 @@ namespace Ryujinx.Ui
{ {
ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged; ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged;
_dsuClient?.Dispose(); NpadManager.Dispose();
Dispose(); Dispose();
} }
@ -164,13 +166,13 @@ namespace Ryujinx.Ui
_isFocused = this.ParentWindow.State.HasFlag(Gdk.WindowState.Focused); _isFocused = this.ParentWindow.State.HasFlag(Gdk.WindowState.Focused);
} }
public void HandleScreenState(KeyboardState keyboard) public void HandleScreenState(KeyboardStateSnapshot keyboard)
{ {
bool toggleFullscreen = keyboard.IsKeyDown(OpenTK.Input.Key.F11) bool toggleFullscreen = keyboard.IsPressed(Key.F11)
|| ((keyboard.IsKeyDown(OpenTK.Input.Key.AltLeft) || ((keyboard.IsPressed(Key.AltLeft)
|| keyboard.IsKeyDown(OpenTK.Input.Key.AltRight)) || keyboard.IsPressed(Key.AltRight))
&& keyboard.IsKeyDown(OpenTK.Input.Key.Enter)) && keyboard.IsPressed(Key.Enter))
|| keyboard.IsKeyDown(OpenTK.Input.Key.Escape); || keyboard.IsPressed(Key.Escape);
bool fullScreenToggled = ParentWindow.State.HasFlag(Gdk.WindowState.Fullscreen); bool fullScreenToggled = ParentWindow.State.HasFlag(Gdk.WindowState.Fullscreen);
@ -185,7 +187,7 @@ namespace Ryujinx.Ui
} }
else else
{ {
if (keyboard.IsKeyDown(OpenTK.Input.Key.Escape)) if (keyboard.IsPressed(Key.Escape))
{ {
if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog()) if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
{ {
@ -203,7 +205,7 @@ namespace Ryujinx.Ui
_toggleFullscreen = toggleFullscreen; _toggleFullscreen = toggleFullscreen;
bool toggleDockedMode = keyboard.IsKeyDown(OpenTK.Input.Key.F9); bool toggleDockedMode = keyboard.IsPressed(Key.F9);
if (toggleDockedMode != _toggleDockedMode) if (toggleDockedMode != _toggleDockedMode)
{ {
@ -225,8 +227,8 @@ namespace Ryujinx.Ui
private void GLRenderer_Initialized(object sender, EventArgs e) private void GLRenderer_Initialized(object sender, EventArgs e)
{ {
// Release the GL exclusivity that OpenTK gave us as we aren't going to use it in GTK Thread. // Release the GL exclusivity that SPB gave us as we aren't going to use it in GTK Thread.
GraphicsContext.MakeCurrent(null); OpenGLContext.MakeCurrent(null);
WaitEvent.Set(); WaitEvent.Set();
} }
@ -244,8 +246,6 @@ namespace Ryujinx.Ui
public void Start() public void Start()
{ {
IsRenderHandler = true;
_chrono.Restart(); _chrono.Restart();
_isActive = true; _isActive = true;
@ -389,7 +389,7 @@ namespace Ryujinx.Ui
public void Exit() public void Exit()
{ {
_dsuClient?.Dispose(); NpadManager?.Dispose();
if (_isStopped) if (_isStopped)
{ {
@ -416,15 +416,17 @@ namespace Ryujinx.Ui
public void Render() public void Render()
{ {
// First take exclusivity on the OpenGL context. // First take exclusivity on the OpenGL context.
_renderer.InitializeBackgroundContext(GraphicsContext); _renderer.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(OpenGLContext));
Gtk.Window parent = Toplevel as Gtk.Window; Gtk.Window parent = Toplevel as Gtk.Window;
parent.Present(); parent.Present();
GraphicsContext.MakeCurrent(WindowInfo);
OpenGLContext.MakeCurrent(NativeWindow);
_device.Gpu.Renderer.Initialize(_glLogLevel); _device.Gpu.Renderer.Initialize(_glLogLevel);
// Make sure the first frame is not transparent. // Make sure the first frame is not transparent.
GL.ClearColor(OpenTK.Color.Black); GL.ClearColor(0, 0, 0, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit); GL.Clear(ClearBufferMask.ColorBufferBit);
SwapBuffers(); SwapBuffers();
@ -478,7 +480,7 @@ namespace Ryujinx.Ui
public void SwapBuffers() public void SwapBuffers()
{ {
OpenTK.Graphics.GraphicsContext.CurrentContext.SwapBuffers(); NativeWindow.SwapBuffers();
} }
public void MainLoop() public void MainLoop()
@ -510,13 +512,13 @@ namespace Ryujinx.Ui
{ {
Gtk.Application.Invoke(delegate Gtk.Application.Invoke(delegate
{ {
KeyboardState keyboard = OpenTK.Input.Keyboard.GetState(); KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
HandleScreenState(keyboard); HandleScreenState(keyboard);
if (keyboard.IsKeyDown(OpenTK.Input.Key.Delete)) if (keyboard.IsPressed(Key.Delete))
{ {
if (!ParentWindow.State.HasFlag(Gdk.WindowState.Fullscreen)) if (!ParentWindow.State.HasFlag(WindowState.Fullscreen))
{ {
Ptc.Continue(); Ptc.Continue();
} }
@ -524,154 +526,19 @@ namespace Ryujinx.Ui
}); });
} }
List<GamepadInput> gamepadInputs = new List<GamepadInput>(NpadDevices.MaxControllers); NpadManager.Update(_device.Hid, _device.TamperMachine);
List<SixAxisInput> motionInputs = new List<SixAxisInput>(NpadDevices.MaxControllers);
MotionDevice motionDevice = new MotionDevice(_dsuClient);
foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value)
{
ControllerKeys currentButton = 0;
JoystickPosition leftJoystick = new JoystickPosition();
JoystickPosition rightJoystick = new JoystickPosition();
KeyboardInput? hidKeyboard = null;
int leftJoystickDx = 0;
int leftJoystickDy = 0;
int rightJoystickDx = 0;
int rightJoystickDy = 0;
if (inputConfig.EnableMotion)
{
motionDevice.RegisterController(inputConfig.PlayerIndex);
}
if (inputConfig is KeyboardConfig keyboardConfig)
{
if (_isFocused)
{
// Keyboard Input
KeyboardController keyboardController = new KeyboardController(keyboardConfig);
currentButton = keyboardController.GetButtons();
(leftJoystickDx, leftJoystickDy) = keyboardController.GetLeftStick();
(rightJoystickDx, rightJoystickDy) = keyboardController.GetRightStick();
leftJoystick = new JoystickPosition
{
Dx = leftJoystickDx,
Dy = leftJoystickDy
};
rightJoystick = new JoystickPosition
{
Dx = rightJoystickDx,
Dy = rightJoystickDy
};
if (ConfigurationState.Instance.Hid.EnableKeyboard)
{
hidKeyboard = keyboardController.GetKeysDown();
}
if (!hidKeyboard.HasValue)
{
hidKeyboard = new KeyboardInput
{
Modifier = 0,
Keys = new int[0x8]
};
}
if (ConfigurationState.Instance.Hid.EnableKeyboard)
{
_device.Hid.Keyboard.Update(hidKeyboard.Value);
}
}
}
else if (inputConfig is Common.Configuration.Hid.ControllerConfig controllerConfig)
{
// Controller Input
JoystickController joystickController = new JoystickController(controllerConfig);
currentButton |= joystickController.GetButtons();
(leftJoystickDx, leftJoystickDy) = joystickController.GetLeftStick();
(rightJoystickDx, rightJoystickDy) = joystickController.GetRightStick();
leftJoystick = new JoystickPosition
{
Dx = controllerConfig.LeftJoycon.InvertStickX ? -leftJoystickDx : leftJoystickDx,
Dy = controllerConfig.LeftJoycon.InvertStickY ? -leftJoystickDy : leftJoystickDy
};
rightJoystick = new JoystickPosition
{
Dx = controllerConfig.RightJoycon.InvertStickX ? -rightJoystickDx : rightJoystickDx,
Dy = controllerConfig.RightJoycon.InvertStickY ? -rightJoystickDy : rightJoystickDy
};
}
currentButton |= _device.Hid.UpdateStickButtons(leftJoystick, rightJoystick);
motionDevice.Poll(inputConfig, inputConfig.Slot);
SixAxisInput sixAxisInput = new SixAxisInput()
{
PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex,
Accelerometer = motionDevice.Accelerometer,
Gyroscope = motionDevice.Gyroscope,
Rotation = motionDevice.Rotation,
Orientation = motionDevice.Orientation
};
motionInputs.Add(sixAxisInput);
gamepadInputs.Add(new GamepadInput
{
PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex,
Buttons = currentButton,
LStick = leftJoystick,
RStick = rightJoystick
});
if (inputConfig.ControllerType == Common.Configuration.Hid.ControllerType.JoyconPair)
{
if (!inputConfig.MirrorInput)
{
motionDevice.Poll(inputConfig, inputConfig.AltSlot);
sixAxisInput = new SixAxisInput()
{
PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex,
Accelerometer = motionDevice.Accelerometer,
Gyroscope = motionDevice.Gyroscope,
Rotation = motionDevice.Rotation,
Orientation = motionDevice.Orientation
};
}
motionInputs.Add(sixAxisInput);
}
}
_device.Hid.Npads.Update(gamepadInputs);
_device.Hid.Npads.UpdateSixAxis(motionInputs);
_device.TamperMachine.UpdateInput(gamepadInputs);
if(_isFocused) if(_isFocused)
{ {
// Hotkeys KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
HotkeyButtons currentHotkeyButtons = KeyboardController.GetHotkeyButtons(OpenTK.Input.Keyboard.GetState());
if (currentHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync) && if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) &&
!_prevHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync)) !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync))
{ {
_device.EnableDeviceVsync = !_device.EnableDeviceVsync; _device.EnableDeviceVsync = !_device.EnableDeviceVsync;
} }
_prevHotkeyButtons = currentHotkeyButtons; _prevHotkeyState = currentHotkeyState;
} }
//Touchscreen //Touchscreen
@ -739,5 +606,24 @@ namespace Ryujinx.Ui
return true; return true;
} }
[Flags]
private enum KeyboardHotkeyState
{
None,
ToggleVSync
}
private KeyboardHotkeyState GetHotkeyState()
{
KeyboardHotkeyState state = KeyboardHotkeyState.None;
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
{
state |= KeyboardHotkeyState.ToggleVSync;
}
return state;
}
} }
} }

118
Ryujinx/Ui/GLWidget.cs Normal file
View file

@ -0,0 +1,118 @@
using Gtk;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Platform.GLX;
using SPB.Platform.WGL;
using SPB.Windowing;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace Ryujinx.Ui
{
[ToolboxItem(true)]
public class GLWidget : DrawingArea
{
private bool _initialized;
public event EventHandler Initialized;
public event EventHandler ShuttingDown;
public OpenGLContextBase OpenGLContext { get; private set; }
public NativeWindowBase NativeWindow { get; private set; }
public FramebufferFormat FramebufferFormat { get; }
public int GLVersionMajor { get; }
public int GLVersionMinor { get; }
public OpenGLContextFlags ContextFlags { get; }
public bool DirectRendering { get; }
public OpenGLContextBase SharedContext { get; }
public GLWidget(FramebufferFormat framebufferFormat, int major, int minor, OpenGLContextFlags flags = OpenGLContextFlags.Default, bool directRendering = true, OpenGLContextBase sharedContext = null)
{
FramebufferFormat = framebufferFormat;
GLVersionMajor = major;
GLVersionMinor = minor;
ContextFlags = flags;
DirectRendering = directRendering;
SharedContext = sharedContext;
}
protected override bool OnDrawn(Cairo.Context cr)
{
if (!_initialized)
{
Intialize();
}
return true;
}
private NativeWindowBase RetrieveNativeWindow()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle);
return new WGLWindow(new NativeHandle(windowHandle));
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle);
IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle);
return new GLXWindow(new NativeHandle(displayHandle), new NativeHandle(windowHandle));
}
throw new NotImplementedException();
}
[DllImport("libgdk-3-0.dll")]
private static extern IntPtr gdk_win32_window_get_handle(IntPtr d);
[DllImport("libgdk-3.so.0")]
private static extern IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay);
[DllImport("libgdk-3.so.0")]
private static extern IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow);
private void Intialize()
{
NativeWindow = RetrieveNativeWindow();
Window.EnsureNative();
OpenGLContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat, GLVersionMajor, GLVersionMinor, ContextFlags, DirectRendering, SharedContext);
OpenGLContext.Initialize(NativeWindow);
OpenGLContext.MakeCurrent(NativeWindow);
_initialized = true;
Initialized?.Invoke(this, EventArgs.Empty);
}
protected override void Dispose(bool disposing)
{
// Try to bind the OpenGL context before calling the shutdown event
try
{
OpenGLContext?.MakeCurrent(NativeWindow);
}
catch (Exception) { }
ShuttingDown?.Invoke(this, EventArgs.Empty);
// Unbind context and destroy everything
try
{
OpenGLContext?.MakeCurrent(null);
}
catch (Exception) { }
OpenGLContext.Dispose();
}
}
}

View file

@ -1,17 +0,0 @@
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ui.Input
{
interface ButtonAssigner
{
void Init();
void ReadInput();
bool HasAnyButtonPressed();
bool ShouldCancel();
string GetPressedButton();
}
}

View file

@ -1,227 +0,0 @@
using OpenTK.Input;
using Ryujinx.Common.Configuration.Hid;
using System.Collections.Generic;
using System;
using System.IO;
namespace Ryujinx.Ui.Input
{
class JoystickButtonAssigner : ButtonAssigner
{
private int _index;
private double _triggerThreshold;
private JoystickState _currState;
private JoystickState _prevState;
private JoystickButtonDetector _detector;
public JoystickButtonAssigner(int index, double triggerThreshold)
{
_index = index;
_triggerThreshold = triggerThreshold;
_detector = new JoystickButtonDetector();
}
public void Init()
{
_currState = Joystick.GetState(_index);
_prevState = _currState;
}
public void ReadInput()
{
_prevState = _currState;
_currState = Joystick.GetState(_index);
CollectButtonStats();
}
public bool HasAnyButtonPressed()
{
return _detector.HasAnyButtonPressed();
}
public bool ShouldCancel()
{
return Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsAnyKeyDown;
}
public string GetPressedButton()
{
List<ControllerInputId> pressedButtons = _detector.GetPressedButtons();
// Reverse list so axis button take precedence when more than one button is recognized.
pressedButtons.Reverse();
return pressedButtons.Count > 0 ? pressedButtons[0].ToString() : "";
}
private void CollectButtonStats()
{
JoystickCapabilities capabilities = Joystick.GetCapabilities(_index);
ControllerInputId pressedButton;
// Buttons
for (int i = 0; i != capabilities.ButtonCount; i++)
{
if (_currState.IsButtonDown(i) && _prevState.IsButtonUp(i))
{
Enum.TryParse($"Button{i}", out pressedButton);
_detector.AddInput(pressedButton, 1);
}
if (_currState.IsButtonUp(i) && _prevState.IsButtonDown(i))
{
Enum.TryParse($"Button{i}", out pressedButton);
_detector.AddInput(pressedButton, -1);
}
}
// Axis
for (int i = 0; i != capabilities.AxisCount; i++)
{
float axisValue = _currState.GetAxis(i);
Enum.TryParse($"Axis{i}", out pressedButton);
_detector.AddInput(pressedButton, axisValue);
}
// Hats
for (int i = 0; i != capabilities.HatCount; i++)
{
string currPos = GetHatPosition(_currState.GetHat((JoystickHat)i));
string prevPos = GetHatPosition(_prevState.GetHat((JoystickHat)i));
if (currPos == prevPos)
{
continue;
}
if (currPos != "")
{
Enum.TryParse($"Hat{i}{currPos}", out pressedButton);
_detector.AddInput(pressedButton, 1);
}
if (prevPos != "")
{
Enum.TryParse($"Hat{i}{prevPos}", out pressedButton);
_detector.AddInput(pressedButton, -1);
}
}
}
private string GetHatPosition(JoystickHatState hatState)
{
if (hatState.IsUp) return "Up";
if (hatState.IsDown) return "Down";
if (hatState.IsLeft) return "Left";
if (hatState.IsRight) return "Right";
return "";
}
private class JoystickButtonDetector
{
private Dictionary<ControllerInputId, InputSummary> _stats;
public JoystickButtonDetector()
{
_stats = new Dictionary<ControllerInputId, InputSummary>();
}
public bool HasAnyButtonPressed()
{
foreach (var inputSummary in _stats.Values)
{
if (checkButtonPressed(inputSummary))
{
return true;
}
}
return false;
}
public List<ControllerInputId> GetPressedButtons()
{
List<ControllerInputId> pressedButtons = new List<ControllerInputId>();
foreach (var kvp in _stats)
{
if (!checkButtonPressed(kvp.Value))
{
continue;
}
pressedButtons.Add(kvp.Key);
}
return pressedButtons;
}
public void AddInput(ControllerInputId button, float value)
{
InputSummary inputSummary;
if (!_stats.TryGetValue(button, out inputSummary))
{
inputSummary = new InputSummary();
_stats.Add(button, inputSummary);
}
inputSummary.AddInput(value);
}
public override string ToString()
{
TextWriter writer = new StringWriter();
foreach (var kvp in _stats)
{
writer.WriteLine($"Button {kvp.Key} -> {kvp.Value}");
}
return writer.ToString();
}
private bool checkButtonPressed(InputSummary sequence)
{
float distance = Math.Abs(sequence.Min - sequence.Avg) + Math.Abs(sequence.Max - sequence.Avg);
return distance > 1.5; // distance range [0, 2]
}
}
private class InputSummary
{
public float Min, Max, Sum, Avg;
public int NumSamples;
public InputSummary()
{
Min = float.MaxValue;
Max = float.MinValue;
Sum = 0;
NumSamples = 0;
Avg = 0;
}
public void AddInput(float value)
{
Min = Math.Min(Min, value);
Max = Math.Max(Max, value);
Sum += value;
NumSamples += 1;
Avg = Sum / NumSamples;
}
public override string ToString()
{
return $"Avg: {Avg} Min: {Min} Max: {Max} Sum: {Sum} NumSamples: {NumSamples}";
}
}
}
}

View file

@ -1,51 +0,0 @@
using OpenTK.Input;
using System;
using Key = Ryujinx.Configuration.Hid.Key;
namespace Ryujinx.Ui.Input
{
class KeyboardKeyAssigner : ButtonAssigner
{
private int _index;
private KeyboardState _keyboardState;
public KeyboardKeyAssigner(int index)
{
_index = index;
}
public void Init() { }
public void ReadInput()
{
_keyboardState = KeyboardController.GetKeyboardState(_index);
}
public bool HasAnyButtonPressed()
{
return _keyboardState.IsAnyKeyDown;
}
public bool ShouldCancel()
{
return Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsKeyDown(OpenTK.Input.Key.Escape);
}
public string GetPressedButton()
{
string keyPressed = "";
foreach (Key key in Enum.GetValues(typeof(Key)))
{
if (_keyboardState.IsKeyDown((OpenTK.Input.Key)key))
{
keyPressed = key.ToString();
break;
}
}
return !ShouldCancel() ? keyPressed : "";
}
}
}

View file

@ -1,149 +0,0 @@
using OpenTK;
using OpenTK.Input;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using ControllerConfig = Ryujinx.Common.Configuration.Hid.ControllerConfig;
namespace Ryujinx.Ui
{
public class JoystickController
{
private readonly ControllerConfig _config;
public JoystickController(ControllerConfig config)
{
_config = config;
}
private bool IsEnabled()
{
return Joystick.GetState(_config.Index).IsConnected;
}
public ControllerKeys GetButtons()
{
// NOTE: This should be initialized AFTER GTK for compat reasons with OpenTK SDL2 backend and GTK on Linux.
// BODY: Usage of Joystick.GetState must be defer to after GTK full initialization. Otherwise, GTK will segfault because SDL2 was already init *sighs*
if (!IsEnabled())
{
return 0;
}
JoystickState joystickState = Joystick.GetState(_config.Index);
ControllerKeys buttons = 0;
if (IsActivated(joystickState, _config.LeftJoycon.DPadUp)) buttons |= ControllerKeys.DpadUp;
if (IsActivated(joystickState, _config.LeftJoycon.DPadDown)) buttons |= ControllerKeys.DpadDown;
if (IsActivated(joystickState, _config.LeftJoycon.DPadLeft)) buttons |= ControllerKeys.DpadLeft;
if (IsActivated(joystickState, _config.LeftJoycon.DPadRight)) buttons |= ControllerKeys.DpadRight;
if (IsActivated(joystickState, _config.LeftJoycon.StickButton)) buttons |= ControllerKeys.LStick;
if (IsActivated(joystickState, _config.LeftJoycon.ButtonMinus)) buttons |= ControllerKeys.Minus;
if (IsActivated(joystickState, _config.LeftJoycon.ButtonL)) buttons |= ControllerKeys.L;
if (IsActivated(joystickState, _config.LeftJoycon.ButtonZl)) buttons |= ControllerKeys.Zl;
if (IsActivated(joystickState, _config.LeftJoycon.ButtonSl)) buttons |= ControllerKeys.SlLeft;
if (IsActivated(joystickState, _config.LeftJoycon.ButtonSr)) buttons |= ControllerKeys.SrLeft;
if (IsActivated(joystickState, _config.RightJoycon.ButtonA)) buttons |= ControllerKeys.A;
if (IsActivated(joystickState, _config.RightJoycon.ButtonB)) buttons |= ControllerKeys.B;
if (IsActivated(joystickState, _config.RightJoycon.ButtonX)) buttons |= ControllerKeys.X;
if (IsActivated(joystickState, _config.RightJoycon.ButtonY)) buttons |= ControllerKeys.Y;
if (IsActivated(joystickState, _config.RightJoycon.StickButton)) buttons |= ControllerKeys.RStick;
if (IsActivated(joystickState, _config.RightJoycon.ButtonPlus)) buttons |= ControllerKeys.Plus;
if (IsActivated(joystickState, _config.RightJoycon.ButtonR)) buttons |= ControllerKeys.R;
if (IsActivated(joystickState, _config.RightJoycon.ButtonZr)) buttons |= ControllerKeys.Zr;
if (IsActivated(joystickState, _config.RightJoycon.ButtonSl)) buttons |= ControllerKeys.SlRight;
if (IsActivated(joystickState, _config.RightJoycon.ButtonSr)) buttons |= ControllerKeys.SrRight;
return buttons;
}
private bool IsActivated(JoystickState joystickState, ControllerInputId controllerInputId)
{
if (controllerInputId <= ControllerInputId.Button20)
{
return joystickState.IsButtonDown((int)controllerInputId);
}
else if (controllerInputId <= ControllerInputId.Axis5)
{
int axis = controllerInputId - ControllerInputId.Axis0;
return joystickState.GetAxis(axis) > _config.TriggerThreshold;
}
else if (controllerInputId <= ControllerInputId.Hat2Right)
{
int hat = (controllerInputId - ControllerInputId.Hat0Up) / 4;
int baseHatId = (int)ControllerInputId.Hat0Up + (hat * 4);
JoystickHatState hatState = joystickState.GetHat((JoystickHat)hat);
if (hatState.IsUp && ((int)controllerInputId % baseHatId == 0)) return true;
if (hatState.IsDown && ((int)controllerInputId % baseHatId == 1)) return true;
if (hatState.IsLeft && ((int)controllerInputId % baseHatId == 2)) return true;
if (hatState.IsRight && ((int)controllerInputId % baseHatId == 3)) return true;
}
return false;
}
public (short, short) GetLeftStick()
{
if (!IsEnabled())
{
return (0, 0);
}
return GetStick(_config.LeftJoycon.StickX, _config.LeftJoycon.StickY, _config.DeadzoneLeft);
}
public (short, short) GetRightStick()
{
if (!IsEnabled())
{
return (0, 0);
}
return GetStick(_config.RightJoycon.StickX, _config.RightJoycon.StickY, _config.DeadzoneRight);
}
private (short, short) GetStick(ControllerInputId stickXInputId, ControllerInputId stickYInputId, float deadzone)
{
if (stickXInputId < ControllerInputId.Axis0 || stickXInputId > ControllerInputId.Axis5 ||
stickYInputId < ControllerInputId.Axis0 || stickYInputId > ControllerInputId.Axis5)
{
return (0, 0);
}
JoystickState jsState = Joystick.GetState(_config.Index);
int xAxis = stickXInputId - ControllerInputId.Axis0;
int yAxis = stickYInputId - ControllerInputId.Axis0;
float xValue = jsState.GetAxis(xAxis);
float yValue = -jsState.GetAxis(yAxis); // Invert Y-axis
return ApplyDeadzone(new Vector2(xValue, yValue), deadzone);
}
private (short, short) ApplyDeadzone(Vector2 axis, float deadzone)
{
return (ClampAxis(MathF.Abs(axis.X) > deadzone ? axis.X : 0f),
ClampAxis(MathF.Abs(axis.Y) > deadzone ? axis.Y : 0f));
}
private static short ClampAxis(float value)
{
if (value <= -short.MaxValue)
{
return -short.MaxValue;
}
else
{
return (short)(value * short.MaxValue);
}
}
}
}

View file

@ -1,291 +0,0 @@
using System;
using OpenTK;
using OpenTK.Input;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Configuration;
using Ryujinx.HLE.HOS.Services.Hid;
namespace Ryujinx.Ui
{
[Flags]
public enum HotkeyButtons
{
ToggleVSync = 1 << 0,
}
public class KeyboardController
{
private readonly KeyboardConfig _config;
public KeyboardController(KeyboardConfig config)
{
_config = config;
}
public static KeyboardState GetKeyboardState(int index)
{
if (index == KeyboardConfig.AllKeyboardsIndex || index < 0)
{
return Keyboard.GetState();
}
return Keyboard.GetState(index - 1);
}
public ControllerKeys GetButtons()
{
KeyboardState keyboard = GetKeyboardState(_config.Index);
ControllerKeys buttons = 0;
if (keyboard[(Key)_config.LeftJoycon.StickButton]) buttons |= ControllerKeys.LStick;
if (keyboard[(Key)_config.LeftJoycon.DPadUp]) buttons |= ControllerKeys.DpadUp;
if (keyboard[(Key)_config.LeftJoycon.DPadDown]) buttons |= ControllerKeys.DpadDown;
if (keyboard[(Key)_config.LeftJoycon.DPadLeft]) buttons |= ControllerKeys.DpadLeft;
if (keyboard[(Key)_config.LeftJoycon.DPadRight]) buttons |= ControllerKeys.DpadRight;
if (keyboard[(Key)_config.LeftJoycon.ButtonMinus]) buttons |= ControllerKeys.Minus;
if (keyboard[(Key)_config.LeftJoycon.ButtonL]) buttons |= ControllerKeys.L;
if (keyboard[(Key)_config.LeftJoycon.ButtonZl]) buttons |= ControllerKeys.Zl;
if (keyboard[(Key)_config.LeftJoycon.ButtonSl]) buttons |= ControllerKeys.SlLeft;
if (keyboard[(Key)_config.LeftJoycon.ButtonSr]) buttons |= ControllerKeys.SrLeft;
if (keyboard[(Key)_config.RightJoycon.StickButton]) buttons |= ControllerKeys.RStick;
if (keyboard[(Key)_config.RightJoycon.ButtonA]) buttons |= ControllerKeys.A;
if (keyboard[(Key)_config.RightJoycon.ButtonB]) buttons |= ControllerKeys.B;
if (keyboard[(Key)_config.RightJoycon.ButtonX]) buttons |= ControllerKeys.X;
if (keyboard[(Key)_config.RightJoycon.ButtonY]) buttons |= ControllerKeys.Y;
if (keyboard[(Key)_config.RightJoycon.ButtonPlus]) buttons |= ControllerKeys.Plus;
if (keyboard[(Key)_config.RightJoycon.ButtonR]) buttons |= ControllerKeys.R;
if (keyboard[(Key)_config.RightJoycon.ButtonZr]) buttons |= ControllerKeys.Zr;
if (keyboard[(Key)_config.RightJoycon.ButtonSl]) buttons |= ControllerKeys.SlRight;
if (keyboard[(Key)_config.RightJoycon.ButtonSr]) buttons |= ControllerKeys.SrRight;
return buttons;
}
public (short, short) GetLeftStick()
{
KeyboardState keyboard = GetKeyboardState(_config.Index);
short dx = 0;
short dy = 0;
if (keyboard[(Key)_config.LeftJoycon.StickUp]) dy += 1;
if (keyboard[(Key)_config.LeftJoycon.StickDown]) dy += -1;
if (keyboard[(Key)_config.LeftJoycon.StickLeft]) dx += -1;
if (keyboard[(Key)_config.LeftJoycon.StickRight]) dx += 1;
Vector2 stick = new Vector2(dx, dy);
stick.NormalizeFast();
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public (short, short) GetRightStick()
{
KeyboardState keyboard = GetKeyboardState(_config.Index);
short dx = 0;
short dy = 0;
if (keyboard[(Key)_config.RightJoycon.StickUp]) dy += 1;
if (keyboard[(Key)_config.RightJoycon.StickDown]) dy += -1;
if (keyboard[(Key)_config.RightJoycon.StickLeft]) dx += -1;
if (keyboard[(Key)_config.RightJoycon.StickRight]) dx += 1;
Vector2 stick = new Vector2(dx, dy);
stick.NormalizeFast();
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public static HotkeyButtons GetHotkeyButtons(KeyboardState keyboard)
{
HotkeyButtons buttons = 0;
if (keyboard[(Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync])
{
buttons |= HotkeyButtons.ToggleVSync;
}
return buttons;
}
class KeyMappingEntry
{
public Key TargetKey;
public byte Target;
}
private static readonly KeyMappingEntry[] KeyMapping = new KeyMappingEntry[]
{
new KeyMappingEntry { TargetKey = Key.A, Target = 0x4 },
new KeyMappingEntry { TargetKey = Key.B, Target = 0x5 },
new KeyMappingEntry { TargetKey = Key.C, Target = 0x6 },
new KeyMappingEntry { TargetKey = Key.D, Target = 0x7 },
new KeyMappingEntry { TargetKey = Key.E, Target = 0x8 },
new KeyMappingEntry { TargetKey = Key.F, Target = 0x9 },
new KeyMappingEntry { TargetKey = Key.G, Target = 0xA },
new KeyMappingEntry { TargetKey = Key.H, Target = 0xB },
new KeyMappingEntry { TargetKey = Key.I, Target = 0xC },
new KeyMappingEntry { TargetKey = Key.J, Target = 0xD },
new KeyMappingEntry { TargetKey = Key.K, Target = 0xE },
new KeyMappingEntry { TargetKey = Key.L, Target = 0xF },
new KeyMappingEntry { TargetKey = Key.M, Target = 0x10 },
new KeyMappingEntry { TargetKey = Key.N, Target = 0x11 },
new KeyMappingEntry { TargetKey = Key.O, Target = 0x12 },
new KeyMappingEntry { TargetKey = Key.P, Target = 0x13 },
new KeyMappingEntry { TargetKey = Key.Q, Target = 0x14 },
new KeyMappingEntry { TargetKey = Key.R, Target = 0x15 },
new KeyMappingEntry { TargetKey = Key.S, Target = 0x16 },
new KeyMappingEntry { TargetKey = Key.T, Target = 0x17 },
new KeyMappingEntry { TargetKey = Key.U, Target = 0x18 },
new KeyMappingEntry { TargetKey = Key.V, Target = 0x19 },
new KeyMappingEntry { TargetKey = Key.W, Target = 0x1A },
new KeyMappingEntry { TargetKey = Key.X, Target = 0x1B },
new KeyMappingEntry { TargetKey = Key.Y, Target = 0x1C },
new KeyMappingEntry { TargetKey = Key.Z, Target = 0x1D },
new KeyMappingEntry { TargetKey = Key.Number1, Target = 0x1E },
new KeyMappingEntry { TargetKey = Key.Number2, Target = 0x1F },
new KeyMappingEntry { TargetKey = Key.Number3, Target = 0x20 },
new KeyMappingEntry { TargetKey = Key.Number4, Target = 0x21 },
new KeyMappingEntry { TargetKey = Key.Number5, Target = 0x22 },
new KeyMappingEntry { TargetKey = Key.Number6, Target = 0x23 },
new KeyMappingEntry { TargetKey = Key.Number7, Target = 0x24 },
new KeyMappingEntry { TargetKey = Key.Number8, Target = 0x25 },
new KeyMappingEntry { TargetKey = Key.Number9, Target = 0x26 },
new KeyMappingEntry { TargetKey = Key.Number0, Target = 0x27 },
new KeyMappingEntry { TargetKey = Key.Enter, Target = 0x28 },
new KeyMappingEntry { TargetKey = Key.Escape, Target = 0x29 },
new KeyMappingEntry { TargetKey = Key.BackSpace, Target = 0x2A },
new KeyMappingEntry { TargetKey = Key.Tab, Target = 0x2B },
new KeyMappingEntry { TargetKey = Key.Space, Target = 0x2C },
new KeyMappingEntry { TargetKey = Key.Minus, Target = 0x2D },
new KeyMappingEntry { TargetKey = Key.Plus, Target = 0x2E },
new KeyMappingEntry { TargetKey = Key.BracketLeft, Target = 0x2F },
new KeyMappingEntry { TargetKey = Key.BracketRight, Target = 0x30 },
new KeyMappingEntry { TargetKey = Key.BackSlash, Target = 0x31 },
new KeyMappingEntry { TargetKey = Key.Tilde, Target = 0x32 },
new KeyMappingEntry { TargetKey = Key.Semicolon, Target = 0x33 },
new KeyMappingEntry { TargetKey = Key.Quote, Target = 0x34 },
new KeyMappingEntry { TargetKey = Key.Grave, Target = 0x35 },
new KeyMappingEntry { TargetKey = Key.Comma, Target = 0x36 },
new KeyMappingEntry { TargetKey = Key.Period, Target = 0x37 },
new KeyMappingEntry { TargetKey = Key.Slash, Target = 0x38 },
new KeyMappingEntry { TargetKey = Key.CapsLock, Target = 0x39 },
new KeyMappingEntry { TargetKey = Key.F1, Target = 0x3a },
new KeyMappingEntry { TargetKey = Key.F2, Target = 0x3b },
new KeyMappingEntry { TargetKey = Key.F3, Target = 0x3c },
new KeyMappingEntry { TargetKey = Key.F4, Target = 0x3d },
new KeyMappingEntry { TargetKey = Key.F5, Target = 0x3e },
new KeyMappingEntry { TargetKey = Key.F6, Target = 0x3f },
new KeyMappingEntry { TargetKey = Key.F7, Target = 0x40 },
new KeyMappingEntry { TargetKey = Key.F8, Target = 0x41 },
new KeyMappingEntry { TargetKey = Key.F9, Target = 0x42 },
new KeyMappingEntry { TargetKey = Key.F10, Target = 0x43 },
new KeyMappingEntry { TargetKey = Key.F11, Target = 0x44 },
new KeyMappingEntry { TargetKey = Key.F12, Target = 0x45 },
new KeyMappingEntry { TargetKey = Key.PrintScreen, Target = 0x46 },
new KeyMappingEntry { TargetKey = Key.ScrollLock, Target = 0x47 },
new KeyMappingEntry { TargetKey = Key.Pause, Target = 0x48 },
new KeyMappingEntry { TargetKey = Key.Insert, Target = 0x49 },
new KeyMappingEntry { TargetKey = Key.Home, Target = 0x4A },
new KeyMappingEntry { TargetKey = Key.PageUp, Target = 0x4B },
new KeyMappingEntry { TargetKey = Key.Delete, Target = 0x4C },
new KeyMappingEntry { TargetKey = Key.End, Target = 0x4D },
new KeyMappingEntry { TargetKey = Key.PageDown, Target = 0x4E },
new KeyMappingEntry { TargetKey = Key.Right, Target = 0x4F },
new KeyMappingEntry { TargetKey = Key.Left, Target = 0x50 },
new KeyMappingEntry { TargetKey = Key.Down, Target = 0x51 },
new KeyMappingEntry { TargetKey = Key.Up, Target = 0x52 },
new KeyMappingEntry { TargetKey = Key.NumLock, Target = 0x53 },
new KeyMappingEntry { TargetKey = Key.KeypadDivide, Target = 0x54 },
new KeyMappingEntry { TargetKey = Key.KeypadMultiply, Target = 0x55 },
new KeyMappingEntry { TargetKey = Key.KeypadMinus, Target = 0x56 },
new KeyMappingEntry { TargetKey = Key.KeypadPlus, Target = 0x57 },
new KeyMappingEntry { TargetKey = Key.KeypadEnter, Target = 0x58 },
new KeyMappingEntry { TargetKey = Key.Keypad1, Target = 0x59 },
new KeyMappingEntry { TargetKey = Key.Keypad2, Target = 0x5A },
new KeyMappingEntry { TargetKey = Key.Keypad3, Target = 0x5B },
new KeyMappingEntry { TargetKey = Key.Keypad4, Target = 0x5C },
new KeyMappingEntry { TargetKey = Key.Keypad5, Target = 0x5D },
new KeyMappingEntry { TargetKey = Key.Keypad6, Target = 0x5E },
new KeyMappingEntry { TargetKey = Key.Keypad7, Target = 0x5F },
new KeyMappingEntry { TargetKey = Key.Keypad8, Target = 0x60 },
new KeyMappingEntry { TargetKey = Key.Keypad9, Target = 0x61 },
new KeyMappingEntry { TargetKey = Key.Keypad0, Target = 0x62 },
new KeyMappingEntry { TargetKey = Key.KeypadPeriod, Target = 0x63 },
new KeyMappingEntry { TargetKey = Key.NonUSBackSlash, Target = 0x64 },
new KeyMappingEntry { TargetKey = Key.F13, Target = 0x68 },
new KeyMappingEntry { TargetKey = Key.F14, Target = 0x69 },
new KeyMappingEntry { TargetKey = Key.F15, Target = 0x6A },
new KeyMappingEntry { TargetKey = Key.F16, Target = 0x6B },
new KeyMappingEntry { TargetKey = Key.F17, Target = 0x6C },
new KeyMappingEntry { TargetKey = Key.F18, Target = 0x6D },
new KeyMappingEntry { TargetKey = Key.F19, Target = 0x6E },
new KeyMappingEntry { TargetKey = Key.F20, Target = 0x6F },
new KeyMappingEntry { TargetKey = Key.F21, Target = 0x70 },
new KeyMappingEntry { TargetKey = Key.F22, Target = 0x71 },
new KeyMappingEntry { TargetKey = Key.F23, Target = 0x72 },
new KeyMappingEntry { TargetKey = Key.F24, Target = 0x73 },
new KeyMappingEntry { TargetKey = Key.ControlLeft, Target = 0xE0 },
new KeyMappingEntry { TargetKey = Key.ShiftLeft, Target = 0xE1 },
new KeyMappingEntry { TargetKey = Key.AltLeft, Target = 0xE2 },
new KeyMappingEntry { TargetKey = Key.WinLeft, Target = 0xE3 },
new KeyMappingEntry { TargetKey = Key.ControlRight, Target = 0xE4 },
new KeyMappingEntry { TargetKey = Key.ShiftRight, Target = 0xE5 },
new KeyMappingEntry { TargetKey = Key.AltRight, Target = 0xE6 },
new KeyMappingEntry { TargetKey = Key.WinRight, Target = 0xE7 },
};
private static readonly KeyMappingEntry[] KeyModifierMapping = new KeyMappingEntry[]
{
new KeyMappingEntry { TargetKey = Key.ControlLeft, Target = 0 },
new KeyMappingEntry { TargetKey = Key.ShiftLeft, Target = 1 },
new KeyMappingEntry { TargetKey = Key.AltLeft, Target = 2 },
new KeyMappingEntry { TargetKey = Key.WinLeft, Target = 3 },
new KeyMappingEntry { TargetKey = Key.ControlRight, Target = 4 },
new KeyMappingEntry { TargetKey = Key.ShiftRight, Target = 5 },
new KeyMappingEntry { TargetKey = Key.AltRight, Target = 6 },
new KeyMappingEntry { TargetKey = Key.WinRight, Target = 7 },
new KeyMappingEntry { TargetKey = Key.CapsLock, Target = 8 },
new KeyMappingEntry { TargetKey = Key.ScrollLock, Target = 9 },
new KeyMappingEntry { TargetKey = Key.NumLock, Target = 10 },
};
public KeyboardInput GetKeysDown()
{
KeyboardState keyboard = GetKeyboardState(_config.Index);
KeyboardInput hidKeyboard = new KeyboardInput
{
Modifier = 0,
Keys = new int[0x8]
};
foreach (KeyMappingEntry entry in KeyMapping)
{
int value = keyboard[entry.TargetKey] ? 1 : 0;
hidKeyboard.Keys[entry.Target / 0x20] |= (value << (entry.Target % 0x20));
}
foreach (KeyMappingEntry entry in KeyModifierMapping)
{
int value = keyboard[entry.TargetKey] ? 1 : 0;
hidKeyboard.Modifier |= value << entry.Target;
}
return hidKeyboard;
}
}
}

View file

@ -17,6 +17,9 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.GTK3;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2;
using Ryujinx.Modules; using Ryujinx.Modules;
using Ryujinx.Ui.App; using Ryujinx.Ui.App;
using Ryujinx.Ui.Applet; using Ryujinx.Ui.Applet;
@ -65,6 +68,7 @@ namespace Ryujinx.Ui
private bool _lastScannedAmiiboShowAll = false; private bool _lastScannedAmiiboShowAll = false;
public GlRenderer GlRendererWidget; public GlRenderer GlRendererWidget;
public InputManager InputManager;
#pragma warning disable CS0169, CS0649, IDE0044 #pragma warning disable CS0169, CS0649, IDE0044
@ -223,6 +227,8 @@ namespace Ryujinx.Ui
}; };
Task.Run(RefreshFirmwareLabel); Task.Run(RefreshFirmwareLabel);
InputManager = new InputManager(new GTK3KeyboardDriver(this), new SDL2GamepadDriver());
} }
private void WindowStateEvent_Changed(object o, WindowStateEventArgs args) private void WindowStateEvent_Changed(object o, WindowStateEventArgs args)
@ -295,6 +301,11 @@ namespace Ryujinx.Ui
} }
} }
protected override void OnDestroyed()
{
InputManager.Dispose();
}
private void InitializeSwitchInstance() private void InitializeSwitchInstance()
{ {
_virtualFileSystem.Reload(); _virtualFileSystem.Reload();
@ -636,7 +647,7 @@ namespace Ryujinx.Ui
DisplaySleep.Prevent(); DisplaySleep.Prevent();
GlRendererWidget = new GlRenderer(_emulationContext, ConfigurationState.Instance.Logger.GraphicsDebugLevel); GlRendererWidget = new GlRenderer(_emulationContext, InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
Application.Invoke(delegate Application.Invoke(delegate
{ {

View file

@ -0,0 +1,20 @@
using SPB.Graphics;
using System;
namespace Ryujinx.Ui
{
public class OpenToolkitBindingsContext : OpenTK.IBindingsContext
{
private IBindingsContext _bindingContext;
public OpenToolkitBindingsContext(IBindingsContext bindingsContext)
{
_bindingContext = bindingsContext;
}
public IntPtr GetProcAddress(string procName)
{
return _bindingContext.GetProcAddress(procName);
}
}
}

View file

@ -0,0 +1,49 @@
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Graphics.OpenGL;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
namespace Ryujinx.Ui
{
class SPBOpenGLContext : IOpenGLContext
{
private OpenGLContextBase _context;
private NativeWindowBase _window;
private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
{
_context = context;
_window = window;
}
public void Dispose()
{
_context.Dispose();
_window.Dispose();
}
public void MakeCurrent()
{
_context.MakeCurrent(_window);
}
public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
{
OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
NativeWindowBase window = PlatformHelper.CreateWindow(FramebufferFormat.Default, 0, 0, 100, 100);
context.Initialize(window);
context.MakeCurrent(window);
GL.LoadBindings(new OpenToolkitBindingsContext(context));
context.MakeCurrent(null);
return new SPBOpenGLContext(context, window);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -48,8 +48,8 @@
<property name="title" translatable="yes">Ryujinx - Controller Settings</property> <property name="title" translatable="yes">Ryujinx - Controller Settings</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="window_position">center</property> <property name="window_position">center</property>
<property name="default_width">1150</property> <property name="default_width">1200</property>
<property name="default_height">690</property> <property name="default_height">720</property>
<child type="titlebar"> <child type="titlebar">
<placeholder/> <placeholder/>
</child> </child>
@ -113,21 +113,6 @@
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkToggleButton" id="_refreshInputDevicesButton">
<property name="label" translatable="yes">Refresh</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_left">5</property>
<signal name="toggled" handler="RefreshInputDevicesButton_Pressed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -682,7 +667,7 @@
<property name="width_request">80</property> <property name="width_request">80</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">LStick Lt/Rt</property> <property name="label" translatable="yes">LStick</property>
<property name="xalign">0</property> <property name="xalign">0</property>
</object> </object>
<packing> <packing>
@ -691,20 +676,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel"> <object class="GtkToggleButton" id="_lStick">
<property name="width_request">80</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">LStick Up/Dn</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="_lStickX">
<property name="label" translatable="yes"> </property> <property name="label" translatable="yes"> </property>
<property name="width_request">65</property> <property name="width_request">65</property>
<property name="visible">True</property> <property name="visible">True</property>
@ -716,22 +688,9 @@
<property name="top_attach">0</property> <property name="top_attach">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkToggleButton" id="_lStickY">
<property name="label" translatable="yes"> </property>
<property name="width_request">65</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child> <child>
<object class="GtkCheckButton" id="_invertLStickX"> <object class="GtkCheckButton" id="_invertLStickX">
<property name="label" translatable="yes">Invert</property> <property name="label" translatable="yes">Invert Stick X</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
@ -744,7 +703,7 @@
</child> </child>
<child> <child>
<object class="GtkCheckButton" id="_invertLStickY"> <object class="GtkCheckButton" id="_invertLStickY">
<property name="label" translatable="yes">Invert</property> <property name="label" translatable="yes">Invert Stick Y</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
@ -1501,7 +1460,7 @@
<property name="width_request">80</property> <property name="width_request">80</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">RStick Lt/Rt</property> <property name="label" translatable="yes">RStick</property>
<property name="xalign">0</property> <property name="xalign">0</property>
</object> </object>
<packing> <packing>
@ -1510,20 +1469,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel"> <object class="GtkToggleButton" id="_rStick">
<property name="width_request">80</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">RStick Up/Dn</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="_rStickX">
<property name="label" translatable="yes"> </property> <property name="label" translatable="yes"> </property>
<property name="width_request">65</property> <property name="width_request">65</property>
<property name="visible">True</property> <property name="visible">True</property>
@ -1535,22 +1481,9 @@
<property name="top_attach">0</property> <property name="top_attach">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkToggleButton" id="_rStickY">
<property name="label" translatable="yes"> </property>
<property name="width_request">65</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child> <child>
<object class="GtkCheckButton" id="_invertRStickX"> <object class="GtkCheckButton" id="_invertRStickX">
<property name="label" translatable="yes">Invert</property> <property name="label" translatable="yes">Invert Stick X</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
@ -1563,7 +1496,7 @@
</child> </child>
<child> <child>
<object class="GtkCheckButton" id="_invertRStickY"> <object class="GtkCheckButton" id="_invertRStickY">
<property name="label" translatable="yes">Invert</property> <property name="label" translatable="yes">Invert Stick Y</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">False</property> <property name="receives_default">False</property>
@ -1640,7 +1573,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox" id="MotionBox"> <object class="GtkBox" id="_motionBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">10</property> <property name="margin_left">10</property>
@ -1679,7 +1612,21 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkCheckButton" id="_enableCemuHook">
<property name="label" translatable="yes">Use CemuHook compatible motion</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="_motionControllerSlot">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">10</property> <property name="spacing">10</property>
@ -1718,7 +1665,7 @@
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="padding">5</property> <property name="padding">5</property>
<property name="position">2</property> <property name="position">3</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1761,11 +1708,11 @@
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="padding">5</property> <property name="padding">5</property>
<property name="position">3</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox" id="_altBox"> <object class="GtkBox" id="_motionAltBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
@ -1829,11 +1776,11 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">4</property> <property name="position">5</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox" id="_dsuServerHostBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">30</property> <property name="spacing">30</property>
@ -1866,11 +1813,11 @@
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="padding">5</property> <property name="padding">5</property>
<property name="position">5</property> <property name="position">6</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox" id="_dsuServerPortBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">30</property> <property name="spacing">30</property>
@ -1903,7 +1850,7 @@
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="padding">5</property> <property name="padding">5</property>
<property name="position">6</property> <property name="position">7</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1916,7 +1863,7 @@
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">7</property> <property name="position">8</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -1930,7 +1877,7 @@
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">8</property> <property name="position">9</property>
</packing> </packing>
</child> </child>
</object> </object>

View file

@ -578,7 +578,7 @@ namespace Ryujinx.Ui.Windows
{ {
((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
ControllerWindow controllerWindow = new ControllerWindow(playerIndex); ControllerWindow controllerWindow = new ControllerWindow(_parent, playerIndex);
controllerWindow.SetSizeRequest((int)(controllerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(controllerWindow.DefaultHeight * Program.WindowScaleFactor)); controllerWindow.SetSizeRequest((int)(controllerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(controllerWindow.DefaultHeight * Program.WindowScaleFactor));
controllerWindow.Show(); controllerWindow.Show();