main f2d166b

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	clientCapColorPipeline   = 7 // DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE
33	capTimestampMonotonic    = 0x6
34	capAtomicAsync           = 0x15
35
36	objPlane = 0xeeeeeeee
37
38	flipEventFlag    = 0x1   // DRM_MODE_PAGE_FLIP_EVENT
39	flipAsyncFlag    = 0x2   // DRM_MODE_PAGE_FLIP_ASYNC
40	atomicTestOnly   = 0x100 // DRM_MODE_ATOMIC_TEST_ONLY
41	atomicNonblock   = 0x200 // DRM_MODE_ATOMIC_NONBLOCK
42	atomicAllowModes = 0x400 // DRM_MODE_ATOMIC_ALLOW_MODESET
43
44	planeOverlay = 0
45	planePrimary = 1
46	planeCursor  = 2
47)
48
49type setClientCap struct{ capability, value uint64 }
50
51type getPlaneRes struct {
52	planeIDs uint64
53	count, _ uint32
54}
55
56type getPlane struct {
57	planeID, crtcID, fbID, possibleCrtcs, gammaSize, countFormats uint32
58	formats                                                       uint64
59}
60
61type getBlob struct {
62	blobID, length uint32
63	data           uint64
64}
65
66type createBlob struct {
67	data           uint64
68	length, blobID uint32
69}
70
71type modeAtomic struct {
72	flags, countObjs                                    uint32
73	objs, countProps, props, values, reserved, userData uint64
74}
75
76// kms is the kernel side of the output path. kmsDevice is the real one;
77// tests use its Mockery mock.
78type kms interface {
79	// commit issues DRM_IOCTL_MODE_ATOMIC; userData comes back in the flip event.
80	commit(req *atomicReq, flags uint32, userData uint64) error
81	createBlob(data []byte) (uint32, error)
82	destroyBlob(id uint32) error
83	getBlob(id uint32) ([]byte, error)
84	// planes lists every plane with the CRTCs it can show on.
85	planes() ([]planeRes, error)
86	objProps(obj, typ uint32) (map[string][2]uint64, error)
87	// propEnums returns the enum names of a property with their values.
88	propEnums(prop uint32) (map[string]uint64, error)
89	// addFB imports a single-plane dmabuf as a framebuffer of format.
90	addFB(b *ports.DMABuf, format uint32) (uint32, error)
91	rmFB(id uint32) error
92	// createDumb allocates a width×height 32 bpp dumb buffer; mapDumb maps it
93	// for the CPU (read/write) and unmapDumb releases the mapping;
94	// addFBHandle makes a framebuffer of format from it; destroyDumb frees
95	// it. The framebuffer is removed with rmFB first.
96	createDumb(width, height int) (dumbBuffer, error)
97	mapDumb(b dumbBuffer) ([]byte, error)
98	unmapDumb(mem []byte) error
99	addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error)
100	destroyDumb(handle uint32) error
101	createLease(objects []uint32) (*os.File, uint32, error)
102	revokeLease(id uint32) error
103	resources() ([]uint32, []uint32, error)
104	connector(id uint32) (connector, error)
105	pickCrtc(c connector, crtcs []uint32, used map[uint32]bool) (uint32, error)
106}
107
108// planeRes is one plane of the card: possible is a bitmask of CRTC indices.
109type planeRes struct {
110	id, possible uint32
111	// formats is the plane's format list (GETPLANE): used, all linear,
112	// when the plane has no IN_FORMATS (drivers without modifiers).
113	formats []uint32
114}
115
116// atomicReq collects (object, property, value) triples in insertion order.
117// A request is owned by one goroutine and reused through reset: its slices,
118// and the flat scratch kmsDevice.commit fills for the ioctl, keep their
119// capacity, so steady-state commits allocate nothing. The scratch lives on
120// the request, never on the shared kms device.
121type atomicReq struct {
122	objs  []uint32
123	props [][]propValue
124	// counts, flatProps and flatVals are the ioctl arrays commit builds.
125	counts    []uint32
126	flatProps []uint32
127	flatVals  []uint64
128}
129
130// reset empties the request, keeping every buffer's capacity.
131func (r *atomicReq) reset() {
132	r.objs = r.objs[:0]
133	r.props = r.props[:0]
134}
135
136type propValue struct {
137	prop uint32
138	val  uint64
139}
140
141// set adds a property; unknown properties (id 0) are skipped.
142func (r *atomicReq) set(obj, prop uint32, v uint64) {
143	if prop == 0 || obj == 0 {
144		return
145	}
146	i := slices.Index(r.objs, obj)
147	if i < 0 {
148		r.objs = append(r.objs, obj)
149		if n := len(r.props); n < cap(r.props) {
150			// Reuse the property list kept by reset.
151			r.props = r.props[:n+1]
152			r.props[n] = r.props[n][:0]
153		} else {
154			r.props = append(r.props, nil)
155		}
156		i = len(r.objs) - 1
157	}
158	for j, p := range r.props[i] {
159		if p.prop == prop {
160			r.props[i][j].val = v
161			return
162		}
163	}
164	r.props[i] = append(r.props[i], propValue{prop, v})
165}
166
167// value returns a property set on an object.
168func (r *atomicReq) value(obj, prop uint32) (uint64, bool) {
169	i := slices.Index(r.objs, obj)
170	if i < 0 {
171		return 0, false
172	}
173	for _, p := range r.props[i] {
174		if p.prop == prop {
175			return p.val, true
176		}
177	}
178	return 0, false
179}
180
181// kmsDevice drives a card fd.
182type kmsDevice struct {
183	fd int
184	// gemMu serialises GEM handle import and close: handles are shared by
185	// every import of a buffer on the fd, and outputs run on their own
186	// goroutines. modifiers is DRM_CAP_ADDFB2_MODIFIERS.
187	gemMu     *sync.Mutex
188	modifiers bool
189}
190
191func (k kmsDevice) commit(req *atomicReq, flags uint32, userData uint64) error {
192	if len(req.objs) == 0 {
193		return errors.New("empty atomic commit")
194	}
195	req.counts, req.flatProps, req.flatVals = req.counts[:0], req.flatProps[:0], req.flatVals[:0]
196	for _, ps := range req.props {
197		req.counts = append(req.counts, uint32(len(ps)))
198		for _, p := range ps {
199			req.flatProps = append(req.flatProps, p.prop)
200			req.flatVals = append(req.flatVals, p.val)
201		}
202	}
203	counts, props, vals := req.counts, req.flatProps, req.flatVals
204	a := modeAtomic{flags: flags, countObjs: uint32(len(req.objs)), userData: userData,
205		objs: uint64(uintptr(unsafe.Pointer(&req.objs[0]))), countProps: uint64(uintptr(unsafe.Pointer(&counts[0]))),
206		props: uint64(uintptr(unsafe.Pointer(&props[0]))), values: uint64(uintptr(unsafe.Pointer(&vals[0])))}
207	err := ioctl(k.fd, ioctlAtomic, unsafe.Pointer(&a))
208	runtime.KeepAlive(req)
209	runtime.KeepAlive(counts)
210	runtime.KeepAlive(props)
211	runtime.KeepAlive(vals)
212	return err
213}
214
215func (k kmsDevice) createBlob(data []byte) (uint32, error) {
216	b := createBlob{data: uint64(uintptr(unsafe.Pointer(&data[0]))), length: uint32(len(data))}
217	err := ioctl(k.fd, ioctlCreateBlob, unsafe.Pointer(&b))
218	runtime.KeepAlive(data)
219	return b.blobID, err
220}
221
222func (k kmsDevice) destroyBlob(id uint32) error {
223	v := id
224	return ioctl(k.fd, ioctlDestroyBlob, unsafe.Pointer(&v))
225}
226
227func (k kmsDevice) getBlob(id uint32) ([]byte, error) {
228	b := getBlob{blobID: id}
229	if err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b)); err != nil {
230		return nil, err
231	}
232	if b.length == 0 {
233		return nil, nil
234	}
235	data := make([]byte, b.length)
236	b.data = uint64(uintptr(unsafe.Pointer(&data[0])))
237	err := ioctl(k.fd, ioctlGetPropBlob, unsafe.Pointer(&b))
238	runtime.KeepAlive(data)
239	return data[:min(int(b.length), len(data))], err
240}
241
242func (k kmsDevice) planes() ([]planeRes, error) {
243	var r getPlaneRes
244	if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil {
245		return nil, err
246	}
247	if r.count == 0 {
248		return nil, nil
249	}
250	ids := make([]uint32, r.count)
251	r.planeIDs = uint64(uintptr(unsafe.Pointer(&ids[0])))
252	if err := ioctl(k.fd, ioctlGetPlaneRes, unsafe.Pointer(&r)); err != nil {
253		return nil, err
254	}
255	runtime.KeepAlive(ids)
256	var out []planeRes
257	for _, id := range ids[:min(int(r.count), len(ids))] {
258		p := getPlane{planeID: id}
259		if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil {
260			continue
261		}
262		res := planeRes{id: id, possible: p.possibleCrtcs}
263		if p.countFormats > 0 {
264			fs := make([]uint32, p.countFormats)
265			p.formats = uint64(uintptr(unsafe.Pointer(&fs[0])))
266			if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) == nil {
267				res.formats = fs[:min(int(p.countFormats), len(fs))]
268			}
269			runtime.KeepAlive(fs)
270		}
271		out = append(out, res)
272	}
273	return out, nil
274}
275
276func (k kmsDevice) objProps(obj, typ uint32) (map[string][2]uint64, error) {
277	return objProps(k.fd, obj, typ)
278}
279
280func (k kmsDevice) propEnums(prop uint32) (map[string]uint64, error) {
281	return propEnums(k.fd, prop)
282}
283
284func (k kmsDevice) rmFB(id uint32) error {
285	v := id
286	return ioctl(k.fd, ioctlRmFB, unsafe.Pointer(&v))
287}
288
289// enableAtomic turns on atomic modesetting and checks the caps outputs rely on.
290func enableAtomic(fd int) error {
291	for _, c := range []uint64{clientCapUniversalPlanes, clientCapAtomic} {
292		v := setClientCap{capability: c, value: 1}
293		if err := ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v)); err != nil {
294			return err
295		}
296	}
297	if !hasCap(fd, capTimestampMonotonic) {
298		return errors.New("flip timestamps are not CLOCK_MONOTONIC")
299	}
300	return nil
301}
302
303// enableColorPipeline asks for the plane COLOR_PIPELINE property and the
304// colorop objects. It must run after enableAtomic and before any plane or
305// property read: the property set differs per fd. Failure leaves the
306// feature off. With the cap on, the kernel rejects COLOR_ENCODING and
307// COLOR_RANGE on planes (they are replaced by the pipeline), so a YUV buffer
308// can never be put on a plane: planeColor refuses YUV, and every YUV format
309// is composed.
310func enableColorPipeline(fd int) error {
311	v := setClientCap{capability: clientCapColorPipeline, value: 1}
312	return ioctl(fd, ioctlSetClientCap, unsafe.Pointer(&v))
313}
314
315func hasCap(fd int, c uint64) bool {
316	v := getCap{capability: c}
317	return ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value == 1
318}
319
320// plane is a KMS plane an output drives, with its property IDs by name.
321type plane struct {
322	id, typ uint32
323	props   map[string]uint32
324	crtc    uint32 // CRTC_ID when read
325	formats []ports.DMABufFormat
326	// pipeline is the colour pipeline the plane offers for SDR content on
327	// an HDR output (colorop.go); nil: none. colorApplied is the mode the
328	// plane was last committed with, valid while colorKnown.
329	pipeline     *colorPipeline
330	colorApplied colorMode
331	colorKnown   bool
332	colorFormat  uint32 // format of the buffer shown with colorApplied
333	// verdicts are KMS's cached TEST_ONLY answers for the pipeline.
334	verdicts []colorVerdict
335	// zposValue is the plane's zpos when read (its "zpos" property).
336	zposValue uint64
337}
338
339func (p *plane) prop(name string) uint32 {
340	if p == nil {
341		return 0
342	}
343	return p.props[name]
344}
345
346// readPlanes returns the planes that can show on the CRTC at index pipe.
347func readPlanes(k kms, pipe int) ([]*plane, error) {
348	res, err := k.planes()
349	if err != nil {
350		return nil, err
351	}
352	var out []*plane
353	for _, r := range res {
354		if r.possible&(1<<pipe) == 0 {
355			continue
356		}
357		props, err := k.objProps(r.id, objPlane)
358		if err != nil {
359			continue
360		}
361		p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1]), zposValue: props["zpos"][1]}
362		for name, v := range props {
363			p.props[name] = uint32(v[0])
364		}
365		if blob := uint32(props["IN_FORMATS"][1]); blob != 0 {
366			if data, err := k.getBlob(blob); err == nil {
367				p.formats = parseInFormats(data)
368			}
369		}
370		if p.formats == nil {
371			for _, f := range r.formats {
372				p.formats = append(p.formats, ports.DMABufFormat{Format: f})
373			}
374		}
375		out = append(out, p)
376	}
377	return out, nil
378}
379
380// parseInFormats decodes an IN_FORMATS blob (struct drm_format_modifier_blob)
381// into format and modifier pairs.
382func parseInFormats(b []byte) []ports.DMABufFormat {
383	if len(b) < 24 {
384		return nil
385	}
386	le := binary.LittleEndian
387	nf, fo := int(le.Uint32(b[8:])), int(le.Uint32(b[12:]))
388	nm, mo := int(le.Uint32(b[16:])), int(le.Uint32(b[20:]))
389	if fo < 0 || fo+nf*4 > len(b) || mo < 0 || mo+nm*24 > len(b) {
390		return nil
391	}
392	var out []ports.DMABufFormat
393	for m := range nm {
394		e := b[mo+m*24:]
395		mask, offset, mod := le.Uint64(e), int(le.Uint32(e[8:])), le.Uint64(e[16:])
396		for bit := range 64 {
397			i := offset + bit
398			if mask&(1<<bit) == 0 || i >= nf {
399				continue
400			}
401			out = append(out, ports.DMABufFormat{Format: le.Uint32(b[fo+i*4:]), Modifier: mod})
402		}
403	}
404	return out
405}
406
407// flipEvent is a completed commit on a CRTC: when is its CLOCK_MONOTONIC
408// time, seq the CRTC's vblank counter, user the commit's userData.
409type flipEvent struct {
410	crtc uint32
411	user uint64
412	when time.Duration
413	seq  uint32
414}
415
416// parseFlips appends the flip events of buf, DRM events read from the card
417// fd, to out (pass out[:0] to reuse its storage) and returns it.
418func parseFlips(out []flipEvent, buf []byte) []flipEvent {
419	le := binary.LittleEndian
420	for len(buf) >= 8 {
421		typ := le.Uint32(buf)
422		length := int(le.Uint32(buf[4:]))
423		if length < 8 || length > len(buf) {
424			break
425		}
426		// struct drm_event_vblank: base(8) user_data(8) sec usec sequence crtc_id.
427		if typ == eventFlipDone && length >= 32 {
428			sec, usec := le.Uint32(buf[16:]), le.Uint32(buf[20:])
429			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:])})
430		}
431		buf = buf[length:]
432	}
433	return out
434}

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 25

Tags 10

↑↓ moveenter openesc close