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,5 +1,5 @@
{
"version": 22,
"version": 24,
"res_scale": 1,
"res_scale_custom": 1,
"max_anisotropy": -1,
@ -13,6 +13,7 @@
"logging_enable_guest": true,
"logging_enable_fs_access_log": false,
"logging_filtered_classes": [],
"logging_graphics_debug_level": "None",
"enable_file_log": true,
"system_language": "AmericanEnglish",
"system_region": "USA",
@ -25,10 +26,12 @@
"hide_cursor_on_idle": false,
"enable_vsync": true,
"enable_shader_cache": true,
"enable_multicore_scheduling": false,
"enable_ptc": true,
"enable_fs_integrity_checks": true,
"fs_global_access_log_mode": 0,
"audio_backend": "OpenAl",
"expand_ram": false,
"ignore_missing_services": false,
"gui_columns": {
"fav_column": true,
@ -54,51 +57,51 @@
"hotkeys": {
"toggle_vsync": "Tab"
},
"keyboard_config": [
"keyboard_config": [],
"controller_config": [],
"input_config": [
{
"index": 0,
"controller_type": "JoyconPair",
"player_index": "Player1",
"left_joycon": {
"left_joycon_stick": {
"stick_up": "W",
"stick_down": "S",
"stick_left": "A",
"stick_right": "D",
"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"
"stick_button": "F"
},
"right_joycon": {
"right_joycon_stick": {
"stick_up": "I",
"stick_down": "K",
"stick_left": "J",
"stick_right": "L",
"stick_button": "H",
"button_a": "Z",
"button_b": "X",
"button_x": "C",
"button_y": "V",
"stick_button": "H"
},
"left_joycon": {
"button_minus": "Minus",
"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_r": "U",
"button_zr": "O",
"button_sl": "Unbound",
"button_sr": "Unbound"
"button_sr": "Unbound",
"button_x": "C",
"button_b": "X",
"button_y": "V",
"button_a": "Z"
},
"slot": 0,
"alt_slot": 0,
"mirror_input": false,
"dsu_server_host": "127.0.0.1",
"dsu_server_port": 26760,
"sensitivity": 100,
"enable_motion": false
"version": 1,
"backend": "WindowKeyboard",
"id": "0",
"controller_type": "JoyconPair",
"player_index": "Player1"
}
],
"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,459 +0,0 @@
using Force.Crc32;
using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Threading.Tasks;
namespace Ryujinx.Modules.Motion
{
public class Client : IDisposable
{
public const uint Magic = 0x43555344; // DSUC
public const ushort Version = 1001;
private bool _active;
private readonly Dictionary<int, IPEndPoint> _hosts;
private readonly Dictionary<int, Dictionary<int, MotionInput>> _motionData;
private readonly Dictionary<int, UdpClient> _clients;
private readonly bool[] _clientErrorStatus = new bool[Enum.GetValues(typeof(PlayerIndex)).Length];
private readonly long[] _clientRetryTimer = new long[Enum.GetValues(typeof(PlayerIndex)).Length];
public Client()
{
_hosts = new Dictionary<int, IPEndPoint>();
_motionData = new Dictionary<int, Dictionary<int, MotionInput>>();
_clients = new Dictionary<int, UdpClient>();
CloseClients();
}
public void CloseClients()
{
_active = false;
lock (_clients)
{
foreach (var client in _clients)
{
try
{
client.Value?.Dispose();
}
catch (SocketException socketException)
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to dispose motion client. Error: {socketException.ErrorCode}");
}
}
_hosts.Clear();
_clients.Clear();
_motionData.Clear();
}
}
public void RegisterClient(int player, string host, int port)
{
if (_clients.ContainsKey(player) || !CanConnect(player))
{
return;
}
lock (_clients)
{
if (_clients.ContainsKey(player) || !CanConnect(player))
{
return;
}
UdpClient client = null;
try
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(host), port);
client = new UdpClient(host, port);
_clients.Add(player, client);
_hosts.Add(player, endPoint);
_active = true;
Task.Run(() =>
{
ReceiveLoop(player);
});
}
catch (FormatException formatException)
{
if (!_clientErrorStatus[player])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {formatException.Message}");
_clientErrorStatus[player] = true;
}
}
catch (SocketException socketException)
{
if (!_clientErrorStatus[player])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {socketException.ErrorCode}");
_clientErrorStatus[player] = true;
}
RemoveClient(player);
client?.Dispose();
SetRetryTimer(player);
}
catch (Exception exception)
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to register motion client. Error: {exception.Message}");
_clientErrorStatus[player] = true;
RemoveClient(player);
client?.Dispose();
SetRetryTimer(player);
}
}
}
public bool TryGetData(int player, int slot, out MotionInput input)
{
lock (_motionData)
{
if (_motionData.ContainsKey(player))
{
if (_motionData[player].TryGetValue(slot, out input))
{
return true;
}
}
}
input = null;
return false;
}
private void RemoveClient(int clientId)
{
_clients?.Remove(clientId);
_hosts?.Remove(clientId);
}
private void Send(byte[] data, int clientId)
{
if (_clients.TryGetValue(clientId, out UdpClient _client))
{
if (_client != null && _client.Client != null && _client.Client.Connected)
{
try
{
_client?.Send(data, data.Length);
}
catch (SocketException socketException)
{
if (!_clientErrorStatus[clientId])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to send data request to motion source at {_client.Client.RemoteEndPoint}. Error: {socketException.ErrorCode}");
}
_clientErrorStatus[clientId] = true;
RemoveClient(clientId);
_client?.Dispose();
SetRetryTimer(clientId);
}
catch (ObjectDisposedException)
{
_clientErrorStatus[clientId] = true;
RemoveClient(clientId);
_client?.Dispose();
SetRetryTimer(clientId);
}
}
}
}
private byte[] Receive(int clientId, int timeout = 0)
{
if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client))
{
if (_client != null && _client.Client != null && _client.Client.Connected)
{
_client.Client.ReceiveTimeout = timeout;
var result = _client?.Receive(ref endPoint);
if (result.Length > 0)
{
_clientErrorStatus[clientId] = false;
}
return result;
}
}
throw new Exception($"Client {clientId} is not registered.");
}
private void SetRetryTimer(int clientId)
{
var elapsedMs = PerformanceCounter.ElapsedMilliseconds;
_clientRetryTimer[clientId] = elapsedMs;
}
private void ResetRetryTimer(int clientId)
{
_clientRetryTimer[clientId] = 0;
}
private bool CanConnect(int clientId)
{
return _clientRetryTimer[clientId] == 0 || PerformanceCounter.ElapsedMilliseconds - 5000 > _clientRetryTimer[clientId];
}
public void ReceiveLoop(int clientId)
{
if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client))
{
if (_client != null && _client.Client != null && _client.Client.Connected)
{
try
{
while (_active)
{
byte[] data = Receive(clientId);
if (data.Length == 0)
{
continue;
}
Task.Run(() => HandleResponse(data, clientId));
}
}
catch (SocketException socketException)
{
if (!_clientErrorStatus[clientId])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to receive data from motion source at {endPoint}. Error: {socketException.ErrorCode}");
}
_clientErrorStatus[clientId] = true;
RemoveClient(clientId);
_client?.Dispose();
SetRetryTimer(clientId);
}
catch (ObjectDisposedException)
{
_clientErrorStatus[clientId] = true;
RemoveClient(clientId);
_client?.Dispose();
SetRetryTimer(clientId);
}
}
}
}
public void HandleResponse(byte[] data, int clientId)
{
ResetRetryTimer(clientId);
MessageType type = (MessageType)BitConverter.ToUInt32(data.AsSpan().Slice(16, 4));
data = data.AsSpan()[16..].ToArray();
using MemoryStream stream = new MemoryStream(data);
using BinaryReader reader = new BinaryReader(stream);
switch (type)
{
case MessageType.Protocol:
break;
case MessageType.Info:
ControllerInfoResponse contollerInfo = reader.ReadStruct<ControllerInfoResponse>();
break;
case MessageType.Data:
ControllerDataResponse inputData = reader.ReadStruct<ControllerDataResponse>();
Vector3 accelerometer = new Vector3()
{
X = -inputData.AccelerometerX,
Y = inputData.AccelerometerZ,
Z = -inputData.AccelerometerY
};
Vector3 gyroscrope = new Vector3()
{
X = inputData.GyroscopePitch,
Y = inputData.GyroscopeRoll,
Z = -inputData.GyroscopeYaw
};
ulong timestamp = inputData.MotionTimestamp;
InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == (PlayerIndex)clientId);
lock (_motionData)
{
int slot = inputData.Shared.Slot;
if (_motionData.ContainsKey(clientId))
{
if (_motionData[clientId].ContainsKey(slot))
{
MotionInput previousData = _motionData[clientId][slot];
previousData.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
}
else
{
MotionInput input = new MotionInput();
input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
_motionData[clientId].Add(slot, input);
}
}
else
{
MotionInput input = new MotionInput();
input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
_motionData.Add(clientId, new Dictionary<int, MotionInput>() { { slot, input } });
}
}
break;
}
}
public void RequestInfo(int clientId, int slot)
{
if (!_active)
{
return;
}
Header header = GenerateHeader(clientId);
using (MemoryStream stream = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.WriteStruct(header);
ControllerInfoRequest request = new ControllerInfoRequest()
{
Type = MessageType.Info,
PortsCount = 4
};
request.PortIndices[0] = (byte)slot;
writer.WriteStruct(request);
header.Length = (ushort)(stream.Length - 16);
writer.Seek(6, SeekOrigin.Begin);
writer.Write(header.Length);
header.Crc32 = Crc32Algorithm.Compute(stream.ToArray());
writer.Seek(8, SeekOrigin.Begin);
writer.Write(header.Crc32);
byte[] data = stream.ToArray();
Send(data, clientId);
}
}
public unsafe void RequestData(int clientId, int slot)
{
if (!_active)
{
return;
}
Header header = GenerateHeader(clientId);
using (MemoryStream stream = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.WriteStruct(header);
ControllerDataRequest request = new ControllerDataRequest()
{
Type = MessageType.Data,
Slot = (byte)slot,
SubscriberType = SubscriberType.Slot
};
writer.WriteStruct(request);
header.Length = (ushort)(stream.Length - 16);
writer.Seek(6, SeekOrigin.Begin);
writer.Write(header.Length);
header.Crc32 = Crc32Algorithm.Compute(stream.ToArray());
writer.Seek(8, SeekOrigin.Begin);
writer.Write(header.Crc32);
byte[] data = stream.ToArray();
Send(data, clientId);
}
}
private Header GenerateHeader(int clientId)
{
Header header = new Header()
{
Id = (uint)clientId,
MagicString = Magic,
Version = Version,
Length = 0,
Crc32 = 0
};
return header;
}
public void Dispose()
{
_active = false;
CloseClients();
}
}
}

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,85 +0,0 @@
using System;
using System.Numerics;
namespace Ryujinx.Modules.Motion
{
public class MotionInput
{
public ulong TimeStamp { get; set; }
public Vector3 Accelerometer { get; set; }
public Vector3 Gyroscrope { get; set; }
public Vector3 Rotation { get; set; }
private readonly MotionSensorFilter _filter;
private int _calibrationFrame = 0;
public MotionInput()
{
TimeStamp = 0;
Accelerometer = new Vector3();
Gyroscrope = new Vector3();
Rotation = new Vector3();
// TODO: RE the correct filter.
_filter = new MotionSensorFilter(0f);
}
public void Update(Vector3 accel, Vector3 gyro, ulong timestamp, int sensitivity, float deadzone)
{
if (TimeStamp != 0)
{
if (gyro.Length() <= 1f && accel.Length() >= 0.8f && accel.Z >= 0.8f)
{
_calibrationFrame++;
if (_calibrationFrame >= 90)
{
gyro = Vector3.Zero;
Rotation = Vector3.Zero;
_filter.Reset();
_calibrationFrame = 0;
}
}
else
{
_calibrationFrame = 0;
}
Accelerometer = -accel;
if (gyro.Length() < deadzone)
{
gyro = Vector3.Zero;
}
gyro *= (sensitivity / 100f);
Gyroscrope = gyro;
float deltaTime = MathF.Abs((long)(timestamp - TimeStamp) / 1000000f);
Vector3 deltaGyro = gyro * deltaTime;
Rotation += deltaGyro;
_filter.SamplePeriod = deltaTime;
_filter.Update(accel, DegreeToRad(gyro));
}
TimeStamp = timestamp;
}
public Matrix4x4 GetOrientation()
{
return Matrix4x4.CreateFromQuaternion(_filter.Quaternion);
}
private static Vector3 DegreeToRad(Vector3 degree)
{
return degree * (MathF.PI / 180);
}
}
}

