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:
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.
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.
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.
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) untilmakoctl reload. The D-Bus side stays alive —makoctl listcorrectly 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():When this early-return is taken, there is no recovery path:
send_frame()is typically reached viaframe_handle_done(), which has already destroyedsurface->frame_callbackand 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.surface->dirtyremainstrue, so every subsequentset_dirty()short-circuits atif (surface->dirty) return;— new notifications, timeouts and dismissals no longer trigger a redraw.buffer_handle_release()only clearsbuffer->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.releaserace 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):
After "no buffer available" appears in the log, the on-screen popup freezes on a stale notification while
makoctl listkeeps 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.
The
surface->surface != NULLguard avoids infinite recursion throughschedule_frame_and_commit()'s "no surface yet → callsend_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.