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