Moved most variables in sfml-graphics, GlResource and GlContext out of namespace scope.

This commit is contained in:
binary1248 2023-04-01 02:44:05 +02:00 committed by Chris Thrasher
parent 6a3e44bc4d
commit b39be46db0
11 changed files with 523 additions and 477 deletions

View File

@ -29,6 +29,8 @@
////////////////////////////////////////////////////////////
#include <SFML/Window/Export.hpp>
#include <memory>
namespace sf
{
@ -48,23 +50,24 @@ protected:
GlResource();
////////////////////////////////////////////////////////////
/// \brief Destructor
///
////////////////////////////////////////////////////////////
~GlResource();
////////////////////////////////////////////////////////////
/// \brief Register a function to be called when a context is destroyed
/// \brief Register an OpenGL object to be destroyed when its containing context is destroyed
///
/// This is used for internal purposes in order to properly
/// clean up OpenGL resources that cannot be shared between
/// contexts.
///
/// \param callback Function to be called when a context is destroyed
/// \param arg Argument to pass when calling the function
/// \param object Object to be destroyed when its containing context is destroyed
///
////////////////////////////////////////////////////////////
static void registerContextDestroyCallback(ContextDestroyCallback callback, void* arg);
static void registerUnsharedGlObject(std::shared_ptr<void> object);
////////////////////////////////////////////////////////////
/// \brief Unregister an OpenGL object from its containing context
///
/// \param object Object to be unregistered
///
////////////////////////////////////////////////////////////
static void unregisterUnsharedGlObject(std::shared_ptr<void> object);
////////////////////////////////////////////////////////////
/// \brief RAII helper class to temporarily lock an available context for use
@ -97,6 +100,12 @@ protected:
////////////////////////////////////////////////////////////
TransientContextLock& operator=(const TransientContextLock&) = delete;
};
private:
////////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
std::shared_ptr<void> m_sharedContext; //!< Shared context used to link all contexts together for resource sharing
};
} // namespace sf

View File

