guest/gl: a swap at interval 0 submits its frame without waiting

eglSwapBuffers (and glXSwapBuffers through it) waited for the frame's
batch to run so it could pace on the reply's count of frames shown. At
swap interval 0 nothing is paced (Unity with vsync off), so the batch now
goes with GEX_GL_SUBMIT_ASYNC, which both hosts already serve for the NT
layer's frames: the host copies it and the guest builds the next frame
while it runs; the next submit waits for it. In Muck's page the render
thread spent 48% of its time in synchronous submits. A host that refuses
the ioctl gets every batch the waiting way; GEXGL_SYNC_PRESENT=1 never
tries.

The state-query probe adds 50 swaps at interval 0 with reads between
(tests/gl-state-query-cache.sh: 10 of 10 natively and under gexc --gl).

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 10:22:09 -04:00
1 parent 707e84b817
commit 7605de5ce4
2 files changed
+69 -1

No files matched your search

+50
View File
@@ -317,6 +317,49 @@ static int fbq_invalidates(uint32_t opcode) {
}
}
/* Hand everything recorded so far to the host without waiting for it to
run: GEX_GL_SUBMIT_ASYNC (shims/native/src/gl.rs, shims/browser/src/gl.rs)
copies the batch and returns, and the next submit of either kind waits for
it. For a batch with nothing to answer -- a frame's last, when nothing is
paced on its reply. In the page a synchronous submit is a round trip to
the compositor's Worker, which is most of what Unity's render thread spent
its time on. A host without it refuses once, and every batch then goes
the waiting way. GEXGL_SYNC_PRESENT=1 never tries. */
#define GEXGL_SUBMIT_ASYNC 0xc0086704u
static int async_refused = -1;
static void flush_async(void) {
if (!ring || ring->tail == 0) {
reply_at = 0;
return;
}
if (async_refused < 0) async_refused = getenv("GEXGL_SYNC_PRESENT") ? 1 : 0;
if (async_refused) {
flush();
return;
}
gexgl_note_submit();
if (ioctl(gl_fd, GEXGL_SUBMIT_ASYNC, ring) != 0) {
if (errno == ENOTTY || errno == EINVAL || errno == ENOSYS) {
/* Refused before anything ran (an older host): the batch is still
in the ring, so submit it the waiting way and stop trying. */
async_refused = 1;
flush();
return;
}
/* EIO: the batch was taken, and the one before it failed; that is
this call's report, as flush() makes one. */
static int said;
if (!said) {
said = 1;
fprintf(stderr, "gexgl: an asynchronous batch failed on the host\n");
}
}
ring->tail = 0;
ring->reply_len = 0;
reply_at = 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. */
@@ -3235,6 +3278,13 @@ unsigned eglSwapBuffers(void *dpy, void *surface) {
gexgl_x11_follow_size(s);
gexgl_wl_commit(s);
CMD0(GEXGL_PRESENT);
/* Nothing is paced on the reply at swap interval 0 (Unity with vsync
off), so the frame goes without waiting for it. */
if (egl_swap_interval == 0) {
flush_async();
++egl_swaps_issued;
return EGL_TRUE;
}
flush();
/* How many frames the page has actually shown. Zero from a decoder that
predates this reply, which is why nothing below acts on zero. */
@@ -1,7 +1,8 @@
/* 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;
* framebuffer whose name comes back; and swaps at interval 0, which the
* library submits without waiting. 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)
@@ -83,6 +84,23 @@ int main(void) {
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb[0]);
expect("a new framebuffer: read buffer", GL_READ_BUFFER, GL_COLOR_ATTACHMENT0);
/* Swaps at interval 0, which the guest library submits without waiting
* (flush_async), interleaved with reads that must see each frame. */
eglSwapInterval(display, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
int frames_ok = 1;
for (int i = 0; i < 50; i++) {
glClearColor((float)(i & 1), 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
unsigned char px[4] = {7, 7, 7, 7};
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
if (px[0] != ((i & 1) ? 255 : 0) || px[2] != 255) frames_ok = 0;
eglSwapBuffers(display, surface);
}
cases++;
passed += frames_ok;
printf("CASE swaps at interval 0 %s\n", frames_ok ? "PASS" : "FAIL");
printf("SUMMARY cases=%d passed=%d failed=%d\n", cases, passed, cases - passed);
return cases == passed ? 0 : 1;
}