1//go:build linux
2
3// Package syncobj bridges Vulkan SYNC_FD semaphores and Wayland DRM syncobj
4// timelines on the compositor-selected render node. It never interprets a
5// Vulkan OPAQUE_FD as a DRM syncobj FD.
6package syncobj
7
8import (
9 "fmt"
10 "runtime"
11 "time"
12 "unsafe"
13
14 "golang.org/x/sys/unix"
15)
16
17// These ABI layouts and request numbers correspond to Linux drm.h syncobj
18// ioctls (DRM_IOWR, type 'd'). They are intentionally fixed-width; keep the
19// layout tests aligned with the pinned host UAPI when updating the bridge.
20type createArg struct{ Handle, Flags uint32 }
21type destroyArg struct{ Handle, Pad uint32 }
22type handleArg struct {
23 Handle, Flags uint32
24 FD int32
25 Pad uint32
26 Point uint64
27}
28type transferArg struct {
29 Src, Dst uint32
30 SrcPoint, DstPoint uint64
31 Flags, Pad uint32
32}
33type timelineWaitArg struct {
34 Handles, Points uint64
35 TimeoutNS int64
36 Count, Flags, First, Pad uint32
37 DeadlineNS uint64
38}
39type queryArg struct {
40 Handles, Points uint64
41 Count, Flags uint32
42}
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.
46type eventFDArg struct {
47 Handle, Flags uint32
48 Point uint64
49 FD int32
50 Pad uint32
51}
52
53const (
54 ioctlCreate = uintptr(0xc00864bf)
55 ioctlDestroy = uintptr(0xc00864c0)
56 ioctlHandleToFD = uintptr(0xc01864c1)
57 ioctlFDToHandle = uintptr(0xc01864c2)
58 ioctlTimelineWait = uintptr(0xc03064ca)
59 ioctlQuery = uintptr(0xc01864cb)
60 ioctlTransfer = uintptr(0xc02064cc)
61 ioctlTimelineSig = uintptr(0xc01864cd)
62 ioctlEventFD = uintptr(0xc01864cf)
63 importSyncFile = 1
64 exportSyncFile = 1
65)
66
67// Node owns the DRM render fd. Only open the render node of the Vulkan device
68// matched to linux-dmabuf main_device. Never substitute a different GPU.
69type Node struct{ fd int }
70
71func Open(path string) (*Node, error) {
72 fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
73 if err != nil {
74 return nil, fmt.Errorf("open matching render node %s: %w", path, err)
75 }
76 return &Node{fd: fd}, nil
77}
78
79func (n *Node) Close() error {
80 if n.fd < 0 {
81 return nil
82 }
83 err := unix.Close(n.fd)
84 n.fd = -1
85 return err
86}
87
88// ioctlErrno performs the ioctl and returns the raw errno without allocating.
89// Embedded addresses must not move: no split-checked call may occur between
90// encoding them and the syscall. KeepAlive alone does not prevent stack moves.
91//
92//go:nosplit
93func (n *Node) ioctlErrno(request uintptr, arg unsafe.Pointer) unix.Errno {
94 _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(n.fd), request, uintptr(arg))
95 return errno
96}
97
98// ioctlError adds request context to a real errno. Only failure paths pay for it.
99func ioctlError(request uintptr, errno unix.Errno) error {
100 return fmt.Errorf("DRM syncobj ioctl %#x: %w", request, errno)
101}
102
103func (n *Node) ioctl(request uintptr, arg unsafe.Pointer) error {
104 if errno := n.ioctlErrno(request, arg); errno != 0 {
105 return ioctlError(request, errno)
106 }
107 return nil
108}
109
110func (n *Node) Create() (uint32, error) {
111 var a createArg
112 if err := n.ioctl(ioctlCreate, unsafe.Pointer(&a)); err != nil {
113 return 0, err
114 }
115 return a.Handle, nil
116}
117func (n *Node) Destroy(handle uint32) error {
118 return n.ioctl(ioctlDestroy, unsafe.Pointer(&destroyArg{Handle: handle}))
119}
120
121// ExportTimeline returns a new owned syncobj FD (not a sync_file). The
122// caller owns it and closes it, or passes a duplicate to its Wayland client.
123func (n *Node) ExportTimeline(handle uint32) (int, error) {
124 a := handleArg{Handle: handle, FD: -1}
125 if err := n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
126 return -1, err
127 }
128 return int(a.FD), nil
129}
130
131// EventFD arms efd, an eventfd owned by the caller, to become readable when
132// point of the timeline behind handle is signaled. The point need not be
133// submitted yet. The registration is one-shot: re-arm after every use. The
134// call returns at once and never waits.
135func (n *Node) EventFD(handle uint32, point uint64, efd int) error {
136 a := eventFDArg{Handle: handle, Point: point, FD: int32(efd)}
137 return n.ioctl(ioctlEventFD, unsafe.Pointer(&a))
138}
139
140// Signal signals one timeline point from the CPU (DRM_IOCTL_SYNCOBJ_TIMELINE_SIGNAL).
141// The compositor does this through its release timeline; clients use it only
142// to simulate that in tests.
143func (n *Node) Signal(handle uint32, point uint64) error {
144 h, p := handle, point
145 a := queryArg{Handles: uint64(uintptr(unsafe.Pointer(&h))), Points: uint64(uintptr(unsafe.Pointer(&p))), Count: 1}
146 errno := n.ioctlErrno(ioctlTimelineSig, unsafe.Pointer(&a))
147 runtime.KeepAlive(h)
148 runtime.KeepAlive(p)
149 if errno != 0 {
150 return ioctlError(ioctlTimelineSig, errno)
151 }
152 return nil
153}
154
155// ImportFence consumes syncFile only after FD_TO_HANDLE imports it into a
156// pre-created destination syncobj. With IMPORT_SYNC_FILE, Handle is an input,
157// not an output: asking the kernel to import into handle zero returns ENOENT.
158// The caller retains syncFile if the ioctl rejects it.
159func (n *Node) ImportFence(syncFile int) (uint32, error) {
160 handle, err := n.Create()
161 if err != nil {
162 return 0, err
163 }
164 a := handleArg{Handle: handle, Flags: importSyncFile, FD: int32(syncFile)}
165 if err = n.ioctl(ioctlFDToHandle, unsafe.Pointer(&a)); err != nil {
166 _ = n.Destroy(handle)
167 return 0, err
168 }
169 if err = unix.Close(syncFile); err != nil {
170 _ = n.Destroy(handle)
171 return 0, err
172 }
173 return handle, nil
174}
175
176func (n *Node) Transfer(src uint32, srcPoint uint64, dst uint32, dstPoint uint64) error {
177 a := transferArg{Src: src, Dst: dst, SrcPoint: srcPoint, DstPoint: dstPoint}
178 return n.ioctl(ioctlTransfer, unsafe.Pointer(&a))
179}
180
181// AcquirePoint transfers the Vulkan submission's exported SYNC_FD into the
182// persistent acquire timeline. The caller retains syncFile on import error.
183func (n *Node) AcquirePoint(syncFile int, timeline uint32, point uint64) error {
184 tmp, err := n.ImportFence(syncFile)
185 if err != nil {
186 return err
187 }
188 defer n.Destroy(tmp)
189 return n.Transfer(tmp, 0, timeline, point)
190}
191
192// ReleaseFence exports a compositor release point as a sync_file suitable for
193// TEMPORARY Vulkan semaphore import. The caller owns the returned FD until
194// Vulkan accepts the import; failed imports must close it.
195func (n *Node) ReleaseFence(timeline uint32, point uint64) (int, error) {
196 tmp, err := n.Create()
197 if err != nil {
198 return -1, err
199 }
200 defer n.Destroy(tmp)
201 if err = n.Transfer(timeline, point, tmp, 0); err != nil {
202 return -1, err
203 }
204 a := handleArg{Handle: tmp, Flags: exportSyncFile, FD: -1}
205 if err = n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
206 return -1, err
207 }
208 return int(a.FD), nil
209}
210
211// WaitPoint waits for a submitted release point for at most timeout. Call it
212// only off the render path: it can block for up to timeout. WAIT_FOR_SUBMIT
213// handles the interval before the compositor attaches its fence to the point.
214func (n *Node) WaitPoint(handle uint32, point uint64, timeout time.Duration) (bool, error) {
215 if timeout < 0 || timeout > time.Second {
216 return false, fmt.Errorf("invalid syncobj wait timeout %s", timeout)
217 }
218 h := handle
219 p := point
220 var now unix.Timespec
221 if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
222 return false, err
223 }
224 deadline := now.Nano() + timeout.Nanoseconds()
225 a := timelineWaitArg{Handles: uint64(uintptr(unsafe.Pointer(&h))), Points: uint64(uintptr(unsafe.Pointer(&p))), TimeoutNS: deadline, Count: 1, Flags: 2}
226 errno := n.ioctlErrno(ioctlTimelineWait, unsafe.Pointer(&a))
227 runtime.KeepAlive(h)
228 runtime.KeepAlive(p)
229 return waitResult(errno)
230}
231
232// waitResult maps a TIMELINE_WAIT errno. ETIME and EBUSY are the expected idle
233// timeout outcomes and build no error value; anything else keeps its context.
234func waitResult(errno unix.Errno) (bool, error) {
235 switch errno {
236 case 0:
237 return true, nil
238 case unix.ETIME, unix.EBUSY:
239 return false, nil
240 default:
241 return false, ioctlError(ioctlTimelineWait, errno)
242 }
243}
244
245// Signaled performs a zero-time wait for tests and shutdown diagnostics.
246func (n *Node) Signaled(handle uint32, point uint64) (bool, error) {
247 return n.WaitPoint(handle, point, 0)
248}
249
250// Query returns the last signaled point. This is diagnostic only: timeline
251// wait, not an optimistic query, authorizes release.
252func (n *Node) Query(handle uint32) (uint64, error) {
253 h := handle
254 var point uint64
255 a := queryArg{Handles: uint64(uintptr(unsafe.Pointer(&h))), Points: uint64(uintptr(unsafe.Pointer(&point))), Count: 1}
256 errno := n.ioctlErrno(ioctlQuery, unsafe.Pointer(&a))
257 runtime.KeepAlive(h)
258 runtime.KeepAlive(point)
259 if errno != 0 {
260 return 0, ioctlError(ioctlQuery, errno)
261 }
262 return point, nil
263}