1// Package drm drives KMS outputs with raw ioctls and atomic commits.
2package drm
3
4import (
5 "fmt"
6 "math"
7 "time"
8 "unsafe"
9
10 "github.com/bnema/neferwl/internal/ports"
11 "golang.org/x/sys/unix"
12)
13
14// ioctl numbers for amd64/arm64 (DRM_IOWR('d', nr, size)).
15const (
16 ioctlGetResources = 0xC04064A0
17 ioctlGetCrtc = 0xC06864A1
18 ioctlGetEncoder = 0xC01464A6
19 ioctlGetConnector = 0xC05064A7
20 ioctlRmFB = 0xC00464AF
21
22 eventFlipDone = 2
23 modeTypePrefered = 1 << 3
24 connected = 1
25)
26
27type cardRes struct {
28 fbs, crtcs, connectors, encoders uint64
29 countFbs, countCrtcs, countConns, countEncoders uint32
30 minW, maxW, minH, maxH uint32
31}
32
33type modeInfo struct {
34 Clock uint32
35 HDisplay, HSyncStart, HSyncEnd, HTotal uint16
36 HSkew uint16
37 VDisplay, VSyncStart, VSyncEnd, VTotal uint16
38 VScan uint16
39 VRefresh, Flags, Type uint32
40 Name [32]byte
41}
42
43func (m modeInfo) String() string {
44 return fmt.Sprintf("%dx%d@%.3f", m.HDisplay, m.VDisplay, float64(m.refreshMilli())/1000)
45}
46
47type getConnector struct {
48 encoders, modes, props, propValues uint64
49 countModes, countProps, countEncoders uint32
50 encoderID, connectorID, connType, typeID uint32
51 connection, mmW, mmH, subpixel uint32
52 _ uint32
53}
54
55type getEncoder struct{ encoderID, encoderType, crtcID, possibleCrtcs, possibleClones uint32 }
56
57type modeCrtc struct {
58 // unsafe.Pointer (not uint64) so the GC keeps it valid if the stack moves.
59 setConnectors unsafe.Pointer
60 countConnectors uint32
61 crtcID, fbID uint32
62 x, y uint32
63 gammaSize uint32
64 modeValid uint32
65 mode modeInfo
66}
67
68func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
69 for {
70 _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
71 if e == unix.EINTR || e == unix.EAGAIN {
72 continue
73 }
74 if e != 0 {
75 return e
76 }
77 return nil
78 }
79}
80
81// connector is the subset of KMS connector state used to pick an output.
82type connector struct {
83 id, encoderID uint32
84 name string
85 connected bool
86 nonDesktop bool
87 mmW, mmH int
88 modes []modeInfo
89 encoders []uint32
90}
91
92var connectorTypes = map[uint32]string{1: "VGA", 2: "DVI-I", 3: "DVI-D", 10: "DP", 11: "HDMI-A", 12: "HDMI-B", 7: "LVDS", 14: "eDP", 15: "Virtual", 16: "DSI", 17: "DPI", 20: "USB"}
93
94// Want configures connectors; see ports.OutputConfig.
95type Want struct {
96 Disabled map[string]bool
97 // Modes maps a connector to W, H and Hz (0: highest refresh at W x H).
98 Modes map[string][3]float64
99 // HDR settings are retained per connector; phase 1 never applies them to KMS.
100 HDR map[string]HDRSettings
101 // NoScanout disables direct scanout (render.direct-scanout = off).
102 NoScanout bool
103 // NoTearing ignores tearing requests (render.tearing = off).
104 NoTearing bool
105 // NoVRR keeps variable refresh off (render.vrr = off).
106 NoVRR bool
107 // Sampled are the dmabuf formats the renderer composes: direct
108 // scanout is offered only for those, so a refused buffer still draws.
109 Sampled []ports.DMABufFormat
110 // Device is the KMS device (dev_t) offered in scanout tranches.
111 Device uint64
112 // TraceFlips logs each frame and commit completion with its timing
113 // (--debug=drm-flip).
114 TraceFlips bool
115 // VRRFlipGap delays a game frame after the previous flip under VRR
116 // (render.vrr-flip-gap, 0: off; see vrr_flip_gap.go).
117 VRRFlipGap time.Duration
118}
119
120// HDRSettings is the requested HDR state and SDR reference white (nits).
121type HDRSettings struct {
122 Enabled bool
123 SDRBrightness int
124}
125
126// usable reports whether a connector should be driven.
127func (w Want) usable(c connector) bool {
128 return c.connected && !c.nonDesktop && len(c.modes) > 0 && !w.Disabled[c.name]
129}
130
131// refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.
132func (m modeInfo) refreshMilli() int {
133 if m.HTotal == 0 || m.VTotal == 0 {
134 return int(m.VRefresh) * 1000
135 }
136 r := (int64(m.Clock)*1_000_000/int64(m.HTotal) + int64(m.VTotal)/2) / int64(m.VTotal)
137 if m.Flags&(1<<4) != 0 { // interlace
138 r *= 2
139 }
140 if m.Flags&(1<<5) != 0 { // doublescan
141 r /= 2
142 }
143 if m.VScan > 1 {
144 r /= int64(m.VScan)
145 }
146 return int(r)
147}
148
149// pickMode returns the configured mode of the connector, else its preferred
150// one, else the first.
151func (w Want) pickMode(c connector) modeInfo {
152 if m, ok := w.Modes[c.name]; ok {
153 if mode, ok := pickMode(c.modes, int(m[0]), int(m[1]), m[2]); ok {
154 return mode
155 }
156 }
157 for _, m := range c.modes {
158 if m.Type&modeTypePrefered != 0 {
159 return m
160 }
161 }
162 return c.modes[0]
163}
164
165// pickMode finds W x H with the refresh closest to hz, or the highest when hz is 0.
166func pickMode(modes []modeInfo, w, h int, hz float64) (modeInfo, bool) {
167 best, found := modeInfo{}, false
168 score := func(m modeInfo) float64 {
169 r := float64(m.refreshMilli()) / 1000
170 if hz == 0 {
171 return -r
172 }
173 return math.Abs(r - hz)
174 }
175 for _, m := range modes {
176 if int(m.HDisplay) != w || int(m.VDisplay) != h || m.Flags&(1<<4) != 0 {
177 continue
178 }
179 if !found || score(m) < score(best) {
180 best, found = m, true
181 }
182 }
183 return best, found
184}
185
186func resources(fd int) (crtcs, conns []uint32, err error) {
187 var r cardRes
188 if err := ioctl(fd, ioctlGetResources, unsafe.Pointer(&r)); err != nil {
189 return nil, nil, fmt.Errorf("get resources: %w", err)
190 }
191 crtcs = make([]uint32, r.countCrtcs)
192 conns = make([]uint32, r.countConns)
193 r = cardRes{countCrtcs: r.countCrtcs, countConns: r.countConns}
194 if len(crtcs) > 0 {
195 r.crtcs = uint64(uintptr(unsafe.Pointer(&crtcs[0])))
196 }
197 if len(conns) > 0 {
198 r.connectors = uint64(uintptr(unsafe.Pointer(&conns[0])))
199 }
200 if err := ioctl(fd, ioctlGetResources, unsafe.Pointer(&r)); err != nil {
201 return nil, nil, fmt.Errorf("get resources: %w", err)
202 }
203 return crtcs[:min(int(r.countCrtcs), len(crtcs))], conns[:min(int(r.countConns), len(conns))], nil
204}
205
206func readConnector(fd int, id uint32) (connector, error) {
207 g := getConnector{connectorID: id}
208 if err := ioctl(fd, ioctlGetConnector, unsafe.Pointer(&g)); err != nil {
209 return connector{}, err
210 }
211 modes := make([]modeInfo, g.countModes)
212 encs := make([]uint32, g.countEncoders)
213 g2 := getConnector{connectorID: id, countModes: g.countModes, countEncoders: g.countEncoders}
214 if len(modes) > 0 {
215 g2.modes = uint64(uintptr(unsafe.Pointer(&modes[0])))
216 }
217 if len(encs) > 0 {
218 g2.encoders = uint64(uintptr(unsafe.Pointer(&encs[0])))
219 }
220 if err := ioctl(fd, ioctlGetConnector, unsafe.Pointer(&g2)); err != nil {
221 return connector{}, err
222 }
223 name := connectorTypes[g2.connType]
224 if name == "" {
225 name = "Unknown"
226 }
227 props, err := objProps(fd, id, objConnector)
228 if err != nil {
229 return connector{}, fmt.Errorf("connector %d properties: %w", id, err)
230 }
231 return connector{
232 id: id, encoderID: g2.encoderID, name: fmt.Sprintf("%s-%d", name, g2.typeID),
233 connected: g2.connection == connected, nonDesktop: props["non-desktop"][1] == 1,
234 mmW: int(g2.mmW), mmH: int(g2.mmH),
235 modes: modes[:min(int(g2.countModes), len(modes))],
236 encoders: encs[:min(int(g2.countEncoders), len(encs))],
237 }, nil
238}
239
240// pickCrtc prefers the connector's current encoder CRTC, else any free CRTC
241// an encoder allows. used holds CRTCs already driving other outputs.
242func pickCrtc(fd int, c connector, crtcs []uint32, used map[uint32]bool) (uint32, error) {
243 encs := c.encoders
244 if c.encoderID != 0 {
245 encs = append([]uint32{c.encoderID}, encs...)
246 }
247 for _, id := range encs {
248 e := getEncoder{encoderID: id}
249 if ioctl(fd, ioctlGetEncoder, unsafe.Pointer(&e)) != nil {
250 continue
251 }
252 if id == c.encoderID && e.crtcID != 0 && !used[e.crtcID] {
253 return e.crtcID, nil
254 }
255 for i, crtc := range crtcs {
256 if e.possibleCrtcs&(1<<i) != 0 && !used[crtc] {
257 return crtc, nil
258 }
259 }
260 }
261 return 0, fmt.Errorf("no free CRTC for %s", c.name)
262}
263
264func getCrtc(fd int, crtc uint32) (modeCrtc, error) {
265 c := modeCrtc{crtcID: crtc}
266 err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c))
267 return c, err
268}