View file

@ -1,162 +0,0 @@
using System.Numerics;
namespace Ryujinx.Modules.Motion
{
// MahonyAHRS class. Madgwick's implementation of Mayhony's AHRS algorithm.
// 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
public class MotionSensorFilter
{
/// <summary>
/// Sample rate coefficient.
/// </summary>
public const float SampleRateCoefficient = 0.45f;
/// <summary>
/// Gets or sets the sample period.
/// </summary>
public float SamplePeriod { get; set; }
/// <summary>
/// Gets or sets the algorithm proportional gain.
/// </summary>
public float Kp { get; set; }
/// <summary>
/// Gets or sets the algorithm integral gain.
/// </summary>
public float Ki { get; set; }
/// <summary>
/// Gets the Quaternion output.
/// </summary>
public Quaternion Quaternion { get; private set; }
/// <summary>
/// Integral error.
/// </summary>
private Vector3 _intergralError;
/// <summary>
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
/// </summary>
/// <param name="samplePeriod">
/// Sample period.
/// </param>
public MotionSensorFilter(float samplePeriod) : this(samplePeriod, 1f, 0f) { }
/// <summary>
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
/// </summary>
/// <param name="samplePeriod">
/// Sample period.
/// </param>
/// <param name="kp">
/// Algorithm proportional gain.
/// </param>
public MotionSensorFilter(float samplePeriod, float kp) : this(samplePeriod, kp, 0f) { }
/// <summary>
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
/// </summary>
/// <param name="samplePeriod">
/// Sample period.
/// </param>
/// <param name="kp">
/// Algorithm proportional gain.
/// </param>
/// <param name="ki">
/// Algorithm integral gain.
/// </param>
public MotionSensorFilter(float samplePeriod, float kp, float ki)
{
SamplePeriod = samplePeriod;
Kp = kp;
Ki = ki;
Reset();
_intergralError = new Vector3();
}
/// <summary>
/// Algorithm IMU update method. Requires only gyroscope and accelerometer data.
/// </summary>
/// <param name="accel">
/// Accelerometer measurement in any calibrated units.
/// </param>
/// <param name="gyro">
/// Gyroscope measurement in radians.
/// </param>
public void Update(Vector3 accel, Vector3 gyro)
{
// Normalise accelerometer measurement.
float norm = 1f / accel.Length();
if (!float.IsFinite(norm))
{
return;
}
accel *= norm;
float q2 = Quaternion.X;
float q3 = Quaternion.Y;
float q4 = Quaternion.Z;
float q1 = Quaternion.W;
// Estimated direction of gravity.
Vector3 gravity = new Vector3()
{
X = 2f * (q2 * q4 - q1 * q3),
Y = 2f * (q1 * q2 + q3 * q4),
Z = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4
};
// Error is cross product between estimated direction and measured direction of gravity.
Vector3 error = new Vector3()
{
X = accel.Y * gravity.Z - accel.Z * gravity.Y,
Y = accel.Z * gravity.X - accel.X * gravity.Z,
Z = accel.X * gravity.Y - accel.Y * gravity.X
};
if (Ki > 0f)
{
_intergralError += error; // Accumulate integral error.
}
else
{
_intergralError = Vector3.Zero; // Prevent integral wind up.
}
// Apply feedback terms.
gyro += (Kp * error) + (Ki * _intergralError);
// Integrate rate of change of quaternion.
Vector3 delta = new Vector3(q2, q3, q4);
q1 += (-q2 * gyro.X - q3 * gyro.Y - q4 * gyro.Z) * (SampleRateCoefficient * SamplePeriod);
q2 += (q1 * gyro.X + delta.Y * gyro.Z - delta.Z * gyro.Y) * (SampleRateCoefficient * SamplePeriod);
q3 += (q1 * gyro.Y - delta.X * gyro.Z + delta.Z * gyro.X) * (SampleRateCoefficient * SamplePeriod);
q4 += (q1 * gyro.Z + delta.X * gyro.Y - delta.Y * gyro.X) * (SampleRateCoefficient * SamplePeriod);
// Normalise quaternion.
Quaternion quaternion = new Quaternion(q2, q3, q4, q1);
norm = 1f / quaternion.Length();
if (!float.IsFinite(norm))
{
return;
}
Quaternion = quaternion * norm;
}
public void Reset()
{
Quaternion = Quaternion.Identity;
}
}
}

