Skip to content

Instantly share code, notes, and snippets.

@vittorioromeo
Created May 2, 2025 16:53
Show Gist options
  • Save vittorioromeo/d58202a947ed0f6dbfca34757b7b7fd2 to your computer and use it in GitHub Desktop.
Save vittorioromeo/d58202a947ed0f6dbfca34757b7b7fd2 to your computer and use it in GitHub Desktop.
// Compile on MSYS2 with:
// gcc ./fboBug.c -lSDL3 -lopengl32
#include <SDL3/SDL.h>
#include <SDL3/SDL_opengl.h>
#include <assert.h>
#include <stdio.h>
#define glCheck(...) \
do \
{ \
const unsigned int openGlError = glGetError(); \
\
__VA_ARGS__; \
\
while (!glCheckError(openGlError, __FILE__, __LINE__, #__VA_ARGS__)) \
/* no-op */; \
\
} while (false)
static bool glCheckError(const unsigned int openGlError, const char* const file, const unsigned int line, const char* const expression)
{
if (openGlError == GL_NO_ERROR)
return true;
printf(
"An internal OpenGL call failed in %s(%u).\n"
"Expression:\n %s\n"
"Error description:\n %s\n %s\n",
file,
line,
expression,
"TODO: ERROR",
"TODO: DESCRIPTION");
abort();
return false;
}
PFNGLGENFRAMEBUFFERSPROC pglGenFramebuffers = NULL;
PFNGLBINDFRAMEBUFFERPROC pglBindFramebuffer = NULL;
PFNGLFRAMEBUFFERTEXTURE2DPROC pglFramebufferTexture2D = NULL;
PFNGLCHECKFRAMEBUFFERSTATUSPROC pglCheckFramebufferStatus = NULL;
PFNGLBLITFRAMEBUFFERPROC pglBlitFramebuffer = NULL;
PFNGLDELETEFRAMEBUFFERSPROC pglDeleteFramebuffers = NULL;
PFNGLGETINTEGER64VPROC pglGetIntegerv = NULL;
int main(void)
{
assert(SDL_InitSubSystem(SDL_INIT_VIDEO));
// SDL window associated with the shared OpenGL context
SDL_Window* sharedCtxHiddenWindow = SDL_CreateWindow("", 1, 1, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
assert(sharedCtxHiddenWindow);
// SDL shared OpenGL context handle
SDL_GLContext sharedCtx = SDL_GL_CreateContext(sharedCtxHiddenWindow);
assert(sharedCtx);
// Activate shared context and load GL functions
assert(SDL_GL_MakeCurrent(sharedCtxHiddenWindow, sharedCtx));
// clang-format off
assert(pglGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC)SDL_GL_GetProcAddress("glGenFramebuffers"));
assert(pglBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC)SDL_GL_GetProcAddress("glBindFramebuffer"));
assert(pglFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC)SDL_GL_GetProcAddress("glFramebufferTexture2D"));
assert(pglCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC)SDL_GL_GetProcAddress("glCheckFramebufferStatus"));
assert(pglBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC)SDL_GL_GetProcAddress("glBlitFramebuffer"));
assert(pglDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC)SDL_GL_GetProcAddress("glDeleteFramebuffers"));
assert(pglGetIntegerv = (PFNGLGETINTEGER64VPROC)SDL_GL_GetProcAddress("glGetIntegerv"));
// clang-format on
// SDL window that will be cleared to red
SDL_Window* window = SDL_CreateWindow("", 256, 256, SDL_WINDOW_OPENGL);
assert(window);
// OpenGL context associated with the window
assert(SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1)); // Share shared context with next created context
SDL_GLContext windowCtx = SDL_GL_CreateContext(window);
assert(windowCtx);
// Activate window context and bind default FBO
assert(SDL_GL_MakeCurrent(window, windowCtx));
glCheck(pglBindFramebuffer(GL_FRAMEBUFFER, 0u));
// Create OpenGL texture on the shared context
assert(SDL_GL_MakeCurrent(sharedCtxHiddenWindow, sharedCtx));
GLuint glTexture = 0u;
glCheck(glGenTextures(1, &glTexture));
assert(glTexture);
// Initialize the texture
glCheck(glBindTexture(GL_TEXTURE_2D, glTexture));
glCheck(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL));
// Revert to window context and bind default FBO
assert(SDL_GL_MakeCurrent(window, windowCtx));
glCheck(pglBindFramebuffer(GL_FRAMEBUFFER, 0u));
// Bind the texture to the window context and clear to red
glCheck(glClearColor(1.f, 0.f, 0.f, 1.f));
glCheck(glClear(GL_COLOR_BUFFER_BIT));
// Intentionally not calling `SDL_GL_SwapWindow`
// Create destination framebuffer and bind the previously created texture to it
GLuint destFrameBuffer = 0u;
glCheck(pglGenFramebuffers(1, &destFrameBuffer));
assert(destFrameBuffer);
// Bind FBOs: read from the window FBO, write to the texture FBO
glCheck(pglBindFramebuffer(GL_READ_FRAMEBUFFER, 0u /* default FBO */));
glCheck(pglBindFramebuffer(GL_DRAW_FRAMEBUFFER, destFrameBuffer));
glCheck(pglFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, glTexture, 0));
assert(pglCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);
assert(pglCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);
// (!) This context switching should be idempotent, but commenting out `sharedCtxPtr.makeCurrent(true)` FIXES THE ISSUE (!)
assert(SDL_GL_MakeCurrent(sharedCtxHiddenWindow, sharedCtx));
assert(SDL_GL_MakeCurrent(window, windowCtx));
// (!) Uncommenting this line fixes the issue even if the context switching or the query is not commented out (!)
// glCheck(pglBindFramebuffer(GL_FRAMEBUFFER, 0u));
// (!) This query should be a noop, but commenting out `glGetIntegerv` FIXES THE ISSUE (!)
GLint out = 0u;
glCheck(glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &out));
// (!) EITHER the context switching or the query must be commented out to FIX THE ISSUE (!)
// (!) ALTERNATIVELY, the line `glCheck(pglBindFramebuffer(GL_FRAMEBUFFER, 0u))` must be uncommented (!)
// Blit the framebuffer from the window FBO to the texture FBO, then delete the framebuffer
glCheck(pglBindFramebuffer(GL_READ_FRAMEBUFFER, 0u));
glCheck(pglBindFramebuffer(GL_DRAW_FRAMEBUFFER, destFrameBuffer));
glCheck(pglBlitFramebuffer(0, 0, 256, 256, 0, 0, 256, 256, GL_COLOR_BUFFER_BIT, GL_NEAREST));
glCheck(pglDeleteFramebuffers(1, &destFrameBuffer));
// Write texture data into a pixel array and read it back
glCheck(glBindTexture(GL_TEXTURE_2D, glTexture));
unsigned char pixels[256 * 256 * 4] = {0};
GLuint imageFBO = 0u;
glCheck(pglGenFramebuffers(1, &imageFBO));
assert(imageFBO);
glCheck(pglBindFramebuffer(GL_FRAMEBUFFER, imageFBO));
glCheck(pglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, glTexture, 0));
glCheck(glReadPixels(0, 0, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
glCheck(pglDeleteFramebuffers(1, &imageFBO));
const int index = (64 + 64 * 256) * 4; // pixel at (64, 64)
const unsigned char* pixel = &pixels[index];
// This assertion will fail unless
// - EITHER the context switching or the query is commented out
// - OR `glBindFramebuffer(GL_FRAMEBUFFER, 0u)` is uncommented after the context switching
assert(pixel[0] == 255);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment