v0.1.0 d9218d6

output.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"maps"
8	"os"
9	"slices"
10	"sync/atomic"
11	"time"
12	"unsafe"
13
14	"github.com/bnema/neferwl/internal/adapters/syncfile"
15	"github.com/bnema/neferwl/internal/ports"
16	"github.com/bnema/zerowrap"
17	"golang.org/x/sys/unix"
18)
19
20// Output is one KMS connector on a CRTC, driven by atomic commits (ADR
21// 015). One goroutine runs it (Run) and closes it. Every commit that
22// changes the screen asks for an event and leaves the output pending until
23// it arrives: the next commit waits for it. The kernel may still refuse
24// it with EBUSY (it retries soon), and an event that never comes is
25// bounded (stuckTimeout, then a modeset).
26type Output struct {
27	k       kms
28	flipped <-chan flipEvent // commit events of this CRTC, from Card.ReadEvents
29	crtc    uint32
30	conn    connector
31	mode    modeInfo
32	saved   modeCrtc
33	monitor Monitor
34	log     zerowrap.Logger
35	// Properties: CRTC and connector property IDs by name.
36	crtcProps map[string]uint32
37	connCrtc  uint32 // the connector's CRTC_ID property
38	modeBlob  uint32
39	// primary shows frames; cursor is the cursor plane (nil: none);
40	// stray are other planes that can show on this CRTC, turned off at
41	// every modeset when a previous master left them on it.
42	primary *plane
43	cursor  *Cursor
44	stray   []*plane
45	// overlay shows one window's buffer above the composed frame
46	// (overlay.go); overlayOn is the buffer on it in the pending or last
47	// frame (0: off), overlayReason why it is off.
48	overlay       *plane
49	overlayOn     uint64
50	overlayReason string
51	// fbs are the two renderer images frames alternate between, back the
52	// one the next frame draws into (ADR 014: zero copy).
53	fbs  [2]uint32
54	back int
55	// pending: a commit waits for its event; pendingFrame when it flips a
56	// new frame (else it only changed the cursor or VRR).
57	pending      bool
58	pendingFrame pendingFrame
59	// serials numbers commits per card, so an event queued before a
60	// modeset, or for an earlier output on this CRTC, never completes a
61	// newer commit; pendingSerial is the pending commit's.
62	serials                   *atomic.Uint64
63	pendingSerial, nextSerial uint64
64	flipStart                 time.Time
65	flips                     int
66	// A commit whose event does not come by stuckAt is abandoned with a
67	// modeset once its fences signalled (stuckAfter, default
68	// stuckTimeout; fenceWarned once it waits for a fence). A commit
69	// refused with EBUSY is retried after busyRetry; busySince is when
70	// the EBUSY run began, lastBusy the latest EBUSY.
71	stuckAfter          time.Duration
72	stuckAt             time.Time
73	fenceWarned         bool
74	busySince, lastBusy time.Time
75	// readFences are fences of frames rendered but not committed: the
76	// GPU may still read client buffers until they signal, so what was
77	// seen is reported only then (or after a later frame flipped).
78	readFences []*os.File
79	// Direct scanout: client framebuffers by DMABuf ID and the buffer on
80	// screen and queued (0: the composed image).
81	scanout       bool
82	clientFBs     map[uint64]*clientFB
83	shown, queued uint64
84	reason        string // why the last frame was composed ("" = scanout)
85	unsent        []ports.OutputPresented
86	// Tearing (ADR 006): async commits of scanned-out buffers that ask for
87	// them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence
88	// is set when an async commit may carry IN_FENCE_FD (probed once).
89	tearing     bool
90	asyncFence  bool
91	asyncProbed bool
92	async       bool
93	// VRR: the CRTC's VRR_ENABLED property (0: none), on while a buffer is
94	// scanned out, set inside frame commits.
95	vrrProp       uint32
96	vrrOn         bool
97	composedSince time.Time
98	// kind is how the images were made.
99	kind imageKind
100	// formats receives the direct scanout formats after each modeset;
101	// sampled and device are what they are built from (Want).
102	formats chan<- ports.OutputFormats
103	sampled []ports.DMABufFormat
104	device  uint64
105}
106
107// pendingFrame is what the pending frame commit shows.
108type pendingFrame struct {
109	frame    bool
110	queued   uint64
111	zeroCopy ports.WindowID // window shown without composition
112	async    bool
113	shows    map[ports.WindowID]uint64
114	// fences are duplicates of the fences the commit waits on, owned by
115	// the output until the commit ends. composed is set when the frame
116	// was rendered (its fence is the renderer's).
117	fences   []*os.File
118	composed bool
119}
120
121// closeFences closes the fences of f.
122func (f *pendingFrame) closeFences() {
123	for _, fd := range f.fences {
124		fd.Close()
125	}
126	f.fences = nil
127}
128
129// dupFences duplicates the non-nil fences.
130func dupFences(fs ...*os.File) []*os.File {
131	var out []*os.File
132	for _, f := range fs {
133		if d := dupFence(f); d != nil {
134			out = append(out, d)
135		}
136	}
137	return out
138}
139
140// signalled reports whether every fence signalled (readable); an fd that
141// cannot be polled counts as signalled.
142func signalled(fs []*os.File) bool {
143	if len(fs) == 0 {
144		return true
145	}
146	pfds := make([]unix.PollFd, len(fs))
147	for i, f := range fs {
148		pfds[i] = unix.PollFd{Fd: int32(f.Fd()), Events: unix.POLLIN}
149	}
150	if _, err := unix.Poll(pfds, 0); err != nil {
151		return true
152	}
153	for _, p := range pfds {
154		if p.Revents == 0 {
155			return false
156		}
157	}
158	return true
159}
160
161// holdRead keeps the fences of a frame that was not committed.
162func (o *Output) holdRead(fs ...*os.File) {
163	o.readFences = append(o.readFences, dupFences(fs...)...)
164}
165
166// readDone reports whether no uncommitted frame may still read client
167// buffers, closing the fences that signalled.
168func (o *Output) readDone() bool {
169	kept := o.readFences[:0]
170	for _, f := range o.readFences {
171		if signalled([]*os.File{f}) {
172			f.Close()
173			continue
174		}
175		kept = append(kept, f)
176	}
177	clear(o.readFences[len(kept):])
178	o.readFences = kept
179	return len(kept) == 0
180}
181
182// dropRead closes the fences of uncommitted frames.
183func (o *Output) dropRead() {
184	for _, f := range o.readFences {
185		f.Close()
186	}
187	o.readFences = nil
188}
189
190// endPending ends the pending commit, closing its fences.
191func (o *Output) endPending() {
192	o.pendingFrame.closeFences()
193	o.pending, o.pendingFrame = false, pendingFrame{}
194}
195
196// Commit event userData: the commit serial above userKindBits, the kind
197// of commit below.
198const (
199	userFrame    = 1
200	userState    = 2
201	userKindBits = 2
202)
203
204// userData takes a new serial for the next commit of kind and returns
205// what the commit carries.
206func (o *Output) userData(kind uint64) uint64 {
207	o.nextSerial = o.serials.Add(1)
208	return o.nextSerial<<userKindBits | kind
209}
210
211// begin marks the commit made with the last userData pending.
212func (o *Output) begin(f pendingFrame) {
213	o.pendingSerial = o.nextSerial
214	o.pendingFrame.closeFences()
215	o.pending, o.pendingFrame, o.flipStart = true, f, time.Now()
216	o.stuckAt, o.fenceWarned = o.flipStart.Add(o.stuckLimit()), false
217	o.busySince = time.Time{}
218}
219
220// Bounds on waiting for a commit event: no path may leave an output
221// pending forever.
222const (
223	stuckTimeout = time.Second
224	busyRetry    = 50 * time.Millisecond
225)
226
227func (o *Output) stuckLimit() time.Duration {
228	if o.stuckAfter > 0 {
229		return o.stuckAfter
230	}
231	return stuckTimeout
232}
233
234// expire handles a pending commit whose event did not come by stuckAt.
235// A commit still waiting for a fence (a long GPU job) keeps waiting.
236// Else the commit is abandoned so the next can go; it reports whether
237// the output needs a modeset: an event is missing (the kernel state is
238// unknown) or EBUSY lasted past the limit.
239func (o *Output) expire() bool {
240	now := time.Now()
241	age := now.Sub(o.flipStart)
242	if o.pendingSerial != 0 && !signalled(o.pendingFrame.fences) {
243		if !o.fenceWarned {
244			o.fenceWarned = true
245			o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Dur("age", age).Msg("frame waits for GPU fence")
246		}
247		o.stuckAt = now.Add(o.stuckLimit())
248		return false
249	}
250	kind := "state"
251	if o.pendingFrame.frame {
252		kind = "frame"
253	}
254	o.endPending()
255	if o.pendingSerial == 0 {
256		busy := now.Sub(o.busySince)
257		if o.busySince.IsZero() || busy < o.stuckLimit() {
258			return false
259		}
260		o.log.Warn().Str("connector", o.conn.name).Str("kind", "busy").Dur("age", busy).Msg("commits refused as busy; modeset")
261		return true
262	}
263	o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Str("kind", kind).Dur("age", age).Msg("commit event missing; modeset")
264	return true
265}
266
267// CursorLoader returns the image of a cursor at an output scale, at most
268// limit pixels on a side; an empty image hides the cursor.
269type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
270
271// newOutput reads the CRTC's planes and properties for a connector on crtc.
272func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
273	log := card.log
274	pipe := slices.Index(card.crtcs, crtc)
275	o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
276	var err error
277	if o.saved, err = getCrtc(card.fd, crtc); err != nil {
278		log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
279	}
280	if err := o.readProps(pipe, card.taken, cursorSize(card.fd)); err != nil {
281		return nil, err
282	}
283	o.tearing = card.async && !card.want.NoTearing
284	if !card.want.NoVRR {
285		o.vrrProp = vrrProperty(o.k, c.id, crtc)
286	}
287	log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Uint32("primary", o.primary.id).Bool("cursor", o.cursor != nil).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output")
288	return o, nil
289}
290
291// readProps finds the output's planes and property IDs. taken are planes
292// other outputs drive.
293func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) error {
294	cp, err := o.k.objProps(o.crtc, objCrtc)
295	if err != nil {
296		return fmt.Errorf("crtc properties: %w", err)
297	}
298	o.crtcProps = map[string]uint32{}
299	for name, v := range cp {
300		o.crtcProps[name] = uint32(v[0])
301	}
302	np, err := o.k.objProps(o.conn.id, objConnector)
303	if err != nil {
304		return fmt.Errorf("connector properties: %w", err)
305	}
306	o.connCrtc = uint32(np["CRTC_ID"][0])
307	if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 {
308		return errors.New("crtc or connector lacks atomic properties")
309	}
310	planes, err := readPlanes(o.k, pipe)
311	if err != nil {
312		return fmt.Errorf("planes: %w", err)
313	}
314	pick := func(typ uint32) *plane {
315		var best *plane
316		for _, p := range planes {
317			if p.typ != typ || taken[p.id] {
318				continue
319			}
320			// Prefer the plane already on this CRTC.
321			if best == nil || p.crtc == o.crtc && best.crtc != o.crtc {
322				best = p
323			}
324		}
325		return best
326	}
327	if o.primary = pick(planePrimary); o.primary == nil {
328		return errors.New("no free primary plane")
329	}
330	if p := pick(planeCursor); p != nil {
331		o.cursor = newCursor(p, cursorSide)
332	}
333	for _, p := range planes {
334		if p != o.primary && (o.cursor == nil || p != o.cursor.plane) && !taken[p.id] {
335			o.stray = append(o.stray, p)
336		}
337	}
338	o.pickOverlay()
339	return nil
340}
341
342// owned are the planes this output drives.
343func (o *Output) owned() []*plane {
344	out := []*plane{o.primary}
345	if o.cursor != nil {
346		out = append(out, o.cursor.plane)
347	}
348	if o.overlay != nil {
349		out = append(out, o.overlay)
350	}
351	return out
352}
353
354// cursorSize is the cursor plane side: the smaller of the driver's width
355// and height caps, 64 when it does not say.
356func cursorSize(fd int) int {
357	size := 0
358	for _, c := range []uint64{capCursorW, capCursorH} {
359		v := getCap{capability: c}
360		if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 {
361			if size == 0 || int(v.value) < size {
362				size = int(v.value)
363			}
364		}
365	}
366	if size == 0 {
367		size = 64
368	}
369	return size
370}
371
372// Cursor is the hardware cursor, or nil when the CRTC has no cursor plane.
373func (o *Output) Cursor() *Cursor { return o.cursor }
374
375// Info describes the output for core and clients. Unknown EDID fields are empty.
376func (o *Output) Info() ports.OutputInfo {
377	known := func(v string) string {
378		if v == "Unknown" {
379			return ""
380		}
381		return v
382	}
383	return ports.OutputInfo{Name: o.conn.name, Make: known(o.monitor.Make), Model: known(o.monitor.Model), Serial: o.monitor.Serial, Width: o.Width(), Height: o.Height(), RefreshMilli: o.mode.refreshMilli(), PhysicalW: o.conn.mmW, PhysicalH: o.conn.mmH}
384}
385
386func (o *Output) Width() int  { return int(o.mode.HDisplay) }
387func (o *Output) Height() int { return int(o.mode.VDisplay) }
388
389// primaryProps shows fb full screen on the primary plane.
390func (o *Output) primaryProps(req *atomicReq, fb uint32) {
391	p := o.primary
392	w, h := uint64(o.Width()), uint64(o.Height())
393	req.set(p.id, p.prop("FB_ID"), uint64(fb))
394	req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
395	req.set(p.id, p.prop("SRC_X"), 0)
396	req.set(p.id, p.prop("SRC_Y"), 0)
397	req.set(p.id, p.prop("SRC_W"), w<<16)
398	req.set(p.id, p.prop("SRC_H"), h<<16)
399	req.set(p.id, p.prop("CRTC_X"), 0)
400	req.set(p.id, p.prop("CRTC_Y"), 0)
401	req.set(p.id, p.prop("CRTC_W"), w)
402	req.set(p.id, p.prop("CRTC_H"), h)
403}
404
405// modeset shows the front image with the output's mode, turning off every
406// other plane on the CRTC; needed at start and after every VT resume.
407// It blocks until the kernel applied it.
408func (o *Output) modeset() error {
409	blob, err := o.modeBlobFor(o.mode)
410	if err != nil {
411		return err
412	}
413	if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil {
414		_ = o.k.destroyBlob(blob)
415		return fmt.Errorf("modeset: %w", err)
416	}
417	// Only now is nothing of ours pending or on screen: on failure the
418	// previous buffers may still show.
419	o.endPending()
420	o.busySince = time.Time{}
421	o.shown, o.queued = 0, 0
422	if o.modeBlob != 0 {
423		_ = o.k.destroyBlob(o.modeBlob)
424	}
425	o.modeBlob = blob
426	o.vrrOn, o.overlayOn = false, 0
427	if o.cursor != nil {
428		o.cursor.applied = cursorState{}
429		o.cursor.screen, o.cursor.flying = 0, false
430	}
431	o.log.Info().Str("connector", o.conn.name).Msg("modeset")
432	o.testCursor()
433	o.sendFormats()
434	return nil
435}
436
437// sendFormats reports the formats clients may allocate for direct
438// scanout on this output (none when scanout is off). It never blocks:
439// wayland keeps the latest per output, a dropped report is resent at the
440// next modeset.
441func (o *Output) sendFormats() {
442	if o.formats == nil {
443		return
444	}
445	f := ports.OutputFormats{Output: o.conn.name, Device: o.device}
446	if o.scanout {
447		f.Formats = o.scanoutFormats(o.sampled)
448	}
449	select {
450	case o.formats <- f:
451	default:
452		o.log.Warn().Str("connector", o.conn.name).Msg("scanout formats not sent: wayland busy")
453	}
454}
455
456// testModeset asks KMS whether it would take the modeset of the output
457// images without applying it.
458func (o *Output) testModeset() error {
459	blob, err := o.modeBlobFor(o.mode)
460	if err != nil {
461		return err
462	}
463	defer func() { _ = o.k.destroyBlob(blob) }()
464	if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil {
465		return fmt.Errorf("modeset test: %w", err)
466	}
467	return nil
468}
469
470func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) {
471	blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode)))
472	if err != nil {
473		return 0, fmt.Errorf("mode blob: %w", err)
474	}
475	return blob, nil
476}
477
478// testCursor turns the hardware cursor off when KMS refuses its images on
479// the cursor plane (e.g. a pitch the plane cannot scan out).
480func (o *Output) testCursor() {
481	c := o.cursor
482	if c == nil || c.off || c.fbs[0] == 0 {
483		return
484	}
485	req := &atomicReq{}
486	c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
487	if err := o.k.commit(req, atomicTestOnly, 0); refused(err) {
488		o.cursorOff(err)
489	}
490}
491
492// cursorOff stops using the cursor plane for the rest of the output's life.
493func (o *Output) cursorOff(err error) {
494	o.cursor.off = true
495	o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off")
496}
497
498// modesetReq is the modeset of the front image with mode blob.
499func (o *Output) modesetReq(blob uint32) *atomicReq {
500	req := &atomicReq{}
501	req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob))
502	req.set(o.crtc, o.crtcProps["ACTIVE"], 1)
503	req.set(o.crtc, o.vrrProp, 0)
504	req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
505	o.primaryProps(req, o.fbs[1-o.back])
506	if o.cursor != nil {
507		o.cursor.props(req, o.crtc, cursorState{})
508	}
509	o.overlayProps(req, overlayWin{})
510	for _, p := range o.stray {
511		// Only planes a previous master left on this CRTC.
512		props, err := o.k.objProps(p.id, objPlane)
513		if err != nil || uint32(props["CRTC_ID"][1]) != o.crtc {
514			continue
515		}
516		req.set(p.id, p.prop("FB_ID"), 0)
517		req.set(p.id, p.prop("CRTC_ID"), 0)
518	}
519	return req
520}
521
522// probeAsync checks once whether async commits may carry IN_FENCE_FD.
523func (o *Output) probeAsync(r ports.Renderer) {
524	if o.asyncProbed || !o.tearing {
525		return
526	}
527	o.asyncProbed = true
528	r.UseTarget(o.back)
529	done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
530	if err != nil {
531		return
532	}
533	req := &atomicReq{}
534	req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back]))
535	if done != nil {
536		defer done.Close()
537		req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(done.Fd()))
538	}
539	err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0)
540	o.asyncFence = err == nil && done != nil
541	o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe")
542}
543
544// wantVRR is the VRR state the next frame commit sets: on while a buffer
545// is scanned out, off after composing for vrrHold (each toggle may
546// flicker, so short compositions keep it).
547func (o *Output) wantVRR(direct bool) bool {
548	if o.vrrProp == 0 {
549		return false
550	}
551	if direct {
552		o.composedSince = time.Time{}
553		return true
554	}
555	if o.composedSince.IsZero() {
556		o.composedSince = time.Now()
557	}
558	return o.vrrOn && time.Since(o.composedSince) <= vrrHold
559}
560
561// commitFrame flips fb in one commit with the cursor and VRR. An async
562// commit carries only the primary plane (kernel rule): a frame that must
563// also move the cursor or change VRR flips at vblank. fence is the
564// frame's GPU fence (nil: none); the caller keeps and closes it.
565func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error {
566	return o.commitWith(fb, fence, async, vrr, f, overlayWin{})
567}
568
569// commitWith is commitFrame with ov on the overlay plane (zero: off).
570func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error {
571	cur := cursorState{}
572	if o.cursor != nil {
573		cur = o.cursor.desired()
574	}
575	if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) {
576		async = false
577	}
578	req := &atomicReq{}
579	flags := uint32(atomicNonblock | flipEventFlag)
580	if async {
581		flags |= flipAsyncFlag
582		req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb))
583	} else {
584		o.primaryProps(req, fb)
585		req.set(o.crtc, o.vrrProp, boolValue(vrr))
586		if o.cursor != nil {
587			o.cursor.props(req, o.crtc, cur)
588		}
589		if ov.buf != 0 || o.overlayOn != 0 {
590			o.overlayProps(req, ov)
591		}
592	}
593	if fence != nil {
594		req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
595	}
596	// The kernel takes its own reference on the fence.
597	if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil {
598		if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
599			return o.commitWith(fb, fence, false, vrr, f, ov)
600		}
601		if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
602			// Retry without turning VRR on: if that passes, the driver
603			// refuses VRR on this output.
604			if o.commitFrame(fb, fence, false, false, f) == nil {
605				o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")
606				o.vrrProp = 0
607				return nil
608			}
609		}
610		return err
611	}
612	if !async {
613		if o.vrrOn != vrr {
614			o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
615		}
616		o.vrrOn = vrr
617	}
618	if o.cursor != nil {
619		// An async commit leaves the cursor plane as applied.
620		o.cursor.committed(cur)
621	}
622	if !async {
623		o.overlayOn = ov.buf
624	}
625	f.frame, f.async = true, async
626	f.fences = dupFences(fence, ov.acquire)
627	o.setAsync(async)
628	o.begin(f)
629	return nil
630}
631
632// cursorRefused reports, after a commit carrying the cursor failed with
633// EINVAL, whether the cursor plane was the cause; then the cursor is off
634// and the commit can be retried without it.
635func (o *Output) cursorRefused() bool {
636	o.testCursor()
637	return o.cursor.off
638}
639
640// commitState commits a cursor or VRR change without a new frame.
641func (o *Output) commitState(vrr bool) error {
642	req := &atomicReq{}
643	cur := cursorState{}
644	if o.cursor != nil {
645		cur = o.cursor.desired()
646		if cur != o.cursor.applied {
647			o.cursor.props(req, o.crtc, cur)
648		}
649	}
650	if vrr != o.vrrOn {
651		req.set(o.crtc, o.vrrProp, boolValue(vrr))
652	}
653	if len(req.objs) == 0 {
654		return nil
655	}
656	if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil {
657		if o.overlayConflict(err, o.overlayOn) {
658			// The next frame is composed without the overlay.
659			return errOverlayDropped
660		}
661		if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
662			return o.commitState(vrr)
663		}
664		return err
665	}
666	if o.cursor != nil {
667		o.cursor.committed(cur)
668	}
669	if vrr != o.vrrOn {
670		o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
671	}
672	o.vrrOn = vrr
673	o.begin(pendingFrame{})
674	return nil
675}
676
677func boolValue(b bool) uint64 {
678	if b {
679		return 1
680	}
681	return 0
682}
683
684// scanoutFrame picks a fullscreen client buffer to flip directly. It
685// returns fb 0, with the reason logged on change, when the frame must be
686// composed.
687func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent) {
688	reason := "disabled"
689	if o.scanout {
690		c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height())
691	}
692	if reason == "" {
693		fb, reason = o.scanoutFB(c.DMABuf, time.Now())
694	}
695	if reason != o.reason {
696		o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout")
697		o.reason = reason
698	}
699	if reason != "" {
700		return 0, c
701	}
702	return fb, c
703}
704
705// commitScanout flips a client buffer. It reports false when KMS refused
706// the buffer: the frame must be composed.
707func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) {
708	cfb := o.clientFBs[c.DMABuf.ID]
709	// Drivers refuse async flips that change the format or modifier: the
710	// flip from the composed image into scanout waits for vblank.
711	async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync
712	vrr := o.wantVRR(true)
713	f.queued, f.zeroCopy = c.DMABuf.ID, c.ID
714	// Explicit sync: KMS waits on the client's acquire fence. Wayland may
715	// close its file at any time: commit a duplicate taken while it is
716	// held open (the kernel keeps its own reference).
717	fence := dupFence(c.Acquire)
718	if fence != nil {
719		defer fence.Close()
720	}
721	err := o.commitFrame(fb, fence, async, vrr, f)
722	if err != nil && async && errors.Is(err, unix.EINVAL) {
723		// Refused (e.g. not a fast update): this buffer flips at vblank
724		// from now on.
725		o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
726		cfb.noAsync = true
727		err = o.commitFrame(fb, fence, false, vrr, f)
728	}
729	if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) {
730		// KMS refuses this buffer on the plane: compose it instead.
731		o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")
732		cfb.failed, o.reason = "flip_refused", "flip_refused"
733		o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout")
734		return false, nil
735	}
736	if err == nil {
737		o.queued = c.DMABuf.ID
738	}
739	return true, err
740}
741
742// vrrHold is how long composition runs before VRR turns off.
743const vrrHold = 500 * time.Millisecond
744
745// setAsync logs when the output starts or stops tearing.
746func (o *Output) setAsync(on bool) {
747	if on != o.async {
748		o.async = on
749		o.log.Info().Bool("tearing", on).Str("connector", o.conn.name).Msg("tearing")
750	}
751}
752
753// Close restores the CRTC state found at startup, when it had a mode, and
754// frees buffers.
755func (o *Output) Close() {
756	req := &atomicReq{}
757	if o.cursor != nil {
758		o.cursor.props(req, o.crtc, cursorState{})
759	}
760	o.overlayProps(req, overlayWin{})
761	req.set(o.crtc, o.vrrProp, 0)
762	s := o.saved
763	var blob uint32
764	p := o.primary
765	restored := false
766	if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 {
767		if b, err := o.modeBlobFor(s.mode); err == nil {
768			blob, restored = b, true
769			req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b))
770			req.set(o.crtc, o.crtcProps["ACTIVE"], 1)
771			req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
772			req.set(p.id, p.prop("FB_ID"), uint64(s.fbID))
773			req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
774			req.set(p.id, p.prop("SRC_X"), 0)
775			req.set(p.id, p.prop("SRC_Y"), 0)
776			req.set(p.id, p.prop("SRC_W"), uint64(s.mode.HDisplay)<<16)
777			req.set(p.id, p.prop("SRC_H"), uint64(s.mode.VDisplay)<<16)
778			req.set(p.id, p.prop("CRTC_X"), 0)
779			req.set(p.id, p.prop("CRTC_Y"), 0)
780			req.set(p.id, p.prop("CRTC_W"), uint64(s.mode.HDisplay))
781			req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay))
782		}
783	}
784	if !restored {
785		// Nothing to give back: leave the CRTC off.
786		req.set(o.crtc, o.crtcProps["ACTIVE"], 0)
787		req.set(o.crtc, o.crtcProps["MODE_ID"], 0)
788		req.set(p.id, p.prop("FB_ID"), 0)
789		req.set(p.id, p.prop("CRTC_ID"), 0)
790		req.set(o.conn.id, o.connCrtc, 0)
791	}
792	// Best effort: at exit the card may already belong to another session.
793	if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
794		o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("restore crtc")
795	}
796	if blob != 0 {
797		_ = o.k.destroyBlob(blob)
798	}
799	if o.modeBlob != 0 {
800		_ = o.k.destroyBlob(o.modeBlob)
801		o.modeBlob = 0
802	}
803	if o.cursor != nil {
804		o.cursor.free(o.k)
805	}
806	o.freeImages()
807	o.endPending()
808	o.dropRead()
809	o.shown, o.queued = 0, 0
810	o.dropClientFBs(time.Now(), true)
811}
812
813// Run renders scenes and commits them until ctx ends. active reports seat
814// enable/disable. What the output shows and read is reported on presented.
815func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, presented chan<- ports.OutputPresented) error {
816	r, err := newRenderer(o.Width(), o.Height())
817	if err != nil {
818		return fmt.Errorf("create renderer: %w", err)
819	}
820	defer r.Close()
821	if o.cursor != nil {
822		if err := o.cursor.setup(o.k, r); err != nil {
823			o.cursor.off = true
824			o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")
825		}
826	}
827	// The seat sends its state first; while switched away (e.g. a monitor
828	// plugged in on another VT) the modeset waits for the enable.
829	enabled := true
830	select {
831	case enabled = <-active:
832	default:
833	}
834	// Output images, from the best to the linear fallback (ADR 014).
835	// While switched away they are made on the first enable.
836	if enabled {
837		if err := o.showImages(r, imagesDriver, nil); err != nil {
838			return err
839		}
840		o.probeAsync(r)
841	}
842	surfaces := make(map[ports.WindowID]ports.SurfaceContent)
843	var scene ports.Scene
844	haveScene, dirty := false, false
845	var want ports.CursorChange
846	cursorScale := -1.0 // not loaded yet
847	frame := 0
848	stats := time.NewTicker(10 * time.Second)
849	defer stats.Stop()
850	// seen is the latest content Seq per window. It is reported only while
851	// no frame is in flight: then the GPU has finished every frame that
852	// read older buffers (the kernel waited for their fence to flip).
853	seen := map[ports.WindowID]uint64{}
854	reportDirty := false
855	var cursorWake <-chan struct{}
856	if o.cursor != nil {
857		cursorWake = o.cursor.wake
858	}
859	for {
860		readWait := false
861		if reportDirty && !o.pending {
862			if readWait = !o.readDone(); !readWait {
863				o.report(nil, seen)
864				reportDirty = false
865			}
866		}
867		o.flushReport(presented)
868		// An unsent report is retried soon, not only on the next event;
869		// so is one that waits for an uncommitted frame's fence.
870		var retry <-chan time.Time
871		if len(o.unsent) > 0 || readWait {
872			retry = time.After(time.Millisecond)
873		}
874		// A composed screen that stopped changing still drops VRR on time.
875		var vrrOff <-chan time.Time
876		if enabled && o.vrrOn && !o.pending && !o.composedSince.IsZero() {
877			vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince)))
878		}
879		// A commit whose event never comes must not stop the output.
880		var stuck <-chan time.Time
881		if enabled && o.pending {
882			stuck = time.After(max(0, time.Until(o.stuckAt)))
883		}
884		stateDirty := false
885		select {
886		case <-retry:
887			continue
888		case <-stuck:
889			if o.pending && o.stuckAt.After(time.Now()) {
890				continue // waits for a fence
891			}
892			if o.expire() {
893				if err := o.modeset(); err != nil {
894					if !o.commitFailed(err, &enabled) {
895						return fmt.Errorf("recovery modeset: %w", err)
896					}
897					continue
898				}
899			}
900			dirty = haveScene
901			stateDirty = true
902		case <-vrrOff:
903			stateDirty = true
904		case <-cursorWake:
905			stateDirty = true
906		case <-ctx.Done():
907			return nil
908		case on := <-active:
909			// Modeset on every enable: a fast disable+enable can coalesce to one true.
910			if on {
911				var err error
912				if o.fbs[0] == 0 {
913					err = o.showImages(r, imagesDriver, nil)
914				} else {
915					err = o.modeset()
916				}
917				if lostMaster(err) {
918					// The seat took DRM master back: wait for the next enable.
919					o.log.Warn().Err(err).Msg("resume")
920					continue
921				}
922				if err != nil {
923					o.log.Error().Err(err).Msg("resume")
924					return err
925				}
926				o.probeAsync(r)
927				dirty = haveScene
928				stateDirty = true
929			}
930			enabled = on
931			o.log.Info().Bool("enabled", on).Msg("output")
932		case ev := <-o.flipped:
933			if !o.completed(ev, seen) {
934				continue // stale event from before a modeset
935			}
936			stateDirty = true
937			if o.cursor != nil {
938				// An image that waited for a free slot loads now.
939				if _, err := o.cursor.flushLater(r); err != nil {
940					o.log.Warn().Err(err).Msg("cursor")
941				}
942			}
943		case s := <-scenes:
944			if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale {
945				cursorScale = s.Scale
946				o.setCursor(r, loadCursor, want, s.Scale)
947				stateDirty = true
948			}
949			scene, haveScene, dirty = s, true, true
950		case c := <-cursor:
951			want = c
952			// Before the first scene the scale is unknown: loaded then.
953			if o.cursor != nil && loadCursor != nil && cursorScale > 0 {
954				o.setCursor(r, loadCursor, want, cursorScale)
955				stateDirty = true
956			}
957		case c := <-contents:
958			if c.Empty() {
959				delete(surfaces, c.ID)
960			} else {
961				surfaces[c.ID] = c
962			}
963			if c.Seq > seen[c.ID] {
964				seen[c.ID] = c.Seq
965				reportDirty = true
966			}
967			// Windows on other outputs or workspaces do not need a frame.
968			dirty = dirty || scene.Shows(c.ID)
969		case <-stats.C:
970			o.dropClientFBs(time.Now(), false)
971			ev := o.log.Info().Str("connector", o.conn.name).Int("frames", frame).Int("flips", o.flips).Bool("pending", o.pending)
972			if o.cursor != nil {
973				cs := o.cursor.TakeStats()
974				ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_commits", cs.Commits)
975			}
976			ev.Msg("stats")
977		}
978		if !enabled || o.pending {
979			continue
980		}
981		if !dirty || !haveScene {
982			if stateDirty {
983				err := o.commitState(o.wantVRR(o.shown != 0))
984				if errors.Is(err, errOverlayDropped) {
985					dirty = haveScene
986					continue
987				}
988				if err != nil && !o.commitFailed(err, &enabled) {
989					return fmt.Errorf("commit: %w", err)
990				}
991			}
992			continue
993		}
994		start := time.Now()
995		// shows is what the frame puts on screen: presentation feedback
996		// of windows it does not draw is discarded, not presented.
997		f := pendingFrame{shows: shownBy(scene, seen)}
998		fb, c := o.scanoutFrame(scene, surfaces)
999		direct := false
1000		if fb != 0 {
1001			direct, err = o.commitScanout(fb, c, pendingFrame{shows: map[ports.WindowID]uint64{c.ID: seen[c.ID]}})
1002		}
1003		if !direct {
1004			ov, composed := o.overlayFrame(scene, surfaces)
1005			r.UseTarget(o.back)
1006			done, rerr := r.Render(composed, surfaces)
1007			if rerr != nil {
1008				ov.close()
1009				return fmt.Errorf("render frame: %w", rerr)
1010			}
1011			if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
1012				// Refused: compose the window too (cached per buffer).
1013				if done != nil {
1014					done.Close()
1015				}
1016				ov.close()
1017				ov = overlayWin{}
1018				if done, rerr = r.Render(scene, surfaces); rerr != nil {
1019					return fmt.Errorf("render frame: %w", rerr)
1020				}
1021			}
1022			// The overlay buffer is on screen like a scanned-out one: it
1023			// is reported shown, so it is not released under the plane.
1024			f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
1025			err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov)
1026			if err != nil {
1027				// The GPU may still read client buffers for this frame.
1028				o.holdRead(done)
1029			}
1030			if done != nil {
1031				done.Close()
1032			}
1033			ov.close()
1034			if err != nil && o.overlayConflict(err, ov.buf) {
1035				err = errOverlayDropped
1036			}
1037			if err == nil {
1038				o.queued = ov.buf
1039				o.back = 1 - o.back
1040			}
1041		}
1042		if err != nil {
1043			if errors.Is(err, errOverlayDropped) {
1044				dirty = true
1045				continue
1046			}
1047			if !o.commitFailed(err, &enabled) {
1048				return fmt.Errorf("page flip: %w", err)
1049			}
1050			continue // dirty stays true
1051		}
1052		dirty = false
1053		frame++
1054		o.log.Debug().Int("frame", frame).Uint64("seq", scene.Seq).Int("windows", len(scene.Windows)).Bool("direct", direct).Dur("took", time.Since(start)).Msg("frame")
1055	}
1056}
1057
1058// commitFailed handles a commit error that does not stop the output: a
1059// commit still in flight (EBUSY, retried after its event) or DRM master
1060// lost mid-switch (the seat disable follows).
1061func (o *Output) commitFailed(err error, enabled *bool) bool {
1062	switch {
1063	case errors.Is(err, unix.EBUSY):
1064		// The kernel still works on an earlier commit (e.g. across a VT
1065		// switch, or one whose event already came): wait for any event
1066		// of this CRTC, or retry after busyRetry since none may come.
1067		o.endPending()
1068		now := time.Now()
1069		o.pending, o.flipStart, o.stuckAt = true, now, now.Add(busyRetry)
1070		o.pendingSerial = 0
1071		// A run of EBUSY: each refusal follows the previous retry.
1072		if o.busySince.IsZero() || now.Sub(o.lastBusy) > 4*busyRetry {
1073			o.busySince = now
1074			o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy")
1075		}
1076		o.lastBusy = now
1077		return true
1078	case lostMaster(err):
1079		o.log.Warn().Err(err).Msg("commit refused")
1080		*enabled = false
1081		return true
1082	}
1083	o.log.Error().Err(err).Msg("commit")
1084	return false
1085}
1086
1087// completed handles the event of the pending commit: a flipped frame is
1088// reported with its kernel timestamp. It reports false for an event of
1089// another commit, which is dropped.
1090func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool {
1091	if !o.pending || o.pendingSerial != 0 && ev.user>>userKindBits != o.pendingSerial {
1092		return false
1093	}
1094	f := o.pendingFrame
1095	o.endPending()
1096	o.busySince = time.Time{}
1097	if d := time.Since(o.flipStart); d > 20*time.Millisecond {
1098		o.log.Info().Dur("flip_ms", d).Str("connector", o.conn.name).Msg("slow flip")
1099	}
1100	if o.cursor != nil {
1101		o.cursor.landed()
1102	}
1103	if !f.frame {
1104		return true
1105	}
1106	if f.composed {
1107		// Frames render in order: this flip's fence signalled after
1108		// those of frames rendered before it and not committed.
1109		o.dropRead()
1110	}
1111	o.flips++
1112	o.shown = f.queued
1113	if o.queued == f.queued {
1114		o.queued = 0
1115	}
1116	refresh := time.Duration(0)
1117	if !o.vrrOn {
1118		refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli())))
1119	}
1120	o.report(&ports.FlipInfo{When: ev.when, Seq: uint64(ev.seq), Refresh: refresh, ZeroCopy: f.zeroCopy, Async: f.async, HardwareClock: true, Shows: f.shows}, seen)
1121	return true
1122}
1123
1124// setCursor loads a cursor for scale into the cursor images.
1125func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) {
1126	if o.cursor.off {
1127		return
1128	}
1129	img, err := load(c, scale, o.cursor.Limit())
1130	if err == nil {
1131		err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY)
1132	}
1133	if err != nil {
1134		o.log.Warn().Err(err).Msg("cursor")
1135		return
1136	}
1137	o.log.Debug().Float64("scale", scale).Str("shape", c.Shape).Bool("client", c.Image != nil).Int("w", img.W).Int("h", img.H).Msg("cursor image")
1138}
1139
1140// maxUnsent bounds reports waiting for wayland; past it the oldest flip is
1141// folded into the next one.
1142const maxUnsent = 16
1143
1144// report queues what the output shows and has read, so replaced client
1145// buffers can be released. A flip-less report merges into the newest
1146// queued one; flips are never merged unless maxUnsent is reached.
1147func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
1148	r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}
1149	if n := len(o.unsent); n > 0 && (flip == nil || o.unsent[n-1].Flip == nil) {
1150		if flip == nil {
1151			r.Flip = o.unsent[n-1].Flip
1152		}
1153		o.unsent[n-1] = r
1154		return
1155	}
1156	o.unsent = append(o.unsent, r)
1157	if len(o.unsent) > maxUnsent {
1158		old, next := o.unsent[0].Flip, o.unsent[1].Flip
1159		if next != nil && old != nil {
1160			next.Merged += 1 + old.Merged
1161			for id, seq := range old.Shows {
1162				if _, ok := next.Shows[id]; !ok {
1163					next.Shows[id] = seq
1164				}
1165			}
1166		}
1167		o.unsent = slices.Delete(o.unsent, 0, 1)
1168		o.log.Warn().Str("connector", o.conn.name).Msg("wayland is not reading output reports; flips merged")
1169	}
1170}
1171
1172// flushReport sends queued reports without blocking.
1173func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
1174	for len(o.unsent) > 0 {
1175		select {
1176		case presented <- o.unsent[0]:
1177			o.unsent = slices.Delete(o.unsent, 0, 1)
1178		default:
1179			return
1180		}
1181	}
1182}
1183
1184// imageKind is how output images are made, best first.
1185type imageKind int
1186
1187const (
1188	imagesDriver imageKind = iota // exported, modifier chosen by the driver
1189	imagesLinear                  // exported, linear
1190)
1191
1192// showImages sets up images from kind on and modesets them. KMS may refuse
1193// an image only in a modeset: a TEST_ONLY modeset checks it first, and the
1194// next kind is tried on refusal. cause is why the
1195// previous kind failed, for the log.
1196func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
1197	for {
1198		got, err := o.setupImages(r, kind, cause)
1199		if err != nil {
1200			return err
1201		}
1202		o.kind = got
1203		if err = o.testModeset(); err == nil {
1204			return o.modeset()
1205		}
1206		if !refused(err) || got == imagesLinear {
1207			o.freeImages()
1208			return err
1209		}
1210		o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
1211		o.freeImages()
1212		kind, cause = got+1, err
1213	}
1214}
1215
1216// refused reports a modeset error meaning KMS rejects the images.
1217func refused(err error) bool {
1218	return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)
1219}
1220
1221// lostMaster reports a commit error meaning the seat is switched away.
1222func lostMaster(err error) bool {
1223	return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM)
1224}
1225
1226// setupImages gives the output the two renderer images it flips between,
1227// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that
1228// cannot export a scanout-capable image cannot drive the output.
1229func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
1230	err := cause
1231	for ; kind <= imagesLinear; kind++ {
1232		mods := []uint64(nil)
1233		if kind == imagesLinear {
1234			mods = []uint64{0}
1235		}
1236		if err = o.exportImages(r, mods); err == nil {
1237			return kind, nil
1238		}
1239		o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
1240	}
1241	return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
1242}
1243
1244// exportImages makes the renderer's exported targets the output images.
1245func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
1246	bufs, err := r.ExportTargets(len(o.fbs), mods)
1247	if err != nil {
1248		return err
1249	}
1250	for i := range bufs {
1251		if err == nil {
1252			o.fbs[i], err = o.k.addFB(&bufs[i], fourccXRGB)
1253		}
1254		bufs[i].Planes[0].File.Close()
1255	}
1256	// GPU memory starts undefined (old VRAM contents): clear both images
1257	// before the modeset shows one.
1258	for i := range o.fbs {
1259		if err != nil {
1260			break
1261		}
1262		r.UseTarget(i)
1263		var done *os.File
1264		if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
1265			// The modeset is a blocking commit: wait for the clear.
1266			err = errors.Join(err, syncfile.Wait(context.Background(), done))
1267			done.Close()
1268		}
1269	}
1270	if err != nil {
1271		o.freeImages()
1272		_, _ = r.ExportTargets(0, nil)
1273		return err
1274	}
1275	o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
1276	return nil
1277}
1278
1279// freeImages removes the output images' framebuffers.
1280func (o *Output) freeImages() {
1281	for i, fb := range o.fbs {
1282		if fb != 0 {
1283			_ = o.k.rmFB(fb)
1284		}
1285		o.fbs[i] = 0
1286	}
1287}
1288
1289// shownBy is the content Seq of each window the scene draws.
1290func shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 {
1291	out := map[ports.WindowID]uint64{}
1292	for id, seq := range seen {
1293		if s.Shows(id) {
1294			out[id] = seq
1295		}
1296	}
1297	return out
1298}

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