@ -54,30 +54,35 @@ namespace
namespace RenderTargetImpl
{
// Mutex to protect ID generation and our context-RenderTarget-map
std::recursive_mutex mutex;
std::recursive_mutex& getMutex()
{
static std::recursive_mutex mutex;
return mutex;
}
// Unique identifier, used for identifying RenderTargets when
// tracking the currently active RenderTarget within a given context
std::uint64_t getUniqueId()
{
const std::lock_guard lock(mutex);
const std::lock_guard lock(getMutex());
static std::uint64_t id = 1; // start at 1, zero is "no RenderTarget"
return id++;
}
// Map to help us detect whether a different RenderTarget
// has been activated within a single context
using ContextRenderTargetMap = std::unordered_map<std::uint64_t, std::uint64_t>;
ContextRenderTargetMap contextRenderTargetMap;
ContextRenderTargetMap& getContextRenderTargetMap()
{
static ContextRenderTargetMap contextRenderTargetMap;
return contextRenderTargetMap;
}
// Check if a RenderTarget with the given ID is active in the current context
bool isActive(std::uint64_t id)
{
const auto it = contextRenderTargetMap.find(sf::Context::getActiveContextId());
return (it != contextRenderTargetMap.end()) && (it->second == id);
const auto it = getContextRenderTargetMap().find(sf::Context::getActiveContextId());
return (it != getContextRenderTargetMap().end()) && (it->second == id);
}
// Convert an sf::BlendMode::Factor constant to the corresponding OpenGL constant.
@ -393,12 +398,13 @@ bool RenderTarget::setActive(bool active)
{
// Mark this RenderTarget as active or no longer active in the tracking map
{
const std::lock_guard lock(RenderTargetImpl::mutex);
const std::lock_guard lock(RenderTargetImpl::getMutex());
const std::uint64_t contextId = Context::getActiveContextId();
using RenderTargetImpl::contextRenderTargetMap;
const auto it = contextRenderTargetMap.find(contextId);
using RenderTargetImpl::getContextRenderTargetMap;
auto& contextRenderTargetMap = getContextRenderTargetMap();
auto it = contextRenderTargetMap.find(contextId);
if (active)
{

View File

@ -36,111 +36,38 @@
#include <mutex>
#include <ostream>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
namespace
{
// Set to track all active FBO mappings
// This is used to free active FBOs while their owning
// RenderTextureImplFBO is still alive
std::unordered_set<std::unordered_map<std::uint64_t, unsigned int>*> frameBuffers;
// Set to track all stale FBOs
// This is used to free stale FBOs after their owning
// RenderTextureImplFBO has already been destroyed
// An FBO cannot be destroyed until it's containing context
// becomes active, so the destruction of the RenderTextureImplFBO
// has to be decoupled from the destruction of the FBOs themselves
std::set<std::pair<std::uint64_t, unsigned int>> staleFrameBuffers;
// Mutex to protect both active and stale frame buffer sets
std::recursive_mutex mutex;
// This function is called either when a RenderTextureImplFBO is
// destroyed or via contextDestroyCallback when context destruction
// might trigger deletion of its contained stale FBOs
void destroyStaleFBOs()
{
const std::uint64_t contextId = sf::Context::getActiveContextId();
for (auto it = staleFrameBuffers.begin(); it != staleFrameBuffers.end();)
{
if (it->first == contextId)
{
const auto frameBuffer = static_cast<GLuint>(it->second);
glCheck(GLEXT_glDeleteFramebuffers(1, &frameBuffer));
staleFrameBuffers.erase(it++);
}
else
{
++it;
}
}
}
// Callback that is called every time a context is destroyed
void contextDestroyCallback(void* /*arg*/)
{
const std::lock_guard lock(mutex);
const std::uint64_t contextId = sf::Context::getActiveContextId();
// Destroy active frame buffer objects
for (auto* frameBuffer : frameBuffers)
{
for (auto it = frameBuffer->begin(); it != frameBuffer->end(); ++it)
{
if (it->first == contextId)
{
const GLuint frameBufferId = it->second;
glCheck(GLEXT_glDeleteFramebuffers(1, &frameBufferId));
// Erase the entry from the RenderTextureImplFBO's map
frameBuffer->erase(it);
break;
}
}
}
// Destroy stale frame buffer objects
destroyStaleFBOs();
}
} // namespace
namespace sf::priv
{
////////////////////////////////////////////////////////////
RenderTextureImplFBO::RenderTextureImplFBO()
struct RenderTextureImplFBO::FrameBufferObject
{
const std::lock_guard lock(mutex);
// Register the context destruction callback
registerContextDestroyCallback(contextDestroyCallback, nullptr);
// Insert the new frame buffer mapping into the set of all active mappings
frameBuffers.insert(&m_frameBuffers);
frameBuffers.insert(&m_multisampleFrameBuffers);
FrameBufferObject()
{
// Create the framebuffer object
glCheck(GLEXT_glGenFramebuffers(1, &object));
}
~FrameBufferObject()
{
if (object)
glCheck(GLEXT_glDeleteFramebuffers(1, &object));
}
GLuint object{};
};
////////////////////////////////////////////////////////////
RenderTextureImplFBO::RenderTextureImplFBO() = default;
////////////////////////////////////////////////////////////
RenderTextureImplFBO::~RenderTextureImplFBO()
{
const TransientContextLock contextLock;
const std::lock_guard lock(mutex);
// Remove the frame buffer mapping from the set of all active mappings
frameBuffers.erase(&m_frameBuffers);
frameBuffers.erase(&m_multisampleFrameBuffers);
// Destroy the color buffer
if (m_colorBuffer)
{
@ -155,15 +82,22 @@ RenderTextureImplFBO::~RenderTextureImplFBO()
glCheck(GLEXT_glDeleteRenderbuffers(1, &depthStencilBuffer));
}
// Move all frame buffer objects to stale set
for (auto& [contextId, frameBufferId] : m_frameBuffers)
staleFrameBuffers.emplace(contextId, frameBufferId);
// Unregister FBOs with the contexts if they haven't already been destroyed
for (auto& entry : m_frameBuffers)
{
auto frameBuffer = entry.second.lock();
for (auto& [contextId, multisampleFrameBufferId] : m_multisampleFrameBuffers)
staleFrameBuffers.emplace(contextId, multisampleFrameBufferId);
if (frameBuffer)
unregisterUnsharedGlObject(std::move(frameBuffer));
}
// Clean up FBOs
destroyStaleFBOs();
for (auto& entry : m_multisampleFrameBuffers)
{
auto frameBuffer = entry.second.lock();
if (frameBuffer)
unregisterUnsharedGlObject(std::move(frameBuffer));
}
}
@ -422,15 +356,14 @@ bool RenderTextureImplFBO::create(const Vector2u& size, unsigned int textureId,
bool RenderTextureImplFBO::createFrameBuffer()
{
// Create the framebuffer object
GLuint frameBuffer = 0;
glCheck(GLEXT_glGenFramebuffers(1, &frameBuffer));
auto frameBuffer = std::make_shared<FrameBufferObject>();
if (!frameBuffer)
if (!frameBuffer->object)
{
err() << "Impossible to create render texture (failed to create the frame buffer object)" << std::endl;
return false;
}
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, frameBuffer));
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, frameBuffer->object));
// Link the depth/stencil renderbuffer to the frame buffer
if (!m_multisample && m_depthStencilBuffer)
@ -462,33 +395,30 @@ bool RenderTextureImplFBO::createFrameBuffer()
if (status != GLEXT_GL_FRAMEBUFFER_COMPLETE)
{
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, 0));
glCheck(GLEXT_glDeleteFramebuffers(1, &frameBuffer));
err() << "Impossible to create render texture (failed to link the target texture to the frame buffer)" << std::endl;
return false;
}
{
const std::lock_guard lock(mutex);
// Insert the FBO into our map
m_frameBuffers.emplace(Context::getActiveContextId(), frameBuffer);
}
// Register the object with the current context so it is automatically destroyed
registerUnsharedGlObject(std::move(frameBuffer));
#ifndef SFML_OPENGL_ES
if (m_multisample)
{
// Create the multisample framebuffer object
GLuint multisampleFrameBuffer = 0;
glCheck(GLEXT_glGenFramebuffers(1, &multisampleFrameBuffer));
auto multisampleFrameBuffer = std::make_shared<FrameBufferObject>();
if (!multisampleFrameBuffer)
if (!multisampleFrameBuffer->object)
{
err() << "Impossible to create render texture (failed to create the multisample frame buffer object)"
<< std::endl;
return false;
}
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, multisampleFrameBuffer));
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, multisampleFrameBuffer->object));
// Link the multisample color buffer to the frame buffer
glCheck(GLEXT_glBindRenderbuffer(GLEXT_GL_RENDERBUFFER, m_colorBuffer));
@ -517,19 +447,17 @@ bool RenderTextureImplFBO::createFrameBuffer()
if (status != GLEXT_GL_FRAMEBUFFER_COMPLETE)
{
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, 0));
glCheck(GLEXT_glDeleteFramebuffers(1, &multisampleFrameBuffer));
err() << "Impossible to create render texture (failed to link the render buffers to the multisample frame "
"buffer)"
<< std::endl;
return false;
}
{
const std::lock_guard lock(mutex);
// Insert the FBO into our map
m_multisampleFrameBuffers.emplace(Context::getActiveContextId(), multisampleFrameBuffer);
}
// Register the object with the current context so it is automatically destroyed
registerUnsharedGlObject(std::move(multisampleFrameBuffer));
}
#endif
@ -575,29 +503,33 @@ bool RenderTextureImplFBO::activate(bool active)
// Lookup the FBO corresponding to the currently active context
// If none is found, there is no FBO corresponding to the
// currently active context so we will have to create a new FBO
{
const std::lock_guard lock(mutex);
std::unordered_map<std::uint64_t, unsigned int>::iterator it;
if (m_multisample)
{
it = m_multisampleFrameBuffers.find(contextId);
const auto it = m_multisampleFrameBuffers.find(contextId);
if (it != m_multisampleFrameBuffers.end())
{
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, it->second));
auto frameBuffer = it->second.lock();
if (frameBuffer)
{
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, frameBuffer->object));
return true;
}
}
}
else
{
it = m_frameBuffers.find(contextId);
const auto it = m_frameBuffers.find(contextId);
if (it != m_frameBuffers.end())
{
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, it->second));
auto frameBuffer = it->second.lock();
if (frameBuffer)
{
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_FRAMEBUFFER, frameBuffer->object));
return true;
}
@ -630,15 +562,18 @@ void RenderTextureImplFBO::updateTexture(unsigned int)
{
const std::uint64_t contextId = Context::getActiveContextId();
const std::lock_guard lock(mutex);
const auto frameBufferIt = m_frameBuffers.find(contextId);
const auto multisampleIt = m_multisampleFrameBuffers.find(contextId);
if ((frameBufferIt != m_frameBuffers.end()) && (multisampleIt != m_multisampleFrameBuffers.end()))
{
auto frameBuffer = frameBufferIt->second.lock();
auto multiSampleFrameBuffer = multisampleIt->second.lock();
if (frameBuffer && multiSampleFrameBuffer)
{
// Set up the blit target (draw framebuffer) and blit (from the read framebuffer, our multisample FBO)
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_DRAW_FRAMEBUFFER, frameBufferIt->second));
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_DRAW_FRAMEBUFFER, frameBuffer->object));
glCheck(GLEXT_glBlitFramebuffer(0,
0,
static_cast<GLint>(m_size.x),
@ -649,7 +584,8 @@ void RenderTextureImplFBO::updateTexture(unsigned int)
static_cast<GLint>(m_size.y),
GL_COLOR_BUFFER_BIT,
GL_NEAREST));
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_DRAW_FRAMEBUFFER, multisampleIt->second));
glCheck(GLEXT_glBindFramebuffer(GLEXT_GL_DRAW_FRAMEBUFFER, multiSampleFrameBuffer->object));
}
}
}

