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