From a3eb373e9f5a67da540560e9b74820dcce62d805 Mon Sep 17 00:00:00 2001 From: KiralyCraft <10259742+KiralyCraft@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:22:28 +0300 Subject: [PATCH 1/7] dri3: bridge native render fences to Present --- src/egl/drivers/dri2/platform_x11_dri3.c | 10 + .../drivers/freedreno/freedreno_batch.c | 8 + .../drivers/freedreno/freedreno_context.c | 19 ++ .../drivers/freedreno/freedreno_context.h | 5 + .../drivers/freedreno/freedreno_resource.c | 11 +- src/gallium/frontends/dri/dri_drawable.c | 92 +++++-- src/gallium/frontends/dri/dri_drawable.h | 6 + src/gallium/frontends/dri/dri_util.h | 5 + .../frontends/dri/loader_dri3_helper.c | 255 +++++++++++++++++- .../frontends/dri/loader_dri3_helper.h | 13 + src/gallium/include/pipe/p_defines.h | 6 + src/glx/dri3_glx.c | 9 + 12 files changed, 416 insertions(+), 23 deletions(-) diff --git a/src/egl/drivers/dri2/platform_x11_dri3.c b/src/egl/drivers/dri2/platform_x11_dri3.c index 3c5a90ac6609..a79f3a3c8731 100644 --- a/src/egl/drivers/dri2/platform_x11_dri3.c +++ b/src/egl/drivers/dri2/platform_x11_dri3.c @@ -101,12 +101,22 @@ egl_dri3_flush_drawable(struct loader_dri3_drawable *draw, unsigned flags) dri2_flush_drawable_for_swapbuffers(disp, &dri3_surf->surf.base); } +static int +egl_dri3_flush_drawable_with_fence_fd(struct loader_dri3_drawable *draw, + unsigned flags) +{ + return loader_dri3_flush_with_fence_fd( + draw, __DRI2_FLUSH_DRAWABLE | __DRI2_FLUSH_INVALIDATE_ANCILLARY, + __DRI2_THROTTLE_SWAPBUFFER); +} + static const struct loader_dri3_vtable egl_dri3_vtable = { .set_drawable_size = egl_dri3_set_drawable_size, .in_current_context = egl_dri3_in_current_context, .get_dri_context = egl_dri3_get_dri_context, .get_dri_screen = egl_dri3_get_dri_screen, .flush_drawable = egl_dri3_flush_drawable, + .flush_drawable_with_fence_fd = egl_dri3_flush_drawable_with_fence_fd, }; static EGLBoolean diff --git a/src/gallium/drivers/freedreno/freedreno_batch.c b/src/gallium/drivers/freedreno/freedreno_batch.c index 22172e576ccc..334c6a17d528 100644 --- a/src/gallium/drivers/freedreno/freedreno_batch.c +++ b/src/gallium/drivers/freedreno/freedreno_batch.c @@ -386,6 +386,14 @@ batch_flush(struct fd_batch *batch, bool last_batch) if (last_batch && !batch->fence) batch->fence = fd_pipe_fence_create(batch); + /* flush_resource() can submit the drawable's writer before the frontend's + * context flush reaches fd_context_flush(). Mark that writer's fence for + * native-fd export here so the Present fence still comes from the rendering + * submission rather than a following empty submit. + */ + if (last_batch && batch->ctx->explicit_present_fence && batch->fence) + batch->fence->use_fence_fd = true; + if (batch->fence) fd_pipe_fence_ref(&batch->ctx->last_fence, batch->fence); diff --git a/src/gallium/drivers/freedreno/freedreno_context.c b/src/gallium/drivers/freedreno/freedreno_context.c index a453d5d38f7d..24be91b1e2b4 100644 --- a/src/gallium/drivers/freedreno/freedreno_context.c +++ b/src/gallium/drivers/freedreno/freedreno_context.c @@ -44,6 +44,7 @@ fd_context_flush(struct pipe_context *pctx, struct pipe_fence_handle **fencep, fd_bc_dump(ctx, "need fence, last_fence=%p", ctx->last_fence); batch = fd_context_batch(ctx); } else if (!batch) { + ctx->explicit_present_fence = false; return; } @@ -118,6 +119,8 @@ fd_context_flush(struct pipe_context *pctx, struct pipe_fence_handle **fencep, fd_bc_dump(ctx, "%p: remaining:\n", ctx); out: + ctx->explicit_present_fence = false; + if (fencep) fd_pipe_fence_ref(fencep, fence); @@ -134,6 +137,21 @@ fd_context_flush(struct pipe_context *pctx, struct pipe_fence_handle **fencep, assert(pctx->get_device_reset_status(pctx) == PIPE_NO_RESET); } +static void +fd_set_context_param(struct pipe_context *pctx, enum pipe_context_param param, + unsigned value) +{ + struct fd_context *ctx = fd_context(pctx); + + switch (param) { + case PIPE_CONTEXT_PARAM_EXPLICIT_PRESENT_FENCE: + ctx->explicit_present_fence = value; + break; + default: + break; + } +} + static void fd_texture_barrier(struct pipe_context *pctx, unsigned flags) in_dt { @@ -699,6 +717,7 @@ fd_context_init(struct fd_context *ctx, struct pipe_screen *pscreen, pctx->screen = pscreen; pctx->priv = priv; pctx->flush = fd_context_flush; + pctx->set_context_param = fd_set_context_param; pctx->emit_string_marker = fd_emit_string_marker; pctx->set_debug_callback = fd_set_debug_callback; pctx->create_fence_fd = fd_create_pipe_fence_fd; diff --git a/src/gallium/drivers/freedreno/freedreno_context.h b/src/gallium/drivers/freedreno/freedreno_context.h index 79529aac2e72..6b31855a79f5 100644 --- a/src/gallium/drivers/freedreno/freedreno_context.h +++ b/src/gallium/drivers/freedreno/freedreno_context.h @@ -358,6 +358,11 @@ struct fd_context { */ struct pipe_fence_handle *last_fence dt; + /* The next context flush exports the render-completion fence directly to + * the window-system presentation path. + */ + bool explicit_present_fence; + /* * Counter to keep track of batch's most recent update. Ie. the batch with * the higher update count is the one that has been drawn/etc to the most diff --git a/src/gallium/drivers/freedreno/freedreno_resource.c b/src/gallium/drivers/freedreno/freedreno_resource.c index eefbad2282e3..3d9e9d9df17f 100644 --- a/src/gallium/drivers/freedreno/freedreno_resource.c +++ b/src/gallium/drivers/freedreno/freedreno_resource.c @@ -694,7 +694,7 @@ fd_flush_resource(struct pipe_context *pctx, struct pipe_resource *prsc) * to the kernel for the fence to be added to the backing GEM * object. */ - if (ctx->no_implicit_sync) + if (ctx->no_implicit_sync && !ctx->screen->is_kgsl) return; flush_resource(ctx, rsc, PIPE_MAP_READ); @@ -703,6 +703,15 @@ fd_flush_resource(struct pipe_context *pctx, struct pipe_resource *prsc) * way to the kernel: */ fd_resource_wait(ctx, rsc, FD_BO_PREP_FLUSH); + + /* KGSL does not attach Mesa's tracked render fence to an exported dma-buf. + * If the window-system path is not carrying the next submission's native + * fence explicitly, wait here before handing the resource to an + * implicit-sync consumer. The explicit Present-fence path avoids this + * per-frame CPU stall. + */ + if (ctx->screen->is_kgsl && !ctx->explicit_present_fence) + fd_resource_wait(ctx, rsc, FD_BO_PREP_READ); } static void diff --git a/src/gallium/frontends/dri/dri_drawable.c b/src/gallium/frontends/dri/dri_drawable.c index cb027a25d046..2d3e7a422af9 100644 --- a/src/gallium/frontends/dri/dri_drawable.c +++ b/src/gallium/frontends/dri/dri_drawable.c @@ -455,28 +455,33 @@ notify_before_flush_cb(void* _args) * \param flags a combination of _DRI2_FLUSH_xxx flags * \param throttle_reason the reason for throttling, 0 = no throttling */ -void -dri_flush(struct dri_context *ctx, - struct dri_drawable *drawable, - unsigned flags, - enum __DRI2throttleReason reason) +static int +dri_flush_impl(struct dri_context *ctx, + struct dri_drawable *drawable, + unsigned flags, + enum __DRI2throttleReason reason, + bool request_fence_fd) { struct st_context *st; + struct pipe_screen *screen; + struct pipe_fence_handle *new_fence = NULL; unsigned flush_flags; + int fence_fd = -1; struct notify_before_flush_cb_args args = { 0 }; if (!ctx) { assert(0); - return; + return -1; } st = ctx->st; + screen = ctx->screen->base.screen; _mesa_glthread_finish(st->ctx); if (drawable) { /* prevent recursion */ if (drawable->flushing) - return; + return -1; drawable->flushing = true; } @@ -505,23 +510,53 @@ dri_flush(struct dri_context *ctx, reason == __DRI2_NOTHROTTLE_SWAPBUFFER) flush_flags |= ST_FLUSH_END_OF_FRAME; - /* Flush the context and throttle if needed. */ - if (ctx->screen->throttle && - drawable && - (reason == __DRI2_THROTTLE_SWAPBUFFER || - reason == __DRI2_THROTTLE_FLUSHFRONT)) { + if (request_fence_fd && screen->caps.native_fence_fd) { + if (st->pipe->set_context_param) { + st->pipe->set_context_param(st->pipe, + PIPE_CONTEXT_PARAM_EXPLICIT_PRESENT_FENCE, + true); + } + flush_flags |= ST_FLUSH_FENCE_FD; + } else { + request_fence_fd = false; + } - struct pipe_screen *screen = drawable->screen->base.screen; - struct pipe_fence_handle *new_fence = NULL; + /* Flush the context and throttle if needed. */ + const bool throttle = ctx->screen->throttle && drawable && + (reason == __DRI2_THROTTLE_SWAPBUFFER || + reason == __DRI2_THROTTLE_FLUSHFRONT); + if ((flags & (__DRI2_FLUSH_DRAWABLE | __DRI2_FLUSH_CONTEXT)) && + (throttle || request_fence_fd)) { st_context_flush(st, flush_flags, &new_fence, args.ctx ? notify_before_flush_cb : NULL, &args); - /* throttle on the previous fence */ - if (drawable->throttle_fence) { - screen->fence_finish(screen, NULL, drawable->throttle_fence, OS_TIMEOUT_INFINITE); - screen->fence_reference(screen, &drawable->throttle_fence, NULL); + if (request_fence_fd && new_fence) + fence_fd = screen->fence_get_fd(screen, new_fence); + + /* If native-fence export failed after the rendering flush, wait on the + * exact pipe fence before allowing an unfenced Present request. + */ + if (request_fence_fd && fence_fd < 0) { + if (!new_fence) + st_context_flush(st, 0, &new_fence, NULL, NULL); + if (new_fence) + screen->fence_finish(screen, NULL, new_fence, + OS_TIMEOUT_INFINITE); } - drawable->throttle_fence = new_fence; + + if (throttle) { + /* throttle on the previous fence */ + if (drawable->throttle_fence) { + screen->fence_finish(screen, NULL, drawable->throttle_fence, + OS_TIMEOUT_INFINITE); + screen->fence_reference(screen, &drawable->throttle_fence, NULL); + } + drawable->throttle_fence = new_fence; + new_fence = NULL; + } + + if (new_fence) + screen->fence_reference(screen, &new_fence, NULL); } else if (flags & (__DRI2_FLUSH_DRAWABLE | __DRI2_FLUSH_CONTEXT)) { st_context_flush(st, flush_flags, NULL, args.ctx ? notify_before_flush_cb : NULL, &args); @@ -550,6 +585,25 @@ dri_flush(struct dri_context *ctx, } st_context_invalidate_state(st, ST_INVALIDATE_FB_STATE); + return fence_fd; +} + +void +dri_flush(struct dri_context *ctx, + struct dri_drawable *drawable, + unsigned flags, + enum __DRI2throttleReason reason) +{ + dri_flush_impl(ctx, drawable, flags, reason, false); +} + +int +dri_flush_with_fence_fd(struct dri_context *ctx, + struct dri_drawable *drawable, + unsigned flags, + enum __DRI2throttleReason reason) +{ + return dri_flush_impl(ctx, drawable, flags, reason, true); } /** diff --git a/src/gallium/frontends/dri/dri_drawable.h b/src/gallium/frontends/dri/dri_drawable.h index 4605c8a6b31d..27775dbf82c8 100644 --- a/src/gallium/frontends/dri/dri_drawable.h +++ b/src/gallium/frontends/dri/dri_drawable.h @@ -152,6 +152,12 @@ dri_flush(struct dri_context *ctx, unsigned flags, enum __DRI2throttleReason reason); +int +dri_flush_with_fence_fd(struct dri_context *ctx, + struct dri_drawable *drawable, + unsigned flags, + enum __DRI2throttleReason reason); + void dri_flush_drawable(struct dri_drawable *dPriv); diff --git a/src/gallium/frontends/dri/dri_util.h b/src/gallium/frontends/dri/dri_util.h index 96713679a231..c9f167293b5b 100644 --- a/src/gallium/frontends/dri/dri_util.h +++ b/src/gallium/frontends/dri/dri_util.h @@ -192,6 +192,11 @@ dri_flush(struct dri_context *cPriv, struct dri_drawable *dPriv, unsigned flags, enum __DRI2throttleReason reason); +PUBLIC int +dri_flush_with_fence_fd(struct dri_context *cPriv, + struct dri_drawable *dPriv, + unsigned flags, + enum __DRI2throttleReason reason); PUBLIC void dri_invalidate_drawable(struct dri_drawable *drawable); diff --git a/src/gallium/frontends/dri/loader_dri3_helper.c b/src/gallium/frontends/dri/loader_dri3_helper.c index 62c8bb96049e..f214898b4618 100644 --- a/src/gallium/frontends/dri/loader_dri3_helper.c +++ b/src/gallium/frontends/dri/loader_dri3_helper.c @@ -22,21 +22,30 @@ */ #include +#include +#include #include #include #include +#include +#include #include #include #include +#include #include #include "loader_dri_helper.h" #include "loader_dri3_helper.h" #include "pipe/p_screen.h" +#include "drm-uapi/dma-buf.h" +#include "util/libsync.h" #include "util/log.h" #include "util/macros.h" +#include "util/u_atomic.h" +#include "util/u_queue.h" #include "util/simple_mtx.h" #include "drm-uapi/drm_fourcc.h" #include "dri_screen.h" @@ -57,6 +66,203 @@ static struct loader_dri3_blit_context blit_context = { SIMPLE_MTX_INITIALIZER, NULL }; +struct loader_dri3_present_sync { + struct util_queue queue; + int cancel_fd; +}; + +struct loader_dri3_present_job { + xcb_connection_t *conn; + xcb_sync_fence_t fence; + int *fence_triggered; + int fence_fd; + int cancel_fd; +}; + +static void +dri3_present_job_execute(void *data, void *gdata, int thread_index) +{ + struct loader_dri3_present_job *job = data; + struct pollfd fds[2] = { + { .fd = job->fence_fd, .events = POLLIN }, + { .fd = job->cancel_fd, .events = POLLIN }, + }; + int ret; + + do { + ret = poll(fds, ARRAY_SIZE(fds), -1); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + mesa_loge("DRI3: failed to wait for presentation fence: %s", + strerror(errno)); + } + + if (ret > 0 && fds[1].revents) + return; + + /* A sync_file normally signals with POLLIN. Trigger on an error too so a + * broken fence cannot leave the X server permanently blocked. + */ + if (ret > 0 && !(fds[0].revents & POLLIN)) + mesa_loge("DRI3: presentation fence reported poll events 0x%x", + fds[0].revents); + + /* Queue TriggerFence under XCB's connection lock before publishing the + * state. Any ResetFence submitted by the reuse thread after observing the + * state is therefore serialized after this trigger request. + */ + xcb_sync_trigger_fence(job->conn, job->fence); + p_atomic_set(job->fence_triggered, true); + xcb_flush(job->conn); +} + +static void +dri3_present_job_cleanup(void *data, void *gdata, int thread_index) +{ + struct loader_dri3_present_job *job = data; + + close(job->fence_fd); + free(job); +} + +static void +dri3_present_sync_fini(struct loader_dri3_drawable *draw) +{ + struct loader_dri3_present_sync *sync = draw->present_sync; + + if (!sync) + return; + + eventfd_write(sync->cancel_fd, 1); + util_queue_finish(&sync->queue); + util_queue_destroy(&sync->queue); + close(sync->cancel_fd); + free(sync); + draw->present_sync = NULL; +} + +static bool +dri3_dmabuf_sync_file_unavailable(int fd) +{ + struct dma_buf_export_sync_file export = { + .flags = DMA_BUF_SYNC_RW, + .fd = -1, + }; + + if (ioctl(fd, DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &export) == 0) { + close(export.fd); + return false; + } + + return errno == ENOTTY || errno == ENOSYS; +} + +static bool +dri3_present_sync_init(struct loader_dri3_drawable *draw, int buffer_fd) +{ + struct loader_dri3_present_sync *sync; + + if (draw->present_sync_checked) + return draw->present_sync != NULL; + + draw->present_sync_checked = true; + + if (draw->type != LOADER_DRI3_DRAWABLE_WINDOW || + draw->dri_screen_render_gpu != draw->dri_screen_display_gpu || + !(dri_fence_get_caps(draw->dri_screen_render_gpu) & + __DRI_FENCE_CAP_NATIVE_FD) || + !dri3_dmabuf_sync_file_unavailable(buffer_fd)) + return false; + + sync = calloc(1, sizeof(*sync)); + if (!sync) + return false; + + sync->cancel_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (sync->cancel_fd < 0) + goto fail; + + if (!util_queue_init(&sync->queue, "present", 8, 1, + UTIL_QUEUE_INIT_RESIZE_IF_FULL, NULL)) + goto fail_cancel_fd; + + draw->present_sync = sync; + return true; + +fail_cancel_fd: + close(sync->cancel_fd); +fail: + free(sync); + return false; +} + +static void +dri3_setup_present_wait_fence(struct loader_dri3_drawable *draw, + struct loader_dri3_buffer *buffer) +{ + if (!draw->present_sync) + return; + + buffer->present_wait_fence = xcb_generate_id(draw->conn); + xcb_void_cookie_t cookie = + xcb_sync_create_fence_checked(draw->conn, draw->window, + buffer->present_wait_fence, false); + xcb_generic_error_t *error = xcb_request_check(draw->conn, cookie); + if (error) { + mesa_loge("DRI3: failed to create Present wait fence: X error %u", + error->error_code); + free(error); + buffer->present_wait_fence = 0; + } +} + +static xcb_sync_fence_t +dri3_queue_present_wait_fence(struct loader_dri3_drawable *draw, + struct loader_dri3_buffer *buffer, + int fence_fd) +{ + struct loader_dri3_present_job *job; + + if (fence_fd < 0) + return XCB_NONE; + + if (!draw->present_sync || !buffer->present_wait_fence) + goto sync_fallback; + + job = calloc(1, sizeof(*job)); + if (!job) + goto sync_fallback; + + job->conn = draw->conn; + job->fence = buffer->present_wait_fence; + job->fence_triggered = &buffer->present_wait_fence_triggered; + job->fence_fd = fence_fd; + job->cancel_fd = draw->present_sync->cancel_fd; + + /* A Sync fence remains triggered until its owner resets it. Reset only + * after our previous worker has triggered this per-buffer fence; resetting + * an unsignaled fence is a Sync Match error. XCB serializes the reset + * before the new worker's trigger request on the shared connection. + */ + if (p_atomic_read(&buffer->present_wait_fence_triggered)) { + xcb_sync_reset_fence(draw->conn, buffer->present_wait_fence); + p_atomic_set(&buffer->present_wait_fence_triggered, false); + } + + util_queue_add_job(&draw->present_sync->queue, job, NULL, + dri3_present_job_execute, dri3_present_job_cleanup, + sizeof(*job)); + return buffer->present_wait_fence; + +sync_fallback: + if (sync_wait(fence_fd, -1)) + mesa_loge("DRI3: failed to wait for presentation fence: %s", + strerror(errno)); + close(fence_fd); + return XCB_NONE; +} + static void dri3_flush_present_events(struct loader_dri3_drawable *draw); @@ -335,6 +541,11 @@ dri3_free_render_buffer(struct loader_dri3_drawable *draw, if (!buffer) return; + if (buffer->present_wait_fence && draw->present_sync) + util_queue_finish(&draw->present_sync->queue); + + if (buffer->present_wait_fence) + xcb_sync_destroy_fence(draw->conn, buffer->present_wait_fence); if (buffer->own_pixmap) xcb_free_pixmap(draw->conn, buffer->pixmap); dri2_destroy_image(buffer->image); @@ -353,6 +564,7 @@ loader_dri3_drawable_fini(struct loader_dri3_drawable *draw) { int i; + dri3_present_sync_fini(draw); driDestroyDrawable(draw->dri_drawable); for (i = 0; i < ARRAY_SIZE(draw->buffers); i++) @@ -400,6 +612,8 @@ loader_dri3_drawable_init(xcb_connection_t *conn, draw->multiplanes_available = multiplanes_available; draw->prefer_back_buffer_reuse = prefer_back_buffer_reuse; draw->queries_buffer_age = false; + draw->present_sync_checked = false; + draw->present_sync = NULL; draw->have_back = 0; draw->have_fake_front = 0; @@ -823,6 +1037,20 @@ loader_dri3_flush(struct loader_dri3_drawable *draw, } } +int +loader_dri3_flush_with_fence_fd(struct loader_dri3_drawable *draw, + unsigned flags, + enum __DRI2throttleReason throttle_reason) +{ + struct dri_context *dri_context = draw->vtable->get_dri_context(draw); + + if (!dri_context) + return -1; + + return dri_flush_with_fence_fd(dri_context, draw->dri_drawable, flags, + throttle_reason); +} + void loader_dri3_copy_sub_buffer(struct loader_dri3_drawable *draw, int x, int y, @@ -1000,6 +1228,7 @@ loader_dri3_swap_buffers_msc(struct loader_dri3_drawable *draw, { struct loader_dri3_buffer *back; int64_t ret = 0; + int render_fence_fd = -1; bool wait_for_next_buffer = false; /* GLX spec: @@ -1030,12 +1259,22 @@ loader_dri3_swap_buffers_msc(struct loader_dri3_drawable *draw, if (!draw->have_back || draw->type == LOADER_DRI3_DRAWABLE_PIXMAP) return ret; - draw->vtable->flush_drawable(draw, flush_flags); + if (draw->type == LOADER_DRI3_DRAWABLE_WINDOW && + draw->present_sync && + draw->vtable->flush_drawable_with_fence_fd) { + render_fence_fd = + draw->vtable->flush_drawable_with_fence_fd(draw, flush_flags); + } else { + draw->vtable->flush_drawable(draw, flush_flags); + } back = dri3_find_back_alloc(draw); /* Could only happen when error case, like display is already closed. */ - if (!back) + if (!back) { + if (render_fence_fd >= 0) + close(render_fence_fd); return ret; + } mtx_lock(&draw->mtx); @@ -1160,6 +1399,9 @@ loader_dri3_swap_buffers_msc(struct loader_dri3_drawable *draw, back->busy = 1; back->last_swap = draw->send_sbc; + xcb_sync_fence_t wait_fence = + dri3_queue_present_wait_fence(draw, back, render_fence_fd); + xcb_xfixes_region_t region = 0; xcb_present_pixmap(draw->conn, @@ -1171,7 +1413,7 @@ loader_dri3_swap_buffers_msc(struct loader_dri3_drawable *draw, 0, /* x_off */ 0, /* y_off */ None, /* target_crtc */ - None, + wait_fence, back->sync_fence, options, target_msc, @@ -1559,6 +1801,12 @@ dri3_alloc_render_buffer(struct loader_dri3_drawable *draw, unsigned int fourcc, if (!ret) buffer->modifier = DRM_FORMAT_MOD_INVALID; + /* Android's dma-buf implementation may lack sync-file import/export even + * though DRI3 buffer sharing itself works. In that case Present needs an + * explicit render-completion fence. + */ + dri3_present_sync_init(draw, buffer_fds[0]); + if (draw->dri_screen_render_gpu != draw->dri_screen_display_gpu && draw->dri_screen_display_gpu && linear_buffer_display_gpu) { /* The linear buffer was created in the display GPU's vram, so we @@ -1616,6 +1864,7 @@ dri3_alloc_render_buffer(struct loader_dri3_drawable *draw, unsigned int fourcc, buffer->own_pixmap = true; buffer->width = width; buffer->height = height; + dri3_setup_present_wait_fence(draw, buffer); /* Mark the buffer as idle */ diff --git a/src/gallium/frontends/dri/loader_dri3_helper.h b/src/gallium/frontends/dri/loader_dri3_helper.h index 26f138d1b831..6104050d4ea8 100644 --- a/src/gallium/frontends/dri/loader_dri3_helper.h +++ b/src/gallium/frontends/dri/loader_dri3_helper.h @@ -65,6 +65,8 @@ struct loader_dri3_buffer { */ uint32_t sync_fence; /* XID of X SyncFence object */ + uint32_t present_wait_fence; /* GPU completion fence for Present */ + int present_wait_fence_triggered; struct xshmfence *shm_fence; /* pointer to xshmfence object */ bool busy; /* Set on swap, cleared on IdleNotify */ bool own_pixmap; /* We allocated the pixmap ID, free on destroy */ @@ -95,6 +97,7 @@ loader_dri3_pixmap_buf_id(enum loader_dri3_buffer_type buffer_type) } struct loader_dri3_drawable; +struct loader_dri3_present_sync; struct loader_dri3_vtable { void (*set_drawable_size)(struct loader_dri3_drawable *, int, int); @@ -102,6 +105,8 @@ struct loader_dri3_vtable { struct dri_context *(*get_dri_context)(struct loader_dri3_drawable *); struct dri_screen *(*get_dri_screen)(void); void (*flush_drawable)(struct loader_dri3_drawable *, unsigned); + int (*flush_drawable_with_fence_fd)(struct loader_dri3_drawable *, + unsigned); }; #define LOADER_DRI3_NUM_BUFFERS (1 + LOADER_DRI3_MAX_BACK) @@ -168,8 +173,11 @@ struct loader_dri3_drawable { bool adaptive_sync_active; bool block_on_depleted_buffers; bool queries_buffer_age; + bool present_sync_checked; int swap_interval; + struct loader_dri3_present_sync *present_sync; + const struct loader_dri3_vtable *vtable; unsigned int back_format; @@ -231,6 +239,11 @@ loader_dri3_flush(struct loader_dri3_drawable *draw, unsigned flags, enum __DRI2throttleReason throttle_reason); +PUBLIC int +loader_dri3_flush_with_fence_fd(struct loader_dri3_drawable *draw, + unsigned flags, + enum __DRI2throttleReason throttle_reason); + PUBLIC void loader_dri3_copy_sub_buffer(struct loader_dri3_drawable *draw, int x, int y, diff --git a/src/gallium/include/pipe/p_defines.h b/src/gallium/include/pipe/p_defines.h index e2bd3cda35c6..91bc4afcc274 100644 --- a/src/gallium/include/pipe/p_defines.h +++ b/src/gallium/include/pipe/p_defines.h @@ -1204,6 +1204,12 @@ enum pipe_context_param * benefits from it. */ PIPE_CONTEXT_PARAM_UPDATE_THREAD_SCHEDULING, + + /* The next flush is used as an explicit presentation fence. Drivers may + * use this to avoid implicit-sync fallbacks while the frontend requests a + * native fence from the same submission. + */ + PIPE_CONTEXT_PARAM_EXPLICIT_PRESENT_FENCE, }; /** diff --git a/src/glx/dri3_glx.c b/src/glx/dri3_glx.c index 5eb08eaf4447..0a2ef15d36ea 100644 --- a/src/glx/dri3_glx.c +++ b/src/glx/dri3_glx.c @@ -132,12 +132,21 @@ glx_dri3_flush_drawable(struct loader_dri3_drawable *draw, unsigned flags) loader_dri3_flush(draw, flags, __DRI2_THROTTLE_SWAPBUFFER); } +static int +glx_dri3_flush_drawable_with_fence_fd(struct loader_dri3_drawable *draw, + unsigned flags) +{ + return loader_dri3_flush_with_fence_fd(draw, flags, + __DRI2_THROTTLE_SWAPBUFFER); +} + static const struct loader_dri3_vtable glx_dri3_vtable = { .set_drawable_size = glx_dri3_set_drawable_size, .in_current_context = glx_dri3_in_current_context, .get_dri_context = glx_dri3_get_dri_context, .get_dri_screen = glx_dri3_get_dri_screen, .flush_drawable = glx_dri3_flush_drawable, + .flush_drawable_with_fence_fd = glx_dri3_flush_drawable_with_fence_fd, }; From 91f7e8c6f03e10446f6435ba23b5803fd4fef228 Mon Sep 17 00:00:00 2001 From: KiralyCraft <10259742+KiralyCraft@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:36:55 +0300 Subject: [PATCH 2/7] freedreno/kgsl: retain merged submits through GPU command ioctl Keep deferred submit objects alive until KGSL has consumed their command lists and populated the shared kernel timestamp. This prevents BO teardown from freeing GPU addresses that the merged submission still references. --- src/freedreno/drm/kgsl/kgsl_ringbuffer_sp.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/freedreno/drm/kgsl/kgsl_ringbuffer_sp.c b/src/freedreno/drm/kgsl/kgsl_ringbuffer_sp.c index e8b2842ac3d4..c4843a1f81b0 100644 --- a/src/freedreno/drm/kgsl/kgsl_ringbuffer_sp.c +++ b/src/freedreno/drm/kgsl/kgsl_ringbuffer_sp.c @@ -59,9 +59,6 @@ flush_submit_list(struct list_head *submit_list) DEBUG_MSG("merged %u submits", cmd_idx); break; } - - list_del(&submit->node); - fd_submit_del(submit); } struct kgsl_cmd_syncpoint_fence sync_fence = { @@ -109,6 +106,18 @@ flush_submit_list(struct list_head *submit_list) close(fd_submit->in_fence_fd); fail: + /* Keep merged submits alive until KGSL has consumed the command list and, + * on success, populated the shared kernel timestamp. Dropping them before + * IOCTL_KGSL_GPU_COMMAND can free BOs that the command stream still uses. + */ + foreach_submit_safe (submit, submit_list) { + if (submit == last_submit(submit_list)) + break; + + list_del(&submit->node); + fd_submit_del(submit); + } + return ret; } From d3a656896af5cd49a1c32cb80215033c65e04f8e Mon Sep 17 00:00:00 2001 From: KiralyCraft Date: Sat, 5 Sep 2026 18:47:35 +0300 Subject: [PATCH 3/7] freedreno/kgsl: cover native BO handle lookup regression A native KGSL BO must keep its owning-device handle even when dma-buf export is unavailable. Document that contract and add a hardware regression test covering repeated lookup before and after sharing and export attempts. The old KMS-handle override is absent from this branch, so no runtime handle replacement is needed. The test submits no GPU work and skips when KGSL is unavailable. Validated with host build/skip, AArch64 build, and a passing native Adreno 740 run using a non-exportable BO. --- src/freedreno/drm/freedreno_drmif.h | 4 ++ src/freedreno/drm/meson.build | 13 ++++ src/freedreno/drm/tests/kgsl_bo_handle_test.c | 68 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 src/freedreno/drm/tests/kgsl_bo_handle_test.c diff --git a/src/freedreno/drm/freedreno_drmif.h b/src/freedreno/drm/freedreno_drmif.h index 930a476ab039..b762fcd3175b 100644 --- a/src/freedreno/drm/freedreno_drmif.h +++ b/src/freedreno/drm/freedreno_drmif.h @@ -313,6 +313,10 @@ void fd_bo_del(struct fd_bo *bo); void fd_bo_del_array(struct fd_bo **bos, int count); void fd_bo_del_list_nocache(struct list_head *list); int fd_bo_get_name(struct fd_bo *bo, uint32_t *name); +/* Return the BO's handle in its owning device's namespace, not a handle for + * another device used for scanout. Native KGSL BOs need not be dma-buf + * exportable to have a valid handle. Suballocated BOs return zero. + */ uint32_t fd_bo_handle(struct fd_bo *bo); int fd_bo_dmabuf_drm(struct fd_bo *bo); int fd_bo_dmabuf(struct fd_bo *bo); diff --git a/src/freedreno/drm/meson.build b/src/freedreno/drm/meson.build index 3383a0cb4434..c0fa2eacb340 100644 --- a/src/freedreno/drm/meson.build +++ b/src/freedreno/drm/meson.build @@ -89,3 +89,16 @@ libfreedreno_drm = static_library( build_by_default : false, ) +if with_tests and freedreno_kmds.contains('kgsl') + test( + 'kgsl_bo_handle', + executable( + 'kgsl_bo_handle_test', + ['tests/kgsl_bo_handle_test.c', freedreno_xml_header_files], + include_directories : libfreedreno_drm_includes, + dependencies : [libfreedreno_drm_deps, idep_libfreedreno_common], + link_with : libfreedreno_drm, + ), + suite : ['freedreno'], + ) +endif diff --git a/src/freedreno/drm/tests/kgsl_bo_handle_test.c b/src/freedreno/drm/tests/kgsl_bo_handle_test.c new file mode 100644 index 000000000000..5d6c21727f46 --- /dev/null +++ b/src/freedreno/drm/tests/kgsl_bo_handle_test.c @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include "freedreno_drmif.h" +#include "kgsl/kgsl_priv.h" + +/* Hardware test: allocate one native KGSL BO, without submitting GPU work. + * The removed KMS-handle override tried to export this BO and returned zero. + * A device-local handle must remain valid even when dma-buf export cannot work. + */ +int +main(void) +{ + int fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "SKIP: /dev/kgsl-3d0 is not accessible\n"); + return 77; + } + + struct fd_device *dev = fd_device_new(fd); + if (!dev) { + close(fd); + fprintf(stderr, "Failed to create KGSL device\n"); + return 1; + } + + struct fd_bo *bo = fd_bo_new(dev, 4096, 0, "handle regression test"); + int result = 1; + if (!bo) { + fprintf(stderr, "Failed to allocate native KGSL BO\n"); + goto out; + } + + uint32_t handle = bo->handle; + if (to_kgsl_bo(bo)->bo_type != KGSL_BO_NATIVE || !handle) { + fprintf(stderr, "Expected a native, non-suballocated KGSL BO\n"); + goto out_bo; + } + + /* The first lookup marks the BO shared; repeat to cover both states. */ + if (fd_bo_handle(bo) != handle || fd_bo_handle(bo) != handle) { + fprintf(stderr, "Native handle was replaced during handle lookup\n"); + goto out_bo; + } + + int dma_fd = fd_bo_dmabuf(bo); + if (dma_fd >= 0) + close(dma_fd); + + if (fd_bo_handle(bo) != handle) { + fprintf(stderr, "dma-buf export attempt changed the native handle\n"); + goto out_bo; + } + + printf("PASS: native KGSL handle survives sharing and %s export\n", + dma_fd < 0 ? "failed" : "successful"); + result = 0; + +out_bo: + fd_bo_del(bo); +out: + fd_device_del(dev); + close(fd); + return result; +} From 011eb29a623c6127cecab4307cf91e35077d32bb Mon Sep 17 00:00:00 2001 From: KiralyCraft Date: Sat, 5 Sep 2026 14:46:11 +0300 Subject: [PATCH 4/7] dri: avoid reflushing the outgoing back image after a fenced swap The explicit Present-fence path flushes the back attachment and exports a fence before passing the image to the loader. At the next image rebind, dri2_allocate_textures flushes that outgoing resource again. On KGSL the explicit-present context flag has already been reset, so this second flush falls back to a synchronous CPU-read wait for GPU completion. Record when a swap successfully flushed a back attachment with an exported fence. Skip only that attachment's redundant replacement flush, consuming the marker on replacement or when validation hands attachments back to the state tracker. Clear it at the start of every drawable flush and exclude shared-buffer mode. Other attachments and unfenced/failed exports retain the existing path. GPU fences and both submission/Present workers remain unchanged. On an Adreno 740 with Termux:X11, alternating clean control/candidate runs improved uncapped gears from about 1000 to 2100 FPS and high-poly glmark2 bump from 720-732 to 1293-1328 FPS. Actual X pixels were checked after Present completion with repeated resize/recreation, shared-context switches, 4x MSAA, and front-buffer rendering. This remains a local review candidate; export-failure fault injection, shared-buffer mode and other drivers have not been runtime validated. (cherry picked from commit 2c0dcd26feaaa2a4f727fdb3c978f3b851ff7ffe) --- src/gallium/frontends/dri/dri2.c | 5 ++++- src/gallium/frontends/dri/dri_drawable.c | 14 ++++++++++++++ src/gallium/frontends/dri/dri_drawable.h | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/gallium/frontends/dri/dri2.c b/src/gallium/frontends/dri/dri2.c index 292757a2ab6f..edb836418ff3 100644 --- a/src/gallium/frontends/dri/dri2.c +++ b/src/gallium/frontends/dri/dri2.c @@ -227,7 +227,8 @@ dri2_allocate_textures(struct dri_context *ctx, /* Flush the texture before unreferencing, so that other clients can * see what the driver has rendered. */ - if (i != ST_ATTACHMENT_DEPTH_STENCIL && drawable->textures[i]) { + if (i != ST_ATTACHMENT_DEPTH_STENCIL && drawable->textures[i] && + !(i == ST_ATTACHMENT_BACK_LEFT && drawable->back_buffer_fenced)) { struct pipe_context *pipe = ctx->st->pipe; pipe->flush_resource(pipe, drawable->textures[i]); } @@ -235,6 +236,8 @@ dri2_allocate_textures(struct dri_context *ctx, pipe_resource_reference(&drawable->textures[i], NULL); } + drawable->back_buffer_fenced = false; + if (drawable->stvis.samples > 1) { for (i = 0; i < ST_ATTACHMENT_COUNT; i++) { bool del = true; diff --git a/src/gallium/frontends/dri/dri_drawable.c b/src/gallium/frontends/dri/dri_drawable.c index 2d3e7a422af9..9d151a3b724c 100644 --- a/src/gallium/frontends/dri/dri_drawable.c +++ b/src/gallium/frontends/dri/dri_drawable.c @@ -106,6 +106,12 @@ dri_st_framebuffer_validate(struct st_context *st, if (!out) return true; + /* Once the state tracker gets the attachments it may render to them again. + * This also invalidates the optimization when validation reused an image + * without calling allocate_textures(). + */ + drawable->back_buffer_fenced = false; + /* Set the window-system buffers for the gallium frontend. */ for (i = 0; i < count; i++) pipe_resource_reference(&out[i], textures[statts[i]]); @@ -484,6 +490,7 @@ dri_flush_impl(struct dri_context *ctx, return -1; drawable->flushing = true; + drawable->back_buffer_fenced = false; } else { flags &= ~__DRI2_FLUSH_DRAWABLE; @@ -584,6 +591,13 @@ dri_flush_impl(struct dri_context *ctx, p_atomic_inc(&drawable->base.stamp); } + if (drawable && args.ctx && fence_fd >= 0 && + !ctx->is_shared_buffer_bound && + (reason == __DRI2_THROTTLE_SWAPBUFFER || + reason == __DRI2_NOTHROTTLE_SWAPBUFFER)) { + drawable->back_buffer_fenced = true; + } + st_context_invalidate_state(st, ST_INVALIDATE_FB_STATE); return fence_fd; } diff --git a/src/gallium/frontends/dri/dri_drawable.h b/src/gallium/frontends/dri/dri_drawable.h index 27775dbf82c8..3e342824fd97 100644 --- a/src/gallium/frontends/dri/dri_drawable.h +++ b/src/gallium/frontends/dri/dri_drawable.h @@ -64,6 +64,12 @@ struct dri_drawable struct pipe_fence_handle *throttle_fence; bool flushing; /* prevents recursion in dri_flush */ + /* The current back attachment was flushed by an explicit-fence swap and + * has not been handed back to the state tracker for further use. Its + * replacement need not repeat the external-consumer flush. + */ + bool back_buffer_fenced; + /** * Private data from the loader. We just hold on to it and pass * it back when calling into loader provided functions. From 1fd79ab0ad45713dc99340b03bf599dc55090fe3 Mon Sep 17 00:00:00 2001 From: KiralyCraft Date: Sat, 5 Sep 2026 18:14:37 +0300 Subject: [PATCH 5/7] freedreno: check local GPU completion before waiting on sync files A locally submitted native fence retains the userspace completion marker. Check that marker before entering sync_wait: KGSL kernel traces show that sync-file signaling can lag command retirement, and profiles measured waits even when the GPU marker had already completed. Use the existing zero-timeout fd_pipe_wait_timeout check. Imported native fences have no userspace sequence and retain the sync-file path, as does a wrapped zero sequence. On Adreno 740 with the PR96 fence path and duplicate-flush fix, isolated high-poly tests improved from 1280 to 1768-1843 FPS. Combined validation recovered uncapped glxgears to within 0.6% of the release and improved the full glmark2 score by 10.5%. Imported pending-fence and pixel checks passed. (cherry picked from commit 64dfdcc29dfc3a9553d5072536c459887ce21672) --- src/gallium/drivers/freedreno/freedreno_fence.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/gallium/drivers/freedreno/freedreno_fence.c b/src/gallium/drivers/freedreno/freedreno_fence.c index c40940e2631d..3b0769018c2e 100644 --- a/src/gallium/drivers/freedreno/freedreno_fence.c +++ b/src/gallium/drivers/freedreno/freedreno_fence.c @@ -144,6 +144,16 @@ fd_pipe_fence_finish(struct pipe_screen *pscreen, struct pipe_context *pctx, if (fence->use_fence_fd) { assert(fence->fence); + /* Locally submitted fences also have a GPU-written completion marker. + * Check it before waiting for the kernel's sync-file notification, as + * fence signaling can lag behind completion of the command stream. + * Imported fences have no userspace sequence number. A wrapped zero + * sequence simply takes the existing sync-file path as well. + */ + if (fence->fence->ufence && + !fd_pipe_wait_timeout(fence->pipe, fence->fence, 0)) + return true; + int ret = sync_wait(fence->fence->fence_fd, timeout / 1000000); return ret == 0; } From b0e50c3d9b912ae6be4c57d553d86876d822ea3c Mon Sep 17 00:00:00 2001 From: KiralyCraft Date: Sat, 5 Sep 2026 18:14:37 +0300 Subject: [PATCH 6/7] dri3: finish each Present wait job before reusing its fence Present can skip a pixmap and release it while its render-fence worker is still pending. Reusing the per-buffer X Sync fence at that point allows overlapping trigger/reset sequences, observed as BadMatch from ResetFence and a stalled client with disable_throttling enabled. Track worker completion with a per-buffer util_queue_fence. Wait before resetting and requeueing the X fence, and before freeing its buffer. Drawable shutdown already drains the queue. This also replaces the queue-wide wait when freeing one buffer. Validated on Termux:X11 with a 30-second high-poly stress run that previously stalled, pixel checks with normal rendering, MSAA, resizing, front updates and context switches, and SurfaceFlinger cadence at 60.13 Hz with no intervals over 25 ms. (cherry picked from commit b11704292b6bc4b5e251b5f62adb50f016c4be35) --- src/gallium/frontends/dri/loader_dri3_helper.c | 17 ++++++++++++++--- src/gallium/frontends/dri/loader_dri3_helper.h | 2 ++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/gallium/frontends/dri/loader_dri3_helper.c b/src/gallium/frontends/dri/loader_dri3_helper.c index f214898b4618..3ac87449fdab 100644 --- a/src/gallium/frontends/dri/loader_dri3_helper.c +++ b/src/gallium/frontends/dri/loader_dri3_helper.c @@ -214,6 +214,8 @@ dri3_setup_present_wait_fence(struct loader_dri3_drawable *draw, error->error_code); free(error); buffer->present_wait_fence = 0; + } else { + util_queue_fence_init(&buffer->present_wait_job); } } @@ -240,6 +242,13 @@ dri3_queue_present_wait_fence(struct loader_dri3_drawable *draw, job->fence_fd = fence_fd; job->cancel_fd = draw->present_sync->cancel_fd; + /* A skipped Present can release its pixmap before the render fence has + * signaled. Buffer idleness therefore does not imply that the previous + * worker is done with this reusable X Sync fence. Finish that job before + * resetting the fence or associating it with another submission. + */ + util_queue_fence_wait(&buffer->present_wait_job); + /* A Sync fence remains triggered until its owner resets it. Reset only * after our previous worker has triggered this per-buffer fence; resetting * an unsignaled fence is a Sync Match error. XCB serializes the reset @@ -250,7 +259,7 @@ dri3_queue_present_wait_fence(struct loader_dri3_drawable *draw, p_atomic_set(&buffer->present_wait_fence_triggered, false); } - util_queue_add_job(&draw->present_sync->queue, job, NULL, + util_queue_add_job(&draw->present_sync->queue, job, &buffer->present_wait_job, dri3_present_job_execute, dri3_present_job_cleanup, sizeof(*job)); return buffer->present_wait_fence; @@ -542,10 +551,12 @@ dri3_free_render_buffer(struct loader_dri3_drawable *draw, return; if (buffer->present_wait_fence && draw->present_sync) - util_queue_finish(&draw->present_sync->queue); + util_queue_fence_wait(&buffer->present_wait_job); - if (buffer->present_wait_fence) + if (buffer->present_wait_fence) { + util_queue_fence_destroy(&buffer->present_wait_job); xcb_sync_destroy_fence(draw->conn, buffer->present_wait_fence); + } if (buffer->own_pixmap) xcb_free_pixmap(draw->conn, buffer->pixmap); dri2_destroy_image(buffer->image); diff --git a/src/gallium/frontends/dri/loader_dri3_helper.h b/src/gallium/frontends/dri/loader_dri3_helper.h index 6104050d4ea8..8edad0b6e520 100644 --- a/src/gallium/frontends/dri/loader_dri3_helper.h +++ b/src/gallium/frontends/dri/loader_dri3_helper.h @@ -34,6 +34,7 @@ #include #include "mesa_interface.h" #include "util/macros.h" +#include "util/u_queue.h" #include enum loader_dri3_buffer_type { @@ -67,6 +68,7 @@ struct loader_dri3_buffer { uint32_t sync_fence; /* XID of X SyncFence object */ uint32_t present_wait_fence; /* GPU completion fence for Present */ int present_wait_fence_triggered; + struct util_queue_fence present_wait_job; struct xshmfence *shm_fence; /* pointer to xshmfence object */ bool busy; /* Set on swap, cleared on IdleNotify */ bool own_pixmap; /* We allocated the pixmap ID, free on destroy */ From 373b340a2dcd429617f000b6139c98ff04bdd93c Mon Sep 17 00:00:00 2001 From: KiralyCraft Date: Sat, 5 Sep 2026 18:37:22 +0300 Subject: [PATCH 7/7] dri3: allow a third back buffer for uncapped fenced copies With the render-wait bridge active, two COPY-mode buffers can both remain held during render completion and server copying. Profiling Termux:X11 refraction showed approximately 30% of main-thread time waiting for an available buffer, versus 5% in the unpatched release. Allow one additional buffer on demand for COPY presentation at swap interval zero when present_sync is active. Synchronized swaps retain the existing limit. FLIP and SKIP policies are unchanged. A buffer-depth ablation scored 661/587 with two buffers, 988 with three and 992 with four. The clean three-buffer build, combined with the previous fixes, averaged 2582 FPS in glxgears and 2128 in glmark2 versus release 2282/1842.5 in A/E/E/A runs. Pixel, MSAA, resize, context-switch, queued-frame, imported-fence and cadence checks passed. (cherry picked from commit 609e727193cb42ad9b56baa2e978e18e145aa676) --- src/gallium/frontends/dri/loader_dri3_helper.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/gallium/frontends/dri/loader_dri3_helper.c b/src/gallium/frontends/dri/loader_dri3_helper.c index 3ac87449fdab..3e6109f9664c 100644 --- a/src/gallium/frontends/dri/loader_dri3_helper.c +++ b/src/gallium/frontends/dri/loader_dri3_helper.c @@ -501,6 +501,15 @@ dri3_update_max_num_back(struct loader_dri3_drawable *draw) case XCB_PRESENT_COMPLETE_MODE_SKIP: break; + case XCB_PRESENT_COMPLETE_MODE_COPY: + /* With an explicit render wait, two buffers can both be held by + * Present while the worker signals one and the server copies the other. + * Allow an uncapped client to render into a third buffer during that + * handoff. Keep the existing limit for interval-controlled swaps. + */ + draw->max_num_back = draw->present_sync && draw->swap_interval == 0 ? 3 : 2; + break; + default: draw->max_num_back = 2; }