View File

@ -137,8 +137,12 @@ private:
////////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
std::unordered_map<std::uint64_t, unsigned int> m_frameBuffers; //!< OpenGL frame buffer objects per context
std::unordered_map<std::uint64_t, unsigned int> m_multisampleFrameBuffers; //!< Optional per-context OpenGL frame buffer objects with multisample attachments
struct FrameBufferObject;
using FrameBufferObjectMap = std::unordered_map<std::uint64_t, std::weak_ptr<FrameBufferObject>>;
FrameBufferObjectMap m_frameBuffers; //!< OpenGL frame buffer objects per context
FrameBufferObjectMap m_multisampleFrameBuffers; //!< Optional per-context OpenGL frame buffer objects with multisample attachments
unsigned int m_depthStencilBuffer{}; //!< Optional depth/stencil buffer attached to the frame buffer
unsigned int m_colorBuffer{}; //!< Optional multisample color buffer attached to the frame buffer
Vector2u m_size; //!< Width and height of the attachments

View File

@ -40,7 +40,6 @@
#include <fstream>
#include <iomanip>
#include <mutex>
#include <ostream>
#include <utility>
#include <vector>
@ -61,20 +60,17 @@
namespace
{
std::recursive_mutex isAvailableMutex;
GLint checkMaxTextureUnits()
{
GLint maxUnits = 0;
glCheck(glGetIntegerv(GLEXT_GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits));
return maxUnits;
}
// Retrieve the maximum number of texture units available
std::size_t getMaxTextureUnits()
{
static const GLint maxUnits = checkMaxTextureUnits();
static const GLint maxUnits = []()
{
GLint value = 0;
glCheck(glGetIntegerv(GLEXT_GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &value));
return value;
}();
return static_cast<std::size_t>(maxUnits);
}
@ -747,23 +743,16 @@ void Shader::bind(const Shader* shader)
////////////////////////////////////////////////////////////
bool Shader::isAvailable()
{
const std::lock_guard lock(isAvailableMutex);
static bool checked = false;
static bool available = false;
if (!checked)
static const bool available = []()
{
checked = true;
const TransientContextLock contextLock;
// Make sure that extensions are initialized
sf::priv::ensureExtensionsInit();
available = GLEXT_multitexture && GLEXT_shading_language_100 && GLEXT_shader_objects && GLEXT_vertex_shader &&
return GLEXT_multitexture && GLEXT_shading_language_100 && GLEXT_shader_objects && GLEXT_vertex_shader &&
GLEXT_fragment_shader;
}
}();
return available;
}
@ -772,22 +761,15 @@ bool Shader::isAvailable()
////////////////////////////////////////////////////////////
bool Shader::isGeometryAvailable()
{
const std::lock_guard lock(isAvailableMutex);
static bool checked = false;
static bool available = false;
if (!checked)
static const bool available = []()
{
checked = true;
const TransientContextLock contextLock;
// Make sure that extensions are initialized
sf::priv::ensureExtensionsInit();
available = isAvailable() && (GLEXT_geometry_shader4 || GLEXT_GL_VERSION_3_2);
}
return isAvailable() && (GLEXT_geometry_shader4 || GLEXT_GL_VERSION_3_2);
}();
return available;
}

View File

@ -71,6 +71,7 @@ Texture::Texture() : m_cacheId(TextureImpl::getUniqueId())
////////////////////////////////////////////////////////////
Texture::Texture(const Texture& copy) :
GlResource(copy),
m_isSmooth(copy.m_isSmooth),
m_sRgb(copy.m_sRgb),
m_isRepeated(copy.m_isRepeated),

View File

