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