atomic.go RawHistoryFolder

1package drm
2
3import (
4	"encoding/binary"
5	"errors"
6	"os"
7	"runtime"
8	"slices"
9	"sync"
10	"time"
11	"unsafe"
12
13	"github.com/bnema/neferwl/internal/ports"
14)
15
16// Atomic KMS (ADR 015): every change on screen is one atomic commit. A
17// frame commit flips the primary plane, moves the cursor plane and sets
18// VRR together; the kernel waits for the frame's GPU fence (IN_FENCE_FD)
19// before flipping, so no thread waits for the GPU or a vblank.
20
21const (
22	ioctlSetClientCap = 0x4010640D // DRM_IOW('d', 0x0D, struct drm_set_client_cap)
23	ioctlGetPlaneRes  = 0xC01064B5 // DRM_IOWR('d', 0xB5, struct drm_mode_get_plane_res)
24	ioctlGetPlane     = 0xC02064B6 // DRM_IOWR('d', 0xB6, struct drm_mode_get_plane)
25	ioctlGetPropBlob  = 0xC01064AC // DRM_IOWR('d', 0xAC, struct drm_mode_get_blob)
26	ioctlAtomic       = 0xC03864BC // DRM_IOWR('d', 0xBC, struct drm_mode_atomic)
27	ioctlCreateBlob   = 0xC01064BD // DRM_IOWR('d', 0xBD, struct drm_mode_create_blob)
28	ioctlDestroyBlob  = 0xC00464BE // DRM_IOWR('d', 0xBE, struct drm_mode_destroy_blob)
29
30	clientCapUniversalPlanes = 2
31	clientCapAtomic          = 3
32	capTimestampMonotonic    = 0x6
33	capAtomicAsync           = 0x15
34
35	objPlane = 0xeeeeeeee
36
37	flipEventFlag    = 0x1   // DRM_MODE_PAGE_FLIP_EVENT
38	flipAsyncFlag    = 0x2   // DRM_MODE_PAGE_FLIP_ASYNC
39	atomicTestOnly   = 0x100 // DRM_MODE_ATOMIC_TEST_ONLY
40	atomicNonblock   = 0x200 // DRM_MODE_ATOMIC_NONBLOCK
41	atomicAllowModes = 0x400 // DRM_MODE_ATOMIC_ALLOW_MODESET
42
43	planeOverlay = 0
44	planePrimary = 1
45	planeCursor  = 2
46)
47
48type setClientCap struct{ capability, value uint64 }
49
50type getPlaneRes struct {
51	planeIDs uint64
52	count, _ uint32
53}
54
55type getPlane struct {
56	planeID, crtcID, fbID, possibleCrtcs, gammaSize, countFormats uint32
57	formats                                                       uint64
58}
59
60type getBlob struct {
61	blobID, length uint32
62	data           uint64
63}
64
65type createBlob struct {
66	data           uint64
67	length, blobID uint32
68}
69
70type modeAtomic struct {
71	flags, countObjs                                    uint32
72	objs, countProps, props, values, reserved, userData uint64
73}
74
75// kms is the kernel side of the output path. kmsDevice is the real one;
76// tests use its Mockery mock.
77type kms interface {
78	// commit issues DRM_IOCTL_MODE_ATOMIC; userData comes back in the flip event.
79	commit(req *atomicReq, flags uint32, userData uint64) error
80	createBlob(data []byte) (uint32, error)
81	destroyBlob(id uint32) error
82	getBlob(id uint32) ([]byte, error)
83	// planes lists every plane with the CRTCs it can show on.
84	planes() ([]planeRes, error)
85	objProps(obj, typ uint32) (map[string][2]uint64, error)
86	// addFB imports a single-plane dmabuf as a framebuffer of format.
87	addFB(b *ports.DMABuf, format uint32) (uint32, error)
88	rmFB(id uint32) error
89	createLease(objects []uint32) (*os.File, uint32, error)
90	revokeLease(id uint32) error
91	resources() ([]uint32, []uint32, error)
92	connector(id uint32) (connector, error)
93	pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error)
94}
95
96// planeRes is one plane of the card: possible is a bitmask of CRTC indices.
97type planeRes struct {
98	id, possible uint32
99	// formats is the plane's format list (GETPLANE): used, all linear,
100	// when the plane has no IN_FORMATS (drivers without modifiers).
101	formats []uint32
102}
103
104// atomicReq collects (object, property, value) triples in insertion order.
105type atomicReq struct {
106	objs  []uint32
107	props [][]propValue
108}
109
110type propValue struct {
111	prop uint32
112	val  uint64
113}
114
115// set adds a property; unknown properties (id 0) are skipped.
116func (r *atomicReq) set(obj, prop uint32, v uint64) {
117	if prop == 0 || obj == 0 {
118		return
119	}
120	i := slices.Index(r.objs, obj)
121	if i < 0 {
122		r.objs = append(r.objs, obj)
123		r.props = append(r.props, nil)
124		i = len(r.objs) - 1
125	}
126	for j, p := range r.props[i] {
127		if p.prop == prop {
128			r.props[i][j].val = v
129			return
130		}
131	}
132	r.props[i] = append(r.props[i], propValue{prop, v})
133}
134
135// value returns a property set on an object.
136func (r *atomicReq) value(obj, prop uint32) (uint64, bool) {
137	i := slices.Index(r.objs, obj)
138	if i < 0 {
139		return 0, false
140	}
141	for _, p := range r.props[i] {
142		if p.prop == prop {
143			return p.val, true
144		}
145	}
146	return 0, false
147}
148
149// kmsDevice drives a card fd.
150type kmsDevice struct {
151	fd int
152	// gemMu serialises GEM handle import and close: handles are shared by
153	// every import of a buffer on the fd, and outputs run on their own
154	// goroutines. modifiers is DRM_CAP_ADDFB2_MODIFIERS.
155	gemMu     *sync.Mutex
156	modifiers bool
157}
158
159func (k kmsDevice) commit(req *atomicReq, flags uint32, userData uint64) error {
160	if len(req.objs) == 0 {
161		return errors.New("empty atomic commit")
162	}
163	counts := make([]uint32, len(req.objs))
164	var props []uint32
165	var vals []uint64
166	for i, ps := range req.props {
167		counts[i] = uint32(len(ps))
168		for _, p := range ps {
169			props = append(props, p.prop)
170			vals = append(vals, p.val)
171		}
172	}
173	a := modeAtomic{flags: flags, countObjs: uint32(len(req.objs)), userData: userData,
174		objs: uint64(uintptr(unsafe.Pointer(&req.objs[0]))), countProps: uint64(uintptr(unsafe.Pointer(&counts[0]))),
175		props: uint64(uintptr(unsafe.Pointer(&props[0]))), values: uint64(uintptr(unsafe.Pointer(&vals[0])))}
176	err := ioctl(k.fd, ioctlAtomic, unsafe.Pointer(&a))
177	runtime.KeepAlive(req)
178	runtime.KeepAlive(counts)
179	runtime.KeepAlive(props)
180	runtime.KeepAlive(vals)
181	return err
182}
183
184func (k kmsDevice) createBlob(data []byte) (uint32, error) {
185	b := createBlob{data: uint64(uintptr(unsafe.Pointer(&data[0]))), length: uint32(len(data))}
186	err := ioctl(k.fd, ioctlCreateBlob, unsafe.Pointer(&b))
187	runtime.KeepAlive(data)
188	return b.blobID, err
189}
190
191func (k kmsDevice) destroyBlob(id uint32) error {
192	v := id
193	return ioctl(k.fd, ioctlDestroyBlob, unsafe.Pointer(&v))
194}
195
196func (k kmsDevice) getBlob(id uint32) ([]byte, error) {
197	b := getBlob{blobID: id}
198	if err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b)); err != nil {
199		return nil, err
200	}
201	if b.length == 0 {
202		return nil, nil
203	}
204	data := make([]byte, b.length)
205	b.data = uint64(uintptr(unsafe.Pointer(&data[0])))
206	err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b))
207	runtime.KeepAlive(data)
208	return data[:min(int(b.length), len(data))], err
209}
210
211func (k kmsDevice) planes() ([]planeRes, error) {
212	var r getPlaneRes
213	if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil {
214		return nil, err
215	}
216	if r.count == 0 {
217		return nil, nil
218	}
219	ids := make([]uint32, r.count)
220	r.planeIDs = uint64(uintptr(unsafe.Pointer(&ids[0])))
221	if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil {
222		return nil, err
223	}
224	runtime.KeepAlive(ids)
225	var out []planeRes
226	for _, id := range ids[:min(int(r.count), len(ids))] {
227		p := getPlane{planeID: id}
228		if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil {
229			continue
230		}
231		res := planeRes{id: id, possible: p.possibleCrtcs}
232		if p.countFormats > 0 {
233			fs := make([]uint32, p.countFormats)
234			p.formats = uint64(uintptr(unsafe.Pointer(&fs[0])))
235			if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) == nil {
236				res.formats = fs[:min(int(p.countFormats), len(fs))]
237			}
238			runtime.KeepAlive(fs)
239		}
240		out = append(out, res)
241	}
242	return out, nil
243}
244
245func (k kmsDevice) objProps(obj, typ uint32) (map[string][2]uint64, error) {
246	return objProps(k.fd, obj, typ)
247}
248
249func (k kmsDevice) rmFB(id uint32) error {
250	v := id
251	return ioctl(k.fd, ioctlRmFB, unsafe.Pointer(&v))
252}
253
254// enableAtomic turns on atomic modesetting and checks the caps outputs rely on.
255func enableAtomic(fd int) error {
256	for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} {
257		v := setClientCap{capability: c, value: 1}
258		if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil {
259			return err
260		}
261	}
262	if !hasCap(fd, capTimestampMonotonic) {
263		return errors.New("flip timestamps are not CLOCK_MONOTONIC")
264	}
265	return nil
266}
267
268func hasCap(fd int, c uint64) bool {
269	v := getCap{capability: c}
270	return ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value == 1
271}
272
273// plane is a KMS plane an output drives, with its property IDs by name.
274type plane struct {
275	id, typ uint32
276	props   map[string]uint32
277	crtc    uint32 // CRTC_ID when read
278	formats []ports.DMABufFormat
279	// zposValue is the plane's zpos when read (its "zpos" property).
280	zposValue uint64
281}
282
283func (p *plane) prop(name string) uint32 {
284	if p == nil {
285		return 0
286	}
287	return p.props[name]
288}
289
290// readPlanes returns the planes that can show on the CRTC at index pipe.
291func readPlanes(k kms, pipe int) ([]*plane, error) {
292	res, err := k.planes()
293	if err != nil {
294		return nil, err
295	}
296	var out []*plane
297	for _, r := range res {
298		if r.possible&(1<<pipe) == 0 {
299			continue
300		}
301		props, err := k.objProps(r.id, objPlane)
302		if err != nil {
303			continue
304		}
305		p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1]), zposValue: props["zpos"][1]}
306		for name, v := range props {
307			p.props[name] = uint32(v[0])
308		}
309		if blob := uint32(props["IN_FORMATS"][1]); blob != 0 {
310			if data, err := k.getBlob(blob); err == nil {
311				p.formats = parseInFormats(data)
312			}
313		}
314		if p.formats == nil {
315			for _, f := range r.formats {
316				p.formats = append(p.formats, ports.DMABufFormat{Format: f})
317			}
318		}
319		out = append(out, p)
320	}
321	return out, nil
322}
323
324// parseInFormats decodes an IN_FORMATS blob (struct drm_format_modifier_blob)
325// into format and modifier pairs.
326func parseInFormats(b []byte) []ports.DMABufFormat {
327	if len(b) < 24 {
328		return nil
329	}
330	le := binary.LittleEndian
331	nf, fo := int(le.Uint32(b[8:])), int(le.Uint32(b[12:]))
332	nm, mo := int(le.Uint32(b[16:])), int(le.Uint32(b[20:]))
333	if fo < 0 || fo+nf*4 > len(b) || mo < 0 || mo+nm*24 > len(b) {
334		return nil
335	}
336	var out []ports.DMABufFormat
337	for m := range nm {
338		e := b[mo+m*24:]
339		mask, offset, mod := le.Uint64(e), int(le.Uint32(e[8:])), le.Uint64(e[16:])
340		for bit := range 64 {
341			i := offset + bit
342			if mask&(1<<bit) == 0 || i >= nf {
343				continue
344			}
345			out = append(out, ports.DMABufFormat{Format: le.Uint32(b[fo+i*4:]), Modifier: mod})
346		}
347	}
348	return out
349}
350
351// flipEvent is a completed commit on a CRTC: when is its CLOCK_MONOTONIC
352// time, seq the CRTC's vblank counter, user the commit's userData.
353type flipEvent struct {
354	crtc uint32
355	user uint64
356	when time.Duration
357	seq  uint32
358}
359
360// parseFlips parses DRM events read from the card fd.
361func parseFlips(buf []byte) []flipEvent {
362	var out []flipEvent
363	le := binary.LittleEndian
364	for len(buf) >= 8 {
365		typ := le.Uint32(buf)
366		length := int(le.Uint32(buf[4:]))
367		if length < 8 || length > len(buf) {
368			break
369		}
370		// struct drm_event_vblank: base(8) user_data(8) sec usec sequence crtc_id.
371		if typ == eventFlipDone && length >= 32 {
372			sec, usec := le.Uint32(buf[16:]), le.Uint32(buf[20:])
373			out = append(out, flipEvent{user: le.Uint64(buf[8:]), when: time.Duration(sec)*time.Second + time.Duration(usec)*time.Microsecond, seq: le.Uint32(buf[24:]), crtc: le.Uint32(buf[28:])})
374		}
375		buf = buf[length:]
376	}
377	return out
378}

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 23

Tags 10

↑↓ moveenter openesc close