detached 6f16509

feat(wayland): wp_linux_drm_syncobj_v1 explicit sync with acquire fences for the renderer and KMS

Timelines are imported on the renderer's render node when it supports timeline syncobjs. A commit with points waits in the surface queue until its acquire point has a fence (one goroutine polls the kernel eventfds); the content then carries the fence as a sync file, which the renderer imports as a wait semaphore and direct scanout passes as IN_FENCE_FD from a duplicate. The release point is signalled when no output reads the buffer any more, instead of wl_buffer.release, and the fence is closed then. Protocol errors follow the spec.
commit
6f1650908aece0f1306c4c8e7e06eb5390c3b7b5
author
brice · · 2026-09-26 12:11 UTC
parent
f0db5a6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
19 files · +1468 -27
M .mockery.yml +5 -0
@@ -30,6 +30,11 @@ packages:
3030 dir: internal/adapters/wayland
3131 pkgname: wayland
3232 filename: proc_env_mock_test.go
33+ syncobjDevice:
34+ config:
35+ dir: internal/adapters/wayland
36+ pkgname: wayland
37+ filename: syncobj_device_mock_test.go
3338 github.com/bnema/nefertty/internal/adapters/statefile:
3439 interfaces:
3540 procTree:
@@ -30,6 +30,11 @@ packages:
30 dir: internal/adapters/wayland30 dir: internal/adapters/wayland
31 pkgname: wayland31 pkgname: wayland
32 filename: proc_env_mock_test.go32 filename: proc_env_mock_test.go
33+ syncobjDevice:
34+ config:
35+ dir: internal/adapters/wayland
36+ pkgname: wayland
37+ filename: syncobj_device_mock_test.go
33 github.com/bnema/nefertty/internal/adapters/statefile:38 github.com/bnema/nefertty/internal/adapters/statefile:
34 interfaces:39 interfaces:
35 procTree:40 procTree:
M internal/adapters/drm/output.go +9 -2
@@ -562,13 +562,20 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame
562562 async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync
563563 vrr := o.wantVRR(true)
564564 f.queued, f.zeroCopy = c.DMABuf.ID, true
565- err := o.commitFrame(fb, nil, async, vrr, f)
565+ // Explicit sync: KMS waits on the client's acquire fence. Wayland may
566+ // close its file at any time: commit a duplicate taken while it is
567+ // held open (the kernel keeps its own reference).
568+ fence := dupFence(c.Acquire)
569+ if fence != nil {
570+ defer fence.Close()
571+ }
572+ err := o.commitFrame(fb, fence, async, vrr, f)
566573 if err != nil && async && errors.Is(err, unix.EINVAL) {
567574 // Refused (e.g. not a fast update): this buffer flips at vblank
568575 // from now on.
569576 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
570577 cfb.noAsync = true
571- err = o.commitFrame(fb, nil, false, vrr, f)
578+ err = o.commitFrame(fb, fence, false, vrr, f)
572579 }
573580 if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) {
574581 // KMS refuses this buffer on the plane: compose it instead.
@@ -562,13 +562,20 @@ func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame
562 async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync562 async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync
563 vrr := o.wantVRR(true)563 vrr := o.wantVRR(true)
564 f.queued, f.zeroCopy = c.DMABuf.ID, true564 f.queued, f.zeroCopy = c.DMABuf.ID, true
565- err := o.commitFrame(fb, nil, async, vrr, f)565+ // Explicit sync: KMS waits on the client's acquire fence. Wayland may
566+ // close its file at any time: commit a duplicate taken while it is
567+ // held open (the kernel keeps its own reference).
568+ fence := dupFence(c.Acquire)
569+ if fence != nil {
570+ defer fence.Close()
571+ }
572+ err := o.commitFrame(fb, fence, async, vrr, f)
566 if err != nil && async && errors.Is(err, unix.EINVAL) {573 if err != nil && async && errors.Is(err, unix.EINVAL) {
567 // Refused (e.g. not a fast update): this buffer flips at vblank574 // Refused (e.g. not a fast update): this buffer flips at vblank
568 // from now on.575 // from now on.
569 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")576 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
570 cfb.noAsync = true577 cfb.noAsync = true
571- err = o.commitFrame(fb, nil, false, vrr, f)578+ err = o.commitFrame(fb, fence, false, vrr, f)
572 }579 }
573 if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) {580 if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) {
574 // KMS refuses this buffer on the plane: compose it instead.581 // KMS refuses this buffer on the plane: compose it instead.
M internal/adapters/drm/scanout.go +18 -0
@@ -8,6 +8,7 @@ import (
88 "unsafe"
99
1010 "github.com/bnema/nefertty/internal/ports"
11+ "golang.org/x/sys/unix"
1112 )
1213
1314 // Direct scanout (ADR 006): when one fullscreen window covers the output
@@ -272,3 +273,20 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
272273 delete(o.clientFBs, id)
273274 }
274275 }
276+
277+// dupFence duplicates a sync file under its SyscallConn, so a concurrent
278+// Close cannot hand us a reused fd; nil when f is nil or closed.
279+func dupFence(f *os.File) *os.File {
280+ if f == nil {
281+ return nil
282+ }
283+ raw, err := f.SyscallConn()
284+ if err != nil {
285+ return nil
286+ }
287+ fd := -1
288+ if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 {
289+ return nil
290+ }
291+ return os.NewFile(uintptr(fd), "acquire-fence")
292+}
@@ -8,6 +8,7 @@ import (
8 "unsafe"8 "unsafe"
9 9
10 "github.com/bnema/nefertty/internal/ports"10 "github.com/bnema/nefertty/internal/ports"
11+ "golang.org/x/sys/unix"
11 )12 )
12 13
13 // Direct scanout (ADR 006): when one fullscreen window covers the output14 // Direct scanout (ADR 006): when one fullscreen window covers the output
@@ -272,3 +273,20 @@ func (o *Output) dropClientFBs(now time.Time, all bool) {
272 delete(o.clientFBs, id)273 delete(o.clientFBs, id)
273 }274 }
274 }275 }
276+
277+// dupFence duplicates a sync file under its SyscallConn, so a concurrent
278+// Close cannot hand us a reused fd; nil when f is nil or closed.
279+func dupFence(f *os.File) *os.File {
280+ if f == nil {
281+ return nil
282+ }
283+ raw, err := f.SyscallConn()
284+ if err != nil {
285+ return nil
286+ }
287+ fd := -1
288+ if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 {
289+ return nil
290+ }
291+ return os.NewFile(uintptr(fd), "acquire-fence")
292+}
M internal/adapters/vulkan/compose.go +3 -0
@@ -5,6 +5,7 @@ import (
55 "encoding/binary"
66 "image"
77 "math"
8+ "os"
89 "unsafe"
910
1011 vk "github.com/bnema/purego-vulkan/vulkan"
@@ -51,6 +52,8 @@ type draw struct {
5152 pc pushConstants
5253 set vk.DescriptorSet
5354 im *imported // sampled client dmabuf, for ownership barriers
55+ // acquire is the content's explicit-sync fence (nil: implicit).
56+ acquire *os.File
5457 }
5558
5659 // composer is the pipeline state and the objects every draw can bind.
@@ -5,6 +5,7 @@ import (
5 "encoding/binary"5 "encoding/binary"
6 "image"6 "image"
7 "math"7 "math"
8+ "os"
8 "unsafe"9 "unsafe"
9 10
10 vk "github.com/bnema/purego-vulkan/vulkan"11 vk "github.com/bnema/purego-vulkan/vulkan"
@@ -51,6 +52,8 @@ type draw struct {
51 pc pushConstants52 pc pushConstants
52 set vk.DescriptorSet53 set vk.DescriptorSet
53 im *imported // sampled client dmabuf, for ownership barriers54 im *imported // sampled client dmabuf, for ownership barriers
55+ // acquire is the content's explicit-sync fence (nil: implicit).
56+ acquire *os.File
54 }57 }
55 58
56 // composer is the pipeline state and the objects every draw can bind.59 // composer is the pipeline state and the objects every draw can bind.
M internal/adapters/vulkan/compose_test.go +31 -0
@@ -3,6 +3,7 @@ package vulkan
33 import (
44 "context"
55 "image/color"
6+ "os"
67 "slices"
78 "testing"
89 "time"
@@ -182,3 +183,33 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
182183 t.Fatalf("pixel %v", got)
183184 }
184185 }
186+
187+// A content's explicit acquire fence becomes a wait semaphore; a file
188+// that is not a sync file is refused (no semaphore) without failing the
189+// frame, and the caller's file stays open.
190+func TestRendererImportsAcquireFence(t *testing.T) {
191+ r, err := New(8, 8)
192+ if err != nil {
193+ t.Skipf("Vulkan unavailable: %v", err)
194+ }
195+ defer r.Close()
196+ done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
197+ if err != nil || done == nil {
198+ t.Skipf("no sync file: %v", err)
199+ }
200+ defer done.Close()
201+ sem := r.importFence(done)
202+ if sem == 0 {
203+ t.Fatal("sync file not imported")
204+ }
205+ r.dd.DestroySemaphore(r.device, sem, nil)
206+ if _, err := done.Stat(); err != nil {
207+ t.Fatal("caller's fence closed")
208+ }
209+ f, w, _ := os.Pipe()
210+ defer f.Close()
211+ defer w.Close()
212+ if sem := r.importFence(f); sem != 0 {
213+ t.Fatal("pipe imported as a fence")
214+ }
215+}
@@ -3,6 +3,7 @@ package vulkan
3 import (3 import (
4 "context"4 "context"
5 "image/color"5 "image/color"
6+ "os"
6 "slices"7 "slices"
7 "testing"8 "testing"
8 "time"9 "time"
@@ -182,3 +183,33 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
182 t.Fatalf("pixel %v", got)183 t.Fatalf("pixel %v", got)
183 }184 }
184 }185 }
186+
187+// A content's explicit acquire fence becomes a wait semaphore; a file
188+// that is not a sync file is refused (no semaphore) without failing the
189+// frame, and the caller's file stays open.
190+func TestRendererImportsAcquireFence(t *testing.T) {
191+ r, err := New(8, 8)
192+ if err != nil {
193+ t.Skipf("Vulkan unavailable: %v", err)
194+ }
195+ defer r.Close()
196+ done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
197+ if err != nil || done == nil {
198+ t.Skipf("no sync file: %v", err)
199+ }
200+ defer done.Close()
201+ sem := r.importFence(done)
202+ if sem == 0 {
203+ t.Fatal("sync file not imported")
204+ }
205+ r.dd.DestroySemaphore(r.device, sem, nil)
206+ if _, err := done.Stat(); err != nil {
207+ t.Fatal("caller's fence closed")
208+ }
209+ f, w, _ := os.Pipe()
210+ defer f.Close()
211+ defer w.Close()
212+ if sem := r.importFence(f); sem != 0 {
213+ t.Fatal("pipe imported as a fence")
214+ }
215+}
M internal/adapters/vulkan/dmabuf.go +19 -3
@@ -287,15 +287,31 @@ func (r *Renderer) readFence(im *imported) vk.Semaphore {
287287 if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {
288288 return 0
289289 }
290+ return r.importSyncFD(int(arg.fd))
291+}
292+
293+// importFence makes a wait semaphore of a sync file. The file stays the
294+// caller's: Vulkan takes a duplicate, made while the file is held open.
295+func (r *Renderer) importFence(f *os.File) vk.Semaphore {
296+ fd, err := dupFile(f)
297+ if err != nil {
298+ return 0
299+ }
300+ return r.importSyncFD(fd)
301+}
302+
303+// importSyncFD imports a sync file fd as a temporary semaphore payload;
304+// the fd is Vulkan's on success and closed on failure.
305+func (r *Renderer) importSyncFD(fd int) vk.Semaphore {
290306 var sem vk.Semaphore
291307 si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
292308 if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {
293- unix.Close(int(arg.fd))
309+ unix.Close(fd)
294310 return 0
295311 }
296- imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: arg.fd}
312+ imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: int32(fd)}
297313 if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {
298- unix.Close(int(arg.fd))
314+ unix.Close(fd)
299315 r.dd.DestroySemaphore(r.device, sem, nil)
300316 return 0
301317 }
@@ -287,15 +287,31 @@ func (r *Renderer) readFence(im *imported) vk.Semaphore {
287 if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {287 if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {
288 return 0288 return 0
289 }289 }
290+ return r.importSyncFD(int(arg.fd))
291+}
292+
293+// importFence makes a wait semaphore of a sync file. The file stays the
294+// caller's: Vulkan takes a duplicate, made while the file is held open.
295+func (r *Renderer) importFence(f *os.File) vk.Semaphore {
296+ fd, err := dupFile(f)
297+ if err != nil {
298+ return 0
299+ }
300+ return r.importSyncFD(fd)
301+}
302+
303+// importSyncFD imports a sync file fd as a temporary semaphore payload;
304+// the fd is Vulkan's on success and closed on failure.
305+func (r *Renderer) importSyncFD(fd int) vk.Semaphore {
290 var sem vk.Semaphore306 var sem vk.Semaphore
291 si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}307 si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
292 if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {308 if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {
293- unix.Close(int(arg.fd))309+ unix.Close(fd)
294 return 0310 return 0
295 }311 }
296- imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: arg.fd}312+ imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: int32(fd)}
297 if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {313 if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {
298- unix.Close(int(arg.fd))314+ unix.Close(fd)
299 r.dd.DestroySemaphore(r.device, sem, nil)315 r.dd.DestroySemaphore(r.device, sem, nil)
300 return 0316 return 0
301 }317 }
M internal/adapters/vulkan/frame.go +13 -2
@@ -205,11 +205,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
205205 // Client buffers come from the foreign queue family (their driver)
206206 // and go back to it after the frame, so the next frame acquires them.
207207 var dmas []*imported
208+ acquires := map[*imported]*os.File{}
208209 for _, dr := range ds {
209210 if dr.im != nil && !slices.Contains(dmas, dr.im) {
210211 dmas = append(dmas, dr.im)
211212 dr.im.last = r.frame
212213 }
214+ if dr.acquire != nil {
215+ acquires[dr.im] = dr.acquire
216+ }
213217 }
214218 r.ownership(cmd, dmas, true)
215219 r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial)
@@ -228,10 +232,17 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
228232 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
229233 return nil, err
230234 }
231- // Implicit sync: wait for the client's GPU writes to each buffer.
235+ // Wait for the client's GPU writes to each buffer: its explicit
236+ // acquire fence, else the buffer's implicit fences.
232237 var waits []vk.Semaphore
233238 for _, im := range dmas {
234- if sem := r.readFence(im); sem != 0 {
239+ sem := vk.Semaphore(0)
240+ if f := acquires[im]; f != nil {
241+ sem = r.importFence(f)
242+ } else {
243+ sem = r.readFence(im)
244+ }
245+ if sem != 0 {
235246 waits = append(waits, sem)
236247 }
237248 }
@@ -205,11 +205,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
205 // Client buffers come from the foreign queue family (their driver)205 // Client buffers come from the foreign queue family (their driver)
206 // and go back to it after the frame, so the next frame acquires them.206 // and go back to it after the frame, so the next frame acquires them.
207 var dmas []*imported207 var dmas []*imported
208+ acquires := map[*imported]*os.File{}
208 for _, dr := range ds {209 for _, dr := range ds {
209 if dr.im != nil && !slices.Contains(dmas, dr.im) {210 if dr.im != nil && !slices.Contains(dmas, dr.im) {
210 dmas = append(dmas, dr.im)211 dmas = append(dmas, dr.im)
211 dr.im.last = r.frame212 dr.im.last = r.frame
212 }213 }
214+ if dr.acquire != nil {
215+ acquires[dr.im] = dr.acquire
216+ }
213 }217 }
214 r.ownership(cmd, dmas, true)218 r.ownership(cmd, dmas, true)
215 r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial)219 r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial)
@@ -228,10 +232,17 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
228 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {232 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
229 return nil, err233 return nil, err
230 }234 }
231- // Implicit sync: wait for the client's GPU writes to each buffer.235+ // Wait for the client's GPU writes to each buffer: its explicit
236+ // acquire fence, else the buffer's implicit fences.
232 var waits []vk.Semaphore237 var waits []vk.Semaphore
233 for _, im := range dmas {238 for _, im := range dmas {
234- if sem := r.readFence(im); sem != 0 {239+ sem := vk.Semaphore(0)
240+ if f := acquires[im]; f != nil {
241+ sem = r.importFence(f)
242+ } else {
243+ sem = r.readFence(im)
244+ }
245+ if sem != 0 {
235 waits = append(waits, sem)246 waits = append(waits, sem)
236 }247 }
237 }248 }
M internal/adapters/vulkan/renderer.go +3 -0
@@ -333,6 +333,9 @@ 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+ }
336339 draws = append(draws, dr)
337340 return
338341 }
@@ -333,6 +333,9 @@ 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 {
337+ dr.acquire = content.Acquire
338+ }
336 draws = append(draws, dr)339 draws = append(draws, dr)
337 return340 return
338 }341 }
M internal/adapters/wayland/fifo.go +12 -4
@@ -218,6 +218,7 @@ type update struct {
218218 wait bool
219219 at time.Time
220220 feedback []*presentationtime.WpPresentationFeedback
221+ sync *commitSync
221222 damage []ports.Rect
222223 bufDamage []ports.Rect
223224 }
@@ -230,9 +231,9 @@ type childMove struct {
230231 // takePending moves the pending state into an update: one-shot state is
231232 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
232233 func (s *surface) takePending() update {
233- u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback}
234+ u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync}
234235 s.attached, s.pending, s.callbacks = false, nil, nil
235- s.pendingFeedback = nil
236+ s.pendingFeedback, s.pendingSync = nil, nil
236237 s.pendingDamage, s.pendingBufDamage = nil, nil
237238 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
238239 if v := s.viewport; v != nil {
@@ -263,7 +264,7 @@ func (s *surface) putPending(u update) {
263264 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
264265 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
265266 s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage
266- s.pendingFeedback = u.feedback
267+ s.pendingFeedback, s.pendingSync = u.feedback, u.sync
267268 if u.vp != nil && s.viewport == u.vp {
268269 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
269270 }
@@ -285,7 +286,7 @@ func (s *surface) putPending(u update) {
285286
286287 // mustWait reports whether the pending commit cannot apply now.
287288 func (s *surface) mustWait(now time.Time) bool {
288- return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now)
289+ return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) || !s.syncReady(s.pendingSync)
289290 }
290291
291292 // tooEarly reports whether content applied now would show before at:
@@ -347,6 +348,10 @@ func (s *surface) dropQueue() {
347348 for _, fb := range u.feedback {
348349 discard(fb)
349350 }
351+ s.dropSync(u.sync)
352+ if u.sync != nil {
353+ continue // explicit sync: the release point replaced release
354+ }
350355 if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
351356 released[u.buffer.Resource] = true
352357 u.buffer.SendRelease()
@@ -413,6 +418,9 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
413418 if u.wait && surf.barrier {
414419 break
415420 }
421+ if !surf.syncReady(u.sync) {
422+ break
423+ }
416424 if surf.tooEarly(u.at, now) {
417425 wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond)
418426 break
@@ -218,6 +218,7 @@ type update struct {
218 wait bool218 wait bool
219 at time.Time219 at time.Time
220 feedback []*presentationtime.WpPresentationFeedback220 feedback []*presentationtime.WpPresentationFeedback
221+ sync *commitSync
221 damage []ports.Rect222 damage []ports.Rect
222 bufDamage []ports.Rect223 bufDamage []ports.Rect
223 }224 }
@@ -230,9 +231,9 @@ type childMove struct {
230 // takePending moves the pending state into an update: one-shot state is231 // takePending moves the pending state into an update: one-shot state is
231 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.232 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
232 func (s *surface) takePending() update {233 func (s *surface) takePending() update {
233- u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback}234+ u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync}
234 s.attached, s.pending, s.callbacks = false, nil, nil235 s.attached, s.pending, s.callbacks = false, nil, nil
235- s.pendingFeedback = nil236+ s.pendingFeedback, s.pendingSync = nil, nil
236 s.pendingDamage, s.pendingBufDamage = nil, nil237 s.pendingDamage, s.pendingBufDamage = nil, nil
237 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}238 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
238 if v := s.viewport; v != nil {239 if v := s.viewport; v != nil {
@@ -263,7 +264,7 @@ func (s *surface) putPending(u update) {
263 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind264 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
264 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at265 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
265 s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage266 s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage
266- s.pendingFeedback = u.feedback267+ s.pendingFeedback, s.pendingSync = u.feedback, u.sync
267 if u.vp != nil && s.viewport == u.vp {268 if u.vp != nil && s.viewport == u.vp {
268 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet269 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
269 }270 }
@@ -285,7 +286,7 @@ func (s *surface) putPending(u update) {
285 286
286 // mustWait reports whether the pending commit cannot apply now.287 // mustWait reports whether the pending commit cannot apply now.
287 func (s *surface) mustWait(now time.Time) bool {288 func (s *surface) mustWait(now time.Time) bool {
288- return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now)289+ return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) || !s.syncReady(s.pendingSync)
289 }290 }
290 291
291 // tooEarly reports whether content applied now would show before at:292 // tooEarly reports whether content applied now would show before at:
@@ -347,6 +348,10 @@ func (s *surface) dropQueue() {
347 for _, fb := range u.feedback {348 for _, fb := range u.feedback {
348 discard(fb)349 discard(fb)
349 }350 }
351+ s.dropSync(u.sync)
352+ if u.sync != nil {
353+ continue // explicit sync: the release point replaced release
354+ }
350 if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {355 if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
351 released[u.buffer.Resource] = true356 released[u.buffer.Resource] = true
352 u.buffer.SendRelease()357 u.buffer.SendRelease()
@@ -413,6 +418,9 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration
413 if u.wait && surf.barrier {418 if u.wait && surf.barrier {
414 break419 break
415 }420 }
421+ if !surf.syncReady(u.sync) {
422+ break
423+ }
416 if surf.tooEarly(u.at, now) {424 if surf.tooEarly(u.at, now) {
417 wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond)425 wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond)
418 break426 break
M internal/adapters/wayland/globals.go +1 -0
@@ -20,6 +20,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
2020 func() error { return registerTearing(d, s) },
2121 func() error { return registerPresentationConstraints(d, s) },
2222 func() error { return registerPresentation(d, s) },
23+ func() error { return registerSyncobj(d, s) },
2324 func() error { return registerActivation(d, s) },
2425 func() error { return registerVirtualKeyboard(d, s) },
2526 func() error {
@@ -20,6 +20,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
20 func() error { return registerTearing(d, s) },20 func() error { return registerTearing(d, s) },
21 func() error { return registerPresentationConstraints(d, s) },21 func() error { return registerPresentationConstraints(d, s) },
22 func() error { return registerPresentation(d, s) },22 func() error { return registerPresentation(d, s) },
23+ func() error { return registerSyncobj(d, s) },
23 func() error { return registerActivation(d, s) },24 func() error { return registerActivation(d, s) },
24 func() error { return registerVirtualKeyboard(d, s) },25 func() error { return registerVirtualKeyboard(d, s) },
25 func() error {26 func() error {
M internal/adapters/wayland/release.go +39 -11
@@ -21,7 +21,10 @@ import (
2121 const heldTimeout = 100 * time.Millisecond
2222
2323 type heldBuffer struct {
24- res *wayland.Buffer
24+ res *wayland.Buffer
25+ // sync is the buffer's explicit-sync state: its release point is
26+ // signalled instead of wl_buffer.release.
27+ sync syncHold
2528 window ports.WindowID
2629 id uint64 // DMABuf ID, 0 for wl_shm
2730 after uint64 // the window's content Seq when the buffer was replaced
@@ -30,17 +33,14 @@ type heldBuffer struct {
3033
3134 // releaseBuffer releases a replaced buffer, or holds it until the output
3235 // showing its window no longer reads it.
33-func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
34- if !b.Resource.Alive() {
35- return
36- }
36+func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) {
3737 window, name := surf.root().windowID(), s.frameOutput(surf)
38- if window == 0 || name == "" {
38+ if !b.Resource.Alive() || window == 0 || name == "" {
3939 // Not drawn (unmapped, hidden, a cursor): nothing reads it.
40- b.SendRelease()
40+ s.release(b, sync)
4141 return
4242 }
43- h := heldBuffer{res: b, window: window, at: time.Now()}
43+ h := heldBuffer{res: b, sync: sync, window: window, at: time.Now()}
4444 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {
4545 h.id = d.buf.ID
4646 }
@@ -55,6 +55,36 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
5555 }
5656 }
5757
58+// release gives a buffer back: its release point is signalled when it
59+// has one (explicit sync), else wl_buffer.release.
60+func (s *Server) release(b *wayland.Buffer, sync syncHold) {
61+ if sync.release.set() || sync.acquire != nil {
62+ s.releaseSync(sync)
63+ return
64+ }
65+ if b != nil && b.Resource.Alive() {
66+ b.SendRelease()
67+ }
68+}
69+
70+// releaseBufferSync holds only a buffer's explicit-sync points: the same
71+// buffer committed again with new points (clients reuse one buffer).
72+func (s *Server) releaseBufferSync(surf *surface, sync syncHold) {
73+ if !sync.release.set() && sync.acquire == nil {
74+ return
75+ }
76+ window := surf.root().windowID()
77+ if window == 0 || s.frameOutput(surf) == "" {
78+ s.releaseSync(sync)
79+ return
80+ }
81+ s.contentMu.Lock()
82+ after := s.contentSeq[window]
83+ s.contentMu.Unlock()
84+ s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()})
85+ s.wakePacer()
86+}
87+
5888 // releaseHeld records the output reports and releases the buffers no
5989 // output reads. Every output reads every window's content, so a buffer
6090 // waits until no live output shows or queues it and all of them have seen
@@ -79,9 +109,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
79109 kept = append(kept, h)
80110 continue
81111 }
82- if h.res.Resource.Alive() {
83- h.res.SendRelease()
84- }
112+ s.release(h.res, h.sync)
85113 }
86114 clear(s.held[len(kept):])
87115 s.held = kept
@@ -21,7 +21,10 @@ import (
21 const heldTimeout = 100 * time.Millisecond21 const heldTimeout = 100 * time.Millisecond
22 22
23 type heldBuffer struct {23 type heldBuffer struct {
24- res *wayland.Buffer24+ res *wayland.Buffer
25+ // sync is the buffer's explicit-sync state: its release point is
26+ // signalled instead of wl_buffer.release.
27+ sync syncHold
25 window ports.WindowID28 window ports.WindowID
26 id uint64 // DMABuf ID, 0 for wl_shm29 id uint64 // DMABuf ID, 0 for wl_shm
27 after uint64 // the window's content Seq when the buffer was replaced30 after uint64 // the window's content Seq when the buffer was replaced
@@ -30,17 +33,14 @@ type heldBuffer struct {
30 33
31 // releaseBuffer releases a replaced buffer, or holds it until the output34 // releaseBuffer releases a replaced buffer, or holds it until the output
32 // showing its window no longer reads it.35 // showing its window no longer reads it.
33-func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {36+func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) {
34- if !b.Resource.Alive() {
35- return
36- }
37 window, name := surf.root().windowID(), s.frameOutput(surf)37 window, name := surf.root().windowID(), s.frameOutput(surf)
38- if window == 0 || name == "" {38+ if !b.Resource.Alive() || window == 0 || name == "" {
39 // Not drawn (unmapped, hidden, a cursor): nothing reads it.39 // Not drawn (unmapped, hidden, a cursor): nothing reads it.
40- b.SendRelease()40+ s.release(b, sync)
41 return41 return
42 }42 }
43- h := heldBuffer{res: b, window: window, at: time.Now()}43+ h := heldBuffer{res: b, sync: sync, window: window, at: time.Now()}
44 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {44 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {
45 h.id = d.buf.ID45 h.id = d.buf.ID
46 }46 }
@@ -55,6 +55,36 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
55 }55 }
56 }56 }
57 57
58+// release gives a buffer back: its release point is signalled when it
59+// has one (explicit sync), else wl_buffer.release.
60+func (s *Server) release(b *wayland.Buffer, sync syncHold) {
61+ if sync.release.set() || sync.acquire != nil {
62+ s.releaseSync(sync)
63+ return
64+ }
65+ if b != nil && b.Resource.Alive() {
66+ b.SendRelease()
67+ }
68+}
69+
70+// releaseBufferSync holds only a buffer's explicit-sync points: the same
71+// buffer committed again with new points (clients reuse one buffer).
72+func (s *Server) releaseBufferSync(surf *surface, sync syncHold) {
73+ if !sync.release.set() && sync.acquire == nil {
74+ return
75+ }
76+ window := surf.root().windowID()
77+ if window == 0 || s.frameOutput(surf) == "" {
78+ s.releaseSync(sync)
79+ return
80+ }
81+ s.contentMu.Lock()
82+ after := s.contentSeq[window]
83+ s.contentMu.Unlock()
84+ s.held = append(s.held, heldBuffer{sync: sync, window: window, after: after, at: time.Now()})
85+ s.wakePacer()
86+}
87+
58 // releaseHeld records the output reports and releases the buffers no88 // releaseHeld records the output reports and releases the buffers no
59 // output reads. Every output reads every window's content, so a buffer89 // output reads. Every output reads every window's content, so a buffer
60 // waits until no live output shows or queues it and all of them have seen90 // waits until no live output shows or queues it and all of them have seen
@@ -79,9 +109,7 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
79 kept = append(kept, h)109 kept = append(kept, h)
80 continue110 continue
81 }111 }
82- if h.res.Resource.Alive() {112+ s.release(h.res, h.sync)
83- h.res.SendRelease()
84- }
85 }113 }
86 clear(s.held[len(kept):])114 clear(s.held[len(kept):])
87 s.held = kept115 s.held = kept
M internal/adapters/wayland/server.go +42 -1
@@ -24,9 +24,15 @@ type Options struct {
2424 RuntimeDir string
2525 Outputs ports.Layout
2626 // DMABuf is what the renderer imports; empty disables linux-dmabuf.
27- DMABuf ports.DMABufSupport
27+ DMABuf ports.DMABufSupport
28+ // SyncobjNode is the render node (/dev/dri/renderD*) timeline
29+ // syncobjs are imported on; empty or without timeline support, no
30+ // explicit sync is offered.
31+ SyncobjNode string
2832 Keymap string
2933 RepeatRate, RepeatDelay int
34+ // syncDev replaces the render node's syncobj interface (tests).
35+ syncDev syncobjDevice
3036 }
3137
3238 // Channels carries client notifications and commands. Events may be unbuffered.
@@ -112,6 +118,11 @@ type Server struct {
112118 damage map[ports.WindowID][]ports.SeqDamage
113119 // feedbacks wait for the flip that shows their content (presentation.go).
114120 feedbacks []feedbackWait
121+ // Explicit sync (syncobj.go): the render node, imported timelines by
122+ // resource, and the acquire point waiter.
123+ syncDev syncobjDevice
124+ timelines map[*server.Resource]*timeline
125+ syncWait *syncWaiter
115126 // flips is each output's latest flip (pacer, under Do).
116127 flips map[string]ports.FlipInfo
117128 contentNotify chan struct{}
@@ -173,6 +184,26 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
173184 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
174185 s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
175186 s.flips = map[string]ports.FlipInfo{}
187+ s.timelines = map[*server.Resource]*timeline{}
188+ var node *renderNode
189+ if opts.syncDev != nil {
190+ s.syncDev = opts.syncDev
191+ if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
192+ cleanup()
193+ d.Close()
194+ return nil, err
195+ }
196+ } else if opts.SyncobjNode != "" {
197+ if node, err = openSyncobj(opts.SyncobjNode); err != nil {
198+ log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off")
199+ node = nil
200+ } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
201+ node.close()
202+ node = nil
203+ } else {
204+ s.syncDev = node
205+ }
206+ }
176207 s.tokens = map[string]activationToken{}
177208 if opts.Keymap != "" {
178209 fd, size, e := keymapFile(opts.Keymap)
@@ -188,6 +219,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
188219 unix.Close(s.keymapFD)
189220 }
190221 s.dmabuf.close()
222+ if s.syncWait != nil {
223+ s.syncWait.close()
224+ }
225+ if node != nil {
226+ node.close()
227+ }
191228 cleanup()
192229 }
193230 if err = registerGlobals(d, opts, s); err != nil {
@@ -245,6 +282,10 @@ func (s *Server) Run(ctx context.Context) error {
245282 }()
246283 go func() { defer wg.Done(); s.pace(ctx) }()
247284 go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
285+ if s.syncWait != nil {
286+ wg.Add(1)
287+ go func() { defer wg.Done(); s.syncWait.run(ctx) }()
288+ }
248289 err := s.display.Run(ctx)
249290 wg.Wait()
250291 return err
@@ -24,9 +24,15 @@ type Options struct {
24 RuntimeDir string24 RuntimeDir string
25 Outputs ports.Layout25 Outputs ports.Layout
26 // DMABuf is what the renderer imports; empty disables linux-dmabuf.26 // DMABuf is what the renderer imports; empty disables linux-dmabuf.
27- DMABuf ports.DMABufSupport27+ DMABuf ports.DMABufSupport
28+ // SyncobjNode is the render node (/dev/dri/renderD*) timeline
29+ // syncobjs are imported on; empty or without timeline support, no
30+ // explicit sync is offered.
31+ SyncobjNode string
28 Keymap string32 Keymap string
29 RepeatRate, RepeatDelay int33 RepeatRate, RepeatDelay int
34+ // syncDev replaces the render node's syncobj interface (tests).
35+ syncDev syncobjDevice
30 }36 }
31 37
32 // Channels carries client notifications and commands. Events may be unbuffered.38 // Channels carries client notifications and commands. Events may be unbuffered.
@@ -112,6 +118,11 @@ type Server struct {
112 damage map[ports.WindowID][]ports.SeqDamage118 damage map[ports.WindowID][]ports.SeqDamage
113 // feedbacks wait for the flip that shows their content (presentation.go).119 // feedbacks wait for the flip that shows their content (presentation.go).
114 feedbacks []feedbackWait120 feedbacks []feedbackWait
121+ // Explicit sync (syncobj.go): the render node, imported timelines by
122+ // resource, and the acquire point waiter.
123+ syncDev syncobjDevice
124+ timelines map[*server.Resource]*timeline
125+ syncWait *syncWaiter
115 // flips is each output's latest flip (pacer, under Do).126 // flips is each output's latest flip (pacer, under Do).
116 flips map[string]ports.FlipInfo127 flips map[string]ports.FlipInfo
117 contentNotify chan struct{}128 contentNotify chan struct{}
@@ -173,6 +184,26 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
173 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}184 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
174 s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}185 s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
175 s.flips = map[string]ports.FlipInfo{}186 s.flips = map[string]ports.FlipInfo{}
187+ s.timelines = map[*server.Resource]*timeline{}
188+ var node *renderNode
189+ if opts.syncDev != nil {
190+ s.syncDev = opts.syncDev
191+ if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
192+ cleanup()
193+ d.Close()
194+ return nil, err
195+ }
196+ } else if opts.SyncobjNode != "" {
197+ if node, err = openSyncobj(opts.SyncobjNode); err != nil {
198+ log.Info().Str("component", "wayland").Err(err).Msg("explicit sync off")
199+ node = nil
200+ } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil {
201+ node.close()
202+ node = nil
203+ } else {
204+ s.syncDev = node
205+ }
206+ }
176 s.tokens = map[string]activationToken{}207 s.tokens = map[string]activationToken{}
177 if opts.Keymap != "" {208 if opts.Keymap != "" {
178 fd, size, e := keymapFile(opts.Keymap)209 fd, size, e := keymapFile(opts.Keymap)
@@ -188,6 +219,12 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
188 unix.Close(s.keymapFD)219 unix.Close(s.keymapFD)
189 }220 }
190 s.dmabuf.close()221 s.dmabuf.close()
222+ if s.syncWait != nil {
223+ s.syncWait.close()
224+ }
225+ if node != nil {
226+ node.close()
227+ }
191 cleanup()228 cleanup()
192 }229 }
193 if err = registerGlobals(d, opts, s); err != nil {230 if err = registerGlobals(d, opts, s); err != nil {
@@ -245,6 +282,10 @@ func (s *Server) Run(ctx context.Context) error {
245 }()282 }()
246 go func() { defer wg.Done(); s.pace(ctx) }()283 go func() { defer wg.Done(); s.pace(ctx) }()
247 go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()284 go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
285+ if s.syncWait != nil {
286+ wg.Add(1)
287+ go func() { defer wg.Done(); s.syncWait.run(ctx) }()
288+ }
248 err := s.display.Run(ctx)289 err := s.display.Run(ctx)
249 wg.Wait()290 wg.Wait()
250 return err291 return err
M internal/adapters/wayland/surface.go +32 -3
@@ -69,6 +69,18 @@ type surface struct {
6969 committed damage
7070 // pendingFeedback are wp_presentation feedbacks for the next commit.
7171 pendingFeedback []*presentationtime.WpPresentationFeedback
72+ // Explicit sync (syncobj.go): the surface's syncobj object; the
73+ // committed points of the pending commit (pendingSync, with the wait
74+ // for its acquire point); and the current buffer's hold.
75+ sync *syncState
76+ pendingSync *commitSync
77+ hold syncHold
78+}
79+
80+// commitSync is a commit's explicit-sync points and acquire wait.
81+type commitSync struct {
82+ acquire, release syncPoint
83+ wait *syncWait
7284 }
7385
7486 // damage is what one commit changed in a surface's buffer.
@@ -198,8 +210,11 @@ func (s *surface) Destroy(*wayland.Surface) {
198210 }
199211 s.callbacks = nil
200212 // The client may reuse the buffer on another surface.
213+ s.dropSync(s.pendingSync)
214+ s.pendingSync = nil
201215 if s.current != nil {
202- s.server.releaseBuffer(s, s.current)
216+ s.server.releaseBuffer(s, s.current, s.hold)
217+ s.hold = syncHold{}
203218 }
204219 s.current, s.pending = nil, nil
205220 if s.role != nil {
@@ -228,7 +243,12 @@ func (s *surface) Commit(*wayland.Surface) {
228243 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
229244 return
230245 }
231- // A commit behind a fifo barrier or a future timestamp waits (fifo.go).
246+ if !s.checkSyncCommit() {
247+ return
248+ }
249+ s.takeSyncPoints()
250+ // A commit behind a fifo barrier, a future timestamp or an acquire
251+ // point without a fence yet waits (fifo.go).
232252 if s.mustWait(time.Now()) {
233253 s.queueUpdate()
234254 return
@@ -256,14 +276,22 @@ func (s *surface) applyCommit() {
256276 if s.viewport != nil {
257277 s.viewport.commit()
258278 }
279+ cs := s.pendingSync
280+ s.pendingSync = nil
259281 if s.attached {
260282 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
261- s.server.releaseBuffer(s, s.current)
283+ s.server.releaseBuffer(s, s.current, s.hold)
284+ } else if cs != nil || s.pending == nil {
285+ // Same buffer again, or detached: the old points are done
286+ // with once the new ones take over.
287+ s.server.releaseBufferSync(s, s.hold)
262288 }
289+ s.hold = syncHold{}
263290 s.current = s.pending
264291 s.pending = nil
265292 s.attached = false
266293 }
294+ s.applySync(cs)
267295 if len(s.callbacks) > 0 {
268296 s.server.queueFrames(s.server.frameOutput(s), s.callbacks)
269297 }
@@ -280,6 +308,7 @@ func (s *surface) applyCommit() {
280308 s.lastW, s.lastH = c.Width, c.Height
281309 s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
282310 }
311+ c.Acquire = s.hold.acquire
283312 s.content, s.has = c, true
284313 } else if shm, ok := state.(*buffer); ok {
285314 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
@@ -69,6 +69,18 @@ type surface struct {
69 committed damage69 committed damage
70 // pendingFeedback are wp_presentation feedbacks for the next commit.70 // pendingFeedback are wp_presentation feedbacks for the next commit.
71 pendingFeedback []*presentationtime.WpPresentationFeedback71 pendingFeedback []*presentationtime.WpPresentationFeedback
72+ // Explicit sync (syncobj.go): the surface's syncobj object; the
73+ // committed points of the pending commit (pendingSync, with the wait
74+ // for its acquire point); and the current buffer's hold.
75+ sync *syncState
76+ pendingSync *commitSync
77+ hold syncHold
78+}
79+
80+// commitSync is a commit's explicit-sync points and acquire wait.
81+type commitSync struct {
82+ acquire, release syncPoint
83+ wait *syncWait
72 }84 }
73 85
74 // damage is what one commit changed in a surface's buffer.86 // damage is what one commit changed in a surface's buffer.
@@ -198,8 +210,11 @@ func (s *surface) Destroy(*wayland.Surface) {
198 }210 }
199 s.callbacks = nil211 s.callbacks = nil
200 // The client may reuse the buffer on another surface.212 // The client may reuse the buffer on another surface.
213+ s.dropSync(s.pendingSync)
214+ s.pendingSync = nil
201 if s.current != nil {215 if s.current != nil {
202- s.server.releaseBuffer(s, s.current)216+ s.server.releaseBuffer(s, s.current, s.hold)
217+ s.hold = syncHold{}
203 }218 }
204 s.current, s.pending = nil, nil219 s.current, s.pending = nil, nil
205 if s.role != nil {220 if s.role != nil {
@@ -228,7 +243,12 @@ func (s *surface) Commit(*wayland.Surface) {
228 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")243 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
229 return244 return
230 }245 }
231- // A commit behind a fifo barrier or a future timestamp waits (fifo.go).246+ if !s.checkSyncCommit() {
247+ return
248+ }
249+ s.takeSyncPoints()
250+ // A commit behind a fifo barrier, a future timestamp or an acquire
251+ // point without a fence yet waits (fifo.go).
232 if s.mustWait(time.Now()) {252 if s.mustWait(time.Now()) {
233 s.queueUpdate()253 s.queueUpdate()
234 return254 return
@@ -256,14 +276,22 @@ func (s *surface) applyCommit() {
256 if s.viewport != nil {276 if s.viewport != nil {
257 s.viewport.commit()277 s.viewport.commit()
258 }278 }
279+ cs := s.pendingSync
280+ s.pendingSync = nil
259 if s.attached {281 if s.attached {
260 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {282 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
261- s.server.releaseBuffer(s, s.current)283+ s.server.releaseBuffer(s, s.current, s.hold)
284+ } else if cs != nil || s.pending == nil {
285+ // Same buffer again, or detached: the old points are done
286+ // with once the new ones take over.
287+ s.server.releaseBufferSync(s, s.hold)
262 }288 }
289+ s.hold = syncHold{}
263 s.current = s.pending290 s.current = s.pending
264 s.pending = nil291 s.pending = nil
265 s.attached = false292 s.attached = false
266 }293 }
294+ s.applySync(cs)
267 if len(s.callbacks) > 0 {295 if len(s.callbacks) > 0 {
268 s.server.queueFrames(s.server.frameOutput(s), s.callbacks)296 s.server.queueFrames(s.server.frameOutput(s), s.callbacks)
269 }297 }
@@ -280,6 +308,7 @@ func (s *surface) applyCommit() {
280 s.lastW, s.lastH = c.Width, c.Height308 s.lastW, s.lastH = c.Width, c.Height
281 s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")309 s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
282 }310 }
311+ c.Acquire = s.hold.acquire
283 s.content, s.has = c, true312 s.content, s.has = c, true
284 } else if shm, ok := state.(*buffer); ok {313 } else if shm, ok := state.(*buffer); ok {
285 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")314 shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
A internal/adapters/wayland/syncdev.go +128 -0
@@ -0,0 +1,128 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "os"
6+ "runtime"
7+ "unsafe"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+// syncobjDevice is the DRM syncobj interface of a render node: explicit
13+// sync timelines clients import (wp_linux_drm_syncobj_v1).
14+type syncobjDevice interface {
15+ // fdToHandle imports a timeline syncobj fd.
16+ fdToHandle(fd int) (uint32, error)
17+ // exportSyncFile returns a sync file of a timeline point's fence.
18+ exportSyncFile(h uint32, point uint64) (*os.File, error)
19+ // signal signals a timeline point (a release point).
20+ signal(h uint32, point uint64) error
21+ // eventfd writes efd once the point has a fence (WAIT_AVAILABLE).
22+ eventfd(h uint32, point uint64, efd int) error
23+ destroy(h uint32) error
24+}
25+
26+const (
27+ ioctlSyncobjDestroy = 0xC00864C0 // DRM_IOWR(0xC0, struct drm_syncobj_destroy)
28+ ioctlSyncobjHandleToFD = 0xC01864C1 // DRM_IOWR(0xC1, struct drm_syncobj_handle)
29+ ioctlSyncobjFDToHandle = 0xC01864C2 // DRM_IOWR(0xC2, struct drm_syncobj_handle)
30+ ioctlSyncobjTimelineSignal = 0xC01864CD // DRM_IOWR(0xCD, struct drm_syncobj_timeline_array)
31+ ioctlSyncobjEventfd = 0xC01864CF // DRM_IOWR(0xCF, struct drm_syncobj_eventfd)
32+ ioctlGetCap = 0xC010640C // DRM_IOWR(0x0C, struct drm_get_cap)
33+
34+ syncobjExportSyncFile = 1 << 0
35+ syncobjTimeline = 1 << 1
36+ syncobjWaitAvailable = 1 << 2
37+ capSyncobjTimeline = 0x14
38+)
39+
40+type syncobjHandle struct {
41+ handle, flags uint32
42+ fd int32
43+ pad uint32
44+ point uint64
45+}
46+
47+type syncobjDestroy struct{ handle, pad uint32 }
48+
49+type syncobjTimelineArray struct {
50+ handles, points uint64
51+ count, flags uint32
52+}
53+
54+type syncobjEventfd struct {
55+ handle, flags uint32
56+ point uint64
57+ fd int32
58+ pad uint32
59+}
60+
61+type drmCap struct{ capability, value uint64 }
62+
63+// renderNode is a DRM render node opened for syncobj ioctls.
64+type renderNode struct{ fd int }
65+
66+// openSyncobj opens a render node that supports timeline syncobjs.
67+func openSyncobj(path string) (*renderNode, error) {
68+ fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
69+ if err != nil {
70+ return nil, err
71+ }
72+ c := drmCap{capability: capSyncobjTimeline}
73+ if ioctl(fd, ioctlGetCap, unsafe.Pointer(&c)) != nil || c.value == 0 {
74+ unix.Close(fd)
75+ return nil, fmt.Errorf("%s: no timeline syncobjs", path)
76+ }
77+ return &renderNode{fd: fd}, nil
78+}
79+
80+func (n *renderNode) close() { unix.Close(n.fd) }
81+
82+func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
83+ for {
84+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
85+ if errno == unix.EINTR || errno == unix.EAGAIN {
86+ continue
87+ }
88+ if errno != 0 {
89+ return errno
90+ }
91+ return nil
92+ }
93+}
94+
95+func (n *renderNode) fdToHandle(fd int) (uint32, error) {
96+ a := syncobjHandle{fd: int32(fd)}
97+ if err := ioctl(n.fd, ioctlSyncobjFDToHandle, unsafe.Pointer(&a)); err != nil {
98+ return 0, err
99+ }
100+ return a.handle, nil
101+}
102+
103+func (n *renderNode) exportSyncFile(h uint32, point uint64) (*os.File, error) {
104+ a := syncobjHandle{handle: h, flags: syncobjExportSyncFile | syncobjTimeline, fd: -1, point: point}
105+ if err := ioctl(n.fd, ioctlSyncobjHandleToFD, unsafe.Pointer(&a)); err != nil {
106+ return nil, err
107+ }
108+ return os.NewFile(uintptr(a.fd), "acquire-fence"), nil
109+}
110+
111+func (n *renderNode) signal(h uint32, point uint64) error {
112+ handles, points := [1]uint32{h}, [1]uint64{point}
113+ a := syncobjTimelineArray{handles: uint64(uintptr(unsafe.Pointer(&handles))), points: uint64(uintptr(unsafe.Pointer(&points))), count: 1}
114+ err := ioctl(n.fd, ioctlSyncobjTimelineSignal, unsafe.Pointer(&a))
115+ runtime.KeepAlive(&handles)
116+ runtime.KeepAlive(&points)
117+ return err
118+}
119+
120+func (n *renderNode) eventfd(h uint32, point uint64, efd int) error {
121+ a := syncobjEventfd{handle: h, flags: syncobjWaitAvailable, point: point, fd: int32(efd)}
122+ return ioctl(n.fd, ioctlSyncobjEventfd, unsafe.Pointer(&a))
123+}
124+
125+func (n *renderNode) destroy(h uint32) error {
126+ a := syncobjDestroy{handle: h}
127+ return ioctl(n.fd, ioctlSyncobjDestroy, unsafe.Pointer(&a))
128+}
@@ -0,0 +1,128 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "os"
6+ "runtime"
7+ "unsafe"
8+
9+ "golang.org/x/sys/unix"
10+)
11+
12+// syncobjDevice is the DRM syncobj interface of a render node: explicit
13+// sync timelines clients import (wp_linux_drm_syncobj_v1).
14+type syncobjDevice interface {
15+ // fdToHandle imports a timeline syncobj fd.
16+ fdToHandle(fd int) (uint32, error)
17+ // exportSyncFile returns a sync file of a timeline point's fence.
18+ exportSyncFile(h uint32, point uint64) (*os.File, error)
19+ // signal signals a timeline point (a release point).
20+ signal(h uint32, point uint64) error
21+ // eventfd writes efd once the point has a fence (WAIT_AVAILABLE).
22+ eventfd(h uint32, point uint64, efd int) error
23+ destroy(h uint32) error
24+}
25+
26+const (
27+ ioctlSyncobjDestroy = 0xC00864C0 // DRM_IOWR(0xC0, struct drm_syncobj_destroy)
28+ ioctlSyncobjHandleToFD = 0xC01864C1 // DRM_IOWR(0xC1, struct drm_syncobj_handle)
29+ ioctlSyncobjFDToHandle = 0xC01864C2 // DRM_IOWR(0xC2, struct drm_syncobj_handle)
30+ ioctlSyncobjTimelineSignal = 0xC01864CD // DRM_IOWR(0xCD, struct drm_syncobj_timeline_array)
31+ ioctlSyncobjEventfd = 0xC01864CF // DRM_IOWR(0xCF, struct drm_syncobj_eventfd)
32+ ioctlGetCap = 0xC010640C // DRM_IOWR(0x0C, struct drm_get_cap)
33+
34+ syncobjExportSyncFile = 1 << 0
35+ syncobjTimeline = 1 << 1
36+ syncobjWaitAvailable = 1 << 2
37+ capSyncobjTimeline = 0x14
38+)
39+
40+type syncobjHandle struct {
41+ handle, flags uint32
42+ fd int32
43+ pad uint32
44+ point uint64
45+}
46+
47+type syncobjDestroy struct{ handle, pad uint32 }
48+
49+type syncobjTimelineArray struct {
50+ handles, points uint64
51+ count, flags uint32
52+}
53+
54+type syncobjEventfd struct {
55+ handle, flags uint32
56+ point uint64
57+ fd int32
58+ pad uint32
59+}
60+
61+type drmCap struct{ capability, value uint64 }
62+
63+// renderNode is a DRM render node opened for syncobj ioctls.
64+type renderNode struct{ fd int }
65+
66+// openSyncobj opens a render node that supports timeline syncobjs.
67+func openSyncobj(path string) (*renderNode, error) {
68+ fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
69+ if err != nil {
70+ return nil, err
71+ }
72+ c := drmCap{capability: capSyncobjTimeline}
73+ if ioctl(fd, ioctlGetCap, unsafe.Pointer(&c)) != nil || c.value == 0 {
74+ unix.Close(fd)
75+ return nil, fmt.Errorf("%s: no timeline syncobjs", path)
76+ }
77+ return &renderNode{fd: fd}, nil
78+}
79+
80+func (n *renderNode) close() { unix.Close(n.fd) }
81+
82+func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
83+ for {
84+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
85+ if errno == unix.EINTR || errno == unix.EAGAIN {
86+ continue
87+ }
88+ if errno != 0 {
89+ return errno
90+ }
91+ return nil
92+ }
93+}
94+
95+func (n *renderNode) fdToHandle(fd int) (uint32, error) {
96+ a := syncobjHandle{fd: int32(fd)}
97+ if err := ioctl(n.fd, ioctlSyncobjFDToHandle, unsafe.Pointer(&a)); err != nil {
98+ return 0, err
99+ }
100+ return a.handle, nil
101+}
102+
103+func (n *renderNode) exportSyncFile(h uint32, point uint64) (*os.File, error) {
104+ a := syncobjHandle{handle: h, flags: syncobjExportSyncFile | syncobjTimeline, fd: -1, point: point}
105+ if err := ioctl(n.fd, ioctlSyncobjHandleToFD, unsafe.Pointer(&a)); err != nil {
106+ return nil, err
107+ }
108+ return os.NewFile(uintptr(a.fd), "acquire-fence"), nil
109+}
110+
111+func (n *renderNode) signal(h uint32, point uint64) error {
112+ handles, points := [1]uint32{h}, [1]uint64{point}
113+ a := syncobjTimelineArray{handles: uint64(uintptr(unsafe.Pointer(&handles))), points: uint64(uintptr(unsafe.Pointer(&points))), count: 1}
114+ err := ioctl(n.fd, ioctlSyncobjTimelineSignal, unsafe.Pointer(&a))
115+ runtime.KeepAlive(&handles)
116+ runtime.KeepAlive(&points)
117+ return err
118+}
119+
120+func (n *renderNode) eventfd(h uint32, point uint64, efd int) error {
121+ a := syncobjEventfd{handle: h, flags: syncobjWaitAvailable, point: point, fd: int32(efd)}
122+ return ioctl(n.fd, ioctlSyncobjEventfd, unsafe.Pointer(&a))
123+}
124+
125+func (n *renderNode) destroy(h uint32) error {
126+ a := syncobjDestroy{handle: h}
127+ return ioctl(n.fd, ioctlSyncobjDestroy, unsafe.Pointer(&a))
128+}
A internal/adapters/wayland/syncobj.go +396 -0
@@ -0,0 +1,396 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "sync"
8+
9+ "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
10+ "github.com/bnema/purego-libwayland/protocol/wayland"
11+ "github.com/bnema/purego-libwayland/server"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// wp_linux_drm_syncobj_v1: explicit sync. A client names, per commit, the
16+// timeline point its GPU signals when the buffer is drawn (acquire) and
17+// the point we signal once nothing reads the buffer any more (release).
18+// A commit waits (like a fifo barrier) until its acquire point has a
19+// fence; the fence then travels with the content as a sync file the
20+// renderer and the display wait on, so no CPU waits for the GPU. The
21+// release point replaces wl_buffer.release.
22+
23+const syncobjVersion = 1
24+
25+// timeline is an imported client timeline.
26+type timeline struct {
27+ dev syncobjDevice
28+ handle uint32
29+ alive bool
30+ // uses counts committed points still to signal or wait on.
31+ uses int
32+}
33+
34+// syncPoint is a point on a timeline.
35+type syncPoint struct {
36+ tl *timeline
37+ point uint64
38+}
39+
40+func (p syncPoint) set() bool { return p.tl != nil }
41+
42+// syncState is a surface's wp_linux_drm_syncobj_surface_v1 state.
43+type syncState struct {
44+ resource *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1
45+ acquire, release syncPoint // pending, for the next commit
46+}
47+
48+func registerSyncobj(d *server.Display, s *Server) error {
49+ if s.syncDev == nil {
50+ return nil
51+ }
52+ return linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1Global(d, syncobjVersion, func(c server.Client, v, id uint32) {
53+ _, _ = linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1(c, int32(v), id, syncManager{s})
54+ })
55+}
56+
57+type syncManager struct{ server *Server }
58+
59+func (syncManager) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1) {}
60+
61+func (m syncManager) GetSurface(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, surf *wayland.Surface) {
62+ state := m.server.surfaceOf(surf)
63+ if state == nil {
64+ return
65+ }
66+ if state.sync != nil {
67+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists), "surface already has a syncobj surface")
68+ return
69+ }
70+ st := &syncState{}
71+ res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjSurfaceV1(r.Client(), r.Version(), id, syncSurface{state, st})
72+ if err != nil {
73+ return
74+ }
75+ st.resource = res
76+ state.sync = st
77+ res.OnDestroy = func() {
78+ if state.sync == st {
79+ state.sync = nil
80+ }
81+ }
82+}
83+
84+func (m syncManager) ImportTimeline(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, fd int) {
85+ defer unix.Close(fd)
86+ h, err := m.server.syncDev.fdToHandle(fd)
87+ if err != nil {
88+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline), "invalid timeline fd")
89+ return
90+ }
91+ tl := &timeline{dev: m.server.syncDev, handle: h, alive: true}
92+ res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjTimelineV1(r.Client(), r.Version(), id, timelineHandler{})
93+ if err != nil {
94+ _ = tl.dev.destroy(h)
95+ return
96+ }
97+ m.server.timelines[res.Resource] = tl
98+ res.OnDestroy = func() {
99+ delete(m.server.timelines, res.Resource)
100+ // Points already committed keep the syncobj: destroyed once no
101+ // held buffer or waiting commit uses it (timeline.drop).
102+ tl.alive = false
103+ m.server.dropTimeline(tl)
104+ }
105+}
106+
107+type timelineHandler struct{}
108+
109+func (timelineHandler) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1) {}
110+
111+type syncSurface struct {
112+ surf *surface
113+ st *syncState
114+}
115+
116+func (syncSurface) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1) {}
117+
118+func (h syncSurface) point(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) (syncPoint, bool) {
119+ if h.surf.destroyed {
120+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface), "surface destroyed")
121+ return syncPoint{}, false
122+ }
123+ t := h.surf.server.timelines[tl.Resource]
124+ if t == nil {
125+ return syncPoint{}, false
126+ }
127+ return syncPoint{tl: t, point: uint64(hi)<<32 | uint64(lo)}, true
128+}
129+
130+func (h syncSurface) SetAcquirePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) {
131+ if p, ok := h.point(r, tl, hi, lo); ok {
132+ h.st.acquire = p
133+ }
134+}
135+
136+func (h syncSurface) SetReleasePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) {
137+ if p, ok := h.point(r, tl, hi, lo); ok {
138+ h.st.release = p
139+ }
140+}
141+
142+// checkSyncCommit applies the protocol rules to a commit with sync
143+// points; false means a protocol error was posted.
144+func (s *surface) checkSyncCommit() bool {
145+ st := s.sync
146+ if st == nil {
147+ return true
148+ }
149+ a, r := st.acquire, st.release
150+ hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil
151+ res := st.resource
152+ switch {
153+ case !a.set() && !r.set():
154+ if hasBuffer && s.attached {
155+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point")
156+ return false
157+ }
158+ return true
159+ case !hasBuffer:
160+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer), "points without a buffer")
161+ case !a.set():
162+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "no acquire point")
163+ case !r.set():
164+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint), "no release point")
165+ case a.tl == r.tl && a.point >= r.point:
166+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints), "release point not after acquire point")
167+ case s.pendingIsSHM():
168+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer), "explicit sync on a wl_shm buffer")
169+ default:
170+ return true
171+ }
172+ return false
173+}
174+
175+// pendingIsSHM reports whether the buffer of the next commit is wl_shm.
176+func (s *surface) pendingIsSHM() bool {
177+ b := s.pending
178+ if !s.attached {
179+ b = s.current
180+ }
181+ if b == nil {
182+ return false
183+ }
184+ _, dma := s.server.buffers[b.Resource].(*dmabufBuffer)
185+ return !dma
186+}
187+
188+// syncWaiter watches acquire points: each gets an eventfd the kernel
189+// writes once the point has a fence, and the pacer is woken to apply the
190+// commits that waited for it.
191+type syncWaiter struct {
192+ mu sync.Mutex
193+ ready map[*syncWait]bool
194+ wake func()
195+ pipeR *os.File
196+ pipeW *os.File
197+ add chan *syncWait
198+}
199+
200+// syncWait is one acquire point a queued commit waits for.
201+type syncWait struct {
202+ efd int
203+ done bool // the eventfd fired (pacer, under Do)
204+}
205+
206+func newSyncWaiter(wake func()) (*syncWaiter, error) {
207+ r, w, err := os.Pipe()
208+ if err != nil {
209+ return nil, err
210+ }
211+ return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil
212+}
213+
214+// watch registers an eventfd for p; the wait is ready once it fires.
215+func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {
216+ efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
217+ if err != nil {
218+ return nil, err
219+ }
220+ if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil {
221+ unix.Close(efd)
222+ return nil, err
223+ }
224+ w := &syncWait{efd: efd}
225+ sw.add <- w
226+ _, _ = sw.pipeW.Write([]byte{0})
227+ return w, nil
228+}
229+
230+// fired reports whether w's point has a fence, and forgets w once it has.
231+func (sw *syncWaiter) fired(w *syncWait) bool {
232+ sw.mu.Lock()
233+ defer sw.mu.Unlock()
234+ if sw.ready[w] {
235+ delete(sw.ready, w)
236+ w.done = true
237+ }
238+ return w.done
239+}
240+
241+// cancel stops watching w (the commit was dropped).
242+func (sw *syncWaiter) cancel(w *syncWait) {
243+ sw.mu.Lock()
244+ delete(sw.ready, w)
245+ sw.mu.Unlock()
246+ w.done = true
247+}
248+
249+// run polls every watched eventfd; it owns and closes them.
250+func (sw *syncWaiter) run(ctx context.Context) {
251+ defer sw.pipeR.Close()
252+ var waits []*syncWait
253+ defer func() {
254+ for _, w := range waits {
255+ unix.Close(w.efd)
256+ }
257+ }()
258+ for ctx.Err() == nil {
259+ fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}}
260+ for _, w := range waits {
261+ fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN})
262+ }
263+ n, err := unix.Poll(fds, 100)
264+ if err != nil && !errors.Is(err, unix.EINTR) {
265+ return
266+ }
267+ if n <= 0 {
268+ continue
269+ }
270+ fired := false
271+ kept := waits[:0]
272+ for i, w := range waits {
273+ if fds[i+1].Revents == 0 {
274+ kept = append(kept, w)
275+ continue
276+ }
277+ unix.Close(w.efd)
278+ sw.mu.Lock()
279+ sw.ready[w] = true
280+ sw.mu.Unlock()
281+ fired = true
282+ }
283+ waits = kept
284+ // New waits join the next poll.
285+ if fds[0].Revents != 0 {
286+ var b [64]byte
287+ _, _ = 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+ }
297+ }
298+ if fired {
299+ sw.wake()
300+ }
301+ }
302+}
303+
304+// close stops the waiter's input side.
305+func (sw *syncWaiter) close() { sw.pipeW.Close() }
306+
307+// syncHold is a buffer's explicit-sync state while it is on screen: the
308+// acquire fence handed to readers and the release point to signal.
309+type syncHold struct {
310+ acquire *os.File
311+ release syncPoint
312+}
313+
314+// releaseSync signals the release point once nothing reads the buffer,
315+// and closes the acquire fence.
316+func (s *Server) releaseSync(h syncHold) {
317+ if h.acquire != nil {
318+ h.acquire.Close()
319+ }
320+ if h.release.set() {
321+ if err := h.release.tl.dev.signal(h.release.tl.handle, h.release.point); err != nil {
322+ s.log.Warn().Str("component", "wayland").Err(err).Msg("release point")
323+ }
324+ h.release.tl.uses--
325+ s.dropTimeline(h.release.tl)
326+ }
327+}
328+
329+// dropTimeline destroys a timeline the client destroyed once no held
330+// buffer still needs to signal it.
331+func (s *Server) dropTimeline(tl *timeline) {
332+ if !tl.alive && tl.uses <= 0 && tl.handle != 0 {
333+ _ = tl.dev.destroy(tl.handle)
334+ tl.handle = 0
335+ }
336+}
337+
338+// takeSyncPoints moves the surface's requested points to the commit and
339+// starts waiting for the acquire point's fence.
340+func (s *surface) takeSyncPoints() {
341+ st := s.sync
342+ if st == nil || !st.acquire.set() {
343+ return
344+ }
345+ cs := &commitSync{acquire: st.acquire, release: st.release}
346+ st.acquire, st.release = syncPoint{}, syncPoint{}
347+ cs.acquire.tl.uses++
348+ cs.release.tl.uses++
349+ w, err := s.server.syncWait.watch(cs.acquire)
350+ if err != nil {
351+ // The point cannot be watched: apply now, readers wait on the
352+ // fence (or its absence) when they read the buffer.
353+ s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire point")
354+ } else {
355+ cs.wait = w
356+ }
357+ s.pendingSync = cs
358+}
359+
360+// syncReady reports whether a commit's acquire point has its fence.
361+func (s *surface) syncReady(cs *commitSync) bool {
362+ return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait)
363+}
364+
365+// dropSync forgets a commit that will never apply: its points are
366+// signalled so the client can reuse the buffer.
367+func (s *surface) dropSync(cs *commitSync) {
368+ if cs == nil {
369+ return
370+ }
371+ if cs.wait != nil {
372+ s.server.syncWait.cancel(cs.wait)
373+ }
374+ cs.acquire.tl.uses--
375+ s.server.dropTimeline(cs.acquire.tl)
376+ s.server.releaseSync(syncHold{release: cs.release})
377+}
378+
379+// applySync makes a commit's points the current buffer's: its acquire
380+// fence goes with the content, its release point waits for release.
381+func (s *surface) applySync(cs *commitSync) {
382+ if cs == nil {
383+ return
384+ }
385+ if cs.wait != nil {
386+ s.server.syncWait.cancel(cs.wait)
387+ }
388+ f, err := cs.acquire.tl.dev.exportSyncFile(cs.acquire.tl.handle, cs.acquire.point)
389+ if err != nil {
390+ s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire fence")
391+ f = nil
392+ }
393+ cs.acquire.tl.uses--
394+ s.server.dropTimeline(cs.acquire.tl)
395+ s.hold = syncHold{acquire: f, release: cs.release}
396+}
@@ -0,0 +1,396 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "sync"
8+
9+ "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
10+ "github.com/bnema/purego-libwayland/protocol/wayland"
11+ "github.com/bnema/purego-libwayland/server"
12+ "golang.org/x/sys/unix"
13+)
14+
15+// wp_linux_drm_syncobj_v1: explicit sync. A client names, per commit, the
16+// timeline point its GPU signals when the buffer is drawn (acquire) and
17+// the point we signal once nothing reads the buffer any more (release).
18+// A commit waits (like a fifo barrier) until its acquire point has a
19+// fence; the fence then travels with the content as a sync file the
20+// renderer and the display wait on, so no CPU waits for the GPU. The
21+// release point replaces wl_buffer.release.
22+
23+const syncobjVersion = 1
24+
25+// timeline is an imported client timeline.
26+type timeline struct {
27+ dev syncobjDevice
28+ handle uint32
29+ alive bool
30+ // uses counts committed points still to signal or wait on.
31+ uses int
32+}
33+
34+// syncPoint is a point on a timeline.
35+type syncPoint struct {
36+ tl *timeline
37+ point uint64
38+}
39+
40+func (p syncPoint) set() bool { return p.tl != nil }
41+
42+// syncState is a surface's wp_linux_drm_syncobj_surface_v1 state.
43+type syncState struct {
44+ resource *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1
45+ acquire, release syncPoint // pending, for the next commit
46+}
47+
48+func registerSyncobj(d *server.Display, s *Server) error {
49+ if s.syncDev == nil {
50+ return nil
51+ }
52+ return linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1Global(d, syncobjVersion, func(c server.Client, v, id uint32) {
53+ _, _ = linuxdrmsyncobj.NewWpLinuxDrmSyncobjManagerV1(c, int32(v), id, syncManager{s})
54+ })
55+}
56+
57+type syncManager struct{ server *Server }
58+
59+func (syncManager) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1) {}
60+
61+func (m syncManager) GetSurface(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, surf *wayland.Surface) {
62+ state := m.server.surfaceOf(surf)
63+ if state == nil {
64+ return
65+ }
66+ if state.sync != nil {
67+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists), "surface already has a syncobj surface")
68+ return
69+ }
70+ st := &syncState{}
71+ res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjSurfaceV1(r.Client(), r.Version(), id, syncSurface{state, st})
72+ if err != nil {
73+ return
74+ }
75+ st.resource = res
76+ state.sync = st
77+ res.OnDestroy = func() {
78+ if state.sync == st {
79+ state.sync = nil
80+ }
81+ }
82+}
83+
84+func (m syncManager) ImportTimeline(r *linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1, id uint32, fd int) {
85+ defer unix.Close(fd)
86+ h, err := m.server.syncDev.fdToHandle(fd)
87+ if err != nil {
88+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline), "invalid timeline fd")
89+ return
90+ }
91+ tl := &timeline{dev: m.server.syncDev, handle: h, alive: true}
92+ res, err := linuxdrmsyncobj.NewWpLinuxDrmSyncobjTimelineV1(r.Client(), r.Version(), id, timelineHandler{})
93+ if err != nil {
94+ _ = tl.dev.destroy(h)
95+ return
96+ }
97+ m.server.timelines[res.Resource] = tl
98+ res.OnDestroy = func() {
99+ delete(m.server.timelines, res.Resource)
100+ // Points already committed keep the syncobj: destroyed once no
101+ // held buffer or waiting commit uses it (timeline.drop).
102+ tl.alive = false
103+ m.server.dropTimeline(tl)
104+ }
105+}
106+
107+type timelineHandler struct{}
108+
109+func (timelineHandler) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1) {}
110+
111+type syncSurface struct {
112+ surf *surface
113+ st *syncState
114+}
115+
116+func (syncSurface) Destroy(*linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1) {}
117+
118+func (h syncSurface) point(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) (syncPoint, bool) {
119+ if h.surf.destroyed {
120+ r.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface), "surface destroyed")
121+ return syncPoint{}, false
122+ }
123+ t := h.surf.server.timelines[tl.Resource]
124+ if t == nil {
125+ return syncPoint{}, false
126+ }
127+ return syncPoint{tl: t, point: uint64(hi)<<32 | uint64(lo)}, true
128+}
129+
130+func (h syncSurface) SetAcquirePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) {
131+ if p, ok := h.point(r, tl, hi, lo); ok {
132+ h.st.acquire = p
133+ }
134+}
135+
136+func (h syncSurface) SetReleasePoint(r *linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1, tl *linuxdrmsyncobj.WpLinuxDrmSyncobjTimelineV1, hi, lo uint32) {
137+ if p, ok := h.point(r, tl, hi, lo); ok {
138+ h.st.release = p
139+ }
140+}
141+
142+// checkSyncCommit applies the protocol rules to a commit with sync
143+// points; false means a protocol error was posted.
144+func (s *surface) checkSyncCommit() bool {
145+ st := s.sync
146+ if st == nil {
147+ return true
148+ }
149+ a, r := st.acquire, st.release
150+ hasBuffer := s.attached && s.pending != nil || !s.attached && s.current != nil
151+ res := st.resource
152+ switch {
153+ case !a.set() && !r.set():
154+ if hasBuffer && s.attached {
155+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "buffer without acquire point")
156+ return false
157+ }
158+ return true
159+ case !hasBuffer:
160+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer), "points without a buffer")
161+ case !a.set():
162+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint), "no acquire point")
163+ case !r.set():
164+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint), "no release point")
165+ case a.tl == r.tl && a.point >= r.point:
166+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints), "release point not after acquire point")
167+ case s.pendingIsSHM():
168+ res.PostError(uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer), "explicit sync on a wl_shm buffer")
169+ default:
170+ return true
171+ }
172+ return false
173+}
174+
175+// pendingIsSHM reports whether the buffer of the next commit is wl_shm.
176+func (s *surface) pendingIsSHM() bool {
177+ b := s.pending
178+ if !s.attached {
179+ b = s.current
180+ }
181+ if b == nil {
182+ return false
183+ }
184+ _, dma := s.server.buffers[b.Resource].(*dmabufBuffer)
185+ return !dma
186+}
187+
188+// syncWaiter watches acquire points: each gets an eventfd the kernel
189+// writes once the point has a fence, and the pacer is woken to apply the
190+// commits that waited for it.
191+type syncWaiter struct {
192+ mu sync.Mutex
193+ ready map[*syncWait]bool
194+ wake func()
195+ pipeR *os.File
196+ pipeW *os.File
197+ add chan *syncWait
198+}
199+
200+// syncWait is one acquire point a queued commit waits for.
201+type syncWait struct {
202+ efd int
203+ done bool // the eventfd fired (pacer, under Do)
204+}
205+
206+func newSyncWaiter(wake func()) (*syncWaiter, error) {
207+ r, w, err := os.Pipe()
208+ if err != nil {
209+ return nil, err
210+ }
211+ return &syncWaiter{ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, add: make(chan *syncWait, 64)}, nil
212+}
213+
214+// watch registers an eventfd for p; the wait is ready once it fires.
215+func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) {
216+ efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
217+ if err != nil {
218+ return nil, err
219+ }
220+ if err := p.tl.dev.eventfd(p.tl.handle, p.point, efd); err != nil {
221+ unix.Close(efd)
222+ return nil, err
223+ }
224+ w := &syncWait{efd: efd}
225+ sw.add <- w
226+ _, _ = sw.pipeW.Write([]byte{0})
227+ return w, nil
228+}
229+
230+// fired reports whether w's point has a fence, and forgets w once it has.
231+func (sw *syncWaiter) fired(w *syncWait) bool {
232+ sw.mu.Lock()
233+ defer sw.mu.Unlock()
234+ if sw.ready[w] {
235+ delete(sw.ready, w)
236+ w.done = true
237+ }
238+ return w.done
239+}
240+
241+// cancel stops watching w (the commit was dropped).
242+func (sw *syncWaiter) cancel(w *syncWait) {
243+ sw.mu.Lock()
244+ delete(sw.ready, w)
245+ sw.mu.Unlock()
246+ w.done = true
247+}
248+
249+// run polls every watched eventfd; it owns and closes them.
250+func (sw *syncWaiter) run(ctx context.Context) {
251+ defer sw.pipeR.Close()
252+ var waits []*syncWait
253+ defer func() {
254+ for _, w := range waits {
255+ unix.Close(w.efd)
256+ }
257+ }()
258+ for ctx.Err() == nil {
259+ fds := []unix.PollFd{{Fd: int32(sw.pipeR.Fd()), Events: unix.POLLIN}}
260+ for _, w := range waits {
261+ fds = append(fds, unix.PollFd{Fd: int32(w.efd), Events: unix.POLLIN})
262+ }
263+ n, err := unix.Poll(fds, 100)
264+ if err != nil && !errors.Is(err, unix.EINTR) {
265+ return
266+ }
267+ if n <= 0 {
268+ continue
269+ }
270+ fired := false
271+ kept := waits[:0]
272+ for i, w := range waits {
273+ if fds[i+1].Revents == 0 {
274+ kept = append(kept, w)
275+ continue
276+ }
277+ unix.Close(w.efd)
278+ sw.mu.Lock()
279+ sw.ready[w] = true
280+ sw.mu.Unlock()
281+ fired = true
282+ }
283+ waits = kept
284+ // New waits join the next poll.
285+ if fds[0].Revents != 0 {
286+ var b [64]byte
287+ _, _ = 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+ }
297+ }
298+ if fired {
299+ sw.wake()
300+ }
301+ }
302+}
303+
304+// close stops the waiter's input side.
305+func (sw *syncWaiter) close() { sw.pipeW.Close() }
306+
307+// syncHold is a buffer's explicit-sync state while it is on screen: the
308+// acquire fence handed to readers and the release point to signal.
309+type syncHold struct {
310+ acquire *os.File
311+ release syncPoint
312+}
313+
314+// releaseSync signals the release point once nothing reads the buffer,
315+// and closes the acquire fence.
316+func (s *Server) releaseSync(h syncHold) {
317+ if h.acquire != nil {
318+ h.acquire.Close()
319+ }
320+ if h.release.set() {
321+ if err := h.release.tl.dev.signal(h.release.tl.handle, h.release.point); err != nil {
322+ s.log.Warn().Str("component", "wayland").Err(err).Msg("release point")
323+ }
324+ h.release.tl.uses--
325+ s.dropTimeline(h.release.tl)
326+ }
327+}
328+
329+// dropTimeline destroys a timeline the client destroyed once no held
330+// buffer still needs to signal it.
331+func (s *Server) dropTimeline(tl *timeline) {
332+ if !tl.alive && tl.uses <= 0 && tl.handle != 0 {
333+ _ = tl.dev.destroy(tl.handle)
334+ tl.handle = 0
335+ }
336+}
337+
338+// takeSyncPoints moves the surface's requested points to the commit and
339+// starts waiting for the acquire point's fence.
340+func (s *surface) takeSyncPoints() {
341+ st := s.sync
342+ if st == nil || !st.acquire.set() {
343+ return
344+ }
345+ cs := &commitSync{acquire: st.acquire, release: st.release}
346+ st.acquire, st.release = syncPoint{}, syncPoint{}
347+ cs.acquire.tl.uses++
348+ cs.release.tl.uses++
349+ w, err := s.server.syncWait.watch(cs.acquire)
350+ if err != nil {
351+ // The point cannot be watched: apply now, readers wait on the
352+ // fence (or its absence) when they read the buffer.
353+ s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire point")
354+ } else {
355+ cs.wait = w
356+ }
357+ s.pendingSync = cs
358+}
359+
360+// syncReady reports whether a commit's acquire point has its fence.
361+func (s *surface) syncReady(cs *commitSync) bool {
362+ return cs == nil || cs.wait == nil || s.server.syncWait.fired(cs.wait)
363+}
364+
365+// dropSync forgets a commit that will never apply: its points are
366+// signalled so the client can reuse the buffer.
367+func (s *surface) dropSync(cs *commitSync) {
368+ if cs == nil {
369+ return
370+ }
371+ if cs.wait != nil {
372+ s.server.syncWait.cancel(cs.wait)
373+ }
374+ cs.acquire.tl.uses--
375+ s.server.dropTimeline(cs.acquire.tl)
376+ s.server.releaseSync(syncHold{release: cs.release})
377+}
378+
379+// applySync makes a commit's points the current buffer's: its acquire
380+// fence goes with the content, its release point waits for release.
381+func (s *surface) applySync(cs *commitSync) {
382+ if cs == nil {
383+ return
384+ }
385+ if cs.wait != nil {
386+ s.server.syncWait.cancel(cs.wait)
387+ }
388+ f, err := cs.acquire.tl.dev.exportSyncFile(cs.acquire.tl.handle, cs.acquire.point)
389+ if err != nil {
390+ s.server.log.Warn().Str("component", "wayland").Err(err).Msg("acquire fence")
391+ f = nil
392+ }
393+ cs.acquire.tl.uses--
394+ s.server.dropTimeline(cs.acquire.tl)
395+ s.hold = syncHold{acquire: f, release: cs.release}
396+}
A internal/adapters/wayland/syncobj_device_mock_test.go +346 -0
@@ -0,0 +1,346 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package wayland
6+
7+import (
8+ "os"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// newMocksyncobjDevice creates a new instance of mocksyncobjDevice. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func newMocksyncobjDevice(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *mocksyncobjDevice {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &mocksyncobjDevice{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// mocksyncobjDevice is an autogenerated mock type for the syncobjDevice type
37+type mocksyncobjDevice struct {
38+ mock.Mock
39+}
40+
41+type mocksyncobjDevice_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *mocksyncobjDevice) EXPECT() *mocksyncobjDevice_Expecter {
46+ return &mocksyncobjDevice_Expecter{mock: &_m.Mock}
47+}
48+
49+// destroy provides a mock function for the type mocksyncobjDevice
50+func (_mock *mocksyncobjDevice) destroy(h uint32) error {
51+ ret := _mock.Called(h)
52+
53+ if len(ret) == 0 {
54+ panic("no return value specified for destroy")
55+ }
56+
57+ var r0 error
58+ if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
59+ r0 = returnFunc(h)
60+ } else {
61+ r0 = ret.Error(0)
62+ }
63+ return r0
64+}
65+
66+// mocksyncobjDevice_destroy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroy'
67+type mocksyncobjDevice_destroy_Call struct {
68+ *mock.Call
69+}
70+
71+// destroy is a helper method to define mock.On call
72+// - h uint32
73+func (_e *mocksyncobjDevice_Expecter) destroy(h any) *mocksyncobjDevice_destroy_Call {
74+ return &mocksyncobjDevice_destroy_Call{Call: _e.mock.On("destroy", h)}
75+}
76+
77+func (_c *mocksyncobjDevice_destroy_Call) Run(run func(h uint32)) *mocksyncobjDevice_destroy_Call {
78+ _c.Call.Run(func(args mock.Arguments) {
79+ var arg0 uint32
80+ if args[0] != nil {
81+ arg0 = args[0].(uint32)
82+ }
83+ run(
84+ arg0,
85+ )
86+ })
87+ return _c
88+}
89+
90+func (_c *mocksyncobjDevice_destroy_Call) Return(err error) *mocksyncobjDevice_destroy_Call {
91+ _c.Call.Return(err)
92+ return _c
93+}
94+
95+func (_c *mocksyncobjDevice_destroy_Call) RunAndReturn(run func(h uint32) error) *mocksyncobjDevice_destroy_Call {
96+ _c.Call.Return(run)
97+ return _c
98+}
99+
100+// eventfd provides a mock function for the type mocksyncobjDevice
101+func (_mock *mocksyncobjDevice) eventfd(h uint32, point uint64, efd int) error {
102+ ret := _mock.Called(h, point, efd)
103+
104+ if len(ret) == 0 {
105+ panic("no return value specified for eventfd")
106+ }
107+
108+ var r0 error
109+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64, int) error); ok {
110+ r0 = returnFunc(h, point, efd)
111+ } else {
112+ r0 = ret.Error(0)
113+ }
114+ return r0
115+}
116+
117+// mocksyncobjDevice_eventfd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'eventfd'
118+type mocksyncobjDevice_eventfd_Call struct {
119+ *mock.Call
120+}
121+
122+// eventfd is a helper method to define mock.On call
123+// - h uint32
124+// - point uint64
125+// - efd int
126+func (_e *mocksyncobjDevice_Expecter) eventfd(h any, point any, efd any) *mocksyncobjDevice_eventfd_Call {
127+ return &mocksyncobjDevice_eventfd_Call{Call: _e.mock.On("eventfd", h, point, efd)}
128+}
129+
130+func (_c *mocksyncobjDevice_eventfd_Call) Run(run func(h uint32, point uint64, efd int)) *mocksyncobjDevice_eventfd_Call {
131+ _c.Call.Run(func(args mock.Arguments) {
132+ var arg0 uint32
133+ if args[0] != nil {
134+ arg0 = args[0].(uint32)
135+ }
136+ var arg1 uint64
137+ if args[1] != nil {
138+ arg1 = args[1].(uint64)
139+ }
140+ var arg2 int
141+ if args[2] != nil {
142+ arg2 = args[2].(int)
143+ }
144+ run(
145+ arg0,
146+ arg1,
147+ arg2,
148+ )
149+ })
150+ return _c
151+}
152+
153+func (_c *mocksyncobjDevice_eventfd_Call) Return(err error) *mocksyncobjDevice_eventfd_Call {
154+ _c.Call.Return(err)
155+ return _c
156+}
157+
158+func (_c *mocksyncobjDevice_eventfd_Call) RunAndReturn(run func(h uint32, point uint64, efd int) error) *mocksyncobjDevice_eventfd_Call {
159+ _c.Call.Return(run)
160+ return _c
161+}
162+
163+// exportSyncFile provides a mock function for the type mocksyncobjDevice
164+func (_mock *mocksyncobjDevice) exportSyncFile(h uint32, point uint64) (*os.File, error) {
165+ ret := _mock.Called(h, point)
166+
167+ if len(ret) == 0 {
168+ panic("no return value specified for exportSyncFile")
169+ }
170+
171+ var r0 *os.File
172+ var r1 error
173+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) (*os.File, error)); ok {
174+ return returnFunc(h, point)
175+ }
176+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) *os.File); ok {
177+ r0 = returnFunc(h, point)
178+ } else {
179+ if ret.Get(0) != nil {
180+ r0 = ret.Get(0).(*os.File)
181+ }
182+ }
183+ if returnFunc, ok := ret.Get(1).(func(uint32, uint64) error); ok {
184+ r1 = returnFunc(h, point)
185+ } else {
186+ r1 = ret.Error(1)
187+ }
188+ return r0, r1
189+}
190+
191+// mocksyncobjDevice_exportSyncFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'exportSyncFile'
192+type mocksyncobjDevice_exportSyncFile_Call struct {
193+ *mock.Call
194+}
195+
196+// exportSyncFile is a helper method to define mock.On call
197+// - h uint32
198+// - point uint64
199+func (_e *mocksyncobjDevice_Expecter) exportSyncFile(h any, point any) *mocksyncobjDevice_exportSyncFile_Call {
200+ return &mocksyncobjDevice_exportSyncFile_Call{Call: _e.mock.On("exportSyncFile", h, point)}
201+}
202+
203+func (_c *mocksyncobjDevice_exportSyncFile_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_exportSyncFile_Call {
204+ _c.Call.Run(func(args mock.Arguments) {
205+ var arg0 uint32
206+ if args[0] != nil {
207+ arg0 = args[0].(uint32)
208+ }
209+ var arg1 uint64
210+ if args[1] != nil {
211+ arg1 = args[1].(uint64)
212+ }
213+ run(
214+ arg0,
215+ arg1,
216+ )
217+ })
218+ return _c
219+}
220+
221+func (_c *mocksyncobjDevice_exportSyncFile_Call) Return(file *os.File, err error) *mocksyncobjDevice_exportSyncFile_Call {
222+ _c.Call.Return(file, err)
223+ return _c
224+}
225+
226+func (_c *mocksyncobjDevice_exportSyncFile_Call) RunAndReturn(run func(h uint32, point uint64) (*os.File, error)) *mocksyncobjDevice_exportSyncFile_Call {
227+ _c.Call.Return(run)
228+ return _c
229+}
230+
231+// fdToHandle provides a mock function for the type mocksyncobjDevice
232+func (_mock *mocksyncobjDevice) fdToHandle(fd int) (uint32, error) {
233+ ret := _mock.Called(fd)
234+
235+ if len(ret) == 0 {
236+ panic("no return value specified for fdToHandle")
237+ }
238+
239+ var r0 uint32
240+ var r1 error
241+ if returnFunc, ok := ret.Get(0).(func(int) (uint32, error)); ok {
242+ return returnFunc(fd)
243+ }
244+ if returnFunc, ok := ret.Get(0).(func(int) uint32); ok {
245+ r0 = returnFunc(fd)
246+ } else {
247+ r0 = ret.Get(0).(uint32)
248+ }
249+ if returnFunc, ok := ret.Get(1).(func(int) error); ok {
250+ r1 = returnFunc(fd)
251+ } else {
252+ r1 = ret.Error(1)
253+ }
254+ return r0, r1
255+}
256+
257+// mocksyncobjDevice_fdToHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fdToHandle'
258+type mocksyncobjDevice_fdToHandle_Call struct {
259+ *mock.Call
260+}
261+
262+// fdToHandle is a helper method to define mock.On call
263+// - fd int
264+func (_e *mocksyncobjDevice_Expecter) fdToHandle(fd any) *mocksyncobjDevice_fdToHandle_Call {
265+ return &mocksyncobjDevice_fdToHandle_Call{Call: _e.mock.On("fdToHandle", fd)}
266+}
267+
268+func (_c *mocksyncobjDevice_fdToHandle_Call) Run(run func(fd int)) *mocksyncobjDevice_fdToHandle_Call {
269+ _c.Call.Run(func(args mock.Arguments) {
270+ var arg0 int
271+ if args[0] != nil {
272+ arg0 = args[0].(int)
273+ }
274+ run(
275+ arg0,
276+ )
277+ })
278+ return _c
279+}
280+
281+func (_c *mocksyncobjDevice_fdToHandle_Call) Return(v uint32, err error) *mocksyncobjDevice_fdToHandle_Call {
282+ _c.Call.Return(v, err)
283+ return _c
284+}
285+
286+func (_c *mocksyncobjDevice_fdToHandle_Call) RunAndReturn(run func(fd int) (uint32, error)) *mocksyncobjDevice_fdToHandle_Call {
287+ _c.Call.Return(run)
288+ return _c
289+}
290+
291+// signal provides a mock function for the type mocksyncobjDevice
292+func (_mock *mocksyncobjDevice) signal(h uint32, point uint64) error {
293+ ret := _mock.Called(h, point)
294+
295+ if len(ret) == 0 {
296+ panic("no return value specified for signal")
297+ }
298+
299+ var r0 error
300+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) error); ok {
301+ r0 = returnFunc(h, point)
302+ } else {
303+ r0 = ret.Error(0)
304+ }
305+ return r0
306+}
307+
308+// mocksyncobjDevice_signal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'signal'
309+type mocksyncobjDevice_signal_Call struct {
310+ *mock.Call
311+}
312+
313+// signal is a helper method to define mock.On call
314+// - h uint32
315+// - point uint64
316+func (_e *mocksyncobjDevice_Expecter) signal(h any, point any) *mocksyncobjDevice_signal_Call {
317+ return &mocksyncobjDevice_signal_Call{Call: _e.mock.On("signal", h, point)}
318+}
319+
320+func (_c *mocksyncobjDevice_signal_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_signal_Call {
321+ _c.Call.Run(func(args mock.Arguments) {
322+ var arg0 uint32
323+ if args[0] != nil {
324+ arg0 = args[0].(uint32)
325+ }
326+ var arg1 uint64
327+ if args[1] != nil {
328+ arg1 = args[1].(uint64)
329+ }
330+ run(
331+ arg0,
332+ arg1,
333+ )
334+ })
335+ return _c
336+}
337+
338+func (_c *mocksyncobjDevice_signal_Call) Return(err error) *mocksyncobjDevice_signal_Call {
339+ _c.Call.Return(err)
340+ return _c
341+}
342+
343+func (_c *mocksyncobjDevice_signal_Call) RunAndReturn(run func(h uint32, point uint64) error) *mocksyncobjDevice_signal_Call {
344+ _c.Call.Return(run)
345+ return _c
346+}
@@ -0,0 +1,346 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package wayland
6+
7+import (
8+ "os"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// newMocksyncobjDevice creates a new instance of mocksyncobjDevice. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func newMocksyncobjDevice(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *mocksyncobjDevice {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &mocksyncobjDevice{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// mocksyncobjDevice is an autogenerated mock type for the syncobjDevice type
37+type mocksyncobjDevice struct {
38+ mock.Mock
39+}
40+
41+type mocksyncobjDevice_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *mocksyncobjDevice) EXPECT() *mocksyncobjDevice_Expecter {
46+ return &mocksyncobjDevice_Expecter{mock: &_m.Mock}
47+}
48+
49+// destroy provides a mock function for the type mocksyncobjDevice
50+func (_mock *mocksyncobjDevice) destroy(h uint32) error {
51+ ret := _mock.Called(h)
52+
53+ if len(ret) == 0 {
54+ panic("no return value specified for destroy")
55+ }
56+
57+ var r0 error
58+ if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
59+ r0 = returnFunc(h)
60+ } else {
61+ r0 = ret.Error(0)
62+ }
63+ return r0
64+}
65+
66+// mocksyncobjDevice_destroy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroy'
67+type mocksyncobjDevice_destroy_Call struct {
68+ *mock.Call
69+}
70+
71+// destroy is a helper method to define mock.On call
72+// - h uint32
73+func (_e *mocksyncobjDevice_Expecter) destroy(h any) *mocksyncobjDevice_destroy_Call {
74+ return &mocksyncobjDevice_destroy_Call{Call: _e.mock.On("destroy", h)}
75+}
76+
77+func (_c *mocksyncobjDevice_destroy_Call) Run(run func(h uint32)) *mocksyncobjDevice_destroy_Call {
78+ _c.Call.Run(func(args mock.Arguments) {
79+ var arg0 uint32
80+ if args[0] != nil {
81+ arg0 = args[0].(uint32)
82+ }
83+ run(
84+ arg0,
85+ )
86+ })
87+ return _c
88+}
89+
90+func (_c *mocksyncobjDevice_destroy_Call) Return(err error) *mocksyncobjDevice_destroy_Call {
91+ _c.Call.Return(err)
92+ return _c
93+}
94+
95+func (_c *mocksyncobjDevice_destroy_Call) RunAndReturn(run func(h uint32) error) *mocksyncobjDevice_destroy_Call {
96+ _c.Call.Return(run)
97+ return _c
98+}
99+
100+// eventfd provides a mock function for the type mocksyncobjDevice
101+func (_mock *mocksyncobjDevice) eventfd(h uint32, point uint64, efd int) error {
102+ ret := _mock.Called(h, point, efd)
103+
104+ if len(ret) == 0 {
105+ panic("no return value specified for eventfd")
106+ }
107+
108+ var r0 error
109+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64, int) error); ok {
110+ r0 = returnFunc(h, point, efd)
111+ } else {
112+ r0 = ret.Error(0)
113+ }
114+ return r0
115+}
116+
117+// mocksyncobjDevice_eventfd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'eventfd'
118+type mocksyncobjDevice_eventfd_Call struct {
119+ *mock.Call
120+}
121+
122+// eventfd is a helper method to define mock.On call
123+// - h uint32
124+// - point uint64
125+// - efd int
126+func (_e *mocksyncobjDevice_Expecter) eventfd(h any, point any, efd any) *mocksyncobjDevice_eventfd_Call {
127+ return &mocksyncobjDevice_eventfd_Call{Call: _e.mock.On("eventfd", h, point, efd)}
128+}
129+
130+func (_c *mocksyncobjDevice_eventfd_Call) Run(run func(h uint32, point uint64, efd int)) *mocksyncobjDevice_eventfd_Call {
131+ _c.Call.Run(func(args mock.Arguments) {
132+ var arg0 uint32
133+ if args[0] != nil {
134+ arg0 = args[0].(uint32)
135+ }
136+ var arg1 uint64
137+ if args[1] != nil {
138+ arg1 = args[1].(uint64)
139+ }
140+ var arg2 int
141+ if args[2] != nil {
142+ arg2 = args[2].(int)
143+ }
144+ run(
145+ arg0,
146+ arg1,
147+ arg2,
148+ )
149+ })
150+ return _c
151+}
152+
153+func (_c *mocksyncobjDevice_eventfd_Call) Return(err error) *mocksyncobjDevice_eventfd_Call {
154+ _c.Call.Return(err)
155+ return _c
156+}
157+
158+func (_c *mocksyncobjDevice_eventfd_Call) RunAndReturn(run func(h uint32, point uint64, efd int) error) *mocksyncobjDevice_eventfd_Call {
159+ _c.Call.Return(run)
160+ return _c
161+}
162+
163+// exportSyncFile provides a mock function for the type mocksyncobjDevice
164+func (_mock *mocksyncobjDevice) exportSyncFile(h uint32, point uint64) (*os.File, error) {
165+ ret := _mock.Called(h, point)
166+
167+ if len(ret) == 0 {
168+ panic("no return value specified for exportSyncFile")
169+ }
170+
171+ var r0 *os.File
172+ var r1 error
173+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) (*os.File, error)); ok {
174+ return returnFunc(h, point)
175+ }
176+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) *os.File); ok {
177+ r0 = returnFunc(h, point)
178+ } else {
179+ if ret.Get(0) != nil {
180+ r0 = ret.Get(0).(*os.File)
181+ }
182+ }
183+ if returnFunc, ok := ret.Get(1).(func(uint32, uint64) error); ok {
184+ r1 = returnFunc(h, point)
185+ } else {
186+ r1 = ret.Error(1)
187+ }
188+ return r0, r1
189+}
190+
191+// mocksyncobjDevice_exportSyncFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'exportSyncFile'
192+type mocksyncobjDevice_exportSyncFile_Call struct {
193+ *mock.Call
194+}
195+
196+// exportSyncFile is a helper method to define mock.On call
197+// - h uint32
198+// - point uint64
199+func (_e *mocksyncobjDevice_Expecter) exportSyncFile(h any, point any) *mocksyncobjDevice_exportSyncFile_Call {
200+ return &mocksyncobjDevice_exportSyncFile_Call{Call: _e.mock.On("exportSyncFile", h, point)}
201+}
202+
203+func (_c *mocksyncobjDevice_exportSyncFile_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_exportSyncFile_Call {
204+ _c.Call.Run(func(args mock.Arguments) {
205+ var arg0 uint32
206+ if args[0] != nil {
207+ arg0 = args[0].(uint32)
208+ }
209+ var arg1 uint64
210+ if args[1] != nil {
211+ arg1 = args[1].(uint64)
212+ }
213+ run(
214+ arg0,
215+ arg1,
216+ )
217+ })
218+ return _c
219+}
220+
221+func (_c *mocksyncobjDevice_exportSyncFile_Call) Return(file *os.File, err error) *mocksyncobjDevice_exportSyncFile_Call {
222+ _c.Call.Return(file, err)
223+ return _c
224+}
225+
226+func (_c *mocksyncobjDevice_exportSyncFile_Call) RunAndReturn(run func(h uint32, point uint64) (*os.File, error)) *mocksyncobjDevice_exportSyncFile_Call {
227+ _c.Call.Return(run)
228+ return _c
229+}
230+
231+// fdToHandle provides a mock function for the type mocksyncobjDevice
232+func (_mock *mocksyncobjDevice) fdToHandle(fd int) (uint32, error) {
233+ ret := _mock.Called(fd)
234+
235+ if len(ret) == 0 {
236+ panic("no return value specified for fdToHandle")
237+ }
238+
239+ var r0 uint32
240+ var r1 error
241+ if returnFunc, ok := ret.Get(0).(func(int) (uint32, error)); ok {
242+ return returnFunc(fd)
243+ }
244+ if returnFunc, ok := ret.Get(0).(func(int) uint32); ok {
245+ r0 = returnFunc(fd)
246+ } else {
247+ r0 = ret.Get(0).(uint32)
248+ }
249+ if returnFunc, ok := ret.Get(1).(func(int) error); ok {
250+ r1 = returnFunc(fd)
251+ } else {
252+ r1 = ret.Error(1)
253+ }
254+ return r0, r1
255+}
256+
257+// mocksyncobjDevice_fdToHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fdToHandle'
258+type mocksyncobjDevice_fdToHandle_Call struct {
259+ *mock.Call
260+}
261+
262+// fdToHandle is a helper method to define mock.On call
263+// - fd int
264+func (_e *mocksyncobjDevice_Expecter) fdToHandle(fd any) *mocksyncobjDevice_fdToHandle_Call {
265+ return &mocksyncobjDevice_fdToHandle_Call{Call: _e.mock.On("fdToHandle", fd)}
266+}
267+
268+func (_c *mocksyncobjDevice_fdToHandle_Call) Run(run func(fd int)) *mocksyncobjDevice_fdToHandle_Call {
269+ _c.Call.Run(func(args mock.Arguments) {
270+ var arg0 int
271+ if args[0] != nil {
272+ arg0 = args[0].(int)
273+ }
274+ run(
275+ arg0,
276+ )
277+ })
278+ return _c
279+}
280+
281+func (_c *mocksyncobjDevice_fdToHandle_Call) Return(v uint32, err error) *mocksyncobjDevice_fdToHandle_Call {
282+ _c.Call.Return(v, err)
283+ return _c
284+}
285+
286+func (_c *mocksyncobjDevice_fdToHandle_Call) RunAndReturn(run func(fd int) (uint32, error)) *mocksyncobjDevice_fdToHandle_Call {
287+ _c.Call.Return(run)
288+ return _c
289+}
290+
291+// signal provides a mock function for the type mocksyncobjDevice
292+func (_mock *mocksyncobjDevice) signal(h uint32, point uint64) error {
293+ ret := _mock.Called(h, point)
294+
295+ if len(ret) == 0 {
296+ panic("no return value specified for signal")
297+ }
298+
299+ var r0 error
300+ if returnFunc, ok := ret.Get(0).(func(uint32, uint64) error); ok {
301+ r0 = returnFunc(h, point)
302+ } else {
303+ r0 = ret.Error(0)
304+ }
305+ return r0
306+}
307+
308+// mocksyncobjDevice_signal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'signal'
309+type mocksyncobjDevice_signal_Call struct {
310+ *mock.Call
311+}
312+
313+// signal is a helper method to define mock.On call
314+// - h uint32
315+// - point uint64
316+func (_e *mocksyncobjDevice_Expecter) signal(h any, point any) *mocksyncobjDevice_signal_Call {
317+ return &mocksyncobjDevice_signal_Call{Call: _e.mock.On("signal", h, point)}
318+}
319+
320+func (_c *mocksyncobjDevice_signal_Call) Run(run func(h uint32, point uint64)) *mocksyncobjDevice_signal_Call {
321+ _c.Call.Run(func(args mock.Arguments) {
322+ var arg0 uint32
323+ if args[0] != nil {
324+ arg0 = args[0].(uint32)
325+ }
326+ var arg1 uint64
327+ if args[1] != nil {
328+ arg1 = args[1].(uint64)
329+ }
330+ run(
331+ arg0,
332+ arg1,
333+ )
334+ })
335+ return _c
336+}
337+
338+func (_c *mocksyncobjDevice_signal_Call) Return(err error) *mocksyncobjDevice_signal_Call {
339+ _c.Call.Return(err)
340+ return _c
341+}
342+
343+func (_c *mocksyncobjDevice_signal_Call) RunAndReturn(run func(h uint32, point uint64) error) *mocksyncobjDevice_signal_Call {
344+ _c.Call.Return(run)
345+ return _c
346+}
A internal/adapters/wayland/syncobj_test.go +355 -0
@@ -0,0 +1,355 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "sync"
7+ "testing"
8+ "time"
9+ "unsafe"
10+
11+ "github.com/bnema/nefertty/internal/logging"
12+ "github.com/bnema/nefertty/internal/ports"
13+ "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
14+ "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
15+ "github.com/bnema/purego-libwayland/protocol/wayland"
16+ "github.com/bnema/wlturbo"
17+ "github.com/stretchr/testify/mock"
18+ "golang.org/x/sys/unix"
19+)
20+
21+func TestSyncobjStructSizes(t *testing.T) {
22+ for _, tc := range []struct {
23+ name string
24+ got uintptr
25+ req uintptr
26+ }{
27+ {"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy},
28+ {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD},
29+ {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle},
30+ {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal},
31+ {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd},
32+ {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap},
33+ } {
34+ if want := (tc.req >> 16) & 0x3fff; tc.got != want {
35+ t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
36+ }
37+ }
38+}
39+
40+// syncHarness is a server with a mocked syncobj device and a client with
41+// a mapped window, a dmabuf buffer and the syncobj objects.
42+type syncHarness struct {
43+ t *testing.T
44+ s *Server
45+ c *wlturbo.Display
46+ dev *mocksyncobjDevice
47+ contents chan ports.SurfaceContent
48+ presented chan ports.OutputPresented
49+ surf uint32
50+ syncSurf uint32
51+ timeline uint32
52+ win ports.WindowID
53+ mu sync.Mutex
54+ efds map[uint64]int // eventfd by acquire point
55+ signals []uint64
56+ fences []*os.File
57+ releases map[uint32]*syncReleaseProxy
58+ mgr uint32
59+}
60+
61+func newSyncHarness(t *testing.T) *syncHarness {
62+ h := &syncHarness{t: t, efds: map[uint64]int{}}
63+ h.dev = newMocksyncobjDevice(t)
64+ h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe()
65+ h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
66+ h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error {
67+ // Keep a duplicate: the waiter closes its own when it fires.
68+ dup, err := unix.Dup(efd)
69+ if err != nil {
70+ return err
71+ }
72+ h.mu.Lock()
73+ h.efds[point] = dup
74+ h.mu.Unlock()
75+ return nil
76+ }).Maybe()
77+ h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) {
78+ r, w, err := os.Pipe()
79+ if err != nil {
80+ return nil, err
81+ }
82+ w.Close()
83+ h.mu.Lock()
84+ h.fences = append(h.fences, r)
85+ h.mu.Unlock()
86+ return r, nil
87+ }).Maybe()
88+ h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error {
89+ h.mu.Lock()
90+ h.signals = append(h.signals, point)
91+ h.mu.Unlock()
92+ return nil
93+ }).Maybe()
94+ dir := t.TempDir()
95+ events := make(chan ports.ClientEvent, 16)
96+ commands := make(chan ports.ClientCommand, 16)
97+ h.contents = make(chan ports.SurfaceContent, 64)
98+ h.presented = make(chan ports.OutputPresented, 16)
99+ sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}}
100+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents, Presented: h.presented}, logging.For(context.Background(), "wayland"))
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ ctx, cancel := context.WithCancel(context.Background())
105+ done := make(chan error, 1)
106+ go func() { done <- s.Run(ctx) }()
107+ t.Cleanup(func() {
108+ cancel()
109+ <-done
110+ h.mu.Lock()
111+ for _, fd := range h.efds {
112+ unix.Close(fd)
113+ }
114+ h.mu.Unlock()
115+ })
116+ h.s = s
117+ h.c = protocolClient(t, s, dir)
118+ w, surf, xdg := surfaceMapper(t, h.c, events)()
119+ registerProtocol(t, h.c, xdg)
120+ h.win, h.surf = w.ID, surf
121+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1"}
122+ mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
123+ h.syncSurf = h.c.AllocateID()
124+ requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf)
125+ registerProtocol(t, h.c, h.syncSurf)
126+ fd, err := unix.MemfdCreate("timeline", 0)
127+ if err != nil {
128+ t.Fatal(err)
129+ }
130+ defer unix.Close(fd)
131+ h.timeline = h.c.AllocateID()
132+ registerProtocol(t, h.c, h.timeline)
133+ if err := h.c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil {
134+ t.Fatal(err)
135+ }
136+ for deadline := time.Now().Add(2 * time.Second); ; {
137+ var out string
138+ s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
139+ if out != "" {
140+ break
141+ }
142+ if time.Now().After(deadline) {
143+ t.Fatal("configure not applied")
144+ }
145+ time.Sleep(2 * time.Millisecond)
146+ }
147+ return h
148+}
149+
150+// dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it).
151+func (h *syncHarness) dmabuf() uint32 {
152+ t, c := h.t, h.c
153+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
154+ fd, err := unix.MemfdCreate("buf", 0)
155+ if err != nil {
156+ t.Fatal(err)
157+ }
158+ defer unix.Close(fd)
159+ if err := unix.Ftruncate(fd, 256*16); err != nil {
160+ t.Fatal(err)
161+ }
162+ p := c.AllocateID()
163+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
164+ registerProtocol(t, c, p)
165+ if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
166+ t.Fatal(err)
167+ }
168+ b := c.AllocateID()
169+ rel := &syncReleaseProxy{released: make(chan struct{}, 4)}
170+ rel.SetID(b)
171+ c.Context().Register(rel)
172+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0))
173+ h.releases[b] = rel
174+ return b
175+}
176+
177+type syncReleaseProxy struct {
178+ wlturbo.BaseProxy
179+ released chan struct{}
180+}
181+
182+func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) {
183+ if uint32(e.Opcode) == wayland.BufferEventRelease {
184+ p.released <- struct{}{}
185+ }
186+}
187+
188+func (h *syncHarness) commit(buf uint32, acquire, release uint64) {
189+ t, c := h.t, h.c
190+ 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))
193+ requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)
194+ if err := c.Roundtrip(); err != nil {
195+ t.Fatal(err)
196+ }
197+}
198+
199+// content waits for a DMABuf content of the window.
200+func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) {
201+ deadline := time.After(wait)
202+ for {
203+ select {
204+ case c := <-h.contents:
205+ if c.ID == h.win && c.DMABuf != nil {
206+ return c, true
207+ }
208+ case <-deadline:
209+ return ports.SurfaceContent{}, false
210+ }
211+ }
212+}
213+
214+// fire makes the kernel's eventfd for an acquire point readable.
215+func (h *syncHarness) fire(point uint64) {
216+ h.t.Helper()
217+ deadline := time.Now().Add(2 * time.Second)
218+ for {
219+ h.mu.Lock()
220+ fd, ok := h.efds[point]
221+ h.mu.Unlock()
222+ if ok {
223+ var one [8]byte
224+ one[0] = 1
225+ if _, err := unix.Write(fd, one[:]); err != nil {
226+ h.t.Fatal(err)
227+ }
228+ return
229+ }
230+ if time.Now().After(deadline) {
231+ h.t.Fatalf("no eventfd for point %d", point)
232+ }
233+ time.Sleep(2 * time.Millisecond)
234+ }
235+}
236+
237+func (h *syncHarness) signalled() []uint64 {
238+ h.mu.Lock()
239+ defer h.mu.Unlock()
240+ return append([]uint64(nil), h.signals...)
241+}
242+
243+// A commit waits for its acquire point; the content carries the fence;
244+// replacing it signals the release point once outputs moved on, and
245+// wl_buffer.release is never sent. The acquire fence closes only then.
246+func TestSyncobjAcquireAndRelease(t *testing.T) {
247+ h := newSyncHarness(t)
248+ h.releases = map[uint32]*syncReleaseProxy{}
249+ a, b := h.dmabuf(), h.dmabuf()
250+ h.commit(a, 1, 2)
251+ if _, ok := h.content(50 * time.Millisecond); ok {
252+ t.Fatal("content before the acquire point had a fence")
253+ }
254+ h.fire(1)
255+ first, ok := h.content(2 * time.Second)
256+ if !ok || first.Acquire == nil {
257+ t.Fatalf("content %+v", first)
258+ }
259+ h.commit(b, 3, 4)
260+ h.fire(3)
261+ second, ok := h.content(2 * time.Second)
262+ if !ok {
263+ t.Fatal("second content")
264+ }
265+ if _, err := first.Acquire.Stat(); err != nil {
266+ t.Fatal("acquire fence closed before release")
267+ }
268+ // The output read the second content: the first buffer is released.
269+ h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}}
270+ deadline := time.Now().Add(2 * time.Second)
271+ for len(h.signalled()) == 0 && time.Now().Before(deadline) {
272+ if err := h.c.Roundtrip(); err != nil {
273+ t.Fatal(err)
274+ }
275+ time.Sleep(2 * time.Millisecond)
276+ }
277+ if got := h.signalled(); len(got) != 1 || got[0] != 2 {
278+ t.Fatalf("signalled %v", got)
279+ }
280+ if _, err := first.Acquire.Stat(); err == nil {
281+ t.Fatal("acquire fence left open after release")
282+ }
283+ if err := h.c.Roundtrip(); err != nil {
284+ t.Fatal(err)
285+ }
286+ select {
287+ case <-h.releases[a].released:
288+ t.Fatal("wl_buffer.release sent under explicit sync")
289+ default:
290+ }
291+}
292+
293+func TestSyncobjProtocolErrors(t *testing.T) {
294+ for _, tc := range []struct {
295+ name string
296+ run func(h *syncHarness)
297+ obj func(h *syncHarness) uint32
298+ code uint32
299+ }{
300+ {"conflicting points", func(h *syncHarness) {
301+ b := h.dmabuf()
302+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
303+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
304+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
305+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
306+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)},
307+ {"no release point", func(h *syncHarness) {
308+ b := h.dmabuf()
309+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
310+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
311+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
312+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)},
313+ {"buffer without points", func(h *syncHarness) {
314+ b := h.dmabuf()
315+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
316+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
317+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
318+ {"surface exists", func(h *syncHarness) {
319+ mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
320+ h.mgr = mgr
321+ requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf)
322+ }, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)},
323+ } {
324+ t.Run(tc.name, func(t *testing.T) {
325+ h := newSyncHarness(t)
326+ h.releases = map[uint32]*syncReleaseProxy{}
327+ h.t = t
328+ tc.run(h)
329+ expectProtocolError(t, h.c, tc.obj(h), tc.code)
330+ })
331+ }
332+}
333+
334+// An invalid timeline fd is a protocol error.
335+func TestSyncobjInvalidTimeline(t *testing.T) {
336+ dev := newMocksyncobjDevice(t)
337+ dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once()
338+ dir := t.TempDir()
339+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland"))
340+ if err != nil {
341+ t.Fatal(err)
342+ }
343+ ctx, cancel := context.WithCancel(context.Background())
344+ done := make(chan error, 1)
345+ go func() { done <- s.Run(ctx) }()
346+ t.Cleanup(func() { cancel(); <-done })
347+ c := protocolClient(t, s, dir)
348+ mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
349+ fd, _ := unix.MemfdCreate("bad", 0)
350+ defer unix.Close(fd)
351+ if err := c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil {
352+ t.Fatal(err)
353+ }
354+ expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
355+}
@@ -0,0 +1,355 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os"
6+ "sync"
7+ "testing"
8+ "time"
9+ "unsafe"
10+
11+ "github.com/bnema/nefertty/internal/logging"
12+ "github.com/bnema/nefertty/internal/ports"
13+ "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
14+ "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
15+ "github.com/bnema/purego-libwayland/protocol/wayland"
16+ "github.com/bnema/wlturbo"
17+ "github.com/stretchr/testify/mock"
18+ "golang.org/x/sys/unix"
19+)
20+
21+func TestSyncobjStructSizes(t *testing.T) {
22+ for _, tc := range []struct {
23+ name string
24+ got uintptr
25+ req uintptr
26+ }{
27+ {"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy},
28+ {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD},
29+ {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle},
30+ {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal},
31+ {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd},
32+ {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap},
33+ } {
34+ if want := (tc.req >> 16) & 0x3fff; tc.got != want {
35+ t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
36+ }
37+ }
38+}
39+
40+// syncHarness is a server with a mocked syncobj device and a client with
41+// a mapped window, a dmabuf buffer and the syncobj objects.
42+type syncHarness struct {
43+ t *testing.T
44+ s *Server
45+ c *wlturbo.Display
46+ dev *mocksyncobjDevice
47+ contents chan ports.SurfaceContent
48+ presented chan ports.OutputPresented
49+ surf uint32
50+ syncSurf uint32
51+ timeline uint32
52+ win ports.WindowID
53+ mu sync.Mutex
54+ efds map[uint64]int // eventfd by acquire point
55+ signals []uint64
56+ fences []*os.File
57+ releases map[uint32]*syncReleaseProxy
58+ mgr uint32
59+}
60+
61+func newSyncHarness(t *testing.T) *syncHarness {
62+ h := &syncHarness{t: t, efds: map[uint64]int{}}
63+ h.dev = newMocksyncobjDevice(t)
64+ h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe()
65+ h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
66+ h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error {
67+ // Keep a duplicate: the waiter closes its own when it fires.
68+ dup, err := unix.Dup(efd)
69+ if err != nil {
70+ return err
71+ }
72+ h.mu.Lock()
73+ h.efds[point] = dup
74+ h.mu.Unlock()
75+ return nil
76+ }).Maybe()
77+ h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) {
78+ r, w, err := os.Pipe()
79+ if err != nil {
80+ return nil, err
81+ }
82+ w.Close()
83+ h.mu.Lock()
84+ h.fences = append(h.fences, r)
85+ h.mu.Unlock()
86+ return r, nil
87+ }).Maybe()
88+ h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error {
89+ h.mu.Lock()
90+ h.signals = append(h.signals, point)
91+ h.mu.Unlock()
92+ return nil
93+ }).Maybe()
94+ dir := t.TempDir()
95+ events := make(chan ports.ClientEvent, 16)
96+ commands := make(chan ports.ClientCommand, 16)
97+ h.contents = make(chan ports.SurfaceContent, 64)
98+ h.presented = make(chan ports.OutputPresented, 16)
99+ sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}}
100+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents, Presented: h.presented}, logging.For(context.Background(), "wayland"))
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ ctx, cancel := context.WithCancel(context.Background())
105+ done := make(chan error, 1)
106+ go func() { done <- s.Run(ctx) }()
107+ t.Cleanup(func() {
108+ cancel()
109+ <-done
110+ h.mu.Lock()
111+ for _, fd := range h.efds {
112+ unix.Close(fd)
113+ }
114+ h.mu.Unlock()
115+ })
116+ h.s = s
117+ h.c = protocolClient(t, s, dir)
118+ w, surf, xdg := surfaceMapper(t, h.c, events)()
119+ registerProtocol(t, h.c, xdg)
120+ h.win, h.surf = w.ID, surf
121+ commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1"}
122+ mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
123+ h.syncSurf = h.c.AllocateID()
124+ requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf)
125+ registerProtocol(t, h.c, h.syncSurf)
126+ fd, err := unix.MemfdCreate("timeline", 0)
127+ if err != nil {
128+ t.Fatal(err)
129+ }
130+ defer unix.Close(fd)
131+ h.timeline = h.c.AllocateID()
132+ registerProtocol(t, h.c, h.timeline)
133+ if err := h.c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil {
134+ t.Fatal(err)
135+ }
136+ for deadline := time.Now().Add(2 * time.Second); ; {
137+ var out string
138+ s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
139+ if out != "" {
140+ break
141+ }
142+ if time.Now().After(deadline) {
143+ t.Fatal("configure not applied")
144+ }
145+ time.Sleep(2 * time.Millisecond)
146+ }
147+ return h
148+}
149+
150+// dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it).
151+func (h *syncHarness) dmabuf() uint32 {
152+ t, c := h.t, h.c
153+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
154+ fd, err := unix.MemfdCreate("buf", 0)
155+ if err != nil {
156+ t.Fatal(err)
157+ }
158+ defer unix.Close(fd)
159+ if err := unix.Ftruncate(fd, 256*16); err != nil {
160+ t.Fatal(err)
161+ }
162+ p := c.AllocateID()
163+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
164+ registerProtocol(t, c, p)
165+ if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
166+ t.Fatal(err)
167+ }
168+ b := c.AllocateID()
169+ rel := &syncReleaseProxy{released: make(chan struct{}, 4)}
170+ rel.SetID(b)
171+ c.Context().Register(rel)
172+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0))
173+ h.releases[b] = rel
174+ return b
175+}
176+
177+type syncReleaseProxy struct {
178+ wlturbo.BaseProxy
179+ released chan struct{}
180+}
181+
182+func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) {
183+ if uint32(e.Opcode) == wayland.BufferEventRelease {
184+ p.released <- struct{}{}
185+ }
186+}
187+
188+func (h *syncHarness) commit(buf uint32, acquire, release uint64) {
189+ t, c := h.t, h.c
190+ 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))
193+ requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)
194+ if err := c.Roundtrip(); err != nil {
195+ t.Fatal(err)
196+ }
197+}
198+
199+// content waits for a DMABuf content of the window.
200+func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) {
201+ deadline := time.After(wait)
202+ for {
203+ select {
204+ case c := <-h.contents:
205+ if c.ID == h.win && c.DMABuf != nil {
206+ return c, true
207+ }
208+ case <-deadline:
209+ return ports.SurfaceContent{}, false
210+ }
211+ }
212+}
213+
214+// fire makes the kernel's eventfd for an acquire point readable.
215+func (h *syncHarness) fire(point uint64) {
216+ h.t.Helper()
217+ deadline := time.Now().Add(2 * time.Second)
218+ for {
219+ h.mu.Lock()
220+ fd, ok := h.efds[point]
221+ h.mu.Unlock()
222+ if ok {
223+ var one [8]byte
224+ one[0] = 1
225+ if _, err := unix.Write(fd, one[:]); err != nil {
226+ h.t.Fatal(err)
227+ }
228+ return
229+ }
230+ if time.Now().After(deadline) {
231+ h.t.Fatalf("no eventfd for point %d", point)
232+ }
233+ time.Sleep(2 * time.Millisecond)
234+ }
235+}
236+
237+func (h *syncHarness) signalled() []uint64 {
238+ h.mu.Lock()
239+ defer h.mu.Unlock()
240+ return append([]uint64(nil), h.signals...)
241+}
242+
243+// A commit waits for its acquire point; the content carries the fence;
244+// replacing it signals the release point once outputs moved on, and
245+// wl_buffer.release is never sent. The acquire fence closes only then.
246+func TestSyncobjAcquireAndRelease(t *testing.T) {
247+ h := newSyncHarness(t)
248+ h.releases = map[uint32]*syncReleaseProxy{}
249+ a, b := h.dmabuf(), h.dmabuf()
250+ h.commit(a, 1, 2)
251+ if _, ok := h.content(50 * time.Millisecond); ok {
252+ t.Fatal("content before the acquire point had a fence")
253+ }
254+ h.fire(1)
255+ first, ok := h.content(2 * time.Second)
256+ if !ok || first.Acquire == nil {
257+ t.Fatalf("content %+v", first)
258+ }
259+ h.commit(b, 3, 4)
260+ h.fire(3)
261+ second, ok := h.content(2 * time.Second)
262+ if !ok {
263+ t.Fatal("second content")
264+ }
265+ if _, err := first.Acquire.Stat(); err != nil {
266+ t.Fatal("acquire fence closed before release")
267+ }
268+ // The output read the second content: the first buffer is released.
269+ h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}}
270+ deadline := time.Now().Add(2 * time.Second)
271+ for len(h.signalled()) == 0 && time.Now().Before(deadline) {
272+ if err := h.c.Roundtrip(); err != nil {
273+ t.Fatal(err)
274+ }
275+ time.Sleep(2 * time.Millisecond)
276+ }
277+ if got := h.signalled(); len(got) != 1 || got[0] != 2 {
278+ t.Fatalf("signalled %v", got)
279+ }
280+ if _, err := first.Acquire.Stat(); err == nil {
281+ t.Fatal("acquire fence left open after release")
282+ }
283+ if err := h.c.Roundtrip(); err != nil {
284+ t.Fatal(err)
285+ }
286+ select {
287+ case <-h.releases[a].released:
288+ t.Fatal("wl_buffer.release sent under explicit sync")
289+ default:
290+ }
291+}
292+
293+func TestSyncobjProtocolErrors(t *testing.T) {
294+ for _, tc := range []struct {
295+ name string
296+ run func(h *syncHarness)
297+ obj func(h *syncHarness) uint32
298+ code uint32
299+ }{
300+ {"conflicting points", func(h *syncHarness) {
301+ b := h.dmabuf()
302+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
303+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
304+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
305+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
306+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)},
307+ {"no release point", func(h *syncHarness) {
308+ b := h.dmabuf()
309+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
310+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
311+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
312+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)},
313+ {"buffer without points", func(h *syncHarness) {
314+ b := h.dmabuf()
315+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
316+ requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
317+ }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
318+ {"surface exists", func(h *syncHarness) {
319+ mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
320+ h.mgr = mgr
321+ requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf)
322+ }, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)},
323+ } {
324+ t.Run(tc.name, func(t *testing.T) {
325+ h := newSyncHarness(t)
326+ h.releases = map[uint32]*syncReleaseProxy{}
327+ h.t = t
328+ tc.run(h)
329+ expectProtocolError(t, h.c, tc.obj(h), tc.code)
330+ })
331+ }
332+}
333+
334+// An invalid timeline fd is a protocol error.
335+func TestSyncobjInvalidTimeline(t *testing.T) {
336+ dev := newMocksyncobjDevice(t)
337+ dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once()
338+ dir := t.TempDir()
339+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland"))
340+ if err != nil {
341+ t.Fatal(err)
342+ }
343+ ctx, cancel := context.WithCancel(context.Background())
344+ done := make(chan error, 1)
345+ go func() { done <- s.Run(ctx) }()
346+ t.Cleanup(func() { cancel(); <-done })
347+ c := protocolClient(t, s, dir)
348+ mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
349+ fd, _ := unix.MemfdCreate("bad", 0)
350+ defer unix.Close(fd)
351+ if err := c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil {
352+ t.Fatal(err)
353+ }
354+ expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
355+}
M internal/app/app.go +10 -1
@@ -25,6 +25,7 @@ import (
2525 "github.com/bnema/nefertty/internal/logging"
2626 "github.com/bnema/nefertty/internal/ports"
2727 "github.com/bnema/zerowrap"
28+ "golang.org/x/sys/unix"
2829 )
2930
3031 type Options struct {
@@ -102,7 +103,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
102103 // Every output renders on the same GPU: its formats are the clients'.
103104 dmabuf := vulkan.Probe()
104105 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
105- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland"))
106+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland"))
106107 if err != nil {
107108 km.Close()
108109 return err
@@ -378,3 +379,11 @@ func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.
378379 log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display")
379380 return d
380381 }
382+
383+// renderNode is the /dev/dri path of a render node dev_t, "" when none.
384+func renderNode(dev uint64) string {
385+ if dev == 0 {
386+ return ""
387+ }
388+ return fmt.Sprintf("/dev/dri/renderD%d", unix.Minor(dev))
389+}
@@ -25,6 +25,7 @@ import (
25 "github.com/bnema/nefertty/internal/logging"25 "github.com/bnema/nefertty/internal/logging"
26 "github.com/bnema/nefertty/internal/ports"26 "github.com/bnema/nefertty/internal/ports"
27 "github.com/bnema/zerowrap"27 "github.com/bnema/zerowrap"
28+ "golang.org/x/sys/unix"
28 )29 )
29 30
30 type Options struct {31 type Options struct {
@@ -102,7 +103,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
102 // Every output renders on the same GPU: its formats are the clients'.103 // Every output renders on the same GPU: its formats are the clients'.
103 dmabuf := vulkan.Probe()104 dmabuf := vulkan.Probe()
104 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")105 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
105- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland"))106+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, SyncobjNode: renderNode(dmabuf.Device), Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented, OutputFormats: outputFormats}, logging.For(ctx, "wayland"))
106 if err != nil {107 if err != nil {
107 km.Close()108 km.Close()
108 return err109 return err
@@ -378,3 +379,11 @@ func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.
378 log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display")379 log.Info().Str("DISPLAY", d.Name()).Msg("listening on X11 display")
379 return d380 return d
380 }381 }
382+
383+// renderNode is the /dev/dri path of a render node dev_t, "" when none.
384+func renderNode(dev uint64) string {
385+ if dev == 0 {
386+ return ""
387+ }
388+ return fmt.Sprintf("/dev/dri/renderD%d", unix.Minor(dev))
389+}
M internal/ports/messages.go +6 -0
@@ -622,6 +622,12 @@ type SurfaceContent struct {
622622 // Async asks for tearing presentation (wp_tearing_control_v1); outputs
623623 // honour it only in direct scanout.
624624 Async bool
625+ // Acquire is the explicit-sync fence of this content's root buffer
626+ // (wp_linux_drm_syncobj_v1): readers wait on it before reading the
627+ // buffer, instead of the buffer's implicit fences. Wayland owns it and
628+ // closes it when the buffer is released; a reader that keeps it past
629+ // the call duplicates it under Acquire.SyscallConn. nil: implicit sync.
630+ Acquire *os.File
625631 // DamageHistory is what changed in the root surface's buffer over the
626632 // window's last contents, oldest first, ending with this one (Seq).
627633 // A renderer holding an older content redraws the union of the
@@ -622,6 +622,12 @@ type SurfaceContent struct {
622 // Async asks for tearing presentation (wp_tearing_control_v1); outputs622 // Async asks for tearing presentation (wp_tearing_control_v1); outputs
623 // honour it only in direct scanout.623 // honour it only in direct scanout.
624 Async bool624 Async bool
625+ // Acquire is the explicit-sync fence of this content's root buffer
626+ // (wp_linux_drm_syncobj_v1): readers wait on it before reading the
627+ // buffer, instead of the buffer's implicit fences. Wayland owns it and
628+ // closes it when the buffer is released; a reader that keeps it past
629+ // the call duplicates it under Acquire.SyscallConn. nil: implicit sync.
630+ Acquire *os.File
625 // DamageHistory is what changed in the root surface's buffer over the631 // DamageHistory is what changed in the root surface's buffer over the
626 // window's last contents, oldest first, ending with this one (Seq).632 // window's last contents, oldest first, ending with this one (Seq).
627 // A renderer holding an older content redraws the union of the633 // A renderer holding an older content redraws the union of the

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