Skip to content

Permanent render freeze after "no buffer available" — send_frame() never retries #655

Description

@freasy

Summary

When send_frame() finds both pool buffers busy ("no buffer available"), mako permanently stops rendering: the last-drawn frame stays on screen (expired notifications never disappear, new ones never show) until makoctl reload. The D-Bus side stays alive — makoctl list correctly reports new notifications — only rendering is dead.

Observed on mako 1.11.0 under KWin (Plasma 6 Wayland), roughly once a day in normal use. The code path is unchanged on current master (cce37cd).

Root cause

In wayland.c, send_frame():

	surface->current_buffer =
		get_next_buffer(state->shm, surface->buffers,
		surface->width * scale, surface->height * scale);
	if (surface->current_buffer == NULL) {
		fprintf(stderr, "no buffer available\n");
		return;
	}

When this early-return is taken, there is no recovery path:

  1. send_frame() is typically reached via frame_handle_done(), which has already destroyed surface->frame_callback and set it to NULL. The early return commits nothing and requests no new frame callback, so the compositor never sends another frame event for this surface.
  2. surface->dirty remains true, so every subsequent set_dirty() short-circuits at if (surface->dirty) return; — new notifications, timeouts and dismissals no longer trigger a redraw.
  3. buffer_handle_release() only clears buffer->busy; it does not schedule a redraw.

So a single transient "both buffers busy" event turns into a permanent stall. Compositors are allowed to hold buffers for a while (KWin appears to lose the wl_buffer.release race more often than wlroots during rapid resize/attach sequences), so the client needs to handle this case by retrying.

Reproduction

Under KWin, a burst of differently-sized notifications combined with dismissals reproduces it within a few seconds (each iteration forces a surface resize and reattach):

for round in 1 2 3 4 5; do
  for i in 1 2 3 4 5 6 7 8; do
    notify-send -t 800 "burst$round-$i" "$(head -c $((RANDOM % 200)) /dev/zero | tr '\0' x)" &
  done
  sleep 0.1
  makoctl dismiss -a &
done

After "no buffer available" appears in the log, the on-screen popup freezes on a stale notification while makoctl list keeps reporting new ones.

Fix

Requesting a new frame callback before returning resolves it — on the next frame event the buffer has usually been released and the redraw succeeds. With this patch applied, the same stress test hit the "no buffer available" branch 163 times in a row and recovered every time; unpatched mako freezes on the first hit.

--- a/wayland.c
+++ b/wayland.c
@@ -605,6 +605,14 @@ static void send_frame(struct mako_surface *surface) {
 		surface->width * scale, surface->height * scale);
 	if (surface->current_buffer == NULL) {
 		fprintf(stderr, "no buffer available\n");
+		// Both buffers are still held by the compositor. Keep the surface
+		// dirty and request another frame callback so we retry after the
+		// compositor releases a buffer, instead of stalling forever: without
+		// this, dirty stays true, set_dirty() short-circuits, and no redraw
+		// ever happens again.
+		if (surface->surface != NULL) {
+			schedule_frame_and_commit(surface);
+		}
 		return;
 	}

The surface->surface != NULL guard avoids infinite recursion through schedule_frame_and_commit()'s "no surface yet → call send_frame() directly" path in the (unlikely) case where the surface has been torn down while buffers are still awaiting release.

An alternative/additional fix would be to trigger a redraw from buffer_handle_release() when the surface is dirty, but the frame-callback approach above is minimal and naturally rate-limited by the compositor.

Possibly related: #629 describes another trigger that ends in the same "stops rendering until reload" state.

Happy to turn this into a PR if the approach looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions