detached a30b85b

fix(syncobj): reject points without an attached buffer, wait on acquire fences for subsurfaces and the overlay, and stop polling dropped waits

commit
a30b85b24cceadd1638560e254850c8a46b543f4
author
brice · · 2026-09-26 12:31 UTC
parent
1d6f996
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +240 -32
M internal/adapters/drm/output.go +2 -0
@@ -838,6 +838,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
838838 if done != nil {
839839 done.Close()
840840 }
841+ ov.close()
841842 ov = overlayWin{}
842843 if done, rerr = r.Render(scene, surfaces); rerr != nil {
843844 return fmt.Errorf("render frame: %w", rerr)
@@ -850,6 +851,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
850851 if done != nil {
851852 done.Close()
852853 }
854+ ov.close()
853855 if err != nil && o.overlayConflict(err, ov.buf) {
854856 err = errOverlayDropped
855857 }
@@ -838,6 +838,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
838 if done != nil {838 if done != nil {
839 done.Close()839 done.Close()
840 }840 }
841+ ov.close()
841 ov = overlayWin{}842 ov = overlayWin{}
842 if done, rerr = r.Render(scene, surfaces); rerr != nil {843 if done, rerr = r.Render(scene, surfaces); rerr != nil {
843 return fmt.Errorf("render frame: %w", rerr)844 return fmt.Errorf("render frame: %w", rerr)
@@ -850,6 +851,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
850 if done != nil {851 if done != nil {
851 done.Close()852 done.Close()
852 }853 }
854+ ov.close()
853 if err != nil && o.overlayConflict(err, ov.buf) {855 if err != nil && o.overlayConflict(err, ov.buf) {
854 err = errOverlayDropped856 err = errOverlayDropped
855 }857 }
M internal/adapters/drm/overlay.go +17 -1
@@ -2,6 +2,7 @@ package drm
22
33 import (
44 "errors"
5+ "os"
56 "time"
67
78 "github.com/bnema/nefertty/internal/ports"
@@ -49,6 +50,9 @@ type overlayWin struct {
4950 buf uint64
5051 rect ports.Rect // CRTC rect
5152 w, h int // buffer size
53+ // acquire is the client's explicit-sync fence, a duplicate owned by
54+ // the frame loop (closed after the commit); nil without one.
55+ acquire *os.File
5256 }
5357
5458 // overlayCandidate finds the one window the overlay can show: an opaque
@@ -121,7 +125,8 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S
121125 scale = 1
122126 }
123127 ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height,
124- rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}}
128+ rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height},
129+ acquire: dupFence(c.Acquire)}
125130 }
126131 }
127132 }
@@ -164,6 +169,17 @@ func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
164169 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
165170 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
166171 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
172+ if ov.acquire != nil {
173+ // KMS waits for the client's GPU work before scanning it out.
174+ req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd()))
175+ }
176+}
177+
178+// close releases the frame's duplicate of the acquire fence.
179+func (ov overlayWin) close() {
180+ if ov.acquire != nil {
181+ ov.acquire.Close()
182+ }
167183 }
168184
169185 // testOverlay asks KMS whether it takes the frame with ov on the overlay,
@@ -2,6 +2,7 @@ package drm
2 2
3 import (3 import (
4 "errors"4 "errors"
5+ "os"
5 "time"6 "time"
6 7
7 "github.com/bnema/nefertty/internal/ports"8 "github.com/bnema/nefertty/internal/ports"
@@ -49,6 +50,9 @@ type overlayWin struct {
49 buf uint6450 buf uint64
50 rect ports.Rect // CRTC rect51 rect ports.Rect // CRTC rect
51 w, h int // buffer size52 w, h int // buffer size
53+ // acquire is the client's explicit-sync fence, a duplicate owned by
54+ // the frame loop (closed after the commit); nil without one.
55+ acquire *os.File
52 }56 }
53 57
54 // overlayCandidate finds the one window the overlay can show: an opaque58 // overlayCandidate finds the one window the overlay can show: an opaque
@@ -121,7 +125,8 @@ func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S
121 scale = 1125 scale = 1
122 }126 }
123 ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height,127 ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height,
124- rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}}128+ rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height},
129+ acquire: dupFence(c.Acquire)}
125 }130 }
126 }131 }
127 }132 }
@@ -164,6 +169,17 @@ func (o *Output) overlayProps(req *atomicReq, ov overlayWin) {
164 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))169 req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y)))
165 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))170 req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W))
166 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))171 req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H))
172+ if ov.acquire != nil {
173+ // KMS waits for the client's GPU work before scanning it out.
174+ req.set(p.id, p.prop("IN_FENCE_FD"), uint64(ov.acquire.Fd()))
175+ }
176+}
177+
178+// close releases the frame's duplicate of the acquire fence.
179+func (ov overlayWin) close() {
180+ if ov.acquire != nil {
181+ ov.acquire.Close()
182+ }
167 }183 }
168 184
169 // testOverlay asks KMS whether it takes the frame with ov on the overlay,185 // testOverlay asks KMS whether it takes the frame with ov on the overlay,
M internal/adapters/drm/overlay_test.go +35 -0
@@ -1,6 +1,7 @@
11 package drm
22
33 import (
4+ "os"
45 "testing"
56 "time"
67
@@ -145,3 +146,37 @@ func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
145146 t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
146147 }
147148 }
149+
150+// An explicit-sync window on the overlay: the plane carries a duplicate of
151+// the client's acquire fence, closed once the frame is committed.
152+func TestOverlayCarriesAcquireFence(t *testing.T) {
153+ o, k, commits := overlayOutput(t)
154+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
155+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
156+ s, c := overlayScene()
157+ r, w, err := os.Pipe()
158+ if err != nil {
159+ t.Fatal(err)
160+ }
161+ defer r.Close()
162+ defer w.Close()
163+ win := c[2]
164+ win.Acquire = r
165+ c[2] = win
166+ ov, _ := o.overlayFrame(s, c)
167+ if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
168+ t.Fatal("overlay has no duplicate of the acquire fence")
169+ }
170+ fd := ov.acquire.Fd()
171+ if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
172+ t.Fatal(err)
173+ }
174+ ov.close()
175+ last := (*commits)[len(*commits)-1]
176+ if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
177+ t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
178+ }
179+ if _, err := ov.acquire.Stat(); err == nil {
180+ t.Fatal("duplicate left open")
181+ }
182+}
@@ -1,6 +1,7 @@
1 package drm1 package drm
2 2
3 import (3 import (
4+ "os"
4 "testing"5 "testing"
5 "time"6 "time"
6 7
@@ -145,3 +146,37 @@ func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
145 t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)146 t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
146 }147 }
147 }148 }
149+
150+// An explicit-sync window on the overlay: the plane carries a duplicate of
151+// the client's acquire fence, closed once the frame is committed.
152+func TestOverlayCarriesAcquireFence(t *testing.T) {
153+ o, k, commits := overlayOutput(t)
154+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
155+ o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
156+ s, c := overlayScene()
157+ r, w, err := os.Pipe()
158+ if err != nil {
159+ t.Fatal(err)
160+ }
161+ defer r.Close()
162+ defer w.Close()
163+ win := c[2]
164+ win.Acquire = r
165+ c[2] = win
166+ ov, _ := o.overlayFrame(s, c)
167+ if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
168+ t.Fatal("overlay has no duplicate of the acquire fence")
169+ }
170+ fd := ov.acquire.Fd()
171+ if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
172+ t.Fatal(err)
173+ }
174+ ov.close()
175+ last := (*commits)[len(*commits)-1]
176+ if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
177+ t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
178+ }
179+ if _, err := ov.acquire.Stat(); err == nil {
180+ t.Fatal("duplicate left open")
181+ }
182+}
M internal/adapters/vulkan/dmabuf_test.go +29 -0
@@ -1,6 +1,7 @@
11 package vulkan
22
33 import (
4+ "image"
45 "image/color"
56 "os"
67 "slices"
@@ -106,3 +107,31 @@ func TestRendererImportsDMABuf(t *testing.T) {
106107 t.Fatalf("import kept: %d", len(r.imports))
107108 }
108109 }
110+
111+// A subsurface dmabuf is read after its own acquire fence, not the root's.
112+func TestRendererSubsurfaceAcquire(t *testing.T) {
113+ r, err := New(80, 32)
114+ if err != nil {
115+ t.Skipf("Vulkan unavailable: %v", err)
116+ }
117+ defer r.Close()
118+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
119+ if !slices.Contains(r.DMABuf().Formats, linear) {
120+ t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
121+ }
122+ f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
123+ fence, w, _ := os.Pipe()
124+ defer fence.Close()
125+ defer w.Close()
126+ child := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Acquire: fence,
127+ DMABuf: &ports.DMABuf{ID: 8, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}}
128+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
129+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, Children: []ports.Subsurface{{SurfaceContent: child}}}}
130+ ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 80, 32)))
131+ for _, d := range ds {
132+ if d.im != nil && d.acquire == fence {
133+ return
134+ }
135+ }
136+ t.Fatal("subsurface draw has no acquire fence")
137+}
@@ -1,6 +1,7 @@
1 package vulkan1 package vulkan
2 2
3 import (3 import (
4+ "image"
4 "image/color"5 "image/color"
5 "os"6 "os"
6 "slices"7 "slices"
@@ -106,3 +107,31 @@ func TestRendererImportsDMABuf(t *testing.T) {
106 t.Fatalf("import kept: %d", len(r.imports))107 t.Fatalf("import kept: %d", len(r.imports))
107 }108 }
108 }109 }
110+
111+// A subsurface dmabuf is read after its own acquire fence, not the root's.
112+func TestRendererSubsurfaceAcquire(t *testing.T) {
113+ r, err := New(80, 32)
114+ if err != nil {
115+ t.Skipf("Vulkan unavailable: %v", err)
116+ }
117+ defer r.Close()
118+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
119+ if !slices.Contains(r.DMABuf().Formats, linear) {
120+ t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
121+ }
122+ f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
123+ fence, w, _ := os.Pipe()
124+ defer fence.Close()
125+ defer w.Close()
126+ child := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Acquire: fence,
127+ DMABuf: &ports.DMABuf{ID: 8, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}}
128+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
129+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, Children: []ports.Subsurface{{SurfaceContent: child}}}}
130+ ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 80, 32)))
131+ for _, d := range ds {
132+ if d.im != nil && d.acquire == fence {
133+ return
134+ }
135+ }
136+ t.Fatal("subsurface draw has no acquire fence")
137+}
M internal/adapters/vulkan/renderer.go +1 -3
@@ -333,9 +333,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
333333 }
334334 dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
335335 dr.set, dr.im = im.set, im
336- if key.index == 0 {
337- dr.acquire = content.Acquire
338- }
336+ dr.acquire = content.Acquire
339337 draws = append(draws, dr)
340338 return
341339 }
@@ -333,9 +333,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
333 }333 }
334 dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)334 dr := r.contentDraw(rect, full, content.Width, content.Height, modeImage, content.Opaque)
335 dr.set, dr.im = im.set, im335 dr.set, dr.im = im.set, im
336- if key.index == 0 {336+ dr.acquire = content.Acquire
337- dr.acquire = content.Acquire
338- }
339 draws = append(draws, dr)337 draws = append(draws, dr)
340 return338 return
341 }339 }
M internal/adapters/wayland/inhibit.go +2 -2
@@ -18,8 +18,8 @@ import (
1818 type inhibitor struct {
1919 surf *surface
2020 shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle
21- window ports.WindowID // reported to core (0: not yet)
22- active bool // shortcuts: last state sent
21+ window ports.WindowID // reported to core (0: not yet)
22+ active bool // shortcuts: last state sent
2323 }
2424
2525 func registerInhibit(d *server.Display, s *Server) error {
@@ -18,8 +18,8 @@ import (
18 type inhibitor struct {18 type inhibitor struct {
19 surf *surface19 surf *surface
20 shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle20 shortcut *keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1 // nil: idle
21- window ports.WindowID // reported to core (0: not yet)21+ window ports.WindowID // reported to core (0: not yet)
22- active bool // shortcuts: last state sent22+ active bool // shortcuts: last state sent
23 }23 }
24 24
25 func registerInhibit(d *server.Display, s *Server) error {25 func registerInhibit(d *server.Display, s *Server) error {
M internal/adapters/wayland/presentation.go +5 -5
@@ -22,11 +22,11 @@ const presentationVersion = 2
2222 // feedbackWait is a committed feedback: presented once a flip on the
2323 // surface's output shows content seq of window win.
2424 type feedbackWait struct {
25- fb *presentationtime.WpPresentationFeedback
26- surf *surface
27- win ports.WindowID
28- seq uint64
29- at time.Time
25+ fb *presentationtime.WpPresentationFeedback
26+ surf *surface
27+ win ports.WindowID
28+ seq uint64
29+ at time.Time
3030 // replaced is the window content Seq of the surface's next commit (0:
3131 // none yet): a flip showing it or later shows the replacement.
3232 replaced uint64
@@ -22,11 +22,11 @@ const presentationVersion = 2
22 // feedbackWait is a committed feedback: presented once a flip on the22 // feedbackWait is a committed feedback: presented once a flip on the
23 // surface's output shows content seq of window win.23 // surface's output shows content seq of window win.
24 type feedbackWait struct {24 type feedbackWait struct {
25- fb *presentationtime.WpPresentationFeedback25+ fb *presentationtime.WpPresentationFeedback
26- surf *surface26+ surf *surface
27- win ports.WindowID27+ win ports.WindowID
28- seq uint6428+ seq uint64
29- at time.Time29+ at time.Time
30 // replaced is the window content Seq of the surface's next commit (0:30 // replaced is the window content Seq of the surface's next commit (0:
31 // none yet): a flip showing it or later shows the replacement.31 // none yet): a flip showing it or later shows the replacement.
32 replaced uint6432 replaced uint64
M internal/adapters/wayland/syncobj.go +41 -19
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "os"
77 "sync"
8+ "sync/atomic"
89
910 "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
1011 "github.com/bnema/purego-libwayland/protocol/wayland"
@@ -147,11 +148,12 @@ func (s *surface) checkSyncCommit() bool {
147148 return true
148149 }
149150 a, r := st.acquire, st.release
150- hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil
151+ // Points belong only to a commit that attaches a non-null buffer.
152+ hasBuffer := s.attached && s.pending != nil
151153 res := st.resource
152154 switch {
153155 case !a.set() && !r.set():
154- if hasBuffer && s.attached {
156+ if hasBuffer {
155157 res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point")
156158 return false
157159 }
@@ -194,13 +196,14 @@ type syncWaiter struct {
194196 wake func()
195197 pipeR *os.File
196198 pipeW *os.File
197- add chan *syncWait
199+ added []*syncWait // new waits for run (under mu)
198200 }
199201
200202 // syncWait is one acquire point a queued commit waits for.
201203 type syncWait struct {
202- efd int
203- done bool // the eventfd fired (pacer, under Do)
204+ efd int
205+ done bool // the eventfd fired or the wait was cancelled (pacer, under Do)
206+ cancelled atomic.Bool // run stops polling it and closes efd
204207 }
205208
206209 func newSyncWaiter(wake func()) (*syncWaiter, error) {
@@ -208,7 +211,7 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) {
208211 if err != nil {
209212 return nil, err
210213 }
211- return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil
214+ return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil
212215 }
213216
214217 // watch registers an eventfd for p; the wait is ready once it fires.
@@ -222,7 +225,9 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {
222225 return nil, err
223226 }
224227 w := &syncWait{efd: efd}
225- sw.add <- w
228+ sw.mu.Lock()
229+ sw.added = append(sw.added, w)
230+ sw.mu.Unlock()
226231 _, _ = sw.pipeW.Write([]byte{0})
227232 return w, nil
228233 }
@@ -240,10 +245,12 @@ func (sw *syncWaiter) fired(w *syncWait) bool {
240245
241246 // cancel stops watching w (the commit was dropped).
242247 func (sw *syncWaiter) cancel(w *syncWait) {
248+ w.cancelled.Store(true)
243249 sw.mu.Lock()
244250 delete(sw.ready, w)
245251 sw.mu.Unlock()
246252 w.done = true
253+ _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd
247254 }
248255
249256 // run polls every watched eventfd; it owns and closes them.
@@ -251,11 +258,24 @@ func (sw *syncWaiter) run(ctx context.Context) {
251258 defer sw.pipeR.Close()
252259 var waits []*syncWait
253260 defer func() {
261+ sw.mu.Lock()
262+ waits = append(waits, sw.added...)
263+ sw.added = nil
264+ sw.mu.Unlock()
254265 for _, w := range waits {
255266 unix.Close(w.efd)
256267 }
257268 }()
258269 for ctx.Err() == nil {
270+ live := waits[:0]
271+ for _, w := range waits {
272+ if w.cancelled.Load() {
273+ unix.Close(w.efd)
274+ continue
275+ }
276+ live = append(live, w)
277+ }
278+ waits = live
259279 fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}}
260280 for _, w := range waits {
261281 fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN})
@@ -285,15 +305,10 @@ func (sw *syncWaiter) run(ctx context.Context) {
285305 if fds[0].Revents != 0 {
286306 var b [64]byte
287307 _, _ = sw.pipeR.Read(b[:])
288- drain:
289- for {
290- select {
291- case w := <-sw.add:
292- waits = append(waits, w)
293- default:
294- break drain
295- }
296- }
308+ sw.mu.Lock()
309+ waits = append(waits, sw.added...)
310+ sw.added = nil
311+ sw.mu.Unlock()
297312 }
298313 if fired {
299314 sw.wake()
@@ -362,18 +377,25 @@ func (s *surface) syncReady(cs *commitSync) bool {
362377 return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait)
363378 }
364379
365-// dropSync forgets a commit that will never apply: its points are
366-// signalled so the client can reuse the buffer.
380+// dropSync forgets a commit that will never apply. Its release point is
381+// signalled only once the acquire point has a fence: signalling earlier
382+// could move a shared timeline past a point the client has not submitted.
367383 func (s *surface) dropSync(cs *commitSync) {
368384 if cs == nil {
369385 return
370386 }
387+ ready := cs.wait != nil && s.server.syncWait.fired(cs.wait)
371388 if cs.wait != nil {
372389 s.server.syncWait.cancel(cs.wait)
373390 }
374391 cs.acquire.tl.uses--
375392 s.server.dropTimeline(cs.acquire.tl)
376- s.server.releaseSync(syncHold{release: cs.release})
393+ if ready {
394+ s.server.releaseSync(syncHold{release: cs.release})
395+ return
396+ }
397+ cs.release.tl.uses--
398+ s.server.dropTimeline(cs.release.tl)
377399 }
378400
379401 // applySync makes a commit's points the current buffer's: its acquire
@@ -5,6 +5,7 @@ import (
5 "errors"5 "errors"
6 "os"6 "os"
7 "sync"7 "sync"
8+ "sync/atomic"
8 9
9 "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"10 "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
10 "github.com/bnema/purego-libwayland/protocol/wayland"11 "github.com/bnema/purego-libwayland/protocol/wayland"
@@ -147,11 +148,12 @@ func (s *surface) checkSyncCommit() bool {
147 return true148 return true
148 }149 }
149 a, r := st.acquire, st.release150 a, r := st.acquire, st.release
150- hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil151+ // Points belong only to a commit that attaches a non-null buffer.
152+ hasBuffer := s.attached && s.pending != nil
151 res := st.resource153 res := st.resource
152 switch {154 switch {
153 case !a.set() && !r.set():155 case !a.set() && !r.set():
154- if hasBuffer && s.attached {156+ if hasBuffer {
155 res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point")157 res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point")
156 return false158 return false
157 }159 }
@@ -194,13 +196,14 @@ type syncWaiter struct {
194 wake func()196 wake func()
195 pipeR *os.File197 pipeR *os.File
196 pipeW *os.File198 pipeW *os.File
197- add chan *syncWait199+ added []*syncWait // new waits for run (under mu)
198 }200 }
199 201
200 // syncWait is one acquire point a queued commit waits for.202 // syncWait is one acquire point a queued commit waits for.
201 type syncWait struct {203 type syncWait struct {
202- efd int204+ efd int
203- done bool // the eventfd fired (pacer, under Do)205+ done bool // the eventfd fired or the wait was cancelled (pacer, under Do)
206+ cancelled atomic.Bool // run stops polling it and closes efd
204 }207 }
205 208
206 func newSyncWaiter(wake func()) (*syncWaiter, error) {209 func newSyncWaiter(wake func()) (*syncWaiter, error) {
@@ -208,7 +211,7 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) {
208 if err != nil {211 if err != nil {
209 return nil, err212 return nil, err
210 }213 }
211- return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil214+ return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w}, nil
212 }215 }
213 216
214 // watch registers an eventfd for p; the wait is ready once it fires.217 // watch registers an eventfd for p; the wait is ready once it fires.
@@ -222,7 +225,9 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {
222 return nil, err225 return nil, err
223 }226 }
224 w := &syncWait{efd: efd}227 w := &syncWait{efd: efd}
225- sw.add <- w228+ sw.mu.Lock()
229+ sw.added = append(sw.added, w)
230+ sw.mu.Unlock()
226 _, _ = sw.pipeW.Write([]byte{0})231 _, _ = sw.pipeW.Write([]byte{0})
227 return w, nil232 return w, nil
228 }233 }
@@ -240,10 +245,12 @@ func (sw *syncWaiter) fired(w *syncWait) bool {
240 245
241 // cancel stops watching w (the commit was dropped).246 // cancel stops watching w (the commit was dropped).
242 func (sw *syncWaiter) cancel(w *syncWait) {247 func (sw *syncWaiter) cancel(w *syncWait) {
248+ w.cancelled.Store(true)
243 sw.mu.Lock()249 sw.mu.Lock()
244 delete(sw.ready, w)250 delete(sw.ready, w)
245 sw.mu.Unlock()251 sw.mu.Unlock()
246 w.done = true252 w.done = true
253+ _, _ = sw.pipeW.Write([]byte{0}) // run closes its eventfd
247 }254 }
248 255
249 // run polls every watched eventfd; it owns and closes them.256 // run polls every watched eventfd; it owns and closes them.
@@ -251,11 +258,24 @@ func (sw *syncWaiter) run(ctx context.Context) {
251 defer sw.pipeR.Close()258 defer sw.pipeR.Close()
252 var waits []*syncWait259 var waits []*syncWait
253 defer func() {260 defer func() {
261+ sw.mu.Lock()
262+ waits = append(waits, sw.added...)
263+ sw.added = nil
264+ sw.mu.Unlock()
254 for _, w := range waits {265 for _, w := range waits {
255 unix.Close(w.efd)266 unix.Close(w.efd)
256 }267 }
257 }()268 }()
258 for ctx.Err() == nil {269 for ctx.Err() == nil {
270+ live := waits[:0]
271+ for _, w := range waits {
272+ if w.cancelled.Load() {
273+ unix.Close(w.efd)
274+ continue
275+ }
276+ live = append(live, w)
277+ }
278+ waits = live
259 fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}}279 fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}}
260 for _, w := range waits {280 for _, w := range waits {
261 fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN})281 fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN})
@@ -285,15 +305,10 @@ func (sw *syncWaiter) run(ctx context.Context) {
285 if fds[0].Revents != 0 {305 if fds[0].Revents != 0 {
286 var b [64]byte306 var b [64]byte
287 _, _ = sw.pipeR.Read(b[:])307 _, _ = sw.pipeR.Read(b[:])
288- drain:308+ sw.mu.Lock()
289- for {309+ waits = append(waits, sw.added...)
290- select {310+ sw.added = nil
291- case w := <-sw.add:311+ sw.mu.Unlock()
292- waits = append(waits, w)
293- default:
294- break drain
295- }
296- }
297 }312 }
298 if fired {313 if fired {
299 sw.wake()314 sw.wake()
@@ -362,18 +377,25 @@ func (s *surface) syncReady(cs *commitSync) bool {
362 return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait)377 return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait)
363 }378 }
364 379
365-// dropSync forgets a commit that will never apply: its points are380+// dropSync forgets a commit that will never apply. Its release point is
366-// signalled so the client can reuse the buffer.381+// signalled only once the acquire point has a fence: signalling earlier
382+// could move a shared timeline past a point the client has not submitted.
367 func (s *surface) dropSync(cs *commitSync) {383 func (s *surface) dropSync(cs *commitSync) {
368 if cs == nil {384 if cs == nil {
369 return385 return
370 }386 }
387+ ready := cs.wait != nil && s.server.syncWait.fired(cs.wait)
371 if cs.wait != nil {388 if cs.wait != nil {
372 s.server.syncWait.cancel(cs.wait)389 s.server.syncWait.cancel(cs.wait)
373 }390 }
374 cs.acquire.tl.uses--391 cs.acquire.tl.uses--
375 s.server.dropTimeline(cs.acquire.tl)392 s.server.dropTimeline(cs.acquire.tl)
376- s.server.releaseSync(syncHold{release: cs.release})393+ if ready {
394+ s.server.releaseSync(syncHold{release: cs.release})
395+ return
396+ }
397+ cs.release.tl.uses--
398+ s.server.dropTimeline(cs.release.tl)
377 }399 }
378 400
379 // applySync makes a commit's points the current buffer's: its acquire401 // applySync makes a commit's points the current buffer's: its acquire
M internal/adapters/wayland/syncobj_test.go +108 -2
@@ -4,6 +4,7 @@ import (
44 "context"
55 "os"
66 "sync"
7+ "strings"
78 "testing"
89 "time"
910 "unsafe"
@@ -185,11 +186,15 @@ func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) {
185186 }
186187 }
187188
189+func (h *syncHarness) setPoints(acquire, release uint64) {
190+ requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire))
191+ requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release))
192+}
193+
188194 func (h *syncHarness) commit(buf uint32, acquire, release uint64) {
189195 t, c := h.t, h.c
190196 requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
191- requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire))
192- requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release))
197+ h.setPoints(acquire, release)
193198 requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)
194199 if err := c.Roundtrip(); err != nil {
195200 t.Fatal(err)
@@ -315,6 +320,34 @@ func TestSyncobjProtocolErrors(t *testing.T) {
315320 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
316321 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
317322 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
323+ {"no acquire point", func(h *syncHarness) {
324+ b := h.dmabuf()
325+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
326+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
327+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
328+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
329+ {"points without attach", func(h *syncHarness) {
330+ b := h.dmabuf()
331+ h.commit(b, 1, 2)
332+ h.fire(1)
333+ h.setPoints(3, 4)
334+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
335+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
336+ {"points with null attach", func(h *syncHarness) {
337+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
338+ h.setPoints(1, 2)
339+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
340+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
341+ {"wl_shm buffer", func(h *syncHarness) {
342+ b := shmBuffer(t, h.c)
343+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
344+ h.setPoints(1, 2)
345+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
346+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},
347+ {"surface destroyed", func(h *syncHarness) {
348+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
349+ h.setPoints(1, 2)
350+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},
318351 {"surface exists", func(h *syncHarness) {
319352 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
320353 h.mgr = mgr
@@ -353,3 +386,76 @@ func TestSyncobjInvalidTimeline(t *testing.T) {
353386 }
354387 expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
355388 }
389+
390+// A commit dropped before its acquire point has a fence signals nothing
391+// (the client has not submitted that point) and sends no wl_buffer.release;
392+// the waiter closes its eventfd and no acquire file is left open.
393+func TestSyncobjDroppedCommit(t *testing.T) {
394+ h := newSyncHarness(t)
395+ h.releases = map[uint32]*syncReleaseProxy{}
396+ a := h.dmabuf()
397+ before := openFDs(t)
398+ h.commit(a, 1, 2)
399+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
400+ if err := h.c.Roundtrip(); err != nil {
401+ t.Fatal(err)
402+ }
403+ // The point never gets a fence: the waiter must still stop polling it.
404+ time.Sleep(250 * time.Millisecond)
405+ if err := h.c.Roundtrip(); err != nil {
406+ t.Fatal(err)
407+ }
408+ if got := h.signalled(); len(got) != 0 {
409+ t.Fatalf("signalled %v for a commit whose acquire point never arrived", got)
410+ }
411+ select {
412+ case <-h.releases[a].released:
413+ t.Fatal("wl_buffer.release sent under explicit sync")
414+ default:
415+ }
416+ // The harness keeps its own duplicate of the eventfd.
417+ if after := openFDs(t); after > before+1 {
418+ t.Fatalf("eventfds and sync files %d -> %d", before, after)
419+ }
420+}
421+
422+// A commit dropped after its acquire point fired signals its release point.
423+func TestSyncobjDroppedReadyCommitReleases(t *testing.T) {
424+ h := newSyncHarness(t)
425+ h.releases = map[uint32]*syncReleaseProxy{}
426+ a := h.dmabuf()
427+ h.commit(a, 1, 2)
428+ h.fire(1)
429+ if _, ok := h.content(2 * time.Second); !ok {
430+ t.Fatal("content")
431+ }
432+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
433+ deadline := time.Now().Add(2 * time.Second)
434+ for len(h.signalled()) == 0 && time.Now().Before(deadline) {
435+ if err := h.c.Roundtrip(); err != nil {
436+ t.Fatal(err)
437+ }
438+ time.Sleep(2 * time.Millisecond)
439+ }
440+ if got := h.signalled(); len(got) != 1 || got[0] != 2 {
441+ t.Fatalf("signalled %v", got)
442+ }
443+}
444+
445+// openFDs counts the eventfds and sync files (pipes stand in for them)
446+// the process holds.
447+func openFDs(t *testing.T) int {
448+ t.Helper()
449+ ents, err := os.ReadDir("/proc/self/fd")
450+ if err != nil {
451+ t.Fatal(err)
452+ }
453+ n := 0
454+ for _, e := range ents {
455+ l, _ := os.Readlink("/proc/self/fd/" + e.Name())
456+ if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") {
457+ n++
458+ }
459+ }
460+ return n
461+}
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "os"5 "os"
6 "sync"6 "sync"
7+ "strings"
7 "testing"8 "testing"
8 "time"9 "time"
9 "unsafe"10 "unsafe"
@@ -185,11 +186,15 @@ func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) {
185 }186 }
186 }187 }
187 188
189+func (h *syncHarness) setPoints(acquire, release uint64) {
190+ requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire))
191+ requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release))
192+}
193+
188 func (h *syncHarness) commit(buf uint32, acquire, release uint64) {194 func (h *syncHarness) commit(buf uint32, acquire, release uint64) {
189 t, c := h.t, h.c195 t, c := h.t, h.c
190 requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))196 requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
191- requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire))197+ h.setPoints(acquire, release)
192- requestProtocol(t, c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release))
193 requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)198 requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)
194 if err := c.Roundtrip(); err != nil {199 if err := c.Roundtrip(); err != nil {
195 t.Fatal(err)200 t.Fatal(err)
@@ -315,6 +320,34 @@ func TestSyncobjProtocolErrors(t *testing.T) {
315 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))320 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
316 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)321 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
317 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},322 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
323+ {"no acquire point", func(h *syncHarness) {
324+ b := h.dmabuf()
325+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
326+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
327+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
328+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
329+ {"points without attach", func(h *syncHarness) {
330+ b := h.dmabuf()
331+ h.commit(b, 1, 2)
332+ h.fire(1)
333+ h.setPoints(3, 4)
334+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
335+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
336+ {"points with null attach", func(h *syncHarness) {
337+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
338+ h.setPoints(1, 2)
339+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
340+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
341+ {"wl_shm buffer", func(h *syncHarness) {
342+ b := shmBuffer(t, h.c)
343+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
344+ h.setPoints(1, 2)
345+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
346+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},
347+ {"surface destroyed", func(h *syncHarness) {
348+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
349+ h.setPoints(1, 2)
350+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},
318 {"surface exists", func(h *syncHarness) {351 {"surface exists", func(h *syncHarness) {
319 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")352 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
320 h.mgr = mgr353 h.mgr = mgr
@@ -353,3 +386,76 @@ func TestSyncobjInvalidTimeline(t *testing.T) {
353 }386 }
354 expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))387 expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
355 }388 }
389+
390+// A commit dropped before its acquire point has a fence signals nothing
391+// (the client has not submitted that point) and sends no wl_buffer.release;
392+// the waiter closes its eventfd and no acquire file is left open.
393+func TestSyncobjDroppedCommit(t *testing.T) {
394+ h := newSyncHarness(t)
395+ h.releases = map[uint32]*syncReleaseProxy{}
396+ a := h.dmabuf()
397+ before := openFDs(t)
398+ h.commit(a, 1, 2)
399+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
400+ if err := h.c.Roundtrip(); err != nil {
401+ t.Fatal(err)
402+ }
403+ // The point never gets a fence: the waiter must still stop polling it.
404+ time.Sleep(250 * time.Millisecond)
405+ if err := h.c.Roundtrip(); err != nil {
406+ t.Fatal(err)
407+ }
408+ if got := h.signalled(); len(got) != 0 {
409+ t.Fatalf("signalled %v for a commit whose acquire point never arrived", got)
410+ }
411+ select {
412+ case <-h.releases[a].released:
413+ t.Fatal("wl_buffer.release sent under explicit sync")
414+ default:
415+ }
416+ // The harness keeps its own duplicate of the eventfd.
417+ if after := openFDs(t); after > before+1 {
418+ t.Fatalf("eventfds and sync files %d -> %d", before, after)
419+ }
420+}
421+
422+// A commit dropped after its acquire point fired signals its release point.
423+func TestSyncobjDroppedReadyCommitReleases(t *testing.T) {
424+ h := newSyncHarness(t)
425+ h.releases = map[uint32]*syncReleaseProxy{}
426+ a := h.dmabuf()
427+ h.commit(a, 1, 2)
428+ h.fire(1)
429+ if _, ok := h.content(2 * time.Second); !ok {
430+ t.Fatal("content")
431+ }
432+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
433+ deadline := time.Now().Add(2 * time.Second)
434+ for len(h.signalled()) == 0 && time.Now().Before(deadline) {
435+ if err := h.c.Roundtrip(); err != nil {
436+ t.Fatal(err)
437+ }
438+ time.Sleep(2 * time.Millisecond)
439+ }
440+ if got := h.signalled(); len(got) != 1 || got[0] != 2 {
441+ t.Fatalf("signalled %v", got)
442+ }
443+}
444+
445+// openFDs counts the eventfds and sync files (pipes stand in for them)
446+// the process holds.
447+func openFDs(t *testing.T) int {
448+ t.Helper()
449+ ents, err := os.ReadDir("/proc/self/fd")
450+ if err != nil {
451+ t.Fatal(err)
452+ }
453+ n := 0
454+ for _, e := range ents {
455+ l, _ := os.Readlink("/proc/self/fd/" + e.Name())
456+ if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") {
457+ n++
458+ }
459+ }
460+ return n
461+}

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