detached e8dd46c

fix(wayland): skip dead queued buffers without unmapping, keep frame callbacks on role destroy

commit
e8dd46cc7fd34a3151ddac398874b4aa4f22ce0b
author
brice · · 2026-09-25 19:17 UTC
parent
b754135
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +45 -5
M internal/adapters/wayland/fifo.go +12 -5
@@ -127,7 +127,8 @@ func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secL
127127 }
128128 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
129129
130-// maxTimestampAhead bounds how far a commit timestamp can hold a commit.
130+// maxTimestampAhead bounds how far a commit timestamp can hold a commit;
131+// a later one applies early (a deviation from "not before").
131132 const maxTimestampAhead = time.Second
132133
133134 // monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most
@@ -294,9 +295,10 @@ func (s *surface) queueUpdate() {
294295
295296 // applyUpdate applies a queued update, keeping the requests made since.
296297 func (s *surface) applyUpdate(u update) {
297- // A buffer destroyed while queued (a swapchain resize) shows nothing.
298+ // A buffer destroyed while queued (a swapchain resize) is skipped: the
299+ // surface keeps its content, and is not unmapped behind the client.
298300 if u.buffer != nil && !u.buffer.Resource.Alive() {
299- u.buffer = nil
301+ u.buffer, u.attached = nil, false
300302 }
301303 later := s.takePending()
302304 s.putPending(u)
@@ -305,11 +307,16 @@ func (s *surface) applyUpdate(u update) {
305307 }
306308
307309 // dropQueue discards the queued updates of a destroyed surface or role.
310+// A live surface still gets its queued frame callbacks.
308311 func (s *surface) dropQueue() {
309312 released := map[*server.Resource]bool{}
310313 for _, u := range s.queue {
311- for _, cb := range u.callbacks {
312- cb.Destroy()
314+ if s.destroyed {
315+ for _, cb := range u.callbacks {
316+ cb.Destroy()
317+ }
318+ } else if len(u.callbacks) > 0 {
319+ s.server.queueFrames(s.server.frameOutput(s), u.callbacks)
313320 }
314321 if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
315322 released[u.buffer.Resource] = true
@@ -127,7 +127,8 @@ func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secL
127 }127 }
128 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}128 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
129 129
130-// maxTimestampAhead bounds how far a commit timestamp can hold a commit.130+// maxTimestampAhead bounds how far a commit timestamp can hold a commit;
131+// a later one applies early (a deviation from "not before").
131 const maxTimestampAhead = time.Second132 const maxTimestampAhead = time.Second
132 133
133 // monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most134 // monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most
@@ -294,9 +295,10 @@ func (s *surface) queueUpdate() {
294 295
295 // applyUpdate applies a queued update, keeping the requests made since.296 // applyUpdate applies a queued update, keeping the requests made since.
296 func (s *surface) applyUpdate(u update) {297 func (s *surface) applyUpdate(u update) {
297- // A buffer destroyed while queued (a swapchain resize) shows nothing.298+ // A buffer destroyed while queued (a swapchain resize) is skipped: the
299+ // surface keeps its content, and is not unmapped behind the client.
298 if u.buffer != nil && !u.buffer.Resource.Alive() {300 if u.buffer != nil && !u.buffer.Resource.Alive() {
299- u.buffer = nil301+ u.buffer, u.attached = nil, false
300 }302 }
301 later := s.takePending()303 later := s.takePending()
302 s.putPending(u)304 s.putPending(u)
@@ -305,11 +307,16 @@ func (s *surface) applyUpdate(u update) {
305 }307 }
306 308
307 // dropQueue discards the queued updates of a destroyed surface or role.309 // dropQueue discards the queued updates of a destroyed surface or role.
310+// A live surface still gets its queued frame callbacks.
308 func (s *surface) dropQueue() {311 func (s *surface) dropQueue() {
309 released := map[*server.Resource]bool{}312 released := map[*server.Resource]bool{}
310 for _, u := range s.queue {313 for _, u := range s.queue {
311- for _, cb := range u.callbacks {314+ if s.destroyed {
312- cb.Destroy()315+ for _, cb := range u.callbacks {
316+ cb.Destroy()
317+ }
318+ } else if len(u.callbacks) > 0 {
319+ s.server.queueFrames(s.server.frameOutput(s), u.callbacks)
313 }320 }
314 if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {321 if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
315 released[u.buffer.Resource] = true322 released[u.buffer.Resource] = true
M internal/adapters/wayland/fifo_test.go +33 -0
@@ -170,3 +170,36 @@ func TestTickFifoClearsBarrierOnFlip(t *testing.T) {
170170 t.Fatal("barrier kept after the flip")
171171 }
172172 }
173+
174+// A queued commit whose buffer is destroyed (a swapchain resize) is skipped
175+// without unmapping the window.
176+func TestFifoQueuedBufferDestroyed(t *testing.T) {
177+ s, events, _, contents, dir := contentServer(t)
178+ c := protocolClient(t, s, dir)
179+ _, surf, _ := surfaceMapper(t, c, events)()
180+ manager := bindProtocol(t, c, "wp_fifo_manager_v1")
181+ f := c.AllocateID()
182+ registerProtocol(t, c, f)
183+ requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
184+ buf := shmBuffer(t, c)
185+ requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
186+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
187+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
188+ // Queued behind the barrier, then its buffer goes away.
189+ requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
191+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
192+ requestProtocol(t, c, buf, wayland.BufferRequestDestroy)
193+ if err := c.Roundtrip(); err != nil {
194+ t.Fatal(err)
195+ }
196+ drainContents(contents)
197+ time.Sleep(3 * defaultFramePeriod)
198+ select {
199+ case ev := <-events:
200+ if _, ok := ev.(ports.WindowUnmapped); ok {
201+ t.Fatal("window unmapped by a dead queued buffer")
202+ }
203+ default:
204+ }
205+}
@@ -170,3 +170,36 @@ func TestTickFifoClearsBarrierOnFlip(t *testing.T) {
170 t.Fatal("barrier kept after the flip")170 t.Fatal("barrier kept after the flip")
171 }171 }
172 }172 }
173+
174+// A queued commit whose buffer is destroyed (a swapchain resize) is skipped
175+// without unmapping the window.
176+func TestFifoQueuedBufferDestroyed(t *testing.T) {
177+ s, events, _, contents, dir := contentServer(t)
178+ c := protocolClient(t, s, dir)
179+ _, surf, _ := surfaceMapper(t, c, events)()
180+ manager := bindProtocol(t, c, "wp_fifo_manager_v1")
181+ f := c.AllocateID()
182+ registerProtocol(t, c, f)
183+ requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
184+ buf := shmBuffer(t, c)
185+ requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
186+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
187+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
188+ // Queued behind the barrier, then its buffer goes away.
189+ requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
191+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
192+ requestProtocol(t, c, buf, wayland.BufferRequestDestroy)
193+ if err := c.Roundtrip(); err != nil {
194+ t.Fatal(err)
195+ }
196+ drainContents(contents)
197+ time.Sleep(3 * defaultFramePeriod)
198+ select {
199+ case ev := <-events:
200+ if _, ok := ev.(ports.WindowUnmapped); ok {
201+ t.Fatal("window unmapped by a dead queued buffer")
202+ }
203+ default:
204+ }
205+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close