refactor/kvconf 3462c76

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

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