// Copyright (C) 2020 Jérôme Leclercq // This file is part of the "Nazara Engine - OpenGL Renderer" // For conditions of distribution and use, see copyright notice in Config.hpp #include #include #include #include #include #include #include #include #include #include #include namespace Nz::GL { thread_local const Context* s_currentContext = nullptr; namespace { template struct GLWrapper; template struct GLWrapper { static auto WrapErrorHandling() { return [](Args... args) -> Ret { const Context* context = s_currentContext; //< pay TLS cost once assert(context); FuncType funcPtr = reinterpret_cast(context->GetFunctionByIndex(FuncIndex)); context->ClearErrorStack(); if constexpr (std::is_same_v) { funcPtr(std::forward(args)...); context->ProcessErrorStack(); } else { Ret r = funcPtr(std::forward(args)...); context->ProcessErrorStack(); return r; } }; } }; } struct Context::SymbolLoader { SymbolLoader(Context& parent) : context(parent) { } template bool Load(Func& func, const char* funcName, bool mandatory, bool implementFallback = false) { const Loader& loader = context.GetLoader(); GLFunction originalFuncPtr = loader.LoadFunction(funcName); func = reinterpret_cast(originalFuncPtr); #if defined(NAZARA_OPENGLRENDERER_DEBUG) && (!defined(NAZARA_COMPILER_MSVC) || defined(NAZARA_PLATFORM_x64)) if (func) { if (std::strcmp(funcName, "glGetError") != 0) //< Prevent infinite recursion { using Wrapper = GLWrapper>; func = Wrapper::WrapErrorHandling(); } } #endif if (!func) { if (!implementFallback || (!context.ImplementFallback(funcName) && !func)) //< double-check { if (mandatory) throw std::runtime_error("failed to load core function " + std::string(funcName)); } } context.m_originalFunctionPointer[FuncIndex] = originalFuncPtr; return func != nullptr; } Context& context; }; Context::~Context() { if (m_device) m_device->NotifyContextDestruction(*this); } void Context::BindBuffer(BufferTarget target, GLuint buffer, bool force) const { if (m_state.bufferTargets[UnderlyingCast(target)] != buffer || force) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glBindBuffer(ToOpenGL(target), buffer); m_state.bufferTargets[UnderlyingCast(target)] = buffer; } } void Context::BindFramebuffer(GLuint fbo) const { if (m_state.boundDrawFBO != fbo || m_state.boundReadFBO != fbo) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glBindFramebuffer(GL_FRAMEBUFFER, fbo); m_state.boundDrawFBO = fbo; m_state.boundReadFBO = fbo; } } void Context::BindFramebuffer(FramebufferTarget target, GLuint fbo) const { auto& currentFbo = (target == FramebufferTarget::Draw) ? m_state.boundDrawFBO : m_state.boundReadFBO; if (currentFbo != fbo) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glBindFramebuffer((target == FramebufferTarget::Draw) ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, fbo); currentFbo = fbo; } } void Context::BindProgram(GLuint program) const { if (m_state.boundProgram != program) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glUseProgram(program); m_state.boundProgram = program; } } void Context::BindSampler(UInt32 textureUnit, GLuint sampler) const { if (textureUnit >= m_state.textureUnits.size()) throw std::runtime_error("unsupported texture unit #" + std::to_string(textureUnit)); auto& unit = m_state.textureUnits[textureUnit]; if (unit.sampler != sampler) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glBindSampler(textureUnit, sampler); unit.sampler = sampler; } } void Context::BindTexture(TextureTarget target, GLuint texture) const { BindTexture(m_state.currentTextureUnit, target, texture); } void Context::BindTexture(UInt32 textureUnit, TextureTarget target, GLuint texture) const { if (textureUnit >= m_state.textureUnits.size()) throw std::runtime_error("unsupported texture unit #" + std::to_string(textureUnit)); auto& unit = m_state.textureUnits[textureUnit]; if (unit.textureTargets[UnderlyingCast(target)] != texture) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); SetCurrentTextureUnit(textureUnit); glBindTexture(ToOpenGL(target), texture); unit.textureTargets[UnderlyingCast(target)] = texture; } } void Context::BindUniformBuffer(UInt32 uboUnit, GLuint buffer, GLintptr offset, GLsizeiptr size) const { if (uboUnit >= m_state.uboUnits.size()) throw std::runtime_error("unsupported uniform buffer unit #" + std::to_string(uboUnit)); auto& unit = m_state.uboUnits[uboUnit]; if (unit.buffer != buffer || unit.offset != offset || unit.size != size) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glBindBufferRange(GL_UNIFORM_BUFFER, uboUnit, buffer, offset, size); unit.buffer = buffer; unit.offset = offset; unit.size = size; // glBindBufferRange does replace the currently bound buffer m_state.bufferTargets[UnderlyingCast(BufferTarget::Uniform)] = buffer; } } void Context::BindVertexArray(GLuint vertexArray, bool force) const { if (m_state.boundVertexArray != vertexArray || force) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glBindVertexArray(vertexArray); m_state.boundVertexArray = vertexArray; } } bool Context::ClearErrorStack() const { assert(GetCurrentContext() == this); while (glGetError() != GL_NO_ERROR); return true; } bool Context::Initialize(const ContextParams& params) { if (!SetCurrentContext(this)) { NazaraError("failed to activate context"); return false; } SymbolLoader loader(*this); try { #define NAZARA_OPENGLRENDERER_FUNC(name, sig) loader.Load(name, #name, true, true); #define NAZARA_OPENGLRENDERER_EXT_FUNC(name, sig) //< Do nothing NAZARA_OPENGLRENDERER_FOREACH_GLES_FUNC(NAZARA_OPENGLRENDERER_FUNC, NAZARA_OPENGLRENDERER_EXT_FUNC) #undef NAZARA_OPENGLRENDERER_EXT_FUNC #undef NAZARA_OPENGLRENDERER_FUNC } catch (const std::exception& e) { NazaraError(e.what()); return false; } GLint majorVersion = 0; glGetIntegerv(GL_MAJOR_VERSION, &majorVersion); GLint minorVersion = 0; glGetIntegerv(GL_MINOR_VERSION, &minorVersion); m_params.glMajorVersion = majorVersion; m_params.glMinorVersion = minorVersion; // Load extensions GLint extensionCount = 0; glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount); for (GLint i = 0; i < extensionCount; ++i) m_supportedExtensions.emplace(reinterpret_cast(glGetStringi(GL_EXTENSIONS, i))); m_extensionStatus.fill(ExtensionStatus::NotSupported); // Depth clamp if (m_params.type == ContextType::OpenGL && m_params.glMajorVersion >= 3 && m_params.glMajorVersion >= 2) m_extensionStatus[UnderlyingCast(Extension::DepthClamp)] = ExtensionStatus::Core; else if (m_supportedExtensions.count("GL_ARB_depth_clamp")) m_extensionStatus[UnderlyingCast(Extension::DepthClamp)] = ExtensionStatus::ARB; else if (m_supportedExtensions.count("GL_EXT_depth_clamp")) m_extensionStatus[UnderlyingCast(Extension::DepthClamp)] = ExtensionStatus::EXT; else if (m_supportedExtensions.count("GL_NV_depth_clamp")) m_extensionStatus[UnderlyingCast(Extension::DepthClamp)] = ExtensionStatus::Vendor; // SpirV if (m_params.type == ContextType::OpenGL && m_params.glMajorVersion >= 4 && m_params.glMajorVersion >= 6) m_extensionStatus[UnderlyingCast(Extension::SpirV)] = ExtensionStatus::Core; else if (m_supportedExtensions.count("GL_ARB_gl_spirv")) m_extensionStatus[UnderlyingCast(Extension::SpirV)] = ExtensionStatus::ARB; // Texture compression (S3tc) if (m_supportedExtensions.count("GL_EXT_texture_compression_s3tc")) m_extensionStatus[UnderlyingCast(Extension::TextureCompressionS3tc)] = ExtensionStatus::EXT; // Texture anisotropic filter if (m_params.type == ContextType::OpenGL && m_params.glMajorVersion >= 4 && m_params.glMajorVersion >= 6) m_extensionStatus[UnderlyingCast(Extension::TextureFilterAnisotropic)] = ExtensionStatus::Core; else if (m_supportedExtensions.count("GL_ARB_texture_filter_anisotropic")) m_extensionStatus[UnderlyingCast(Extension::TextureFilterAnisotropic)] = ExtensionStatus::ARB; else if (m_supportedExtensions.count("GL_EXT_texture_filter_anisotropic")) m_extensionStatus[UnderlyingCast(Extension::TextureFilterAnisotropic)] = ExtensionStatus::EXT; #define NAZARA_OPENGLRENDERER_FUNC(name, sig) #define NAZARA_OPENGLRENDERER_EXT_FUNC(name, sig) loader.Load(name, #name, false); NAZARA_OPENGLRENDERER_FOREACH_GLES_FUNC(NAZARA_OPENGLRENDERER_FUNC, NAZARA_OPENGLRENDERER_EXT_FUNC) #undef NAZARA_OPENGLRENDERER_EXT_FUNC #undef NAZARA_OPENGLRENDERER_FUNC // If we requested an OpenGL ES context but cannot create one, check for some compatibility extensions if (params.type == ContextType::OpenGL_ES && m_params.type != params.type) { if (m_supportedExtensions.count("GL_ARB_ES3_2_compatibility")) { m_params.type = ContextType::OpenGL_ES; m_params.glMajorVersion = 3; m_params.glMinorVersion = 2; } else if (m_supportedExtensions.count("GL_ARB_ES3_1_compatibility")) { m_params.type = ContextType::OpenGL_ES; m_params.glMajorVersion = 3; m_params.glMinorVersion = 1; } else if (m_supportedExtensions.count("GL_ARB_ES3_compatibility")) { m_params.type = ContextType::OpenGL_ES; m_params.glMajorVersion = 3; m_params.glMinorVersion = 0; } else NazaraWarning("desktop support for OpenGL ES is missing, falling back to OpenGL..."); } // Set debug callback (if supported) if (glDebugMessageCallback) { glEnable(GL_DEBUG_OUTPUT); #ifdef NAZARA_DEBUG glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); #endif glDebugMessageCallback([](GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { const Context* context = static_cast(userParam); context->HandleDebugMessage(source, type, id, severity, length, message); }, this); // Disable driver notifications (NVidia driver is very verbose) if (glDebugMessageControl) glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, GL_FALSE); } GLint maxTextureUnits = -1; glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits); if (maxTextureUnits < 32) //< OpenGL ES 3.0 requires at least 32 textures units NazaraWarning("GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS is " + std::to_string(maxTextureUnits) + ", >= 32 expected"); assert(maxTextureUnits > 0); m_state.textureUnits.resize(maxTextureUnits); GLint maxUniformBufferUnits = -1; glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxUniformBufferUnits); if (maxUniformBufferUnits < 24) //< OpenGL ES 3.0 requires at least 24 uniform buffers units NazaraWarning("GL_MAX_UNIFORM_BUFFER_BINDINGS is " + std::to_string(maxUniformBufferUnits) + ", >= 24 expected"); assert(maxUniformBufferUnits > 0); m_state.uboUnits.resize(maxUniformBufferUnits); std::array res; glGetIntegerv(GL_SCISSOR_BOX, res.data()); m_state.scissorBox = { res[0], res[1], res[2], res[3] }; glGetIntegerv(GL_VIEWPORT, res.data()); m_state.viewport = { res[0], res[1], res[2], res[3] }; m_state.renderStates.frontFace = FrontFace::CounterClockwise; //< OpenGL default front face is counter-clockwise EnableVerticalSync(false); return true; } bool Context::ProcessErrorStack() const { assert(GetCurrentContext() == this); bool hasAnyError = false; GLuint lastError; while ((lastError = glGetError()) != GL_NO_ERROR) { hasAnyError = true; NazaraError("OpenGL error: " + TranslateOpenGLError(lastError)); } return hasAnyError; } void Context::SetCurrentTextureUnit(UInt32 textureUnit) const { if (m_state.currentTextureUnit != textureUnit) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glActiveTexture(GL_TEXTURE0 + textureUnit); m_state.currentTextureUnit = textureUnit; } } void Context::SetScissorBox(GLint x, GLint y, GLsizei width, GLsizei height) const { if (m_state.scissorBox.x != x || m_state.scissorBox.y != y || m_state.scissorBox.width != width || m_state.scissorBox.height != height) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glScissor(x, y, width, height); m_state.scissorBox.x = x; m_state.scissorBox.y = y; m_state.scissorBox.width = width; m_state.scissorBox.height = height; } } void Context::SetViewport(GLint x, GLint y, GLsizei width, GLsizei height) const { if (m_state.viewport.x != x || m_state.viewport.y != y || m_state.viewport.width != width || m_state.viewport.height != height) { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); glViewport(x, y, width, height); m_state.viewport.x = x; m_state.viewport.y = y; m_state.viewport.width = width; m_state.viewport.height = height; } } void Context::UpdateStates(const RenderStates& renderStates, bool isViewportFlipped) const { if (!SetCurrentContext(this)) throw std::runtime_error("failed to activate context"); // Depth compare and depth write if (renderStates.depthBuffer) { if (m_state.renderStates.depthCompare != renderStates.depthCompare) { glDepthFunc(ToOpenGL(renderStates.depthCompare)); m_state.renderStates.depthCompare = renderStates.depthCompare; } if (m_state.renderStates.depthWrite != renderStates.depthWrite) { glDepthMask((renderStates.depthWrite) ? GL_TRUE : GL_FALSE); m_state.renderStates.depthWrite = renderStates.depthWrite; } } // Face culling side if (renderStates.faceCulling) { if (m_state.renderStates.cullingSide != renderStates.cullingSide) { glCullFace(ToOpenGL(renderStates.cullingSide)); m_state.renderStates.cullingSide = renderStates.cullingSide; } } // Front face FrontFace targetFrontFace = renderStates.frontFace; if (!isViewportFlipped) targetFrontFace = (targetFrontFace == FrontFace::Clockwise) ? FrontFace::CounterClockwise : FrontFace::Clockwise; if (m_state.renderStates.frontFace != targetFrontFace) { glFrontFace(ToOpenGL(targetFrontFace)); m_state.renderStates.frontFace = targetFrontFace; } // Face filling if (m_state.renderStates.faceFilling != renderStates.faceFilling) { assert(glPolygonMode); glPolygonMode(GL_FRONT_AND_BACK, ToOpenGL(renderStates.faceFilling)); m_state.renderStates.faceFilling = renderStates.faceFilling; } // Front and back stencil states if (renderStates.stencilTest) { auto ApplyStencilStates = [&](bool front) { auto& currentStencilData = (front) ? m_state.renderStates.stencilFront : m_state.renderStates.stencilBack; auto& newStencilData = (front) ? renderStates.stencilFront : renderStates.stencilBack; if (currentStencilData.compare != newStencilData.compare || currentStencilData.reference != newStencilData.reference || currentStencilData.compareMask != newStencilData.compareMask) { glStencilFuncSeparate((front) ? GL_FRONT : GL_BACK, ToOpenGL(newStencilData.compare), newStencilData.reference, newStencilData.compareMask); currentStencilData.compare = newStencilData.compare; currentStencilData.compareMask = newStencilData.compareMask; currentStencilData.reference = newStencilData.reference; } if (currentStencilData.depthFail != newStencilData.depthFail || currentStencilData.fail != newStencilData.fail || currentStencilData.pass != newStencilData.pass) { glStencilOpSeparate((front) ? GL_FRONT : GL_BACK, ToOpenGL(newStencilData.fail), ToOpenGL(newStencilData.depthFail), ToOpenGL(newStencilData.pass)); currentStencilData.depthFail = newStencilData.depthFail; currentStencilData.fail = newStencilData.fail; currentStencilData.pass = newStencilData.pass; } if (currentStencilData.writeMask != newStencilData.writeMask) { glStencilMaskSeparate((front) ? GL_FRONT : GL_BACK, newStencilData.writeMask); currentStencilData.writeMask = newStencilData.writeMask; } }; ApplyStencilStates(true); ApplyStencilStates(false); } // Line width if (!NumberEquals(m_state.renderStates.lineWidth, renderStates.lineWidth, 0.001f)) { glLineWidth(renderStates.lineWidth); m_state.renderStates.lineWidth = renderStates.lineWidth; } // Point size (TODO) /*if (!NumberEquals(m_state.renderStates.pointSize, renderStates.pointSize, 0.001f)) { glPointSize(renderStates.pointSize); m_state.renderStates.pointSize = renderStates.pointSize; }*/ // Blend states if (m_state.renderStates.blending != renderStates.blending) { if (renderStates.blending) glEnable(GL_BLEND); else glDisable(GL_BLEND); m_state.renderStates.blending = renderStates.blending; } if (renderStates.blending) { auto& currentBlend = m_state.renderStates.blend; const auto& targetBlend = renderStates.blend; if (currentBlend.modeColor != targetBlend.modeColor || currentBlend.modeAlpha != targetBlend.modeAlpha) { glBlendEquationSeparate(ToOpenGL(targetBlend.modeColor), ToOpenGL(targetBlend.modeAlpha)); currentBlend.modeAlpha = targetBlend.modeAlpha; currentBlend.modeColor = targetBlend.modeColor; } if (currentBlend.dstAlpha != targetBlend.dstAlpha || currentBlend.dstColor != targetBlend.dstColor || currentBlend.srcAlpha != targetBlend.srcAlpha || currentBlend.srcColor != targetBlend.srcColor) { glBlendFuncSeparate(ToOpenGL(targetBlend.srcColor), ToOpenGL(targetBlend.dstColor), ToOpenGL(targetBlend.srcAlpha), ToOpenGL(targetBlend.dstAlpha)); currentBlend.dstAlpha = targetBlend.dstAlpha; currentBlend.dstColor = targetBlend.dstColor; currentBlend.srcAlpha = targetBlend.srcAlpha; currentBlend.srcColor = targetBlend.srcColor; } } // Color write if (m_state.renderStates.colorWrite != renderStates.colorWrite) { GLboolean param = (renderStates.colorWrite) ? GL_TRUE : GL_FALSE; glColorMask(param, param, param, param); m_state.renderStates.colorWrite = renderStates.colorWrite; } // Depth buffer if (m_state.renderStates.depthBuffer != renderStates.depthBuffer) { if (renderStates.depthBuffer) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); m_state.renderStates.depthBuffer = renderStates.depthBuffer; } // Depth clamp if (m_state.renderStates.depthClamp != renderStates.depthClamp) { assert(IsExtensionSupported(Extension::DepthClamp)); if (renderStates.depthClamp) glEnable(GL_DEPTH_CLAMP_EXT); else glDisable(GL_DEPTH_CLAMP_EXT); m_state.renderStates.depthClamp = renderStates.depthClamp; } // Face culling if (m_state.renderStates.faceCulling != renderStates.faceCulling) { if (renderStates.faceCulling) glEnable(GL_CULL_FACE); else glDisable(GL_CULL_FACE); m_state.renderStates.faceCulling = renderStates.faceCulling; } // Scissor test if (m_state.renderStates.scissorTest != renderStates.scissorTest) { if (renderStates.scissorTest) glEnable(GL_SCISSOR_TEST); else glDisable(GL_SCISSOR_TEST); m_state.renderStates.scissorTest = renderStates.scissorTest; } // Stencil test if (m_state.renderStates.stencilTest != renderStates.stencilTest) { if (renderStates.stencilTest) glEnable(GL_STENCIL_TEST); else glDisable(GL_STENCIL_TEST); m_state.renderStates.stencilTest = renderStates.stencilTest; } } const Context* Context::GetCurrentContext() { return s_currentContext; } bool Context::SetCurrentContext(const Context* context) { const Context*& currentContext = s_currentContext; //< Pay TLS cost only once if (currentContext == context) return true; if (currentContext) { currentContext->Desactivate(); currentContext = nullptr; } if (context) { if (!context->Activate()) return false; currentContext = context; } return true; } void Context::OnContextRelease() { m_vaoCache.Clear(); } bool Context::ImplementFallback(const std::string_view& function) { SymbolLoader loader(*this); if (function == "glDebugMessageCallback") { constexpr std::size_t functionIndex = UnderlyingCast(FunctionIndex::glDebugMessageCallback); return loader.Load(glDebugMessageCallback, "glDebugMessageCallbackKHR", false) || //< from GL_KHR_debug loader.Load(glDebugMessageCallback, "glDebugMessageCallbackARB", false); //< from GL_ARB_debug_output } else if (function == "glPolygonMode") { constexpr std::size_t functionIndex = UnderlyingCast(FunctionIndex::glPolygonMode); return loader.Load(glPolygonMode, "glPolygonModeNV", false); //< from GL_NV_polygon_mode } return false; } void Context::NotifyContextDestruction(Context* context) { const Context*& currentContext = s_currentContext; //< Pay TLS cost only once if (currentContext == context) currentContext = nullptr; } void Context::HandleDebugMessage(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message) const { // Ignore debug groups if (id == 0) return; std::stringstream ss; ss << "OpenGL debug message (ID: 0x" << id << "):\n"; ss << "Sent by context: " << this; ss << "\n-Source: "; switch (source) { case GL_DEBUG_SOURCE_API: ss << "OpenGL API"; break; case GL_DEBUG_SOURCE_WINDOW_SYSTEM: ss << "Operating system"; break; case GL_DEBUG_SOURCE_SHADER_COMPILER: ss << "Shader compiler"; break; case GL_DEBUG_SOURCE_THIRD_PARTY: ss << "Third party"; break; case GL_DEBUG_SOURCE_APPLICATION: ss << "Application"; break; case GL_DEBUG_SOURCE_OTHER: ss << "Other"; break; default: // Extension source ss << "Unknown"; break; } ss << '\n'; ss << "-Type: "; switch (type) { case GL_DEBUG_TYPE_ERROR: ss << "Error"; break; case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: ss << "Deprecated behavior"; break; case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: ss << "Undefined behavior"; break; case GL_DEBUG_TYPE_PORTABILITY: ss << "Portability"; break; case GL_DEBUG_TYPE_PERFORMANCE: ss << "Performance"; break; case GL_DEBUG_TYPE_OTHER: ss << "Other"; break; default: // Extension type ss << "Unknown"; break; } ss << '\n'; ss << "-Severity: "; switch (severity) { case GL_DEBUG_SEVERITY_HIGH: ss << "High"; break; case GL_DEBUG_SEVERITY_MEDIUM: ss << "Medium"; break; case GL_DEBUG_SEVERITY_LOW: ss << "Low"; break; case GL_DEBUG_SEVERITY_NOTIFICATION: ss << "Notification"; break; default: ss << "Unknown"; break; } ss << '\n'; ss << "Message: " << std::string_view(message, length) << '\n'; NazaraNotice(ss.str()); } }