feat/data-dnd e05cb15

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

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 25

Tags 10

↑↓ moveenter openesc close