1package drm
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "maps"
8 "os"
9 "slices"
10 "sync/atomic"
11 "time"
12 "unsafe"
13
14 "github.com/bnema/neferwl/internal/adapters/syncfile"
15 "github.com/bnema/neferwl/internal/ports"
16 "github.com/bnema/zerowrap"
17 "golang.org/x/sys/unix"
18)
19
20// Output is one KMS connector on a CRTC, driven by atomic commits (ADR
21// 015). One goroutine runs it (Run) and closes it. Every commit that
22// changes the screen asks for an event and leaves the output pending until
23// it arrives: the next commit waits for it. The kernel may still refuse
24// it with EBUSY (it retries soon), and an event that never comes is
25// bounded (stuckTimeout, then a modeset).
26type Output struct {
27 k kms
28 flipped <-chan flipEvent // commit events of this CRTC, from Card.ReadEvents
29 crtc uint32
30 conn connector
31 mode modeInfo
32 saved modeCrtc
33 monitor Monitor
34 log zerowrap.Logger
35 // Properties: CRTC and connector property IDs by name.
36 crtcProps map[string]uint32
37 connCrtc uint32 // the connector's CRTC_ID property
38 modeBlob uint32
39 // primary shows frames; cursor is the cursor plane (nil: none);
40 // stray are other planes that can show on this CRTC, turned off at
41 // every modeset when a previous master left them on it.
42 primary *plane
43 cursor *Cursor
44 stray []*plane
45 // overlay shows one window's buffer above the composed frame
46 // (overlay.go); overlayOn is the buffer on it in the pending or last
47 // frame (0: off), overlayReason why it is off.
48 overlay *plane
49 overlayOn uint64
50 overlayReason string
51 // fbs are the two renderer images frames alternate between, back the
52 // one the next frame draws into (ADR 014: zero copy).
53 fbs [2]uint32
54 back int
55 // pending: a commit waits for its event; pendingFrame when it flips a
56 // new frame (else it only changed the cursor or VRR).
57 pending bool
58 pendingFrame pendingFrame
59 // serials numbers commits per card, so an event queued before a
60 // modeset, or for an earlier output on this CRTC, never completes a
61 // newer commit; pendingSerial is the pending commit's.
62 serials *atomic.Uint64
63 pendingSerial, nextSerial uint64
64 flipStart time.Time
65 flips int
66 // A commit whose event does not come by stuckAt is abandoned with a
67 // modeset once its fences signalled (stuckAfter, default
68 // stuckTimeout; fenceWarned once it waits for a fence). A commit
69 // refused with EBUSY is retried after busyRetry; busySince is when
70 // the EBUSY run began, lastBusy the latest EBUSY.
71 stuckAfter time.Duration
72 stuckAt time.Time
73 fenceWarned bool
74 busySince, lastBusy time.Time
75 // readFences are fences of frames rendered but not committed: the
76 // GPU may still read client buffers until they signal, so what was
77 // seen is reported only then (or after a later frame flipped).
78 readFences []*os.File
79 // Direct scanout: client framebuffers by DMABuf ID and the buffer on
80 // screen and queued (0: the composed image).
81 scanout bool
82 clientFBs map[uint64]*clientFB
83 shown, queued uint64
84 reason string // why the last frame was composed ("" = scanout)
85 unsent []ports.OutputPresented
86 // Tearing (ADR 006): async commits of scanned-out buffers that ask for
87 // them, when the card has DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP. asyncFence
88 // is set when an async commit may carry IN_FENCE_FD (probed once).
89 tearing bool
90 asyncFence bool
91 asyncProbed bool
92 async bool
93 // VRR: the CRTC's VRR_ENABLED property (0: none), on while a buffer is
94 // scanned out, set inside frame commits.
95 vrrProp uint32
96 vrrOn bool
97 composedSince time.Time
98 // kind is how the images were made.
99 kind imageKind
100 // formats receives the direct scanout formats after each modeset;
101 // sampled and device are what they are built from (Want).
102 formats chan<- ports.OutputFormats
103 sampled []ports.DMABufFormat
104 device uint64
105}
106
107// pendingFrame is what the pending frame commit shows.
108type pendingFrame struct {
109 frame bool
110 queued uint64
111 zeroCopy ports.WindowID // window shown without composition
112 async bool
113 shows map[ports.WindowID]uint64
114 // fences are duplicates of the fences the commit waits on, owned by
115 // the output until the commit ends. composed is set when the frame
116 // was rendered (its fence is the renderer's).
117 fences []*os.File
118 composed bool
119}
120
121// closeFences closes the fences of f.
122func (f *pendingFrame) closeFences() {
123 for _, fd := range f.fences {
124 fd.Close()
125 }
126 f.fences = nil
127}
128
129// dupFences duplicates the non-nil fences.
130func dupFences(fs ...*os.File) []*os.File {
131 var out []*os.File
132 for _, f := range fs {
133 if d := dupFence(f); d != nil {
134 out = append(out, d)
135 }
136 }
137 return out
138}
139
140// signalled reports whether every fence signalled (readable); an fd that
141// cannot be polled counts as signalled.
142func signalled(fs []*os.File) bool {
143 if len(fs) == 0 {
144 return true
145 }
146 pfds := make([]unix.PollFd, len(fs))
147 for i, f := range fs {
148 pfds[i] = unix.PollFd{Fd: int32(f.Fd()), Events: unix.POLLIN}
149 }
150 if _, err := unix.Poll(pfds, 0); err != nil {
151 return true
152 }
153 for _, p := range pfds {
154 if p.Revents == 0 {
155 return false
156 }
157 }
158 return true
159}
160
161// holdRead keeps the fences of a frame that was not committed.
162func (o *Output) holdRead(fs ...*os.File) {
163 o.readFences = append(o.readFences, dupFences(fs...)...)
164}
165
166// readDone reports whether no uncommitted frame may still read client
167// buffers, closing the fences that signalled.
168func (o *Output) readDone() bool {
169 kept := o.readFences[:0]
170 for _, f := range o.readFences {
171 if signalled([]*os.File{f}) {
172 f.Close()
173 continue
174 }
175 kept = append(kept, f)
176 }
177 clear(o.readFences[len(kept):])
178 o.readFences = kept
179 return len(kept) == 0
180}
181
182// dropRead closes the fences of uncommitted frames.
183func (o *Output) dropRead() {
184 for _, f := range o.readFences {
185 f.Close()
186 }
187 o.readFences = nil
188}
189
190// endPending ends the pending commit, closing its fences.
191func (o *Output) endPending() {
192 o.pendingFrame.closeFences()
193 o.pending, o.pendingFrame = false, pendingFrame{}
194}
195
196// Commit event userData: the commit serial above userKindBits, the kind
197// of commit below.
198const (
199 userFrame = 1
200 userState = 2
201 userKindBits = 2
202)
203
204// userData takes a new serial for the next commit of kind and returns
205// what the commit carries.
206func (o *Output) userData(kind uint64) uint64 {
207 o.nextSerial = o.serials.Add(1)
208 return o.nextSerial<<userKindBits | kind
209}
210
211// begin marks the commit made with the last userData pending.
212func (o *Output) begin(f pendingFrame) {
213 o.pendingSerial = o.nextSerial
214 o.pendingFrame.closeFences()
215 o.pending, o.pendingFrame, o.flipStart = true, f, time.Now()
216 o.stuckAt, o.fenceWarned = o.flipStart.Add(o.stuckLimit()), false
217 o.busySince = time.Time{}
218}
219
220// Bounds on waiting for a commit event: no path may leave an output
221// pending forever.
222const (
223 stuckTimeout = time.Second
224 busyRetry = 50 * time.Millisecond
225)
226
227func (o *Output) stuckLimit() time.Duration {
228 if o.stuckAfter > 0 {
229 return o.stuckAfter
230 }
231 return stuckTimeout
232}
233
234// expire handles a pending commit whose event did not come by stuckAt.
235// A commit still waiting for a fence (a long GPU job) keeps waiting.
236// Else the commit is abandoned so the next can go; it reports whether
237// the output needs a modeset: an event is missing (the kernel state is
238// unknown) or EBUSY lasted past the limit.
239func (o *Output) expire() bool {
240 now := time.Now()
241 age := now.Sub(o.flipStart)
242 if o.pendingSerial != 0 && !signalled(o.pendingFrame.fences) {
243 if !o.fenceWarned {
244 o.fenceWarned = true
245 o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Dur("age", age).Msg("frame waits for GPU fence")
246 }
247 o.stuckAt = now.Add(o.stuckLimit())
248 return false
249 }
250 kind := "state"
251 if o.pendingFrame.frame {
252 kind = "frame"
253 }
254 o.endPending()
255 if o.pendingSerial == 0 {
256 busy := now.Sub(o.busySince)
257 if o.busySince.IsZero() || busy < o.stuckLimit() {
258 return false
259 }
260 o.log.Warn().Str("connector", o.conn.name).Str("kind", "busy").Dur("age", busy).Msg("commits refused as busy; modeset")
261 return true
262 }
263 o.log.Warn().Str("connector", o.conn.name).Uint64("serial", o.pendingSerial).Str("kind", kind).Dur("age", age).Msg("commit event missing; modeset")
264 return true
265}
266
267// CursorLoader returns the image of a cursor at an output scale, at most
268// limit pixels on a side; an empty image hides the cursor.
269type CursorLoader func(c ports.CursorChange, scale float64, limit int) (ports.CursorImage, error)
270
271// newOutput reads the CRTC's planes and properties for a connector on crtc.
272func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, error) {
273 log := card.log
274 pipe := slices.Index(card.crtcs, crtc)
275 o := &Output{k: card.k, flipped: card.flips[crtc], serials: &card.serials, formats: card.formats, sampled: card.want.Sampled, device: card.want.Device, crtc: crtc, conn: c, mode: mode, log: log, monitor: readMonitor(card.path, c.name), scanout: !card.want.NoScanout, clientFBs: map[uint64]*clientFB{}, reason: "start"}
276 var err error
277 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
278 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
279 }
280 if err := o.readProps(pipe, card.taken, cursorSize(card.fd)); err != nil {
281 return nil, err
282 }
283 o.tearing = card.async && !card.want.NoTearing
284 if !card.want.NoVRR {
285 o.vrrProp = vrrProperty(o.k, c.id, crtc)
286 }
287 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")
288 return o, nil
289}
290
291// readProps finds the output's planes and property IDs. taken are planes
292// other outputs drive.
293func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) error {
294 cp, err := o.k.objProps(o.crtc, objCrtc)
295 if err != nil {
296 return fmt.Errorf("crtc properties: %w", err)
297 }
298 o.crtcProps = map[string]uint32{}
299 for name, v := range cp {
300 o.crtcProps[name] = uint32(v[0])
301 }
302 np, err := o.k.objProps(o.conn.id, objConnector)
303 if err != nil {
304 return fmt.Errorf("connector properties: %w", err)
305 }
306 o.connCrtc = uint32(np["CRTC_ID"][0])
307 if o.crtcProps["MODE_ID"] == 0 || o.crtcProps["ACTIVE"] == 0 || o.connCrtc == 0 {
308 return errors.New("crtc or connector lacks atomic properties")
309 }
310 planes, err := readPlanes(o.k, pipe)
311 if err != nil {
312 return fmt.Errorf("planes: %w", err)
313 }
314 pick := func(typ uint32) *plane {
315 var best *plane
316 for _, p := range planes {
317 if p.typ != typ || taken[p.id] {
318 continue
319 }
320 // Prefer the plane already on this CRTC.
321 if best == nil || p.crtc == o.crtc && best.crtc != o.crtc {
322 best = p
323 }
324 }
325 return best
326 }
327 if o.primary = pick(planePrimary); o.primary == nil {
328 return errors.New("no free primary plane")
329 }
330 if p := pick(planeCursor); p != nil {
331 o.cursor = newCursor(p, cursorSide)
332 }
333 for _, p := range planes {
334 if p != o.primary && (o.cursor == nil || p != o.cursor.plane) && !taken[p.id] {
335 o.stray = append(o.stray, p)
336 }
337 }
338 o.pickOverlay()
339 return nil
340}
341
342// owned are the planes this output drives.
343func (o *Output) owned() []*plane {
344 out := []*plane{o.primary}
345 if o.cursor != nil {
346 out = append(out, o.cursor.plane)
347 }
348 if o.overlay != nil {
349 out = append(out, o.overlay)
350 }
351 return out
352}
353
354// cursorSize is the cursor plane side: the smaller of the driver's width
355// and height caps, 64 when it does not say.
356func cursorSize(fd int) int {
357 size := 0
358 for _, c := range []uint64{capCursorW, capCursorH} {
359 v := getCap{capability: c}
360 if ioctl(fd, ioctlGetCap, unsafe.Pointer(&v)) == nil && v.value >= 16 && v.value <= 512 {
361 if size == 0 || int(v.value) < size {
362 size = int(v.value)
363 }
364 }
365 }
366 if size == 0 {
367 size = 64
368 }
369 return size
370}
371
372// Cursor is the hardware cursor, or nil when the CRTC has no cursor plane.
373func (o *Output) Cursor() *Cursor { return o.cursor }
374
375// Info describes the output for core and clients. Unknown EDID fields are empty.
376func (o *Output) Info() ports.OutputInfo {
377 known := func(v string) string {
378 if v == "Unknown" {
379 return ""
380 }
381 return v
382 }
383 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}
384}
385
386func (o *Output) Width() int { return int(o.mode.HDisplay) }
387func (o *Output) Height() int { return int(o.mode.VDisplay) }
388
389// primaryProps shows fb full screen on the primary plane.
390func (o *Output) primaryProps(req *atomicReq, fb uint32) {
391 p := o.primary
392 w, h := uint64(o.Width()), uint64(o.Height())
393 req.set(p.id, p.prop("FB_ID"), uint64(fb))
394 req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
395 req.set(p.id, p.prop("SRC_X"), 0)
396 req.set(p.id, p.prop("SRC_Y"), 0)
397 req.set(p.id, p.prop("SRC_W"), w<<16)
398 req.set(p.id, p.prop("SRC_H"), h<<16)
399 req.set(p.id, p.prop("CRTC_X"), 0)
400 req.set(p.id, p.prop("CRTC_Y"), 0)
401 req.set(p.id, p.prop("CRTC_W"), w)
402 req.set(p.id, p.prop("CRTC_H"), h)
403}
404
405// modeset shows the front image with the output's mode, turning off every
406// other plane on the CRTC; needed at start and after every VT resume.
407// It blocks until the kernel applied it.
408func (o *Output) modeset() error {
409 blob, err := o.modeBlobFor(o.mode)
410 if err != nil {
411 return err
412 }
413 if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil {
414 _ = o.k.destroyBlob(blob)
415 return fmt.Errorf("modeset: %w", err)
416 }
417 // Only now is nothing of ours pending or on screen: on failure the
418 // previous buffers may still show.
419 o.endPending()
420 o.busySince = time.Time{}
421 o.shown, o.queued = 0, 0
422 if o.modeBlob != 0 {
423 _ = o.k.destroyBlob(o.modeBlob)
424 }
425 o.modeBlob = blob
426 o.vrrOn, o.overlayOn = false, 0
427 if o.cursor != nil {
428 o.cursor.applied = cursorState{}
429 o.cursor.screen, o.cursor.flying = 0, false
430 }
431 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
432 o.testCursor()
433 o.sendFormats()
434 return nil
435}
436
437// sendFormats reports the formats clients may allocate for direct
438// scanout on this output (none when scanout is off). It never blocks:
439// wayland keeps the latest per output, a dropped report is resent at the
440// next modeset.
441func (o *Output) sendFormats() {
442 if o.formats == nil {
443 return
444 }
445 f := ports.OutputFormats{Output: o.conn.name, Device: o.device}
446 if o.scanout {
447 f.Formats = o.scanoutFormats(o.sampled)
448 }
449 select {
450 case o.formats <- f:
451 default:
452 o.log.Warn().Str("connector", o.conn.name).Msg("scanout formats not sent: wayland busy")
453 }
454}
455
456// testModeset asks KMS whether it would take the modeset of the output
457// images without applying it.
458func (o *Output) testModeset() error {
459 blob, err := o.modeBlobFor(o.mode)
460 if err != nil {
461 return err
462 }
463 defer func() { _ = o.k.destroyBlob(blob) }()
464 if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil {
465 return fmt.Errorf("modeset test: %w", err)
466 }
467 return nil
468}
469
470func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) {
471 blob, err := o.k.createBlob(unsafe.Slice((*byte)(unsafe.Pointer(&mode)), unsafe.Sizeof(mode)))
472 if err != nil {
473 return 0, fmt.Errorf("mode blob: %w", err)
474 }
475 return blob, nil
476}
477
478// testCursor turns the hardware cursor off when KMS refuses its images on
479// the cursor plane (e.g. a pitch the plane cannot scan out).
480func (o *Output) testCursor() {
481 c := o.cursor
482 if c == nil || c.off || c.fbs[0] == 0 {
483 return
484 }
485 req := &atomicReq{}
486 c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
487 if err := o.k.commit(req, atomicTestOnly, 0); refused(err) {
488 o.cursorOff(err)
489 }
490}
491
492// cursorOff stops using the cursor plane for the rest of the output's life.
493func (o *Output) cursorOff(err error) {
494 o.cursor.off = true
495 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off")
496}
497
498// modesetReq is the modeset of the front image with mode blob.
499func (o *Output) modesetReq(blob uint32) *atomicReq {
500 req := &atomicReq{}
501 req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob))
502 req.set(o.crtc, o.crtcProps["ACTIVE"], 1)
503 req.set(o.crtc, o.vrrProp, 0)
504 req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
505 o.primaryProps(req, o.fbs[1-o.back])
506 if o.cursor != nil {
507 o.cursor.props(req, o.crtc, cursorState{})
508 }
509 o.overlayProps(req, overlayWin{})
510 for _, p := range o.stray {
511 // Only planes a previous master left on this CRTC.
512 props, err := o.k.objProps(p.id, objPlane)
513 if err != nil || uint32(props["CRTC_ID"][1]) != o.crtc {
514 continue
515 }
516 req.set(p.id, p.prop("FB_ID"), 0)
517 req.set(p.id, p.prop("CRTC_ID"), 0)
518 }
519 return req
520}
521
522// probeAsync checks once whether async commits may carry IN_FENCE_FD.
523func (o *Output) probeAsync(r ports.Renderer) {
524 if o.asyncProbed || !o.tearing {
525 return
526 }
527 o.asyncProbed = true
528 r.UseTarget(o.back)
529 done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
530 if err != nil {
531 return
532 }
533 req := &atomicReq{}
534 req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(o.fbs[o.back]))
535 if done != nil {
536 defer done.Close()
537 req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(done.Fd()))
538 }
539 err = o.k.commit(req, atomicTestOnly|flipAsyncFlag, 0)
540 o.asyncFence = err == nil && done != nil
541 o.log.Info().Err(err).Str("connector", o.conn.name).Bool("async_fence", o.asyncFence).Msg("tearing probe")
542}
543
544// wantVRR is the VRR state the next frame commit sets: on while a buffer
545// is scanned out, off after composing for vrrHold (each toggle may
546// flicker, so short compositions keep it).
547func (o *Output) wantVRR(direct bool) bool {
548 if o.vrrProp == 0 {
549 return false
550 }
551 if direct {
552 o.composedSince = time.Time{}
553 return true
554 }
555 if o.composedSince.IsZero() {
556 o.composedSince = time.Now()
557 }
558 return o.vrrOn && time.Since(o.composedSince) <= vrrHold
559}
560
561// commitFrame flips fb in one commit with the cursor and VRR. An async
562// commit carries only the primary plane (kernel rule): a frame that must
563// also move the cursor or change VRR flips at vblank. fence is the
564// frame's GPU fence (nil: none); the caller keeps and closes it.
565func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error {
566 return o.commitWith(fb, fence, async, vrr, f, overlayWin{})
567}
568
569// commitWith is commitFrame with ov on the overlay plane (zero: off).
570func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error {
571 cur := cursorState{}
572 if o.cursor != nil {
573 cur = o.cursor.desired()
574 }
575 if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) {
576 async = false
577 }
578 req := &atomicReq{}
579 flags := uint32(atomicNonblock | flipEventFlag)
580 if async {
581 flags |= flipAsyncFlag
582 req.set(o.primary.id, o.primary.prop("FB_ID"), uint64(fb))
583 } else {
584 o.primaryProps(req, fb)
585 req.set(o.crtc, o.vrrProp, boolValue(vrr))
586 if o.cursor != nil {
587 o.cursor.props(req, o.crtc, cur)
588 }
589 if ov.buf != 0 || o.overlayOn != 0 {
590 o.overlayProps(req, ov)
591 }
592 }
593 if fence != nil {
594 req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd()))
595 }
596 // The kernel takes its own reference on the fence.
597 if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil {
598 if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
599 return o.commitWith(fb, fence, false, vrr, f, ov)
600 }
601 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
602 // Retry without turning VRR on: if that passes, the driver
603 // refuses VRR on this output.
604 if o.commitFrame(fb, fence, false, false, f) == nil {
605 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr refused; disabled")
606 o.vrrProp = 0
607 return nil
608 }
609 }
610 return err
611 }
612 if !async {
613 if o.vrrOn != vrr {
614 o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
615 }
616 o.vrrOn = vrr
617 }
618 if o.cursor != nil {
619 // An async commit leaves the cursor plane as applied.
620 o.cursor.committed(cur)
621 }
622 if !async {
623 o.overlayOn = ov.buf
624 }
625 f.frame, f.async = true, async
626 f.fences = dupFences(fence, ov.acquire)
627 o.setAsync(async)
628 o.begin(f)
629 return nil
630}
631
632// cursorRefused reports, after a commit carrying the cursor failed with
633// EINVAL, whether the cursor plane was the cause; then the cursor is off
634// and the commit can be retried without it.
635func (o *Output) cursorRefused() bool {
636 o.testCursor()
637 return o.cursor.off
638}
639
640// commitState commits a cursor or VRR change without a new frame.
641func (o *Output) commitState(vrr bool) error {
642 req := &atomicReq{}
643 cur := cursorState{}
644 if o.cursor != nil {
645 cur = o.cursor.desired()
646 if cur != o.cursor.applied {
647 o.cursor.props(req, o.crtc, cur)
648 }
649 }
650 if vrr != o.vrrOn {
651 req.set(o.crtc, o.vrrProp, boolValue(vrr))
652 }
653 if len(req.objs) == 0 {
654 return nil
655 }
656 if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil {
657 if o.overlayConflict(err, o.overlayOn) {
658 // The next frame is composed without the overlay.
659 return errOverlayDropped
660 }
661 if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
662 return o.commitState(vrr)
663 }
664 return err
665 }
666 if o.cursor != nil {
667 o.cursor.committed(cur)
668 }
669 if vrr != o.vrrOn {
670 o.log.Info().Bool("vrr", vrr).Str("connector", o.conn.name).Msg("vrr")
671 }
672 o.vrrOn = vrr
673 o.begin(pendingFrame{})
674 return nil
675}
676
677func boolValue(b bool) uint64 {
678 if b {
679 return 1
680 }
681 return 0
682}
683
684// scanoutFrame picks a fullscreen client buffer to flip directly. It
685// returns fb 0, with the reason logged on change, when the frame must be
686// composed.
687func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent) {
688 reason := "disabled"
689 if o.scanout {
690 c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height())
691 }
692 if reason == "" {
693 fb, reason = o.scanoutFB(c.DMABuf, time.Now())
694 }
695 if reason != o.reason {
696 o.log.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout")
697 o.reason = reason
698 }
699 if reason != "" {
700 return 0, c
701 }
702 return fb, c
703}
704
705// commitScanout flips a client buffer. It reports false when KMS refused
706// the buffer: the frame must be composed.
707func (o *Output) commitScanout(fb uint32, c ports.SurfaceContent, f pendingFrame) (bool, error) {
708 cfb := o.clientFBs[c.DMABuf.ID]
709 // Drivers refuse async flips that change the format or modifier: the
710 // flip from the composed image into scanout waits for vblank.
711 async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync
712 vrr := o.wantVRR(true)
713 f.queued, f.zeroCopy = c.DMABuf.ID, c.ID
714 // Explicit sync: KMS waits on the client's acquire fence. Wayland may
715 // close its file at any time: commit a duplicate taken while it is
716 // held open (the kernel keeps its own reference).
717 fence := dupFence(c.Acquire)
718 if fence != nil {
719 defer fence.Close()
720 }
721 err := o.commitFrame(fb, fence, async, vrr, f)
722 if err != nil && async && errors.Is(err, unix.EINVAL) {
723 // Refused (e.g. not a fast update): this buffer flips at vblank
724 // from now on.
725 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
726 cfb.noAsync = true
727 err = o.commitFrame(fb, fence, false, vrr, f)
728 }
729 if err != nil && (errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)) {
730 // KMS refuses this buffer on the plane: compose it instead.
731 o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")
732 cfb.failed, o.reason = "flip_refused", "flip_refused"
733 o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout")
734 return false, nil
735 }
736 if err == nil {
737 o.queued = c.DMABuf.ID
738 }
739 return true, err
740}
741
742// vrrHold is how long composition runs before VRR turns off.
743const vrrHold = 500 * time.Millisecond
744
745// setAsync logs when the output starts or stops tearing.
746func (o *Output) setAsync(on bool) {
747 if on != o.async {
748 o.async = on
749 o.log.Info().Bool("tearing", on).Str("connector", o.conn.name).Msg("tearing")
750 }
751}
752
753// Close restores the CRTC state found at startup, when it had a mode, and
754// frees buffers.
755func (o *Output) Close() {
756 req := &atomicReq{}
757 if o.cursor != nil {
758 o.cursor.props(req, o.crtc, cursorState{})
759 }
760 o.overlayProps(req, overlayWin{})
761 req.set(o.crtc, o.vrrProp, 0)
762 s := o.saved
763 var blob uint32
764 p := o.primary
765 restored := false
766 if s.crtcID != 0 && s.modeValid != 0 && s.fbID != 0 {
767 if b, err := o.modeBlobFor(s.mode); err == nil {
768 blob, restored = b, true
769 req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(b))
770 req.set(o.crtc, o.crtcProps["ACTIVE"], 1)
771 req.set(o.conn.id, o.connCrtc, uint64(o.crtc))
772 req.set(p.id, p.prop("FB_ID"), uint64(s.fbID))
773 req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc))
774 req.set(p.id, p.prop("SRC_X"), 0)
775 req.set(p.id, p.prop("SRC_Y"), 0)
776 req.set(p.id, p.prop("SRC_W"), uint64(s.mode.HDisplay)<<16)
777 req.set(p.id, p.prop("SRC_H"), uint64(s.mode.VDisplay)<<16)
778 req.set(p.id, p.prop("CRTC_X"), 0)
779 req.set(p.id, p.prop("CRTC_Y"), 0)
780 req.set(p.id, p.prop("CRTC_W"), uint64(s.mode.HDisplay))
781 req.set(p.id, p.prop("CRTC_H"), uint64(s.mode.VDisplay))
782 }
783 }
784 if !restored {
785 // Nothing to give back: leave the CRTC off.
786 req.set(o.crtc, o.crtcProps["ACTIVE"], 0)
787 req.set(o.crtc, o.crtcProps["MODE_ID"], 0)
788 req.set(p.id, p.prop("FB_ID"), 0)
789 req.set(p.id, p.prop("CRTC_ID"), 0)
790 req.set(o.conn.id, o.connCrtc, 0)
791 }
792 // Best effort: at exit the card may already belong to another session.
793 if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
794 o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("restore crtc")
795 }
796 if blob != 0 {
797 _ = o.k.destroyBlob(blob)
798 }
799 if o.modeBlob != 0 {
800 _ = o.k.destroyBlob(o.modeBlob)
801 o.modeBlob = 0
802 }
803 if o.cursor != nil {
804 o.cursor.free(o.k)
805 }
806 o.freeImages()
807 o.endPending()
808 o.dropRead()
809 o.shown, o.queued = 0, 0
810 o.dropClientFBs(time.Now(), true)
811}
812
813// Run renders scenes and commits them until ctx ends. active reports seat
814// enable/disable. What the output shows and read is reported on presented.
815func (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, presented chan<- ports.OutputPresented) error {
816 r, err := newRenderer(o.Width(), o.Height())
817 if err != nil {
818 return fmt.Errorf("create renderer: %w", err)
819 }
820 defer r.Close()
821 if o.cursor != nil {
822 if err := o.cursor.setup(o.k, r); err != nil {
823 o.cursor.off = true
824 o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")
825 }
826 }
827 // The seat sends its state first; while switched away (e.g. a monitor
828 // plugged in on another VT) the modeset waits for the enable.
829 enabled := true
830 select {
831 case enabled = <-active:
832 default:
833 }
834 // Output images, from the best to the linear fallback (ADR 014).
835 // While switched away they are made on the first enable.
836 if enabled {
837 if err := o.showImages(r, imagesDriver, nil); err != nil {
838 return err
839 }
840 o.probeAsync(r)
841 }
842 surfaces := make(map[ports.WindowID]ports.SurfaceContent)
843 var scene ports.Scene
844 haveScene, dirty := false, false
845 var want ports.CursorChange
846 cursorScale := -1.0 // not loaded yet
847 frame := 0
848 stats := time.NewTicker(10 * time.Second)
849 defer stats.Stop()
850 // seen is the latest content Seq per window. It is reported only while
851 // no frame is in flight: then the GPU has finished every frame that
852 // read older buffers (the kernel waited for their fence to flip).
853 seen := map[ports.WindowID]uint64{}
854 reportDirty := false
855 var cursorWake <-chan struct{}
856 if o.cursor != nil {
857 cursorWake = o.cursor.wake
858 }
859 for {
860 readWait := false
861 if reportDirty && !o.pending {
862 if readWait = !o.readDone(); !readWait {
863 o.report(nil, seen)
864 reportDirty = false
865 }
866 }
867 o.flushReport(presented)
868 // An unsent report is retried soon, not only on the next event;
869 // so is one that waits for an uncommitted frame's fence.
870 var retry <-chan time.Time
871 if len(o.unsent) > 0 || readWait {
872 retry = time.After(time.Millisecond)
873 }
874 // A composed screen that stopped changing still drops VRR on time.
875 var vrrOff <-chan time.Time
876 if enabled && o.vrrOn && !o.pending && !o.composedSince.IsZero() {
877 vrrOff = time.After(max(0, vrrHold-time.Since(o.composedSince)))
878 }
879 // A commit whose event never comes must not stop the output.
880 var stuck <-chan time.Time
881 if enabled && o.pending {
882 stuck = time.After(max(0, time.Until(o.stuckAt)))
883 }
884 stateDirty := false
885 select {
886 case <-retry:
887 continue
888 case <-stuck:
889 if o.pending && o.stuckAt.After(time.Now()) {
890 continue // waits for a fence
891 }
892 if o.expire() {
893 if err := o.modeset(); err != nil {
894 if !o.commitFailed(err, &enabled) {
895 return fmt.Errorf("recovery modeset: %w", err)
896 }
897 continue
898 }
899 }
900 dirty = haveScene
901 stateDirty = true
902 case <-vrrOff:
903 stateDirty = true
904 case <-cursorWake:
905 stateDirty = true
906 case <-ctx.Done():
907 return nil
908 case on := <-active:
909 // Modeset on every enable: a fast disable+enable can coalesce to one true.
910 if on {
911 var err error
912 if o.fbs[0] == 0 {
913 err = o.showImages(r, imagesDriver, nil)
914 } else {
915 err = o.modeset()
916 }
917 if lostMaster(err) {
918 // The seat took DRM master back: wait for the next enable.
919 o.log.Warn().Err(err).Msg("resume")
920 continue
921 }
922 if err != nil {
923 o.log.Error().Err(err).Msg("resume")
924 return err
925 }
926 o.probeAsync(r)
927 dirty = haveScene
928 stateDirty = true
929 }
930 enabled = on
931 o.log.Info().Bool("enabled", on).Msg("output")
932 case ev := <-o.flipped:
933 if !o.completed(ev, seen) {
934 continue // stale event from before a modeset
935 }
936 stateDirty = true
937 if o.cursor != nil {
938 // An image that waited for a free slot loads now.
939 if _, err := o.cursor.flushLater(r); err != nil {
940 o.log.Warn().Err(err).Msg("cursor")
941 }
942 }
943 case s := <-scenes:
944 if o.cursor != nil && loadCursor != nil && s.Scale != cursorScale {
945 cursorScale = s.Scale
946 o.setCursor(r, loadCursor, want, s.Scale)
947 stateDirty = true
948 }
949 scene, haveScene, dirty = s, true, true
950 case c := <-cursor:
951 want = c
952 // Before the first scene the scale is unknown: loaded then.
953 if o.cursor != nil && loadCursor != nil && cursorScale > 0 {
954 o.setCursor(r, loadCursor, want, cursorScale)
955 stateDirty = true
956 }
957 case c := <-contents:
958 if c.Empty() {
959 delete(surfaces, c.ID)
960 } else {
961 surfaces[c.ID] = c
962 }
963 if c.Seq > seen[c.ID] {
964 seen[c.ID] = c.Seq
965 reportDirty = true
966 }
967 // Windows on other outputs or workspaces do not need a frame.
968 dirty = dirty || scene.Shows(c.ID)
969 case <-stats.C:
970 o.dropClientFBs(time.Now(), false)
971 ev := o.log.Info().Str("connector", o.conn.name).Int("frames", frame).Int("flips", o.flips).Bool("pending", o.pending)
972 if o.cursor != nil {
973 cs := o.cursor.TakeStats()
974 ev = ev.Int("cursor_moves", cs.Moves).Int("cursor_commits", cs.Commits)
975 }
976 ev.Msg("stats")
977 }
978 if !enabled || o.pending {
979 continue
980 }
981 if !dirty || !haveScene {
982 if stateDirty {
983 err := o.commitState(o.wantVRR(o.shown != 0))
984 if errors.Is(err, errOverlayDropped) {
985 dirty = haveScene
986 continue
987 }
988 if err != nil && !o.commitFailed(err, &enabled) {
989 return fmt.Errorf("commit: %w", err)
990 }
991 }
992 continue
993 }
994 start := time.Now()
995 // shows is what the frame puts on screen: presentation feedback
996 // of windows it does not draw is discarded, not presented.
997 f := pendingFrame{shows: shownBy(scene, seen)}
998 fb, c := o.scanoutFrame(scene, surfaces)
999 direct := false
1000 if fb != 0 {
1001 direct, err = o.commitScanout(fb, c, pendingFrame{shows: map[ports.WindowID]uint64{c.ID: seen[c.ID]}})
1002 }
1003 if !direct {
1004 ov, composed := o.overlayFrame(scene, surfaces)
1005 r.UseTarget(o.back)
1006 done, rerr := r.Render(composed, surfaces)
1007 if rerr != nil {
1008 ov.close()
1009 return fmt.Errorf("render frame: %w", rerr)
1010 }
1011 if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) {
1012 // Refused: compose the window too (cached per buffer).
1013 if done != nil {
1014 done.Close()
1015 }
1016 ov.close()
1017 ov = overlayWin{}
1018 if done, rerr = r.Render(scene, surfaces); rerr != nil {
1019 return fmt.Errorf("render frame: %w", rerr)
1020 }
1021 }
1022 // The overlay buffer is on screen like a scanned-out one: it
1023 // is reported shown, so it is not released under the plane.
1024 f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true
1025 err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov)
1026 if err != nil {
1027 // The GPU may still read client buffers for this frame.
1028 o.holdRead(done)
1029 }
1030 if done != nil {
1031 done.Close()
1032 }
1033 ov.close()
1034 if err != nil && o.overlayConflict(err, ov.buf) {
1035 err = errOverlayDropped
1036 }
1037 if err == nil {
1038 o.queued = ov.buf
1039 o.back = 1 - o.back
1040 }
1041 }
1042 if err != nil {
1043 if errors.Is(err, errOverlayDropped) {
1044 dirty = true
1045 continue
1046 }
1047 if !o.commitFailed(err, &enabled) {
1048 return fmt.Errorf("page flip: %w", err)
1049 }
1050 continue // dirty stays true
1051 }
1052 dirty = false
1053 frame++
1054 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")
1055 }
1056}
1057
1058// commitFailed handles a commit error that does not stop the output: a
1059// commit still in flight (EBUSY, retried after its event) or DRM master
1060// lost mid-switch (the seat disable follows).
1061func (o *Output) commitFailed(err error, enabled *bool) bool {
1062 switch {
1063 case errors.Is(err, unix.EBUSY):
1064 // The kernel still works on an earlier commit (e.g. across a VT
1065 // switch, or one whose event already came): wait for any event
1066 // of this CRTC, or retry after busyRetry since none may come.
1067 o.endPending()
1068 now := time.Now()
1069 o.pending, o.flipStart, o.stuckAt = true, now, now.Add(busyRetry)
1070 o.pendingSerial = 0
1071 // A run of EBUSY: each refusal follows the previous retry.
1072 if o.busySince.IsZero() || now.Sub(o.lastBusy) > 4*busyRetry {
1073 o.busySince = now
1074 o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy")
1075 }
1076 o.lastBusy = now
1077 return true
1078 case lostMaster(err):
1079 o.log.Warn().Err(err).Msg("commit refused")
1080 *enabled = false
1081 return true
1082 }
1083 o.log.Error().Err(err).Msg("commit")
1084 return false
1085}
1086
1087// completed handles the event of the pending commit: a flipped frame is
1088// reported with its kernel timestamp. It reports false for an event of
1089// another commit, which is dropped.
1090func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool {
1091 if !o.pending || o.pendingSerial != 0 && ev.user>>userKindBits != o.pendingSerial {
1092 return false
1093 }
1094 f := o.pendingFrame
1095 o.endPending()
1096 o.busySince = time.Time{}
1097 if d := time.Since(o.flipStart); d > 20*time.Millisecond {
1098 o.log.Info().Dur("flip_ms", d).Str("connector", o.conn.name).Msg("slow flip")
1099 }
1100 if o.cursor != nil {
1101 o.cursor.landed()
1102 }
1103 if !f.frame {
1104 return true
1105 }
1106 if f.composed {
1107 // Frames render in order: this flip's fence signalled after
1108 // those of frames rendered before it and not committed.
1109 o.dropRead()
1110 }
1111 o.flips++
1112 o.shown = f.queued
1113 if o.queued == f.queued {
1114 o.queued = 0
1115 }
1116 refresh := time.Duration(0)
1117 if !o.vrrOn {
1118 refresh = time.Duration(int64(time.Second) * 1000 / int64(max(1, o.mode.refreshMilli())))
1119 }
1120 o.report(&ports.FlipInfo{When: ev.when, Seq: uint64(ev.seq), Refresh: refresh, ZeroCopy: f.zeroCopy, Async: f.async, HardwareClock: true, Shows: f.shows}, seen)
1121 return true
1122}
1123
1124// setCursor loads a cursor for scale into the cursor images.
1125func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64) {
1126 if o.cursor.off {
1127 return
1128 }
1129 img, err := load(c, scale, o.cursor.Limit())
1130 if err == nil {
1131 err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY)
1132 }
1133 if err != nil {
1134 o.log.Warn().Err(err).Msg("cursor")
1135 return
1136 }
1137 o.log.Debug().Float64("scale", scale).Str("shape", c.Shape).Bool("client", c.Image != nil).Int("w", img.W).Int("h", img.H).Msg("cursor image")
1138}
1139
1140// maxUnsent bounds reports waiting for wayland; past it the oldest flip is
1141// folded into the next one.
1142const maxUnsent = 16
1143
1144// report queues what the output shows and has read, so replaced client
1145// buffers can be released. A flip-less report merges into the newest
1146// queued one; flips are never merged unless maxUnsent is reached.
1147func (o *Output) report(flip *ports.FlipInfo, seen map[ports.WindowID]uint64) {
1148 r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}
1149 if n := len(o.unsent); n > 0 && (flip == nil || o.unsent[n-1].Flip == nil) {
1150 if flip == nil {
1151 r.Flip = o.unsent[n-1].Flip
1152 }
1153 o.unsent[n-1] = r
1154 return
1155 }
1156 o.unsent = append(o.unsent, r)
1157 if len(o.unsent) > maxUnsent {
1158 old, next := o.unsent[0].Flip, o.unsent[1].Flip
1159 if next != nil && old != nil {
1160 next.Merged += 1 + old.Merged
1161 for id, seq := range old.Shows {
1162 if _, ok := next.Shows[id]; !ok {
1163 next.Shows[id] = seq
1164 }
1165 }
1166 }
1167 o.unsent = slices.Delete(o.unsent, 0, 1)
1168 o.log.Warn().Str("connector", o.conn.name).Msg("wayland is not reading output reports; flips merged")
1169 }
1170}
1171
1172// flushReport sends queued reports without blocking.
1173func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
1174 for len(o.unsent) > 0 {
1175 select {
1176 case presented <- o.unsent[0]:
1177 o.unsent = slices.Delete(o.unsent, 0, 1)
1178 default:
1179 return
1180 }
1181 }
1182}
1183
1184// imageKind is how output images are made, best first.
1185type imageKind int
1186
1187const (
1188 imagesDriver imageKind = iota // exported, modifier chosen by the driver
1189 imagesLinear // exported, linear
1190)
1191
1192// showImages sets up images from kind on and modesets them. KMS may refuse
1193// an image only in a modeset: a TEST_ONLY modeset checks it first, and the
1194// next kind is tried on refusal. cause is why the
1195// previous kind failed, for the log.
1196func (o *Output) showImages(r ports.Renderer, kind imageKind, cause error) error {
1197 for {
1198 got, err := o.setupImages(r, kind, cause)
1199 if err != nil {
1200 return err
1201 }
1202 o.kind = got
1203 if err = o.testModeset(); err == nil {
1204 return o.modeset()
1205 }
1206 if !refused(err) || got == imagesLinear {
1207 o.freeImages()
1208 return err
1209 }
1210 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
1211 o.freeImages()
1212 kind, cause = got+1, err
1213 }
1214}
1215
1216// refused reports a modeset error meaning KMS rejects the images.
1217func refused(err error) bool {
1218 return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE)
1219}
1220
1221// lostMaster reports a commit error meaning the seat is switched away.
1222func lostMaster(err error) bool {
1223 return errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM)
1224}
1225
1226// setupImages gives the output the two renderer images it flips between,
1227// exported as dmabufs. There is no CPU fallback (ADR 014): a GPU that
1228// cannot export a scanout-capable image cannot drive the output.
1229func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (imageKind, error) {
1230 err := cause
1231 for ; kind <= imagesLinear; kind++ {
1232 mods := []uint64(nil)
1233 if kind == imagesLinear {
1234 mods = []uint64{0}
1235 }
1236 if err = o.exportImages(r, mods); err == nil {
1237 return kind, nil
1238 }
1239 o.log.Info().Err(err).Str("connector", o.conn.name).Int("kind", int(kind)).Msg("output image export")
1240 }
1241 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
1242}
1243
1244// exportImages makes the renderer's exported targets the output images.
1245func (o *Output) exportImages(r ports.Renderer, mods []uint64) error {
1246 bufs, err := r.ExportTargets(len(o.fbs), mods)
1247 if err != nil {
1248 return err
1249 }
1250 for i := range bufs {
1251 if err == nil {
1252 o.fbs[i], err = o.k.addFB(&bufs[i], fourccXRGB)
1253 }
1254 bufs[i].Planes[0].File.Close()
1255 }
1256 // GPU memory starts undefined (old VRAM contents): clear both images
1257 // before the modeset shows one.
1258 for i := range o.fbs {
1259 if err != nil {
1260 break
1261 }
1262 r.UseTarget(i)
1263 var done *os.File
1264 if done, err = r.Render(ports.Scene{Background: "#000000"}, nil); done != nil {
1265 // The modeset is a blocking commit: wait for the clear.
1266 err = errors.Join(err, syncfile.Wait(context.Background(), done))
1267 done.Close()
1268 }
1269 }
1270 if err != nil {
1271 o.freeImages()
1272 _, _ = r.ExportTargets(0, nil)
1273 return err
1274 }
1275 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Msg("zero-copy output")
1276 return nil
1277}
1278
1279// freeImages removes the output images' framebuffers.
1280func (o *Output) freeImages() {
1281 for i, fb := range o.fbs {
1282 if fb != 0 {
1283 _ = o.k.rmFB(fb)
1284 }
1285 o.fbs[i] = 0
1286 }
1287}
1288
1289// shownBy is the content Seq of each window the scene draws.
1290func shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 {
1291 out := map[ports.WindowID]uint64{}
1292 for id, seq := range seen {
1293 if s.Shows(id) {
1294 out[id] = seq
1295 }
1296 }
1297 return out
1298}