View file

@ -1,51 +0,0 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ControllerDataRequest
{
public MessageType Type;
public SubscriberType SubscriberType;
public byte Slot;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] MacAddress;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ControllerDataResponse
{
public SharedResponse Shared;
public byte Connected;
public uint PacketId;
public byte ExtraButtons;
public byte MainButtons;
public ushort PSExtraInput;
public ushort LeftStickXY;
public ushort RightStickXY;
public uint DPadAnalog;
public ulong MainButtonsAnalog;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] Touch1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] Touch2;
public ulong MotionTimestamp;
public float AccelerometerX;
public float AccelerometerY;
public float AccelerometerZ;
public float GyroscopePitch;
public float GyroscopeYaw;
public float GyroscopeRoll;
}
enum SubscriberType : byte
{
All,
Slot,
Mac
}
}

View file

@ -1,21 +0,0 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ControllerInfoResponse
{
public SharedResponse Shared;
private byte _zero;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ControllerInfoRequest
{
public MessageType Type;
public int PortsCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[] PortIndices;
}
}

View file

@ -1,14 +0,0 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Header
{
public uint MagicString;
public ushort Version;
public ushort Length;
public uint Crc32;
public uint Id;
}
}

View file

@ -1,9 +0,0 @@
namespace Ryujinx.Modules.Motion
{
public enum MessageType : uint
{
Protocol = 0x100000,
Info,
Data
}
}

