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