// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "SDL3/SDL_events.h" #include "SDL3/SDL_hints.h" #include "SDL3/SDL_init.h" #include "SDL3/SDL_properties.h" #include "SDL3/SDL_timer.h" #include "SDL3/SDL_video.h" #include "common/assert.h" #include "common/elf_info.h" #include "core/debug_state.h" #include "core/devtools/layer.h" #include "core/emulator_settings.h" #include "core/libraries/kernel/time.h" #include "core/libraries/pad/pad.h" #include "core/libraries/system/userservice.h" #include "core/user_settings.h" #include "imgui/renderer/imgui_core.h" #include "input/controller.h" #include "input/input_handler.h" #include "input/input_mouse.h" #include "sdl_window.h" #include "video_core/renderdoc.h" #ifdef __APPLE__ #include "SDL3/SDL_metal.h" #endif #include namespace Frontend { using namespace Libraries::Pad; static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { using OPBDO = OrbisPadButtonDataOffset; switch (button) { case SDL_GAMEPAD_BUTTON_DPAD_DOWN: return OPBDO::Down; case SDL_GAMEPAD_BUTTON_DPAD_UP: return OPBDO::Up; case SDL_GAMEPAD_BUTTON_DPAD_LEFT: return OPBDO::Left; case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: return OPBDO::Right; case SDL_GAMEPAD_BUTTON_SOUTH: return OPBDO::Cross; case SDL_GAMEPAD_BUTTON_NORTH: return OPBDO::Triangle; case SDL_GAMEPAD_BUTTON_WEST: return OPBDO::Square; case SDL_GAMEPAD_BUTTON_EAST: return OPBDO::Circle; case SDL_GAMEPAD_BUTTON_START: return OPBDO::Options; case SDL_GAMEPAD_BUTTON_TOUCHPAD: return OPBDO::TouchPad; case SDL_GAMEPAD_BUTTON_BACK: return OPBDO::TouchPad; case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return OPBDO::L1; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return OPBDO::R1; case SDL_GAMEPAD_BUTTON_LEFT_STICK: return OPBDO::L3; case SDL_GAMEPAD_BUTTON_RIGHT_STICK: return OPBDO::R3; default: return OPBDO::None; } } static Uint32 SDLCALL PollController(void* userdata, SDL_TimerID timer_id, Uint32 interval) { auto* controller = reinterpret_cast(userdata); controller->UpdateAxisSmoothing(); controller->Gyro(0); controller->Acceleration(0); return interval; } static Uint32 SDLCALL PollControllerLightColour(void* userdata, SDL_TimerID timer_id, Uint32 interval) { auto* controller = reinterpret_cast(userdata); controller->PollLightColour(); return interval; } WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controllers_, std::string_view window_title) : width{width_}, height{height_}, controllers{*controllers_} { if (!SDL_SetHint(SDL_HINT_APP_NAME, "shadPS4")) { UNREACHABLE_MSG("Failed to set SDL window hint: {}", SDL_GetError()); } if (!SDL_Init(SDL_INIT_VIDEO)) { UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError()); } if (!SDL_Init(SDL_INIT_CAMERA)) { LOG_ERROR(Input, "Failed to initialize SDL camera subsystem: {}", SDL_GetError()); } SDL_InitSubSystem(SDL_INIT_AUDIO); SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, std::string(window_title).c_str()); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height); SDL_SetNumberProperty(props, "flags", SDL_WINDOW_VULKAN); SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true); window = SDL_CreateWindowWithProperties(props); SDL_DestroyProperties(props); if (window == nullptr) { UNREACHABLE_MSG("Failed to create window handle: {}", SDL_GetError()); } SDL_SetWindowMinimumSize(window, 640, 360); bool error = false; const SDL_DisplayID displayIndex = SDL_GetDisplayForWindow(window); if (displayIndex < 0) { LOG_ERROR(Frontend, "Error getting display index: {}", SDL_GetError()); error = true; } const SDL_DisplayMode* displayMode; if ((displayMode = SDL_GetCurrentDisplayMode(displayIndex)) == 0) { LOG_ERROR(Frontend, "Error getting display mode: {}", SDL_GetError()); error = true; } if (!error) { SDL_SetWindowFullscreenMode( window, EmulatorSettings.GetFullScreenMode() == "Fullscreen" ? displayMode : NULL); } SDL_SetWindowFullscreen(window, EmulatorSettings.IsFullScreen()); SDL_SyncWindow(window); SDL_InitSubSystem(SDL_INIT_GAMEPAD); #if defined(SDL_PLATFORM_WIN32) window_info.type = WindowSystemType::Windows; window_info.render_surface = SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); #elif defined(SDL_PLATFORM_LINUX) || defined(__FreeBSD__) // SDL doesn't have a platform define for FreeBSD AAAAAAAAAA if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) { window_info.type = WindowSystemType::X11; window_info.display_connection = SDL_GetPointerProperty( SDL_GetWindowProperties(window), SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL); window_info.render_surface = (void*)SDL_GetNumberProperty( SDL_GetWindowProperties(window), SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); } else if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) { window_info.type = WindowSystemType::Wayland; window_info.display_connection = SDL_GetPointerProperty( SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, NULL); window_info.render_surface = SDL_GetPointerProperty( SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, NULL); } #elif defined(SDL_PLATFORM_MACOS) window_info.type = WindowSystemType::Metal; window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window)); #endif // input handler init-s Input::ControllerOutput::LinkJoystickAxes(); Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); controllers.TryOpenSDLControllers(); if (EmulatorSettings.IsBackgroundControllerInput()) { SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); } } WindowSDL::~WindowSDL() = default; void WindowSDL::WaitEvent() { // Called on main thread SDL_Event event; if (!SDL_WaitEvent(&event)) { return; } if (ImGui::Core::ProcessEvent(&event)) { return; } switch (event.type) { case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_MAXIMIZED: case SDL_EVENT_WINDOW_RESTORED: OnResize(); break; case SDL_EVENT_WINDOW_MINIMIZED: case SDL_EVENT_WINDOW_EXPOSED: is_shown = event.type == SDL_EVENT_WINDOW_EXPOSED; OnResize(); break; case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_WHEEL: case SDL_EVENT_MOUSE_WHEEL_OFF: case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: OnKeyboardMouseInput(&event); break; case SDL_EVENT_GAMEPAD_ADDED: case SDL_EVENT_GAMEPAD_REMOVED: controllers.TryOpenSDLControllers(); break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_GAMEPAD_AXIS_MOTION: case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: OnGamepadEvent(&event); break; case SDL_EVENT_QUIT: is_open = false; break; case SDL_EVENT_QUIT_DIALOG: Overlay::ToggleQuitWindow(); break; case SDL_EVENT_TOGGLE_FULLSCREEN: { if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) { SDL_SetWindowFullscreen(window, 0); } else { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN); } break; } case SDL_EVENT_TOGGLE_PAUSE: if (DebugState.IsGuestThreadsPaused()) { LOG_INFO(Frontend, "Game Resumed"); DebugState.ResumeGuestThreads(); } else { LOG_INFO(Frontend, "Game Paused"); DebugState.PauseGuestThreads(); } break; case SDL_EVENT_CHANGE_CONTROLLER: UNREACHABLE_MSG("todo"); break; case SDL_EVENT_TOGGLE_SIMPLE_FPS: Overlay::ToggleSimpleFps(); break; case SDL_EVENT_RELOAD_INPUTS: Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); break; case SDL_EVENT_MOUSE_TO_JOYSTICK: SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), Input::ToggleMouseModeTo(Input::MouseMode::Joystick)); break; case SDL_EVENT_MOUSE_TO_GYRO: SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), Input::ToggleMouseModeTo(Input::MouseMode::Gyro)); break; case SDL_EVENT_MOUSE_TO_TOUCHPAD: SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), Input::ToggleMouseModeTo(Input::MouseMode::Touchpad)); SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), false); break; case SDL_EVENT_ADD_VIRTUAL_USER: for (int i = 0; i < 4; i++) { if (controllers[i]->user_id == -1) { auto u = UserManagement.GetUserByPlayerIndex(i + 1); if (!u) { break; } controllers[i]->user_id = u->user_id; UserManagement.LoginUser(u, i + 1); break; } } break; case SDL_EVENT_REMOVE_VIRTUAL_USER: LOG_INFO(Input, "Remove user"); for (int i = 3; i >= 0; i--) { if (controllers[i]->user_id != -1) { UserManagement.LogoutUser(UserManagement.GetUserByID(controllers[i]->user_id)); controllers[i]->user_id = -1; break; } } break; case SDL_EVENT_RDOC_CAPTURE: VideoCore::TriggerCapture(); break; default: break; } } void WindowSDL::InitTimers() { for (int i = 0; i < 4; ++i) { SDL_AddTimer(4, &PollController, controllers[i]); } SDL_AddTimer(33, Input::MousePolling, (void*)controllers[0]); } void WindowSDL::RequestKeyboard() { if (keyboard_grab == 0) { SDL_RunOnMainThread( [](void* userdata) { SDL_StartTextInput(static_cast(userdata)); }, window, true); } keyboard_grab++; } void WindowSDL::ReleaseKeyboard() { ASSERT(keyboard_grab > 0); keyboard_grab--; if (keyboard_grab == 0) { SDL_RunOnMainThread( [](void* userdata) { SDL_StopTextInput(static_cast(userdata)); }, window, true); } } void WindowSDL::OnResize() { SDL_GetWindowSizeInPixels(window, &width, &height); ImGui::Core::OnResize(); } Uint32 wheelOffCallback(void* og_event, Uint32 timer_id, Uint32 interval) { SDL_Event off_event = *(SDL_Event*)og_event; off_event.type = SDL_EVENT_MOUSE_WHEEL_OFF; SDL_PushEvent(&off_event); delete (SDL_Event*)og_event; return 0; } void WindowSDL::OnKeyboardMouseInput(const SDL_Event* event) { using Libraries::Pad::OrbisPadButtonDataOffset; // get the event's id, if it's keyup or keydown const bool input_down = event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_MOUSE_BUTTON_DOWN || event->type == SDL_EVENT_MOUSE_WHEEL; Input::InputEvent input_event = Input::InputBinding::GetInputEventFromSDLEvent(*event); // if it's a wheel event, make a timer that turns it off after a set time if (event->type == SDL_EVENT_MOUSE_WHEEL) { const SDL_Event* copy = new SDL_Event(*event); SDL_AddTimer(33, wheelOffCallback, (void*)copy); } // add/remove it from the list bool inputs_changed = Input::UpdatePressedKeys(input_event); // update bindings if (inputs_changed) { Input::ActivateOutputsFromInputs(); } } void WindowSDL::OnGamepadEvent(const SDL_Event* event) { bool input_down = event->type == SDL_EVENT_GAMEPAD_AXIS_MOTION || event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN; Input::InputEvent input_event = Input::InputBinding::GetInputEventFromSDLEvent(*event); // the touchpad button shouldn't be rebound to anything else, // as it would break the entire touchpad handling // You can still bind other things to it though if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) { controllers[controllers.GetGamepadIndexFromJoystickId(event->gbutton.which)]->Button( OrbisPadButtonDataOffset::TouchPad, input_down); return; } u8 gamepad; switch (event->type) { case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: switch ((SDL_SensorType)event->gsensor.sensor) { case SDL_SENSOR_GYRO: gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); if (gamepad < 5) { controllers[gamepad]->UpdateGyro(event->gsensor.data); } break; case SDL_SENSOR_ACCEL: gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); if (gamepad < 5) { controllers[gamepad]->UpdateAcceleration(event->gsensor.data); } break; default: break; } return; case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: controllers[controllers.GetGamepadIndexFromJoystickId(event->gtouchpad.which)] ->SetTouchpadState(event->gtouchpad.finger, event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event->gtouchpad.x, event->gtouchpad.y); return; default: break; } // add/remove it from the list bool inputs_changed = Input::UpdatePressedKeys(input_event); if (inputs_changed) { // update bindings Input::ActivateOutputsFromInputs(); } } } // namespace Frontend