main df9bf83

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

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