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.experimental.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 /// Returns: A path suitable for writing configuration files, saved games, etc... 303 /// See_also: $(LINK http://wiki.libsdl.org/SDL_GetPrefPath) 304 /// Throws: $(D SDL2Exception) on error. 305 string getPrefPath(string orgName, string applicationName) 306 { 307 char* basePath = SDL_GetPrefPath(toStringz(orgName), toStringz(applicationName)); 308 if (basePath != null) 309 { 310 string result = sanitizeUTF8(basePath, _logger, "SDL pref path"); 311 SDL_free(basePath); 312 return result; 313 } 314 else 315 { 316 throwSDL2Exception("SDL_GetPrefPath"); 317 return null; // unreachable 318 } 319 } 320 } 321 322 package 323 { 324 Logger _logger; 325 326 // exception mechanism that shall be used by every module here 327 void throwSDL2Exception(string callThatFailed) 328 { 329 string message = format("%s failed: %s", callThatFailed, getErrorString()); 330 throw new SDL2Exception(message); 331 } 332 333 // return last SDL error and clears it 334 string getErrorString() 335 { 336 const(char)* message = SDL_GetError(); 337 SDL_ClearError(); // clear error 338 return sanitizeUTF8(message, _logger, "SDL error string"); 339 } 340 341 void registerWindow(SDL2Window window) 342 { 343 _knownWindows[window.id()] = window; 344 } 345 346 void unregisterWindow(SDL2Window window) 347 { 348 assert((window.id() in _knownWindows) !is null); 349 _knownWindows.remove(window.id()); 350 } 351 } 352 353 private 354 { 355 bool _SDL2LoggingRedirected; 356 SDL_LogOutputFunction _previousLogCallback; 357 void* _previousLogUserdata; 358 359 360 bool _SDLInitialized; 361 362 // all created windows are keeped in this map 363 // to be able to dispatch event 364 SDL2Window[uint] _knownWindows; 365 366 // SDL_QUIT was received 367 bool _quitWasRequested = false; 368 369 // Holds keyboard state 370 SDL2Keyboard _keyboard; 371 372 // Holds mouse state 373 SDL2Mouse _mouse; 374 375 bool subSystemInitialized(int subSystem) 376 { 377 int inited = SDL_WasInit(SDL_INIT_EVERYTHING); 378 return 0 != ( inited & subSystem ); 379 } 380 381 void subSystemInit(int flag) 382 { 383 if (!subSystemInitialized(flag)) 384 { 385 int res = SDL_InitSubSystem(flag); 386 if (0 != res) 387 throwSDL2Exception("SDL_InitSubSystem"); 388 } 389 } 390 391 void onLogMessage(int category, SDL_LogPriority priority, const(char)* message) 392 { 393 static string readablePriority(SDL_LogPriority priority) pure 394 { 395 switch(priority) 396 { 397 case SDL_LOG_PRIORITY_VERBOSE : return "verbose"; 398 case SDL_LOG_PRIORITY_DEBUG : return "debug"; 399 case SDL_LOG_PRIORITY_INFO : return "info"; 400 case SDL_LOG_PRIORITY_WARN : return "warn"; 401 case SDL_LOG_PRIORITY_ERROR : return "error"; 402 case SDL_LOG_PRIORITY_CRITICAL : return "critical"; 403 default : return "unknown"; 404 } 405 } 406 407 static string readableCategory(SDL_LogPriority priority) pure 408 { 409 switch(priority) 410 { 411 case SDL_LOG_CATEGORY_APPLICATION : return "application"; 412 case SDL_LOG_CATEGORY_ERROR : return "error"; 413 case SDL_LOG_CATEGORY_SYSTEM : return "system"; 414 case SDL_LOG_CATEGORY_AUDIO : return "audio"; 415 case SDL_LOG_CATEGORY_VIDEO : return "video"; 416 case SDL_LOG_CATEGORY_RENDER : return "render"; 417 case SDL_LOG_CATEGORY_INPUT : return "input"; 418 default : return "unknown"; 419 } 420 } 421 422 string formattedMessage = format("SDL (category %s, priority %s): %s", 423 readableCategory(category), 424 readablePriority(priority), 425 sanitizeUTF8(message, _logger, "SDL logging")); 426 427 if (priority == SDL_LOG_PRIORITY_WARN) 428 _logger.warning(formattedMessage); 429 else if (priority == SDL_LOG_PRIORITY_ERROR || priority == SDL_LOG_PRIORITY_CRITICAL) 430 _logger.error(formattedMessage); 431 else 432 _logger.info(formattedMessage); 433 } 434 435 SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata) 436 { 437 _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum); 438 439 debug 440 return SDL_ASSERTION_ABORT; // crash in debug mode 441 else 442 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release 443 } 444 445 // dispatch to relevant event callbacks 446 void dispatchEvent(const (SDL_Event*) event) 447 { 448 switch(event.type) 449 { 450 case SDL_WINDOWEVENT: 451 dispatchWindowEvent(&event.window); 452 break; 453 454 case SDL_KEYDOWN: 455 case SDL_KEYUP: 456 const (SDL_KeyboardEvent*) keyboardEvent = &event.key; 457 SDL2Window* window = (keyboardEvent.windowID in _knownWindows); 458 if (window !is null) 459 { 460 if (event.type == SDL_KEYDOWN) 461 window.onKeyDown(keyboardEvent.timestamp, _keyboard, keyboardEvent.keysym.sym); 462 else 463 window.onKeyUp(keyboardEvent.timestamp, _keyboard, keyboardEvent.keysym.sym); 464 } 465 break; 466 467 case SDL_MOUSEBUTTONUP: 468 case SDL_MOUSEBUTTONDOWN: 469 const (SDL_MouseButtonEvent*) mbEvent = &event.button; 470 SDL2Window* window = (mbEvent.windowID in _knownWindows); 471 if (window !is null) 472 { 473 if (event.type == SDL_MOUSEBUTTONDOWN) 474 window.onMouseButtonPressed(mbEvent.timestamp, _mouse, mbEvent.button, mbEvent.clicks > 1); 475 else 476 window.onMouseButtonReleased(mbEvent.timestamp, _mouse, mbEvent.button); 477 } 478 break; 479 480 case SDL_MOUSEWHEEL: 481 const (SDL_MouseWheelEvent*) wheelEvent = &event.wheel; 482 SDL2Window* window = (wheelEvent.windowID in _knownWindows); 483 if (window !is null) 484 window.onMouseWheel(wheelEvent.timestamp, _mouse, wheelEvent.x, wheelEvent.y); 485 break; 486 487 case SDL_MOUSEMOTION: 488 const (SDL_MouseMotionEvent*) motionEvent = &event.motion; 489 SDL2Window* window = (motionEvent.windowID in _knownWindows); 490 if (window !is null) 491 window.onMouseMove(motionEvent.timestamp, _mouse); 492 break; 493 494 default: 495 break; 496 } 497 } 498 499 // update state based on event 500 // TODO: add joystick state 501 // add haptic state 502 void updateState(const (SDL_Event*) event) 503 { 504 switch(event.type) 505 { 506 case SDL_QUIT: 507 _quitWasRequested = true; 508 break; 509 510 case SDL_KEYDOWN: 511 case SDL_KEYUP: 512 updateKeyboard(&event.key); 513 break; 514 515 case SDL_MOUSEMOTION: 516 _mouse.updateMotion(&event.motion); 517 break; 518 519 case SDL_MOUSEBUTTONUP: 520 case SDL_MOUSEBUTTONDOWN: 521 _mouse.updateButtons(&event.button); 522 break; 523 524 case SDL_MOUSEWHEEL: 525 _mouse.updateWheel(&event.wheel); 526 break; 527 528 default: 529 break; 530 } 531 } 532 533 // TODO: add window callbacks when pressing a key? 534 void updateKeyboard(const(SDL_KeyboardEvent*) event) 535 { 536 // ignore key-repeat 537 if (event.repeat != 0) 538 return; 539 540 switch (event.type) 541 { 542 case SDL_KEYDOWN: 543 assert(event.state == SDL_PRESSED); 544 _keyboard.markKeyAsPressed(event.keysym.scancode); 545 break; 546 547 case SDL_KEYUP: 548 assert(event.state == SDL_RELEASED); 549 _keyboard.markKeyAsReleased(event.keysym.scancode); 550 break; 551 552 default: 553 break; 554 } 555 } 556 557 // call callbacks that can be overriden by subclassing SDL2Window 558 void dispatchWindowEvent(const (SDL_WindowEvent*) windowEvent) 559 { 560 assert(windowEvent.type == SDL_WINDOWEVENT); 561 562 SDL2Window* window = (windowEvent.windowID in _knownWindows); 563 564 if (window is null) 565 { 566 _logger.warningf("Received a SDL event for an unknown window (id = %s)", windowEvent.windowID); 567 return; // no such id known, warning 568 } 569 570 switch (windowEvent.event) 571 { 572 case SDL_WINDOWEVENT_SHOWN: 573 window.onShow(); 574 break; 575 576 case SDL_WINDOWEVENT_HIDDEN: 577 window.onHide(); 578 break; 579 580 case SDL_WINDOWEVENT_EXPOSED: 581 window.onExposed(); 582 break; 583 584 case SDL_WINDOWEVENT_MOVED: 585 window.onMove(windowEvent.data1, windowEvent.data2); 586 break; 587 588 case SDL_WINDOWEVENT_RESIZED: 589 window.onResized(windowEvent.data1, windowEvent.data2); 590 break; 591 592 case SDL_WINDOWEVENT_SIZE_CHANGED: 593 window.onSizeChanged(); 594 break; 595 596 case SDL_WINDOWEVENT_MINIMIZED: 597 window.onMinimized(); 598 break; 599 600 case SDL_WINDOWEVENT_MAXIMIZED: 601 window.onMaximized(); 602 break; 603 604 case SDL_WINDOWEVENT_RESTORED: 605 window.onRestored(); 606 break; 607 608 case SDL_WINDOWEVENT_ENTER: 609 window.onEnter(); 610 break; 611 612 case SDL_WINDOWEVENT_LEAVE: 613 window.onLeave(); 614 break; 615 616 case SDL_WINDOWEVENT_FOCUS_GAINED: 617 window.onFocusGained(); 618 break; 619 620 case SDL_WINDOWEVENT_FOCUS_LOST: 621 window.onFocusLost(); 622 break; 623 624 case SDL_WINDOWEVENT_CLOSE: 625 window.onClose(); 626 break; 627 628 default: 629 // not a window event 630 break; 631 } 632 } 633 } 634 } 635 636 extern(C) private nothrow 637 { 638 void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message) 639 { 640 try 641 { 642 SDL2 sdl2 = cast(SDL2)userData; 643 644 try 645 sdl2.onLogMessage(category, priority, message); 646 catch (Exception e) 647 { 648 // got exception while logging, ignore it 649 } 650 } 651 catch (Throwable e) 652 { 653 // No Throwable is supposed to cross C callbacks boundaries 654 // Crash immediately 655 exit(-1); 656 } 657 } 658 659 SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData) 660 { 661 try 662 { 663 SDL2 sdl2 = cast(SDL2)userData; 664 665 try 666 return sdl2.onLogSDLAssertion(data); 667 catch (Exception e) 668 { 669 // got exception while logging, ignore it 670 } 671 } 672 catch (Throwable e) 673 { 674 // No Throwable is supposed to cross C callbacks boundaries 675 // Crash immediately 676 exit(-1); 677 } 678 return SDL_ASSERTION_ALWAYS_IGNORE; 679 } 680 } 681 682 final class SDL2DisplayMode 683 { 684 public 685 { 686 this(int modeIndex, SDL_DisplayMode mode) 687 { 688 _modeIndex = modeIndex; 689 _mode = mode; 690 } 691 692 override string toString() 693 { 694 return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)", 695 _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format); 696 } 697 } 698 699 private 700 { 701 int _modeIndex; 702 SDL_DisplayMode _mode; 703 } 704 } 705 706 final class SDL2VideoDisplay 707 { 708 public 709 { 710 this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes) 711 { 712 _displayindex = displayindex; 713 _bounds = bounds; 714 _availableModes = availableModes; 715 } 716 717 const(SDL2DisplayMode[]) availableModes() pure const nothrow 718 { 719 return _availableModes; 720 } 721 722 SDL_Point dimension() pure const nothrow 723 { 724 return SDL_Point(_bounds.w, _bounds.h); 725 } 726 727 SDL_Rect bounds() pure const nothrow 728 { 729 return _bounds; 730 } 731 732 override string toString() 733 { 734 string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex, 735 _bounds.x, _bounds.y, _bounds.w, _bounds.h); 736 foreach (mode; _availableModes) 737 res ~= format(" - %s\n", mode); 738 return res; 739 } 740 } 741 742 private 743 { 744 int _displayindex; 745 SDL2DisplayMode[] _availableModes; 746 SDL_Rect _bounds; 747 } 748 } 749