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