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 }