1 module gfmod.opengl.opengl;
2 
3 import core.stdc.stdlib;
4 
5 import std..string,
6        std.conv,
7        std.exception,
8        std.array,
9        std.algorithm;
10 
11 import derelict.util.exception,
12        derelict.opengl3.gl3;
13 
14 import std.experimental.logger;
15 
16 import gfmod.core.text,
17        gfmod.opengl.textureunit;
18 
19 /// The one exception type thrown in this wrapper.
20 /// A failing OpenGL function should <b>always</b> throw an $(D OpenGLException).
21 class OpenGLException : Exception
22 {
23     public
24     {
25         @safe pure nothrow this(string message, string file =__FILE__, size_t line = __LINE__, Throwable next = null)
26         {
27             super(message, file, line, next);
28         }
29     }
30 }
31 
32 /// This object is passed around to other OpenGL wrapper objects
33 /// to ensure library loading.
34 /// Create one to use OpenGL.
35 final class OpenGL
36 {
37     public
38     {
39         enum Vendor
40         {
41             AMD,
42             Apple, // for software rendering aka no driver
43             Intel,
44             Mesa,
45             Microsoft, // for "GDI generic" aka no driver
46             NVIDIA,
47             other
48         }
49 
50         /// Load OpenGL library, redirect debug output to our logger.
51         /// You can pass a null logger if you don't want logging.
52         /// Throws: $(D OpenGLException) on error.
53         this(Logger logger) @system
54         {
55             _logger = logger is null ? new NullLogger() : logger;
56             try
57             {
58                 DerelictGL3.load();
59             }
60             catch(DerelictException e)
61             {
62                 throw new OpenGLException(e.msg, __FILE__, __LINE__, e);
63             }
64             
65 
66             //DerelictGL.load(); // load deprecated functions too
67 
68             _logger.infof("OpenGL loaded, version %s", DerelictGL3.loadedVersion());
69 
70             // do not log here since unimportant errors might happen:
71             // no context is necessarily created at this point
72             getLimits(false); 
73 
74             _textureUnits = new TextureUnits(this);
75         }
76 
77         /// Unload the OpenGL library.
78         ///
79         /// Also checks for and logs any OpenGL objects that were not deleted to detect
80         /// leaks.
81         ~this()
82         {
83             logGLLeaks();
84             close();
85         }
86 
87         /// Assume any existing OpenGL objects are leaks and write them into the log.
88         ///
89         /// Should only be called when no GL objects should exist, e.g. after deleting
90         /// all GL objects. Called automatically by the OpenGL destructor.
91         void logGLLeaks()
92         {
93             // Yeah, this is a pretty ugly hack. But it works.
94             GLuint maxID = 100000;
95             foreach(id; 0 .. maxID)
96             {
97                 void leak(string type, GLuint id)
98                 {
99                     _logger.errorf("Leaked a %s OpenGL object. Handle: %s", type, id); 
100                 }
101                 if(glIsTexture(id))           { leak("texture",       id); }
102                 else if(glIsBuffer(id))       { leak("buffer",        id); }
103                 else if(glIsFramebuffer(id))  { leak("framebuffer",   id); }
104                 else if(glIsRenderbuffer(id)) { leak("renderbuffer",  id); }
105                 else if(glIsVertexArray(id))  { leak("vertex array",  id); }
106                 else if(glIsShader(id))       { leak("shader handle", id); }
107                 else if(glIsQuery(id))        { leak("query",         id); }
108                 else if(glIsProgram(id))      { leak("program",       id); }
109                 // These are not present in (our base requirement) OpenGL 3.0 so we
110                 // check if they're available.
111                 else if(glIsSampler !is null && glIsSampler(id))
112                 {
113                     leak("sampler", id); 
114                 }
115                 else if(glIsTransformFeedback !is null && glIsTransformFeedback(id))
116                 {
117                     leak("transform feedback", id); 
118                 }
119                 else if(glIsProgramPipeline !is null && glIsProgramPipeline(id))
120                 {
121                     leak("program pipeline", id); 
122                 }
123             }
124         }
125 
126         /// Returns: true if the OpenGL extension is supported.
127         bool supportsExtension(string extension)
128         {
129             foreach(s; _extensions)
130                 if (s == extension)
131                     return true;
132             return false;
133         }
134 
135         /// Reload OpenGL function pointers.
136         /// Once a first OpenGL context has been created, 
137         /// you should call reload() to get the context you want.
138         GLVersion reload() @system
139         {
140             GLVersion result;
141             try
142             {
143                 result = DerelictGL3.reload();
144             }
145             catch(DerelictException e)
146             {
147                 throw new OpenGLException(e.msg, __FILE__, __LINE__, e);
148             }
149             
150             _logger.infof("OpenGL reloaded, version %s", DerelictGL3.loadedVersion());
151             _logger.infof("    Version: %s", getVersionString());
152             _logger.infof("    Renderer: %s", getRendererString());
153             _logger.infof("    Vendor: %s", getVendorString());
154             _logger.infof("    GLSL version: %s", getGLSLVersionString());
155 
156             runtimeCheck();
157 
158             // parse extensions
159             _extensions = getExtensionsStrings();
160             runtimeCheck();
161 
162             _logger.infof("    Extensions: %s found", _extensions.length);
163             _logger.infof("    - EXT_texture_filter_anisotropic is%s supported", EXT_texture_filter_anisotropic() ? "": " not");
164             _logger.infof("    - EXT_framebuffer_object is%s supported", EXT_framebuffer_object() ? "": " not");
165             _logger.info("Extensions:");
166             _logger.info(_extensions);
167             getLimits(true);
168             _textureUnits = new TextureUnits(this);
169 
170             debug
171             {
172                 // now that the context exists, pipe OpenGL output
173                 pipeOpenGLDebugOutput();
174             }
175 
176             return result;
177         }
178 
179         /// Releases the OpenGL dynamic library.
180         /// All resources should have been released at this point,
181         /// since you won't be able to call any OpenGL function afterwards.
182         void close()
183         {
184             DerelictGL3.unload();
185         }
186 
187         /// Check for pending OpenGL errors, log a message if there is.
188         /// Only for debug purpose since this check will be disabled in a release build.
189         void debugCheck()
190         {
191             debug
192             {
193                 GLint r = glGetError();
194                 if (r != GL_NO_ERROR)
195                 {
196                     flushGLErrors(); // flush other errors if any
197                     _logger.errorf("OpenGL error: %s", getErrorString(r));
198                     assert(false); // break here
199                 }
200             }
201         }
202 
203         /**
204          * Checks pending OpenGL errors.
205          *
206          * Returns: true if at least one OpenGL error was pending. OpenGL error status is cleared.
207          */
208         bool runtimeCheck(string file = __FILE__, int line = __LINE__) @trusted nothrow
209         {
210             GLint r = glGetError();
211             if (r != GL_NO_ERROR)
212             {
213                 string errorString = getErrorString(r);
214                 flushGLErrors(); // flush other errors if any
215                 _logger.warningf("GL error detected in runtimeCheck called "
216                                  "from file %s, line %s: ", file, line, errorString)
217                                  .assumeWontThrow;
218                 return false;
219             }
220             return true;
221         }
222 
223         /// Sanitize a GL string returned by glGetString/glGetStringi.
224         string sanitizeGLString(const(char)* sZ) 
225         {
226             import core.stdc..string: strlen;
227             if (sZ is null) { return "(unknown)"; }
228             // Need to copy the string as it's const
229             char[] text = sZ[0 .. strlen(sZ)].dup;
230             if(!text.sanitizeASCIIInPlace())
231             {
232                 _logger.warning("Invalid (non-ASCII) character in a GL string result");
233             }
234 
235             return text.assumeUnique();
236         }
237 
238 
239         /// Returns: OpenGL string returned by $(D glGetString).
240         /// See_also: $(WEB www.opengl.org/sdk/docs/man/xhtml/glGetString.xml)
241         string getString(GLenum name)
242         {
243             const(char)* sZ = glGetString(name);
244             runtimeCheck();
245             return sanitizeGLString(sZ);
246         }
247 
248         /// Returns: An array of OpenGL strings returned by $(D glGetStringi).
249         /// See_also: $(WEB https://www.opengl.org/wiki/GLAPI/glGetString)
250         string[] getStrings(GLenum pname, int numStrings)
251         {
252             auto result = new string[numStrings];
253             foreach(uint i, ref str; result)
254             {
255                 const(char)* sZ = glGetStringi(pname, i);
256                 // If error, return strings we got so far.
257                 if(!runtimeCheck()) { return result[0 .. i]; }
258 
259                 str = sanitizeGLString(sZ);
260             }
261 
262             return result;
263         }
264 
265         /// Returns: OpenGL version string, can be "major_number.minor_number" or 
266         ///          "major_number.minor_number.release_number".
267         string getVersionString()
268         {
269             return getString(GL_VERSION);
270         }
271 
272         /// Returns: The company responsible for this OpenGL implementation, so
273         ///          that you can plant a giant toxic mushroom below their office.
274         string getVendorString()
275         {
276             return getString(GL_VENDOR);
277         }
278 
279         /// Tries to detect the driver maker.
280         /// Returns: Identified vendor.
281         Vendor getVendor()
282         {
283             string s = getVendorString();
284             if (canFind(s, "AMD") || canFind(s, "ATI") || canFind(s, "Advanced Micro Devices"))
285                 return Vendor.AMD;
286             else if (canFind(s, "NVIDIA") || canFind(s, "nouveau") || canFind(s, "Nouveau"))
287                 return Vendor.NVIDIA;
288             else if (canFind(s, "Intel"))
289                 return Vendor.Intel;
290             else if (canFind(s, "Mesa"))
291                 return Vendor.Mesa;
292             else if (canFind(s, "Microsoft"))
293                 return Vendor.Microsoft;
294             else if (canFind(s, "Apple"))
295                 return Vendor.Apple;
296             else
297                 return Vendor.other;
298         }
299 
300         /// Returns: Name of the renderer. This name is typically specific 
301         ///          to a particular configuration of a hardware platform.
302         string getRendererString()
303         {
304             return getString(GL_RENDERER);
305         }
306 
307         /// Returns: GLSL version string, can be "major_number.minor_number" or 
308         ///          "major_number.minor_number.release_number".
309         string getGLSLVersionString()
310         {
311             return getString(GL_SHADING_LANGUAGE_VERSION);
312         }
313 
314 
315         /// Returns: An array of names of supported OpenGL extensions.
316         string[] getExtensionsStrings()
317         {
318             scope(exit) { runtimeCheck(); }
319             const numExtensions = getInteger(GL_NUM_EXTENSIONS, 0, true);
320             runtimeCheck();
321             return getStrings(GL_EXTENSIONS, numExtensions);
322         }
323 
324         /// Calls $(D glGetIntegerv) and gives back the requested integer.
325         /// Returns: true if $(D glGetIntegerv) succeeded.
326         /// See_also: $(WEB www.opengl.org/sdk/docs/man4/xhtml/glGet.xml).
327         /// Note: It is generally a bad idea to call $(D glGetSomething) since it might stall
328         ///       the OpenGL pipeline.
329         bool getInteger(GLenum pname, out int result,
330                        string file = __FILE__, int line = __LINE__) nothrow
331         {
332             GLint param;
333             glGetIntegerv(pname, &param);
334 
335             if (runtimeCheck())
336             {
337                 result = param;
338                 return true;
339             }
340             else
341                 return false;
342         }
343 
344         /// Returns: The requested integer returned by $(D glGetIntegerv) 
345         ///          or defaultValue if an error occured.
346         /// See_also: $(WEB www.opengl.org/sdk/docs/man4/xhtml/glGet.xml).        
347         int getInteger(GLenum pname, GLint defaultValue, bool logging, 
348                        string file = __FILE__, int line = __LINE__)
349         {
350             int result;
351 
352             if (getInteger(pname, result))
353             {
354                 return result;
355             }
356             else
357             {
358                 if (logging)
359                 {
360                     _logger.warningf("couldn't get OpenGL integer (called from " 
361                                      "file %s, line %s): ID %s", file, line, pname);
362                 }
363                 return defaultValue;
364             }
365         }
366 
367         /// Returns: The requested float returned by $(D glGetFloatv).
368         /// See_also: $(WEB www.opengl.org/sdk/docs/man4/xhtml/glGet.xml). 
369         /// Throws: $(D OpenGLException) if at least one OpenGL error was pending.
370         float getFloat(GLenum pname)
371         {
372             GLfloat res;
373             glGetFloatv(pname, &res);
374             runtimeCheck();
375             return res;
376         }
377 
378         /// Returns: The requested float returned by $(D glGetFloatv) 
379         ///          or defaultValue if an error occured.
380         /// See_also: $(WEB www.opengl.org/sdk/docs/man4/xhtml/glGet.xml).   
381         float getFloat(GLenum pname, GLfloat defaultValue, bool logging)
382         {
383             try
384             {
385                 return getFloat(pname);
386             }
387             catch(OpenGLException e)
388             {
389                 if (logging)
390                     _logger.warning(e.msg);
391                 return defaultValue;
392             }
393         }
394     }
395 
396     package
397     {
398         Logger _logger;
399 
400         static string getErrorString(GLint r) pure nothrow
401         {
402             switch(r)
403             {
404                 case GL_NO_ERROR:          return "GL_NO_ERROR";
405                 case GL_INVALID_ENUM:      return "GL_INVALID_ENUM";
406                 case GL_INVALID_VALUE:     return "GL_INVALID_VALUE";
407                 case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
408                 case GL_OUT_OF_MEMORY:     return "GL_OUT_OF_MEMORY";
409                 // case GL_TABLE_TOO_LARGE:   return "GL_TABLE_TOO_LARGE";
410                 // case GL_STACK_OVERFLOW:    return "GL_STACK_OVERFLOW";
411                 // case GL_STACK_UNDERFLOW:   return "GL_STACK_UNDERFLOW";
412                 default:                   return "Unknown OpenGL error";
413             }
414         }
415 
416     }
417 
418     public
419     {
420         /// Returns: Maximum texture size. 
421         ///          This value should be at least 512 for a conforming OpenGL implementation.
422         int maxTextureSize() pure const nothrow
423         {
424             return _maxTextureSize;
425         }
426 
427         /// Returns: Number of texture units.
428         int maxTextureUnits() pure const nothrow
429         {
430             return _maxTextureUnits;
431         }
432 
433         /// Returns: Number of texture image units usable in a fragment shader.
434         int maxFragmentTextureImageUnits() pure const nothrow
435         {
436             return _maxFragmentTextureImageUnits;
437         }
438 
439         /// Returns: Number of texture image units usable in a vertex shader.
440         int maxVertexImageUnits() pure const nothrow
441         {
442             return _maxVertexTextureImageUnits;
443         }
444 
445         /// Returns: Number of combined texture image units.
446         int maxCombinedImageUnits() pure const nothrow
447         {
448             return _maxCombinedTextureImageUnits;
449         }
450 
451         /// Returns: Maximum number of color attachments. This is the number of targets a fragment shader can output to.
452         /// You can rely on this number being at least 4 if MRT is supported.
453         int maxColorAttachments() pure const nothrow
454         {
455             return _maxColorAttachments;
456         }
457 
458         /// Returns: Texture units abstraction.
459         TextureUnits textureUnits() pure nothrow
460         {
461             return _textureUnits;
462         }
463 
464         /// Returns: Maximum value of anisotropic filter.
465         float maxTextureMaxAnisotropy() pure const nothrow
466         {
467             return _maxTextureMaxAnisotropy;
468         }
469     }
470 
471     private
472     {
473         string[] _extensions;
474         TextureUnits _textureUnits;
475         int _majorVersion = 1;
476         int _minorVersion = 1;
477         int _maxTextureSize;
478         int _maxTextureUnits; // number of conventional units, deprecated
479         int _maxFragmentTextureImageUnits; // max for fragment shader
480         int _maxVertexTextureImageUnits; // max for vertex shader
481         int _maxCombinedTextureImageUnits; // max total
482         int _maxColorAttachments;
483         float _maxTextureMaxAnisotropy;
484 
485         void getLimits(bool logging)
486         {
487             runtimeCheck();
488             // GL_MAJOR_VERSION/GL_MINOR_VERSION may not always work, so we parse version
489             // string.
490             auto verString = getVersionString().split()[0];
491  
492             import std.format;
493             bool failedParse = false;
494             // If this can't parse the version string, _majorVersion/_minorVersion will be 1.1
495             try if(formattedRead(verString, "%s.%s", &_minorVersion, &_majorVersion) < 2)
496             {
497                 failedParse = true;
498             }
499             catch(Exception e)
500             {
501                 failedParse = true;
502             }
503 
504             if(logging && failedParse)
505             {
506                 _logger.warningf("couldn't parse OpenGL version string. "
507                                  "Assuming version %s.%s", _majorVersion, _minorVersion);
508             }
509 
510             runtimeCheck();
511 
512             _maxTextureSize = getInteger(GL_MAX_TEXTURE_SIZE, 512, logging);
513             // For other textures, add calls to:
514             // GL_MAX_ARRAY_TEXTURE_LAYERS​, GL_MAX_3D_TEXTURE_SIZE​
515             _maxTextureUnits = getInteger(GL_MAX_TEXTURE_IMAGE_UNITS, 2, logging);
516 
517             _maxFragmentTextureImageUnits = getInteger(GL_MAX_TEXTURE_IMAGE_UNITS, 2, logging); // odd GL enum name because of legacy reasons (initially only fragment shader could access textures)
518             _maxVertexTextureImageUnits = getInteger(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, 2, logging);
519             _maxCombinedTextureImageUnits = getInteger(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, 2, logging);
520             // Get texture unit max for other shader stages with:
521             // GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS, GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS
522 
523             _maxColorAttachments = getInteger(GL_MAX_COLOR_ATTACHMENTS, 4, logging);
524 
525             if (EXT_texture_filter_anisotropic())
526                 _maxTextureMaxAnisotropy = getFloat(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, 1.0f, logging);
527             else
528                 _maxTextureMaxAnisotropy = 1.0f;
529         }
530 
531         // flush out OpenGL errors
532         void flushGLErrors() nothrow
533         {            
534             int timeout = 0;
535             while (++timeout <= 5) // avoid infinite loop in a no-driver situation
536             {
537                 GLint r = glGetError();
538                 if (r == GL_NO_ERROR)
539                     break;
540             }
541         }
542 
543         void pipeOpenGLDebugOutput()
544         {
545             if (KHR_debug())
546             {
547                 glDebugMessageCallback(&loggingCallbackOpenGL, cast(void*)this);
548 
549                 // enable all messages
550                 glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, null, GL_TRUE);
551 
552                 glEnable(GL_DEBUG_OUTPUT);
553                 //glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
554             }
555         }
556     }
557 }
558 
559 extern(System) private
560 {
561     // This callback can be called from multiple threads
562     // TODO synchronization for Log objects
563     nothrow void loggingCallbackOpenGL(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const(GLchar)* message, GLvoid* userParam)
564     {
565         try
566         {
567             OpenGL opengl = cast(OpenGL)userParam;
568 
569             try
570             {
571                 Logger logger = opengl._logger;
572 
573                 string ssource;
574                 switch (source)
575                 {
576                     case GL_DEBUG_SOURCE_API:             ssource = "API"; break;
577                     case GL_DEBUG_SOURCE_WINDOW_SYSTEM:   ssource = "window system"; break;
578                     case GL_DEBUG_SOURCE_SHADER_COMPILER: ssource = "shader compiler"; break;
579                     case GL_DEBUG_SOURCE_THIRD_PARTY:     ssource = "third party"; break;
580                     case GL_DEBUG_SOURCE_APPLICATION:     ssource = "application"; break;
581                     case GL_DEBUG_SOURCE_OTHER:           ssource = "other"; break;
582                     default:                              ssource= "unknown"; break;
583                 }
584 
585                 string stype;
586                 switch (type)
587                 {
588                     case GL_DEBUG_TYPE_ERROR:               stype = "error"; break;
589                     case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: stype = "deprecated behaviour"; break;
590                     case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:  stype = "undefined behaviour"; break;
591                     case GL_DEBUG_TYPE_PORTABILITY:         stype = "portabiliy"; break;
592                     case GL_DEBUG_TYPE_PERFORMANCE:         stype = "performance"; break;
593                     case GL_DEBUG_TYPE_OTHER:               stype = "other"; break;
594                     default:                                stype = "unknown"; break;
595                 }
596 
597                 LogLevel level;
598 
599                 string sseverity;
600                 switch (severity)
601                 {
602                     case GL_DEBUG_SEVERITY_HIGH:
603                         level = LogLevel.error;
604                         sseverity = "high";
605                         break;
606 
607                     case GL_DEBUG_SEVERITY_MEDIUM: 
608                         level = LogLevel.warning;
609                         sseverity = "medium";
610                         break;
611 
612                     case GL_DEBUG_SEVERITY_LOW:    
613                         level = LogLevel.warning;
614                         sseverity = "low";
615                         break;
616 
617                     case GL_DEBUG_SEVERITY_NOTIFICATION:
618                         level = LogLevel.info;
619                         sseverity = "notification";
620                         break;
621 
622                     default:
623                         level = LogLevel.warning;
624                         sseverity = "unknown";
625                         break;
626                 }
627 
628                 // Need to copy message as it's const
629                 import core.stdc..string;
630                 auto text = FixedString!1024.fromCString(message);
631                 if(!text.sanitizeASCIIInPlace())
632                 {
633                     logger.warning("Invalid (non-ASCII) character in GL debug output");
634                 }
635 
636                 // Spammy NVidia binary driver message that pretty much tells us that the
637                 // buffer objects works as it should.
638                 if(id == 131185) { return; }
639                 // Spammy NVidia binary driver message that tells as that mipmap level
640                 // of texture '0' is inconsistent with its min filter
641                 // (no idea what causes this, possibly dimgui).
642                 // - thought we may be binding '0' somewhere, but that's not the case,
643                 // and dimgui textures have no mimpaps afaics.
644                 if(id == 131204) { return; }
645 
646                 if (level == LogLevel.info)
647                     logger.infof("opengl: %s (id: %s, source: %s, type: %s, severity: %s)", text, id, ssource, stype, sseverity);
648                 if (level == LogLevel.warning)
649                     logger.warningf("opengl: %s (id: %s, source: %s, type: %s, severity: %s)", text, id, ssource, stype, sseverity);
650                 if (level == LogLevel.error)
651                     logger.errorf("opengl: %s (id: %s, source: %s, type: %s, severity: %s)", text, id, ssource, stype, sseverity);
652             }
653             catch (Exception e)
654             {
655                 // got exception while logging, ignore it
656             }
657         }
658         catch (Throwable e)
659         {
660             // No Throwable is supposed to cross C callbacks boundaries
661             // Crash immediately
662             exit(-1);
663         }
664     }
665 }
666 
667 import std.traits;
668 
669 struct FixedBuffer(size_t size, T)
670     if(!hasElaborateDestructor!T)
671 {
672 private:
673     T[size] buffer;
674 
675     size_t used;
676 
677 public:
678     T[] data() @safe pure nothrow @nogc
679     {
680         return buffer[0 .. used];
681     }
682 
683     alias data this;
684 }
685 
686 struct FixedString(size_t size)
687 {
688     FixedBuffer!(size, char) base;
689     alias base this;
690 
691     this(const char[] data) @safe pure nothrow @nogc 
692     {
693         assert(data.length <= size, "FixedString too small to contain assigned data");
694         buffer[0 .. data.length] = data[];
695         used = data.length;
696     }
697 
698     static auto fromCString(const char* data) @system pure nothrow @nogc
699     {
700         import core.stdc..string;
701         return FixedString!size(data[0 .. strlen(data)]);
702     }
703 }