v0.8.0 446d1e3

output.go RawHistoryFolder

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

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