| 1 | //============================================================================ |
| 2 | // |
| 3 | // SSSS tt lll lll |
| 4 | // SS SS tt ll ll |
| 5 | // SS tttttt eeee ll ll aaaa |
| 6 | // SSSS tt ee ee ll ll aa |
| 7 | // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" |
| 8 | // SS SS tt ee ll ll aa aa |
| 9 | // SSSS ttt eeeee llll llll aaaaa |
| 10 | // |
| 11 | // Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony |
| 12 | // and the Stella Team |
| 13 | // |
| 14 | // See the file "License.txt" for information on usage and redistribution of |
| 15 | // this file, and for a DISCLAIMER OF ALL WARRANTIES. |
| 16 | //============================================================================ |
| 17 | |
| 18 | #include "SDL_lib.hxx" |
| 19 | #include "bspf.hxx" |
| 20 | #include "Logger.hxx" |
| 21 | |
| 22 | #include "Console.hxx" |
| 23 | #include "OSystem.hxx" |
| 24 | #include "Settings.hxx" |
| 25 | |
| 26 | #include "ThreadDebugging.hxx" |
| 27 | #include "FBSurfaceSDL2.hxx" |
| 28 | #include "FrameBufferSDL2.hxx" |
| 29 | |
| 30 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 31 | FrameBufferSDL2::FrameBufferSDL2(OSystem& osystem) |
| 32 | : FrameBuffer(osystem), |
| 33 | myWindow(nullptr), |
| 34 | myRenderer(nullptr), |
| 35 | myCenter(false) |
| 36 | { |
| 37 | ASSERT_MAIN_THREAD; |
| 38 | |
| 39 | // Initialize SDL2 context |
| 40 | if(SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0) |
| 41 | { |
| 42 | ostringstream buf; |
| 43 | buf << "ERROR: Couldn't initialize SDL: " << SDL_GetError() << endl; |
| 44 | Logger::error(buf.str()); |
| 45 | throw runtime_error("FATAL ERROR" ); |
| 46 | } |
| 47 | Logger::debug("FrameBufferSDL2::FrameBufferSDL2 SDL_Init()" ); |
| 48 | |
| 49 | // We need a pixel format for palette value calculations |
| 50 | // It's done this way (vs directly accessing a FBSurfaceSDL2 object) |
| 51 | // since the structure may be needed before any FBSurface's have |
| 52 | // been created |
| 53 | myPixelFormat = SDL_AllocFormat(SDL_PIXELFORMAT_ARGB8888); |
| 54 | |
| 55 | myWindowedPos = myOSystem.settings().getPoint("windowedpos" ); |
| 56 | } |
| 57 | |
| 58 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 59 | FrameBufferSDL2::~FrameBufferSDL2() |
| 60 | { |
| 61 | ASSERT_MAIN_THREAD; |
| 62 | |
| 63 | SDL_FreeFormat(myPixelFormat); |
| 64 | |
| 65 | if(myRenderer) |
| 66 | { |
| 67 | // Make sure to free surfaces/textures before destroying the renderer itself |
| 68 | // Most platforms are fine with doing this in either order, but it seems |
| 69 | // that OpenBSD in particular crashes when attempting to destroy textures |
| 70 | // *after* the renderer is already destroyed |
| 71 | freeSurfaces(); |
| 72 | |
| 73 | SDL_DestroyRenderer(myRenderer); |
| 74 | myRenderer = nullptr; |
| 75 | } |
| 76 | if(myWindow) |
| 77 | { |
| 78 | SDL_SetWindowFullscreen(myWindow, 0); // on some systems, a crash occurs |
| 79 | // when destroying fullscreen window |
| 80 | SDL_DestroyWindow(myWindow); |
| 81 | myWindow = nullptr; |
| 82 | } |
| 83 | SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER); |
| 84 | } |
| 85 | |
| 86 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 87 | void FrameBufferSDL2::queryHardware(vector<Common::Size>& fullscreenRes, |
| 88 | vector<Common::Size>& windowedRes, |
| 89 | VariantList& renderers) |
| 90 | { |
| 91 | ASSERT_MAIN_THREAD; |
| 92 | |
| 93 | // Get number of displays (for most systems, this will be '1') |
| 94 | myNumDisplays = SDL_GetNumVideoDisplays(); |
| 95 | |
| 96 | // First get the maximum fullscreen desktop resolution |
| 97 | SDL_DisplayMode display; |
| 98 | for(int i = 0; i < myNumDisplays; ++i) |
| 99 | { |
| 100 | SDL_GetDesktopDisplayMode(i, &display); |
| 101 | fullscreenRes.emplace_back(display.w, display.h); |
| 102 | } |
| 103 | |
| 104 | // Now get the maximum windowed desktop resolution |
| 105 | // Try to take into account taskbars, etc, if available |
| 106 | #if SDL_VERSION_ATLEAST(2,0,5) |
| 107 | SDL_Rect r; |
| 108 | for(int i = 0; i < myNumDisplays; ++i) |
| 109 | { |
| 110 | // Display bounds minus dock |
| 111 | SDL_GetDisplayUsableBounds(i, &r); // Requires SDL-2.0.5 or higher |
| 112 | windowedRes.emplace_back(r.w, r.h); |
| 113 | } |
| 114 | #else |
| 115 | for(int i = 0; i < myNumDisplays; ++i) |
| 116 | { |
| 117 | SDL_GetDesktopDisplayMode(i, &display); |
| 118 | windowedRes.emplace_back(display.w, display.h); |
| 119 | } |
| 120 | #endif |
| 121 | |
| 122 | struct RenderName |
| 123 | { |
| 124 | string sdlName; |
| 125 | string stellaName; |
| 126 | }; |
| 127 | // Create name map for all currently known SDL renderers |
| 128 | const int NUM_RENDERERS = 5; |
| 129 | static const RenderName RENDERER_NAMES[NUM_RENDERERS] = { |
| 130 | { "direct3d" , "Direct3D" }, |
| 131 | { "opengl" , "OpenGL" }, |
| 132 | { "opengles" , "OpenGLES" }, |
| 133 | { "opengles2" , "OpenGLES2" }, |
| 134 | { "software" , "Software" } |
| 135 | }; |
| 136 | |
| 137 | int numDrivers = SDL_GetNumRenderDrivers(); |
| 138 | for(int i = 0; i < numDrivers; ++i) |
| 139 | { |
| 140 | SDL_RendererInfo info; |
| 141 | if(SDL_GetRenderDriverInfo(i, &info) == 0) |
| 142 | { |
| 143 | // Map SDL names into nicer Stella names (if available) |
| 144 | bool found = false; |
| 145 | for(int j = 0; j < NUM_RENDERERS; ++j) |
| 146 | { |
| 147 | if(RENDERER_NAMES[j].sdlName == info.name) |
| 148 | { |
| 149 | VarList::push_back(renderers, RENDERER_NAMES[j].stellaName, info.name); |
| 150 | found = true; |
| 151 | break; |
| 152 | } |
| 153 | } |
| 154 | if(!found) |
| 155 | VarList::push_back(renderers, info.name, info.name); |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 161 | Int32 FrameBufferSDL2::getCurrentDisplayIndex() |
| 162 | { |
| 163 | ASSERT_MAIN_THREAD; |
| 164 | |
| 165 | return SDL_GetWindowDisplayIndex(myWindow); |
| 166 | } |
| 167 | |
| 168 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 169 | void FrameBufferSDL2::updateWindowedPos() |
| 170 | { |
| 171 | ASSERT_MAIN_THREAD; |
| 172 | |
| 173 | // only save if the window is not centered and not in full screen mode |
| 174 | if (!myCenter && myWindow && !(SDL_GetWindowFlags(myWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP)) |
| 175 | { |
| 176 | // save current windowed position |
| 177 | SDL_GetWindowPosition(myWindow, &myWindowedPos.x, &myWindowedPos.y); |
| 178 | myOSystem.settings().setValue("windowedpos" , myWindowedPos); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 183 | bool FrameBufferSDL2::setVideoMode(const string& title, const VideoMode& mode) |
| 184 | { |
| 185 | ASSERT_MAIN_THREAD; |
| 186 | |
| 187 | // If not initialized by this point, then immediately fail |
| 188 | if(SDL_WasInit(SDL_INIT_VIDEO) == 0) |
| 189 | return false; |
| 190 | |
| 191 | Int32 displayIndex = mode.fsIndex; |
| 192 | if (displayIndex == -1) |
| 193 | { |
| 194 | // windowed mode |
| 195 | if (myWindow) |
| 196 | { |
| 197 | // Show it on same screen as the previous window |
| 198 | displayIndex = SDL_GetWindowDisplayIndex(myWindow); |
| 199 | } |
| 200 | if (displayIndex < 0) |
| 201 | { |
| 202 | // fallback to the last used screen if still existing |
| 203 | displayIndex = std::min(myNumDisplays, myOSystem.settings().getInt("display" )); |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | // save and get last windowed window's position |
| 208 | updateWindowedPos(); |
| 209 | |
| 210 | // Always recreate renderer (some systems need this) |
| 211 | if(myRenderer) |
| 212 | { |
| 213 | SDL_DestroyRenderer(myRenderer); |
| 214 | myRenderer = nullptr; |
| 215 | } |
| 216 | |
| 217 | int posX, posY; |
| 218 | |
| 219 | myCenter = myOSystem.settings().getBool("center" ); |
| 220 | if (myCenter) |
| 221 | posX = posY = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex); |
| 222 | else |
| 223 | { |
| 224 | posX = myWindowedPos.x; |
| 225 | posY = myWindowedPos.y; |
| 226 | |
| 227 | // make sure the window is at least partially visibile |
| 228 | int x0 = 0, y0 = 0, x1 = 0, y1 = 0; |
| 229 | |
| 230 | for (int display = SDL_GetNumVideoDisplays() - 1; display >= 0; display--) |
| 231 | { |
| 232 | SDL_Rect rect; |
| 233 | |
| 234 | if (!SDL_GetDisplayUsableBounds(display, &rect)) |
| 235 | { |
| 236 | x0 = std::min(x0, rect.x); |
| 237 | y0 = std::min(y0, rect.y); |
| 238 | x1 = std::max(x1, rect.x + rect.w); |
| 239 | y1 = std::max(y1, rect.y + rect.h); |
| 240 | } |
| 241 | } |
| 242 | posX = BSPF::clamp(posX, x0 - Int32(mode.screen.w) + 50, x1 - 50); |
| 243 | posY = BSPF::clamp(posY, y0 + 50, y1 - 50); |
| 244 | } |
| 245 | uInt32 flags = mode.fsIndex != -1 ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0; |
| 246 | |
| 247 | // macOS seems to have issues with destroying the window, and wants to |
| 248 | // keep the same handle |
| 249 | // Problem is, doing so on other platforms results in flickering when |
| 250 | // toggling fullscreen windowed mode |
| 251 | // So we have a special case for macOS |
| 252 | #ifndef BSPF_MACOS |
| 253 | // Don't re-create the window if its size hasn't changed, as it's not |
| 254 | // necessary, and causes flashing in fullscreen mode |
| 255 | if(myWindow) |
| 256 | { |
| 257 | int w, h; |
| 258 | SDL_GetWindowSize(myWindow, &w, &h); |
| 259 | if(uInt32(w) != mode.screen.w || uInt32(h) != mode.screen.h) |
| 260 | { |
| 261 | SDL_DestroyWindow(myWindow); |
| 262 | myWindow = nullptr; |
| 263 | } |
| 264 | } |
| 265 | if(myWindow) |
| 266 | { |
| 267 | // Even though window size stayed the same, the title may have changed |
| 268 | SDL_SetWindowTitle(myWindow, title.c_str()); |
| 269 | SDL_SetWindowPosition(myWindow, posX, posY); |
| 270 | } |
| 271 | #else |
| 272 | // macOS wants to *never* re-create the window |
| 273 | // This sometimes results in the window being resized *after* it's displayed, |
| 274 | // but at least the code works and doesn't crash |
| 275 | if(myWindow) |
| 276 | { |
| 277 | SDL_SetWindowFullscreen(myWindow, flags); |
| 278 | SDL_SetWindowSize(myWindow, mode.screen.w, mode.screen.h); |
| 279 | SDL_SetWindowPosition(myWindow, posX, posY); |
| 280 | SDL_SetWindowTitle(myWindow, title.c_str()); |
| 281 | } |
| 282 | #endif |
| 283 | else |
| 284 | { |
| 285 | myWindow = SDL_CreateWindow(title.c_str(), posX, posY, |
| 286 | mode.screen.w, mode.screen.h, flags); |
| 287 | if(myWindow == nullptr) |
| 288 | { |
| 289 | string msg = "ERROR: Unable to open SDL window: " + string(SDL_GetError()); |
| 290 | Logger::error(msg); |
| 291 | return false; |
| 292 | } |
| 293 | setWindowIcon(); |
| 294 | } |
| 295 | |
| 296 | uInt32 renderFlags = SDL_RENDERER_ACCELERATED; |
| 297 | if(myOSystem.settings().getBool("vsync" )) // V'synced blits option |
| 298 | renderFlags |= SDL_RENDERER_PRESENTVSYNC; |
| 299 | const string& video = myOSystem.settings().getString("video" ); // Render hint |
| 300 | if(video != "" ) |
| 301 | SDL_SetHint(SDL_HINT_RENDER_DRIVER, video.c_str()); |
| 302 | myRenderer = SDL_CreateRenderer(myWindow, -1, renderFlags); |
| 303 | if(myRenderer == nullptr) |
| 304 | { |
| 305 | string msg = "ERROR: Unable to create SDL renderer: " + string(SDL_GetError()); |
| 306 | Logger::error(msg); |
| 307 | return false; |
| 308 | } |
| 309 | clear(); |
| 310 | |
| 311 | SDL_RendererInfo renderinfo; |
| 312 | if(SDL_GetRendererInfo(myRenderer, &renderinfo) >= 0) |
| 313 | myOSystem.settings().setValue("video" , renderinfo.name); |
| 314 | |
| 315 | return true; |
| 316 | } |
| 317 | |
| 318 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 319 | void FrameBufferSDL2::setTitle(const string& title) |
| 320 | { |
| 321 | ASSERT_MAIN_THREAD; |
| 322 | |
| 323 | myScreenTitle = title; |
| 324 | |
| 325 | if(myWindow) |
| 326 | SDL_SetWindowTitle(myWindow, title.c_str()); |
| 327 | } |
| 328 | |
| 329 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 330 | string FrameBufferSDL2::about() const |
| 331 | { |
| 332 | ASSERT_MAIN_THREAD; |
| 333 | |
| 334 | ostringstream out; |
| 335 | out << "Video system: " << SDL_GetCurrentVideoDriver() << endl; |
| 336 | SDL_RendererInfo info; |
| 337 | if(SDL_GetRendererInfo(myRenderer, &info) >= 0) |
| 338 | { |
| 339 | out << " Renderer: " << info.name << endl; |
| 340 | if(info.max_texture_width > 0 && info.max_texture_height > 0) |
| 341 | out << " Max texture: " << info.max_texture_width << "x" |
| 342 | << info.max_texture_height << endl; |
| 343 | out << " Flags: " |
| 344 | << ((info.flags & SDL_RENDERER_PRESENTVSYNC) ? "+" : "-" ) << "vsync, " |
| 345 | << ((info.flags & SDL_RENDERER_ACCELERATED) ? "+" : "-" ) << "accel" |
| 346 | << endl; |
| 347 | } |
| 348 | return out.str(); |
| 349 | } |
| 350 | |
| 351 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 352 | void FrameBufferSDL2::showCursor(bool show) |
| 353 | { |
| 354 | ASSERT_MAIN_THREAD; |
| 355 | |
| 356 | SDL_ShowCursor(show ? SDL_ENABLE : SDL_DISABLE); |
| 357 | } |
| 358 | |
| 359 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 360 | void FrameBufferSDL2::grabMouse(bool grab) |
| 361 | { |
| 362 | ASSERT_MAIN_THREAD; |
| 363 | |
| 364 | SDL_SetRelativeMouseMode(grab ? SDL_TRUE : SDL_FALSE); |
| 365 | } |
| 366 | |
| 367 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 368 | bool FrameBufferSDL2::fullScreen() const |
| 369 | { |
| 370 | ASSERT_MAIN_THREAD; |
| 371 | |
| 372 | #ifdef WINDOWED_SUPPORT |
| 373 | return SDL_GetWindowFlags(myWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP; |
| 374 | #else |
| 375 | return true; |
| 376 | #endif |
| 377 | } |
| 378 | |
| 379 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 380 | void FrameBufferSDL2::renderToScreen() |
| 381 | { |
| 382 | ASSERT_MAIN_THREAD; |
| 383 | |
| 384 | // Show all changes made to the renderer |
| 385 | SDL_RenderPresent(myRenderer); |
| 386 | } |
| 387 | |
| 388 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 389 | void FrameBufferSDL2::setWindowIcon() |
| 390 | { |
| 391 | ASSERT_MAIN_THREAD; |
| 392 | |
| 393 | #if !defined(BSPF_MACOS) && !defined(RETRON77) |
| 394 | #include "stella_icon.hxx" |
| 395 | |
| 396 | SDL_Surface* surface = SDL_CreateRGBSurfaceFrom(stella_icon, 32, 32, 32, |
| 397 | 32 * 4, 0xFF0000, 0x00FF00, 0x0000FF, 0xFF000000); |
| 398 | SDL_SetWindowIcon(myWindow, surface); |
| 399 | SDL_FreeSurface(surface); |
| 400 | #endif |
| 401 | } |
| 402 | |
| 403 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 404 | unique_ptr<FBSurface> |
| 405 | FrameBufferSDL2::createSurface(uInt32 w, uInt32 h, const uInt32* data) const |
| 406 | { |
| 407 | return make_unique<FBSurfaceSDL2>(const_cast<FrameBufferSDL2&>(*this), w, h, data); |
| 408 | } |
| 409 | |
| 410 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 411 | void FrameBufferSDL2::readPixels(uInt8* pixels, uInt32 pitch, |
| 412 | const Common::Rect& rect) const |
| 413 | { |
| 414 | ASSERT_MAIN_THREAD; |
| 415 | |
| 416 | SDL_Rect r; |
| 417 | r.x = rect.x(); r.y = rect.y(); |
| 418 | r.w = rect.w(); r.h = rect.h(); |
| 419 | |
| 420 | SDL_RenderReadPixels(myRenderer, &r, 0, pixels, pitch); |
| 421 | } |
| 422 | |
| 423 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 424 | void FrameBufferSDL2::clear() |
| 425 | { |
| 426 | ASSERT_MAIN_THREAD; |
| 427 | |
| 428 | SDL_RenderClear(myRenderer); |
| 429 | } |
| 430 | |