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}