Multiple GtkGLArea widget

Hello everyone,

I have a question about creating a GtkApplication that uses 4 GtkGLArea widgets. So my question is : Are the widgets sharing the same OpenGL context or not ?

Because the gdk_gl_context_is_shared() function return “TRUE” so my guess is the context is in fact shared, so no need to load everything 4 times. But if I don’t load everything 4 times (for each widget) the rendering process don’t work.

A demo source code :

// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// GTK 4 - Multiple GtkGLArea widget in the same program
// Creation date : march 21st, 2025
// Platform : Debian 12
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// The GL Context is supposed to be shared between the 
// GtkGLArea widgets. If so, why I need to initialize the 
// resources 4 times to make it working ? Also if the 
// context is shared, the openglDebugCallback() should be
// triggered in case of errors.
//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// This the printf() output for "SHOW_BUG = FALSE"
//
// OpenGL Debug Message [131185]: Buffer detailed info: Buffer object 1 (bound to GL_ARRAY_BUFFER_ARB, usage hint is GL_STATIC_DRAW) will use VIDEO memory as the source for buffer object operations.
// Severity: Notification
// Initialized: VAO=1, Shader=51
// Context are Shared!
// Initialized: VAO=1, Shader=54
// Context are Shared!
// Initialized: VAO=1, Shader=57
// Context are Shared!
// Initialized: VAO=1, Shader=60
// Rendering FB = 1
// Render completed FB = 1
// Rendering FB = 2
// Render completed FB = 2
// Rendering FB = 3
// Render completed FB = 3
// Rendering FB = 4
// Render completed FB = 4
//
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// This the printf() output for "SHOW_BUG = TRUE"
//
// OpenGL Debug Message [131185]: Buffer detailed info: Buffer object 1 (bound to GL_ARRAY_BUFFER_ARB, usage hint is GL_STATIC_DRAW) will use VIDEO memory as the source for buffer object operations.
// Severity: Notification
// Initialized: VAO=1, Shader=51
// Context are Shared!
// Context are Shared!
// Context are Shared!
// Rendering FB = 1
// Render completed FB = 1
// Rendering FB = 2
// GL Error: 1282 <-- openglDebugCallback() not triggered !
// Render completed FB = 2
// Rendering FB = 3
// GL Error: 1282 <-- openglDebugCallback() not triggered !
// Render completed FB = 3
// Rendering FB = 4
// GL Error: 1282 <-- openglDebugCallback() not triggered !
// Render completed FB = 4
//
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

#include <gtk/gtk.h>
#include <epoxy/gl.h>

#include <stdio.h>

static int SHOW_BUG = TRUE; // Change this to TRUE to see the problem.

typedef enum { VIEWPORT_A, VIEWPORT_B, VIEWPORT_C, VIEWPORT_D, NUM_VIEWPORTS } ViewportID;

typedef struct {
    GLuint vao, shader;
    GtkGLArea *area;
    float colors[4];
} WidgetResources;

static WidgetResources resources[NUM_VIEWPORTS] = {0};
static GtkWidget *gl_areas[NUM_VIEWPORTS];
static GdkGLContext* SharedContext = NULL;

static const char *triangle_vertex_src =
    "#version 330 core\n"
    "layout(location = 0) in vec2 aPos;\n"
    "void main() {\n"
    "    gl_Position = vec4(aPos, 0.0, 1.0);\n"
    "}\n";

static const char *triangle_fragment_src =
    "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main() {\n"
    "    FragColor = vec4(1.0, 1.0, 0.0, 1.0);\n" // Yellow triangle
    "}\n";

