v0.2.0 13c6a90

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

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 26

Tags 10

↑↓ moveenter openesc close