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