View file

@ -1,51 +0,0 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Modules.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SharedResponse
{
public MessageType Type;
public byte Slot;
public SlotState State;
public DeviceModelType ModelType;
public ConnectionType ConnectionType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] MacAddress;
public BatteryStatus BatteryStatus;
}
public enum SlotState : byte
{
Disconnected,
Reserved,
Connected
}
public enum DeviceModelType : byte
{
None,
PartialGyro,
FullGyro
}
public enum ConnectionType : byte
{
None,
USB,
Bluetooth
}
public enum BatteryStatus : byte
{
NA,
Dying,
Low,
Medium,
High,
Full,
Charging,
Charged
}
}

View file

@ -1,6 +1,5 @@
using ARMeilleure.Translation.PTC;
using Gtk;
using OpenTK;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.System;
@ -12,6 +11,7 @@ using Ryujinx.Ui.Widgets;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Ryujinx
@ -24,6 +24,9 @@ namespace Ryujinx
public static string ConfigurationPath { get; set; }
[DllImport("libX11")]
private extern static int XInitThreads();
static void Main(string[] args)
{
// Parse Arguments.
@ -63,15 +66,17 @@ namespace Ryujinx
// Delete backup files after updating.
Task.Run(Updater.CleanupUpdate);
Toolkit.Init(new ToolkitOptions
{
Backend = PlatformBackend.PreferNative
});
Version = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
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);
Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}");