static void init_resources(WidgetResources *res) 
{
	gtk_gl_area_make_current(res->area);
    glGenVertexArrays(1, &res->vao);
    glBindVertexArray(res->vao);
    
	static GLfloat vertices[] = {-0.5f, -0.5f, 0.5f, -0.5f, 0.0f, 0.5f};
	
	GLuint vbo;
	glGenBuffers(1, &vbo);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (void*)0);
	glEnableVertexAttribArray(0);
    glBindVertexArray(0);

    const char *vs_src = triangle_vertex_src;
    const char *fs_src = triangle_fragment_src;
    GLuint vs = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vs, 1, &vs_src, NULL);
    glCompileShader(vs);
    GLint success;
    glGetShaderiv(vs, GL_COMPILE_STATUS, &success);
    
    char log[512] = {0};
    
    if (!success) 
    {
		glGetShaderInfoLog(vs, 512, NULL, log);
		printf("VS Error: %s\n", log);
	}

    GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fs, 1, &fs_src, NULL);
    glCompileShader(fs);
    glGetShaderiv(fs, GL_COMPILE_STATUS, &success);
    
    if (!success) 
    {
		glGetShaderInfoLog(fs, 512, NULL, log);
		printf("FS Error: %s\n", log);
	}
	
    res->shader = glCreateProgram();
    glAttachShader(res->shader, vs);
    glAttachShader(res->shader, fs);
    glLinkProgram(res->shader);
    glGetProgramiv(res->shader, GL_LINK_STATUS, &success);
    
    if (!success) 
    {
		glGetProgramInfoLog(res->shader, 512, NULL, log);
		printf("Link Error: %s\n", log);
	}
	
    glDeleteShader(vs);
    glDeleteShader(fs);
    
    printf("Initialized: VAO=%u, Shader=%u\n", res->vao, res->shader);
}

static void openglDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) 
{
	
	printf("OpenGL Debug Message [%u]: %s\n", id, message);
	
	
    if (severity == GL_DEBUG_SEVERITY_HIGH) 
    {
        printf("Severity: High\n");
    } 
    else if (severity == GL_DEBUG_SEVERITY_MEDIUM) 
    {
        printf("Severity: Medium\n");
    } 
    else if (severity == GL_DEBUG_SEVERITY_LOW) 
    {
        printf("Severity: Low\n");
    } 
    else if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) 
    {
        printf("Severity: Notification\n");
    }
}

static void on_realize_callback(GtkWidget* Widget, void* UserData)
{
	gtk_gl_area_make_current(GTK_GL_AREA(Widget));
    
    if (gtk_gl_area_get_error(GTK_GL_AREA(Widget)) != NULL) 
    {
        g_printerr("Failed to create OpenGL context\n");
        return;
    }
    
	if (SharedContext == NULL)
	{
		SharedContext = gtk_gl_area_get_context(GTK_GL_AREA(Widget));
		
		// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
		// If the context is in fact shared, the openglDebugCallback() 
		// will be triggered if an error occur.
		
		glEnable(GL_DEBUG_OUTPUT);
		glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); // Makes sure OpenGL calls the callback immediately after errors
		glDebugMessageCallback(openglDebugCallback, NULL);
	}
	else
	{
		GdkGLContext* Context = gtk_gl_area_get_context(GTK_GL_AREA(Widget));
		
        if (gdk_gl_context_is_shared(SharedContext, Context) == TRUE)
        {
			printf("Context are Shared!\n");
		}
	}
	
	if (SHOW_BUG == FALSE)
	{
		for (int i = 0; i < NUM_VIEWPORTS; i++) 
		{
			if (resources[i].area == GTK_GL_AREA(Widget)) 
			{
				init_resources(&resources[i]);
				break;
			} 
		}
	}
	else
	{
		if (resources[0].area == GTK_GL_AREA(Widget))
		{
			init_resources(&resources[0]);
		}
	}
}

static gboolean render_callback(GtkGLArea *area, GdkGLContext *context, void *user_data) {
    gtk_gl_area_make_current(area);
    GLint fb;
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fb);
    printf("Rendering FB = %d\n", fb);
	
	int idx = -1;
	int idx2 = -1;
	
	if (SHOW_BUG == FALSE)
	{
		for (int i = 0; i < NUM_VIEWPORTS; i++) {
			if (resources[i].area == area) {
				idx = i;
				idx2 = i;
				break;
			} 
		}
	}
	else
	{
		for (int i = 0; i < NUM_VIEWPORTS; i++) {
			if (resources[i].area == area) {
				idx2 = i;
				break;
			} 
		}
		
		idx = 0;
	}


    int width = gtk_widget_get_width(GTK_WIDGET(area));
    int height = gtk_widget_get_height(GTK_WIDGET(area));
    
    glViewport(0, 0, width, height);

    // Clear
    glClearColor(resources[idx2].colors[0], resources[idx2].colors[1], resources[idx2].colors[2], resources[idx2].colors[3]);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Draw
    glUseProgram(resources[idx].shader);
    
    glBindVertexArray(resources[idx].vao);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    GLenum err = glGetError();
    if (err != GL_NO_ERROR) printf("GL Error: %d\n", err);
    glBindVertexArray(0);
    glUseProgram(0);

    glFlush();
    glFinish();
    printf("Render completed FB = %d\n", fb);
    return TRUE;
}

