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