Pace host presents and add cross-context sync barrier

Gate host presents with the surface's wl_callback so at most one buffer is
outstanding at a time (coalescing bursts via back_dirty), add a glFinish
barrier between the wlr GLES2 renderer context and the host EGL context used
by the blit, and switch the scene clear colour to black after removing the
import-failure present. Keep temporary F8/F9 frame dumps (scene vs. host
back buffer) for grey-band diagnosis.
This commit is contained in:
Andrew Hamilton 2026-08-08 12:14:29 +10:00
parent 3d2c627552
commit 570465c6e6

View file

@ -47,6 +47,8 @@
#include <wlr/interfaces/wlr_keyboard.h>
#include <wlr/render/allocator.h>
#include <wlr/render/drm_format_set.h>
#include <wlr/render/egl.h>
#include <wlr/render/gles2.h>
#include <wlr/render/swapchain.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_buffer.h>
@ -144,6 +146,11 @@ struct tabbed_host {
GLint gl_pos_loc, gl_uv_loc, gl_tex_loc;
GLuint import_tex;
GLuint popup_tex;
/* Present gating: at most one buffer aimed at the host compositor at a
time, paced by the host surface's wl_callback. */
struct wl_callback *frame_cb;
bool present_pending;
bool back_dirty;
};
static struct tabbed_server server;
@ -153,6 +160,12 @@ static bool needs_redraw;
static bool frame_pending;
static bool shutting_down;
/* Temporary frame-dump keys (F8/F9) to attribute the lxterm grey band: F8
saves the scene buffer (client content as wlroots drew it), F9 saves our
composited host back buffer. To be removed after diagnosis. */
static bool dump_scene_req, dump_host_req;
static int dump_seq;
static void set_active_surface(struct wlr_xdg_toplevel *toplevel);
static void render_and_present(void);
@ -913,10 +926,85 @@ static void host_kbd_leave(void *data, struct wl_keyboard *kbd, uint32_t serial,
wlr_seat_keyboard_notify_clear_focus(server.seat);
}
/* Temporary grey-band diagnosis: dump the scene buffer (client content as
wlroots rendered it) to /tmp/scene_N.ppm. The buffer memory is top-down,
XRGB8888. */
static void dump_scene_buffer(struct wlr_buffer *buf) {
if (!buf) return;
void *data;
uint32_t format;
size_t stride;
if (!wlr_buffer_begin_data_ptr_access(buf, WLR_BUFFER_DATA_PTR_ACCESS_READ,
&data, &format, &stride))
return;
char path[64];
snprintf(path, sizeof(path), "/tmp/scene_%d.ppm", dump_seq);
FILE *f = fopen(path, "wb");
if (f) {
int w = buf->width, h = buf->height;
fprintf(f, "P6\n%d %d\n255\n", w, h);
unsigned char *p = data;
for (int y = 0; y < h; y++) {
unsigned char *row = p + (size_t)y * stride;
for (int x = 0; x < w; x++) {
fputc(row[x*4+0], f);
fputc(row[x*4+1], f);
fputc(row[x*4+2], f);
}
}
fclose(f);
fprintf(stderr, "[dump] scene buffer -> %s (%dx%d, fmt=0x%x)\n",
path, w, h, format);
}
wlr_buffer_end_data_ptr_access(buf);
dump_seq++;
}
/* Dump what we are about to present to the host (our composited back buffer).
Must be called with the host EGL context current and back buffer freshly
drawn. GL readback is bottom-up, so flip vertically. */
static void dump_host_back(void) {
int w = host.width, h = host.height;
unsigned char *px = malloc((size_t)w * h * 4);
if (!px) return;
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px);
char path[64];
snprintf(path, sizeof(path), "/tmp/host_%d.ppm", dump_seq);
FILE *f = fopen(path, "wb");
if (f) {
fprintf(f, "P6\n%d %d\n255\n", w, h);
for (int y = h - 1; y >= 0; y--) {
unsigned char *row = px + (size_t)y * w * 4;
for (int x = 0; x < w; x++) {
fputc(row[x*4+0], f);
fputc(row[x*4+1], f);
fputc(row[x*4+2], f);
}
}
fclose(f);
fprintf(stderr, "[dump] host back buffer -> %s (%dx%d)\n", path, w, h);
}
free(px);
dump_seq++;
}
static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
uint32_t time, uint32_t key, uint32_t state)
{
(void)data; (void)kbd; (void)serial;
/* Temporary: F8/F9 frame dumps for the grey-band diagnosis. */
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (key == 67) /* F8 */ {
dump_scene_req = true;
needs_redraw = true;
return;
}
if (key == 68) /* F9 */ {
dump_host_req = true;
needs_redraw = true;
return;
}
}
/* Ctrl+PageUp / Ctrl+PageDown cycle tabs */
bool ctrl = (host.host_mods_depressed & (1u << 2)) != 0;
if (state == WL_KEYBOARD_KEY_STATE_PRESSED && ctrl &&
@ -1337,13 +1425,71 @@ static void composite_popups(void) {
popup_draw_cb, &ctx);
}
/* Pace presents to the host compositor: this fires after the host has shown
the buffer we queued, so we know we may queue the next one. Any frame drawn
while a present is still pending is coalesced into the back buffer and
presented here, keeping exactly one outstanding buffer (no burst, no
tearing on busy repainters like terminals). */
static const struct wl_callback_listener host_frame_listener;
/* Request a host frame callback and present the drawing in the back buffer.
The callback must be requested *before* the swap: frame requests are part
of the state committed by eglSwapBuffers, so requesting it after would wait
on a commit that never comes. */
static void present_to_host(void) {
host.frame_cb = wl_surface_frame(host.surface);
wl_callback_add_listener(host.frame_cb, &host_frame_listener, NULL);
eglSwapBuffers(host.egl_display, host.egl_surface);
host.present_pending = true;
}
static void host_frame_done(void *data, struct wl_callback *callback,
uint32_t callback_data)
{
(void)data; (void)callback; (void)callback_data;
if (host.frame_cb) {
wl_callback_destroy(host.frame_cb);
host.frame_cb = NULL;
}
host.present_pending = false;
if (host.back_dirty) {
host.back_dirty = false;
present_to_host();
}
}
static const struct wl_callback_listener host_frame_listener = {
.done = host_frame_done,
};
/* The scene is rendered by the wlroots GLES2 renderer, which owns its own
EGL display/context, while we sample that buffer from a *different* EGL
context (host.egl_display). Without an explicit barrier the GPU may still
be executing the renderer's draw commands when we import+read the dmabuf,
which shows as flicker/tearing on content that repaints continuously.
Make the renderer's context current on this thread and glFinish() so the
buffer is guaranteed fully rendered before we blit it. */
static void sync_renderer_context(void) {
struct wlr_egl *src = wlr_gles2_renderer_get_egl(server.renderer);
if (!src) return;
EGLDisplay d = wlr_egl_get_display(src);
EGLContext c = wlr_egl_get_context(src);
if (d == EGL_NO_DISPLAY || c == EGL_NO_CONTEXT) return;
if (d == host.egl_display && c == host.egl_context) return;
eglMakeCurrent(d, EGL_NO_SURFACE, EGL_NO_SURFACE, c);
glFinish();
eglMakeCurrent(host.egl_display, host.egl_surface, host.egl_surface,
host.egl_context);
}
static void blit_scene_buffer(struct wlr_buffer *buf) {
int w = host.width, h = host.height;
glViewport(0, 0, w, h);
glClearColor(0.13f, 0.13f, 0.13f, 1.0f);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (buf && import_scene_buffer(buf, host.import_tex)) {
bool imported = buf && import_scene_buffer(buf, host.import_tex);
if (imported) {
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
draw_texture_rect(host.import_tex, buf->width, buf->height,
@ -1351,7 +1497,26 @@ static void blit_scene_buffer(struct wlr_buffer *buf) {
glDisable(GL_BLEND);
}
composite_popups();
eglSwapBuffers(host.egl_display, host.egl_surface);
if (dump_host_req) {
dump_host_req = false;
dump_host_back();
}
if (!imported) {
/* Never present a frame that is only the clear colour: keep the last
good frame on the host. A later frame with a good import presents
normally. */
return;
}
if (host.present_pending) {
/* Host hasn't shown the previous buffer yet: redraw into the back
buffer and coalesce the swap until the frame callback arrives. */
host.back_dirty = true;
} else {
present_to_host();
}
}
static void render_and_present(void) {
@ -1371,6 +1536,14 @@ static void render_and_present(void) {
goto frame_done;
}
/* Barrier between the wlr renderer's context and our host blit. */
sync_renderer_context();
if (dump_scene_req) {
dump_scene_req = false;
dump_scene_buffer(state.buffer);
}
blit_scene_buffer(state.buffer);
if (!wlr_output_commit_state(server.output, &state))