emersion / emersion/mako

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

Open Beginner friendly
#655 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
C
Stars
3.3k
Forks
173
PR merge metrics
No merged PRs in 30d

Description

### 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()`:

```c
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):

```sh
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.

```diff
--- 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.

Contributor guide

Open the contributing guide

Research direction

Start in wayland.c at send_frame(), the get_next_buffer() failure branch, and read schedule_frame_and_commit() to understand its callback behavior. Reproduce with the notification burst and dismissal loop, then verify that repeated "no buffer available" events recover rendering and that notifications, timeouts, and dismissals continue updating without reload.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
desktop
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.