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