1 module gfm.sdl2.sdl; 2 3 import core.stdc.stdlib; 4 5 import std.conv, 6 std.string, 7 std.array; 8 9 import derelict.sdl2.sdl, 10 derelict.sdl2.image, 11 derelict.util.exception; 12 13 import std.logger; 14 15 import gfm.core.text, 16 gfm.sdl2.renderer, 17 gfm.sdl2.window, 18 gfm.sdl2.keyboard, 19 gfm.sdl2.mouse; 20 21 /// The one exception type thrown in this wrapper. 22 /// A failing SDL function should <b>always</b> throw a $(D SDL2Exception). 23 class SDL2Exception : Exception 24 { 25 public 26 { 27 @safe pure nothrow this(string message, string file =__FILE__, size_t line = __LINE__, Throwable next = null) 28 { 29 super(message, file, line, next); 30 } 31 } 32 } 33 34 /// Owns both the loader, logging, keyboard state... 35 /// This object is passed around to other SDL wrapper objects 36 /// to ensure library loading. 37 final class SDL2 38 { 39 public 40 { 41 /// Load SDL2 library, redirect logging to our logger. 42 /// You can pass a null logger if you don't want logging. 43 /// Throws: $(D SDL2Exception) on error. 44 /// TODO: Custom SDL assertion handler. 45 this(Logger logger) 46 { 47 _logger = logger is null ? new NullLogger() : logger; 48 _SDLInitialized = false; 49 _SDL2LoggingRedirected = false; 50 try 51 { 52 DerelictSDL2.load(); 53 } 54 catch(DerelictException e) 55 { 56 throw new SDL2Exception(e.msg); 57 } 58 59 // enable all logging, and pipe it to our own logger object 60 { 61 SDL_LogGetOutputFunction(_previousLogCallback, &_previousLogUserdata); 62 SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); 63 SDL_LogSetOutputFunction(&loggingCallbackSDL, cast(void*)this); 64 65 SDL_SetAssertionHandler(&assertCallbackSDL, cast(void*)this); 66 _SDL2LoggingRedirected = true; 67 } 68 69 if (0 != SDL_Init(0)) 70 throwSDL2Exception("SDL_Init"); 71 72 _logger.infof("Platform: %s, %s CPU", getPlatform(), getCPUCount()); 73 74 subSystemInit(SDL_INIT_TIMER); 75 subSystemInit(SDL_INIT_VIDEO); 76 subSystemInit(SDL_INIT_JOYSTICK); 77 subSystemInit(SDL_INIT_AUDIO); 78 subSystemInit(SDL_INIT_HAPTIC); 79 80 _logger.infof("Running using video driver: %s", sanitizeUTF8(SDL_GetCurrentVideoDriver(), _logger, "SDL_GetCurrentVideoDriver")); 81 82 int numDisplays = SDL_GetNumVideoDisplays(); 83 84 _logger.infof("%s video display(s) detected.", numDisplays); 85 86 _keyboard = new SDL2Keyboard(this); 87 _mouse = new SDL2Mouse(this); 88 } 89 90 /// Releases the SDL library and all resources. 91 /// All resources should have been released at this point, 92 /// since you won't be able to call any SDL function afterwards. 93 void close() 94 { 95 // restore previously set logging function 96 if (_SDL2LoggingRedirected) 97 { 98 SDL_LogSetOutputFunction(_previousLogCallback, _previousLogUserdata); 99 _SDL2LoggingRedirected = false; 100 101 SDL_SetAssertionHandler(null, cast(void*)this); 102 } 103 104 if (_SDLInitialized) 105 { 106 SDL_Quit(); 107 _SDLInitialized = false; 108 } 109 110 if (DerelictSDL2.isLoaded()) 111 DerelictSDL2.unload(); 112 } 113 114 ~this() 115 { 116 close(); 117 } 118 119 /// Returns: Available displays information. 120 /// Throws: $(D SDL2Exception) on error. 121 SDL2VideoDisplay[] getDisplays() 122 { 123 int numDisplays = SDL_GetNumVideoDisplays(); 124 125 SDL2VideoDisplay[] availableDisplays; 126 127 for (int displayIndex = 0; displayIndex < numDisplays; ++displayIndex) 128 { 129 SDL_Rect rect; 130 int res = SDL_GetDisplayBounds(displayIndex, &rect); 131 if (res != 0) 132 throwSDL2Exception("SDL_GetDisplayBounds"); 133 134 SDL2DisplayMode[] availableModes; 135 136 int numModes = SDL_GetNumDisplayModes(displayIndex); 137 for(int modeIndex = 0; modeIndex < numModes; ++modeIndex) 138 { 139 SDL_DisplayMode mode; 140 if (0 != SDL_GetDisplayMode(displayIndex, modeIndex, &mode)) 141 throwSDL2Exception("SDL_GetDisplayMode"); 142 143 availableModes ~= new SDL2DisplayMode(modeIndex, mode); 144 } 145 146 availableDisplays ~= new SDL2VideoDisplay(displayIndex, rect, availableModes); 147 } 148 return availableDisplays; 149 } 150 151 /// Returns: Resolution of the first display. 152 /// Throws: $(D SDL2Exception) on error. 153 SDL_Point firstDisplaySize() 154 { 155 auto displays = getDisplays(); 156 if (displays.length == 0) 157 throw new SDL2Exception("no display"); 158 return displays[0].dimension(); 159 } 160 161 /// Returns: Available renderers information. 162 /// Throws: $(D SDL2Exception) on error. 163 SDL2RendererInfo[] getRenderersInfo() 164 { 165 SDL2RendererInfo[] res; 166 int num = SDL_GetNumRenderDrivers(); 167 if (num < 0) 168 throwSDL2Exception("SDL_GetNumRenderDrivers"); 169 170 for (int i = 0; i < num; ++i) 171 { 172 SDL_RendererInfo info; 173 int err = SDL_GetRenderDriverInfo(i, &info); 174 if (err != 0) 175 throwSDL2Exception("SDL_GetRenderDriverInfo"); 176 res ~= new SDL2RendererInfo(_logger, i, info); 177 } 178 return res; 179 } 180 181 /// Get next SDL event. 182 /// Input state gets updated and window callbacks are called too. 183 /// Returns: true if returned an event. 184 bool pollEvent(SDL_Event* event) 185 { 186 if (SDL_PollEvent(event) != 0) 187 { 188 updateState(event); 189 dispatchEvent(event); 190 return true; 191 } 192 else 193 return false; 194 } 195 196 /// Process all pending SDL events. 197 /// Input state gets updated and window callbacks are called too. 198 void processEvents() 199 { 200 SDL_Event event; 201 202 while(SDL_PollEvent(&event) != 0) 203 { 204 updateState(&event); 205 dispatchEvent(&event); 206 } 207 } 208 209 /// Returns: Keyboard state. 210 /// The keyboard state is updated by processEvents() and pollEvent(). 211 SDL2Keyboard keyboard() 212 { 213 return _keyboard; 214 } 215 216 /// Returns: Mouse state. 217 /// The mouse state is updated by processEvents() and pollEvent(). 218 SDL2Mouse mouse() 219 { 220 return _mouse; 221 } 222 223 /// Returns: true if an application termination has been requested. 224 bool wasQuitRequested() const 225 { 226 return _quitWasRequested; 227 } 228 229 deprecated alias wasQuitResquested = wasQuitRequested; 230 231 /// Start text input. 232 void startTextInput() 233 { 234 SDL_StartTextInput(); 235 } 236 237 /// Stops text input. 238 void stopTextInput() 239 { 240 SDL_StopTextInput(); 241 } 242 243 /// Sets clipboard content. 244 /// Throws: $(D SDL2Exception) on error. 245 string setClipboard(string s) 246 { 247 int err = SDL_SetClipboardText(toStringz(s)); 248 if (err != 0) 249 throwSDL2Exception("SDL_SetClipboardText"); 250 return s; 251 } 252 253 /// Returns: Clipboard content. 254 /// Throws: $(D SDL2Exception) on error. 255 string getClipboard() 256 { 257 if (SDL_HasClipboardText() == SDL_FALSE) 258 return null; 259 260 const(char)* s = SDL_GetClipboardText(); 261 if (s is null) 262 throwSDL2Exception("SDL_GetClipboardText"); 263 264 return sanitizeUTF8(s, _logger, "SDL clipboard text"); 265 } 266 267 /// Returns: Available SDL video drivers. 268 string[] getVideoDrivers() 269 { 270 const int numDrivers = SDL_GetNumVideoDrivers(); 271 string[] res; 272 res.length = numDrivers; 273 for(int i = 0; i < numDrivers; ++i) 274 res[i] = sanitizeUTF8(SDL_GetVideoDriver(i), _logger, "SDL_GetVideoDriver"); 275 return res; 276 } 277 278 /// Returns: Platform name. 279 string getPlatform() 280 { 281 return sanitizeUTF8(SDL_GetPlatform(), _logger, "SDL_GetPlatform"); 282 } 283 284 /// Returns: L1 cacheline size in bytes. 285 int getL1LineSize() 286 { 287 int res = SDL_GetCPUCacheLineSize(); 288 if (res <= 0) 289 res = 64; 290 return res; 291 } 292 293 /// Returns: number of CPUs. 294 int getCPUCount() 295 { 296 int res = SDL_GetCPUCount(); 297 if (res <= 0) 298 res = 1; 299 return res; 300 } 301 } 302 303 package 304 { 305 Logger _logger; 306 307 // exception mechanism that shall be used by every module here 308 void throwSDL2Exception(string callThatFailed) 309 { 310 string message = format("%s failed: %s", callThatFailed, getErrorString()); 311 throw new SDL2Exception(message); 312 } 313 314 // return last SDL error and clears it 315 string getErrorString() 316 { 317 const(char)* message = SDL_GetError(); 318 SDL_ClearError(); // clear error 319 return sanitizeUTF8(message, _logger, "SDL error string"); 320 } 321 322 void registerWindow(SDL2Window window) 323 { 324 _knownWindows[window.id()] = window; 325 } 326 327 void unregisterWindow(SDL2Window window) 328 { 329 assert((window.id() in _knownWindows) !is null); 330 _knownWindows.remove(window.id()); 331 } 332 } 333 334 private 335 { 336 bool _SDL2LoggingRedirected; 337 SDL_LogOutputFunction _previousLogCallback; 338 void* _previousLogUserdata; 339 340 341 bool _SDLInitialized; 342 343 // all created windows are keeped in this map 344 // to be able to dispatch event 345 SDL2Window[uint] _knownWindows; 346 347 // SDL_QUIT was received 348 bool _quitWasRequested = false; 349 350 // Holds keyboard state 351 SDL2Keyboard _keyboard; 352 353 // Holds mouse state 354 SDL2Mouse _mouse; 355 356 bool subSystemInitialized(int subSystem) 357 { 358 int inited = SDL_WasInit(SDL_INIT_EVERYTHING); 359 return 0 != ( inited & subSystem ); 360 } 361 362 void subSystemInit(int flag) 363 { 364 if (!subSystemInitialized(flag)) 365 { 366 int res = SDL_InitSubSystem(flag); 367 if (0 != res) 368 throwSDL2Exception("SDL_InitSubSystem"); 369 } 370 } 371 372 void onLogMessage(int category, SDL_LogPriority priority, const(char)* message) 373 { 374 static string readablePriority(SDL_LogPriority priority) pure 375 { 376 switch(priority) 377 { 378 case SDL_LOG_PRIORITY_VERBOSE : return "verbose"; 379 case SDL_LOG_PRIORITY_DEBUG : return "debug"; 380 case SDL_LOG_PRIORITY_INFO : return "info"; 381 case SDL_LOG_PRIORITY_WARN : return "warn"; 382 case SDL_LOG_PRIORITY_ERROR : return "error"; 383 case SDL_LOG_PRIORITY_CRITICAL : return "critical"; 384 default : return "unknown"; 385 } 386 } 387 388 static string readableCategory(SDL_LogPriority priority) pure 389 { 390 switch(priority) 391 { 392 case SDL_LOG_CATEGORY_APPLICATION : return "application"; 393 case SDL_LOG_CATEGORY_ERROR : return "error"; 394 case SDL_LOG_CATEGORY_SYSTEM : return "system"; 395 case SDL_LOG_CATEGORY_AUDIO : return "audio"; 396 case SDL_LOG_CATEGORY_VIDEO : return "video"; 397 case SDL_LOG_CATEGORY_RENDER : return "render"; 398 case SDL_LOG_CATEGORY_INPUT : return "input"; 399 default : return "unknown"; 400 } 401 } 402 403 string formattedMessage = format("SDL (category %s, priority %s): %s", 404 readableCategory(category), 405 readablePriority(priority), 406 sanitizeUTF8(message, _logger, "SDL logging")); 407 408 if (priority == SDL_LOG_PRIORITY_WARN) 409 _logger.warning(formattedMessage); 410 else if (priority == SDL_LOG_PRIORITY_ERROR || priority == SDL_LOG_PRIORITY_CRITICAL) 411 _logger.error(formattedMessage); 412 else 413 _logger.info(formattedMessage); 414 } 415 416 SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata) 417 { 418 _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum); 419 420 debug 421 return SDL_ASSERTION_ABORT; // crash in debug mode 422 else 423 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release 424 } 425 426 // dispatch to relevant event callbacks 427 void dispatchEvent(const (SDL_Event*) event) 428 { 429 switch(event.type) 430 { 431 case SDL_WINDOWEVENT: 432 dispatchWindowEvent(&event.window); 433 break; 434 435 default: 436 break; 437 } 438 } 439 440 // update state based on event 441 // TODO: add mouse state 442 // add joystick state 443 // add haptic state 444 void updateState(const (SDL_Event*) event) 445 { 446 switch(event.type) 447 { 448 case SDL_QUIT: 449 _quitWasRequested = true; 450 break; 451 452 case SDL_KEYDOWN: 453 case SDL_KEYUP: 454 updateKeyboard(&event.key); 455 break; 456 457 case SDL_MOUSEMOTION: 458 _mouse.updateMotion(&event.motion); 459 break; 460 461 case SDL_MOUSEBUTTONUP: 462 case SDL_MOUSEBUTTONDOWN: 463 _mouse.updateButtons(&event.button); 464 break; 465 466 case SDL_MOUSEWHEEL: 467 _mouse.updateWheel(&event.wheel); 468 break; 469 470 default: 471 break; 472 } 473 } 474 475 // TODO: add window callbacks when pressing a key? 476 void updateKeyboard(const(SDL_KeyboardEvent*) event) 477 { 478 // ignore key-repeat 479 if (event.repeat != 0) 480 return; 481 482 switch (event.type) 483 { 484 case SDL_KEYDOWN: 485 assert(event.state == SDL_PRESSED); 486 _keyboard.markKeyAsPressed(event.keysym.scancode); 487 break; 488 489 case SDL_KEYUP: 490 assert(event.state == SDL_RELEASED); 491 _keyboard.markKeyAsReleased(event.keysym.scancode); 492 break; 493 494 default: 495 break; 496 } 497 } 498 499 // call callbacks that can be overriden by subclassing SDL2Window 500 void dispatchWindowEvent(const (SDL_WindowEvent*) windowEvent) 501 { 502 assert(windowEvent.type == SDL_WINDOWEVENT); 503 504 SDL2Window* window = (windowEvent.windowID in _knownWindows); 505 506 if (window is null) 507 { 508 _logger.warningf("Received a SDL event for an unknown window (id = %s)", windowEvent.windowID); 509 return; // no such id known, warning 510 } 511 512 switch (windowEvent.event) 513 { 514 case SDL_WINDOWEVENT_SHOWN: 515 window.onShow(); 516 break; 517 518 case SDL_WINDOWEVENT_HIDDEN: 519 window.onHide(); 520 break; 521 522 case SDL_WINDOWEVENT_EXPOSED: 523 window.onExposed(); 524 break; 525 526 case SDL_WINDOWEVENT_MOVED: 527 window.onMove(windowEvent.data1, windowEvent.data2); 528 break; 529 530 case SDL_WINDOWEVENT_RESIZED: 531 window.onResized(windowEvent.data1, windowEvent.data2); 532 break; 533 534 case SDL_WINDOWEVENT_SIZE_CHANGED: 535 window.onSizeChanged(); 536 break; 537 538 case SDL_WINDOWEVENT_MINIMIZED: 539 window.onMinimized(); 540 break; 541 542 case SDL_WINDOWEVENT_MAXIMIZED: 543 window.onMaximized(); 544 break; 545 546 case SDL_WINDOWEVENT_RESTORED: 547 window.onRestored(); 548 break; 549 550 case SDL_WINDOWEVENT_ENTER: 551 window.onEnter(); 552 break; 553 554 case SDL_WINDOWEVENT_LEAVE: 555 window.onLeave(); 556 break; 557 558 case SDL_WINDOWEVENT_FOCUS_GAINED: 559 window.onFocusGained(); 560 break; 561 562 case SDL_WINDOWEVENT_FOCUS_LOST: 563 window.onFocusLost(); 564 break; 565 566 case SDL_WINDOWEVENT_CLOSE: 567 window.onClose(); 568 break; 569 570 default: 571 // not a window event 572 break; 573 } 574 } 575 } 576 } 577 578 extern(C) private nothrow 579 { 580 void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message) 581 { 582 try 583 { 584 SDL2 sdl2 = cast(SDL2)userData; 585 586 try 587 sdl2.onLogMessage(category, priority, message); 588 catch (Exception e) 589 { 590 // got exception while logging, ignore it 591 } 592 } 593 catch (Throwable e) 594 { 595 // No Throwable is supposed to cross C callbacks boundaries 596 // Crash immediately 597 exit(-1); 598 } 599 } 600 601 SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData) 602 { 603 try 604 { 605 SDL2 sdl2 = cast(SDL2)userData; 606 607 try 608 return sdl2.onLogSDLAssertion(data); 609 catch (Exception e) 610 { 611 // got exception while logging, ignore it 612 } 613 } 614 catch (Throwable e) 615 { 616 // No Throwable is supposed to cross C callbacks boundaries 617 // Crash immediately 618 exit(-1); 619 } 620 return SDL_ASSERTION_ALWAYS_IGNORE; 621 } 622 } 623 624 final class SDL2DisplayMode 625 { 626 public 627 { 628 this(int modeIndex, SDL_DisplayMode mode) 629 { 630 _modeIndex = modeIndex; 631 _mode = mode; 632 } 633 634 override string toString() 635 { 636 return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)", 637 _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format); 638 } 639 } 640 641 private 642 { 643 int _modeIndex; 644 SDL_DisplayMode _mode; 645 } 646 } 647 648 final class SDL2VideoDisplay 649 { 650 public 651 { 652 this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes) 653 { 654 _displayindex = displayindex; 655 _bounds = bounds; 656 _availableModes = availableModes; 657 } 658 659 const(SDL2DisplayMode[]) availableModes() pure const nothrow 660 { 661 return _availableModes; 662 } 663 664 SDL_Point dimension() pure const nothrow 665 { 666 return SDL_Point(_bounds.w, _bounds.h); 667 } 668 669 SDL_Rect bounds() pure const nothrow 670 { 671 return _bounds; 672 } 673 674 override string toString() 675 { 676 string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex, 677 _bounds.x, _bounds.y, _bounds.w, _bounds.h); 678 foreach (mode; _availableModes) 679 res ~= format(" - %s\n", mode); 680 return res; 681 } 682 } 683 684 private 685 { 686 int _displayindex; 687 SDL2DisplayMode[] _availableModes; 688 SDL_Rect _bounds; 689 } 690 } 691