guest GL: a GLX context is what glXCreateContextAttribsARB asked for

SDL2 on X11 makes a throwaway legacy GLX context, destroys it, then makes
the one it wants with glXCreateContextAttribsARB. Three things made every
such context the host's first, compatibility one:
- glXMakeCurrent made the host context through eglCreateContext with no
  attributes, resetting the version and profile and rebinding desktop GL;
  the attributes now stay with the GLX context until it is made current;
- eglMakeCurrent counted a context per call, so the last
  eglDestroyContext never reached the host and the throwaway lived on;
- the native host latched its first desktop context and never went back to
  ES; it now makes a new context when the profile or API asked for changes.
GL_CONTEXT_PROFILE_MASK now says compatibility for a compatibility context
(it said core for every desktop one). Test: tests/games/glx-context.sh
(3.2 core, 2.1 compatibility and ES 2.0 after SDL's throwaway, on Xwayland
inside the guest).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_461fe3ff06374ecba693831148697ba6
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-06 05:45:42 -04:00
1 parent 988e269480
commit 6230d6f4f1
5 files changed
+225 -12

No files matched your search

+66 -4
View File
@@ -258,6 +258,7 @@ const EGL_CONTEXT_MAJOR_VERSION: EGLint = 0x3098;
const EGL_CONTEXT_MINOR_VERSION: EGLint = 0x30FB;
const EGL_CONTEXT_OPENGL_PROFILE_MASK: EGLint = 0x30FD;
const EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT: EGLint = 0x2;
const EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT: EGLint = 0x1;
/// Desktop GL's fixed-function entry points, which libGLESv2 does not
/// export: looked up through EGL once a desktop context exists.
@@ -447,6 +448,9 @@ pub struct Gl {
current_on: Option<std::thread::ThreadId>,
/// Fixed function, once the context is a desktop one.
compat: Option<Compat>,
/// The desktop context's request (major, minor, compatibility), so a
/// later CONTEXT_CREATE asking for something else gets a new context.
desktop: Option<(u32, u32, bool)>,
/// Read when the context is made; the answer never changes after.
pub vendor: String,
pub renderer: String,
@@ -588,6 +592,7 @@ impl Gl {
width,
height,
compat: None,
desktop: None,
vendor: gl_string(0x1F00),
renderer: gl_string(0x1F01),
version: gl_string(0x1F02),
@@ -630,7 +635,51 @@ impl Gl {
/// `major` 0 asks for the highest version the host offers; a
/// compatibility context of an older version is a valid answer to any
/// request for 2.x or 3.0, so those are not passed on.
fn become_desktop(&mut self, major: u32, minor: u32) -> Result<(), String> {
/// Back to an OpenGL ES context (a guest that made a desktop one, threw
/// it away and asked for ES: SDL's GLX path).
fn become_es(&mut self, major: u32) -> Result<(), String> {
unsafe {
eglBindAPI(EGL_OPENGL_ES_API);
let attrs = [
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, EGL_NONE,
];
let mut config: EGLConfig = std::ptr::null_mut();
let mut num = 0;
if eglChooseConfig(self.display, attrs.as_ptr(), &mut config, 1, &mut num) == 0 || num < 1 {
return Err("no OpenGL ES EGL config".into());
}
let ctx_attrs = [EGL_CONTEXT_CLIENT_VERSION, major.max(2) as EGLint, EGL_NONE];
let context = eglCreateContext(self.display, config, std::ptr::null_mut(), ctx_attrs.as_ptr());
if context.is_null() {
return Err(format!("no OpenGL ES context: {:#x}", eglGetError()));
}
let pb = [EGL_WIDTH, self.width, EGL_HEIGHT, self.height, EGL_NONE];
let surface = eglCreatePbufferSurface(self.display, config, pb.as_ptr());
if surface.is_null() || eglMakeCurrent(self.display, surface, surface, context) == 0 {
eglDestroyContext(self.display, context);
return Err(format!("no OpenGL ES pbuffer: {:#x}", eglGetError()));
}
eglDestroySurface(self.display, self.surface);
eglDestroyContext(self.display, self.context);
self.surface = surface;
self.context = context;
self.config = config;
self.names = Names::default();
self.pack_store = PixelStore::default();
self.unpack_store = PixelStore::default();
self.current_on = Some(std::thread::current().id());
self.compat = None;
self.desktop = None;
self.vendor = gl_string(0x1F00);
self.renderer = gl_string(0x1F01);
self.version = gl_string(0x1F02);
}
Ok(())
}
fn become_desktop(&mut self, major: u32, minor: u32, compat: bool) -> Result<(), String> {
unsafe {
if eglBindAPI(EGL_OPENGL_API) == 0 {
return Err(format!("no desktop OpenGL in EGL: {:#x}", eglGetError()));
@@ -660,9 +709,11 @@ impl Gl {
eglBindAPI(EGL_OPENGL_ES_API);
return Err("no desktop OpenGL EGL config".into());
}
// Core only when asked for, from 3.2 (where profiles begin).
let core = !compat && (major > 3 || (major == 3 && minor >= 2));
let mut ctx_attrs = vec![
EGL_CONTEXT_OPENGL_PROFILE_MASK,
EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
if core { EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT } else { EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT },
];
if major > 3 || (major == 3 && minor > 0) {
ctx_attrs.extend_from_slice(&[EGL_CONTEXT_MAJOR_VERSION, major as EGLint]);
@@ -694,6 +745,7 @@ impl Gl {
self.unpack_store = PixelStore::default();
self.current_on = Some(std::thread::current().id());
self.compat = Compat::load();
self.desktop = Some((major, minor, !core));
self.vendor = gl_string(0x1F00);
self.renderer = gl_string(0x1F01);
self.version = gl_string(0x1F02);
@@ -799,10 +851,20 @@ impl Gl {
else {
return true;
};
if api == crate::proto::client_api::DESKTOP && self.compat.is_none() {
if let Err(e) = self.become_desktop(major, minor) {
// { api, major, minor, profile }: profile 1 is
// compatibility (older guests send three words: the
// compatibility profile, as before).
let compat = a.u32().is_none_or(|p| p != 0);
let core = !compat && (major > 3 || (major == 3 && minor >= 2));
let changed = self.desktop.is_some_and(|(_, _, c)| c == core);
if api == crate::proto::client_api::DESKTOP && (self.compat.is_none() || changed) {
if let Err(e) = self.become_desktop(major, minor, compat) {
eprintln!("gex-gl: {e}; staying on OpenGL ES");
}
} else if api != crate::proto::client_api::DESKTOP && self.compat.is_some() {
if let Err(e) = self.become_es(major) {
eprintln!("gex-gl: {e}; staying on desktop OpenGL");
}
}
reply.blob(self.vendor.as_bytes());
reply.blob(self.renderer.as_bytes());
+56 -6
View File
@@ -59,6 +59,19 @@ static const int gexgl_glx_context_token = 0;
static void *glx_current_display;
static unsigned long glx_current_drawable;
static void *glx_current_context;
/* A context made by glXCreateContextAttribsARB: what it asked for, kept until
* glXMakeCurrent makes the host context. The host context is made there, not
* at creation, and making it went through eglCreateContext with no
* attributes, which put the version and profile back to EGL's defaults and
* eglBindAPI back to desktop GL: a 3.2 core or ES 2 request came up as the
* host's compatibility context. */
#define GEXGL_GLX_ATTRIBS_MAGIC 0x67786174u
struct gexgl_glx_attribs_context {
unsigned magic;
int es, major, minor, profile;
};
static void gexgl_glx_ensure_egl(void *dpy);
@@ -276,6 +289,13 @@ void *glXCreateContextAttribsARB(void *dpy, void *config, void *share,
what it actually got through GL_VERSION -- and an explicit core request
overrides the compatibility default above. */
int wants_es = 0;
/* What this context asks for, from GLX's defaults (1.0, compatibility),
whatever a previous context left in the globals. */
if (!host_context_made) {
egl_context_major = 1;
egl_context_minor = 0;
egl_context_profile = 1;
}
for (const int *a = attribs; a && *a != 0 && !host_context_made; a += 2) {
switch (a[0]) {
case 0x2091: egl_context_major = a[1]; break; /* MAJOR_VERSION_ARB */
@@ -302,7 +322,19 @@ void *glXCreateContextAttribsARB(void *dpy, void *config, void *share,
GL because that is what reaching for GLX usually means, and only the
profile mask can say otherwise. */
if (wants_es) eglBindAPI(0x30A0u /* EGL_OPENGL_ES_API */);
return GEXGL_GLX_CONTEXT;
struct gexgl_glx_attribs_context *c = malloc(sizeof(*c));
if (!c) return GEXGL_GLX_CONTEXT;
c->magic = GEXGL_GLX_ATTRIBS_MAGIC;
c->es = wants_es;
c->major = egl_context_major;
c->minor = egl_context_minor;
c->profile = egl_context_profile;
return c;
}
static struct gexgl_glx_attribs_context *gexgl_glx_attribs_of(void *ctx) {
struct gexgl_glx_attribs_context *c = ctx;
return ctx && ctx != GEXGL_GLX_CONTEXT && c->magic == GEXGL_GLX_ATTRIBS_MAGIC ? c : NULL;
}
/* Destroying the context really destroys it, which is what makes an odd
@@ -315,8 +347,14 @@ void *glXCreateContextAttribsARB(void *dpy, void *config, void *share,
* application ran on whatever the throwaway had asked for. Letting the host
* drop it means the next creation is a real creation. */
void glXDestroyContext(void *dpy, void *ctx) {
(void)dpy; (void)ctx;
(void)dpy;
struct gexgl_glx_attribs_context *c = gexgl_glx_attribs_of(ctx);
eglDestroyContext(NULL, ctx);
if (ctx == glx_current_context) glx_current_context = NULL;
if (c) {
c->magic = 0;
free(c);
}
}
int glXMakeCurrent(void *dpy, unsigned long drawable, void *ctx) {
@@ -325,6 +363,7 @@ int glXMakeCurrent(void *dpy, unsigned long drawable, void *ctx) {
eglMakeCurrent(dpy, NULL, NULL, NULL);
glx_current_display = NULL;
glx_current_drawable = 0;
glx_current_context = NULL;
return 1;
}
/* The drawable is an X window id, which is exactly what
@@ -333,10 +372,21 @@ int glXMakeCurrent(void *dpy, unsigned long drawable, void *ctx) {
void *config = NULL;
void *surface = eglCreateWindowSurface(dpy, config,
(void *)(uintptr_t)drawable, NULL);
void *context = egl_current_context ? egl_current_context
: eglCreateContext(dpy, config, NULL,
NULL);
void *context = egl_current_context;
struct gexgl_glx_attribs_context *c = gexgl_glx_attribs_of(ctx);
if (!context && c) {
/* The host context as glXCreateContextAttribsARB asked for it. */
eglBindAPI(c->es ? 0x30A0u /* EGL_OPENGL_ES_API */ : 0x30A2u /* EGL_OPENGL_API */);
egl_context_major = c->major;
egl_context_minor = c->minor;
egl_context_profile = c->profile;
context_create();
context = (void *)3;
} else if (!context) {
context = eglCreateContext(dpy, config, NULL, NULL);
}
if (!eglMakeCurrent(dpy, surface, surface, context)) return 0;
glx_current_context = ctx;
glx_current_display = dpy;
glx_current_drawable = drawable;
return 1;
@@ -359,7 +409,7 @@ void glXSwapBuffers(void *dpy, unsigned long drawable) {
}
void *glXGetCurrentContext(void) {
return egl_current_context ? GEXGL_GLX_CONTEXT : NULL;
return egl_current_context ? (glx_current_context ? glx_current_context : GEXGL_GLX_CONTEXT) : NULL;
}
void *glXGetCurrentDisplay(void) { return glx_current_display; }
unsigned long glXGetCurrentDrawable(void) { return glx_current_drawable; }
+18 -2
View File
@@ -1972,7 +1972,19 @@ void glGetIntegerv(unsigned pname, int *out) {
return;
}
if (pname == 0x9126u) { /* GL_CONTEXT_PROFILE_MASK */
if (out) out[0] = egl_api == 0x30A2u ? 1 : 0; /* GL_CONTEXT_CORE_PROFILE_BIT */
/* What the host's context is: its version string says compatibility
when it is one (GL_CONTEXT_COMPATIBILITY_PROFILE_BIT, 2), and a
desktop context of 3.2 or later otherwise is core (1). ES has no
profiles. This answered core for every desktop context. */
int mask = 0;
if (egl_api == 0x30A2u) {
const char *v = (const char *)glGetString(0x1F02u);
int major = 0, minor = 0;
host_gl_version(&major, &minor);
if (v && strstr(v, "Compatibility")) mask = 2;
else if (major > 3 || (major == 3 && minor >= 2)) mask = 1;
}
if (out) out[0] = mask;
return;
}
/* Sized from the pname before the request goes out, for two reasons.
@@ -3001,7 +3013,11 @@ unsigned eglMakeCurrent(void *dpy, void *draw, void *read, void *ctx) {
/* A safety net for callers that reach a current context without going
through this library's eglCreateContext. The latch inside makes it a
no-op once the host context exists. */
if (ctx) context_create();
/* Only when there is none: the latch counts a context per call, and
counting every make-current as a new context kept the host context
alive after its last eglDestroyContext (SDL's throwaway GLX context
then stood in for the one it really wanted). */
if (ctx && !host_context_made) context_create();
egl_current_display = ctx ? dpy : NULL;
egl_current_context = ctx;
egl_current_draw = ctx ? draw : NULL;
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# GLX contexts under gex (tests/games/src/glx-context.c): a program on
# Xwayland inside the guest, with gex's guest GL for x86-64.
#
# tests/games/glx-context.sh [GEXC]
#
# Env: GEX_GL_X64 (guest/gl/build.sh with GEX_CC=gcc; built here if unset).
set -eu
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../.." && pwd)
gexc=${1:-$root/target/x86/release/gexc}
work=${TMPDIR:-/tmp}/gex-glx-context-$(id -u)
rm -rf "$work"; mkdir -p "$work"
gl=${GEX_GL_X64:-}
if [ -z "$gl" ]; then gl=$work/gl; GEX_CC=gcc "$root/guest/gl/build.sh" "$gl" > /dev/null; fi
gcc -O1 -o "$work/glx-context" "$here/src/glx-context.c" -lX11 -lGL
n=$(( 50 + $(id -u) % 40 ))
rm -f "/tmp/.X$n-lock" "/tmp/.X11-unix/X$n"
cd "$work"
set +e
timeout 1800 "$gexc" run --no-cgen --gl --wayland "$work/wl" --wayland-size 320x240 --wayland-out null \
--env LD_LIBRARY_PATH="$gl" --env XDG_RUNTIME_DIR="$work" --env HOME="$work" \
/bin/sh "$here/xwayland-inguest.sh" "$n" "$work/xwayland.log" ./glx-context > "$work/out" 2>&1
rc=$?
rm -f "/tmp/.X$n-lock" "/tmp/.X11-unix/X$n"
grep -E '^(ok|FAIL| |[0-9]+ failures)' "$work/out"
[ $rc = 0 ] && grep -q '^0 failures' "$work/out"
+58
View File
@@ -0,0 +1,58 @@
/* GLX contexts as SDL2 makes them: a throwaway legacy context, then the one
* wanted, from glXCreateContextAttribsARB. The second must be what it asked
* for (gex's guest GL used to make every context the host's compatibility
* one at glXMakeCurrent). Run by tests/games/glx-context.sh. */
#include <X11/Xlib.h>
#include <GL/glx.h>
#include <stdio.h>
#include <string.h>
typedef GLXContext (*cca_t)(Display *, GLXFBConfig, GLXContext, Bool, const int *);
static int failures;
#define CHECK(name, cond) do { int ok_ = (cond); printf("%s %s\n", ok_ ? "ok" : "FAIL", name); if (!ok_) failures++; } while (0)
int main(void) {
Display *d = XOpenDisplay(NULL);
if (!d) { printf("FAIL no display\n"); return 1; }
int att[] = {GLX_RGBA, GLX_DOUBLEBUFFER, None};
XVisualInfo *vi = glXChooseVisual(d, DefaultScreen(d), att);
XSetWindowAttributes a = {0};
a.colormap = XCreateColormap(d, RootWindow(d, vi->screen), vi->visual, AllocNone);
Window w = XCreateWindow(d, RootWindow(d, vi->screen), 0, 0, 64, 64, 0, vi->depth, InputOutput, vi->visual, CWColormap, &a);
XMapWindow(d, w);
XFlush(d);
int n = 0;
GLXFBConfig *cfg = glXChooseFBConfig(d, DefaultScreen(d), NULL, &n);
cca_t cca = (cca_t)glXGetProcAddressARB((const GLubyte *)"glXCreateContextAttribsARB");
CHECK("glXCreateContextAttribsARB", cca && n > 0);
if (!cca || !n) return 1;
struct { const char *name; int major, minor, mask; } want[] = {
{"3.2 core", 3, 2, 1}, {"2.1 compatibility", 2, 1, 2}, {"ES 2.0", 2, 0, 4},
};
for (unsigned i = 0; i < sizeof want / sizeof want[0]; i++) {
/* SDL's throwaway context first. */
GLXContext t = glXCreateContext(d, vi, NULL, True);
glXMakeCurrent(d, w, t);
glXMakeCurrent(d, None, NULL);
glXDestroyContext(d, t);
int ca[] = {0x2091, want[i].major, 0x2092, want[i].minor, 0x9126, want[i].mask, 0};
GLXContext c = cca(d, cfg[0], NULL, True, ca);
int made = c && glXMakeCurrent(d, w, c);
const char *v = (const char *)glGetString(GL_VERSION);
GLint mask = 0;
glGetIntegerv(0x9126 /* GL_CONTEXT_PROFILE_MASK */, &mask);
printf(" %s: %s, profile mask %d\n", want[i].name, v ? v : "(null)", mask);
char what[64];
snprintf(what, sizeof what, "%s: made current", want[i].name);
CHECK(what, made && glXGetCurrentContext() == c);
snprintf(what, sizeof what, "%s: what it asked for", want[i].name);
if (want[i].mask == 4) CHECK(what, v && !strncmp(v, "OpenGL ES", 9));
else if (want[i].mask == 1) CHECK(what, v && v[0] >= '3' && (mask & 1));
else CHECK(what, v && v[0] >= '2' && strncmp(v, "OpenGL ES", 9) && !(mask & 1));
glXMakeCurrent(d, None, NULL);
glXDestroyContext(d, c);
}
printf("%d failures\n", failures);
return failures;
}