feat/data-dnd e05cb15

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

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