@ -84,7 +84,10 @@ VertexBuffer::VertexBuffer(PrimitiveType type, Usage usage) : m_primitiveType(ty
////////////////////////////////////////////////////////////
VertexBuffer::VertexBuffer(const VertexBuffer& copy) : m_primitiveType(copy.m_primitiveType), m_usage(copy.m_usage)
VertexBuffer::VertexBuffer(const VertexBuffer& copy) :
GlResource(copy),
m_primitiveType(copy.m_primitiveType),
m_usage(copy.m_usage)
{
if (copy.m_buffer && copy.m_size)
{

View File

@ -167,17 +167,6 @@ namespace
// A nested named namespace is used here to allow unity builds of SFML.
namespace GlContextImpl
{
// AMD drivers have issues with internal synchronization
// We need to make sure that no operating system context
// or pixel format operations are performed simultaneously
// This mutex is also used to protect the shared context
// from being locked on multiple threads and for managing
// the resource count
std::recursive_mutex mutex;
// OpenGL resources counter
unsigned int resourceCount = 0;
// This structure contains all the state necessary to
// track current context information for each thread
struct CurrentContext
@ -185,101 +174,87 @@ struct CurrentContext
std::uint64_t id{};
sf::priv::GlContext* ptr{};
unsigned int transientCount{};
};
// This per-thread variable holds the current context information for each thread
static CurrentContext& get()
{
thread_local CurrentContext currentContext;
return currentContext;
}
// The hidden, inactive context that will be shared with all other contexts
std::unique_ptr<ContextType> sharedContext;
private:
// Private constructor to prevent CurrentContext from being constructed outside of get()
CurrentContext() = default;
};
} // namespace GlContextImpl
} // namespace
// Unique identifier, used for identifying contexts when managing unshareable OpenGL resources
std::atomic<std::uint64_t> nextContextId(1); // start at 1, zero is "no context"
// Set containing callback functions to be called whenever a
// context is going to be destroyed
// Unshareable OpenGL resources rely on this to clean up properly
// whenever a context containing them is destroyed
using ContextDestroyCallbacks = std::unordered_map<sf::ContextDestroyCallback, void*>;
ContextDestroyCallbacks contextDestroyCallbacks;
namespace sf::priv
{
// This structure contains all the state necessary to
// track TransientContext usage
struct TransientContext
// track SharedContext usage
struct GlContext::SharedContext
{
////////////////////////////////////////////////////////////
/// \brief Constructor
///
////////////////////////////////////////////////////////////
TransientContext()
SharedContext()
{
// TransientContext should never be created if there is
// already a context active on the current thread
assert(!currentContext.id);
const std::lock_guard lock(mutex);
std::unique_lock lock(mutex);
context.emplace(nullptr);
context->initialize(sf::ContextSettings());
if (resourceCount == 0)
{
// No GlResources, no shared context yet
assert(!sharedContext);
loadExtensions();
// Create a Context object for temporary use
context.emplace();
}
else
{
// GlResources exist, currentContextId not yet set
assert(sharedContext);
// Lock the shared context for temporary use
sharedContextLock = std::move(lock);
sharedContext->setActive(true);
}
context->setActive(false);
}
////////////////////////////////////////////////////////////
/// \brief Destructor
/// \brief Get weak_ptr
///
/// \return weak_ptr to the SharedContext
///
////////////////////////////////////////////////////////////
~TransientContext()
static std::weak_ptr<SharedContext>& getWeakPtr()
{
if (sharedContextLock)
sharedContext->setActive(false);
static std::weak_ptr<SharedContext> weakSharedContext;
return weakSharedContext;
}
////////////////////////////////////////////////////////////
/// \brief Deleted copy constructor
/// \brief Get shared_ptr to the shared context
///
/// Create new object if one doesn't already exist.
///
/// \return shared_ptr to the shared context
///
////////////////////////////////////////////////////////////
TransientContext(const TransientContext&) = delete;
static std::shared_ptr<SharedContext> get()
{
auto& weakSharedContext = getWeakPtr();
auto sharedContext = weakSharedContext.lock();
if (!sharedContext)
{
sharedContext = std::make_shared<GlContext::SharedContext>();
weakSharedContext = sharedContext;
}
return sharedContext;
}
////////////////////////////////////////////////////////////
/// \brief Deleted copy assignment
/// \brief Load our extensions vector with the supported extensions
///
////////////////////////////////////////////////////////////
TransientContext& operator=(const TransientContext&) = delete;
///////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
std::optional<sf::Context> context;
std::unique_lock<std::recursive_mutex> sharedContextLock;
};
// This per-thread variable tracks if and how a transient
// context is currently being used on the current thread
thread_local std::optional<TransientContext> transientContext;
// Supported OpenGL extensions
std::vector<std::string> extensions;
// Load our extensions vector with the supported extensions
void loadExtensions()
{
auto glGetErrorFunc = reinterpret_cast<glGetErrorFuncType>(sf::priv::GlContext::getFunction("glGetError"));
auto glGetIntegervFunc = reinterpret_cast<glGetIntegervFuncType>(sf::priv::GlContext::getFunction("glGetIntegerv"));
auto glGetStringFunc = reinterpret_cast<glGetStringFuncType>(sf::priv::GlContext::getFunction("glGetString"));
auto glGetErrorFunc = reinterpret_cast<glGetErrorFuncType>(getFunction("glGetError"));
auto glGetIntegervFunc = reinterpret_cast<glGetIntegervFuncType>(getFunction("glGetIntegerv"));
auto glGetStringFunc = reinterpret_cast<glGetStringFuncType>(getFunction("glGetString"));
if (!glGetErrorFunc || !glGetIntegervFunc || !glGetStringFunc)
return;
@ -288,7 +263,7 @@ void loadExtensions()
int majorVersion = 0;
glGetIntegervFunc(GL_MAJOR_VERSION, &majorVersion);
auto glGetStringiFunc = reinterpret_cast<glGetStringiFuncType>(sf::priv::GlContext::getFunction("glGetStringi"));
auto glGetStringiFunc = reinterpret_cast<glGetStringiFuncType>(getFunction("glGetStringi"));
if (glGetErrorFunc() == GL_INVALID_ENUM || !majorVersion || !glGetStringiFunc)
{
@ -331,105 +306,211 @@ void loadExtensions()
}
}
// Helper to parse OpenGL version strings
bool parseVersionString(const char* version, const char* prefix, unsigned int& major, unsigned int& minor)
{
const std::size_t prefixLength = std::strlen(prefix);
// AMD drivers have issues with internal synchronization
// We need to make sure that no operating system context
// or pixel format operations are performed simultaneously
// This mutex is also used to protect the shared context
// from being locked on multiple threads
std::recursive_mutex mutex;
if ((std::strlen(version) >= (prefixLength + 3)) && (std::strncmp(version, prefix, prefixLength) == 0) &&
std::isdigit(version[prefixLength]) && (version[prefixLength + 1] == '.') &&
std::isdigit(version[prefixLength + 2]))
{
major = static_cast<unsigned int>(version[prefixLength] - '0');
minor = static_cast<unsigned int>(version[prefixLength + 2] - '0');
// Supported OpenGL extensions
std::vector<std::string> extensions;
return true;
}
return false;
}
} // namespace GlContextImpl
} // namespace
// The hidden, inactive context that will be shared with all other contexts
std::optional<ContextType> context;
};
namespace sf::priv
// This structure contains all the state necessary to
// track TransientContext usage
struct GlContext::TransientContext
{
////////////////////////////////////////////////////////////
void GlContext::initResource()
{
using GlContextImpl::loadExtensions;
using GlContextImpl::mutex;
using GlContextImpl::resourceCount;
using GlContextImpl::sharedContext;
// Protect from concurrent access
const std::lock_guard lock(mutex);
// If this is the very first resource, trigger the global context initialization
if (resourceCount == 0)
{
if (sharedContext)
{
// Increment the resources counter
++resourceCount;
return;
}
// Create the shared context
sharedContext = std::make_unique<ContextType>(nullptr);
sharedContext->initialize(ContextSettings());
// Load our extensions vector
loadExtensions();
// Deactivate the shared context so that others can activate it when necessary
sharedContext->setActive(false);
}
// Increment the resources counter
++resourceCount;
}
/// \brief Constructor
///
////////////////////////////////////////////////////////////
void GlContext::cleanupResource()
TransientContext()
{
using GlContextImpl::mutex;
using GlContextImpl::resourceCount;
using GlContextImpl::sharedContext;
// TransientContext should never be created if there is
// already a context active on the current thread
assert(!GlContextImpl::CurrentContext::get().id);
// Protect from concurrent access
const std::lock_guard lock(mutex);
// Lock ourselves so we don't create a new object if one doesn't already exist
sharedContext = SharedContext::getWeakPtr().lock();
// Decrement the resources counter
--resourceCount;
// If there's no more resource alive, we can trigger the global context cleanup
if (resourceCount == 0)
{
if (!sharedContext)
return;
// Destroy the shared context
sharedContext.reset();
{
// Create a Context object for temporary use
context.emplace();
}
else
{
// GlResources exist, currentContextId not yet set
assert(sharedContext);
// Lock the shared context for temporary use
sharedContextLock = std::unique_lock(sharedContext->mutex);
sharedContext->context->setActive(true);
}
}
////////////////////////////////////////////////////////////
/// \brief Destructor
///
////////////////////////////////////////////////////////////
~TransientContext()
{
if (sharedContextLock)
sharedContext->context->setActive(false);
}
////////////////////////////////////////////////////////////
/// \brief Deleted copy constructor
///
////////////////////////////////////////////////////////////
TransientContext(const TransientContext&) = delete;
////////////////////////////////////////////////////////////
/// \brief Deleted copy assignment
///
////////////////////////////////////////////////////////////
TransientContext& operator=(const TransientContext&) = delete;
////////////////////////////////////////////////////////////
/// \brief Get the thread local TransientContext
///
/// This per-thread variable tracks if and how a transient
/// context is currently being used on the current thread
///
/// \return The thread local TransientContext
///
////////////////////////////////////////////////////////////
static std::optional<TransientContext>& get()
{
thread_local std::optional<TransientContext> transientContext;
return transientContext;
}
///////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
std::optional<sf::Context> context;
std::unique_lock<std::recursive_mutex> sharedContextLock;
std::shared_ptr<SharedContext> sharedContext;
};
// This structure contains all the implementation data we
// don't want to expose through the visible interface
struct GlContext::Impl
{
////////////////////////////////////////////////////////////
/// \brief Constructor
///
////////////////////////////////////////////////////////////
Impl() :
m_id(
[]()
{
static std::atomic<std::uint64_t> id(1); // start at 1, zero is "no context"
return id.fetch_add(1);
}())
{
auto& weakUnsharedGlObjects = getWeakUnsharedGlObjects();
unsharedGlObjects = weakUnsharedGlObjects.lock();
if (!unsharedGlObjects)
{
unsharedGlObjects = std::make_shared<UnsharedGlObjects>();
weakUnsharedGlObjects = unsharedGlObjects;
}
}
// Structure to track which unshared object belongs to which context
struct UnsharedGlObject
{
std::uint64_t contextId;
std::shared_ptr<void> object;
};
using UnsharedGlObjects = std::vector<UnsharedGlObject>;
////////////////////////////////////////////////////////////
/// \brief Get weak_ptr to unshared objects
///
/// \return weak_ptr to unshared objects
///
////////////////////////////////////////////////////////////
static std::weak_ptr<UnsharedGlObjects>& getWeakUnsharedGlObjects()
{
static std::weak_ptr<UnsharedGlObjects> weakUnsharedGlObjects;
return weakUnsharedGlObjects;
}
////////////////////////////////////////////////////////////
/// \brief Get mutex protecting unshared objects
///
/// \return Mutex protecting unshared objects
///
////////////////////////////////////////////////////////////
static std::mutex& getUnsharedGlObjectsMutex()
{
static std::mutex mutex;
return mutex;
}
///////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
std::shared_ptr<UnsharedGlObjects> unsharedGlObjects; //!< The current object's handle to unshared objects
const std::uint64_t m_id; //!< Unique identifier, used for identifying contexts when managing unshareable OpenGL resources
};
////////////////////////////////////////////////////////////
std::shared_ptr<void> GlContext::getSharedContext()
{
return GlContext::SharedContext::get();
}
////////////////////////////////////////////////////////////
void GlContext::registerContextDestroyCallback(ContextDestroyCallback callback, void* arg)
void GlContext::registerUnsharedGlObject(std::shared_ptr<void> object)
{
GlContextImpl::contextDestroyCallbacks.emplace(callback, arg);
if (const std::lock_guard lock(Impl::getUnsharedGlObjectsMutex());
auto unsharedGlObjects = Impl::getWeakUnsharedGlObjects().lock())
unsharedGlObjects->emplace_back(Impl::UnsharedGlObject{GlContextImpl::CurrentContext::get().id, std::move(object)});
}
////////////////////////////////////////////////////////////
void GlContext::unregisterUnsharedGlObject(std::shared_ptr<void> object)
{
if (const std::lock_guard lock(Impl::getUnsharedGlObjectsMutex());
auto unsharedGlObjects = Impl::getWeakUnsharedGlObjects().lock())
{
// Find the object in unshared objects and remove it if its associated context is currently active
// This will trigger the destructor of the object since shared_ptr
// in unshared objects should be the only one existing
auto iter = std::find_if(unsharedGlObjects->begin(),
unsharedGlObjects->end(),
[&](const Impl::UnsharedGlObject& obj) {
return (obj.object == object) &&
(obj.contextId == GlContextImpl::CurrentContext::get().id);
});
if (iter != unsharedGlObjects->end())
unsharedGlObjects->erase(iter);
}
}
////////////////////////////////////////////////////////////
void GlContext::acquireTransientContext()
{
using GlContextImpl::currentContext;
using GlContextImpl::TransientContext;
using GlContextImpl::transientContext;
auto& currentContext = GlContextImpl::CurrentContext::get();
// Fast path if we already have a context active on this thread
if (currentContext.id)
@ -443,7 +524,7 @@ void GlContext::acquireTransientContext()
// If currentContextId is not set, this must be the first
// TransientContextLock on this thread, construct the state object
transientContext.emplace();
TransientContext::get().emplace();
// Make sure a context is active at this point
assert(currentContext.id);
@ -453,8 +534,7 @@ void GlContext::acquireTransientContext()
////////////////////////////////////////////////////////////
void GlContext::releaseTransientContext()
{
using GlContextImpl::currentContext;
using GlContextImpl::transientContext;
auto& currentContext = GlContextImpl::CurrentContext::get();
// Make sure a context was left active after acquireTransientContext() was called
assert(currentContext.id);
@ -468,34 +548,29 @@ void GlContext::releaseTransientContext()
// If currentContextId is set and currentContextTransientCount is 0,
// this is the last TransientContextLock that is released, destroy the state object
transientContext.reset();
TransientContext::get().reset();
}
////////////////////////////////////////////////////////////
std::unique_ptr<GlContext> GlContext::create()
{
using GlContextImpl::mutex;
using GlContextImpl::sharedContext;
// Make sure that there's an active context (context creation may need extensions, and thus a valid context)
assert(sharedContext != nullptr);
auto sharedContext = SharedContext::get();
const std::lock_guard lock(mutex);
const std::lock_guard lock(sharedContext->mutex);
std::unique_ptr<GlContext> context;
// We don't use acquireTransientContext here since we have
// to ensure we have exclusive access to the shared context
// in order to make sure it is not active during context creation
{
sharedContext->setActive(true);
sharedContext->context->setActive(true);
// Create the context
context = std::make_unique<ContextType>(sharedContext.get());
context = std::make_unique<ContextType>(&sharedContext->context.value());
sharedContext->setActive(false);
}
sharedContext->context->setActive(false);
context->initialize(ContextSettings());
@ -506,31 +581,26 @@ std::unique_ptr<GlContext> GlContext::create()
////////////////////////////////////////////////////////////
std::unique_ptr<GlContext> GlContext::create(const ContextSettings& settings, const WindowImpl& owner, unsigned int bitsPerPixel)
{
using GlContextImpl::loadExtensions;
using GlContextImpl::mutex;
using GlContextImpl::resourceCount;
using GlContextImpl::sharedContext;
// Make sure that there's an active context (context creation may need extensions, and thus a valid context)
assert(sharedContext != nullptr);
auto sharedContext = SharedContext::get();
const std::lock_guard lock(mutex);
const std::lock_guard lock(sharedContext->mutex);
// If resourceCount is 1 we know that we are inside sf::Context or sf::Window
// If use_count is 2 (GlResource + sharedContext) we know that we are inside sf::Context or sf::Window
// Only in this situation we allow the user to indirectly re-create the shared context as a core context
// Check if we need to convert our shared context into a core context
if ((resourceCount == 1) && (settings.attributeFlags & ContextSettings::Core) &&
!(sharedContext->m_settings.attributeFlags & ContextSettings::Core))
if ((sharedContext.use_count() == 2) && (settings.attributeFlags & ContextSettings::Core) &&
!(sharedContext->context->m_settings.attributeFlags & ContextSettings::Core))
{
// Re-create our shared context as a core context
const ContextSettings sharedSettings(0, 0, 0, settings.majorVersion, settings.minorVersion, settings.attributeFlags);
sharedContext = std::make_unique<ContextType>(nullptr, sharedSettings, Vector2u(1, 1));
sharedContext->initialize(sharedSettings);
sharedContext->context.emplace(nullptr, sharedSettings, Vector2u(1, 1));
sharedContext->context->initialize(sharedSettings);
// Reload our extensions vector
loadExtensions();
sharedContext->loadExtensions();
}
std::unique_ptr<GlContext> context;
@ -538,14 +608,12 @@ std::unique_ptr<GlContext> GlContext::create(const ContextSettings& settings, co
// We don't use acquireTransientContext here since we have
// to ensure we have exclusive access to the shared context
// in order to make sure it is not active during context creation
{
sharedContext->setActive(true);
sharedContext->context->setActive(true);
// Create the context
context = std::make_unique<ContextType>(sharedContext.get(), settings, owner, bitsPerPixel);
context = std::make_unique<ContextType>(&sharedContext->context.value(), settings, owner, bitsPerPixel);
sharedContext->setActive(false);
}
sharedContext->context->setActive(false);
context->initialize(settings);
context->checkSettings(settings);
@ -557,31 +625,26 @@ std::unique_ptr<GlContext> GlContext::create(const ContextSettings& settings, co
////////////////////////////////////////////////////////////
std::unique_ptr<GlContext> GlContext::create(const ContextSettings& settings, const Vector2u& size)
{
using GlContextImpl::loadExtensions;
using GlContextImpl::mutex;
using GlContextImpl::resourceCount;
using GlContextImpl::sharedContext;
// Make sure that there's an active context (context creation may need extensions, and thus a valid context)
assert(sharedContext != nullptr);
auto sharedContext = SharedContext::get();
const std::lock_guard lock(mutex);
const std::lock_guard lock(sharedContext->mutex);
// If resourceCount is 1 we know that we are inside sf::Context or sf::Window
// If use_count is 2 (GlResource + sharedContext) we know that we are inside sf::Context or sf::Window
// Only in this situation we allow the user to indirectly re-create the shared context as a core context
// Check if we need to convert our shared context into a core context
if ((resourceCount == 1) && (settings.attributeFlags & ContextSettings::Core) &&
!(sharedContext->m_settings.attributeFlags & ContextSettings::Core))
if ((sharedContext.use_count() == 2) && (settings.attributeFlags & ContextSettings::Core) &&
!(sharedContext->context->m_settings.attributeFlags & ContextSettings::Core))
{
// Re-create our shared context as a core context
const ContextSettings sharedSettings(0, 0, 0, settings.majorVersion, settings.minorVersion, settings.attributeFlags);
sharedContext = std::make_unique<ContextType>(nullptr, sharedSettings, Vector2u(1, 1));
sharedContext->initialize(sharedSettings);
sharedContext->context.emplace(nullptr, sharedSettings, Vector2u(1, 1));
sharedContext->context->initialize(sharedSettings);
// Reload our extensions vector
loadExtensions();
sharedContext->loadExtensions();
}
std::unique_ptr<GlContext> context;
@ -589,14 +652,12 @@ std::unique_ptr<GlContext> GlContext::create(const ContextSettings& settings, co
// We don't use acquireTransientContext here since we have
// to ensure we have exclusive access to the shared context
// in order to make sure it is not active during context creation
{
sharedContext->setActive(true);
sharedContext->context->setActive(true);
// Create the context
context = std::make_unique<ContextType>(sharedContext.get(), settings, size);
context = std::make_unique<ContextType>(&sharedContext->context.value(), settings, size);
sharedContext->setActive(false);
}
sharedContext->context->setActive(false);
context->initialize(settings);
context->checkSettings(settings);
@ -608,15 +669,27 @@ std::unique_ptr<GlContext> GlContext::create(const ContextSettings& settings, co
////////////////////////////////////////////////////////////
bool GlContext::isExtensionAvailable(const char* name)
{
using GlContextImpl::extensions;
return std::find(extensions.begin(), extensions.end(), name) != extensions.end();
// If this function is called before any context is available,
// the shared context will be created for the duration of this call
const auto sharedContext = SharedContext::get();
return std::find(sharedContext->extensions.begin(), sharedContext->extensions.end(), name) !=
sharedContext->extensions.end();
}
////////////////////////////////////////////////////////////
GlFunctionPointer GlContext::getFunction(const char* name)
{
const std::lock_guard lock(GlContextImpl::mutex);
// Make sure we don't try to create the shared context here since
// setActive can be called during construction and lead to infinite recursion
auto* sharedContext = SharedContext::getWeakPtr().lock().get();
// We can't and don't need to lock when we are currently creating the shared context
std::unique_lock<std::recursive_mutex> lock;
if (sharedContext)
lock = std::unique_lock(sharedContext->mutex);
return ContextType::getFunction(name);
}
@ -625,27 +698,23 @@ GlFunctionPointer GlContext::getFunction(const char* name)
////////////////////////////////////////////////////////////
const GlContext* GlContext::getActiveContext()
{
using GlContextImpl::currentContext;
return currentContext.ptr;
return GlContextImpl::CurrentContext::get().ptr;
}
////////////////////////////////////////////////////////////
std::uint64_t GlContext::getActiveContextId()
{
using GlContextImpl::currentContext;
return currentContext.id;
return GlContextImpl::CurrentContext::get().id;
}
////////////////////////////////////////////////////////////
GlContext::~GlContext()
{
using GlContextImpl::currentContext;
auto& currentContext = GlContextImpl::CurrentContext::get();
if (m_id == currentContext.id)
if (m_impl->m_id == currentContext.id)
{
currentContext.id = 0;
currentContext.ptr = nullptr;
@ -663,20 +732,27 @@ const ContextSettings& GlContext::getSettings() const
////////////////////////////////////////////////////////////
bool GlContext::setActive(bool active)
{
using GlContextImpl::currentContext;
using GlContextImpl::mutex;
auto& currentContext = GlContextImpl::CurrentContext::get();
// Make sure we don't try to create the shared context here since
// setActive can be called during construction and lead to infinite recursion
auto* sharedContext = SharedContext::getWeakPtr().lock().get();
if (active)
{
if (m_id != currentContext.id)
if (m_impl->m_id != currentContext.id)
{
const std::lock_guard lock(mutex);
// We can't and don't need to lock when we are currently creating the shared context
std::unique_lock<std::recursive_mutex> lock;
if (sharedContext)
lock = std::unique_lock(sharedContext->mutex);
// Activate the context
if (makeCurrent(true))
{
// Set it as the new current context for this thread
currentContext.id = m_id;
currentContext.id = m_impl->m_id;
currentContext.ptr = this;
return true;
}
@ -693,9 +769,13 @@ bool GlContext::setActive(bool active)
}
else
{
if (m_id == currentContext.id)
if (m_impl->m_id == currentContext.id)
{
const std::lock_guard lock(mutex);
// We can't and don't need to lock when we are currently creating the shared context
std::unique_lock<std::recursive_mutex> lock;
if (sharedContext)
lock = std::unique_lock(sharedContext->mutex);
// Deactivate the context
if (makeCurrent(false))
@ -719,7 +799,7 @@ bool GlContext::setActive(bool active)
////////////////////////////////////////////////////////////
GlContext::GlContext() : m_id(GlContextImpl::nextContextId.fetch_add(1))
GlContext::GlContext() : m_impl(std::make_unique<Impl>())
{
// Nothing to do
}
@ -765,23 +845,34 @@ int GlContext::evaluateFormat(
////////////////////////////////////////////////////////////
void GlContext::cleanupUnsharedResources()
{
using GlContextImpl::ContextDestroyCallbacks;
using GlContextImpl::contextDestroyCallbacks;
using GlContextImpl::currentContext;
const auto& currentContext = GlContextImpl::CurrentContext::get();
// Save the current context so we can restore it later
GlContext* contextToRestore = currentContext.ptr;
// If this context is already active there is no need to save it
if (m_id == currentContext.id)
if (m_impl->m_id == currentContext.id)
contextToRestore = nullptr;
// Make this context active so resources can be freed
setActive(true);
// Call the registered destruction callbacks
for (auto& [callback, ptr] : contextDestroyCallbacks)
callback(ptr);
{
const std::lock_guard lock(Impl::getUnsharedGlObjectsMutex());
// Destroy the unshared objects contained in this context
for (auto iter = m_impl->unsharedGlObjects->begin(); iter != m_impl->unsharedGlObjects->end();)
{
if (iter->contextId == m_impl->m_id)
{
iter = m_impl->unsharedGlObjects->erase(iter);
}
else
{
++iter;
}
}
}
// Make the originally active context active again
if (contextToRestore)
@ -815,7 +906,7 @@ void GlContext::initialize(const ContextSettings& requestedSettings)
glGetIntegervFunc(GL_MAJOR_VERSION, &majorVersion);
glGetIntegervFunc(GL_MINOR_VERSION, &minorVersion);
if ((glGetErrorFunc() != GL_INVALID_ENUM) && (majorVersion != 0))
if (glGetErrorFunc() != GL_INVALID_ENUM)
{
m_settings.majorVersion = static_cast<unsigned int>(majorVersion);
m_settings.minorVersion = static_cast<unsigned int>(minorVersion);
@ -836,7 +927,24 @@ void GlContext::initialize(const ContextSettings& requestedSettings)
// OpenGL ES Full profile: The beginning of the returned string is "OpenGL ES major.minor"
// Desktop OpenGL: The beginning of the returned string is "major.minor"
using GlContextImpl::parseVersionString;
// Helper to parse OpenGL version strings
static const auto parseVersionString =
[](const char* versionString, const char* prefix, unsigned int& major, unsigned int& minor)
{
const std::size_t prefixLength = std::strlen(prefix);
if ((std::strlen(versionString) >= (prefixLength + 3)) &&
(std::strncmp(versionString, prefix, prefixLength) == 0) && std::isdigit(versionString[prefixLength]) &&
(versionString[prefixLength + 1] == '.') && std::isdigit(versionString[prefixLength + 2]))
{
major = static_cast<unsigned int>(versionString[prefixLength] - '0');
minor = static_cast<unsigned int>(versionString[prefixLength + 2] - '0');
return true;
}
return false;
};
if (!parseVersionString(version, "OpenGL ES-CL ", m_settings.majorVersion, m_settings.minorVersion) &&
!parseVersionString(version, "OpenGL ES-CM ", m_settings.majorVersion, m_settings.minorVersion) &&

View File

@ -50,38 +50,32 @@ class GlContext
{
public:
////////////////////////////////////////////////////////////
/// \brief Perform resource initialization
/// \brief Get a shared_ptr to the shared context
///
/// This function is called every time an OpenGL resource is
/// created. When the first resource is initialized, it makes
/// sure that everything is ready for contexts to work properly.
/// \return shared_ptr to the shared context
///
////////////////////////////////////////////////////////////
static void initResource();
static std::shared_ptr<void> getSharedContext();
////////////////////////////////////////////////////////////
/// \brief Perform resource cleanup
///
/// This function is called every time an OpenGL resource is
/// destroyed. When the last resource is destroyed, it makes
/// sure that everything that was created by initResource()
/// is properly released.
///
////////////////////////////////////////////////////////////
static void cleanupResource();
////////////////////////////////////////////////////////////
/// \brief Register a function to be called when a context is destroyed
/// \brief Register an OpenGL object to be destroyed when its containing context is destroyed
///
/// This is used for internal purposes in order to properly
/// clean up OpenGL resources that cannot be shared bwteen
/// contexts.
///
/// \param callback Function to be called when a context is destroyed
/// \param arg Argument to pass when calling the function
/// \param object Object to be destroyed when its containing context is destroyed
///
////////////////////////////////////////////////////////////
static void registerContextDestroyCallback(ContextDestroyCallback callback, void* arg);
static void registerUnsharedGlObject(std::shared_ptr<void> object);
////////////////////////////////////////////////////////////
/// \brief Unregister an OpenGL object from its containing context
///
/// \param object Object to be unregister
///
////////////////////////////////////////////////////////////
static void unregisterUnsharedGlObject(std::shared_ptr<void> object);
////////////////////////////////////////////////////////////
/// \brief Acquires a context for short-term use on the current thread
@ -300,6 +294,10 @@ protected:
ContextSettings m_settings; //!< Creation settings of the context
private:
struct TransientContext;
struct SharedContext;
struct Impl;
////////////////////////////////////////////////////////////
/// \brief Perform various initializations after the context construction
/// \param requestedSettings Requested settings during context creation
@ -317,7 +315,7 @@ private:
////////////////////////////////////////////////////////////
// Member data
////////////////////////////////////////////////////////////
const std::uint64_t m_id; //!< Unique number that identifies the context
const std::unique_ptr<Impl> m_impl; //!< Implementation details
};
} // namespace sf::priv

View File

@ -32,23 +32,22 @@
namespace sf
{
////////////////////////////////////////////////////////////
GlResource::GlResource()
GlResource::GlResource() : m_sharedContext(priv::GlContext::getSharedContext())
{
priv::GlContext::initResource();
}
////////////////////////////////////////////////////////////
GlResource::~GlResource()
void GlResource::registerUnsharedGlObject(std::shared_ptr<void> object)
{
priv::GlContext::cleanupResource();
priv::GlContext::registerUnsharedGlObject(std::move(object));
}
////////////////////////////////////////////////////////////
void GlResource::registerContextDestroyCallback(ContextDestroyCallback callback, void* arg)
void GlResource::unregisterUnsharedGlObject(std::shared_ptr<void> object)
{
priv::GlContext::registerContextDestroyCallback(callback, arg);
priv::GlContext::unregisterUnsharedGlObject(std::move(object));
}

View File

@ -3,7 +3,7 @@
#include <type_traits>
static_assert(!std::is_constructible_v<sf::GlResource>);
static_assert(!std::is_copy_constructible_v<sf::GlResource>);
static_assert(std::is_copy_constructible_v<sf::GlResource>);
static_assert(std::is_copy_assignable_v<sf::GlResource>);
static_assert(!std::is_nothrow_move_constructible_v<sf::GlResource>);
static_assert(std::is_nothrow_move_constructible_v<sf::GlResource>);
static_assert(std::is_nothrow_move_assignable_v<sf::GlResource>);