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