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, ¶m); 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 }