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