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
44const (
45 ioctlCreate = uintptr(0xc00864bf)
46 ioctlDestroy = uintptr(0xc00864c0)
47 ioctlHandleToFD = uintptr(0xc01864c1)
48 ioctlFDToHandle = uintptr(0xc01864c2)
49 ioctlTimelineWait = uintptr(0xc03064ca)
50 ioctlQuery = uintptr(0xc01864cb)
51 ioctlTransfer = uintptr(0xc02064cc)
52 importSyncFile = 1
53 exportSyncFile = 1
54)
55
56// Node owns the DRM render fd. Only open the render node of the Vulkan device
57// matched to linux-dmabuf main_device. Never substitute a different GPU.
58type Node struct{ fd int }
59
60func Open(path string) (*Node, error) {
61 fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
62 if err != nil {
63 return nil, fmt.Errorf("open matching render node %s: %w", path, err)
64 }
65 return &Node{fd: fd}, nil
66}
67
68func (n *Node) Close() error {
69 if n.fd < 0 {
70 return nil
71 }
72 err := unix.Close(n.fd)
73 n.fd = -1
74 return err
75}
76
77// ioctlErrno performs the ioctl and returns the raw errno without allocating.
78// Embedded addresses must not move: no split-checked call may occur between
79// encoding them and the syscall. KeepAlive alone does not prevent stack moves.
80//
81//go:nosplit
82func (n *Node) ioctlErrno(request uintptr, arg unsafe.Pointer) unix.Errno {
83 _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(n.fd), request, uintptr(arg))
84 return errno
85}
86
87// ioctlError adds request context to a real errno. Only failure paths pay for it.
88func ioctlError(request uintptr, errno unix.Errno) error {
89 return fmt.Errorf("DRM syncobj ioctl %#x: %w", request, errno)
90}
91
92func (n *Node) ioctl(request uintptr, arg unsafe.Pointer) error {
93 if errno := n.ioctlErrno(request, arg); errno != 0 {
94 return ioctlError(request, errno)
95 }
96 return nil
97}
98
99func (n *Node) Create() (uint32, error) {
100 var a createArg
101 if err := n.ioctl(ioctlCreate, unsafe.Pointer(&a)); err != nil {
102 return 0, err
103 }
104 return a.Handle, nil
105}
106func (n *Node) Destroy(handle uint32) error {
107 return n.ioctl(ioctlDestroy, unsafe.Pointer(&destroyArg{Handle: handle}))
108}
109
110// ExportTimeline returns a new owned syncobj FD (not a sync_file). The caller
111// hands it to WLTurbo's ImportTimeline, which closes only after a successful
112// request send; on send failure the caller must close the FD.
113func (n *Node) ExportTimeline(handle uint32) (int, error) {
114 a := handleArg{Handle: handle, FD: -1}
115 if err := n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
116 return -1, err
117 }
118 return int(a.FD), nil
119}
120
121// 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,
123// not an output: asking the kernel to import into handle zero returns ENOENT.
124// The caller retains syncFile if the ioctl rejects it.
125func (n *Node) ImportFence(syncFile int) (uint32, error) {
126 handle, err := n.Create()
127 if err != nil {
128 return 0, err
129 }
130 a := handleArg{Handle: handle, Flags: importSyncFile, FD: int32(syncFile)}
131 if err = n.ioctl(ioctlFDToHandle, unsafe.Pointer(&a)); err != nil {
132 _ = n.Destroy(handle)
133 return 0, err
134 }
135 if err = unix.Close(syncFile); err != nil {
136 _ = n.Destroy(handle)
137 return 0, err
138 }
139 return handle, nil
140}
141
142func (n *Node) Transfer(src uint32, srcPoint uint64, dst uint32, dstPoint uint64) error {
143 a := transferArg{Src: src, Dst: dst, SrcPoint: srcPoint, DstPoint: dstPoint}
144 return n.ioctl(ioctlTransfer, unsafe.Pointer(&a))
145}
146
147// AcquirePoint transfers the Vulkan submission's exported SYNC_FD into the
148// persistent acquire timeline. The caller retains syncFile on import error.
149func (n *Node) AcquirePoint(syncFile int, timeline uint32, point uint64) error {
150 tmp, err := n.ImportFence(syncFile)
151 if err != nil {
152 return err
153 }
154 defer n.Destroy(tmp)
155 return n.Transfer(tmp, 0, timeline, point)
156}
157
158// ReleaseFence exports a compositor release point as a sync_file suitable for
159// TEMPORARY Vulkan semaphore import. The caller owns the returned FD until
160// Vulkan accepts the import; failed imports must close it.
161func (n *Node) ReleaseFence(timeline uint32, point uint64) (int, error) {
162 tmp, err := n.Create()
163 if err != nil {
164 return -1, err
165 }
166 defer n.Destroy(tmp)
167 if err = n.Transfer(timeline, point, tmp, 0); err != nil {
168 return -1, err
169 }
170 a := handleArg{Handle: tmp, Flags: exportSyncFile, FD: -1}
171 if err = n.ioctl(ioctlHandleToFD, unsafe.Pointer(&a)); err != nil {
172 return -1, err
173 }
174 return int(a.FD), nil
175}
176
177// WaitPoint waits for a submitted release point for at most timeout. Call it
178// only from a waiter goroutine, never from the presentation loop. WAIT_FOR_SUBMIT
179// handles the interval before the compositor attaches its fence to the point.
180func (n *Node) WaitPoint(handle uint32, point uint64, timeout time.Duration) (bool, error) {
181 if timeout < 0 || timeout > time.Second {
182 return false, fmt.Errorf("invalid syncobj wait timeout %s", timeout)
183 }
184 h := handle
185 p := point
186 var now unix.Timespec
187 if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
188 return false, err
189 }
190 deadline := now.Nano() + timeout.Nanoseconds()
191 a := timelineWaitArg{Handles: uint64(uintptr(unsafe.Pointer(&h))), Points: uint64(uintptr(unsafe.Pointer(&p))), TimeoutNS: deadline, Count: 1, Flags: 2}
192 errno := n.ioctlErrno(ioctlTimelineWait, unsafe.Pointer(&a))
193 runtime.KeepAlive(h)
194 runtime.KeepAlive(p)
195 return waitResult(errno)
196}
197
198// waitResult maps a TIMELINE_WAIT errno. ETIME and EBUSY are the expected idle
199// timeout outcomes and build no error value; anything else keeps its context.
200func waitResult(errno unix.Errno) (bool, error) {
201 switch errno {
202 case 0:
203 return true, nil
204 case unix.ETIME, unix.EBUSY:
205 return false, nil
206 default:
207 return false, ioctlError(ioctlTimelineWait, errno)
208 }
209}
210
211// Signaled performs a zero-time wait for tests and shutdown diagnostics.
212func (n *Node) Signaled(handle uint32, point uint64) (bool, error) {
213 return n.WaitPoint(handle, point, 0)
214}
215
216// Query returns the last signaled point. This is diagnostic only: timeline
217// wait, not an optimistic query, authorizes release.
218func (n *Node) Query(handle uint32) (uint64, error) {
219 h := handle
220 var point uint64
221 a := queryArg{Handles: uint64(uintptr(unsafe.Pointer(&h))), Points: uint64(uintptr(unsafe.Pointer(&point))), Count: 1}
222 errno := n.ioctlErrno(ioctlQuery, unsafe.Pointer(&a))
223 runtime.KeepAlive(h)
224 runtime.KeepAlive(point)
225 if errno != 0 {
226 return 0, ioctlError(ioctlQuery, errno)
227 }
228 return point, nil
229}