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