static void activate(GtkApplication *app, gpointer user_data) {
    GtkWidget *window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(window), "GTK 4 GL Bug Demo");
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);

    GtkWidget *grid = gtk_grid_new();
    gtk_window_set_child(GTK_WINDOW(window), grid);
    
    // Connect render callbacks
    for (int i = 0; i < NUM_VIEWPORTS; i++) 
    {
		gl_areas[i] = gtk_gl_area_new();
		resources[i].area = GTK_GL_AREA(gl_areas[i]);
		gtk_gl_area_set_required_version(GTK_GL_AREA(gl_areas[i]), 3, 3);
        gtk_widget_set_size_request(gl_areas[i], 400, 300);
        g_signal_connect(gl_areas[i], "render", G_CALLBACK(render_callback), NULL);
        g_signal_connect(gl_areas[i], "realize", G_CALLBACK(on_realize_callback), NULL);
    }

    // Layout: 2x2 grid
    gtk_grid_attach(GTK_GRID(grid), gl_areas[VIEWPORT_A], 0, 0, 1, 1);
    gtk_grid_attach(GTK_GRID(grid), gl_areas[VIEWPORT_B], 1, 0, 1, 1);
    gtk_grid_attach(GTK_GRID(grid), gl_areas[VIEWPORT_C], 0, 1, 1, 1);
    gtk_grid_attach(GTK_GRID(grid), gl_areas[VIEWPORT_D], 1, 1, 1, 1);

	resources[VIEWPORT_A].colors[0] = 0.0f;
	resources[VIEWPORT_A].colors[1] = 0.0f;
	resources[VIEWPORT_A].colors[2] = 0.0f;
	resources[VIEWPORT_A].colors[3] = 1.0f;
	
	resources[VIEWPORT_B].colors[0] = 1.0f;
	resources[VIEWPORT_B].colors[1] = 0.0f;
	resources[VIEWPORT_B].colors[2] = 0.0f;
	resources[VIEWPORT_B].colors[3] = 1.0f;
	
	resources[VIEWPORT_C].colors[0] = 0.0f;
	resources[VIEWPORT_C].colors[1] = 1.0f;
	resources[VIEWPORT_C].colors[2] = 0.0f;
	resources[VIEWPORT_C].colors[3] = 1.0f;
	
	resources[VIEWPORT_D].colors[0] = 0.0f;
	resources[VIEWPORT_D].colors[1] = 0.0f;
	resources[VIEWPORT_D].colors[2] = 1.0f;
	resources[VIEWPORT_D].colors[3] = 1.0f;

	gtk_window_present(GTK_WINDOW(window));
}

int main(int argc, char **argv) {
    GtkApplication *app = gtk_application_new("org.gtk.glbug", G_APPLICATION_DEFAULT_FLAGS);
    g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
    int status = g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);
    return status;
}

A convienent Makefile

TARGET := bugdemo

CC := gcc
LINKER := gcc

GTKCFLAGS := `pkg-config gtk4 --cflags`
GTKLFLAGS := `pkg-config gtk4 --libs`

EPOXYCFLAGS := `pkg-config epoxy --cflags`
EPOXYLFLAGS := `pkg-config epoxy --libs`

OTHERLFLAGS := -lGL

CFLAGS := -Wall -O2 $(GTKCFLAGS) $(EPOXYCFLAGS)
LFLAGS := -Wall -no-pie $(OTHERLFLAGS) $(GTKLFLAGS) $(EPOXYLFLAGS)

all : $(TARGET)

$(TARGET) : main.o
	@$(LINKER) main.o $(LFLAGS) -o $@
	@echo "Linking complete!"

main.o : %.o : %.c
	@$(CC) $(CFLAGS) -c $< -o $@
	@echo "Compiled "$<" successfully!"

run : $(TARGET)
	./$(TARGET)


Each GtkGLArea widget has its own GL context instance.

All GL contexts created by GTK have the “shared” bit set, so you can load a texture object in one GL context, and use it (once it’s ready) in another. GTK relies on this mechanism, so it’s known to be working.

