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         const(char)[][] getVideoDrivers()
301         {
302             const int numDrivers = SDL_GetNumVideoDrivers();
303             const(char)[][] res;
304             res.length = numDrivers;
305             for(int i = 0; i < numDrivers; ++i)
306                 res[i] = fromStringz(SDL_GetVideoDriver(i));
307             return res;
308         }
309 
310         /// Returns: Platform name.
311         const(char)[] getPlatform()
312         {
313             return fromStringz(SDL_GetPlatform());
314         }
315 
316         /// Returns: L1 cacheline size in bytes.
317         int getL1LineSize()
318         {
319             int res = SDL_GetCPUCacheLineSize();
320             if (res <= 0)
321                 res = 64;
322             return res;
323         }
324 
325         /// Returns: number of CPUs.
326         int getCPUCount()
327         {
328             int res = SDL_GetCPUCount();
329             if (res <= 0)
330                 res = 1;
331             return res;
332         }
333 
334         /// Returns: A path suitable for writing configuration files, saved games, etc...
335         /// See_also: $(LINK http://wiki.libsdl.org/SDL_GetPrefPath)
336         /// Throws: $(D SDL2Exception) on error.
337         const(char)[] getPrefPath(string orgName, string applicationName)
338         {
339             char* basePath = SDL_GetPrefPath(toStringz(orgName), toStringz(applicationName));
340             if (basePath != null)
341             {
342                 const(char)[] result = fromStringz(basePath);
343                 SDL_free(basePath);
344                 return result;
345             }
346             else
347             {
348                 throwSDL2Exception("SDL_GetPrefPath");
349                 return null; // unreachable
350             }
351         }
352     }
353 
354     package
355     {
356         Logger _logger;
357 
358         // exception mechanism that shall be used by every module here
359         void throwSDL2Exception(string callThatFailed)
360         {
361             string message = format("%s failed: %s", callThatFailed, getErrorString());
362             throw new SDL2Exception(message);
363         }
364 
365         // return last SDL error and clears it
366         const(char)[] getErrorString()
367         {
368             const(char)* message = SDL_GetError();
369             SDL_ClearError(); // clear error
370             return fromStringz(message);
371         }
372     }
373 
374     private
375     {
376         bool _SDL2LoggingRedirected;
377         SDL_LogOutputFunction _previousLogCallback;
378         void* _previousLogUserdata;
379 
380 
381         bool _SDLInitialized;
382 
383         // all created windows are keeped in this map
384         // to be able to dispatch event
385         SDL2Window[uint] _knownWindows;
386 
387         // SDL_QUIT was received
388         bool _quitWasRequested = false;
389 
390         // Holds keyboard state
391         SDL2Keyboard _keyboard;
392 
393         // Holds mouse state
394         SDL2Mouse _mouse;
395 
396         void onLogMessage(int category, SDL_LogPriority priority, const(char)* message)
397         {
398             static string readablePriority(SDL_LogPriority priority) pure
399             {
400                 switch(priority)
401                 {
402                     case SDL_LOG_PRIORITY_VERBOSE  : return "verbose";
403                     case SDL_LOG_PRIORITY_DEBUG    : return "debug";
404                     case SDL_LOG_PRIORITY_INFO     : return "info";
405                     case SDL_LOG_PRIORITY_WARN     : return "warn";
406                     case SDL_LOG_PRIORITY_ERROR    : return "error";
407                     case SDL_LOG_PRIORITY_CRITICAL : return "critical";
408                     default                        : return "unknown";
409                 }
410             }
411 
412             static string readableCategory(SDL_LogPriority priority) pure
413             {
414                 switch(priority)
415                 {
416                     case SDL_LOG_CATEGORY_APPLICATION : return "application";
417                     case SDL_LOG_CATEGORY_ERROR       : return "error";
418                     case SDL_LOG_CATEGORY_SYSTEM      : return "system";
419                     case SDL_LOG_CATEGORY_AUDIO       : return "audio";
420                     case SDL_LOG_CATEGORY_VIDEO       : return "video";
421                     case SDL_LOG_CATEGORY_RENDER      : return "render";
422                     case SDL_LOG_CATEGORY_INPUT       : return "input";
423                     default                           : return "unknown";
424                 }
425             }
426 
427             string formattedMessage = format("SDL (category %s, priority %s): %s",
428                                              readableCategory(category),
429                                              readablePriority(priority),
430                                              fromStringz(message));
431 
432             if (priority == SDL_LOG_PRIORITY_WARN)
433                 _logger.warning(formattedMessage);
434             else if (priority == SDL_LOG_PRIORITY_ERROR ||  priority == SDL_LOG_PRIORITY_CRITICAL)
435                 _logger.error(formattedMessage);
436             else
437                 _logger.info(formattedMessage);
438         }
439 
440         SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata)
441         {
442             _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum);
443 
444             debug
445                 return SDL_ASSERTION_ABORT; // crash in debug mode
446             else
447                 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release
448         }
449 
450         // update state based on event
451         // TODO: add joystick state
452         //       add haptic state
453         void updateState(const (SDL_Event*) event)
454         {
455             switch(event.type)
456             {
457                 case SDL_QUIT:
458                     _quitWasRequested = true;
459                     break;
460 
461                 case SDL_KEYDOWN:
462                 case SDL_KEYUP:
463                     updateKeyboard(&event.key);
464                     break;
465 
466                 case SDL_MOUSEMOTION:
467                     _mouse.updateMotion(&event.motion);
468                 break;
469 
470                 case SDL_MOUSEBUTTONUP:
471                 case SDL_MOUSEBUTTONDOWN:
472                     _mouse.updateButtons(&event.button);
473                 break;
474 
475                 case SDL_MOUSEWHEEL:
476                     _mouse.updateWheel(&event.wheel);
477                 break;
478 
479                 default:
480                     break;
481             }
482         }
483 
484         void updateKeyboard(const(SDL_KeyboardEvent*) event)
485         {
486             // ignore key-repeat
487             if (event.repeat != 0)
488                 return;
489 
490             switch (event.type)
491             {
492                 case SDL_KEYDOWN:
493                     assert(event.state == SDL_PRESSED);
494                     _keyboard.markKeyAsPressed(event.keysym.scancode);
495                     break;
496 
497                 case SDL_KEYUP:
498                     assert(event.state == SDL_RELEASED);
499                     _keyboard.markKeyAsReleased(event.keysym.scancode);
500                     break;
501 
502                 default:
503                     break;
504             }
505         }
506     }
507 }
508 
509 extern(C) private nothrow
510 {
511     void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message)
512     {
513         try
514         {
515             SDL2 sdl2 = cast(SDL2)userData;
516 
517             try
518                 sdl2.onLogMessage(category, priority, message);
519             catch (Exception e)
520             {
521                 // got exception while logging, ignore it
522             }
523         }
524         catch (Throwable e)
525         {
526             // No Throwable is supposed to cross C callbacks boundaries
527             // Crash immediately
528             exit(-1);
529         }
530     }
531 
532     SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData)
533     {
534         try
535         {
536             SDL2 sdl2 = cast(SDL2)userData;
537 
538             try
539                 return sdl2.onLogSDLAssertion(data);
540             catch (Exception e)
541             {
542                 // got exception while logging, ignore it
543             }
544         }
545         catch (Throwable e)
546         {
547             // No Throwable is supposed to cross C callbacks boundaries
548             // Crash immediately
549             exit(-1);
550         }
551         return SDL_ASSERTION_ALWAYS_IGNORE;
552     }
553 }
554 
555 final class SDL2DisplayMode
556 {
557     public
558     {
559         this(int modeIndex, SDL_DisplayMode mode)
560         {
561             _modeIndex = modeIndex;
562             _mode = mode;
563         }
564 
565         override string toString()
566         {
567             return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)",
568                           _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format);
569         }
570     }
571 
572     private
573     {
574         int _modeIndex;
575         SDL_DisplayMode _mode;
576     }
577 }
578 
579 final class SDL2VideoDisplay
580 {
581     public
582     {
583         this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes)
584         {
585             _displayindex = displayindex;
586             _bounds = bounds;
587             _availableModes = availableModes;
588         }
589 
590         const(SDL2DisplayMode[]) availableModes() pure const nothrow
591         {
592             return _availableModes;
593         }
594 
595         SDL_Point dimension() pure const nothrow
596         {
597             return SDL_Point(_bounds.w, _bounds.h);
598         }
599 
600         SDL_Rect bounds() pure const nothrow
601         {
602             return _bounds;
603         }
604 
605         override string toString()
606         {
607             string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex,
608                                 _bounds.x, _bounds.y, _bounds.w, _bounds.h);
609             foreach (mode; _availableModes)
610                 res ~= format("  - %s\n", mode);
611             return res;
612         }
613     }
614 
615     private
616     {
617         int _displayindex;
618         SDL2DisplayMode[] _availableModes;
619         SDL_Rect _bounds;
620     }
621 }
622 
623 /// Crash if the GC is running.
624 /// Useful in destructors to avoid reliance GC resource release.
625 package void ensureNotInGC(string resourceName) nothrow
626 {
627     import core.exception;
628     try
629     {
630         import core.memory;
631         cast(void) GC.malloc(1); // not ideal since it allocates
632         return;
633     }
634     catch(InvalidMemoryOperationError e)
635     {
636 
637         import core.stdc.stdio;
638         fprintf(stderr, "Error: clean-up of %s incorrectly depends on destructors called by the GC.\n", resourceName.ptr);
639         assert(false);
640     }
641 }