/ src / Ryujinx.SDL2.Common / SDL2Driver.cs
SDL2Driver.cs
  1  using Ryujinx.Common.Configuration;
  2  using Ryujinx.Common.Logging;
  3  using System;
  4  using System.Collections.Concurrent;
  5  using System.Collections.Generic;
  6  using System.IO;
  7  using System.Threading;
  8  using static SDL2.SDL;
  9  
 10  namespace Ryujinx.SDL2.Common
 11  {
 12      public class SDL2Driver : IDisposable
 13      {
 14          private static SDL2Driver _instance;
 15  
 16          public static SDL2Driver Instance
 17          {
 18              get
 19              {
 20                  _instance ??= new SDL2Driver();
 21  
 22                  return _instance;
 23              }
 24          }
 25  
 26          public static Action<Action> MainThreadDispatcher { get; set; }
 27  
 28          private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
 29  
 30          private bool _isRunning;
 31          private uint _refereceCount;
 32          private Thread _worker;
 33  
 34          public event Action<int, int> OnJoyStickConnected;
 35          public event Action<int> OnJoystickDisconnected;
 36  
 37          private ConcurrentDictionary<uint, Action<SDL_Event>> _registeredWindowHandlers;
 38  
 39          private readonly object _lock = new();
 40  
 41          private SDL2Driver() { }
 42  
 43          private const string SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS = "SDL_JOYSTICK_HIDAPI_COMBINE_JOY_CONS";
 44  
 45          public void Initialize()
 46          {
 47              lock (_lock)
 48              {
 49                  _refereceCount++;
 50  
 51                  if (_isRunning)
 52                  {
 53                      return;
 54                  }
 55  
 56                  SDL_SetHint(SDL_HINT_APP_NAME, "Ryujinx");
 57                  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
 58                  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
 59                  SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
 60                  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED, "0");
 61                  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
 62                  SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
 63  
 64  
 65                  // NOTE: As of SDL2 2.24.0, joycons are combined by default but the motion source only come from one of them.
 66                  // We disable this behavior for now.
 67                  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");
 68  
 69                  if (SDL_Init(SdlInitFlags) != 0)
 70                  {
 71                      string errorMessage = $"SDL2 initialization failed with error \"{SDL_GetError()}\"";
 72  
 73                      Logger.Error?.Print(LogClass.Application, errorMessage);
 74  
 75                      throw new Exception(errorMessage);
 76                  }
 77  
 78                  // First ensure that we only enable joystick events (for connected/disconnected).
 79                  if (SDL_GameControllerEventState(SDL_IGNORE) != SDL_IGNORE)
 80                  {
 81                      Logger.Error?.PrintMsg(LogClass.Application, "Couldn't change the state of game controller events.");
 82                  }
 83  
 84                  if (SDL_JoystickEventState(SDL_ENABLE) < 0)
 85                  {
 86                      Logger.Error?.PrintMsg(LogClass.Application, $"Failed to enable joystick event polling: {SDL_GetError()}");
 87                  }
 88  
 89                  // Disable all joysticks information, we don't need them no need to flood the event queue for that.
 90                  SDL_EventState(SDL_EventType.SDL_JOYAXISMOTION, SDL_DISABLE);
 91                  SDL_EventState(SDL_EventType.SDL_JOYBALLMOTION, SDL_DISABLE);
 92                  SDL_EventState(SDL_EventType.SDL_JOYHATMOTION, SDL_DISABLE);
 93                  SDL_EventState(SDL_EventType.SDL_JOYBUTTONDOWN, SDL_DISABLE);
 94                  SDL_EventState(SDL_EventType.SDL_JOYBUTTONUP, SDL_DISABLE);
 95  
 96                  SDL_EventState(SDL_EventType.SDL_CONTROLLERSENSORUPDATE, SDL_DISABLE);
 97  
 98                  string gamepadDbPath = Path.Combine(AppDataManager.BaseDirPath, "SDL_GameControllerDB.txt");
 99  
100                  if (File.Exists(gamepadDbPath))
101                  {
102                      SDL_GameControllerAddMappingsFromFile(gamepadDbPath);
103                  }
104  
105                  _registeredWindowHandlers = new ConcurrentDictionary<uint, Action<SDL_Event>>();
106                  _worker = new Thread(EventWorker);
107                  _isRunning = true;
108                  _worker.Start();
109              }
110          }
111  
112          public bool RegisterWindow(uint windowId, Action<SDL_Event> windowEventHandler)
113          {
114              return _registeredWindowHandlers.TryAdd(windowId, windowEventHandler);
115          }
116  
117          public void UnregisterWindow(uint windowId)
118          {
119              _registeredWindowHandlers.Remove(windowId, out _);
120          }
121  
122          private void HandleSDLEvent(ref SDL_Event evnt)
123          {
124              if (evnt.type == SDL_EventType.SDL_JOYDEVICEADDED)
125              {
126                  int deviceId = evnt.cbutton.which;
127  
128                  // 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.
129                  int instanceId = SDL_JoystickGetDeviceInstanceID(deviceId);
130  
131                  if (instanceId == -1)
132                  {
133                      return;
134                  }
135  
136                  Logger.Debug?.Print(LogClass.Application, $"Added joystick instance id {instanceId}");
137  
138                  OnJoyStickConnected?.Invoke(deviceId, instanceId);
139              }
140              else if (evnt.type == SDL_EventType.SDL_JOYDEVICEREMOVED)
141              {
142                  Logger.Debug?.Print(LogClass.Application, $"Removed joystick instance id {evnt.cbutton.which}");
143  
144                  OnJoystickDisconnected?.Invoke(evnt.cbutton.which);
145              }
146              else if (evnt.type == SDL_EventType.SDL_WINDOWEVENT || evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN || evnt.type == SDL_EventType.SDL_MOUSEBUTTONUP)
147              {
148                  if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action<SDL_Event> handler))
149                  {
150                      handler(evnt);
151                  }
152              }
153          }
154  
155          private void EventWorker()
156          {
157              const int WaitTimeMs = 10;
158  
159              using ManualResetEventSlim waitHandle = new(false);
160  
161              while (_isRunning)
162              {
163                  MainThreadDispatcher?.Invoke(() =>
164                  {
165                      while (SDL_PollEvent(out SDL_Event evnt) != 0)
166                      {
167                          HandleSDLEvent(ref evnt);
168                      }
169                  });
170  
171                  waitHandle.Wait(WaitTimeMs);
172              }
173          }
174  
175          protected virtual void Dispose(bool disposing)
176          {
177              if (!disposing)
178              {
179                  return;
180              }
181  
182              lock (_lock)
183              {
184                  if (_isRunning)
185                  {
186                      _refereceCount--;
187  
188                      if (_refereceCount == 0)
189                      {
190                          _isRunning = false;
191  
192                          _worker?.Join();
193  
194                          SDL_Quit();
195  
196                          OnJoyStickConnected = null;
197                          OnJoystickDisconnected = null;
198                      }
199                  }
200              }
201          }
202  
203          public void Dispose()
204          {
205              GC.SuppressFinalize(this);
206              Dispose(true);
207          }
208      }
209  }