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)