If you want to create a GL object with one context, and use it with another, you will need to ensure that the object is ready when you change the current GL context; this is usually done with GL sync objects (which are shared across GL contexts).

Remember that not all GL objects are shared across GL contexts; container objects (FBOs, program pipelines, transform feedback, and VAOs) are not shared. For instance, your code example seems to be assuming that the VAO is shared, when it really isn’t.

So developing my project will be complete nightmare. Thanks for your reply.

That’s just how GL works, I’m afraid.

You could share the exact same GdkGLContext instance with every GtkGLArea by using GtkGLArea::create-context, but that comes with its own cost.

I will try with multiple context first. And if I can’t find a solution (very unlikely) I will try to create the context myself. Thanks again !

Is it possible to create a custom widget that will do it’s rendering with OpenGL but off screen ?

No. GTK widgets are already rendered with GL (or Vulkan), so I’m not entirely sure what you’re asking.

You mean take a GLArea, never show it on screen, and then take its contents?

I guess the real question is: what are you actually trying to achieve?

Creating a single context, rendering off screen on a large texture, use glReadPixels() on that texture, create a GdkPixbuf and split the texture in regions and draw the content using 4 GtkDrawingArea instead of 4 GtkGLArea.

I have a prototype of rendering in a GLArea and copying the rendering into a drawing area.

That is somewhat horrifying:

  • you’re reading back from the GPU memory and putting data into a pixel buffer
  • you’re moving pixels around in system memory
  • you’re re-uploading into a drawing area as a Cairo image surface
  • GTK will then take the image surface and re-upload it into the GPU

Why not use four FBOs, take the backing GL textures and put them into four separate GdkGLTexture, and then use gtk_snapshot_append_texture()?

Good point, but I still want to render everything in one rendering pass off screen for let’s say a Top view, Perspective view, a Front view and a Right view. Since textures can be shared across many context, splitting a large texture into 4 smaller ones and rendering them for each GtkGLArea might be easy.

Hello everyone,

I’m making some progress with my MultiGLView widget. I have changed my mind using a single framebuffer/texture. Now I’m using 5 framebuffers/textures instead. Two mode for the widget one with 4 small views and a miximized view. Unfortunatly, I hit a bump in the road…

error: unknown type name ‘GdkGLTextureBuilder’
error: unknown type name ‘GdkDmabuf’
error: unknown type name ‘GdkDmabufTextureBuilder’

warning: implicit declaration of function ‘gdk_gl_texture_builder_get_width’
warning: implicit declaration of function ‘gdk_gl_texture_builder_get_height’
warning: implicit declaration of function ‘gdk_gl_texture_builder_get_id’
warning: implicit declaration of function ‘gdk_gl_texture_builder_set_id’
warning: implicit declaration of function ‘gdk_gl_texture_builder_set_width’
warning: implicit declaration of function ‘gdk_gl_texture_builder_set_height’
warning: implicit declaration of function ‘gdk_gl_texture_builder_build’
warning: implicit declaration of function ‘gdk_gl_texture_builder_set_sync’
warning: implicit declaration of function ‘gdk_gl_context_export_dmabuf’

warning: implicit declaration of function ‘gdk_dmabuf_texture_builder_set_display’
warning: implicit declaration of function ‘gdk_dmabuf_texture_builder_set_width’
warning: implicit declaration of function ‘gdk_dmabuf_texture_builder_set_height’
warning: implicit declaration of function ‘gdk_dmabuf_texture_builder_set_premultiplied’
warning: implicit declaration of function ‘gdk_dmabuf_texture_builder_set_dmabuf’
warning: implicit declaration of function ‘gdk_dmabuf_texture_builder_build’
warning: implicit declaration of function ‘gdk_dmabuf_close_fds’
warning: implicit declaration of function ‘gdk_dmabuf_texture_get_dmabuf’

Did I miss something ?

Which version of GTK4 are you using?

Which platform?

I’m using Debian GNU/Linux 12 (bookworm) stable. Is there a quick command I can launch in the terminal to retrieve the actual version ?

EDIT : gtk4-launch --version
4.8.3

Yeah, that’s absolutely ancient.

You’ll need at least GTK 4.16, or possibly the latest stable which is 4.18.

gtk4 - Debian Package Tracking

So I will need to switch from Debian GNU/Linux 12 (bookworm) stable to Debian GNU/Linux 12 (bookworm) testing in order to have the 4.18.2 of GTK 4.

