v0.1.0 47aaa0f

syncobj.go RawHistoryFolder

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}

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