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.logger; 14 15 import gfm.core.text, 16 gfm.math.vector, 17 gfm.math.box, 18 gfm.sdl2.renderer, 19 gfm.sdl2.window, 20 gfm.sdl2.keyboard; 21 22 /// The one exception type thrown in this wrapper. 23 /// A failing SDL function should <b>always</b> throw a $(D SDL2Exception). 24 class SDL2Exception : Exception 25 { 26 public 27 { 28 this(string msg) 29 { 30 super(msg); 31 } 32 } 33 } 34 35 /// Owns both the loader, logging, keyboard state... 36 /// This object is passed around to other SDL wrapper objects 37 /// to ensure library loading. 38 final class SDL2 39 { 40 public 41 { 42 /// Load SDL2 library, redirect logging to our logger. 43 /// You can pass a null logger if you don't want logging. 44 /// Throws: $(D SDL2Exception) on error. 45 /// TODO: Custom SDL assertion handler. 46 this(Logger logger) 47 { 48 _logger = logger is null ? new NullLogger() : logger; 49 _SDLInitialized = false; 50 _SDL2LoggingRedirected = false; 51 try 52 { 53 DerelictSDL2.load(); 54 } 55 catch(DerelictException e) 56 { 57 throw new SDL2Exception(e.msg); 58 } 59 60 // enable all logging, and pipe it to our own logger object 61 { 62 SDL_LogGetOutputFunction(_previousLogCallback, &_previousLogUserdata); 63 SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); 64 SDL_LogSetOutputFunction(&loggingCallbackSDL, cast(void*)this); 65 66 SDL_SetAssertionHandler(&assertCallbackSDL, cast(void*)this); 67 _SDL2LoggingRedirected = true; 68 } 69 70 if (0 != SDL_Init(0)) 71 throwSDL2Exception("SDL_Init"); 72 73 _logger.infof("Platform: %s, %s CPU, L1 cacheline size: %sb", getPlatform(), getCPUCount(), getL1LineSize()); 74 75 subSystemInit(SDL_INIT_TIMER); 76 subSystemInit(SDL_INIT_VIDEO); 77 subSystemInit(SDL_INIT_JOYSTICK); 78 subSystemInit(SDL_INIT_AUDIO); 79 subSystemInit(SDL_INIT_HAPTIC); 80 81 _logger.infof("Available drivers: %s", join(getVideoDrivers(), ", ")); 82 _logger.infof("Running using video driver: %s", sanitizeUTF8(SDL_GetCurrentVideoDriver(), _logger, "SDL_GetCurrentVideoDriver")); 83 84 int numDisplays = SDL_GetNumVideoDisplays(); 85 86 _logger.infof("%s video display(s) detected.", numDisplays); 87 88 _keyboard = new SDL2Keyboard(this); 89 } 90 91 /// Releases the SDL library and all resources. 92 /// All resources should have been released at this point, 93 /// since you won't be able to call any SDL function afterwards. 94 void close() 95 { 96 // restore previously set logging function 97 if (_SDL2LoggingRedirected) 98 { 99 SDL_LogSetOutputFunction(_previousLogCallback, _previousLogUserdata); 100 _SDL2LoggingRedirected = false; 101 102 SDL_SetAssertionHandler(null, cast(void*)this); 103 } 104 105 if (_SDLInitialized) 106 { 107 SDL_Quit(); 108 _SDLInitialized = false; 109 } 110 111 if (DerelictSDL2.isLoaded()) 112 DerelictSDL2.unload(); 113 } 114 115 ~this() 116 { 117 close(); 118 } 119 120 /// Returns: Available displays information. 121 /// Throws: $(D SDL2Exception) on error. 122 SDL2VideoDisplay[] getDisplays() 123 { 124 int numDisplays = SDL_GetNumVideoDisplays(); 125 126 SDL2VideoDisplay[] availableDisplays; 127 128 for (int displayIndex = 0; displayIndex < numDisplays; ++displayIndex) 129 { 130 SDL_Rect rect; 131 int res = SDL_GetDisplayBounds(displayIndex, &rect); 132 if (res != 0) 133 throwSDL2Exception("SDL_GetDisplayBounds"); 134 135 box2i bounds = box2i(rect.x, rect.y, rect.x + rect.w, rect.y + rect.h); 136 SDL2DisplayMode[] availableModes; 137 138 int numModes = SDL_GetNumDisplayModes(displayIndex); 139 for(int modeIndex = 0; modeIndex < numModes; ++modeIndex) 140 { 141 SDL_DisplayMode mode; 142 if (0 != SDL_GetDisplayMode(displayIndex, modeIndex, &mode)) 143 throwSDL2Exception("SDL_GetDisplayMode"); 144 145 availableModes ~= new SDL2DisplayMode(modeIndex, mode); 146 } 147 148 availableDisplays ~= new SDL2VideoDisplay(displayIndex, bounds, availableModes); 149 } 150 return availableDisplays; 151 } 152 153 /// Returns: Resolution of the first display. 154 /// Throws: $(D SDL2Exception) on error. 155 vec2i firstDisplaySize() 156 { 157 auto displays = getDisplays(); 158 if (displays.length == 0) 159 throw new SDL2Exception("no display"); 160 return displays[0].dimension(); 161 } 162 163 /// Returns: Available renderers information. 164 /// Throws: $(D SDL2Exception) on error. 165 SDL2RendererInfo[] getRenderersInfo() 166 { 167 SDL2RendererInfo[] res; 168 int num = SDL_GetNumRenderDrivers(); 169 if (num < 0) 170 throwSDL2Exception("SDL_GetNumRenderDrivers"); 171 172 for (int i = 0; i < num; ++i) 173 { 174 SDL_RendererInfo info; 175 int err = SDL_GetRenderDriverInfo(i, &info); 176 if (err != 0) 177 throwSDL2Exception("SDL_GetRenderDriverInfo"); 178 res ~= new SDL2RendererInfo(_logger, i, info); 179 } 180 return res; 181 } 182 183 /// Get next SDL event. 184 /// Input state gets updated and window callbacks are called too. 185 /// Returns: true if returned an event. 186 bool pollEvent(SDL_Event* event) 187 { 188 if (SDL_PollEvent(event) != 0) 189 { 190 updateState(event); 191 dispatchEvent(event); 192 return true; 193 } 194 else 195 return false; 196 } 197 198 /// Process all pending SDL events. 199 /// Input state gets updated and window callbacks are called too. 200 void processEvents() 201 { 202 SDL_Event event; 203 204 while(SDL_PollEvent(&event) != 0) 205 { 206 updateState(&event); 207 dispatchEvent(&event); 208 } 209 } 210 211 /// Returns: Keyboard state. 212 /// The keyboard state is updated by processEvents() and pollEvent(). 213 SDL2Keyboard keyboard() 214 { 215 return _keyboard; 216 } 217 218 /// Returns: true if an application termiantion has been requested. 219 bool wasQuitResquested() const 220 { 221 return _quitWasRequested; 222 } 223 224 /// Start text input. 225 void startTextInput() 226 { 227 SDL_StartTextInput(); 228 } 229 230 /// Stops text input. 231 void stopTextInput() 232 { 233 SDL_StopTextInput(); 234 } 235 236 /// Sets clipboard content. 237 /// Throws: $(D SDL2Exception) on error. 238 string setClipboard(string s) 239 { 240 int err = SDL_SetClipboardText(toStringz(s)); 241 if (err != 0) 242 throwSDL2Exception("SDL_SetClipboardText"); 243 return s; 244 } 245 246 /// Returns: Clipboard content. 247 /// Throws: $(D SDL2Exception) on error. 248 string getClipboard() 249 { 250 if (SDL_HasClipboardText() == SDL_FALSE) 251 return null; 252 253 const(char)* s = SDL_GetClipboardText(); 254 if (s is null) 255 throwSDL2Exception("SDL_GetClipboardText"); 256 257 return sanitizeUTF8(s, _logger, "SDL clipboard text"); 258 } 259 260 /// Returns: Available SDL video drivers. 261 string[] getVideoDrivers() 262 { 263 const int numDrivers = SDL_GetNumVideoDrivers(); 264 string[] res; 265 res.length = numDrivers; 266 for(int i = 0; i < numDrivers; ++i) 267 res[i] = sanitizeUTF8(SDL_GetVideoDriver(i), _logger, "SDL_GetVideoDriver"); 268 return res; 269 } 270 271 /// Returns: Platform name. 272 string getPlatform() 273 { 274 return sanitizeUTF8(SDL_GetPlatform(), _logger, "SDL_GetPlatform"); 275 } 276 277 /// Returns: L1 cacheline size in bytes. 278 int getL1LineSize() 279 { 280 int res = SDL_GetCPUCacheLineSize(); 281 if (res <= 0) 282 res = 64; 283 return res; 284 } 285 286 /// Returns: number of CPUs. 287 int getCPUCount() 288 { 289 int res = SDL_GetCPUCount(); 290 if (res <= 0) 291 res = 1; 292 return res; 293 } 294 } 295 296 package 297 { 298 Logger _logger; 299 300 // exception mechanism that shall be used by every module here 301 void throwSDL2Exception(string callThatFailed) 302 { 303 string message = format("%s failed: %s", callThatFailed, getErrorString()); 304 throw new SDL2Exception(message); 305 } 306 307 // return last SDL error and clears it 308 string getErrorString() 309 { 310 const(char)* message = SDL_GetError(); 311 SDL_ClearError(); // clear error 312 return sanitizeUTF8(message, _logger, "SDL error string"); 313 } 314 315 void registerWindow(SDL2Window window) 316 { 317 _knownWindows[window.id()] = window; 318 } 319 320 void unregisterWindow(SDL2Window window) 321 { 322 assert((window.id() in _knownWindows) !is null); 323 _knownWindows.remove(window.id()); 324 } 325 } 326 327 private 328 { 329 bool _SDL2LoggingRedirected; 330 SDL_LogOutputFunction _previousLogCallback; 331 void* _previousLogUserdata; 332 333 334 bool _SDLInitialized; 335 336 // all created windows are keeped in this map 337 // to be able to dispatch event 338 SDL2Window[uint] _knownWindows; 339 340 // SDL_QUIT was received 341 bool _quitWasRequested = false; 342 343 // hold keyboard state 344 SDL2Keyboard _keyboard; 345 346 bool subSystemInitialized(int subSystem) 347 { 348 int inited = SDL_WasInit(SDL_INIT_EVERYTHING); 349 return 0 != ( inited & subSystem ); 350 } 351 352 void subSystemInit(int flag) 353 { 354 if (!subSystemInitialized(flag)) 355 { 356 int res = SDL_InitSubSystem(flag); 357 if (0 != res) 358 throwSDL2Exception("SDL_InitSubSystem"); 359 } 360 } 361 362 void onLogMessage(int category, SDL_LogPriority priority, const(char)* message) 363 { 364 static string readablePriority(SDL_LogPriority priority) pure 365 { 366 switch(priority) 367 { 368 case SDL_LOG_PRIORITY_VERBOSE : return "verbose"; 369 case SDL_LOG_PRIORITY_DEBUG : return "debug"; 370 case SDL_LOG_PRIORITY_INFO : return "info"; 371 case SDL_LOG_PRIORITY_WARN : return "warn"; 372 case SDL_LOG_PRIORITY_ERROR : return "error"; 373 case SDL_LOG_PRIORITY_CRITICAL : return "critical"; 374 default : return "unknown"; 375 } 376 } 377 378 static string readableCategory(SDL_LogPriority priority) pure 379 { 380 switch(priority) 381 { 382 case SDL_LOG_CATEGORY_APPLICATION : return "application"; 383 case SDL_LOG_CATEGORY_ERROR : return "error"; 384 case SDL_LOG_CATEGORY_SYSTEM : return "system"; 385 case SDL_LOG_CATEGORY_AUDIO : return "audio"; 386 case SDL_LOG_CATEGORY_VIDEO : return "video"; 387 case SDL_LOG_CATEGORY_RENDER : return "render"; 388 case SDL_LOG_CATEGORY_INPUT : return "input"; 389 default : return "unknown"; 390 } 391 } 392 393 string formattedMessage = format("SDL (category %s, priority %s): %s", 394 readableCategory(category), 395 readablePriority(priority), 396 sanitizeUTF8(message, _logger, "SDL logging")); 397 398 if (priority == SDL_LOG_PRIORITY_WARN) 399 _logger.warning(formattedMessage); 400 else if (priority == SDL_LOG_PRIORITY_ERROR || priority == SDL_LOG_PRIORITY_CRITICAL) 401 _logger.error(formattedMessage); 402 else 403 _logger.info(formattedMessage); 404 } 405 406 SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata) 407 { 408 _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum); 409 410 debug 411 return SDL_ASSERTION_ABORT; // crash in debug mode 412 else 413 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release 414 } 415 416 // dispatch to relevant event callbacks 417 void dispatchEvent(const (SDL_Event*) event) 418 { 419 switch(event.type) 420 { 421 case SDL_WINDOWEVENT: 422 dispatchWindowEvent(&event.window); 423 break; 424 425 default: 426 break; 427 } 428 } 429 430 // update state based on event 431 // TODO: add mouse state 432 // add joystick state 433 // add haptic state 434 void updateState(const (SDL_Event*) event) 435 { 436 switch(event.type) 437 { 438 case SDL_QUIT: 439 _quitWasRequested = true; 440 break; 441 442 case SDL_KEYDOWN: 443 case SDL_KEYUP: 444 updateKeyboard(&event.key); 445 break; 446 447 default: 448 break; 449 } 450 } 451 452 // TODO: add window callbacks when pressing a key? 453 void updateKeyboard(const(SDL_KeyboardEvent*) event) 454 { 455 // ignore key-repeat 456 if (event.repeat != 0) 457 return; 458 459 switch (event.type) 460 { 461 case SDL_KEYDOWN: 462 assert(event.state == SDL_PRESSED); 463 _keyboard.markKeyAsPressed(event.keysym.scancode); 464 break; 465 466 case SDL_KEYUP: 467 assert(event.state == SDL_RELEASED); 468 _keyboard.markKeyAsReleased(event.keysym.scancode); 469 break; 470 471 default: 472 break; 473 } 474 } 475 476 // call callbacks that can be overriden by subclassing SDL2Window 477 void dispatchWindowEvent(const (SDL_WindowEvent*) windowEvent) 478 { 479 assert(windowEvent.type == SDL_WINDOWEVENT); 480 481 SDL2Window* window = (windowEvent.windowID in _knownWindows); 482 483 if (window is null) 484 { 485 _logger.warningf("Received a SDL event for an unknown window (id = %s)", windowEvent.windowID); 486 return; // no such id known, warning 487 } 488 489 switch (windowEvent.event) 490 { 491 case SDL_WINDOWEVENT_SHOWN: 492 window.onShow(); 493 break; 494 495 case SDL_WINDOWEVENT_HIDDEN: 496 window.onHide(); 497 break; 498 499 case SDL_WINDOWEVENT_EXPOSED: 500 window.onExposed(); 501 break; 502 503 case SDL_WINDOWEVENT_MOVED: 504 window.onMove(windowEvent.data1, windowEvent.data2); 505 break; 506 507 case SDL_WINDOWEVENT_RESIZED: 508 window.onResized(windowEvent.data1, windowEvent.data2); 509 break; 510 511 case SDL_WINDOWEVENT_SIZE_CHANGED: 512 window.onSizeChanged(); 513 break; 514 515 case SDL_WINDOWEVENT_MINIMIZED: 516 window.onMinimized(); 517 break; 518 519 case SDL_WINDOWEVENT_MAXIMIZED: 520 window.onMaximized(); 521 break; 522 523 case SDL_WINDOWEVENT_RESTORED: 524 window.onRestored(); 525 break; 526 527 case SDL_WINDOWEVENT_ENTER: 528 window.onEnter(); 529 break; 530 531 case SDL_WINDOWEVENT_LEAVE: 532 window.onLeave(); 533 break; 534 535 case SDL_WINDOWEVENT_FOCUS_GAINED: 536 window.onFocusGained(); 537 break; 538 539 case SDL_WINDOWEVENT_FOCUS_LOST: 540 window.onFocusLost(); 541 break; 542 543 case SDL_WINDOWEVENT_CLOSE: 544 window.onClose(); 545 break; 546 547 default: 548 // not a window event 549 break; 550 } 551 } 552 } 553 } 554 555 extern(C) private nothrow 556 { 557 void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message) 558 { 559 try 560 { 561 SDL2 sdl2 = cast(SDL2)userData; 562 563 try 564 sdl2.onLogMessage(category, priority, message); 565 catch (Exception e) 566 { 567 // got exception while logging, ignore it 568 } 569 } 570 catch (Throwable e) 571 { 572 // No Throwable is supposed to cross C callbacks boundaries 573 // Crash immediately 574 exit(-1); 575 } 576 } 577 578 SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData) 579 { 580 try 581 { 582 SDL2 sdl2 = cast(SDL2)userData; 583 584 try 585 return sdl2.onLogSDLAssertion(data); 586 catch (Exception e) 587 { 588 // got exception while logging, ignore it 589 } 590 } 591 catch (Throwable e) 592 { 593 // No Throwable is supposed to cross C callbacks boundaries 594 // Crash immediately 595 exit(-1); 596 } 597 return SDL_ASSERTION_ALWAYS_IGNORE; 598 } 599 } 600 601 final class SDL2DisplayMode 602 { 603 public 604 { 605 this(int modeIndex, SDL_DisplayMode mode) 606 { 607 _modeIndex = modeIndex; 608 _mode = mode; 609 } 610 611 override string toString() 612 { 613 return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)", 614 _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format); 615 } 616 } 617 618 private 619 { 620 int _modeIndex; 621 SDL_DisplayMode _mode; 622 } 623 } 624 625 final class SDL2VideoDisplay 626 { 627 public 628 { 629 this(int displayindex, box2i bounds, SDL2DisplayMode[] availableModes) 630 { 631 _displayindex = displayindex; 632 _bounds = bounds; 633 _availableModes = availableModes; 634 } 635 636 const(SDL2DisplayMode[]) availableModes() pure const nothrow 637 { 638 return _availableModes; 639 } 640 641 const(vec2i) dimension() pure const nothrow 642 { 643 return vec2i(_bounds.width, _bounds.height); 644 } 645 646 const(box2i) bounds() pure const nothrow 647 { 648 return _bounds; 649 } 650 651 override string toString() 652 { 653 string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex, 654 _bounds.min.x, _bounds.min.y, _bounds.width, _bounds.height); 655 foreach (mode; _availableModes) 656 res ~= format(" - %s\n", mode); 657 return res; 658 } 659 } 660 661 private 662 { 663 int _displayindex; 664 SDL2DisplayMode[] _availableModes; 665 box2i _bounds; 666 } 667 } 668