Either that, or I recommend using Flatpak with the GNOME 48 run time, which ships GTK 4.18 and can be used on multiple distributions.

This is +/- how my custom widget is structured for now :

View
├── guint Fbo
├── guint depth_stencil_buffer
├── int width
├── int height
├── GdkGLTextureBuilder* builder
└── GdkTexture gl_texture

SimpleGLView
├── GtkWidget parent_instance
├── GError* Error
└── GdkTexture* gl_texture

MultiGLView (my custom widget derived from a GtkBox)
├── GdkGLContext* MainContext
├── GError* Error
├── int RequiredGLVersion
├── GdkGLAPI allowed_apis
├── gboolean HaveDepthBuffers
├── gboolean HaveStencilBuffers
├── gboolean HaveBuffers
├── gboolean NeedsResize
├── gboolean NeedsRender
├── gboolean AutoRender
├── gboolean IsMaximized
├── View views[5]
├── RenderCallback render_scene
├── gpointer userdata
├── Split Mode: GtkPaned (vertical)
│   ├── Top: GtkPaned (horizontal)
│   │   ├── Left: SingleGLView (View 1)
│   │   └── Right: SingleGLView (View 2)
│   └── Bottom: GtkPaned (horizontal)
│       ├── Left: SingleGLView (View 3)
│       └── Right: SingleGLView (View 4)
└── Maximized Mode: SingleGLView (View 5)

So far the application compile without any errors or warnings but the window don’t show up when I try to add my custom widget into the application. Without
my custom widget the window actually show up. Any ideas ?

Hello everyone,

After many days of hard work I finally have a working prototype for the widget described above. I still have work to do but as soon as I have the loose ends tied up I will release the code on my GitHub account.

Hello everyone,

Maybe I have celebrated too early… My custom widget was working with the nouveau graphic driver. But with the NVIDIA driver my code fail to create the GL context. So the questions I have at this time are :

Can a GtkBox widget be used to create an OpenGL context?
What can make the OpenGL context creation to fail?
Why it worked with the “nouveau” driver and not the “NVIDIA” driver ?

Edit : After 7 hours of investigation I have discovered something. The context is being created but the context realization fail and the error message is “Impossible to create GL context”.

Edit 2 : After more investigation then only way I can have a fully realized GL context is by omitting the gdk_gl_context_set_required_version() call. So I’m ending with a 3.2 OpenGL context while my graphic card support 4.6.

Hello everyone,

Apparently the GdkGLContext appear to be stuck with OpenGL ES version 3.2.
A testing code :

#include <gtk/gtk.h>
#include <epoxy/gl.h>

static void print_gl_info(GdkGLContext* context) {
    gdk_gl_context_make_current(context);
    const GLubyte* version = glGetString(GL_VERSION);
    const GLubyte* glsl_version = glGetString(GL_SHADING_LANGUAGE_VERSION);
    const GLubyte* vendor = glGetString(GL_VENDOR);
    const GLubyte* renderer = glGetString(GL_RENDERER);
    g_print("Actual GL version: %s\n", version ? (const char*)version : "Unknown");
    g_print("GLSL version: %s\n", glsl_version ? (const char*)glsl_version : "Unknown");
    g_print("Vendor: %s\n", vendor ? (const char*)vendor : "Unknown");
    g_print("Renderer: %s\n", renderer ? (const char*)renderer : "Unknown");
}

