Skip to content

Instantly share code, notes, and snippets.

@valignatev
Last active August 11, 2023 22:08
Show Gist options
  • Save valignatev/60fdd91fefabd131a0e53fe2e3ef0ec7 to your computer and use it in GitHub Desktop.
Save valignatev/60fdd91fefabd131a0e53fe2e3ef0ec7 to your computer and use it in GitHub Desktop.
setting up and using modern OpenGL 4.5 core context on X11 with EGL and xcb, without xlib or glx
// This is @mmozeiko's libx + EGL example ported to xcb + EGL
// Original code: https://gist.github.com/mmozeiko/911347b5e3d998621295794e0ba334c4
// It requires EGL_EXT_platform_xcb extension, so if your system is circa the end of 2021 or newer you should be fine.
// Also, I don't know if it works on nvidia. LMK if you test it
// example how to set up OpenGL core context on X11 with EGL
// and use basic functionality of OpenGL 4.5 version
// to compile on Ubuntu first install following packages: build-essential libxcb1-dev libgl-dev libegl-dev, then run:
// gcc xcb_opengl.c -o xcb_opengl -lm -lxcb -lEGL
// On Arch that would be (I'm not sure if it's the full list): base-devel libxcb libglvnd
// important extension functionality used here:
// https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_platform_xcb.txt
// (4.3) KHR_debug: https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_debug.txt
// (4.5) ARB_direct_state_access: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_direct_state_access.txt
// (4.1) ARB_separate_shader_objects: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_separate_shader_objects.txt
// (4.2) ARB_shading_language_420pack: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_shading_language_420pack.txt
// (4.3) ARB_explicit_uniform_location: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_explicit_uniform_location.txt
// #define EGL_EGLEXT_PROTOTYPES
#include <xcb/xcb.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/glcorearb.h>
#include <time.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stddef.h>
// replace this with your favorite Assert() implementation
#include <assert.h>
#define Assert(cond) assert(cond)
// This is needed to get EGL native display from xcb connection
// eglGetDisplay(EGL_DEFAULT_DISPLAY) and eglGetDisplay(conn) work on my maching (Intel graphics),
// but this makes EGL guess what kind of pointer you pass to it, so I guess might not work in some cases.
PFNEGLGETPLATFORMDISPLAYEXTPROC GetPlatformDisplayEXT;
// make sure you use functions that are valid for selected GL version (specified when context is created)
#define GL_FUNCTIONS(X) \
X(PFNGLENABLEPROC, glEnable ) \
X(PFNGLDISABLEPROC, glDisable ) \
X(PFNGLBLENDFUNCPROC, glBlendFunc ) \
X(PFNGLVIEWPORTPROC, glViewport ) \
X(PFNGLCLEARCOLORPROC, glClearColor ) \
X(PFNGLCLEARPROC, glClear ) \
X(PFNGLDRAWARRAYSPROC, glDrawArrays ) \
X(PFNGLCREATEBUFFERSPROC, glCreateBuffers ) \
X(PFNGLNAMEDBUFFERSTORAGEPROC, glNamedBufferStorage ) \
X(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray ) \
X(PFNGLCREATEVERTEXARRAYSPROC, glCreateVertexArrays ) \
X(PFNGLVERTEXARRAYATTRIBBINDINGPROC, glVertexArrayAttribBinding ) \
X(PFNGLVERTEXARRAYVERTEXBUFFERPROC, glVertexArrayVertexBuffer ) \
X(PFNGLVERTEXARRAYATTRIBFORMATPROC, glVertexArrayAttribFormat ) \
X(PFNGLENABLEVERTEXARRAYATTRIBPROC, glEnableVertexArrayAttrib ) \
X(PFNGLCREATESHADERPROGRAMVPROC, glCreateShaderProgramv ) \
X(PFNGLGETPROGRAMIVPROC, glGetProgramiv ) \
X(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog ) \
X(PFNGLGENPROGRAMPIPELINESPROC, glGenProgramPipelines ) \
X(PFNGLUSEPROGRAMSTAGESPROC, glUseProgramStages ) \
X(PFNGLBINDPROGRAMPIPELINEPROC, glBindProgramPipeline ) \
X(PFNGLPROGRAMUNIFORMMATRIX2FVPROC, glProgramUniformMatrix2fv ) \
X(PFNGLBINDTEXTUREUNITPROC, glBindTextureUnit ) \
X(PFNGLCREATETEXTURESPROC, glCreateTextures ) \
X(PFNGLTEXTUREPARAMETERIPROC, glTextureParameteri ) \
X(PFNGLTEXTURESTORAGE2DPROC, glTextureStorage2D ) \
X(PFNGLTEXTURESUBIMAGE2DPROC, glTextureSubImage2D ) \
X(PFNGLDEBUGMESSAGECALLBACKPROC, glDebugMessageCallback )
#define X(type, name) static type name;
GL_FUNCTIONS(X)
#undef X
#define STR2(x) #x
#define STR(x) STR2(x)
static void FatalError(const char* message)
{
fprintf(stderr, "%s\n", message);
char cmd[1024];
snprintf(cmd, sizeof(cmd), "zenity --error --no-wrap --text=\"%s\"", message);
system(cmd);
exit(0);
}
#ifndef NDEBUG
static void APIENTRY DebugCallback(
GLenum source, GLenum type, GLuint id, GLenum severity,
GLsizei length, const GLchar* message, const void* user)
{
fprintf(stderr, "%s\n", message);
if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM)
{
assert(!"OpenGL API usage error! Use debugger to examine call stack!");
}
}
#endif
int main()
{
xcb_connection_t *conn;
conn = xcb_connect(NULL, NULL);
if (!conn || xcb_connection_has_error(conn))
{
fprintf(stderr, "Couldn't connect to X server: error %d\n",
conn ? xcb_connection_has_error(conn) : -1);
return 1;
}
xcb_generic_error_t *error;
xcb_void_cookie_t cookie;
xcb_screen_t *screen =
xcb_setup_roots_iterator(xcb_get_setup(conn)).data;
xcb_window_t window = xcb_generate_id(conn);
uint32_t attributes[3] = {
0,
XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_STRUCTURE_NOTIFY,
XCB_GRAVITY_STATIC
};
int width = 1280;
int height = 720;
cookie = xcb_create_window_checked(conn, XCB_COPY_FROM_PARENT,
window, screen->root,
0, 0, width, height, 1,
XCB_WINDOW_CLASS_INPUT_OUTPUT,
screen->root_visual,
XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_BIT_GRAVITY,
attributes);
if ((error = xcb_request_check(conn, cookie)) != NULL) {
fprintf(stderr, "Couldn't create X window: error %d\n", error);
return 1;
}
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window,
XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
13, "OpenGL Window");
xcb_intern_atom_cookie_t protocols_cookie = xcb_intern_atom(conn, 1, 12, "WM_PROTOCOLS");
xcb_intern_atom_cookie_t delete_cookie = xcb_intern_atom(conn, 0, 16, "WM_DELETE_WINDOW");
xcb_intern_atom_reply_t* wm_protocols = xcb_intern_atom_reply(conn, protocols_cookie, 0);
xcb_intern_atom_reply_t* delete_window = xcb_intern_atom_reply(conn, delete_cookie, 0);
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window, (*wm_protocols).atom, 4, 32, 1, &(*delete_window).atom);
GetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
// initialize EGL
EGLDisplay display;
{
display = GetPlatformDisplayEXT(EGL_PLATFORM_XCB_EXT, conn,
(const EGLint[]){
EGL_PLATFORM_XCB_SCREEN_EXT,
0, // This is a screenp that you can get from 2nd argument of xcb_connect
EGL_NONE,
});
Assert(display != EGL_NO_DISPLAY && "Failed to get EGL display");
EGLint major, minor;
if (!eglInitialize(display, &major, &minor))
{
FatalError("Cannot initialize EGL display");
}
if (major < 1 || (major == 1 && minor < 5))
{
FatalError("EGL version 1.5 or higher required");
}
}
// choose OpenGL API for EGL, by default it uses OpenGL ES
EGLBoolean ok = eglBindAPI(EGL_OPENGL_API);
Assert(ok && "Failed to select OpenGL API for EGL");
// choose EGL configuration
EGLConfig config;
{
EGLint attr[] =
{
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_CONFORMANT, EGL_OPENGL_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_STENCIL_SIZE, 8,
// uncomment for multisampled framebuffer
//EGL_SAMPLE_BUFFERS, 1,
//EGL_SAMPLES, 4, // 4x MSAA
EGL_NONE,
};
EGLint count;
if (!eglChooseConfig(display, attr, &config, 1, &count) || count != 1)
{
FatalError("Cannot choose EGL config");
}
}
// create EGL surface
EGLSurface* surface;
{
EGLint attr[] =
{
EGL_GL_COLORSPACE, EGL_GL_COLORSPACE_LINEAR, // or use EGL_GL_COLORSPACE_SRGB for sRGB framebuffer
EGL_RENDER_BUFFER, EGL_BACK_BUFFER,
EGL_NONE,
};
surface = eglCreateWindowSurface(display, config, window, attr);
if (surface == EGL_NO_SURFACE)
{
FatalError("Cannot create EGL surface");
}
}
// create EGL context
EGLContext* context;
{
EGLint attr[] =
{
EGL_CONTEXT_MAJOR_VERSION, 4,
EGL_CONTEXT_MINOR_VERSION, 5,
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
#ifndef NDEBUG
// ask for debug context for non "Release" builds
// this is so we can enable debug callback
EGL_CONTEXT_OPENGL_DEBUG, EGL_TRUE,
#endif
EGL_NONE,
};
context = eglCreateContext(display, config, EGL_NO_CONTEXT, attr);
if (context == EGL_NO_CONTEXT)
{
FatalError("Cannot create EGL context, OpenGL 4.5 not supported?");
}
}
ok = eglMakeCurrent(display, surface, surface, context);
Assert(ok && "Failed to make context current");
// load OpenGL functions
#define X(type, name) name = (type)eglGetProcAddress(#name); Assert(name);
GL_FUNCTIONS(X)
#undef X
#ifndef NDEBUG
// enable debug callback
glDebugMessageCallback(&DebugCallback, NULL);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
#endif
struct Vertex
{
float position[2];
float uv[2];
float color[3];
};
// vertex buffer containing triangle vertices
GLuint vbo;
{
struct Vertex data[] =
{
{ { -0.00f, +0.75f }, { 25.0f, 50.0f }, { 1, 0, 0 } },
{ { +0.75f, -0.50f }, { 0.0f, 0.0f }, { 0, 1, 0 } },
{ { -0.75f, -0.50f }, { 50.0f, 0.0f }, { 0, 0, 1 } },
};
glCreateBuffers(1, &vbo);
glNamedBufferStorage(vbo, sizeof(data), data, 0);
}
// vertex input
GLuint vao;
{
glCreateVertexArrays(1, &vao);
GLint vbuf_index = 0;
glVertexArrayVertexBuffer(vao, vbuf_index, vbo, 0, sizeof(struct Vertex));
GLint a_pos = 0;
glVertexArrayAttribFormat(vao, a_pos, 2, GL_FLOAT, GL_FALSE, offsetof(struct Vertex, position));
glVertexArrayAttribBinding(vao, a_pos, vbuf_index);
glEnableVertexArrayAttrib(vao, a_pos);
GLint a_uv = 1;
glVertexArrayAttribFormat(vao, a_uv, 2, GL_FLOAT, GL_FALSE, offsetof(struct Vertex, uv));
glVertexArrayAttribBinding(vao, a_uv, vbuf_index);
glEnableVertexArrayAttrib(vao, a_uv);
GLint a_color = 2;
glVertexArrayAttribFormat(vao, a_color, 3, GL_FLOAT, GL_FALSE, offsetof(struct Vertex, color));
glVertexArrayAttribBinding(vao, a_color, vbuf_index);
glEnableVertexArrayAttrib(vao, a_color);
}
// checkerboard texture, with 50% transparency on black colors
GLuint texture;
{
unsigned int pixels[] =
{
0x80000000, 0xffffffff,
0xffffffff, 0x80000000,
};
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTextureParameteri(texture, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_REPEAT);
GLsizei width = 2;
GLsizei height = 2;
glTextureStorage2D(texture, 1, GL_RGBA8, width, height);
glTextureSubImage2D(texture, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
}
// fragment & vertex shaders for drawing triangle
GLuint pipeline, vshader, fshader;
{
const char* glsl_vshader =
"#version 450 core \n"
"#line " STR(__LINE__) " \n\n" // actual line number in this file for nicer error messages
" \n"
"layout (location=0) in vec2 a_pos; \n" // position attribute index 0
"layout (location=1) in vec2 a_uv; \n" // uv attribute index 1
"layout (location=2) in vec3 a_color; \n" // color attribute index 2
" \n"
"layout (location=0) \n" // (from ARB_explicit_uniform_location)
"uniform mat2 u_matrix; \n" // matrix uniform location 0
" \n"
"out gl_PerVertex { vec4 gl_Position; }; \n" // required because of ARB_separate_shader_objects
"out vec2 uv; \n"
"out vec4 color; \n"
" \n"
"void main() \n"
"{ \n"
" vec2 pos = u_matrix * a_pos; \n"
" gl_Position = vec4(pos, 0, 1); \n"
" uv = a_uv; \n"
" color = vec4(a_color, 1); \n"
"} \n"
;
const char* glsl_fshader =
"#version 450 core \n"
"#line " STR(__LINE__) " \n\n" // actual line number in this file for nicer error messages
" \n"
"in vec2 uv; \n"
"in vec4 color; \n"
" \n"
"layout (binding=0) \n" // (from ARB_shading_language_420pack)
"uniform sampler2D s_texture; \n" // texture unit binding 0
" \n"
"layout (location=0) \n"
"out vec4 o_color; \n" // output fragment data location 0
" \n"
"void main() \n"
"{ \n"
" o_color = color * texture(s_texture, uv); \n"
"} \n"
;
vshader = glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &glsl_vshader);
fshader = glCreateShaderProgramv(GL_FRAGMENT_SHADER, 1, &glsl_fshader);
GLint linked;
glGetProgramiv(vshader, GL_LINK_STATUS, &linked);
if (!linked)
{
char message[1024];
glGetProgramInfoLog(vshader, sizeof(message), NULL, message);
fprintf(stderr, "%s\n", message);
Assert(!"Failed to create vertex shader!");
}
glGetProgramiv(fshader, GL_LINK_STATUS, &linked);
if (!linked)
{
char message[1024];
glGetProgramInfoLog(fshader, sizeof(message), NULL, message);
fprintf(stderr, "%s\n", message);
Assert(!"Failed to create fragment shader!");
}
glGenProgramPipelines(1, &pipeline);
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vshader);
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fshader);
}
// setup global GL state
{
// enable alpha blending
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// disble depth testing
glDisable(GL_DEPTH_TEST);
// disable culling
glDisable(GL_CULL_FACE);
}
// use 0 to disable vsync
int vsync = 1;
ok = eglSwapInterval(display, vsync);
Assert(ok && "Failed to set vsync for EGL");
cookie = xcb_map_window_checked(conn, window);
if ((error = xcb_request_check(conn, cookie)) != NULL) {
free(error);
return -1;
}
struct timespec c1;
clock_gettime(CLOCK_MONOTONIC, &c1);
float angle = 0;
for (;;)
{
// process all incoming X11 events
xcb_generic_event_t *event;
event = xcb_poll_for_event(conn);
if (event) {
if (((*event).response_type & ~0x80) == XCB_CLIENT_MESSAGE) {
if((*(xcb_client_message_event_t*)event).data.data32[0] == (*delete_window).atom) {
// Window closed, exit
break;
}
continue;
}
}
free(event);
xcb_get_geometry_cookie_t geom_cookie;
xcb_get_geometry_reply_t *geometry;
geom_cookie = xcb_get_geometry(conn, window);
geometry = xcb_get_geometry_reply(conn, geom_cookie, NULL);
width = geometry->width;
height = geometry->height;
struct timespec c2;
clock_gettime(CLOCK_MONOTONIC, &c2);
float delta = (float)(c2.tv_sec - c1.tv_sec) + 1e-9f * (c2.tv_nsec - c1.tv_nsec);
c1 = c2;
// render only if window size is non-zero
if (width != 0 && height != 0)
{
// setup output size covering all client area of window
glViewport(0, 0, width, height);
// clear screen
glClearColor(0.392f, 0.584f, 0.929f, 1.f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// setup rotation matrix in uniform
{
angle += delta * 2.0f * (float)M_PI / 20.0f; // full rotation in 20 seconds
angle = fmodf(angle, 2.0f * (float)M_PI);
float aspect = (float)height / width;
float matrix[] =
{
cosf(angle) * aspect, -sinf(angle),
sinf(angle) * aspect, cosf(angle),
};
GLint u_matrix = 0;
glProgramUniformMatrix2fv(vshader, u_matrix, 1, GL_FALSE, matrix);
}
// activate shaders for next draw call
glBindProgramPipeline(pipeline);
// provide vertex input
glBindVertexArray(vao);
// bind texture to texture unit
GLint s_texture = 0; // texture unit that sampler2D will use in GLSL code
glBindTextureUnit(s_texture, texture);
// draw 3 vertices as triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
// swap the buffers to show output
if (!eglSwapBuffers(display, surface))
{
FatalError("Failed to swap OpenGL buffers!");
}
}
else
{
// window is minimized, cannot vsync - instead sleep a bit
if (vsync)
{
usleep(10 * 1000);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment