guest/gl: framebuffer state queries answered from a cache

Unity asks glGetIntegerv(GL_SAMPLES) and (GL_READ_BUFFER) four times each
a frame (Muck's menu: 8 of its 10 submits a frame natively), and every
query is a submit and a wait; in the page each is a round trip to the
compositor's Worker. GL_SAMPLES, GL_SAMPLE_BUFFERS, GL_READ_BUFFER and
GL_DRAW_BUFFER are now kept per (draw framebuffer, read framebuffer) once
the host has answered, until a command that can change them goes into the
ring (attachments, renderbuffer and multisample storage, read/draw
buffers, deletions, a binding not made through glBindFramebuffer) or the
context changes. GEXGL_NO_QUERY_CACHE=1 asks every time.

tests/gl-state-query-cache.sh: the probe's nine cases against native
Mesa, then under gexc --gl with the cache on (12 submits) and off (21).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_d2453757b8a84fdbb4ee7c9e093141df
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-08 09:40:26 -04:00
1 parent d6c8b5196a
commit 85c3bb78ba
4 files changed
+215 -2

No files matched your search

+105 -1
View File
@@ -228,6 +228,95 @@ static void flush(void) {
reply_at = 0;
}
/* Framebuffer state queries answered here.
*
* Unity asks glGetIntegerv(GL_SAMPLES) and glGetIntegerv(GL_READ_BUFFER)
* four times each a frame (Muck's menu), and every query is a submit: the
* batch so far runs and the guest waits for it. In the page that is a round
* trip to the compositor's Worker each, and Muck's render thread spent
* nearly half its time in them. The answers depend only on which
* framebuffers are bound and on their attachments and read/draw buffers, so
* the first answer for a binding is kept until a command that could change
* it goes into the ring (fbq_invalidates) or the context changes. Which
* framebuffers are bound is tracked from glBindFramebuffer; a binding made
* any other way stops the caching until GL_FRAMEBUFFER is bound again.
* GEXGL_NO_QUERY_CACHE=1 asks the host every time. */
struct fbq_entry {
unsigned pname, draw, read;
int value;
};
static struct fbq_entry fbq_cache[16];
static unsigned fbq_count;
static unsigned fbq_draw, fbq_read;
static int fbq_known;
static void fbq_forget(void) { fbq_count = 0; }
static int fbq_cacheable(unsigned pname) {
static int off = -1;
if (off < 0) off = getenv("GEXGL_NO_QUERY_CACHE") ? 1 : 0;
if (off) return 0;
switch (pname) {
case 0x80A9u: /* GL_SAMPLES */
case 0x80A8u: /* GL_SAMPLE_BUFFERS */
case 0x0C02u: /* GL_READ_BUFFER */
case 0x0C01u: /* GL_DRAW_BUFFER */
return 1;
default:
return 0;
}
}
static int fbq_lookup(unsigned pname, int *out) {
if (!fbq_known) return 0;
for (unsigned i = 0; i < fbq_count; ++i)
if (fbq_cache[i].pname == pname && fbq_cache[i].draw == fbq_draw &&
fbq_cache[i].read == fbq_read) {
*out = fbq_cache[i].value;
return 1;
}
return 0;
}
static void fbq_store(unsigned pname, int value) {
if (!fbq_known) return;
if (fbq_count == sizeof(fbq_cache) / sizeof(fbq_cache[0])) fbq_count = 0;
fbq_cache[fbq_count++] = (struct fbq_entry){pname, fbq_draw, fbq_read, value};
}
/* Commands after which a cached answer may be wrong: attachments, buffer
* storage that can change a sample count, read and draw buffer selection,
* deletions (a name can come back as another object), and bindings made
* other than through glBindFramebuffer below. The numbers are the wire
* opcodes (crates/gex-gl/src/proto.rs, and the generated GL 4.4 and
* compatibility transports). */
static int fbq_invalidates(uint32_t opcode) {
switch (opcode) {
case 97: case 104: case 107: case 109: case 111: case 114: case 119: case 120:
case 1132: case 1158: case 1160: case 1161: case 1163: case 1374: case 1378:
case 1405: case 1407: case 1416: case 1418: case 1422:
case 8211: case 8751: case 8752: case 8753: case 8754: case 8953: case 8954:
case 8959: case 8960: case 8962: case 8963: case 8964: case 8969: case 8970:
case 8971: case 8972: case 8973: case 8975: case 8979: case 8980: case 8982:
case 8983: case 8984: case 8985: case 8986: case 8987: case 8989: case 8991:
case 9960: case 9991: case 9992: case 9993: case 9994: case 9995: case 9996:
case 9997: case 10001: case 10002: case 10003: case 10004: case 10005:
case 10006: case 10007: case 10008: case 10009: case 10022: case 10023:
case 10024: case 10025: case 10026: case 10027: case 10412: case 10437:
case 10438: case 10439: case 10442: case 10443: case 10444: case 10763:
case 10764: case 10767: case 10768: case 10788: case 10793: case 10796:
case 10802: case 10804: case 10825: case 10826: case 10828: case 10829:
case 10848: case 10853: case 10854: case 10857: case 10858: case 10861:
case 10863: case 10871:
return 1;
case 8260: case 8261: /* glBindFramebuffer(EXT) through the generated transport */
fbq_known = 0;
return 1;
default:
return 0;
}
}
/* Start a command. Returns where to write its arguments, having reserved
room for them, or NULL if the batch is full -- in which case it is flushed
and the caller tries again. */
@@ -248,6 +337,7 @@ static uint32_t *begin(uint32_t opcode, size_t arg_words, size_t tail_bytes) {
return NULL; /* one command larger than the whole buffer */
}
}
if ((fbq_count || fbq_known) && fbq_invalidates(opcode)) fbq_forget();
uint32_t *at = (uint32_t *)(cmds + ring->tail);
at[0] = opcode;
at[1] = (uint32_t)len;
@@ -1501,6 +1591,14 @@ GEN_OBJECTS(glGenRenderbuffers, GEXGL_GEN_RENDERBUFFERS)
DELETE_OBJECTS(glDeleteRenderbuffers, GEXGL_DELETE_RENDERBUFFERS)
void glBindFramebuffer(unsigned target, unsigned name) {
CMD2(GEXGL_BIND_FRAMEBUFFER, target, name);
if (target == 0x8D40u) { /* GL_FRAMEBUFFER: both */
fbq_draw = fbq_read = name;
fbq_known = 1;
} else if (target == 0x8CA9u) { /* GL_DRAW_FRAMEBUFFER */
fbq_draw = name;
} else if (target == 0x8CA8u) { /* GL_READ_FRAMEBUFFER */
fbq_read = name;
}
}
void glBindRenderbuffer(unsigned target, unsigned name) { CMD2(GEXGL_BIND_RENDERBUFFER, target, name); }
unsigned glCheckFramebufferStatus(unsigned target) {
@@ -2015,13 +2113,17 @@ void glGetIntegerv(unsigned pname, int *out) {
* its own reply back as this one's; and copying whatever the host sent
* writes past the caller's array whenever the two disagree, which is an
* out-of-bounds write into guest memory rather than a wrong value. */
const int cacheable = fbq_cacheable(pname);
if (cacheable && out && fbq_lookup(pname, out)) return;
const size_t expected = gexgl_state_query_components(pname) * sizeof(*out);
CMD1(GEXGL_GET_INTEGERV, pname);
flush();
uint32_t bytes = 0;
const uint8_t *values = reply_blob(&bytes);
if (out && values && expected && bytes == expected)
if (out && values && expected && bytes == expected) {
memcpy(out, values, expected);
if (cacheable && expected == sizeof(*out)) fbq_store(pname, out[0]);
}
}
static size_t gexgl_state_query_components(unsigned pname) {
@@ -3088,6 +3190,8 @@ unsigned eglDestroyContext(void *dpy, void *ctx) {
}
unsigned eglMakeCurrent(void *dpy, void *draw, void *read, void *ctx) {
fbq_forget();
fbq_known = 0;
if (!egl_ready) return EGL_FALSE;
/* A safety net for callers that reach a current context without going
through this library's eglCreateContext. The latch inside makes it a
+1 -1
View File
@@ -198,7 +198,7 @@
#define GEXGL_ALPHA_FUNC 197
#define GEXGL_PUSH_ATTRIB 198
#define GEXGL_POP_ATTRIB 199
#define GEXGL_CLIENT_ARRAY 200 /* the NT layer's; see proto.rs */
#define GEXGL_CLIENT_ARRAY 200
#define GEXGL_SURFACE_WAYLAND 0
#define GEXGL_SURFACE_X 1
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
# The guest GL library's cached framebuffer state queries against native Mesa's
# answers (tests/integration/programs/gl-state-query-probe.c), then under gexc
# --gl with the cache on and off.
#
# tests/gl-state-query-cache.sh [gexc] (default target/x86/release/gexc)
set -eu
ROOT=$(CDPATH='' cd -- "$(dirname "$0")/.." && pwd)
GEXC=${1:-$ROOT/target/x86/release/gexc}
SOURCE="$ROOT/tests/integration/programs/gl-state-query-probe.c"
WORK=${WORK:-$(mktemp -d)}
mkdir -p "$WORK"
cc -O2 -Wall -Wextra -o "$WORK/probe" "$SOURCE" -lEGL -lGL
EGL_PLATFORM=surfaceless LIBGL_ALWAYS_SOFTWARE=1 "$WORK/probe" | tee "$WORK/native.log"
grep -q 'failed=0' "$WORK/native.log"
GEX_CC=${GEX_CC:-cc} "$ROOT/guest/gl/build.sh" "$WORK/guest-gl" >/dev/null
"$GEXC" run --gl --env LD_LIBRARY_PATH="$WORK/guest-gl" "$WORK/probe" | tee "$WORK/gex.log"
grep -q 'failed=0' "$WORK/gex.log"
"$GEXC" run --gl --env LD_LIBRARY_PATH="$WORK/guest-gl" --env GEXGL_NO_QUERY_CACHE=1 "$WORK/probe" | tee "$WORK/gex-nocache.log"
grep -q 'failed=0' "$WORK/gex-nocache.log"
echo "ok gl-state-query-cache"
@@ -0,0 +1,88 @@
/* Framebuffer state queries the guest library answers from its cache
* (guest/gl/gexgl.c fbq_*): GL_SAMPLES and GL_READ_BUFFER across binding
* changes, glReadBuffer, renderbuffer storage with samples, and a deleted
* framebuffer whose name comes back. Prints SUMMARY cases=N passed=N failed=0;
* runs on native Mesa too.
*
* cc -o probe gl-state-query-probe.c -lEGL -lGL (native reference)
* gexc run --gl --env LD_LIBRARY_PATH=GUEST_GL probe (tests/gl-state-query-cache.sh) */
#define GL_GLEXT_PROTOTYPES 1
#include <EGL/egl.h>
#include <GL/glcorearb.h>
#include <stdio.h>
static int cases, passed;
static void expect(const char *name, GLenum pname, GLint want) {
GLint got = -1;
glGetIntegerv(pname, &got);
/* Twice: the second answer is the cached one. */
GLint again = -2;
glGetIntegerv(pname, &again);
int ok = got == want && again == want;
cases++;
passed += ok;
printf("CASE %s %s (got %d then %d, want %d)\n", name, ok ? "PASS" : "FAIL", got, again, want);
}
int main(void) {
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY || !eglInitialize(display, NULL, NULL) || !eglBindAPI(EGL_OPENGL_API)) return 2;
const EGLint config_attributes[] = {
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE,
};
EGLConfig config = NULL;
EGLint config_count = 0;
if (!eglChooseConfig(display, config_attributes, &config, 1, &config_count) || config_count < 1) return 2;
const EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE};
const EGLint surface_attributes[] = {EGL_WIDTH, 16, EGL_HEIGHT, 8, EGL_NONE};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attributes);
EGLSurface surface = eglCreatePbufferSurface(display, config, surface_attributes);
if (context == EGL_NO_CONTEXT || surface == EGL_NO_SURFACE || !eglMakeCurrent(display, surface, surface, context))
return 2;
GLint default_read = -1;
glGetIntegerv(GL_READ_BUFFER, &default_read);
GLuint fbo[2], rb[2];
glGenFramebuffers(2, fbo);
glGenRenderbuffers(2, rb);
glBindRenderbuffer(GL_RENDERBUFFER, rb[0]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 16, 8);
glBindRenderbuffer(GL_RENDERBUFFER, rb[1]);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_RGBA8, 16, 8);
GLint ms = 0;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_SAMPLES, &ms);
glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb[0]);
expect("fbo0 samples", GL_SAMPLES, 0);
expect("fbo0 read buffer", GL_READ_BUFFER, GL_COLOR_ATTACHMENT0);
glReadBuffer(GL_NONE);
expect("fbo0 read buffer after glReadBuffer", GL_READ_BUFFER, GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb[1]);
expect("fbo1 samples", GL_SAMPLES, ms);
/* Re-attach the single-sampled one to the same framebuffer. */
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb[0]);
expect("fbo1 samples after re-attaching", GL_SAMPLES, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
expect("default read buffer", GL_READ_BUFFER, default_read);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]);
expect("read default, draw fbo0: read buffer", GL_READ_BUFFER, default_read);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo[0]);
expect("read fbo0: read buffer", GL_READ_BUFFER, GL_NONE);
/* Deleted and made again: the name may come back, with fresh state. */
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo[0]);
GLuint again = 0;
glGenFramebuffers(1, &again);
glBindFramebuffer(GL_FRAMEBUFFER, again);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb[0]);
expect("a new framebuffer: read buffer", GL_READ_BUFFER, GL_COLOR_ATTACHMENT0);
printf("SUMMARY cases=%d passed=%d failed=%d\n", cases, passed, cases - passed);
return cases == passed ? 0 : 1;
}