static void test_context_creation(void) {
    GError* error = NULL;

    // Get default display
    GdkDisplay* display = gdk_display_get_default();
    if (!display) {
        g_warning("No default GdkDisplay available");
        return;
    }
    g_print("Using display: %p\n", display);

    // Check GL support
    if (gdk_display_prepare_gl(display, &error)) {
        g_print("Display supports GL\n");
    } else {
        g_warning("Display does NOT support GL: %s", error ? error->message : "Unknown");
        g_clear_error(&error);
        return;
    }

    // Create offscreen context
    GdkGLContext* context = gdk_display_create_gl_context(display, &error);
    if (context == NULL) {
        g_warning("gdk_display_create_gl_context failed: %s", error ? error->message : "No error message");
        g_clear_error(&error);
        return;
    }
    g_print("Context created: %p\n", context);

    // Set context parameters
    
    GdkGLAPI allowed_apis = GDK_GL_API_GL | GDK_GL_API_GLES; // Working
    //GdkGLAPI allowed_apis = GDK_GL_API_GLES; // Working
    //GdkGLAPI allowed_apis = GDK_GL_API_GL; // Not working
     
    int major = 3; int minor = 2; // Working
    // int major = 3; int minor = 3; // Not working 
    // int major = 4; int minor = 0; // Not working 
    // int major = 4; int minor = 1; // Not working 
    // int major = 4; int minor = 2; // Not working 
    // int major = 4; int minor = 3; // Not working 
    // int major = 4; int minor = 4; // Not working 
    // int major = 4; int minor = 5; // Not working  
    //int major = 4; int minor = 6; // Not working 
    
    gdk_gl_context_set_allowed_apis(context, allowed_apis);
    gdk_gl_context_set_required_version(context, major, minor);
        
    //gdk_gl_context_set_forward_compatible(context, FALSE);
    //gdk_gl_context_set_debug_enabled(context, TRUE);

    g_print("Requesting OpenGL %d.%d, APIs: %d\n", major, minor, allowed_apis);

    // Realize the context
    if (!gdk_gl_context_realize(context, &error)) {
        g_warning("gdk_gl_context_realize failed: %s", error ? error->message : "No error message");
        if (error) {
            g_print("Error code: %d, domain: %s\n", error->code, g_quark_to_string(error->domain));
        }
        g_clear_object(&context);
        return;
    }
    g_print("Context realized successfully\n");

    // Get reported version
    
    gdk_gl_context_get_version(context, &major, &minor);
    g_print("Reported OpenGL version: %d.%d\n", major, minor);

    // Check actual GL version
    print_gl_info(context);

    g_object_unref(context);
}

static void activate(GtkApplication* app, gpointer user_data) {
    // Create a basic window (not used for rendering, just to init GTK)
    GtkWidget* window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(window), "GL Context Test");
    gtk_window_set_default_size(GTK_WINDOW(window), 200, 200);
    gtk_window_present(GTK_WINDOW(window));

    // Test context creation
    test_context_creation();
}

int main(int argc, char** argv) {
    GtkApplication* app = gtk_application_new("org.gtk.gltest", G_APPLICATION_DEFAULT_FLAGS);
    g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
    int status = g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);
    return status;
}

A tiny make file :

TARGET := main

CC := gcc
LINKER := gcc

CFILES := main.c
OFILES := main.o

GTKCFLAGS := `pkg-config gtk4 --cflags`
GTKLFLAGS := `pkg-config gtk4 --libs`

EPOXYCFLAGS := `pkg-config epoxy --cflags`
EPOXYLFLAGS := `pkg-config epoxy --libs`

OTHERLFLAGS := -lm -lGL -lX11 -lpthread -lXrandr -lXi -ldl

CFLAGS := -Wall -O2 $(GTKCFLAGS) $(EPOXYCFLAGS)
LFLAGS := -Wall -no-pie $(OTHERLFLAGS) $(GTKLFLAGS) $(EPOXYLFLAGS)

all : $(TARGET)

$(TARGET) : $(OFILES)
	@$(LINKER) $(OFILES) $(LFLAGS) -o $@
	@echo "Linking complete!"

$(OFILES) : %.o : %.c
	@$(CC) $(CFLAGS) -c $< -o $@
	@echo "Compiled "$<" successfully!"

run : $(TARGET)
	./$(TARGET)

The print out for a working settings look like this :

Using display: 0x21e132b0
Display supports GL
Context created: 0x220d2640
Requesting OpenGL 3.2, APIs: 3
Context realized successfully
Reported OpenGL version: 3.2
Actual GL version: OpenGL ES 3.2 NVIDIA 535.216.03
GLSL version: OpenGL ES GLSL ES 3.20
Vendor: NVIDIA Corporation
Renderer: NVIDIA GeForce RTX 2060/PCIe/SSE2

The print out for a none working setting look like this :

Using display: 0x21fc12b0
Display supports GL
Context created: 0x22280400
Requesting OpenGL 3.3, APIs: 3
Error code: 0, domain: gdk-gl-error-quark
** (main:9143): WARNING **: 10:24:43.945: gdk_gl_context_realize failed: Impossible de créer un contexte GL

I’m under Debian Trixie, Gtk version 4.18.3.

I will be glad if some can confirm the problem I’m experiencing.

Best regards