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