1package drm
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "slices"
8 "sync"
9 "sync/atomic"
10
11 "github.com/bnema/neferwl/internal/adapters/fdwait"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/zerowrap"
14 "golang.org/x/sys/unix"
15)
16
17// Card is one DRM device and the outputs it drives. Scan finds connected
18// connectors; each gets its own Output (CRTC, planes, cursor) run by one
19// goroutine. One reader goroutine reads commit events for all of them
20// and hands each to its output by CRTC.
21type Card struct {
22 fd int
23 path string
24 want Want
25 log zerowrap.Logger
26 // renderLog carries the render component: direct scanout and overlay
27 // decisions belong to the render path, not to KMS bookkeeping.
28 renderLog zerowrap.Logger
29 crtcs []uint32
30 // outputs by connector name; owned by the goroutine that calls Scan.
31 outputs map[string]*Output
32 leasable map[string]connector
33 leases map[uint32]leaseRecord
34 finished []uint32
35 flips map[uint32]chan flipEvent // by CRTC; set before the reader starts
36 k kms
37 // taken are planes driven by an output, by plane ID.
38 taken map[uint32]bool
39 // async is DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP.
40 async bool
41 // cpuFences: commits wait for fences on the CPU instead of IN_FENCE_FD
42 // (the driver mishandles it, see fencegate.go).
43 cpuFences bool
44 // serials numbers the commits of every output (event userData).
45 serials atomic.Uint64
46 // formats receives each output's direct scanout formats (nil: none
47 // sent); set before the first Scan.
48 formats chan<- ports.OutputFormats
49}
50
51// OpenCard turns on atomic modesetting and reads the card's CRTCs. fd
52// stays owned by the caller. A card without atomic KMS is refused. log
53// carries the drm component and renderLog the render component.
54func OpenCard(fd int, path string, want Want, log, renderLog zerowrap.Logger) (*Card, error) {
55 if err := enableAtomic(fd); err != nil {
56 return nil, fmt.Errorf("drm: atomic modesetting unsupported by %s: %w", path, err)
57 }
58 // Before any plane or property read: the property set depends on it.
59 if err := enableColorPipeline(fd); err != nil {
60 log.Info().Err(err).Str("card", path).Msg("plane colour pipelines unavailable")
61 }
62 crtcs, _, err := resources(fd)
63 if err != nil {
64 return nil, fmt.Errorf("%s: %w", path, err)
65 }
66 flips := make(map[uint32]chan flipEvent, len(crtcs))
67 for _, c := range crtcs {
68 flips[c] = make(chan flipEvent, 4)
69 }
70 k := kmsDevice{fd: fd, gemMu: &sync.Mutex{}, modifiers: hasCap(fd, capAddFB2Modifiers)}
71 return &Card{fd: fd, path: path, want: want, log: log, renderLog: renderLog, crtcs: crtcs, outputs: map[string]*Output{}, leasable: map[string]connector{}, leases: map[uint32]leaseRecord{}, flips: flips, k: k, taken: map[uint32]bool{}, async: hasCap(fd, capAtomicAsync), cpuFences: fenceGate(fd, log)}, nil
72}
73
74// Path is the device path, e.g. /dev/dri/card1.
75func (c *Card) Path() string { return c.path }
76
77// Device is the card's dev_t, the KMS device clients allocate scanout
78// buffers for (0 when unknown).
79func (c *Card) Device() uint64 {
80 var st unix.Stat_t
81 if unix.Fstat(c.fd, &st) != nil {
82 return 0
83 }
84 return st.Rdev
85}
86
87// SetFormats sets where outputs report their direct scanout formats.
88// Call before the first Scan.
89func (c *Card) SetFormats(ch chan<- ports.OutputFormats) { c.formats = ch }
90
91// SetWant replaces the connector config; the next Scan applies it.
92func (c *Card) SetWant(w Want) {
93 c.want = w
94}
95
96// Scan compares connectors with the outputs driven now. It returns outputs to
97// start (newly connected, or with a new mode after Release), names of
98// outputs to stop because they are gone (disconnected or disabled), and
99// names to stop because they need a new mode. A stopped output must be
100// closed by its goroutine before its CRTC is reused: call Release.
101func (c *Card) Scan() (added []*Output, removed, replaced []string, err error) {
102 _, ids, err := c.k.resources()
103 if err != nil {
104 return nil, nil, nil, err
105 }
106 seen := map[string]bool{}
107 available := map[string]connector{}
108 for _, id := range ids {
109 conn, err := c.k.connector(id)
110 if err != nil {
111 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector")
112 continue
113 }
114 if conn.nonDesktop && conn.connected {
115 available[conn.name] = conn
116 continue
117 }
118 if !c.want.usable(conn) {
119 continue
120 }
121 seen[conn.name] = true
122 mode := c.want.pickMode(conn)
123 if o := c.outputs[conn.name]; o != nil {
124 if !outputNeedsRestart(o, mode, c.want.HDR[conn.name]) {
125 continue
126 }
127 replaced = append(replaced, conn.name)
128 continue
129 }
130 o, err := c.open(conn, mode)
131 if err != nil {
132 c.log.Warn().Err(err).Str("connector", conn.name).Msg("output unusable")
133 continue
134 }
135 c.outputs[conn.name] = o
136 added = append(added, o)
137 }
138 for id, lease := range c.leases {
139 for _, name := range lease.names {
140 if _, ok := available[name]; !ok {
141 if err := c.Revoke(id); err != nil {
142 c.log.Warn().Err(err).Uint32("lease", id).Msg("revoke disconnected lease")
143 } else {
144 c.finished = append(c.finished, id)
145 }
146 break
147 }
148 }
149 }
150 c.leasable = available
151 for name := range c.outputs {
152 if !seen[name] {
153 removed = append(removed, name)
154 }
155 }
156 slices.Sort(removed)
157 slices.Sort(replaced)
158 return added, removed, replaced, nil
159}
160
161// ConnectedHeads inventories physical connectors independently of the desired
162// enabled state. The caller owns Scan and must invoke this on the same goroutine.
163func (c *Card) ConnectedHeads() ([]ports.OutputHead, error) {
164 _, ids, err := c.k.resources()
165 if err != nil {
166 return nil, err
167 }
168 var heads []ports.OutputHead
169 for _, id := range ids {
170 conn, err := c.k.connector(id)
171 if err != nil {
172 c.log.Warn().Err(err).Uint32("connector", id).Msg("read connector for inventory")
173 continue
174 }
175 if !conn.connected || conn.nonDesktop || len(conn.modes) == 0 {
176 continue
177 }
178 monitor := readMonitor(c.path, conn.name)
179 h := ports.OutputHead{Info: ports.OutputInfo{Name: conn.name, Make: monitor.Make, Model: monitor.Model, Serial: monitor.Serial, PhysicalW: conn.mmW, PhysicalH: conn.mmH}}
180 if h.Info.Make == "Unknown" {
181 h.Info.Make = ""
182 }
183 if h.Info.Model == "Unknown" {
184 h.Info.Model = ""
185 }
186 if o := c.outputs[conn.name]; o != nil {
187 h.Info = o.Info()
188 m := ports.OutputMode{Width: o.Width(), Height: o.Height(), RefreshMilli: o.mode.refreshMilli()}
189 h.Current = &m
190 h.Enabled = true
191 }
192 for _, m := range conn.modes {
193 h.Modes = append(h.Modes, ports.OutputMode{Width: int(m.HDisplay), Height: int(m.VDisplay), RefreshMilli: m.refreshMilli(), Preferred: m.Type&modeTypePrefered != 0})
194 }
195 heads = append(heads, h)
196 }
197 return heads, nil
198}
199
200// Release forgets an output closed by its goroutine, freeing its CRTC and
201// planes for the next Scan.
202func (c *Card) Release(name string) {
203 if o := c.outputs[name]; o != nil {
204 for _, p := range o.owned() {
205 delete(c.taken, p.id)
206 }
207 }
208 delete(c.outputs, name)
209}
210
211// Outputs returns the driven outputs.
212func (c *Card) Outputs() []*Output {
213 out := make([]*Output, 0, len(c.outputs))
214 for _, o := range c.outputs {
215 out = append(out, o)
216 }
217 slices.SortFunc(out, func(a, b *Output) int {
218 if a.conn.name < b.conn.name {
219 return -1
220 }
221 return 1
222 })
223 return out
224}
225
226func (c *Card) open(conn connector, mode modeInfo) (*Output, error) {
227 used := map[uint32]bool{}
228 for _, o := range c.outputs {
229 used[o.crtc] = true
230 }
231 for _, lease := range c.leases {
232 for _, crtc := range lease.crtcs {
233 used[crtc] = true
234 }
235 }
236 crtc, err := c.k.pickCrtc(conn, c.crtcs, used)
237 if err != nil {
238 return nil, err
239 }
240 o, err := newOutput(c, conn, mode, crtc)
241 if err != nil {
242 return nil, err
243 }
244 for _, p := range o.owned() {
245 c.taken[p.id] = true
246 }
247 return o, nil
248}
249
250// ReadEvents reads commit events until ctx ends and routes them to the
251// output of their CRTC. Run it once per card. An event is never dropped:
252// the output owes it a pending commit.
253func (c *Card) ReadEvents(ctx context.Context) error {
254 waiter, err := fdwait.New()
255 if err != nil {
256 return fmt.Errorf("drm event waiter: %w", err)
257 }
258 defer waiter.Close()
259 waiter.StopOn(ctx)
260 if err := waiter.Pin(int32(c.fd)); err != nil {
261 return fmt.Errorf("drm event waiter: %w", err)
262 }
263 buf := make([]byte, 1024)
264 var events []flipEvent // reused by every read
265 fds := []unix.PollFd{{Fd: int32(c.fd), Events: unix.POLLIN}}
266 for ctx.Err() == nil {
267 n, err := waiter.Poll(fds, -1)
268 if err != nil && !errors.Is(err, unix.EINTR) {
269 return fmt.Errorf("drm poll: %w", err)
270 }
271 if n <= 0 {
272 continue
273 }
274 m, err := unix.Read(c.fd, buf)
275 if err != nil {
276 if errors.Is(err, unix.EINTR) || errors.Is(err, unix.EAGAIN) {
277 continue
278 }
279 return fmt.Errorf("drm read: %w", err)
280 }
281 events = parseFlips(events[:0], buf[:m])
282 for _, ev := range events {
283 ch := c.flips[ev.crtc]
284 if ch == nil {
285 continue
286 }
287 select {
288 case ch <- ev:
289 case <-ctx.Done():
290 return nil
291 }
292 }
293 }
294 return nil
295}
296
297func normalizedHDRSettings(settings HDRSettings) HDRSettings {
298 if settings.SDRBrightness == 0 {
299 settings.SDRBrightness = ports.DefaultSDRBrightness
300 }
301 return settings
302}
303
304// outputNeedsRestart applies the same replacement path to mode and HDR
305// configuration changes; Release drops the old output before reopening it.
306func outputNeedsRestart(o *Output, mode modeInfo, settings HDRSettings) bool {
307 return o.mode != mode || o.hdr.settings != normalizedHDRSettings(settings)
308}