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