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