main f2d166b

card.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close