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/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}

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