output.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"os"
8	"slices"
9	"time"
10	"unsafe"
11
12	"github.com/bnema/neferwl/internal/adapters/capture"
13	"github.com/bnema/neferwl/internal/adapters/clock"
14	"github.com/bnema/neferwl/internal/adapters/presented"
15
16	"github.com/bnema/neferwl/internal/ports"
17	"github.com/bnema/zerowrap"
18	"golang.org/x/sys/unix"
19)
20
21// Output is one KMS connector on a CRTC, driven by atomic commits (ADR
22// 015). One goroutine runs it (Run) and closes it. Every commit that
23// changes the screen asks for an event and leaves the output pending until
24// it arrives: the next commit waits for it. The kernel may still refuse
25// it with EBUSY (it retries soon), and an event that never comes is
26// bounded (stuckTimeout, then a modeset).
27type Output struct {
28	// Set before Run. The backend owns instance registration and wake routing.
29	Security         ports.SessionSecurity
30	SecurityChanges  <-chan ports.SecurityState
31	Instance         ports.OutputInstance
32	SecurityEvents   chan<- ports.SecurityBackendEvent
33	securityState    ports.SecurityState
34	securityPrepared bool
35	securityInvalid  bool
36	inactiveOnClose  bool            // affirmative terminal KMS result, owned by Close
37	runContext       context.Context // bounds compositor clear waits during Run startup
38
39	k       kms
40	flipped <-chan flipEvent // commit events of this CRTC, from Card.ReadEvents
41	crtc    uint32
42	conn    connector
43	mode    modeInfo
44	saved   modeCrtc
45	monitor Monitor
46	hdr     hdrState
47	log     zerowrap.Logger
48	// clock paces the periodic stats and renderer trim (nil: system).
49	clock ports.Clock
50	// Properties: CRTC and connector property IDs by name.
51	crtcProps map[string]uint32
52	connCrtc  uint32 // the connector's CRTC_ID property
53	modeBlob  uint32
54	// primary shows frames; cursor is the cursor plane (nil: none);
55	// stray are other planes that can show on this CRTC, turned off at
56	// every modeset when a previous master left them on it.
57	primary *plane
58	cursor  *Cursor
59	stray   []*plane
60	// overlay shows one window's buffer above the composed frame
61	// (overlay.go); overlayOn is the buffer on it in the pending or last
62	// frame (0: off), overlayReason why it is off.
63	overlay       *plane
64	overlayOn     uint64
65	overlayReason string
66	// fbs are the two renderer images frames alternate between, back the
67	// one the next frame draws into (ADR 014: zero copy).
68	fbs   [2]uint32
69	back  int
70	frame frameLifecycle // pending commit, serials, deadlines and timer
71	flips int
72	// NewCaptureRenderer makes the child renderer of a hidden workspace
73	// capture session (Scene.CaptureScene). Nil: such captures fail closed.
74	// Set before Run.
75	NewCaptureRenderer func(w, h int) (ports.Renderer, error)
76	// StartOff, set before Run, keeps the display off until a scene turns
77	// it on: a display that reconnects while turned off never lights up.
78	StartOff bool
79	// capHidden limits a report to what the child renderer of a hidden
80	// workspace was given, window by window, while it may still read (its
81	// device is not ordered with the display's fences). capped is set when
82	// the last report had child reads: it is repeated until they finish.
83	capHidden func(map[ports.WindowID]uint64) (map[ports.WindowID]uint64, map[ports.WindowID]uint64, bool)
84	capped    bool
85	// readFences are fences of frames rendered but not committed: the
86	// GPU may still read client buffers until they signal, so what was
87	// seen is reported only then (or after a later frame flipped).
88	readFences []*os.File
89	// Direct scanout: client framebuffers by DMABuf ID and the buffer on
90	// screen and queued (0: the composed image).
91	scanout       bool
92	clientFBs     map[uint64]*clientFB
93	shown, queued uint64
94	planeRect     planeRect
95	reason        string // why the last frame was composed ("" = scanout)
96	reports       presented.Queue
97	// Last immutable shows snapshot sent in a flip. Never mutate it: wayland
98	// may still be reading a prior report on another goroutine.
99	showsSnapshot       map[ports.WindowID]uint64
100	directShowsSnapshot map[ports.WindowID]uint64
101	showsScratch        map[ports.WindowID]uint64
102	// Tearing (ADR 006): async commits of scanned-out buffers that ask for
103	// them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence
104	// is set when an async commit may carry IN_FENCE_FD (probed once).
105	tearing     bool
106	asyncFence  bool
107	asyncProbed bool
108	async       bool
109	// VRR: the CRTC's VRR_ENABLED property (0: none), on while a buffer is
110	// scanned out or a fullscreen window covers the output, set inside
111	// frame commits. vrrGame is that state for the last frame.
112	vrrProp       uint32
113	vrrOn         bool
114	vrrGame       bool
115	contentProp   uint32
116	contentValues [5]uint64
117	contentValue  uint64
118	contentWanted uint64
119	composedSince time.Time
120	// lastFrame is when the last frame was committed. cursorHeld: a cursor
121	// move waits for the next frame (see cursorWaits).
122	lastFrame  time.Time
123	cursorHeld bool
124	// traceFlips logs every completion with its timing; lastFlipAt is
125	// the previous frame flip's kernel timestamp.
126	traceFlips bool
127	lastFlipAt time.Duration
128	// vrrFlipGap is the minimum time between a game frame's flip event
129	// and the next frame commit under VRR (render.vrr-flip-gap, 0: off);
130	// flipGapUntil is when the next frame may commit. See vrr_flip_gap.go.
131	vrrFlipGap   time.Duration
132	flipGapUntil time.Time
133	// wantOff is the latest Scene.Off: a client turned the display off.
134	// off: the CRTC is inactive for it. Every modeset (resume, recovery)
135	// follows wantOff, so a display turned off never lights up.
136	wantOff, off bool
137	// protected is an output-owned latch. Ordinary modesets must not expose
138	// a previous desktop target while it is set; only protectedModeset may
139	// activate a target after clearing it and waiting for the GPU.
140	protected bool
141	// protectBackoff is the current retry delay after a refused protected
142	// commit; protectNotBefore the earliest next attempt (zero: none).
143	protectBackoff   time.Duration
144	protectNotBefore time.Time
145	// kind is how the images were made.
146	kind imageKind
147	// formats receives the direct scanout formats after each modeset;
148	// sampled and device are what they are built from (Want).
149	formats   chan<- ports.OutputFormats
150	sampled   []ports.DMABufFormat
151	device    uint64
152	ready     chan error // first successful modeset, or a startup failure
153	readySent bool
154}
155
156// Ready reports the result of the first modeset exactly once.
157func (o *Output) Ready() <-chan error { return o.ready }
158
159// CursorLoader returns the image of a cursor at an output scale, already
160// rotated by the output transform t (the cursor plane is not rotated by the
161// hardware), at most limit pixels on a side; an empty image hides the cursor.
162type CursorLoader func(c ports.CursorChange, scale float64, t ports.BufferTransform, limit int) (ports.CursorImage, error)
163
164// newOutput reads the CRTC's planes and properties for a connector on crtc.
165func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
166	log := card.log
167	pipe := slices.Index(card.crtcs, crtc)
168	o := &Output{k: card.k, flipped: card.flips[crtc], frame: frameLifecycle{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", ready: make(chan error, 1), traceFlips: card.want.TraceFlips, vrrFlipGap: card.want.VRRFlipGap}
169	var err error
170	if o.saved, err = getCrtc(card.fd, crtc); err != nil {
171		log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
172	}
173	if err := o.readProps(card.fd, pipe, card.taken, cursorSize(card.fd)); err != nil {
174		return nil, err
175	}
176	o.hdr.settings = normalizedHDRSettings(card.want.HDR[c.name])
177	o.hdr.cap = detectHDR(o.monitor, o.hdr.props)
178	log.Info().Str("connector", c.name).Bool("hdr_capable", o.hdr.cap.Capable).Str("reason", o.hdr.cap.Reason).Float64("max_luminance", o.hdr.cap.MaxLuminance).Float64("max_frame_average", o.hdr.cap.MaxFrameAverage).Float64("min_luminance", o.hdr.cap.MinLuminance).Msg("HDR capability")
179	if o.hdr.settings.Enabled && !o.hdr.cap.Capable {
180		log.Warn().Str("connector", c.name).Str("reason", o.hdr.cap.Reason).Msg("HDR requested but unavailable")
181	}
182	o.tearing = card.async && !card.want.NoTearing
183	if !card.want.NoVRR {
184		o.vrrProp = vrrProperty(o.k, c.id, crtc)
185	}
186	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")
187	return o, nil
188}
189
190// readProps finds the output's planes and property IDs. taken are planes
191// other outputs drive.
192func (o *Output) readProps(fd, pipe int, taken map[uint32]bool, cursorSide int) error {
193	cp, err := o.k.objProps(o.crtc, objCrtc)
194	if err != nil {
195		return fmt.Errorf("crtc properties: %w", err)
196	}
197	o.crtcProps = map[string]uint32{}
198	for name, v := range cp {
199		o.crtcProps[name] = uint32(v[0])
200	}
201	np, err := o.k.objProps(o.conn.id, objConnector)
202	if err != nil {
203		return fmt.Errorf("connector properties: %w", err)
204	}
205	o.connCrtc = uint32(np["CRTC_ID"][0])
206	// The connector's property metadata is needed for enum/range capability.
207	o.hdr.props = readConnectorHDRProps(fd, np)
208	o.contentProp, o.contentValues = readContentTypeProp(fd, np)
209	o.contentValue = np["content type"][1]
210	o.contentWanted = o.contentValue
211	if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 {
212		return errors.New("crtc or connector lacks atomic properties")
213	}
214	planes, err := readPlanes(o.k, pipe)
215	if err != nil {
216		return fmt.Errorf("planes: %w", err)
217	}
218	pick := func(typ uint32) *plane {
219		var best *plane
220		for _, p := range planes {
221			if p.typ != typ || taken[p.id] {
222				continue
223			}
224			// Prefer the plane already on this CRTC.
225			if best == nil || p.crtc == o.crtc && best.crtc != o.crtc {
226				best = p
227			}
228		}
229		return best
230	}
231	if o.primary = pick(planePrimary); o.primary == nil {
232		return errors.New("no free primary plane")
233	}
234	if p := pick(planeCursor); p != nil {
235		o.cursor = newCursor(p, cursorSide)
236	}
237	for _, p := range planes {
238		if p != o.primary && (o.cursor == nil || p != o.cursor.plane) && !taken[p.id] {
239			o.stray = append(o.stray, p)
240		}
241	}
242	o.pickOverlay()
243	return nil
244}
245
246// owned are the planes this output drives.
247func (o *Output) owned() []*plane {
248	out := []*plane{o.primary}
249	if o.cursor != nil {
250		out = append(out, o.cursor.plane)
251	}
252	if o.overlay != nil {
253		out = append(out, o.overlay)
254	}
255	return out
256}
257
258// cursorSize is the cursor plane side: the smaller of the driver's width
259// and height caps, 64 when it does not say.
260func cursorSize(fd int) int {
261	size := 0
262	for _, c := range []uint64{capCursorW, capCursorH} {
263		v := getCap{capability: c}
264		if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 {
265			if size == 0 || int(v.value) < size {
266				size = int(v.value)
267			}
268		}
269	}
270	if size == 0 {
271		size = 64
272	}
273	return size
274}
275
276// Cursor is the hardware cursor, or nil when the CRTC has no cursor plane.
277func (o *Output) Cursor() *Cursor { return o.cursor }
278
279// Info describes the output for core and clients. Unknown EDID fields are empty.
280func (o *Output) Info() ports.OutputInfo {
281	known := func(v string) string {
282		if v == "Unknown" {
283			return ""
284		}
285		return v
286	}
287	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}
288}
289
290func (o *Output) Width() int  { return int(o.mode.HDisplay) }
291func (o *Output) Height() int { return int(o.mode.VDisplay) }
292
293// primaryProps shows fb full screen on the primary plane.
294func (o *Output) primaryProps(req *atomicReq, fb uint32) {
295	o.primaryRectProps(req, fb, fullPlaneRect(o.Width(), o.Height()))
296}
297
298func (o *Output) primaryRectProps(req *atomicReq, fb uint32, rect planeRect) {
299	p := o.primary
300	req.set(p.id, p.prop("FB_ID"), uint64(fb))
301	req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
302	for i, name := range [...]string{"SRC_X", "SRC_Y", "SRC_W", "SRC_H"} {
303		req.set(p.id, p.prop(name), rect.src[i])
304	}
305	for i, name := range [...]string{"CRTC_X", "CRTC_Y", "CRTC_W", "CRTC_H"} {
306		req.set(p.id, p.prop(name), rect.crtc[i])
307	}
308}
309
310// modeset shows the front image with the output's mode, turning off every
311// other plane on the CRTC; needed at start and after every VT resume.
312// It blocks until the kernel applied it.
313func (o *Output) modeset() error {
314	o.observeSecurity()
315	return o.modesetImage(o.fbs[1-o.back], !o.wantOff && !o.protected)
316}
317
318// modesetImage is called with an active protected image only by
319// protectedModeset, after its clear fence has signalled.
320func (o *Output) modesetImage(fb uint32, active bool) error {
321	if o.protected && o.frame.pendingCommit() {
322		return errProtectionPending
323	}
324	if o.protected && !active {
325		if err := o.disableProtected(); err != nil {
326			return err
327		}
328		o.frame.resetAfterModeset()
329		o.planeRect = fullPlaneRect(o.Width(), o.Height())
330		o.signalReady()
331		return nil
332	}
333	blob, err := o.modeBlobFor(o.mode)
334	if err != nil {
335		if o.protected {
336			return protectedCommitError{err}
337		}
338		return err
339	}
340	var req *atomicReq
341	if o.protected {
342		req = o.modesetBaseReq(blob, active)
343		if err := o.detachProtectedPlanes(req); err != nil {
344			_ = o.k.destroyBlob(blob)
345			return protectedCommitError{err}
346		}
347		if active {
348			o.primaryProps(req, fb)
349		}
350	} else {
351		req = o.modesetReq(blob, active)
352	}
353	// Do not admit a desktop-front modeset after an engage observed while
354	// constructing the request. Protected activation only contains black.
355	wasProtected := o.protected
356	o.observeSecurity()
357	if active && !wasProtected && o.protected {
358		_ = o.k.destroyBlob(blob)
359		return errSecurityScene
360	}
361	if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
362		_ = o.k.destroyBlob(blob)
363		if o.protected {
364			return protectedCommitError{fmt.Errorf("modeset: %w", err)}
365		}
366		return fmt.Errorf("modeset: %w", err)
367	}
368	// Only now is nothing of ours pending or on screen: on failure the
369	// previous buffers may still show.
370	o.frame.resetAfterModeset()
371	// Plane scaling capabilities may change after a modeset or VT resume.
372	for _, fb := range o.clientFBs {
373		fb.scaleTestedOK, fb.scaleRefused = false, false
374	}
375	o.shown, o.queued = 0, 0
376	if o.modeBlob != 0 {
377		_ = o.k.destroyBlob(o.modeBlob)
378	}
379	o.modeBlob = blob
380	o.vrrOn, o.vrrGame, o.overlayOn, o.off = false, false, 0, !active
381	// The gap and the traced flip interval belonged to the old state.
382	o.flipGapUntil, o.lastFlipAt = time.Time{}, 0
383	o.contentValue, o.contentWanted = o.contentValues[0], o.contentValues[0]
384	o.planeRect = fullPlaneRect(o.Width(), o.Height())
385	if o.cursor != nil {
386		o.cursor.applied = cursorState{}
387		o.cursor.screen, o.cursor.flying = 0, false
388	}
389	o.log.Info().Str("connector", o.conn.name).Bool("off", o.off).Msg("modeset")
390	if !o.off && !o.protected {
391		// An inactive CRTC tells nothing about the cursor plane. Protected
392		// activation keeps it detached; do not test desktop cursor images.
393		o.testCursor()
394	}
395	o.sendFormats()
396	o.signalReady()
397	return nil
398}
399
400// signalReady reports the first successful modeset once.
401func (o *Output) signalReady() {
402	if o.ready != nil && !o.readySent {
403		o.ready <- nil
404		o.readySent = true
405	}
406}
407
408// powerOff turns the display off: the CRTC goes inactive and keeps its
409// mode, as DPMS off. The commit blocks, so no event is pending after it;
410// a modeset turns the display back on.
411func (o *Output) powerOff() error {
412	req := &atomicReq{}
413	req.set(o.crtc, o.crtcProps["ACTIVE"], 0)
414	req.set(o.crtc, o.vrrProp, 0)
415	req.set(o.conn.id, o.contentProp, o.contentValues[0])
416	if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
417		return fmt.Errorf("power off: %w", err)
418	}
419	o.off, o.vrrOn, o.vrrGame = true, false, false
420	o.contentValue, o.contentWanted = o.contentValues[0], o.contentValues[0]
421	o.log.Info().Str("connector", o.conn.name).Msg("power off")
422	// An inactive output offers neither HDR nor direct scanout.
423	o.sendFormats()
424	return nil
425}
426
427// sendFormats reports the formats clients may allocate for direct
428// scanout on this output (none when scanout is off). It never blocks:
429// wayland keeps the latest per output, a dropped report is resent at the
430// next modeset.
431func (o *Output) sendFormats() {
432	if o.formats == nil {
433		return
434	}
435	f := ports.OutputFormats{Output: o.conn.name, Device: o.device}
436	if o.hdr.on && !o.off {
437		f.HDR = &ports.OutputHDR{MaxLuminance: o.hdr.cap.MaxLuminance, MaxFrameAverage: o.hdr.cap.MaxFrameAverage, MinLuminance: o.hdr.cap.MinLuminance}
438	}
439	if o.scanout && !o.off {
440		for _, format := range o.scanoutFormats(o.sampled) {
441			if !isYUVFormat(format.Format) && isTenBit(format.Format) == o.hdr.on {
442				f.Formats = append(f.Formats, format)
443			}
444		}
445	}
446	select {
447	case o.formats <- f:
448	default:
449		o.log.Warn().Str("connector", o.conn.name).Msg("scanout formats not sent: wayland busy")
450	}
451}
452
453// testModeset asks KMS whether it would take the modeset of the output
454// images without applying it.
455func (o *Output) testModeset() error {
456	blob, err := o.modeBlobFor(o.mode)
457	if err != nil {
458		return err
459	}
460	defer func() { _ = o.k.destroyBlob(blob) }()
461	// Tested active: an inactive CRTC would accept any image.
462	if err := o.k.commit(o.modesetReq(blob, true), atomicTestOnly|atomicAllowModes, 0); err != nil {
463		return fmt.Errorf("modeset test: %w", err)
464	}
465	return nil
466}
467
468func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) {
469	blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode)))
470	if err != nil {
471		return 0, fmt.Errorf("mode blob: %w", err)
472	}
473	return blob, nil
474}
475
476// testCursor turns the hardware cursor off when KMS refuses its images on
477// the cursor plane (e.g. a pitch the plane cannot scan out).
478func (o *Output) testCursor() {
479	c := o.cursor
480	if c == nil || c.off || c.fbs[0] == 0 {
481		return
482	}
483	req := &atomicReq{}
484	c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
485	if err := o.k.commit(req, atomicTestOnly, 0); refused(err) {
486		o.cursorOff(err)
487	}
488}
489
490// cursorOff stops using the cursor plane for the rest of the output's life.
491func (o *Output) cursorOff(err error) {
492	o.cursor.off = true
493	o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off")
494}
495
496// modesetBaseReq sets the CRTC and connector without attaching any plane.
497func (o *Output) modesetBaseReq(blob uint32, active bool) *atomicReq {
498	req := &atomicReq{}
499	req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob))
500	req.set(o.crtc, o.crtcProps["ACTIVE"], boolValue(active))
501	req.set(o.crtc, o.vrrProp, 0)
502	req.set(o.conn.id, o.contentProp, o.contentValues[0])
503	req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
504	o.hdrConnectorProps(req, o.hdr.on)
505	return req
506}
507
508// modesetReq is the unlocked modeset of the front image with mode blob;
509// active false keeps the display off (output power).
510func (o *Output) modesetReq(blob uint32, active bool) *atomicReq {
511	req := o.modesetBaseReq(blob, active)
512	o.primaryProps(req, o.fbs[1-o.back])
513	if o.cursor != nil {
514		o.cursor.props(req, o.crtc, cursorState{})
515	}
516	o.overlayProps(req, overlayWin{})
517	for _, p := range o.stray {
518		// Only planes a previous master left on this CRTC.
519		props, err := o.k.objProps(p.id, objPlane)
520		if err != nil || uint32(props["CRTC_ID"][1]) != o.crtc {
521			continue
522		}
523		req.set(p.id, p.prop("FB_ID"), 0)
524		req.set(p.id, p.prop("CRTC_ID"), 0)
525	}
526	return req
527}
528
529// commitFrame flips fb in one commit with the cursor and VRR. An async
530// commit carries only the primary plane (kernel rule): a frame that must
531// also move the cursor or change VRR flips at vblank. fence is the
532// frame's GPU fence (nil: none); the caller keeps and closes it.
533func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error {
534	return o.commitWithRect(fb, fence, async, vrr, f, overlayWin{}, fullPlaneRect(o.Width(), o.Height()))
535}
536
537// commitWith is commitFrame with ov on the overlay plane (zero: off).
538func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error {
539	return o.commitWithRect(fb, fence, async, vrr, f, ov, fullPlaneRect(o.Width(), o.Height()))
540}
541
542func (o *Output) commitWithRect(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin, rect planeRect) error {
543	o.observeSecurity()
544	if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) {
545		return errSecurityScene
546	}
547	cur := cursorState{}
548	if o.cursor != nil && !o.protected {
549		cur = o.cursor.desired()
550	}
551	if o.protected {
552		async, vrr, ov = false, false, overlayWin{}
553	}
554	if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn || rect != o.planeRect || o.contentWanted != o.contentValue) {
555		async = false
556	}
557	req := &atomicReq{}
558	flags := uint32(atomicNonblock | flipEventFlag)
559	if async {
560		flags |= flipAsyncFlag
561		req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb))
562	} else {
563		o.primaryRectProps(req, fb, rect)
564		req.set(o.crtc, o.vrrProp, boolValue(vrr))
565		if o.cursor != nil {
566			o.cursor.props(req, o.crtc, cur)
567		}
568		if ov.buf != 0 || o.overlayOn != 0 {
569			o.overlayProps(req, ov)
570		}
571	}
572	o.contentProps(req)
573	if fence != nil {
574		req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
575	}
576	// Admission linearizes at this last defensive snapshot. An already
577	// admitted native ioctl can complete, but precedes the black proof.
578	o.observeSecurity()
579	if o.Security != nil && (f.security != o.securityState || o.protected && !o.securityPrepared) {
580		return errSecurityScene
581	}
582	// The kernel takes its own reference on the fence.
583	if err := o.k.commit(req, flags, o.frame.userData(userFrame)); err != nil {
584		if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
585			return o.commitWithRect(fb, fence, false, vrr, f, ov, rect)
586		}
587		if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) && o.contentRefused(fb, fence, vrr, cur, ov, rect) {
588			o.log.Warn().Str("component", "drm").Err(err).Msg("content type refused; disabled")
589			o.contentProp = 0
590			return o.commitWithRect(fb, fence, false, vrr, f, ov, rect)
591		}
592		if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
593			// Retry without turning VRR on: if that passes, the driver
594			// refuses VRR on this output. The overlay stays: the composed
595			// image left its window out.
596			if o.commitWithRect(fb, fence, false, false, f, ov, rect) == nil {
597				o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")
598				o.vrrProp = 0
599				return nil
600			}
601		}
602		return err
603	}
604	if !async {
605		if o.vrrOn != vrr {
606			o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
607		}
608		o.vrrOn = vrr
609	}
610	if o.cursor != nil {
611		// An async commit leaves the cursor plane as applied.
612		o.cursor.committed(cur)
613	}
614	if !async {
615		o.overlayOn = ov.buf
616	}
617	o.contentValue = o.contentWanted
618	o.planeRect = rect
619	o.lastFrame, o.cursorHeld = time.Now(), false
620	f.frame, f.async = true, async
621	f.fences = dupFences(fence, ov.acquire)
622	if o.traceFlips {
623		f.fenceReady = signalled(f.fences)
624	}
625	o.setAsync(async)
626	o.frame.begin(f, time.Now())
627	return nil
628}
629
630// contentRefused tests the same frame without the connector hint before disabling it.
631func (o *Output) contentRefused(fb uint32, fence *os.File, vrr bool, cur cursorState, ov overlayWin, rect planeRect) bool {
632	req := &atomicReq{}
633	o.primaryRectProps(req, fb, rect)
634	req.set(o.crtc, o.vrrProp, boolValue(vrr))
635	if o.cursor != nil {
636		o.cursor.props(req, o.crtc, cur)
637	}
638	if ov.buf != 0 || o.overlayOn != 0 {
639		o.overlayProps(req, ov)
640	}
641	if fence != nil {
642		req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
643	}
644	return o.k.commit(req, atomicTestOnly, 0) == nil
645}
646
647// cursorRefused reports, after a commit carrying the cursor failed with
648// EINVAL, whether the cursor plane was the cause; then the cursor is off
649// and the commit can be retried without it.
650func (o *Output) cursorRefused() bool {
651	o.testCursor()
652	return o.cursor.off
653}
654
655// commitState commits a cursor or VRR change without a new frame.
656func (o *Output) commitState(vrr bool) error {
657	o.observeSecurity()
658	if o.protected {
659		return errSecurityScene
660	}
661	req := &atomicReq{}
662	cur := cursorState{}
663	// A move back to the applied state holds nothing.
664	o.cursorHeld = false
665	if o.cursor != nil {
666		cur = o.cursor.desired()
667		if cur != o.cursor.applied {
668			if o.cursorWaits(time.Now()) {
669				o.cursorHeld = true
670				cur = o.cursor.applied
671			} else {
672				o.cursor.props(req, o.crtc, cur)
673			}
674		}
675	}
676	if vrr != o.vrrOn {
677		req.set(o.crtc, o.vrrProp, boolValue(vrr))
678	}
679	o.contentProps(req)
680	if len(req.objs) == 0 {
681		return nil
682	}
683	o.observeSecurity()
684	if o.protected {
685		return errSecurityScene
686	}
687	if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.frame.userData(userState)); err != nil {
688		if o.overlayConflict(err, o.overlayOn) {
689			// The next frame is composed without the overlay.
690			return errOverlayDropped
691		}
692		if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
693			return o.commitState(vrr)
694		}
695		if o.contentProp != 0 && o.contentWanted != o.contentValue && errors.Is(err, unix.EINVAL) {
696			without := &atomicReq{}
697			if o.cursor != nil && cur != o.cursor.applied {
698				o.cursor.props(without, o.crtc, cur)
699			}
700			if vrr != o.vrrOn {
701				without.set(o.crtc, o.vrrProp, boolValue(vrr))
702			}
703			if len(without.objs) == 0 || o.k.commit(without, atomicTestOnly, 0) == nil {
704				o.contentProp = 0
705				o.log.Warn().Str("component", "drm").Err(err).Msg("content type refused; disabled")
706				return o.commitState(vrr)
707			}
708		}
709		return err
710	}
711	if o.cursor != nil {
712		o.cursor.committed(cur)
713	}
714	if vrr != o.vrrOn {
715		o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
716	}
717	o.vrrOn = vrr
718	o.contentValue = o.contentWanted
719	o.frame.begin(pendingFrame{}, time.Now())
720	return nil
721}
722
723// cursorMinInterval bounds how long a held cursor move waits for a frame.
724const cursorMinInterval = time.Second / 24
725
726// cursorWaits reports whether a cursor move rides on the next frame
727// instead of its own commit. Under VRR a cursor-only commit refreshes the
728// panel at its slowest rate, which delays the game's next frame by up to
729// one such refresh. A game that stops drawing still gets the cursor at
730// cursorMinInterval.
731func (o *Output) cursorWaits(now time.Time) bool {
732	return o.vrrOn && o.vrrGame && now.Sub(o.lastFrame) < cursorMinInterval
733}
734
735func boolValue(b bool) uint64 {
736	if b {
737		return 1
738	}
739	return 0
740}
741
742// commitScanout flips a client buffer. It reports false when KMS refused
743// the buffer: the frame must be composed.
744func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) {
745	return o.commitScanoutRect(fb, c, f, fullPlaneRect(o.Width(), o.Height()))
746}
747
748func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingFrame, rect planeRect) (bool, error) {
749	cfb := o.clientFBs[c.DMABuf.ID]
750	// Drivers refuse async flips that change the format or modifier: the
751	// flip from the composed image into scanout waits for vblank.
752	async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync
753	vrr := o.wantVRR(true)
754	f.queued, f.zeroCopy = c.DMABuf.ID, c.ID
755	// Explicit sync: KMS waits on the client's acquire fence. Wayland may
756	// close its file at any time: commit a duplicate taken while it is
757	// held open (the kernel keeps its own reference).
758	fence := dupFence(c.Acquire)
759	if fence != nil {
760		defer fence.Close()
761	}
762	err := o.commitWithRect(fb, fence, async, vrr, f, overlayWin{}, rect)
763	if err != nil && async && errors.Is(err, unix.EINVAL) {
764		// Refused (e.g. not a fast update): this buffer flips at vblank
765		// from now on.
766		o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
767		cfb.noAsync = true
768		err = o.commitWithRect(fb, fence, false, vrr, f, overlayWin{}, rect)
769	}
770	if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) {
771		// KMS refuses this buffer on the plane: compose it instead.
772		o.log.Info().Str("component", "render").Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")
773		cfb.failed = "flip_refused"
774		o.setScanoutReason("flip_refused")
775		return false, nil
776	}
777	if err == nil {
778		o.queued = c.DMABuf.ID
779	}
780	return true, err
781}
782
783// Close frees buffers and disables scanout. Without a security gate it
784// retains legacy restoration of the CRTC state found at startup; a wired
785// gate never admits saved desktop pixels, even if its snapshot is unlocked.
786func (o *Output) Close() {
787	o.inactiveOnClose = false
788	// Close may run after Run stopped, without receiving the engage wake.
789	o.observeSecurity()
790	req := &atomicReq{}
791	if o.cursor != nil {
792		o.cursor.props(req, o.crtc, cursorState{})
793	}
794	o.overlayProps(req, overlayWin{})
795	o.hdrConnectorProps(req, false)
796	req.set(o.crtc, o.vrrProp, 0)
797	s := o.saved
798	var blob uint32
799	p := o.primary
800	restored := false
801	if o.Security == nil && !o.protected && s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 {
802		if b, err := o.modeBlobFor(s.mode); err == nil {
803			blob, restored = b, true
804			req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b))
805			req.set(o.crtc, o.crtcProps["ACTIVE"], 1)
806			req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
807			req.set(p.id, p.prop("FB_ID"), uint64(s.fbID))
808			req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
809			req.set(p.id, p.prop("SRC_X"), 0)
810			req.set(p.id, p.prop("SRC_Y"), 0)
811			req.set(p.id, p.prop("SRC_W"), uint64(s.mode.HDisplay)<<16)
812			req.set(p.id, p.prop("SRC_H"), uint64(s.mode.VDisplay)<<16)
813			req.set(p.id, p.prop("CRTC_X"), 0)
814			req.set(p.id, p.prop("CRTC_Y"), 0)
815			req.set(p.id, p.prop("CRTC_W"), uint64(s.mode.HDisplay))
816			req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay))
817		}
818	}
819	if !restored {
820		// Nothing to give back: leave the CRTC off.
821		req.set(o.crtc, o.crtcProps["ACTIVE"], 0)
822		req.set(o.crtc, o.crtcProps["MODE_ID"], 0)
823		req.set(p.id, p.prop("FB_ID"), 0)
824		req.set(p.id, p.prop("CRTC_ID"), 0)
825		req.set(o.conn.id, o.connCrtc, 0)
826	}
827	// Only the validated detachment path can certify terminal inactivity.
828	// Legacy nil-gate restoration/disable remains best effort, not evidence.
829	detached := false
830	if o.protected || o.Security != nil {
831		if err := o.detachProtectedPlanes(req); err != nil {
832			o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("protected close plane detachment")
833		} else {
834			detached = o.primary != nil && o.crtc != 0 && o.crtcProps["ACTIVE"] != 0
835		}
836	}
837	// A live metadata blob is freed only once an SDR modeset succeeds.
838	hdrShown := o.hdr.shown()
839	// Best effort: at exit the card may already belong to another session.
840	if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
841		ev := o.log.Debug()
842		if hdrShown {
843			// The display may stay in HDR mode until the next modeset.
844			ev = o.log.Warn()
845		}
846		ev.Err(err).Str("connector", o.conn.name).Bool("hdr", hdrShown).Msg("restore crtc")
847	} else {
848		active, hasActive := req.value(o.crtc, o.crtcProps["ACTIVE"])
849		o.inactiveOnClose = !restored && detached && hasActive && active == 0
850		if hdrShown {
851			o.log.Info().Str("connector", o.conn.name).Msg("HDR10 off")
852		}
853	}
854	if blob != 0 {
855		_ = o.k.destroyBlob(blob)
856	}
857	if o.modeBlob != 0 {
858		_ = o.k.destroyBlob(o.modeBlob)
859		o.modeBlob = 0
860	}
861	o.hdr.releaseBlob(o.k)
862	if o.cursor != nil {
863		o.cursor.free(o.k)
864	}
865	o.freeImages()
866	o.frame.endPending()
867	o.dropRead()
868	o.shown, o.queued = 0, 0
869	o.dropClientFBs(time.Now(), true)
870}
871
872// InactiveOnClose reports affirmative terminal inactivity, not cached power
873// state. Call only after Close's owner has finished and a done channel (or
874// equivalent owner-completion synchronization) has been observed. It is not
875// safe to read concurrently with the output owner; no shared mutation is added.
876func (o *Output) InactiveOnClose() bool { return o.inactiveOnClose }
877
878// Run renders scenes and commits them until ctx ends. active reports seat
879// enable/disable. What the output shows and read is reported on reportsCh.
880func (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, reportsCh chan<- ports.OutputPresented, captures <-chan ports.CaptureRequest, captured chan<- ports.CaptureDone) (runErr error) {
881	defer func() {
882		p := recover()
883		if p != nil {
884			runErr = fmt.Errorf("output panic: %v", p)
885		}
886		if o.ready != nil && !o.readySent {
887			o.ready <- runErr
888			o.readySent = true
889		}
890		if p != nil {
891			panic(p)
892		}
893	}()
894	o.runContext = ctx
895	o.wantOff = o.StartOff
896	o.observeSecurity()
897	r, err := newRenderer(o.Width(), o.Height())
898	if err != nil {
899		return fmt.Errorf("create renderer: %w", err)
900	}
901	defer r.Close()
902	if o.cursor != nil {
903		if err := o.cursor.setup(o.k, r); err != nil {
904			o.cursor.off = true
905			o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")
906		}
907	}
908	// The seat sends its state first; while switched away (e.g. a monitor
909	// plugged in on another VT) the modeset waits for the enable.
910	enabled := true
911	select {
912	case enabled = <-active:
913	default:
914	}
915	// Output images, from the best to the linear fallback (ADR 014).
916	// While switched away they are made on the first enable.
917	if enabled {
918		if err := o.showImages(r, imagesDriver, nil); err != nil {
919			// While protected, a recoverable KMS failure keeps the output
920			// registered and dark; the loop retries with bounded backoff.
921			if !o.protected || !o.commitFailed(err, &enabled) {
922				return err
923			}
924		}
925		if !o.protected {
926			o.probeAsync(r)
927		}
928	}
929	surfaces := make(map[ports.WindowID]ports.SurfaceContent)
930	var scene ports.Scene
931	haveScene, dirty := false, false
932	var want ports.CursorChange
933	cursorScale := -1.0 // not loaded yet
934	var cursorTransform ports.BufferTransform
935	frame := 0
936	var requestStorage [capture.MaxRequests]ports.CaptureRequest
937	requests := requestStorage[:0]
938	ctx, cancelCaptures := context.WithCancel(ctx)
939	pipeline := capture.NewPipeline(ctx, captured)
940	pipeline.Security = o.Security
941	if o.NewCaptureRenderer != nil {
942		pipeline.EnableOffscreen(o.NewCaptureRenderer)
943		o.capHidden = pipeline.CapHiddenSeen
944	}
945	defer func() {
946		cancelCaptures()
947		pipeline.Close(r)
948		for _, q := range requests {
949			if q.ID != 0 || q.Dst.File != nil {
950				capture.Fail(ctx, q, fmt.Errorf("output stopped"), captured)
951			}
952		}
953		for {
954			select {
955			case q := <-captures:
956				capture.Fail(ctx, q, fmt.Errorf("output stopped"), captured)
957			default:
958				return
959			}
960		}
961	}()
962	clk := o.clock
963	if clk == nil {
964		clk = clock.System{}
965	}
966	stats := clk.NewTicker(10 * time.Second)
967	defer stats.Stop()
968	// Timer channels are enabled only when their condition holds. Go 1.27
969	// guarantees a stopped/reset timer cannot deliver its old value.
970	retryTimer := time.NewTimer(time.Hour)
971	vrrTimer := time.NewTimer(time.Hour)
972	cursorTimer := time.NewTimer(time.Hour)
973	protectTimer := time.NewTimer(time.Hour)
974	o.frame.startTimer()
975	gapTimer := time.NewTimer(time.Hour)
976	gapTimer.Stop()
977	defer gapTimer.Stop()
978	// A request waits for a scene that shows its capture indicator, at most
979	// capture.HoldFor (see capture.Hold).
980	holdTimer := time.NewTimer(time.Hour)
981	holdTimer.Stop()
982	defer holdTimer.Stop()
983	retryTimer.Stop()
984	vrrTimer.Stop()
985	cursorTimer.Stop()
986	protectTimer.Stop()
987	defer protectTimer.Stop()
988	defer retryTimer.Stop()
989	defer vrrTimer.Stop()
990	defer cursorTimer.Stop()
991	defer o.frame.stopTimer()
992	// seen is the latest content Seq per window. It is reported only while
993	// no frame is in flight: then the GPU has finished every frame that
994	// read older buffers (the kernel waited for their fence to flip).
995	seen := map[ports.WindowID]uint64{}
996	reportDirty := false
997	var cursorWake <-chan struct{}
998	if o.cursor != nil {
999		cursorWake = o.cursor.wake
1000	}
1001	securityChanges := o.SecurityChanges
1002	for {
1003		if o.observeSecurity() || haveScene && !o.sceneCurrent(scene) {
1004			scene, haveScene, dirty = ports.Scene{}, false, false
1005			want, cursorScale, cursorTransform = ports.CursorChange{}, -1, 0
1006			if o.cursor != nil {
1007				o.cursor.image, o.cursor.later = false, nil
1008			}
1009		}
1010		if o.protected && len(requests) > 0 {
1011			for _, q := range requests {
1012				capture.Fail(ctx, q, fmt.Errorf("session protected"), captured)
1013			}
1014			clear(requests)
1015			requests = requestStorage[:0]
1016		}
1017		if o.protected && enabled && !o.frame.pendingCommit() && !o.securityPrepared && !o.protectBackoffActive() {
1018			if err := o.prepareSecurity(ctx, r); err != nil {
1019				if ctx.Err() != nil {
1020					return nil
1021				}
1022				if !o.commitFailed(err, &enabled) {
1023					return err
1024				}
1025			}
1026			// Recheck immediately: a takeover during the clear needs its own
1027			// transaction, not another scene or wake to make progress.
1028			if enabled && !o.frame.pendingCommit() && !o.securityPrepared && !o.protectBackoffActive() {
1029				continue
1030			}
1031		}
1032		if o.securityInvalid {
1033			if err := o.sendSecurityInvalid(ctx); err != nil {
1034				return nil
1035			}
1036		}
1037		readWait := false
1038		if reportDirty || o.capped {
1039			published := false
1040			if !o.frame.pendingCommit() {
1041				if readWait = !o.readDone(); !readWait {
1042					o.report(nil, seen)
1043					reportDirty, published = false, true
1044					// A report limited by the child's reads is repeated soon.
1045					readWait = o.capped
1046				}
1047			}
1048			if !published && o.capped {
1049				// A display frame or fence that has not finished must not
1050				// keep a finished child read held: child holds do not expire.
1051				// Report them alone, at the last Seen that was safe.
1052				o.report(nil, o.reports.Seen())
1053				readWait = readWait || o.capped
1054			}
1055		}
1056		o.reports.Flush(reportsCh)
1057		// An unsent report is retried soon, not only on the next event;
1058		// so is one that waits for an uncommitted frame's fence.
1059		var retry <-chan time.Time
1060		if o.reports.Len() > 0 || readWait {
1061			retryTimer.Reset(time.Millisecond)
1062			retry = retryTimer.C
1063		} else {
1064			retryTimer.Stop()
1065		}
1066		// A composed screen that stopped changing still drops VRR on time.
1067		var vrrOff <-chan time.Time
1068		if enabled && o.vrrOn && !o.frame.pendingCommit() && !o.composedSince.IsZero() {
1069			vrrTimer.Reset(max(0, vrrHold-time.Since(o.composedSince)))
1070			vrrOff = vrrTimer.C
1071		} else {
1072			vrrTimer.Stop()
1073		}
1074		// A held cursor move commits alone if no frame comes in time.
1075		var cursorDue <-chan time.Time
1076		if enabled && !o.protected && o.cursorHeld && !o.frame.pendingCommit() {
1077			cursorTimer.Reset(max(0, cursorMinInterval-time.Since(o.lastFrame)))
1078			cursorDue = cursorTimer.C
1079		} else {
1080			cursorTimer.Stop()
1081		}
1082		// A game frame waiting out the VRR flip gap commits when it ends.
1083		var gapDue <-chan time.Time
1084		if enabled && dirty && !o.frame.pendingCommit() && !o.flipGapUntil.IsZero() {
1085			gapTimer.Reset(max(0, time.Until(o.flipGapUntil)))
1086			gapDue = gapTimer.C
1087		} else {
1088			gapTimer.Stop()
1089		}
1090		var holdDue <-chan time.Time
1091		if len(requests) > 0 {
1092			var wait time.Duration
1093			requests, wait = pipeline.Expire(scene, requests, time.Now())
1094			if wait > 0 {
1095				holdTimer.Reset(wait)
1096				holdDue = holdTimer.C
1097			}
1098		}
1099		if holdDue == nil {
1100			holdTimer.Stop()
1101		}
1102		// A refused protected commit is retried after a bounded backoff.
1103		var protectDue <-chan time.Time
1104		if o.protected && enabled && !o.securityPrepared && o.protectBackoffActive() {
1105			protectTimer.Reset(max(0, time.Until(o.protectNotBefore)))
1106			protectDue = protectTimer.C
1107		} else {
1108			protectTimer.Stop()
1109		}
1110		// A commit whose event never comes must not stop the output.
1111		stuck := o.frame.wait(enabled)
1112		stateDirty := false
1113		select {
1114		case _, ok := <-securityChanges:
1115			if !ok {
1116				securityChanges = nil
1117			}
1118			continue // gate is authoritative, not the queued wake payload
1119		case <-retry:
1120			continue
1121		case <-protectDue:
1122			continue
1123		case <-stuck:
1124			o.observeSecurity()
1125			if !o.frame.deadlineReached(time.Now()) {
1126				continue // waits for a fence
1127			}
1128			if o.expire() {
1129				if o.protected {
1130					o.invalidateSecurity()
1131					continue // fresh black transaction at the top of the loop
1132				}
1133				if err := o.modeset(); err != nil {
1134					if !o.commitFailed(err, &enabled) {
1135						return fmt.Errorf("recovery modeset: %w", err)
1136					}
1137					continue
1138				}
1139			}
1140			dirty = haveScene
1141			stateDirty = true
1142		case <-vrrOff:
1143			stateDirty = true
1144		case <-cursorDue:
1145			stateDirty = true
1146		case <-cursorWake:
1147			stateDirty = true
1148		case <-gapDue:
1149		case <-holdDue:
1150			continue
1151		case <-ctx.Done():
1152			return nil
1153		case b := <-pipeline.Completed():
1154			pipeline.Recycle(b, r)
1155			continue
1156		case b := <-pipeline.HiddenCompleted():
1157			pipeline.RecycleHidden(b)
1158			pipeline.Retire(scene)
1159			continue
1160		case q := <-captures:
1161			o.observeSecurity()
1162			if o.protected || !enabled || o.off || o.wantOff {
1163				capture.Fail(ctx, q, fmt.Errorf("output off"), captured)
1164			} else if len(requests) == cap(requests) {
1165				capture.Fail(ctx, q, fmt.Errorf("output capture batch full"), captured)
1166			} else {
1167				q.Since = time.Now()
1168				requests = append(requests, q)
1169				// Its indicator may already be on screen; else it waits for the
1170				// scene that shows it.
1171				dirty = dirty || haveScene && capture.IndicatorShown(scene, q)
1172			}
1173		case on := <-active:
1174			o.observeSecurity()
1175			if o.protected {
1176				o.invalidateSecurity()
1177				enabled = on
1178				if on && o.fbs[0] == 0 {
1179					if err := o.showImages(r, imagesDriver, nil); err != nil {
1180						if !o.commitFailed(err, &enabled) {
1181							return err
1182						}
1183					}
1184				}
1185				continue
1186			}
1187			// Modeset on every enable: a fast disable+enable can coalesce to one true.
1188			if on {
1189				enabled = true // this enable may supersede a previous master loss
1190				var err error
1191				if o.fbs[0] == 0 {
1192					err = o.showImages(r, imagesDriver, nil)
1193				} else {
1194					err = o.modeset()
1195				}
1196				if err != nil {
1197					if !o.commitFailed(err, &enabled) {
1198						return err
1199					}
1200					continue // epoch retry or wait disabled for master reacquisition
1201				}
1202				o.observeSecurity()
1203				if o.protected {
1204					continue
1205				}
1206				o.probeAsync(r)
1207				dirty = haveScene
1208				stateDirty = true
1209			}
1210			enabled = on
1211			o.log.Info().Bool("enabled", on).Msg("output")
1212		case ev := <-o.flipped:
1213			o.observeSecurity()
1214			if !o.completed(ev, seen) {
1215				continue // stale event from before a modeset
1216			}
1217			stateDirty = true
1218			if o.cursor != nil && !o.protected {
1219				// An image that waited for a free slot loads now.
1220				if _, err := o.cursor.flushLater(r); err != nil {
1221					o.log.Warn().Err(err).Msg("cursor")
1222				}
1223			}
1224		case s := <-scenes:
1225			o.observeSecurity()
1226			if !o.sceneCurrent(s) {
1227				continue
1228			}
1229			if o.protected {
1230				s.CaptureScene = nil
1231				if o.wantOff != s.Off {
1232					o.invalidateSecurity()
1233				}
1234			}
1235			if !o.protected && o.cursor != nil && loadCursor != nil && (s.Scale != cursorScale || s.Transform != cursorTransform) {
1236				cursorScale, cursorTransform = s.Scale, s.Transform
1237				o.setCursor(r, loadCursor, want, s.Scale, s.Transform)
1238				stateDirty = true
1239			}
1240			scene, haveScene, dirty = s, true, true
1241			o.wantOff = s.Off
1242		case c := <-cursor:
1243			o.observeSecurity()
1244			if o.protected {
1245				continue
1246			}
1247			want = c
1248			// Before the first scene the scale is unknown: loaded then.
1249			if o.cursor != nil && loadCursor != nil && cursorScale > 0 {
1250				o.setCursor(r, loadCursor, want, cursorScale, cursorTransform)
1251				stateDirty = true
1252			}
1253		case c := <-contents:
1254			if c.Empty() {
1255				delete(surfaces, c.ID)
1256			} else {
1257				surfaces[c.ID] = c
1258			}
1259			if c.Seq > seen[c.ID] {
1260				seen[c.ID] = c.Seq
1261				reportDirty = true
1262			}
1263			// Windows on other outputs or workspaces do not need a frame.
1264			dirty = dirty || capture.Shows(scene, c.ID)
1265		case <-stats.C():
1266			now := clk.Now()
1267			pipeline.Retire(scene) // an idle child of an ended session goes
1268			o.dropClientFBs(now, false)
1269			// An idle output renders nothing: free what windows that left
1270			// it held.
1271			if err := r.Trim(now); err != nil {
1272				return fmt.Errorf("trim renderer: %w", err)
1273			}
1274			ev := o.log.Info().Str("connector", o.conn.name).Int("frames", frame).Int("flips", o.flips).Bool("pending", o.frame.pendingCommit())
1275			if o.cursor != nil {
1276				cs := o.cursor.TakeStats()
1277				ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_commits", cs.Commits)
1278			}
1279			ev.Msg("stats")
1280		}
1281		o.observeSecurity()
1282		if haveScene && !o.sceneCurrent(scene) {
1283			continue
1284		}
1285		if o.protected && o.wantOff != o.off {
1286			o.invalidateSecurity()
1287		}
1288		if o.protected && !o.securityPrepared {
1289			continue
1290		}
1291		if len(requests) > 0 && (o.protected || !enabled || o.off || o.wantOff) {
1292			for _, q := range requests {
1293				capture.Fail(ctx, q, fmt.Errorf("output off"), captured)
1294			}
1295			clear(requests)
1296			requests = requestStorage[:0]
1297		}
1298		if !enabled || o.frame.pendingCommit() {
1299			continue
1300		}
1301		// Output power: off waits for no commit in flight; on is a modeset
1302		// and a full frame.
1303		if o.wantOff != o.off {
1304			var err error
1305			if o.wantOff {
1306				err = o.powerOff()
1307			} else if err = o.modeset(); err == nil {
1308				dirty = true
1309				if !o.protected {
1310					o.probeAsync(r) // skipped while it started off
1311				}
1312			}
1313			if err != nil && !o.commitFailed(err, &enabled) {
1314				return err
1315			}
1316		}
1317		if o.off {
1318			continue
1319		}
1320		if !dirty || !haveScene {
1321			if stateDirty && !o.protected {
1322				err := o.commitState(o.stateVRR())
1323				if errors.Is(err, errSecurityScene) {
1324					continue
1325				}
1326				if errors.Is(err, errOverlayDropped) {
1327					dirty = haveScene
1328					continue
1329				}
1330				if err != nil && !o.commitFailed(err, &enabled) {
1331					return fmt.Errorf("commit: %w", err)
1332				}
1333			}
1334			continue
1335		}
1336		if !o.flipGapUntil.IsZero() {
1337			if time.Now().Before(o.flipGapUntil) {
1338				continue // gapDue commits it
1339			}
1340			o.flipGapUntil = time.Time{}
1341		}
1342		if !o.sceneCurrent(scene) {
1343			continue
1344		}
1345		start := time.Now()
1346		direct, err := o.submitFrame(ctx, r, scene, surfaces, seen, requests, pipeline)
1347		var fatal renderError
1348		if errors.As(err, &fatal) || errors.Is(err, errSecurityScene) {
1349			// A failed render or epoch rejection did not hand these requests to the worker.
1350			for _, q := range requests {
1351				if capture.Handed(q) {
1352					continue // already owned by the worker, or answered
1353				}
1354				capture.Fail(ctx, q, err, captured)
1355			}
1356			clear(requests) // held ones included: their frame is gone
1357		}
1358		// Requests still waiting for their indicator stay; the rest is gone.
1359		requests = capture.Waiting(requests, scene)
1360		if errors.As(err, &fatal) {
1361			return err
1362		}
1363		if err != nil {
1364			if errors.Is(err, errSecurityScene) {
1365				continue
1366			}
1367			if errors.Is(err, errOverlayDropped) {
1368				dirty = true
1369				continue
1370			}
1371			if !o.commitFailed(err, &enabled) {
1372				return fmt.Errorf("page flip: %w", err)
1373			}
1374			continue // dirty stays true
1375		}
1376		dirty = false
1377		frame++
1378		if !o.traceFlips {
1379			continue
1380		}
1381		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")
1382	}
1383}
1384
1385// commitFailed handles a commit error that does not stop the output: a
1386// commit still in flight (EBUSY, retried after its event) or DRM master
1387// lost mid-switch (the seat disable follows).
1388func (o *Output) commitFailed(err error, enabled *bool) bool {
1389	switch {
1390	case errors.Is(err, errSecurityScene), errors.Is(err, errProtectionPending):
1391		return true // retry at the owner boundary, never certify stale work
1392	case errors.Is(err, unix.EBUSY):
1393		// Any CRTC event can unblock a refusal; absent one, retry on time.
1394		if o.frame.busy(time.Now()) {
1395			o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy")
1396		}
1397		if o.protected && !o.securityPrepared {
1398			o.deferProtection() // bounded: a busy protected attempt never spins
1399		}
1400		return true
1401	case retryableProtected(err):
1402		// Protection stays latched and the output stays registered, dark.
1403		// Never stop the output: that would remove it from Wayland while the
1404		// session stays locked. Retry later; no proof is sent meanwhile.
1405		o.deferProtection()
1406		o.log.Warn().Err(err).Str("connector", o.conn.name).Dur("retry_in", o.protectBackoff).Msg("protected commit refused; output stays protected, retrying")
1407		return true
1408	case lostMaster(err):
1409		o.observeSecurity()
1410		if o.protected {
1411			o.invalidateSecurity()
1412		}
1413		o.log.Warn().Err(err).Msg("commit refused")
1414		*enabled = false
1415		return true
1416	}
1417	o.log.Error().Err(err).Msg("commit")
1418	return false
1419}
1420
1421// completed handles the event of the pending commit: a flipped frame is
1422// reported with its kernel timestamp. It reports false for an event of
1423// another commit, which is dropped.
1424func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool {
1425	// Only a commit of ours is traced: not a stale event, nor the end of
1426	// an EBUSY wait. Its fences are read before flip closes them.
1427	trace := o.traceFlips && o.frame.ours(ev.user)
1428	var fenceAt time.Duration
1429	if trace {
1430		fenceAt = fencesSignalledAt(o.frame.pendingFrame.fences)
1431	}
1432	f, start, ok := o.frame.flip(ev.user)
1433	if !ok {
1434		return false
1435	}
1436	if trace {
1437		o.traceFlip(ev, f, start, fenceAt)
1438	}
1439	o.startFlipGap(f)
1440	if age := time.Since(start); age > 20*time.Millisecond {
1441		o.log.Info().Dur("flip_ms", age).Bool("frame", f.frame).Bool("vrr", o.vrrOn).Str("connector", o.conn.name).Msg("slow flip")
1442	}
1443	if o.cursor != nil {
1444		o.cursor.landed()
1445	}
1446	if !f.frame {
1447		return true
1448	}
1449	if f.composed {
1450		// Frames render in order: this flip's fence signalled after
1451		// those of frames rendered before it and not committed.
1452		o.dropRead()
1453	}
1454	o.flips++
1455	o.shown = f.queued
1456	if o.queued == f.queued {
1457		o.queued = 0
1458	}
1459	refresh := time.Duration(0)
1460	if !o.vrrOn {
1461		refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli())))
1462	}
1463	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)
1464	return true
1465}
1466
1467// setCursor loads a cursor for scale and output transform t into the cursor
1468// images.
1469func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) {
1470	o.observeSecurity()
1471	if o.protected || o.cursor.off {
1472		return
1473	}
1474	img, err := load(c, scale, t, o.cursor.Limit())
1475	if err == nil {
1476		err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY)
1477	}
1478	if err != nil {
1479		o.log.Warn().Err(err).Float64("scale", scale).Int("transform", int(t)).Msg("cursor")
1480		return
1481	}
1482	if c.Image != nil {
1483		// Client cursors (games) reload on every change: too many to log.
1484		return
1485	}
1486	o.log.Debug().Float64("scale", scale).Int("transform", int(t)).Str("shape", c.Shape).Int("w", img.W).Int("h", img.H).Msg("cursor image")
1487}
1488
1489// refused reports a modeset error meaning KMS rejects the images.
1490func refused(err error) bool {
1491	return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)
1492}
1493
1494// lostMaster reports a commit error meaning the seat is switched away.
1495func lostMaster(err error) bool {
1496	return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM)
1497}

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