v0.3.0 cf370a0

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

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