v0.1.0 d9218d6

atomic.go RawHistoryFolder

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

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