feat/hints a471fa4

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	// 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	waiter.StopOn(ctx)
257	if err := waiter.Pin(int32(c.fd)); err != nil {
258		return fmt.Errorf("drm event waiter: %w", err)
259	}
260	buf := make([]byte, 1024)
261	var events []flipEvent // reused by every read
262	fds := []unix.PollFd{{Fd: int32(c.fd), Events: unix.POLLIN}}
263	for ctx.Err() == nil {
264		n, err := waiter.Poll(fds, -1)
265		if err != nil && !errors.Is(err, unix.EINTR) {
266			return fmt.Errorf("drm poll: %w", err)
267		}
268		if n <= 0 {
269			continue
270		}
271		m, err := unix.Read(c.fd, buf)
272		if err != nil {
273			if errors.Is(err, unix.EINTR) || errors.Is(err, unix.EAGAIN) {
274				continue
275			}
276			return fmt.Errorf("drm read: %w", err)
277		}
278		events = parseFlips(events[:0], buf[:m])
279		for _, ev := range events {
280			ch := c.flips[ev.crtc]
281			if ch == nil {
282				continue
283			}
284			select {
285			case ch <- ev:
286			case <-ctx.Done():
287				return nil
288			}
289		}
290	}
291	return nil
292}
293
294func normalizedHDRSettings(settings HDRSettings) HDRSettings {
295	if settings.SDRBrightness == 0 {
296		settings.SDRBrightness = ports.DefaultSDRBrightness
297	}
298	return settings
299}
300
301// outputNeedsRestart applies the same replacement path to mode and HDR
302// configuration changes; Release drops the old output before reopening it.
303func outputNeedsRestart(o *Output, mode modeInfo, settings HDRSettings) bool {
304	return o.mode != mode || o.hdr.settings != normalizedHDRSettings(settings)
305}

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 23

Tags 10

↑↓ moveenter openesc close