From 0c6e455b8b1897462e520d3bad1dedac88b528e8 Mon Sep 17 00:00:00 2001 From: Andros Fenollosa Date: Fri, 12 Jun 2026 07:29:53 +0200 Subject: [PATCH] glterm: survive the driver-side resize race in the partial present A window-manager resize can reallocate the EGL back buffers while Mesa still reports the pre-resize surface size AND a pre-resize buffer age for the present already in flight: the partial repair then lands on a fresh zero-filled buffer and most of the frame goes on screen as garbage. Caught red-handed on AMD/radeonsi under Muffin by the new GL_VERIFY_PRESENT instrument: 232883 wrong pixels (41% of the buffer) in an age=2 partial present during a maximize, healed only by the next redisplay -- the reported black flicker. The client cannot win that race beforehand (both the size query and the age reflect pre-resize state until Mesa validates), but right after the swap Mesa HAS validated the new geometry. So: re-query the surface size after every swap, and if it changed under us, immediately present again in full at the true size -- the artifact never reaches a vertical retrace instead of surviving until the next redisplay. One bounded retry; the steady state pays one cached-state query per swap. GL_VERIFY_PRESENT=1 is the new debug mode behind this finding: before the overlays, it reads the repaired back buffer and the FBO back and reports any mismatch with the present's age and box count -- catching partial-repair holes at the exact present that produced them. Verified on the same hardware and compositor: ten maximize/restore cycles with concurrent scroll storms, free-form resizes mid-scroll and an M-x burst over 817 partial presents produce zero mismatches; the unfixed build reproduced the 41% mismatch under the identical load. Buffer-switch cross-fades measured at 8-10 frames at 60fps (the configured 150ms), confirming no stale-content ghosts remain beyond the designed fade. --- src/glterm.c | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/glterm.c b/src/glterm.c index d3c2d37c0b9..743211c2bfc 100644 --- a/src/glterm.c +++ b/src/glterm.c @@ -980,6 +980,19 @@ gl_present_to_window (struct gl_frame_data *fd) if (!gl_bind_surface (fd->surf)) return; + /* A window-manager resize reaches the server and reallocates the EGL + buffers asynchronously; Mesa can hand us a pre-resize size AND a + pre-resize buffer age for a present that actually lands on a fresh + (zero-filled) buffer -- a partial blit then leaves visible garbage + for a frame (caught live by GL_VERIFY_PRESENT: 41% of the buffer + wrong on a maximize, with age=2). The client cannot win that race + beforehand, but right after the swap Mesa HAS validated the new + geometry, so: present, re-query the size, and if it changed under + us, immediately present again in full at the true size. Bounded to + one retry; the steady state pays one cached-state query per swap. */ + int size_retry = 0; + retry_present:; + /* Vsync on by default (tear-free); GL_NO_VSYNC=1 frees the swap from the vblank so throughput benchmarks measure raw frame cost, not the 60 Hz cap. Set once per surface after it is current. */ @@ -1083,6 +1096,42 @@ gl_present_to_window (struct gl_frame_data *fd) } /* repair.n == 0: nothing changed since this buffer was shown; just swap. */ + /* GL_VERIFY_PRESENT=1: read the repaired back buffer and the FBO back + and compare, BEFORE the overlays (which legitimately diverge). Any + mismatch is a partial-repair hole -- the exact source of "stale or + black rectangle" artifacts -- caught at the present that produced it, + with its age and box count. Debug-only: two full-frame readbacks per + present. */ + { + static int verify = -1; + if (verify == -1) verify = getenv ("GL_VERIFY_PRESENT") ? 1 : 0; + if (verify && sw == fd->w && sh == fd->h) + { + size_t n = (size_t) fd->w * fd->h * 4; + unsigned char *back = malloc (n), *fbop = malloc (n); + if (back && fbop) + { + glBindFramebuffer (GL_READ_FRAMEBUFFER, 0); + glReadPixels (0, 0, fd->w, fd->h, GL_RGBA, GL_UNSIGNED_BYTE, back); + glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->fbo); + glReadPixels (0, 0, fd->w, fd->h, GL_RGBA, GL_UNSIGNED_BYTE, fbop); + size_t bad = 0; + for (size_t i = 0; i < n; i += 4) + if (back[i] != fbop[i] || back[i+1] != fbop[i+1] + || back[i+2] != fbop[i+2]) + bad++; + if (bad) + fprintf (stderr, + "[glverify] MISMATCH %zu px (age=%d %s boxes=%d)\n", + bad, (int) age, full ? "full" : "partial", + full ? 1 : repair.n); + } + free (back); + free (fbop); + glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->fbo); + } + } + #ifdef HAVE_GSTREAMER /* Inline video overlay: draw the latest decoded frame over the static content at its rect, clipped to the window interior, so redisplay can @@ -1177,6 +1226,24 @@ gl_present_to_window (struct gl_frame_data *fd) fd->swap_head = (fd->swap_head + 1) % GL_SWAP_RING; gl_dirty_clear (&fd->dirty); + /* Post-swap size recheck (see the comment at retry_present): if the + surface turns out to have resized under this present, what just went + on screen is partially undefined -- republish everything at the true + size right now instead of leaving the artifact up until the next + redisplay. */ + { + EGLint qw = sw, qh = sh; + eglQuerySurface (g_dpy, fd->surf, EGL_WIDTH, &qw); + eglQuerySurface (g_dpy, fd->surf, EGL_HEIGHT, &qh); + if ((qw != sw || qh != sh) && size_retry++ == 0) + { + fd->surf_w = qw; + fd->surf_h = qh; + fd->dirty.all = true; + goto retry_present; + } + } + /* Keep the window surface current (FBO rendering does not care which surface is bound), so the next frame needs no make-current at all. */ glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);