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