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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 41 | 41 | 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 | 53 | const ( | |
| 45 | 54 | ioctlCreate = uintptr(0xc00864bf) | |
| 46 | 55 | ioctlDestroy = uintptr(0xc00864c0) | |
| @@ -49,6 +58,8 @@ const ( | |||
| 49 | 58 | ioctlTimelineWait = uintptr(0xc03064ca) | |
| 50 | 59 | ioctlQuery = uintptr(0xc01864cb) | |
| 51 | 60 | ioctlTransfer = uintptr(0xc02064cc) | |
| 61 | + | ioctlTimelineSig = uintptr(0xc01864cd) | |
| 62 | + | ioctlEventFD = uintptr(0xc01864cf) | |
| 52 | 63 | importSyncFile = 1 | |
| 53 | 64 | exportSyncFile = 1 | |
| 54 | 65 | ) | |
| @@ -118,6 +129,41 @@ func (n *Node) ExportTimeline(handle uint32) (int, error) { | |||
| 118 | 129 | 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 | 167 | // ImportFence consumes syncFile only after FD_TO_HANDLE imports it into a | |
| 122 | 168 | // pre-created destination syncobj. With IMPORT_SYNC_FILE, Handle is an input, | |
| 123 | 169 | // not an output: asking the kernel to import into handle zero returns ENOENT. | |
| @@ -41,6 +41,15 @@ type queryArg struct { | |||||
| 41 | Count, Flags uint32 | 41 | 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 = 1 | 63 | importSyncFile = 1 | ||
| 53 | exportSyncFile = 1 | 64 | exportSyncFile = 1 | ||
| 54 | ) | 65 | ) | ||
| @@ -118,6 +129,41 @@ func (n *Node) ExportTimeline(handle uint32) (int, error) { | |||||
| 118 | return int(a.FD), nil | 129 | 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 a | 167 | // 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) { | |||
| 22 | 22 | {"transfer", unsafe.Sizeof(transferArg{}), 32}, | |
| 23 | 23 | {"wait", unsafe.Sizeof(timelineWaitArg{}), 48}, | |
| 24 | 24 | {"query", unsafe.Sizeof(queryArg{}), 24}, | |
| 25 | + | {"eventfd", unsafe.Sizeof(eventFDArg{}), 24}, | |
| 25 | 26 | } { | |
| 26 | 27 | if tc.got != tc.want { | |
| 27 | 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 | 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 | + | } | |
| @@ -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 | + | } | |||