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