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