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}