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