#include "StarMainApplication.hpp" #include "StarLogging.hpp" #include "StarSignalHandler.hpp" #include "StarTickRateMonitor.hpp" #include "StarRenderer_opengl20.hpp" #include "StarTtlCache.hpp" #include "StarImage.hpp" #include "StarImageProcessing.hpp" #include "SDL2/SDL.h" #include "StarPlatformServices_pc.hpp" #ifdef STAR_SYSTEM_WINDOWS #include "SDL2/SDL_syswm.h" #include #endif namespace Star { Maybe keyFromSdlKeyCode(SDL_Keycode sym) { static HashMap KeyCodeMap{ {SDLK_BACKSPACE, Key::Backspace}, {SDLK_TAB, Key::Tab}, {SDLK_CLEAR, Key::Clear}, {SDLK_RETURN, Key::Return}, {SDLK_PAUSE, Key::Pause}, {SDLK_ESCAPE, Key::Escape}, {SDLK_SPACE, Key::Space}, {SDLK_EXCLAIM, Key::Exclaim}, {SDLK_QUOTEDBL, Key::QuotedBL}, {SDLK_HASH, Key::Hash}, {SDLK_DOLLAR, Key::Dollar}, {SDLK_AMPERSAND, Key::Ampersand}, {SDLK_QUOTE, Key::Quote}, {SDLK_LEFTPAREN, Key::LeftParen}, {SDLK_RIGHTPAREN, Key::RightParen}, {SDLK_ASTERISK, Key::Asterisk}, {SDLK_PLUS, Key::Plus}, {SDLK_COMMA, Key::Comma}, {SDLK_MINUS, Key::Minus}, {SDLK_PERIOD, Key::Period}, {SDLK_SLASH, Key::Slash}, {SDLK_0, Key::Zero}, {SDLK_1, Key::One}, {SDLK_2, Key::Two}, {SDLK_3, Key::Three}, {SDLK_4, Key::Four}, {SDLK_5, Key::Five}, {SDLK_6, Key::Six}, {SDLK_7, Key::Seven}, {SDLK_8, Key::Eight}, {SDLK_9, Key::Nine}, {SDLK_COLON, Key::Colon}, {SDLK_SEMICOLON, Key::Semicolon}, {SDLK_LESS, Key::Less}, {SDLK_EQUALS, Key::Equals}, {SDLK_GREATER, Key::Greater}, {SDLK_QUESTION, Key::Question}, {SDLK_AT, Key::At}, {SDLK_LEFTBRACKET, Key::LeftBracket}, {SDLK_BACKSLASH, Key::Backslash}, {SDLK_RIGHTBRACKET, Key::RightBracket}, {SDLK_CARET, Key::Caret}, {SDLK_UNDERSCORE, Key::Underscore}, {SDLK_BACKQUOTE, Key::Backquote}, {SDLK_a, Key::A}, {SDLK_b, Key::B}, {SDLK_c, Key::C}, {SDLK_d, Key::D}, {SDLK_e, Key::E}, {SDLK_f, Key::F}, {SDLK_g, Key::G}, {SDLK_h, Key::H}, {SDLK_i, Key::I}, {SDLK_j, Key::J}, {SDLK_k, Key::K}, {SDLK_l, Key::L}, {SDLK_m, Key::M}, {SDLK_n, Key::N}, {SDLK_o, Key::O}, {SDLK_p, Key::P}, {SDLK_q, Key::Q}, {SDLK_r, Key::R}, {SDLK_s, Key::S}, {SDLK_t, Key::T}, {SDLK_u, Key::U}, {SDLK_v, Key::V}, {SDLK_w, Key::W}, {SDLK_x, Key::X}, {SDLK_y, Key::Y}, {SDLK_z, Key::Z}, {SDLK_DELETE, Key::Delete}, {SDLK_KP_0, Key::Kp0}, {SDLK_KP_1, Key::Kp1}, {SDLK_KP_2, Key::Kp2}, {SDLK_KP_3, Key::Kp3}, {SDLK_KP_4, Key::Kp4}, {SDLK_KP_5, Key::Kp5}, {SDLK_KP_6, Key::Kp6}, {SDLK_KP_7, Key::Kp7}, {SDLK_KP_8, Key::Kp8}, {SDLK_KP_9, Key::Kp9}, {SDLK_KP_PERIOD, Key::Kp_period}, {SDLK_KP_DIVIDE, Key::Kp_divide}, {SDLK_KP_MULTIPLY, Key::Kp_multiply}, {SDLK_KP_MINUS, Key::Kp_minus}, {SDLK_KP_PLUS, Key::Kp_plus}, {SDLK_KP_ENTER, Key::Kp_enter}, {SDLK_KP_EQUALS, Key::Kp_equals}, {SDLK_UP, Key::Up}, {SDLK_DOWN, Key::Down}, {SDLK_RIGHT, Key::Right}, {SDLK_LEFT, Key::Left}, {SDLK_INSERT, Key::Insert}, {SDLK_HOME, Key::Home}, {SDLK_END, Key::End}, {SDLK_PAGEUP, Key::PageUp}, {SDLK_PAGEDOWN, Key::PageDown}, {SDLK_F1, Key::F1}, {SDLK_F2, Key::F2}, {SDLK_F3, Key::F3}, {SDLK_F4, Key::F4}, {SDLK_F5, Key::F5}, {SDLK_F6, Key::F6}, {SDLK_F7, Key::F7}, {SDLK_F8, Key::F8}, {SDLK_F9, Key::F9}, {SDLK_F10, Key::F10}, {SDLK_F11, Key::F11}, {SDLK_F12, Key::F12}, {SDLK_F13, Key::F13}, {SDLK_F14, Key::F14}, {SDLK_F15, Key::F15}, {SDLK_NUMLOCKCLEAR, Key::NumLock}, {SDLK_CAPSLOCK, Key::CapsLock}, {SDLK_SCROLLLOCK, Key::ScrollLock}, {SDLK_RSHIFT, Key::RShift}, {SDLK_LSHIFT, Key::LShift}, {SDLK_RCTRL, Key::RCtrl}, {SDLK_LCTRL, Key::LCtrl}, {SDLK_RALT, Key::RAlt}, {SDLK_LALT, Key::LAlt}, {SDLK_RGUI, Key::RGui}, {SDLK_LGUI, Key::LGui}, {SDLK_MODE, Key::AltGr}, {SDLK_APPLICATION, Key::Compose}, {SDLK_HELP, Key::Help}, {SDLK_PRINTSCREEN, Key::PrintScreen}, {SDLK_SYSREQ, Key::SysReq}, {SDLK_PAUSE, Key::Pause}, {SDLK_MENU, Key::Menu}, {SDLK_POWER, Key::Power} }; return KeyCodeMap.maybe(sym); } KeyMod keyModsFromSdlKeyMods(uint16_t mod) { return static_cast(mod); } MouseButton mouseButtonFromSdlMouseButton(uint8_t button) { switch (button) { case SDL_BUTTON_LEFT: return MouseButton::Left; case SDL_BUTTON_MIDDLE: return MouseButton::Middle; case SDL_BUTTON_RIGHT: return MouseButton::Right; case SDL_BUTTON_X1: return MouseButton::FourthButton; default: return MouseButton::FifthButton; } } ControllerAxis controllerAxisFromSdlControllerAxis(uint8_t axis) { switch (axis) { case SDL_CONTROLLER_AXIS_LEFTX: return ControllerAxis::LeftX; case SDL_CONTROLLER_AXIS_LEFTY: return ControllerAxis::LeftY; case SDL_CONTROLLER_AXIS_RIGHTX: return ControllerAxis::RightX; case SDL_CONTROLLER_AXIS_RIGHTY: return ControllerAxis::RightY; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return ControllerAxis::TriggerLeft; case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return ControllerAxis::TriggerRight; default: return ControllerAxis::Invalid; } } ControllerButton controllerButtonFromSdlControllerButton(uint8_t button) { switch (button) { case SDL_CONTROLLER_BUTTON_A: return ControllerButton::A; case SDL_CONTROLLER_BUTTON_B: return ControllerButton::B; case SDL_CONTROLLER_BUTTON_X: return ControllerButton::X; case SDL_CONTROLLER_BUTTON_Y: return ControllerButton::Y; case SDL_CONTROLLER_BUTTON_BACK: return ControllerButton::Back; case SDL_CONTROLLER_BUTTON_GUIDE: return ControllerButton::Guide; case SDL_CONTROLLER_BUTTON_START: return ControllerButton::Start; case SDL_CONTROLLER_BUTTON_LEFTSTICK: return ControllerButton::LeftStick; case SDL_CONTROLLER_BUTTON_RIGHTSTICK: return ControllerButton::RightStick; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: return ControllerButton::LeftShoulder; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return ControllerButton::RightShoulder; case SDL_CONTROLLER_BUTTON_DPAD_UP: return ControllerButton::DPadUp; case SDL_CONTROLLER_BUTTON_DPAD_DOWN: return ControllerButton::DPadDown; case SDL_CONTROLLER_BUTTON_DPAD_LEFT: return ControllerButton::DPadLeft; case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: return ControllerButton::DPadRight; case SDL_CONTROLLER_BUTTON_MISC1: return ControllerButton::Misc1; case SDL_CONTROLLER_BUTTON_PADDLE1: return ControllerButton::Paddle1; case SDL_CONTROLLER_BUTTON_PADDLE2: return ControllerButton::Paddle2; case SDL_CONTROLLER_BUTTON_PADDLE3: return ControllerButton::Paddle3; case SDL_CONTROLLER_BUTTON_PADDLE4: return ControllerButton::Paddle4; case SDL_CONTROLLER_BUTTON_TOUCHPAD: return ControllerButton::Touchpad; default: return ControllerButton::Invalid; } } class SdlPlatform { public: SdlPlatform(ApplicationUPtr application, StringList cmdLineArgs) { m_application = std::move(application); // extract application path from command line args String applicationPath = cmdLineArgs.first(); cmdLineArgs = cmdLineArgs.slice(1); StringList platformArguments; eraseWhere(cmdLineArgs, [&platformArguments](String& argument) { if (argument.beginsWith("+platform")) { platformArguments.append(std::move(argument)); return true; } return false; }); Logger::info("Application: Initializing SDL"); if (SDL_Init(0)) throw ApplicationException(strf("Couldn't initialize SDL: {}", SDL_GetError())); if (char* basePath = SDL_GetBasePath()) { File::changeDirectory(basePath); SDL_free(basePath); } m_signalHandler.setHandleInterrupt(true); m_signalHandler.setHandleFatal(true); try { Logger::info("Application: startup..."); m_application->startup(cmdLineArgs); } catch (std::exception const& e) { throw ApplicationException("Application threw exception during startup", e); } Logger::info("Application: Initializing SDL Video"); if (SDL_InitSubSystem(SDL_INIT_VIDEO)) throw ApplicationException(strf("Couldn't initialize SDL Video: {}", SDL_GetError())); Logger::info("Application: Initializing SDL Controller"); if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER)) throw ApplicationException(strf("Couldn't initialize SDL Controller: {}", SDL_GetError())); #ifdef STAR_SYSTEM_WINDOWS // Newer SDL is defaulting to xaudio2, which does not support audio capture SDL_setenv("SDL_AUDIODRIVER", "directsound", 1); #endif Logger::info("Application: Initializing SDL Audio"); if (SDL_InitSubSystem(SDL_INIT_AUDIO)) throw ApplicationException(strf("Couldn't initialize SDL Audio: {}", SDL_GetError())); Logger::info("Application: using Audio Driver '{}'", SDL_GetCurrentAudioDriver()); SDL_JoystickEventState(SDL_ENABLE); m_platformServices = PcPlatformServices::create(applicationPath, platformArguments); if (!m_platformServices) Logger::info("Application: No platform services available"); Logger::info("Application: Creating SDL Window"); m_sdlWindow = SDL_CreateWindow(m_windowTitle.utf8Ptr(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, m_windowSize[0], m_windowSize[1], SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); if (!m_sdlWindow) throw ApplicationException::format("Application: Could not create SDL Window: {}", SDL_GetError()); SDL_ShowWindow(m_sdlWindow); SDL_RaiseWindow(m_sdlWindow); // Makes the window border black. From https://github.com/libsdl-org/SDL/commit/89948787#diff-f2ae5c36a8afc0a9a343a6664ab306da2963213e180af8cd97b12397dcbb9ae7R1478 #ifdef STAR_SYSTEM_WINDOWS if (void* handle = SDL_LoadObject("dwmapi.dll")) { if (auto DwmSetWindowAttributeFunc = (decltype(&DwmSetWindowAttribute))SDL_LoadFunction(handle, "DwmSetWindowAttribute")) { SDL_SysWMinfo wmInfo{}; SDL_VERSION(&wmInfo.version); SDL_GetWindowWMInfo(m_sdlWindow, &wmInfo); DWORD type{}, value{}, count = sizeof(value); LSTATUS status = RegGetValue(HKEY_CURRENT_USER, TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"), TEXT("AppsUseLightTheme"), RRF_RT_REG_DWORD, &type, &value, &count); BOOL enabled = status == ERROR_SUCCESS && type == REG_DWORD && value == 0; DwmSetWindowAttributeFunc(wmInfo.info.win.window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enabled, sizeof(enabled)); } SDL_UnloadObject(handle); } #endif int width; int height; SDL_GetWindowSize(m_sdlWindow, &width, &height); m_windowSize = Vec2U(width, height); m_sdlGlContext = SDL_GL_CreateContext(m_sdlWindow); if (!m_sdlGlContext) throw ApplicationException::format("Application: Could not create OpenGL context: {}", SDL_GetError()); SDL_GL_SwapWindow(m_sdlWindow); setVSyncEnabled(m_windowVSync); SDL_StopTextInput(); SDL_AudioSpec desired = {}; desired.freq = 44100; desired.format = AUDIO_S16SYS; desired.samples = 1024; desired.channels = 2; desired.userdata = this; desired.callback = [](void* userdata, Uint8* stream, int len) { ((SdlPlatform*)(userdata))->getAudioData(stream, len); }; SDL_AudioSpec obtained = {}; m_sdlAudioOutputDevice = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0); if (!m_sdlAudioOutputDevice) { Logger::error("Application: Could not open audio device, no sound available!"); } else if (obtained.freq != desired.freq || obtained.channels != desired.channels || obtained.format != desired.format) { SDL_CloseAudioDevice(m_sdlAudioOutputDevice); Logger::error("Application: Could not open 44.1khz / 16 bit stereo audio device, no sound available!"); } else { Logger::info("Application: Opened default audio device with 44.1khz / 16 bit stereo audio, {} sample size buffer", obtained.samples); SDL_PauseAudioDevice(m_sdlAudioOutputDevice, 0); } m_renderer = make_shared(); m_renderer->setScreenSize(m_windowSize); m_cursorCache.setTimeToLive(30000); } ~SdlPlatform() { if (m_sdlAudioOutputDevice) SDL_CloseAudioDevice(m_sdlAudioOutputDevice); closeAudioInputDevice(); m_renderer.reset(); Logger::info("Application: Destroying SDL Window"); SDL_DestroyWindow(m_sdlWindow); SDL_Quit(); } bool openAudioInputDevice(const char* name, int freq, int channels, void* userdata, SDL_AudioCallback callback) { SDL_AudioSpec desired = {}; desired.freq = freq; desired.format = AUDIO_S16SYS; desired.samples = 1024; desired.channels = channels; desired.userdata = userdata; desired.callback = callback; closeAudioInputDevice(); SDL_AudioSpec obtained = {}; m_sdlAudioInputDevice = SDL_OpenAudioDevice(name, 1, &desired, &obtained, 0); if (m_sdlAudioInputDevice) { if (name) Logger::info("Opened audio input device '{}'", name); else Logger::info("Opened default audio input device"); SDL_PauseAudioDevice(m_sdlAudioInputDevice, 0); } else Logger::info("Failed to open audio input device: {}", SDL_GetError()); return m_sdlAudioInputDevice != 0; } bool closeAudioInputDevice() { if (m_sdlAudioInputDevice) { Logger::info("Closing audio input device"); SDL_CloseAudioDevice(m_sdlAudioInputDevice); m_sdlAudioInputDevice = 0; return true; } return false; } void cleanup() { m_cursorCache.ptr(m_currentCursor); m_cursorCache.cleanup(); } void run() { try { Logger::info("Application: initialization..."); m_application->applicationInit(make_shared(this)); Logger::info("Application: renderer initialization..."); m_application->renderInit(m_renderer); Logger::info("Application: main update loop..."); m_updateTicker.reset(); m_renderTicker.reset(); bool quit = false; while (true) { cleanup(); for (auto const& event : processEvents()) m_application->processInput(event); if (m_platformServices) m_platformServices->update(); if (m_platformServices->overlayActive()) SDL_ShowCursor(1); else SDL_ShowCursor(m_cursorVisible ? 1 : 0); int updatesBehind = max(round(m_updateTicker.ticksBehind()), 1); updatesBehind = min(updatesBehind, m_maxFrameSkip + 1); for (int i = 0; i < updatesBehind; ++i) { m_application->update(); m_updateRate = m_updateTicker.tick(); } m_renderer->startFrame(); m_application->render(); m_renderer->finishFrame(); SDL_GL_SwapWindow(m_sdlWindow); m_renderRate = m_renderTicker.tick(); if (m_quitRequested) { Logger::info("Application: quit requested"); quit = true; } if (m_signalHandler.interruptCaught()) { Logger::info("Application: Interrupt caught"); quit = true; } if (quit) { Logger::info("Application: quitting..."); break; } int64_t spareMilliseconds = round(m_updateTicker.spareTime() * 1000); if (spareMilliseconds > 0) Thread::sleepPrecise(spareMilliseconds); } } catch (std::exception const& e) { Logger::error("Application: exception thrown, shutting down: {}", outputException(e, true)); } try { Logger::info("Application: shutdown..."); m_application->shutdown(); } catch (std::exception const& e) { Logger::error("Application: threw exception during shutdown: {}", outputException(e, true)); } SDL_CloseAudioDevice(m_sdlAudioOutputDevice); m_SdlControllers.clear(); SDL_SetCursor(NULL); m_cursorCache.clear(); m_application.reset(); } private: struct Controller : public ApplicationController { Controller(SdlPlatform* parent) : parent(parent) {} bool hasClipboard() override { return SDL_HasClipboardText(); } Maybe getClipboard() override { Maybe string; if (SDL_HasClipboardText()) { if (auto text = SDL_GetClipboardText()) { if (*text != '\0') string.emplace(text); SDL_free(text); } } return string; } void setClipboard(String text) override { SDL_SetClipboardText(text.utf8Ptr()); } void setTargetUpdateRate(float targetUpdateRate) override { parent->m_updateTicker.setTargetTickRate(targetUpdateRate); } void setUpdateTrackWindow(float updateTrackWindow) override { parent->m_updateTicker.setWindow(updateTrackWindow); } void setApplicationTitle(String title) override { parent->m_windowTitle = std::move(title); if (parent->m_sdlWindow) SDL_SetWindowTitle(parent->m_sdlWindow, parent->m_windowTitle.utf8Ptr()); } void setFullscreenWindow(Vec2U fullScreenResolution) override { if (parent->m_windowMode != WindowMode::Fullscreen || parent->m_windowSize != fullScreenResolution) { SDL_DisplayMode requestedDisplayMode = {SDL_PIXELFORMAT_RGB888, (int)fullScreenResolution[0], (int)fullScreenResolution[1], 0, 0}; int currentDisplayIndex = SDL_GetWindowDisplayIndex(parent->m_sdlWindow); SDL_DisplayMode targetDisplayMode; if (SDL_GetClosestDisplayMode(currentDisplayIndex, &requestedDisplayMode, &targetDisplayMode) != NULL) { if (SDL_SetWindowDisplayMode(parent->m_sdlWindow, &requestedDisplayMode) == 0) { if (parent->m_windowMode == WindowMode::Fullscreen) SDL_SetWindowFullscreen(parent->m_sdlWindow, 0); else if (parent->m_windowMode == WindowMode::Borderless) SDL_SetWindowBordered(parent->m_sdlWindow, SDL_TRUE); else if (parent->m_windowMode == WindowMode::Maximized) SDL_RestoreWindow(parent->m_sdlWindow); parent->m_windowMode = WindowMode::Fullscreen; SDL_SetWindowFullscreen(parent->m_sdlWindow, SDL_WINDOW_FULLSCREEN); } else { Logger::warn("Failed to set resolution {}, {}", (unsigned)requestedDisplayMode.w, (unsigned)requestedDisplayMode.h); } } else { Logger::warn("Unable to set requested display resolution {}, {}", (int)fullScreenResolution[0], (int)fullScreenResolution[1]); } SDL_DisplayMode actualDisplayMode; if (SDL_GetWindowDisplayMode(parent->m_sdlWindow, &actualDisplayMode) == 0) { parent->m_windowSize = {(unsigned)actualDisplayMode.w, (unsigned)actualDisplayMode.h}; // call these manually since no SDL_WindowEvent is triggered when changing between fullscreen resolutions for some reason parent->m_renderer->setScreenSize(parent->m_windowSize); parent->m_application->windowChanged(parent->m_windowMode, parent->m_windowSize); } else { Logger::error("Couldn't get window display mode!"); } } } void setNormalWindow(Vec2U windowSize) override { auto window = parent->m_sdlWindow; if (parent->m_windowMode != WindowMode::Normal || parent->m_windowSize != windowSize) { if (parent->m_windowMode == WindowMode::Fullscreen) SDL_SetWindowFullscreen(window, 0); else if (parent->m_windowMode == WindowMode::Borderless) SDL_SetWindowBordered(window, SDL_TRUE); else if (parent->m_windowMode == WindowMode::Maximized) SDL_RestoreWindow(window); SDL_SetWindowBordered(window, SDL_TRUE); SDL_SetWindowSize(window, windowSize[0], windowSize[1]); SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); parent->m_windowMode = WindowMode::Normal; parent->m_windowSize = windowSize; } } void setMaximizedWindow() override { if (parent->m_windowMode != WindowMode::Maximized) { if (parent->m_windowMode == WindowMode::Fullscreen) SDL_SetWindowFullscreen(parent->m_sdlWindow, 0); else if (parent->m_windowMode == WindowMode::Borderless) SDL_SetWindowBordered(parent->m_sdlWindow, SDL_TRUE); SDL_RestoreWindow(parent->m_sdlWindow); SDL_MaximizeWindow(parent->m_sdlWindow); parent->m_windowMode = WindowMode::Maximized; } } void setBorderlessWindow() override { if (parent->m_windowMode != WindowMode::Borderless) { if (parent->m_windowMode == WindowMode::Fullscreen) SDL_SetWindowFullscreen(parent->m_sdlWindow, 0); else if (parent->m_windowMode == WindowMode::Maximized) SDL_RestoreWindow(parent->m_sdlWindow); SDL_SetWindowBordered(parent->m_sdlWindow, SDL_FALSE); parent->m_windowMode = WindowMode::Borderless; SDL_DisplayMode actualDisplayMode; if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(parent->m_sdlWindow), &actualDisplayMode) == 0) { parent->m_windowSize = {(unsigned)actualDisplayMode.w, (unsigned)actualDisplayMode.h}; SDL_SetWindowPosition(parent->m_sdlWindow, 0, 0); SDL_SetWindowSize(parent->m_sdlWindow, parent->m_windowSize[0], parent->m_windowSize[1]); parent->m_renderer->setScreenSize(parent->m_windowSize); parent->m_application->windowChanged(parent->m_windowMode, parent->m_windowSize); } else { Logger::error("Couldn't get desktop display mode!"); } } } void setVSyncEnabled(bool vSync) override { if (parent->m_windowVSync != vSync) { parent->setVSyncEnabled(vSync); parent->m_windowVSync = vSync; } } void setMaxFrameSkip(unsigned maxFrameSkip) override { parent->m_maxFrameSkip = maxFrameSkip; } void setCursorVisible(bool cursorVisible) override { parent->m_cursorVisible = cursorVisible; } void setCursorPosition(Vec2I cursorPosition) override { SDL_WarpMouseInWindow(parent->m_sdlWindow, cursorPosition[0], cursorPosition[1]); } bool setCursorImage(const String& id, const ImageConstPtr& image, unsigned scale, const Vec2I& offset) override { return parent->setCursorImage(id, image, scale, offset); } void setAcceptingTextInput(bool acceptingTextInput) override { if (acceptingTextInput != parent->m_acceptingTextInput) { if (acceptingTextInput) SDL_StartTextInput(); else SDL_StopTextInput(); parent->m_acceptingTextInput = acceptingTextInput; } } AudioFormat enableAudio() override { parent->m_audioEnabled = true; SDL_PauseAudio(false); return AudioFormat{44100, 2}; } void disableAudio() override { parent->m_audioEnabled = false; SDL_PauseAudio(true); } bool openAudioInputDevice(const char* name, int freq, int channels, void* userdata, AudioCallback callback) override { return parent->openAudioInputDevice(name, freq, channels, userdata, callback); }; bool closeAudioInputDevice() override { return parent->closeAudioInputDevice(); }; float updateRate() const override { return parent->m_updateRate; } float renderFps() const override { return parent->m_renderRate; } StatisticsServicePtr statisticsService() const override { if (parent->m_platformServices) return parent->m_platformServices->statisticsService(); return {}; } P2PNetworkingServicePtr p2pNetworkingService() const override { if (parent->m_platformServices) return parent->m_platformServices->p2pNetworkingService(); return {}; } UserGeneratedContentServicePtr userGeneratedContentService() const override { if (parent->m_platformServices) return parent->m_platformServices->userGeneratedContentService(); return {}; } DesktopServicePtr desktopService() const override { if (parent->m_platformServices) return parent->m_platformServices->desktopService(); return {}; } void quit() override { parent->m_quitRequested = true; } SdlPlatform* parent; }; List processEvents() { List inputEvents; SDL_Event event; while (SDL_PollEvent(&event)) { Maybe starEvent; switch (event.type) { case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_MAXIMIZED || event.window.event == SDL_WINDOWEVENT_RESTORED) { auto windowFlags = SDL_GetWindowFlags(m_sdlWindow); if (windowFlags & SDL_WINDOW_MAXIMIZED) m_windowMode = WindowMode::Maximized; else if (windowFlags & SDL_WINDOW_FULLSCREEN) m_windowMode = WindowMode::Fullscreen; else if (windowFlags & SDL_WINDOW_BORDERLESS) m_windowMode = WindowMode::Borderless; else m_windowMode = WindowMode::Normal; m_application->windowChanged(m_windowMode, m_windowSize); } else if (event.window.event == SDL_WINDOWEVENT_RESIZED || event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { m_windowSize = Vec2U(event.window.data1, event.window.data2); m_renderer->setScreenSize(m_windowSize); m_application->windowChanged(m_windowMode, m_windowSize); } break; case SDL_KEYDOWN: if (!event.key.repeat) { if (auto key = keyFromSdlKeyCode(event.key.keysym.sym)) starEvent.set(KeyDownEvent{*key, keyModsFromSdlKeyMods(event.key.keysym.mod)}); } break; case SDL_KEYUP: if (auto key = keyFromSdlKeyCode(event.key.keysym.sym)) starEvent.set(KeyUpEvent{*key}); break; case SDL_TEXTINPUT: starEvent.set(TextInputEvent{String(event.text.text)}); break; case SDL_MOUSEMOTION: starEvent.set(MouseMoveEvent{ {event.motion.xrel, -event.motion.yrel}, {event.motion.x, (int)m_windowSize[1] - event.motion.y}}); break; case SDL_MOUSEBUTTONDOWN: starEvent.set(MouseButtonDownEvent{mouseButtonFromSdlMouseButton(event.button.button), {event.button.x, (int)m_windowSize[1] - event.button.y}}); break; case SDL_MOUSEBUTTONUP: starEvent.set(MouseButtonUpEvent{mouseButtonFromSdlMouseButton(event.button.button), {event.button.x, (int)m_windowSize[1] - event.button.y}}); break; case SDL_MOUSEWHEEL: int x, y; SDL_GetMouseState(&x, &y); starEvent.set(MouseWheelEvent{event.wheel.y < 0 ? MouseWheel::Down : MouseWheel::Up, {x, (int)m_windowSize[1] - y}}); break; case SDL_CONTROLLERAXISMOTION: starEvent.set(ControllerAxisEvent{ (ControllerId)event.caxis.which, controllerAxisFromSdlControllerAxis(event.caxis.axis), (float)event.caxis.value / 32768.0f }); break; case SDL_CONTROLLERBUTTONDOWN: starEvent.set(ControllerButtonDownEvent{ (ControllerId)event.cbutton.which, controllerButtonFromSdlControllerButton(event.cbutton.button) }); break; case SDL_CONTROLLERBUTTONUP: starEvent.set(ControllerButtonUpEvent{ (ControllerId)event.cbutton.which, controllerButtonFromSdlControllerButton(event.cbutton.button) }); break; case SDL_CONTROLLERDEVICEADDED: { auto insertion = m_SdlControllers.insert_or_assign(event.cdevice.which, SDLGameControllerUPtr(SDL_GameControllerOpen(event.cdevice.which), SDL_GameControllerClose)); if (SDL_GameController* controller = insertion.first->second.get()) Logger::info("Controller device '{}' added", SDL_GameControllerName(controller)); } break; case SDL_CONTROLLERDEVICEREMOVED: { auto find = m_SdlControllers.find(event.cdevice.which); if (find != m_SdlControllers.end()) { if (SDL_GameController* controller = find->second.get()) Logger::info("Controller device '{}' removed", SDL_GameControllerName(controller)); m_SdlControllers.erase(event.cdevice.which); } } break; case SDL_QUIT: m_quitRequested = true; starEvent.reset(); break; } if (starEvent) inputEvents.append(starEvent.take()); } return inputEvents; } void getAudioData(Uint8* stream, int len) { if (m_audioEnabled) { m_application->getAudioData((int16_t*)stream, len / 4); } else { for (int i = 0; i < len; ++i) stream[i] = 0; } } void setVSyncEnabled(bool vsyncEnabled) { if (vsyncEnabled) { // If VSync is requested, try for late swap tearing first, then fall back // to regular VSync Logger::info("Application: Enabling VSync with late swap tearing"); if (SDL_GL_SetSwapInterval(-1) < 0) { Logger::info("Application: Enabling VSync late swap tearing failed, falling back to full VSync"); SDL_GL_SetSwapInterval(1); } } else { Logger::info("Application: Disabling VSync"); SDL_GL_SetSwapInterval(0); } } static const size_t MaximumCursorDimensions = 128; static const size_t MaximumCursorPixelCount = MaximumCursorDimensions * MaximumCursorDimensions; bool setCursorImage(const String& id, const ImageConstPtr& image, unsigned scale, const Vec2I& offset) { auto imageSize = image->size().piecewiseMultiply(Vec2U::filled(scale)); if (!scale || imageSize.max() > MaximumCursorDimensions || (size_t)(imageSize[0] * imageSize[1]) > MaximumCursorPixelCount) return m_cursorVisible = false; auto& entry = m_cursorCache.get(m_currentCursor = { scale, offset, id }, [&](auto const&) { auto entry = std::make_shared(); List operations; if (scale != 1) operations = { FlipImageOperation{ FlipImageOperation::Mode::FlipY }, // SDL wants an Australian cursor. BorderImageOperation{ 1, Vec4B(), Vec4B(), false, false }, // Nearest scaling fucks up and clips half off the edges, work around this with border+crop for now. ScaleImageOperation{ ScaleImageOperation::Mode::Nearest, Vec2F::filled(scale) }, CropImageOperation{ RectI::withSize(Vec2I::filled(ceilf((float)scale / 2)), Vec2I(imageSize)) } }; else operations = { FlipImageOperation{ FlipImageOperation::Mode::FlipY } }; auto newImage = std::make_shared(processImageOperations(operations, *image)); // Fix fully transparent pixels inverting the underlying display pixel on Windows (allowing this could be made configurable per cursor later!) newImage->forEachPixel([](unsigned /*x*/, unsigned /*y*/, Vec4B& pixel) { if (!pixel[3]) pixel[0] = pixel[1] = pixel[2] = 0; }); entry->image = std::move(newImage); auto size = entry->image->size(); uint32_t pixelFormat; switch (entry->image->pixelFormat()) { case PixelFormat::RGB24: // I know this conversion looks wrong, but it's correct. I'm confused too. pixelFormat = SDL_PIXELFORMAT_BGR888; break; case PixelFormat::RGBA32: pixelFormat = SDL_PIXELFORMAT_ABGR8888; break; case PixelFormat::BGR24: pixelFormat = SDL_PIXELFORMAT_RGB888; break; case PixelFormat::BGRA32: pixelFormat = SDL_PIXELFORMAT_ARGB8888; break; default: pixelFormat = SDL_PIXELFORMAT_UNKNOWN; } entry->sdlSurface.reset(SDL_CreateRGBSurfaceWithFormatFrom( (void*)entry->image->data(), size[0], size[1], entry->image->bitsPerPixel(), entry->image->bytesPerPixel() * size[0], pixelFormat) ); entry->sdlCursor.reset(SDL_CreateColorCursor(entry->sdlSurface.get(), offset[0] * scale, offset[1] * scale)); return entry; }); SDL_SetCursor(entry->sdlCursor.get()); return m_cursorVisible = true; } SignalHandler m_signalHandler; TickRateApproacher m_updateTicker = TickRateApproacher(60.0f, 1.0f); float m_updateRate = 0.0f; TickRateMonitor m_renderTicker = TickRateMonitor(1.0f); float m_renderRate = 0.0f; SDL_Window* m_sdlWindow = nullptr; SDL_GLContext m_sdlGlContext = nullptr; SDL_AudioDeviceID m_sdlAudioOutputDevice = 0; SDL_AudioDeviceID m_sdlAudioInputDevice = 0; typedef std::unique_ptr SDLGameControllerUPtr; StableHashMap m_SdlControllers; typedef std::unique_ptr SDLSurfaceUPtr; typedef std::unique_ptr SDLCursorUPtr; struct CursorEntry { ImageConstPtr image = nullptr; SDLSurfaceUPtr sdlSurface; SDLCursorUPtr sdlCursor; CursorEntry() : image(nullptr), sdlSurface(nullptr, SDL_FreeSurface), sdlCursor(nullptr, SDL_FreeCursor) {}; }; typedef tuple CursorDescriptor; HashTtlCache> m_cursorCache; CursorDescriptor m_currentCursor; Vec2U m_windowSize = {800, 600}; WindowMode m_windowMode = WindowMode::Normal; String m_windowTitle = "Starbound"; bool m_windowVSync = true; unsigned m_maxFrameSkip = 5; bool m_cursorVisible = true; bool m_acceptingTextInput = false; bool m_audioEnabled = false; bool m_quitRequested = false; OpenGl20RendererPtr m_renderer; ApplicationUPtr m_application; PcPlatformServicesUPtr m_platformServices; }; int runMainApplication(ApplicationUPtr application, StringList cmdLineArgs) { try { { SdlPlatform platform(std::move(application), std::move(cmdLineArgs)); platform.run(); } Logger::info("Application: stopped gracefully"); return 0; } catch (std::exception const& e) { fatalException(e, true); } catch (...) { fatalError("Unknown Exception", true); } return 1; } }