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}