v0.3.0 cf370a0

output.go RawHistoryFolder

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

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