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 bindbc.sdl,
10        bindbc.sdl.image,
11        bindbc.loader;
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         /// To specify a minimum version of SDL2 you need to use
43         /// compile time versions (SDL_201, SDL_202, etc.)
44         /// See_also: $(LINK https://github.com/BindBC/bindbc-sdl)
45         /// Creating this object doesn't initialize any SDL subsystem!
46         /// Params:
47         ///     logger         = The logger to redirect logging to.
48         /// Throws: $(D SDL2Exception) on error.
49         /// See_also: $(LINK http://wiki.libsdl.org/SDL_Init), $(D subSystemInit)
50         this(Logger logger)
51         {
52             _logger = logger is null ? new NullLogger() : logger;
53             _SDLInitialized = false;
54             _SDL2LoggingRedirected = false;
55             const ret = loadSDL();
56             if(ret < sdlSupport)
57             {
58                 if(ret == SDLSupport.noLibrary)
59                     // Exception is used because SDL2Exception to be
60                     // correctly thrown needs initialized SDL library
61                     throw new Exception("SDL shared library failed to load");
62                 else if(SDLSupport.badLibrary)
63                     // One or more symbols failed to load. The likely cause is that the
64                     // shared library is for a lower version than bindbc-sdl was configured
65                     // to load (via SDL_201, SDL_202, etc.)
66                     throwSDL2Exception("One or more symbols of SDL shared library failed to load");
67                 throwSDL2Exception("The version of the SDL library on your system is too low. Please upgrade.");
68             }
69 
70             // enable all logging, and pipe it to our own logger object
71             {
72                 SDL_LogGetOutputFunction(_previousLogCallback, &_previousLogUserdata);
73                 SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
74                 SDL_LogSetOutputFunction(&loggingCallbackSDL, cast(void*)this);
75 
76                 SDL_SetAssertionHandler(&assertCallbackSDL, cast(void*)this);
77                 _SDL2LoggingRedirected = true;
78             }
79 
80             if (0 != SDL_Init(0))
81                 throwSDL2Exception("SDL_Init");
82 
83             _keyboard = new SDL2Keyboard(this);
84             _mouse = new SDL2Mouse(this);
85         }
86 
87         /// Releases the SDL library and all resources.
88         /// All resources should have been released at this point,
89         /// since you won't be able to call any SDL function afterwards.
90         /// See_also: $(LINK http://wiki.libsdl.org/SDL_Quit)
91         ~this()
92         {
93             // restore previously set logging function
94             if (_SDL2LoggingRedirected)
95             {
96                 debug ensureNotInGC("SDL2");
97                 SDL_LogSetOutputFunction(_previousLogCallback, _previousLogUserdata);
98                 _SDL2LoggingRedirected = false;
99 
100                 SDL_SetAssertionHandler(null, cast(void*)this);
101             }
102 
103             if (_SDLInitialized)
104             {
105                 debug ensureNotInGC("SDL2");
106                 SDL_Quit();
107                 _SDLInitialized = false;
108             }
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         alias getVideoDrivers = getDrivers!(SDL_GetNumVideoDrivers, SDL_GetVideoDriver);
307 
308         /// Returns: Available SDL audio drivers.
309         alias getAudioDrivers = getDrivers!(SDL_GetNumAudioDrivers, SDL_GetAudioDriver);
310 
311         /++
312         Returns: Available audio device names.
313         See_also: https://wiki.libsdl.org/SDL_GetAudioDeviceName
314         Bugs: SDL2 currently doesn't support recording, so it's best to
315               call this without any arguments.
316         +/
317         const(char)[][] getAudioDevices(int type = 0)
318         {
319             const(int) numDevices = SDL_GetNumAudioDevices(type);
320 
321             const(char)[][] res;
322             foreach (i; 0..numDevices)
323             {
324                 res ~= fromStringz(SDL_GetAudioDeviceName(i, type));
325             }
326 
327             return res;
328         }
329 
330         /// Returns: Platform name.
331         const(char)[] getPlatform()
332         {
333             return fromStringz(SDL_GetPlatform());
334         }
335 
336         /// Returns: L1 cacheline size in bytes.
337         int getL1LineSize()
338         {
339             int res = SDL_GetCPUCacheLineSize();
340             if (res <= 0)
341                 res = 64;
342             return res;
343         }
344 
345         /// Returns: number of CPUs.
346         int getCPUCount()
347         {
348             int res = SDL_GetCPUCount();
349             if (res <= 0)
350                 res = 1;
351             return res;
352         }
353 
354         static if(sdlSupport >= SDLSupport.sdl201)
355         {
356             /// Returns: A path suitable for writing configuration files, saved games, etc...
357             /// See_also: $(LINK http://wiki.libsdl.org/SDL_GetPrefPath)
358             /// Throws: $(D SDL2Exception) on error.
359             const(char)[] getPrefPath(string orgName, string applicationName)
360             {
361                 char* basePath = SDL_GetPrefPath(toStringz(orgName), toStringz(applicationName));
362                 if (basePath != null)
363                 {
364                     const(char)[] result = fromStringz(basePath);
365                     SDL_free(basePath);
366                     return result;
367                 }
368                 else
369                 {
370                     throwSDL2Exception("SDL_GetPrefPath");
371                     return null; // unreachable
372                 }
373             }
374         }
375     }
376 
377     package
378     {
379         Logger _logger;
380 
381         // exception mechanism that shall be used by every module here
382         void throwSDL2Exception(string callThatFailed)
383         {
384             string message = format("%s failed: %s", callThatFailed, getErrorString());
385             throw new SDL2Exception(message);
386         }
387 
388         // return last SDL error and clears it
389         const(char)[] getErrorString()
390         {
391             const(char)* message = SDL_GetError();
392             SDL_ClearError(); // clear error
393             return fromStringz(message);
394         }
395     }
396 
397     private
398     {
399         bool _SDL2LoggingRedirected;
400         SDL_LogOutputFunction _previousLogCallback;
401         void* _previousLogUserdata;
402 
403 
404         bool _SDLInitialized;
405 
406         // all created windows are keeped in this map
407         // to be able to dispatch event
408         SDL2Window[uint] _knownWindows;
409 
410         // SDL_QUIT was received
411         bool _quitWasRequested = false;
412 
413         // Holds keyboard state
414         SDL2Keyboard _keyboard;
415 
416         // Holds mouse state
417         SDL2Mouse _mouse;
418 
419         const(char)[][] getDrivers(alias numFn, alias elemFn)()
420         {
421             const(int) numDrivers = numFn();
422             const(char)[][] res;
423             res.length = numDrivers;
424             foreach (i; 0..numDrivers)
425             {
426                 res[i] = fromStringz(elemFn(i));
427             }
428             return res;
429         }
430 
431         void onLogMessage(int category, SDL_LogPriority priority, const(char)* message)
432         {
433             static string readablePriority(SDL_LogPriority priority) pure
434             {
435                 switch(priority)
436                 {
437                     case SDL_LOG_PRIORITY_VERBOSE  : return "verbose";
438                     case SDL_LOG_PRIORITY_DEBUG    : return "debug";
439                     case SDL_LOG_PRIORITY_INFO     : return "info";
440                     case SDL_LOG_PRIORITY_WARN     : return "warn";
441                     case SDL_LOG_PRIORITY_ERROR    : return "error";
442                     case SDL_LOG_PRIORITY_CRITICAL : return "critical";
443                     default                        : return "unknown";
444                 }
445             }
446 
447             static string readableCategory(int category) pure
448             {
449                 switch(category)
450                 {
451                     case SDL_LOG_CATEGORY_APPLICATION : return "application";
452                     case SDL_LOG_CATEGORY_ERROR       : return "error";
453                     case SDL_LOG_CATEGORY_SYSTEM      : return "system";
454                     case SDL_LOG_CATEGORY_AUDIO       : return "audio";
455                     case SDL_LOG_CATEGORY_VIDEO       : return "video";
456                     case SDL_LOG_CATEGORY_RENDER      : return "render";
457                     case SDL_LOG_CATEGORY_INPUT       : return "input";
458                     default                           : return "unknown";
459                 }
460             }
461 
462             string formattedMessage = format("SDL (category %s, priority %s): %s",
463                                              readableCategory(category),
464                                              readablePriority(priority),
465                                              fromStringz(message));
466 
467             if (priority == SDL_LOG_PRIORITY_WARN)
468                 _logger.warning(formattedMessage);
469             else if (priority == SDL_LOG_PRIORITY_ERROR ||  priority == SDL_LOG_PRIORITY_CRITICAL)
470                 _logger.error(formattedMessage);
471             else
472                 _logger.info(formattedMessage);
473         }
474 
475         SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata)
476         {
477             _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum);
478 
479             debug
480                 return SDL_ASSERTION_ABORT; // crash in debug mode
481             else
482                 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release
483         }
484 
485         // update state based on event
486         // TODO: add joystick state
487         //       add haptic state
488         void updateState(const (SDL_Event*) event)
489         {
490             switch(event.type)
491             {
492                 case SDL_QUIT:
493                     _quitWasRequested = true;
494                     break;
495 
496                 case SDL_KEYDOWN:
497                 case SDL_KEYUP:
498                     updateKeyboard(&event.key);
499                     break;
500 
501                 case SDL_MOUSEMOTION:
502                     _mouse.updateMotion(&event.motion);
503                 break;
504 
505                 case SDL_MOUSEBUTTONUP:
506                 case SDL_MOUSEBUTTONDOWN:
507                     _mouse.updateButtons(&event.button);
508                 break;
509 
510                 case SDL_MOUSEWHEEL:
511                     _mouse.updateWheel(&event.wheel);
512                 break;
513 
514                 default:
515                     break;
516             }
517         }
518 
519         void updateKeyboard(const(SDL_KeyboardEvent*) event)
520         {
521             // ignore key-repeat
522             if (event.repeat != 0)
523                 return;
524 
525             switch (event.type)
526             {
527                 case SDL_KEYDOWN:
528                     assert(event.state == SDL_PRESSED);
529                     _keyboard.markKeyAsPressed(event.keysym.scancode);
530                     break;
531 
532                 case SDL_KEYUP:
533                     assert(event.state == SDL_RELEASED);
534                     _keyboard.markKeyAsReleased(event.keysym.scancode);
535                     break;
536 
537                 default:
538                     break;
539             }
540         }
541     }
542 }
543 
544 extern(C) private nothrow
545 {
546     void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message)
547     {
548         try
549         {
550             SDL2 sdl2 = cast(SDL2)userData;
551 
552             try
553                 sdl2.onLogMessage(category, priority, message);
554             catch (Exception e)
555             {
556                 // got exception while logging, ignore it
557             }
558         }
559         catch (Throwable e)
560         {
561             // No Throwable is supposed to cross C callbacks boundaries
562             // Crash immediately
563             exit(-1);
564         }
565     }
566 
567     SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData)
568     {
569         try
570         {
571             SDL2 sdl2 = cast(SDL2)userData;
572 
573             try
574                 return sdl2.onLogSDLAssertion(data);
575             catch (Exception e)
576             {
577                 // got exception while logging, ignore it
578             }
579         }
580         catch (Throwable e)
581         {
582             // No Throwable is supposed to cross C callbacks boundaries
583             // Crash immediately
584             exit(-1);
585         }
586         return SDL_ASSERTION_ALWAYS_IGNORE;
587     }
588 }
589 
590 final class SDL2DisplayMode
591 {
592     public
593     {
594         this(int modeIndex, SDL_DisplayMode mode)
595         {
596             _modeIndex = modeIndex;
597             _mode = mode;
598         }
599 
600         override string toString()
601         {
602             return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)",
603                           _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format);
604         }
605     }
606 
607     private
608     {
609         int _modeIndex;
610         SDL_DisplayMode _mode;
611     }
612 }
613 
614 final class SDL2VideoDisplay
615 {
616     public
617     {
618         this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes)
619         {
620             _displayindex = displayindex;
621             _bounds = bounds;
622             _availableModes = availableModes;
623         }
624 
625         const(SDL2DisplayMode[]) availableModes() pure const nothrow
626         {
627             return _availableModes;
628         }
629 
630         SDL_Point dimension() pure const nothrow
631         {
632             return SDL_Point(_bounds.w, _bounds.h);
633         }
634 
635         SDL_Rect bounds() pure const nothrow
636         {
637             return _bounds;
638         }
639 
640         override string toString()
641         {
642             string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex,
643                                 _bounds.x, _bounds.y, _bounds.w, _bounds.h);
644             foreach (mode; _availableModes)
645                 res ~= format("  - %s\n", mode);
646             return res;
647         }
648     }
649 
650     private
651     {
652         int _displayindex;
653         SDL2DisplayMode[] _availableModes;
654         SDL_Rect _bounds;
655     }
656 }
657 
658 /// Crash if the GC is running.
659 /// Useful in destructors to avoid reliance GC resource release.
660 package void ensureNotInGC(string resourceName) nothrow
661 {
662     import core.exception;
663     try
664     {
665         import core.memory;
666         cast(void) GC.malloc(1); // not ideal since it allocates
667         return;
668     }
669     catch(InvalidMemoryOperationError e)
670     {
671 
672         import core.stdc.stdio;
673         fprintf(stderr, "Error: clean-up of %s incorrectly depends on destructors called by the GC.\n", resourceName.ptr);
674         assert(false);
675     }
676 }