detached ed16cf7

feat(syncobj): add EventFD, Signal and ImportTimeline ioctls for eventfd-based release

commit
ed16cf7ee3ed696cf9cf2946db82889a7e956073
author
brice · · 2026-10-01 15:39 UTC
committer
brice · · 2026-10-01 15:57 UTC
parent
2de4e1d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +124 -0
M internal/presentation/syncobj/syncobj.go +46 -0
@@ -41,6 +41,15 @@ type queryArg struct {
4141 Count, Flags uint32
4242 }
4343
44+// eventFDArg is struct drm_syncobj_eventfd: the kernel signals fd once the
45+// timeline point is signaled (flags 0) without any thread waiting on it.
46+type eventFDArg struct {
47+ Handle, Flags uint32
48+ Point uint64
49+ FD int32
50+ Pad uint32
51+}
52+
4453 const (
4554 ioctlCreate = uintptr(0xc00864bf)
4655 ioctlDestroy = uintptr(0xc00864c0)
@@ -49,6 +58,8 @@ const (
4958 ioctlTimelineWait = uintptr(0xc03064ca)
5059 ioctlQuery = uintptr(0xc01864cb)
5160 ioctlTransfer = uintptr(0xc02064cc)
61+ ioctlTimelineSig = uintptr(0xc01864cd)
62+ ioctlEventFD = uintptr(0xc01864cf)
5263 importSyncFile = 1
5364 exportSyncFile = 1
5465 )
@@ -118,6 +129,41 @@ func (n *Node) ExportTimeline(handle uint32) (int, error) {
118129 return int(a.FD), nil
119130 }
120131
132+// ImportTimeline turns a syncobj FD (as returned by ExportTimeline, possibly by
133+// another process or Node) into a handle of this node. The FD stays owned by
134+// the caller.
135+func (n *Node) ImportTimeline(fd int) (uint32, error) {
136+ a := handleArg{FD: int32(fd)}
137+ if err := n.ioctl(ioctlFDToHandle, unsafe.Pointer(&a)); err != nil {
138+ return 0, err
139+ }
140+ return a.Handle, nil
141+}
142+
143+// EventFD arms efd, an eventfd owned by the caller, to become readable when
144+// point of the timeline behind handle is signaled. The point need not be
145+// submitted yet. The registration is one-shot: re-arm after every use. The
146+// call returns at once and never waits.
147+func (n *Node) EventFD(handle uint32, point uint64, efd int) error {
148+ a := eventFDArg{Handle: handle, Point: point, FD: int32(efd)}
149+ return n.ioctl(ioctlEventFD, unsafe.Pointer(&a))
150+}
151+
152+// Signal signals one timeline point from the CPU (DRM_IOCTL_SYNCOBJ_TIMELINE_SIGNAL).
153+// The compositor does this through its release timeline; clients use it only
154+// to simulate that in tests.
155+func (n *Node) Signal(handle uint32, point uint64) error {
156+ h, p := handle, point
157+ a := queryArg{Handles: uint64(uintptr(unsafe.Pointer(&h))), Points: uint64(uintptr(unsafe.Pointer(&p))), Count: 1}
158+ errno := n.ioctlErrno(ioctlTimelineSig, unsafe.Pointer(&a))
159+ runtime.KeepAlive(h)
160+ runtime.KeepAlive(p)
161+ if errno != 0 {
162+ return ioctlError(ioctlTimelineSig, errno)
163+ }
164+ return nil
165+}
166+
121167 // ImportFence consumes syncFile only after FD_TO_HANDLE imports it into a
122168 // pre-created destination syncobj. With IMPORT_SYNC_FILE, Handle is an input,
123169 // not an output: asking the kernel to import into handle zero returns ENOENT.
@@ -41,6 +41,15 @@ type queryArg struct {
41 Count, Flags uint3241 Count, Flags uint32
42 }42 }
43 43
44+// eventFDArg is struct drm_syncobj_eventfd: the kernel signals fd once the
45+// timeline point is signaled (flags 0) without any thread waiting on it.
46+type eventFDArg struct {
47+ Handle, Flags uint32
48+ Point uint64
49+ FD int32
50+ Pad uint32
51+}
52+
44 const (53 const (
45 ioctlCreate = uintptr(0xc00864bf)54 ioctlCreate = uintptr(0xc00864bf)
46 ioctlDestroy = uintptr(0xc00864c0)55 ioctlDestroy = uintptr(0xc00864c0)
@@ -49,6 +58,8 @@ const (
49 ioctlTimelineWait = uintptr(0xc03064ca)58 ioctlTimelineWait = uintptr(0xc03064ca)
50 ioctlQuery = uintptr(0xc01864cb)59 ioctlQuery = uintptr(0xc01864cb)
51 ioctlTransfer = uintptr(0xc02064cc)60 ioctlTransfer = uintptr(0xc02064cc)
61+ ioctlTimelineSig = uintptr(0xc01864cd)
62+ ioctlEventFD = uintptr(0xc01864cf)
52 importSyncFile = 163 importSyncFile = 1
53 exportSyncFile = 164 exportSyncFile = 1
54 )65 )
@@ -118,6 +129,41 @@ func (n *Node) ExportTimeline(handle uint32) (int, error) {
118 return int(a.FD), nil129 return int(a.FD), nil
119 }130 }
120 131
132+// ImportTimeline turns a syncobj FD (as returned by ExportTimeline, possibly by
133+// another process or Node) into a handle of this node. The FD stays owned by
134+// the caller.
135+func (n *Node) ImportTimeline(fd int) (uint32, error) {
136+ a := handleArg{FD: int32(fd)}
137+ if err := n.ioctl(ioctlFDToHandle, unsafe.Pointer(&a)); err != nil {
138+ return 0, err
139+ }
140+ return a.Handle, nil
141+}
142+
143+// EventFD arms efd, an eventfd owned by the caller, to become readable when
144+// point of the timeline behind handle is signaled. The point need not be
145+// submitted yet. The registration is one-shot: re-arm after every use. The
146+// call returns at once and never waits.
147+func (n *Node) EventFD(handle uint32, point uint64, efd int) error {
148+ a := eventFDArg{Handle: handle, Point: point, FD: int32(efd)}
149+ return n.ioctl(ioctlEventFD, unsafe.Pointer(&a))
150+}
151+
152+// Signal signals one timeline point from the CPU (DRM_IOCTL_SYNCOBJ_TIMELINE_SIGNAL).
153+// The compositor does this through its release timeline; clients use it only
154+// to simulate that in tests.
155+func (n *Node) Signal(handle uint32, point uint64) error {
156+ h, p := handle, point
157+ a := queryArg{Handles: uint64(uintptr(unsafe.Pointer(&h))), Points: uint64(uintptr(unsafe.Pointer(&p))), Count: 1}
158+ errno := n.ioctlErrno(ioctlTimelineSig, unsafe.Pointer(&a))
159+ runtime.KeepAlive(h)
160+ runtime.KeepAlive(p)
161+ if errno != 0 {
162+ return ioctlError(ioctlTimelineSig, errno)
163+ }
164+ return nil
165+}
166+
121 // ImportFence consumes syncFile only after FD_TO_HANDLE imports it into a167 // ImportFence consumes syncFile only after FD_TO_HANDLE imports it into a
122 // pre-created destination syncobj. With IMPORT_SYNC_FILE, Handle is an input,168 // pre-created destination syncobj. With IMPORT_SYNC_FILE, Handle is an input,
123 // not an output: asking the kernel to import into handle zero returns ENOENT.169 // not an output: asking the kernel to import into handle zero returns ENOENT.
M internal/presentation/syncobj/syncobj_test.go +78 -0
@@ -22,6 +22,7 @@ func TestUAPILayouts(t *testing.T) {
2222 {"transfer", unsafe.Sizeof(transferArg{}), 32},
2323 {"wait", unsafe.Sizeof(timelineWaitArg{}), 48},
2424 {"query", unsafe.Sizeof(queryArg{}), 24},
25+ {"eventfd", unsafe.Sizeof(eventFDArg{}), 24},
2526 } {
2627 if tc.got != tc.want {
2728 t.Errorf("%s size: got %d want %d", tc.name, tc.got, tc.want)
@@ -102,3 +103,80 @@ func TestFailedImportRetainsFD(t *testing.T) {
102103 t.Fatalf("lost caller FD after failed import: %v", err)
103104 }
104105 }
106+
107+// DRM_IOWR(type 'd', nr, size): direction 3<<30, size<<16, type<<8, nr.
108+func TestEventFDRequestNumber(t *testing.T) {
109+ want := uintptr(3<<30 | unsafe.Sizeof(eventFDArg{})<<16 | 'd'<<8 | 0xCF)
110+ if ioctlEventFD != want {
111+ t.Fatalf("DRM_IOCTL_SYNCOBJ_EVENTFD: got %#x want %#x", ioctlEventFD, want)
112+ }
113+ want = uintptr(3<<30 | unsafe.Sizeof(queryArg{})<<16 | 'd'<<8 | 0xCD)
114+ if ioctlTimelineSig != want {
115+ t.Fatalf("DRM_IOCTL_SYNCOBJ_TIMELINE_SIGNAL: got %#x want %#x", ioctlTimelineSig, want)
116+ }
117+}
118+
119+func TestEventFDBadFDFailsWithErrno(t *testing.T) {
120+ n := &Node{fd: -1}
121+ if err := n.EventFD(1, 1, 0); !errors.Is(err, unix.EBADF) {
122+ t.Fatalf("want EBADF, got %v", err)
123+ }
124+}
125+
126+// A point signaled from the CPU makes the registered eventfd readable without
127+// any waiter thread; the registration is one-shot.
128+func TestEventFDSignaledPointHardware(t *testing.T) {
129+ path := os.Getenv("NEFERGUI_RENDER_NODE")
130+ if path == "" {
131+ t.Skip("opt in with NEFERGUI_RENDER_NODE=/dev/dri/renderD...")
132+ }
133+ n, err := Open(path)
134+ if err != nil {
135+ t.Fatal(err)
136+ }
137+ defer n.Close()
138+ handle, err := n.Create()
139+ if err != nil {
140+ t.Fatal(err)
141+ }
142+ defer n.Destroy(handle)
143+ efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
144+ if err != nil {
145+ t.Fatal(err)
146+ }
147+ defer unix.Close(efd)
148+ if err = n.EventFD(handle, 7, efd); err != nil {
149+ t.Fatalf("EventFD: %v", err)
150+ }
151+ readable := func(timeout int) bool {
152+ fds := []unix.PollFd{{Fd: int32(efd), Events: unix.POLLIN}}
153+ r, err := unix.Poll(fds, timeout)
154+ if err != nil {
155+ t.Fatal(err)
156+ }
157+ return r > 0
158+ }
159+ if readable(0) {
160+ t.Fatal("eventfd readable before the point signaled")
161+ }
162+ if err = n.Signal(handle, 7); err != nil {
163+ t.Fatalf("Signal: %v", err)
164+ }
165+ if !readable(1000) {
166+ t.Fatal("eventfd not readable after the point signaled")
167+ }
168+ var buf [8]byte
169+ if _, err = unix.Read(efd, buf[:]); err != nil {
170+ t.Fatal(err)
171+ }
172+ if readable(0) {
173+ t.Fatal("eventfd still readable after read")
174+ }
175+ // Re-arming for an already signaled point fires immediately.
176+ if err = n.EventFD(handle, 7, efd); err != nil {
177+ t.Fatal(err)
178+ }
179+ if !readable(1000) {
180+ t.Fatal("re-armed eventfd for a signaled point stayed silent")
181+ }
182+}
@@ -22,6 +22,7 @@ func TestUAPILayouts(t *testing.T) {
22 {"transfer", unsafe.Sizeof(transferArg{}), 32},22 {"transfer", unsafe.Sizeof(transferArg{}), 32},
23 {"wait", unsafe.Sizeof(timelineWaitArg{}), 48},23 {"wait", unsafe.Sizeof(timelineWaitArg{}), 48},
24 {"query", unsafe.Sizeof(queryArg{}), 24},24 {"query", unsafe.Sizeof(queryArg{}), 24},
25+ {"eventfd", unsafe.Sizeof(eventFDArg{}), 24},
25 } {26 } {
26 if tc.got != tc.want {27 if tc.got != tc.want {
27 t.Errorf("%s size: got %d want %d", tc.name, tc.got, tc.want)28 t.Errorf("%s size: got %d want %d", tc.name, tc.got, tc.want)
@@ -102,3 +103,80 @@ func TestFailedImportRetainsFD(t *testing.T) {
102 t.Fatalf("lost caller FD after failed import: %v", err)103 t.Fatalf("lost caller FD after failed import: %v", err)
103 }104 }
104 }105 }
106+
107+// DRM_IOWR(type 'd', nr, size): direction 3<<30, size<<16, type<<8, nr.
108+func TestEventFDRequestNumber(t *testing.T) {
109+ want := uintptr(3<<30 | unsafe.Sizeof(eventFDArg{})<<16 | 'd'<<8 | 0xCF)
110+ if ioctlEventFD != want {
111+ t.Fatalf("DRM_IOCTL_SYNCOBJ_EVENTFD: got %#x want %#x", ioctlEventFD, want)
112+ }
113+ want = uintptr(3<<30 | unsafe.Sizeof(queryArg{})<<16 | 'd'<<8 | 0xCD)
114+ if ioctlTimelineSig != want {
115+ t.Fatalf("DRM_IOCTL_SYNCOBJ_TIMELINE_SIGNAL: got %#x want %#x", ioctlTimelineSig, want)
116+ }
117+}
118+
119+func TestEventFDBadFDFailsWithErrno(t *testing.T) {
120+ n := &Node{fd: -1}
121+ if err := n.EventFD(1, 1, 0); !errors.Is(err, unix.EBADF) {
122+ t.Fatalf("want EBADF, got %v", err)
123+ }
124+}
125+
126+// A point signaled from the CPU makes the registered eventfd readable without
127+// any waiter thread; the registration is one-shot.
128+func TestEventFDSignaledPointHardware(t *testing.T) {
129+ path := os.Getenv("NEFERGUI_RENDER_NODE")
130+ if path == "" {
131+ t.Skip("opt in with NEFERGUI_RENDER_NODE=/dev/dri/renderD...")
132+ }
133+ n, err := Open(path)
134+ if err != nil {
135+ t.Fatal(err)
136+ }
137+ defer n.Close()
138+ handle, err := n.Create()
139+ if err != nil {
140+ t.Fatal(err)
141+ }
142+ defer n.Destroy(handle)
143+ efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
144+ if err != nil {
145+ t.Fatal(err)
146+ }
147+ defer unix.Close(efd)
148+ if err = n.EventFD(handle, 7, efd); err != nil {
149+ t.Fatalf("EventFD: %v", err)
150+ }
151+ readable := func(timeout int) bool {
152+ fds := []unix.PollFd{{Fd: int32(efd), Events: unix.POLLIN}}
153+ r, err := unix.Poll(fds, timeout)
154+ if err != nil {
155+ t.Fatal(err)
156+ }
157+ return r > 0
158+ }
159+ if readable(0) {
160+ t.Fatal("eventfd readable before the point signaled")
161+ }
162+ if err = n.Signal(handle, 7); err != nil {
163+ t.Fatalf("Signal: %v", err)
164+ }
165+ if !readable(1000) {
166+ t.Fatal("eventfd not readable after the point signaled")
167+ }
168+ var buf [8]byte
169+ if _, err = unix.Read(efd, buf[:]); err != nil {
170+ t.Fatal(err)
171+ }
172+ if readable(0) {
173+ t.Fatal("eventfd still readable after read")
174+ }
175+ // Re-arming for an already signaled point fires immediately.
176+ if err = n.EventFD(handle, 7, efd); err != nil {
177+ t.Fatal(err)
178+ }
179+ if !readable(1000) {
180+ t.Fatal("re-armed eventfd for a signaled point stayed silent")
181+ }
182+}

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 1

Tags 12

↑↓ moveenter openesc close