detached b754135

fix(wayland): fifo barrier deadline, dead or role-less queued buffers, clamped timestamps

commit
b7541350ca36c23071c1e454561db997c82984e8
author
brice · · 2026-09-25 19:15 UTC
parent
d5fa86c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +92 -18
M internal/adapters/wayland/fifo.go +47 -18
@@ -122,16 +122,31 @@ func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secL
122122 case !h.surface.pendingTime.IsZero():
123123 r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set")
124124 default:
125- h.surface.pendingTime = monotonicTime(int64(secHi)<<32|int64(secLo), int64(nsec))
125+ h.surface.pendingTime = monotonicTime(uint64(secHi)<<32|uint64(secLo), int64(nsec))
126126 }
127127 }
128128 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
129129
130-// monotonicTime converts a CLOCK_MONOTONIC time to wall time.
131-func monotonicTime(sec, nsec int64) time.Time {
130+// maxTimestampAhead bounds how far a commit timestamp can hold a commit.
131+const maxTimestampAhead = time.Second
132+
133+// monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most
134+// maxTimestampAhead from now: a far timestamp must not stall the surface.
135+func monotonicTime(sec uint64, nsec int64) time.Time {
132136 var now unix.Timespec
133- _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &now)
134- return time.Now().Add(time.Duration((sec-now.Sec)*1e9 + nsec - now.Nsec))
137+ if unix.ClockGettime(unix.CLOCK_MONOTONIC, &now) != nil {
138+ return time.Now()
139+ }
140+ ahead := time.Duration(nsec - now.Nsec)
141+ switch n := uint64(now.Sec); {
142+ case sec >= n+2:
143+ ahead = maxTimestampAhead
144+ case sec+2 <= n:
145+ ahead = 0 // in the past: apply now
146+ default:
147+ ahead += time.Duration(int64(sec)-now.Sec) * time.Second
148+ }
149+ return time.Now().Add(min(max(ahead, 0), maxTimestampAhead))
135150 }
136151
137152 // content_type_v1
@@ -279,19 +294,25 @@ func (s *surface) queueUpdate() {
279294
280295 // applyUpdate applies a queued update, keeping the requests made since.
281296 func (s *surface) applyUpdate(u update) {
297+ // A buffer destroyed while queued (a swapchain resize) shows nothing.
298+ if u.buffer != nil && !u.buffer.Resource.Alive() {
299+ u.buffer = nil
300+ }
282301 later := s.takePending()
283302 s.putPending(u)
284303 s.applyCommit()
285304 s.putPending(later)
286305 }
287306
288-// dropQueue discards the queued updates of a destroyed surface.
307+// dropQueue discards the queued updates of a destroyed surface or role.
289308 func (s *surface) dropQueue() {
309+ released := map[*server.Resource]bool{}
290310 for _, u := range s.queue {
291311 for _, cb := range u.callbacks {
292312 cb.Destroy()
293313 }
294- if u.buffer != nil && u.buffer != s.current && u.buffer.Resource.Alive() {
314+ if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
315+ released[u.buffer.Resource] = true
295316 u.buffer.SendRelease()
296317 }
297318 }
@@ -329,23 +350,27 @@ func (s *Server) outputPeriod(name string) time.Duration {
329350 // clear it after one refresh, or 1.5 when they usually flip. It returns
330351 // the time to the next deadline and whether any surface still waits.
331352 func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) {
332- for name := range flipped {
333- if flipped[name] {
353+ for name, f := range flipped {
354+ if f {
334355 s.lastFlip[name] = now
335356 }
336357 }
358+ for name := range s.lastFlip {
359+ if s.outputByNameExact(name) == nil {
360+ delete(s.lastFlip, name)
361+ }
362+ }
337363 wait := defaultFramePeriod
338364 for surf := range s.fifoSurfaces {
339365 name := s.fifoOutput(surf)
340366 p := s.outputPeriod(name)
341- if surf.barrier {
342- deadline := surf.barrierAt.Add(p)
343- if name != "" && now.Sub(s.lastFlip[name]) < time.Second {
344- deadline = deadline.Add(p / 2)
345- }
346- if (flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline) {
347- surf.barrier = false
348- }
367+ // The deadline of outputs that usually flip only catches a missed flip.
368+ deadline := surf.barrierAt.Add(p)
369+ if name != "" && now.Sub(s.lastFlip[name]) < time.Second {
370+ deadline = deadline.Add(p / 2)
371+ }
372+ if surf.barrier && ((flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline)) {
373+ surf.barrier = false
349374 }
350375 for len(surf.queue) > 0 && !surf.destroyed {
351376 u := surf.queue[0]
@@ -360,7 +385,6 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
360385 surf.applyUpdate(u)
361386 }
362387 if surf.barrier {
363- deadline := surf.barrierAt.Add(p)
364388 wait = min(wait, deadline.Sub(now))
365389 }
366390 if !surf.barrier && len(surf.queue) == 0 {
@@ -370,6 +394,11 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
370394 return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0
371395 }
372396
397+// sameBuffer reports whether two wrappers are the same wl_buffer.
398+func sameBuffer(a, b *wayland.Buffer) bool {
399+ return a != nil && b != nil && a.Resource == b.Resource
400+}
401+
373402 // wakePacer makes the pacer look at its queues now.
374403 func (s *Server) wakePacer() {
375404 select {
@@ -122,16 +122,31 @@ func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secL
122 case !h.surface.pendingTime.IsZero():122 case !h.surface.pendingTime.IsZero():
123 r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set")123 r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set")
124 default:124 default:
125- h.surface.pendingTime = monotonicTime(int64(secHi)<<32|int64(secLo), int64(nsec))125+ h.surface.pendingTime = monotonicTime(uint64(secHi)<<32|uint64(secLo), int64(nsec))
126 }126 }
127 }127 }
128 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}128 func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
129 129
130-// monotonicTime converts a CLOCK_MONOTONIC time to wall time.130+// maxTimestampAhead bounds how far a commit timestamp can hold a commit.
131-func monotonicTime(sec, nsec int64) time.Time {131+const maxTimestampAhead = time.Second
132+
133+// monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most
134+// maxTimestampAhead from now: a far timestamp must not stall the surface.
135+func monotonicTime(sec uint64, nsec int64) time.Time {
132 var now unix.Timespec136 var now unix.Timespec
133- _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &now)137+ if unix.ClockGettime(unix.CLOCK_MONOTONIC, &now) != nil {
134- return time.Now().Add(time.Duration((sec-now.Sec)*1e9 + nsec - now.Nsec))138+ return time.Now()
139+ }
140+ ahead := time.Duration(nsec - now.Nsec)
141+ switch n := uint64(now.Sec); {
142+ case sec >= n+2:
143+ ahead = maxTimestampAhead
144+ case sec+2 <= n:
145+ ahead = 0 // in the past: apply now
146+ default:
147+ ahead += time.Duration(int64(sec)-now.Sec) * time.Second
148+ }
149+ return time.Now().Add(min(max(ahead, 0), maxTimestampAhead))
135 }150 }
136 151
137 // content_type_v1152 // content_type_v1
@@ -279,19 +294,25 @@ func (s *surface) queueUpdate() {
279 294
280 // applyUpdate applies a queued update, keeping the requests made since.295 // applyUpdate applies a queued update, keeping the requests made since.
281 func (s *surface) applyUpdate(u update) {296 func (s *surface) applyUpdate(u update) {
297+ // A buffer destroyed while queued (a swapchain resize) shows nothing.
298+ if u.buffer != nil && !u.buffer.Resource.Alive() {
299+ u.buffer = nil
300+ }
282 later := s.takePending()301 later := s.takePending()
283 s.putPending(u)302 s.putPending(u)
284 s.applyCommit()303 s.applyCommit()
285 s.putPending(later)304 s.putPending(later)
286 }305 }
287 306
288-// dropQueue discards the queued updates of a destroyed surface.307+// dropQueue discards the queued updates of a destroyed surface or role.
289 func (s *surface) dropQueue() {308 func (s *surface) dropQueue() {
309+ released := map[*server.Resource]bool{}
290 for _, u := range s.queue {310 for _, u := range s.queue {
291 for _, cb := range u.callbacks {311 for _, cb := range u.callbacks {
292 cb.Destroy()312 cb.Destroy()
293 }313 }
294- if u.buffer != nil && u.buffer != s.current && u.buffer.Resource.Alive() {314+ if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
315+ released[u.buffer.Resource] = true
295 u.buffer.SendRelease()316 u.buffer.SendRelease()
296 }317 }
297 }318 }
@@ -329,23 +350,27 @@ func (s *Server) outputPeriod(name string) time.Duration {
329 // clear it after one refresh, or 1.5 when they usually flip. It returns350 // clear it after one refresh, or 1.5 when they usually flip. It returns
330 // the time to the next deadline and whether any surface still waits.351 // the time to the next deadline and whether any surface still waits.
331 func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) {352 func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) {
332- for name := range flipped {353+ for name, f := range flipped {
333- if flipped[name] {354+ if f {
334 s.lastFlip[name] = now355 s.lastFlip[name] = now
335 }356 }
336 }357 }
358+ for name := range s.lastFlip {
359+ if s.outputByNameExact(name) == nil {
360+ delete(s.lastFlip, name)
361+ }
362+ }
337 wait := defaultFramePeriod363 wait := defaultFramePeriod
338 for surf := range s.fifoSurfaces {364 for surf := range s.fifoSurfaces {
339 name := s.fifoOutput(surf)365 name := s.fifoOutput(surf)
340 p := s.outputPeriod(name)366 p := s.outputPeriod(name)
341- if surf.barrier {367+ // The deadline of outputs that usually flip only catches a missed flip.
342- deadline := surf.barrierAt.Add(p)368+ deadline := surf.barrierAt.Add(p)
343- if name != "" && now.Sub(s.lastFlip[name]) < time.Second {369+ if name != "" && now.Sub(s.lastFlip[name]) < time.Second {
344- deadline = deadline.Add(p / 2)370+ deadline = deadline.Add(p / 2)
345- }371+ }
346- if (flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline) {372+ if surf.barrier && ((flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline)) {
347- surf.barrier = false373+ surf.barrier = false
348- }
349 }374 }
350 for len(surf.queue) > 0 && !surf.destroyed {375 for len(surf.queue) > 0 && !surf.destroyed {
351 u := surf.queue[0]376 u := surf.queue[0]
@@ -360,7 +385,6 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
360 surf.applyUpdate(u)385 surf.applyUpdate(u)
361 }386 }
362 if surf.barrier {387 if surf.barrier {
363- deadline := surf.barrierAt.Add(p)
364 wait = min(wait, deadline.Sub(now))388 wait = min(wait, deadline.Sub(now))
365 }389 }
366 if !surf.barrier && len(surf.queue) == 0 {390 if !surf.barrier && len(surf.queue) == 0 {
@@ -370,6 +394,11 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
370 return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0394 return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0
371 }395 }
372 396
397+// sameBuffer reports whether two wrappers are the same wl_buffer.
398+func sameBuffer(a, b *wayland.Buffer) bool {
399+ return a != nil && b != nil && a.Resource == b.Resource
400+}
401+
373 // wakePacer makes the pacer look at its queues now.402 // wakePacer makes the pacer look at its queues now.
374 func (s *Server) wakePacer() {403 func (s *Server) wakePacer() {
375 select {404 select {
M internal/adapters/wayland/fifo_test.go +42 -0
@@ -128,3 +128,45 @@ func TestCommitTimingDelaysCommit(t *testing.T) {
128128 requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0))
129129 expectProtocolError(t, c, timer, uint32(committiming.WpCommitTimerV1ErrorTimestampExists))
130130 }
131+
132+// Far or past timestamps never hold a surface longer than a second.
133+func TestMonotonicTimeClamps(t *testing.T) {
134+ var now unix.Timespec
135+ if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
136+ t.Fatal(err)
137+ }
138+ for _, tc := range []struct {
139+ sec uint64
140+ nsec int64
141+ lo, hi time.Duration
142+ }{
143+ {^uint64(0), 0, maxTimestampAhead - 10*time.Millisecond, maxTimestampAhead},
144+ {0, 0, -10 * time.Millisecond, 10 * time.Millisecond},
145+ {uint64(now.Sec), now.Nsec, -10 * time.Millisecond, 10 * time.Millisecond},
146+ } {
147+ got := time.Until(monotonicTime(tc.sec, tc.nsec))
148+ if got < tc.lo || got > tc.hi {
149+ t.Errorf("sec %d: %v not in [%v, %v]", tc.sec, got, tc.lo, tc.hi)
150+ }
151+ }
152+}
153+
154+// A barrier on an output that flips clears on its next flip.
155+func TestTickFifoClearsBarrierOnFlip(t *testing.T) {
156+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
157+ s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)}
158+ surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}}
159+ now := time.Unix(100, 0)
160+ s.lastFlip["DP-2"] = now.Add(-time.Millisecond)
161+ surf.setBarrier(now)
162+ if _, waiting := s.tickFifo(now.Add(time.Millisecond), nil); !waiting || !surf.barrier {
163+ t.Fatal("barrier cleared before the flip")
164+ }
165+ // A flipping output waits up to 1.5 refreshes for a missed flip.
166+ if w, _ := s.tickFifo(now.Add(20*time.Millisecond), nil); !surf.barrier || w < 4*time.Millisecond {
167+ t.Fatalf("barrier %v wait %v", surf.barrier, w)
168+ }
169+ if _, waiting := s.tickFifo(now.Add(21*time.Millisecond), map[string]bool{"DP-2": true}); waiting || surf.barrier {
170+ t.Fatal("barrier kept after the flip")
171+ }
172+}
@@ -128,3 +128,45 @@ func TestCommitTimingDelaysCommit(t *testing.T) {
128 requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0))128 requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0))
129 expectProtocolError(t, c, timer, uint32(committiming.WpCommitTimerV1ErrorTimestampExists))129 expectProtocolError(t, c, timer, uint32(committiming.WpCommitTimerV1ErrorTimestampExists))
130 }130 }
131+
132+// Far or past timestamps never hold a surface longer than a second.
133+func TestMonotonicTimeClamps(t *testing.T) {
134+ var now unix.Timespec
135+ if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
136+ t.Fatal(err)
137+ }
138+ for _, tc := range []struct {
139+ sec uint64
140+ nsec int64
141+ lo, hi time.Duration
142+ }{
143+ {^uint64(0), 0, maxTimestampAhead - 10*time.Millisecond, maxTimestampAhead},
144+ {0, 0, -10 * time.Millisecond, 10 * time.Millisecond},
145+ {uint64(now.Sec), now.Nsec, -10 * time.Millisecond, 10 * time.Millisecond},
146+ } {
147+ got := time.Until(monotonicTime(tc.sec, tc.nsec))
148+ if got < tc.lo || got > tc.hi {
149+ t.Errorf("sec %d: %v not in [%v, %v]", tc.sec, got, tc.lo, tc.hi)
150+ }
151+ }
152+}
153+
154+// A barrier on an output that flips clears on its next flip.
155+func TestTickFifoClearsBarrierOnFlip(t *testing.T) {
156+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
157+ s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)}
158+ surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}}
159+ now := time.Unix(100, 0)
160+ s.lastFlip["DP-2"] = now.Add(-time.Millisecond)
161+ surf.setBarrier(now)
162+ if _, waiting := s.tickFifo(now.Add(time.Millisecond), nil); !waiting || !surf.barrier {
163+ t.Fatal("barrier cleared before the flip")
164+ }
165+ // A flipping output waits up to 1.5 refreshes for a missed flip.
166+ if w, _ := s.tickFifo(now.Add(20*time.Millisecond), nil); !surf.barrier || w < 4*time.Millisecond {
167+ t.Fatalf("barrier %v wait %v", surf.barrier, w)
168+ }
169+ if _, waiting := s.tickFifo(now.Add(21*time.Millisecond), map[string]bool{"DP-2": true}); waiting || surf.barrier {
170+ t.Fatal("barrier kept after the flip")
171+ }
172+}
M internal/adapters/wayland/layer.go +1 -0
@@ -76,6 +76,7 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
7676 delete(h.server.layers, l.id)
7777 state.layer = nil
7878 state.role = nil
79+ state.dropQueue()
7980 state.current, state.pending = nil, nil
8081 state.attached = false
8182 }
@@ -76,6 +76,7 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
76 delete(h.server.layers, l.id)76 delete(h.server.layers, l.id)
77 state.layer = nil77 state.layer = nil
78 state.role = nil78 state.role = nil
79+ state.dropQueue()
79 state.current, state.pending = nil, nil80 state.current, state.pending = nil, nil
80 state.attached = false81 state.attached = false
81 }82 }
M internal/adapters/wayland/popup.go +1 -0
@@ -75,6 +75,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
7575 }
7676 w.unmap()
7777 pp.done = true
78+ x.surface.dropQueue()
7879 x.surface.current, x.surface.pending = nil, nil
7980 x.surface.attached = false
8081 delete(s.windows, w.id)
@@ -75,6 +75,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
75 }75 }
76 w.unmap()76 w.unmap()
77 pp.done = true77 pp.done = true
78+ x.surface.dropQueue()
78 x.surface.current, x.surface.pending = nil, nil79 x.surface.current, x.surface.pending = nil, nil
79 x.surface.attached = false80 x.surface.attached = false
80 delete(s.windows, w.id)81 delete(s.windows, w.id)
M internal/adapters/wayland/xdg.go +1 -0
@@ -222,6 +222,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
222222 }
223223 t.OnDestroy = func() {
224224 w.unmap()
225+ x.surface.dropQueue()
225226 x.surface.current, x.surface.pending = nil, nil
226227 x.surface.attached = false
227228 delete(x.server.windows, w.id)
@@ -222,6 +222,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
222 }222 }
223 t.OnDestroy = func() {223 t.OnDestroy = func() {
224 w.unmap()224 w.unmap()
225+ x.surface.dropQueue()
225 x.surface.current, x.surface.pending = nil, nil226 x.surface.current, x.surface.pending = nil, nil
226 x.surface.attached = false227 x.surface.attached = false
227 delete(x.server.windows, w.id)228 delete(x.server.windows, w.id)

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