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 const(char)[][] getVideoDrivers() 301 { 302 const int numDrivers = SDL_GetNumVideoDrivers(); 303 const(char)[][] res; 304 res.length = numDrivers; 305 for(int i = 0; i < numDrivers; ++i) 306 res[i] = fromStringz(SDL_GetVideoDriver(i)); 307 return res; 308 } 309 310 /// Returns: Platform name. 311 const(char)[] getPlatform() 312 { 313 return fromStringz(SDL_GetPlatform()); 314 } 315 316 /// Returns: L1 cacheline size in bytes. 317 int getL1LineSize() 318 { 319 int res = SDL_GetCPUCacheLineSize(); 320 if (res <= 0) 321 res = 64; 322 return res; 323 } 324 325 /// Returns: number of CPUs. 326 int getCPUCount() 327 { 328 int res = SDL_GetCPUCount(); 329 if (res <= 0) 330 res = 1; 331 return res; 332 } 333 334 /// Returns: A path suitable for writing configuration files, saved games, etc... 335 /// See_also: $(LINK http://wiki.libsdl.org/SDL_GetPrefPath) 336 /// Throws: $(D SDL2Exception) on error. 337 const(char)[] getPrefPath(string orgName, string applicationName) 338 { 339 char* basePath = SDL_GetPrefPath(toStringz(orgName), toStringz(applicationName)); 340 if (basePath != null) 341 { 342 const(char)[] result = fromStringz(basePath); 343 SDL_free(basePath); 344 return result; 345 } 346 else 347 { 348 throwSDL2Exception("SDL_GetPrefPath"); 349 return null; // unreachable 350 } 351 } 352 } 353 354 package 355 { 356 Logger _logger; 357 358 // exception mechanism that shall be used by every module here 359 void throwSDL2Exception(string callThatFailed) 360 { 361 string message = format("%s failed: %s", callThatFailed, getErrorString()); 362 throw new SDL2Exception(message); 363 } 364 365 // return last SDL error and clears it 366 const(char)[] getErrorString() 367 { 368 const(char)* message = SDL_GetError(); 369 SDL_ClearError(); // clear error 370 return fromStringz(message); 371 } 372 } 373 374 private 375 { 376 bool _SDL2LoggingRedirected; 377 SDL_LogOutputFunction _previousLogCallback; 378 void* _previousLogUserdata; 379 380 381 bool _SDLInitialized; 382 383 // all created windows are keeped in this map 384 // to be able to dispatch event 385 SDL2Window[uint] _knownWindows; 386 387 // SDL_QUIT was received 388 bool _quitWasRequested = false; 389 390 // Holds keyboard state 391 SDL2Keyboard _keyboard; 392 393 // Holds mouse state 394 SDL2Mouse _mouse; 395 396 void onLogMessage(int category, SDL_LogPriority priority, const(char)* message) 397 { 398 static string readablePriority(SDL_LogPriority priority) pure 399 { 400 switch(priority) 401 { 402 case SDL_LOG_PRIORITY_VERBOSE : return "verbose"; 403 case SDL_LOG_PRIORITY_DEBUG : return "debug"; 404 case SDL_LOG_PRIORITY_INFO : return "info"; 405 case SDL_LOG_PRIORITY_WARN : return "warn"; 406 case SDL_LOG_PRIORITY_ERROR : return "error"; 407 case SDL_LOG_PRIORITY_CRITICAL : return "critical"; 408 default : return "unknown"; 409 } 410 } 411 412 static string readableCategory(SDL_LogPriority priority) pure 413 { 414 switch(priority) 415 { 416 case SDL_LOG_CATEGORY_APPLICATION : return "application"; 417 case SDL_LOG_CATEGORY_ERROR : return "error"; 418 case SDL_LOG_CATEGORY_SYSTEM : return "system"; 419 case SDL_LOG_CATEGORY_AUDIO : return "audio"; 420 case SDL_LOG_CATEGORY_VIDEO : return "video"; 421 case SDL_LOG_CATEGORY_RENDER : return "render"; 422 case SDL_LOG_CATEGORY_INPUT : return "input"; 423 default : return "unknown"; 424 } 425 } 426 427 string formattedMessage = format("SDL (category %s, priority %s): %s", 428 readableCategory(category), 429 readablePriority(priority), 430 fromStringz(message)); 431 432 if (priority == SDL_LOG_PRIORITY_WARN) 433 _logger.warning(formattedMessage); 434 else if (priority == SDL_LOG_PRIORITY_ERROR || priority == SDL_LOG_PRIORITY_CRITICAL) 435 _logger.error(formattedMessage); 436 else 437 _logger.info(formattedMessage); 438 } 439 440 SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata) 441 { 442 _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum); 443 444 debug 445 return SDL_ASSERTION_ABORT; // crash in debug mode 446 else 447 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release 448 } 449 450 // update state based on event 451 // TODO: add joystick state 452 // add haptic state 453 void updateState(const (SDL_Event*) event) 454 { 455 switch(event.type) 456 { 457 case SDL_QUIT: 458 _quitWasRequested = true; 459 break; 460 461 case SDL_KEYDOWN: 462 case SDL_KEYUP: 463 updateKeyboard(&event.key); 464 break; 465 466 case SDL_MOUSEMOTION: 467 _mouse.updateMotion(&event.motion); 468 break; 469 470 case SDL_MOUSEBUTTONUP: 471 case SDL_MOUSEBUTTONDOWN: 472 _mouse.updateButtons(&event.button); 473 break; 474 475 case SDL_MOUSEWHEEL: 476 _mouse.updateWheel(&event.wheel); 477 break; 478 479 default: 480 break; 481 } 482 } 483 484 void updateKeyboard(const(SDL_KeyboardEvent*) event) 485 { 486 // ignore key-repeat 487 if (event.repeat != 0) 488 return; 489 490 switch (event.type) 491 { 492 case SDL_KEYDOWN: 493 assert(event.state == SDL_PRESSED); 494 _keyboard.markKeyAsPressed(event.keysym.scancode); 495 break; 496 497 case SDL_KEYUP: 498 assert(event.state == SDL_RELEASED); 499 _keyboard.markKeyAsReleased(event.keysym.scancode); 500 break; 501 502 default: 503 break; 504 } 505 } 506 } 507 } 508 509 extern(C) private nothrow 510 { 511 void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message) 512 { 513 try 514 { 515 SDL2 sdl2 = cast(SDL2)userData; 516 517 try 518 sdl2.onLogMessage(category, priority, message); 519 catch (Exception e) 520 { 521 // got exception while logging, ignore it 522 } 523 } 524 catch (Throwable e) 525 { 526 // No Throwable is supposed to cross C callbacks boundaries 527 // Crash immediately 528 exit(-1); 529 } 530 } 531 532 SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData) 533 { 534 try 535 { 536 SDL2 sdl2 = cast(SDL2)userData; 537 538 try 539 return sdl2.onLogSDLAssertion(data); 540 catch (Exception e) 541 { 542 // got exception while logging, ignore it 543 } 544 } 545 catch (Throwable e) 546 { 547 // No Throwable is supposed to cross C callbacks boundaries 548 // Crash immediately 549 exit(-1); 550 } 551 return SDL_ASSERTION_ALWAYS_IGNORE; 552 } 553 } 554 555 final class SDL2DisplayMode 556 { 557 public 558 { 559 this(int modeIndex, SDL_DisplayMode mode) 560 { 561 _modeIndex = modeIndex; 562 _mode = mode; 563 } 564 565 override string toString() 566 { 567 return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)", 568 _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format); 569 } 570 } 571 572 private 573 { 574 int _modeIndex; 575 SDL_DisplayMode _mode; 576 } 577 } 578 579 final class SDL2VideoDisplay 580 { 581 public 582 { 583 this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes) 584 { 585 _displayindex = displayindex; 586 _bounds = bounds; 587 _availableModes = availableModes; 588 } 589 590 const(SDL2DisplayMode[]) availableModes() pure const nothrow 591 { 592 return _availableModes; 593 } 594 595 SDL_Point dimension() pure const nothrow 596 { 597 return SDL_Point(_bounds.w, _bounds.h); 598 } 599 600 SDL_Rect bounds() pure const nothrow 601 { 602 return _bounds; 603 } 604 605 override string toString() 606 { 607 string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex, 608 _bounds.x, _bounds.y, _bounds.w, _bounds.h); 609 foreach (mode; _availableModes) 610 res ~= format(" - %s\n", mode); 611 return res; 612 } 613 } 614 615 private 616 { 617 int _displayindex; 618 SDL2DisplayMode[] _availableModes; 619 SDL_Rect _bounds; 620 } 621 } 622 623 /// Crash if the GC is running. 624 /// Useful in destructors to avoid reliance GC resource release. 625 package void ensureNotInGC(string resourceName) nothrow 626 { 627 import core.exception; 628 try 629 { 630 import core.memory; 631 cast(void) GC.malloc(1); // not ideal since it allocates 632 return; 633 } 634 catch(InvalidMemoryOperationError e) 635 { 636 637 import core.stdc.stdio; 638 fprintf(stderr, "Error: clean-up of %s incorrectly depends on destructors called by the GC.\n", resourceName.ptr); 639 assert(false); 640 } 641 }