diff --git a/tabbed-wlroots.c b/tabbed-wlroots.c index d001102..4ee68fd 100644 --- a/tabbed-wlroots.c +++ b/tabbed-wlroots.c @@ -47,6 +47,8 @@ #include #include #include +#include +#include #include #include #include @@ -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))