View file

@ -12,18 +12,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
<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.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.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" />
</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.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />

View file

@ -1,39 +1,37 @@
using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC;
using Gdk;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using OpenTK.Input;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Configuration;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Modules.Motion;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
using Ryujinx.Ui.Widgets;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ui
{
using Switch = HLE.Switch;
public class GlRenderer : GLWidget
{
static GlRenderer()
{
OpenTK.Graphics.GraphicsContext.ShareContexts = true;
}
private const int SwitchPanelWidth = 1280;
private const int SwitchPanelHeight = 720;
private const int TargetFps = 60;
public ManualResetEvent WaitEvent { get; set; }
public NpadManager NpadManager { get; }
public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
@ -58,9 +56,7 @@ namespace Ryujinx.Ui
private Renderer _renderer;
private HotkeyButtons _prevHotkeyButtons;
private Client _dsuClient;
private KeyboardHotkeyState _prevHotkeyState;
private GraphicsDebugLevel _glLogLevel;
@ -71,14 +67,22 @@ namespace Ryujinx.Ui
private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
private long _lastCursorMoveTime;
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(),
3, 3,
glLogLevel == GraphicsDebugLevel.None
? GraphicsContextFlags.ForwardCompatible
: GraphicsContextFlags.ForwardCompatible | GraphicsContextFlags.Debug)
? OpenGLContextFlags.Compat
: 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);
_device = device;
@ -101,8 +105,6 @@ namespace Ryujinx.Ui
Shown += Renderer_Shown;
_dsuClient = new Client();
_glLogLevel = glLogLevel;
_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)
{
_device.DisposeGpu();
_dsuClient?.Dispose();
NpadManager.Dispose();
}
private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args)
@ -155,7 +157,7 @@ namespace Ryujinx.Ui
{
ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged;
_dsuClient?.Dispose();
NpadManager.Dispose();
Dispose();
}
@ -164,13 +166,13 @@ namespace Ryujinx.Ui
_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)
|| ((keyboard.IsKeyDown(OpenTK.Input.Key.AltLeft)
|| keyboard.IsKeyDown(OpenTK.Input.Key.AltRight))
&& keyboard.IsKeyDown(OpenTK.Input.Key.Enter))
|| keyboard.IsKeyDown(OpenTK.Input.Key.Escape);
bool toggleFullscreen = keyboard.IsPressed(Key.F11)
|| ((keyboard.IsPressed(Key.AltLeft)
|| keyboard.IsPressed(Key.AltRight))
&& keyboard.IsPressed(Key.Enter))
|| keyboard.IsPressed(Key.Escape);
bool fullScreenToggled = ParentWindow.State.HasFlag(Gdk.WindowState.Fullscreen);
@ -185,7 +187,7 @@ namespace Ryujinx.Ui
}
else
{
if (keyboard.IsKeyDown(OpenTK.Input.Key.Escape))
if (keyboard.IsPressed(Key.Escape))
{
if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
{
@ -203,7 +205,7 @@ namespace Ryujinx.Ui
_toggleFullscreen = toggleFullscreen;
bool toggleDockedMode = keyboard.IsKeyDown(OpenTK.Input.Key.F9);
bool toggleDockedMode = keyboard.IsPressed(Key.F9);
if (toggleDockedMode != _toggleDockedMode)
{
@ -225,8 +227,8 @@ namespace Ryujinx.Ui
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.
GraphicsContext.MakeCurrent(null);
// Release the GL exclusivity that SPB gave us as we aren't going to use it in GTK Thread.
OpenGLContext.MakeCurrent(null);
WaitEvent.Set();
}
@ -244,8 +246,6 @@ namespace Ryujinx.Ui
public void Start()
{
IsRenderHandler = true;
_chrono.Restart();
_isActive = true;
@ -389,7 +389,7 @@ namespace Ryujinx.Ui
public void Exit()
{
_dsuClient?.Dispose();
NpadManager?.Dispose();
if (_isStopped)
{
@ -416,15 +416,17 @@ namespace Ryujinx.Ui
public void Render()
{
// First take exclusivity on the OpenGL context.
_renderer.InitializeBackgroundContext(GraphicsContext);
_renderer.InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(OpenGLContext));
Gtk.Window parent = Toplevel as Gtk.Window;
parent.Present();
GraphicsContext.MakeCurrent(WindowInfo);
OpenGLContext.MakeCurrent(NativeWindow);
_device.Gpu.Renderer.Initialize(_glLogLevel);
// Make sure the first frame is not transparent.
GL.ClearColor(OpenTK.Color.Black);
GL.ClearColor(0, 0, 0, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit);
SwapBuffers();
@ -478,7 +480,7 @@ namespace Ryujinx.Ui
public void SwapBuffers()
{
OpenTK.Graphics.GraphicsContext.CurrentContext.SwapBuffers();
NativeWindow.SwapBuffers();
}
public void MainLoop()
@ -510,13 +512,13 @@ namespace Ryujinx.Ui
{
Gtk.Application.Invoke(delegate
{
KeyboardState keyboard = OpenTK.Input.Keyboard.GetState();
KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
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();
}
@ -524,154 +526,19 @@ namespace Ryujinx.Ui
});
}
List<GamepadInput> gamepadInputs = new List<GamepadInput>(NpadDevices.MaxControllers);
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);
NpadManager.Update(_device.Hid, _device.TamperMachine);
if(_isFocused)
{
// Hotkeys
HotkeyButtons currentHotkeyButtons = KeyboardController.GetHotkeyButtons(OpenTK.Input.Keyboard.GetState());
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
if (currentHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync) &&
!_prevHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync))
if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) &&
!_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync))
{
_device.EnableDeviceVsync = !_device.EnableDeviceVsync;
}
_prevHotkeyButtons = currentHotkeyButtons;
_prevHotkeyState = currentHotkeyState;
}
//Touchscreen
@ -739,5 +606,24 @@ namespace Ryujinx.Ui
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.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.GTK3;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2;
using Ryujinx.Modules;
using Ryujinx.Ui.App;
using Ryujinx.Ui.Applet;
@ -65,6 +68,7 @@ namespace Ryujinx.Ui
private bool _lastScannedAmiiboShowAll = false;
public GlRenderer GlRendererWidget;
public InputManager InputManager;
#pragma warning disable CS0169, CS0649, IDE0044
@ -223,6 +227,8 @@ namespace Ryujinx.Ui
};
Task.Run(RefreshFirmwareLabel);
InputManager = new InputManager(new GTK3KeyboardDriver(this), new SDL2GamepadDriver());
}
private void WindowStateEvent_Changed(object o, WindowStateEventArgs args)
@ -295,6 +301,11 @@ namespace Ryujinx.Ui
}
}
protected override void OnDestroyed()
{
InputManager.Dispose();
}
private void InitializeSwitchInstance()
{
_virtualFileSystem.Reload();
@ -636,7 +647,7 @@ namespace Ryujinx.Ui
DisplaySleep.Prevent();
GlRendererWidget = new GlRenderer(_emulationContext, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
GlRendererWidget = new GlRenderer(_emulationContext, InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
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="modal">True</property>
<property name="window_position">center</property>
<property name="default_width">1150</property>
<property name="default_height">690</property>
<property name="default_width">1200</property>
<property name="default_height">720</property>
<child type="titlebar">
<placeholder/>
</child>
@ -113,21 +113,6 @@
<property name="position">1</property>
</packing>
</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>
<packing>
<property name="expand">False</property>
@ -682,7 +667,7 @@
<property name="width_request">80</property>
<property name="visible">True</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>
</object>
<packing>
@ -691,20 +676,7 @@
</packing>
</child>
<child>
<object class="GtkLabel">
<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">
<object class="GtkToggleButton" id="_lStick">
<property name="label" translatable="yes"> </property>
<property name="width_request">65</property>
<property name="visible">True</property>
@ -716,22 +688,9 @@
<property name="top_attach">0</property>
</packing>
</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>
<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="can_focus">True</property>
<property name="receives_default">False</property>
@ -744,7 +703,7 @@
</child>
<child>
<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="can_focus">True</property>
<property name="receives_default">False</property>
@ -1501,7 +1460,7 @@
<property name="width_request">80</property>
<property name="visible">True</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>
</object>
<packing>
@ -1510,20 +1469,7 @@
</packing>
</child>
<child>
<object class="GtkLabel">
<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">
<object class="GtkToggleButton" id="_rStick">
<property name="label" translatable="yes"> </property>
<property name="width_request">65</property>
<property name="visible">True</property>
@ -1535,22 +1481,9 @@
<property name="top_attach">0</property>
</packing>
</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>
<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="can_focus">True</property>
<property name="receives_default">False</property>
@ -1563,7 +1496,7 @@
</child>
<child>
<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="can_focus">True</property>
<property name="receives_default">False</property>
@ -1640,7 +1573,7 @@
</packing>
</child>
<child>
<object class="GtkBox" id="MotionBox">
<object class="GtkBox" id="_motionBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
@ -1679,7 +1612,21 @@
</packing>
</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="can_focus">False</property>
<property name="spacing">10</property>
@ -1718,7 +1665,7 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
<child>
@ -1761,11 +1708,11 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkBox" id="_altBox">
<object class="GtkBox" id="_motionAltBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
@ -1829,11 +1776,11 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkBox" id="_dsuServerHostBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">30</property>
@ -1866,11 +1813,11 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">5</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkBox" id="_dsuServerPortBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">30</property>
@ -1903,7 +1850,7 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">6</property>
<property name="position">7</property>
</packing>
</child>
<child>
@ -1916,7 +1863,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
<property name="position">8</property>
</packing>
</child>
<child>
@ -1930,7 +1877,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">8</property>
<property name="position">9</property>
</packing>
</child>
</object>

View file

@ -578,7 +578,7 @@ namespace Ryujinx.Ui.Windows
{
((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.Show();