fix(drm): wait for unsignalled fences before recovering a stuck commit
A frame commit keeps duplicates of the fences it waits on (render fence, scanout or overlay acquire fence). When its event is late and a fence has not signalled, the output logs "frame waits for GPU fence" once and keeps waiting; it modesets only once every fence signalled and still no event came. The fences are closed when the commit ends (event, expiry, modeset, close). A composed frame refused by KMS keeps its render fence: what the output has seen is reported only once that fence signalled or a later composed frame flipped, since the GPU may still read client buffers. An EBUSY run starts again when the previous refusal is older than a few retries, so a later EBUSY is retried instead of modesetting at once. A modeset resets the shown and queued buffers and the pending commit only after its commit succeeds: a failed modeset leaves the buffers on screen held.
- commit
- aadcd19d1358ae3800cb0aa3601430c78188286b
- author
- brice · · 2026-09-26 13:42 UTC
- parent
- ee184c9
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +305 -48
M internal/adapters/drm/output.go +150 -35
| @@ -63,11 +63,19 @@ type Output struct { | |||
| 63 | 63 | pendingSerial, nextSerial uint64 | |
| 64 | 64 | flipStart time.Time | |
| 65 | 65 | flips int | |
| 66 | - | // A commit whose event does not come within stuckAfter (default | |
| 67 | - | // stuckTimeout) is abandoned with a modeset. A commit refused with | |
| 68 | - | // EBUSY is retried after busyRetry; busySince is when EBUSY began. | |
| 69 | - | stuckAfter time.Duration | |
| 70 | - | busySince time.Time | |
| 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 | |
| 71 | 79 | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | |
| 72 | 80 | // screen and queued (0: the composed image). | |
| 73 | 81 | scanout bool | |
| @@ -103,6 +111,86 @@ type pendingFrame struct { | |||
| 103 | 111 | zeroCopy ports.WindowID // window shown without composition | |
| 104 | 112 | async bool | |
| 105 | 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. | |
| 122 | + | func (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. | |
| 130 | + | func 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. | |
| 142 | + | func 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. | |
| 162 | + | func (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. | |
| 168 | + | func (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. | |
| 183 | + | func (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. | |
| 191 | + | func (o *Output) endPending() { | |
| 192 | + | o.pendingFrame.closeFences() | |
| 193 | + | o.pending, o.pendingFrame = false, pendingFrame{} | |
| 106 | 194 | } | |
| 107 | 195 | ||
| 108 | 196 | // Commit event userData: the commit serial above userKindBits, the kind | |
| @@ -123,7 +211,9 @@ func (o *Output) userData(kind uint64) uint64 { | |||
| 123 | 211 | // begin marks the commit made with the last userData pending. | |
| 124 | 212 | func (o *Output) begin(f pendingFrame) { | |
| 125 | 213 | o.pendingSerial = o.nextSerial | |
| 214 | + | o.pendingFrame.closeFences() | |
| 126 | 215 | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | |
| 216 | + | o.stuckAt, o.fenceWarned = o.flipStart.Add(o.stuckLimit()), false | |
| 127 | 217 | o.busySince = time.Time{} | |
| 128 | 218 | } | |
| 129 | 219 | ||
| @@ -141,29 +231,29 @@ func (o *Output) stuckLimit() time.Duration { | |||
| 141 | 231 | return stuckTimeout | |
| 142 | 232 | } | |
| 143 | 233 | ||
| 144 | - | // pendingLimit is how long the pending commit may wait for its event. | |
| 145 | - | // After EBUSY no commit of ours may be in flight: no event may come, so | |
| 146 | - | // the commit is retried soon. | |
| 147 | - | func (o *Output) pendingLimit() time.Duration { | |
| 148 | - | if o.pendingSerial == 0 { | |
| 149 | - | return busyRetry | |
| 150 | - | } | |
| 151 | - | return o.stuckLimit() | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | // expire ends a pending commit whose event did not come in time, so the | |
| 155 | - | // next commit can go. It reports whether the output needs a modeset: an | |
| 156 | - | // event is missing (the kernel state is unknown) or EBUSY lasted past | |
| 157 | - | // the limit. | |
| 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. | |
| 158 | 239 | func (o *Output) expire() bool { | |
| 159 | - | age := time.Since(o.flipStart) | |
| 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 | + | } | |
| 160 | 250 | kind := "state" | |
| 161 | 251 | if o.pendingFrame.frame { | |
| 162 | 252 | kind = "frame" | |
| 163 | 253 | } | |
| 164 | - | o.pending, o.pendingFrame = false, pendingFrame{} | |
| 254 | + | o.endPending() | |
| 165 | 255 | if o.pendingSerial == 0 { | |
| 166 | - | busy := time.Since(o.busySince) | |
| 256 | + | busy := now.Sub(o.busySince) | |
| 167 | 257 | if o.busySince.IsZero() || busy < o.stuckLimit() { | |
| 168 | 258 | return false | |
| 169 | 259 | } | |
| @@ -316,9 +406,6 @@ func (o *Output) primaryProps(req *atomicReq, fb uint32) { | |||
| 316 | 406 | // other plane on the CRTC; needed at start and after every VT resume. | |
| 317 | 407 | // It blocks until the kernel applied it. | |
| 318 | 408 | func (o *Output) modeset() error { | |
| 319 | - | o.pending, o.busySince = false, time.Time{} | |
| 320 | - | o.pendingFrame = pendingFrame{} | |
| 321 | - | o.shown, o.queued = 0, 0 | |
| 322 | 409 | blob, err := o.modeBlobFor(o.mode) | |
| 323 | 410 | if err != nil { | |
| 324 | 411 | return err | |
| @@ -327,6 +414,11 @@ func (o *Output) modeset() error { | |||
| 327 | 414 | _ = o.k.destroyBlob(blob) | |
| 328 | 415 | return fmt.Errorf("modeset: %w", err) | |
| 329 | 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 | |
| 330 | 422 | if o.modeBlob != 0 { | |
| 331 | 423 | _ = o.k.destroyBlob(o.modeBlob) | |
| 332 | 424 | } | |
| @@ -531,6 +623,7 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||
| 531 | 623 | o.overlayOn = ov.buf | |
| 532 | 624 | } | |
| 533 | 625 | f.frame, f.async = true, async | |
| 626 | + | f.fences = dupFences(fence, ov.acquire) | |
| 534 | 627 | o.setAsync(async) | |
| 535 | 628 | o.begin(f) | |
| 536 | 629 | return nil | |
| @@ -711,6 +804,8 @@ func (o *Output) Close() { | |||
| 711 | 804 | o.cursor.free(o.k) | |
| 712 | 805 | } | |
| 713 | 806 | o.freeImages() | |
| 807 | + | o.endPending() | |
| 808 | + | o.dropRead() | |
| 714 | 809 | o.shown, o.queued = 0, 0 | |
| 715 | 810 | o.dropClientFBs(time.Now(), true) | |
| 716 | 811 | } | |
| @@ -762,14 +857,18 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 762 | 857 | cursorWake = o.cursor.wake | |
| 763 | 858 | } | |
| 764 | 859 | for { | |
| 860 | + | readWait := false | |
| 765 | 861 | if reportDirty && !o.pending { | |
| 766 | - | o.report(nil, seen) | |
| 767 | - | reportDirty = false | |
| 862 | + | if readWait = !o.readDone(); !readWait { | |
| 863 | + | o.report(nil, seen) | |
| 864 | + | reportDirty = false | |
| 865 | + | } | |
| 768 | 866 | } | |
| 769 | 867 | o.flushReport(presented) | |
| 770 | - | // An unsent report is retried soon, not only on the next event. | |
| 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. | |
| 771 | 870 | var retry <-chan time.Time | |
| 772 | - | if len(o.unsent) > 0 { | |
| 871 | + | if len(o.unsent) > 0 || readWait { | |
| 773 | 872 | retry = time.After(time.Millisecond) | |
| 774 | 873 | } | |
| 775 | 874 | // A composed screen that stopped changing still drops VRR on time. | |
| @@ -780,13 +879,16 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 780 | 879 | // A commit whose event never comes must not stop the output. | |
| 781 | 880 | var stuck <-chan time.Time | |
| 782 | 881 | if enabled && o.pending { | |
| 783 | - | stuck = time.After(max(0, o.pendingLimit()-time.Since(o.flipStart))) | |
| 882 | + | stuck = time.After(max(0, time.Until(o.stuckAt))) | |
| 784 | 883 | } | |
| 785 | 884 | stateDirty := false | |
| 786 | 885 | select { | |
| 787 | 886 | case <-retry: | |
| 788 | 887 | continue | |
| 789 | 888 | case <-stuck: | |
| 889 | + | if o.pending && o.stuckAt.After(time.Now()) { | |
| 890 | + | continue // waits for a fence | |
| 891 | + | } | |
| 790 | 892 | if o.expire() { | |
| 791 | 893 | if err := o.modeset(); err != nil { | |
| 792 | 894 | if !o.commitFailed(err, &enabled) { | |
| @@ -919,8 +1021,12 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 919 | 1021 | } | |
| 920 | 1022 | // The overlay buffer is on screen like a scanned-out one: it | |
| 921 | 1023 | // is reported shown, so it is not released under the plane. | |
| 922 | - | f.queued, f.zeroCopy = ov.buf, ov.id | |
| 1024 | + | f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true | |
| 923 | 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 | + | } | |
| 924 | 1030 | if done != nil { | |
| 925 | 1031 | done.Close() | |
| 926 | 1032 | } | |
| @@ -958,12 +1064,16 @@ func (o *Output) commitFailed(err error, enabled *bool) bool { | |||
| 958 | 1064 | // The kernel still works on an earlier commit (e.g. across a VT | |
| 959 | 1065 | // switch, or one whose event already came): wait for any event | |
| 960 | 1066 | // of this CRTC, or retry after busyRetry since none may come. | |
| 961 | - | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | |
| 1067 | + | o.endPending() | |
| 1068 | + | now := time.Now() | |
| 1069 | + | o.pending, o.flipStart, o.stuckAt = true, now, now.Add(busyRetry) | |
| 962 | 1070 | o.pendingSerial = 0 | |
| 963 | - | if o.busySince.IsZero() { | |
| 964 | - | o.busySince = o.flipStart | |
| 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 | |
| 965 | 1074 | o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy") | |
| 966 | 1075 | } | |
| 1076 | + | o.lastBusy = now | |
| 967 | 1077 | return true | |
| 968 | 1078 | case lostMaster(err): | |
| 969 | 1079 | o.log.Warn().Err(err).Msg("commit refused") | |
| @@ -982,7 +1092,7 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool { | |||
| 982 | 1092 | return false | |
| 983 | 1093 | } | |
| 984 | 1094 | f := o.pendingFrame | |
| 985 | - | o.pending, o.pendingFrame = false, pendingFrame{} | |
| 1095 | + | o.endPending() | |
| 986 | 1096 | o.busySince = time.Time{} | |
| 987 | 1097 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| 988 | 1098 | o.log.Info().Dur("flip_ms", d).Str("connector", o.conn.name).Msg("slow flip") | |
| @@ -993,6 +1103,11 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool { | |||
| 993 | 1103 | if !f.frame { | |
| 994 | 1104 | return true | |
| 995 | 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 | + | } | |
| 996 | 1111 | o.flips++ | |
| 997 | 1112 | o.shown = f.queued | |
| 998 | 1113 | if o.queued == f.queued { | |
| @@ -63,11 +63,19 @@ type Output struct { | |||||
| 63 | pendingSerial, nextSerial uint64 | 63 | pendingSerial, nextSerial uint64 | ||
| 64 | flipStart time.Time | 64 | flipStart time.Time | ||
| 65 | flips int | 65 | flips int | ||
| 66 | - | // A commit whose event does not come within stuckAfter (default | 66 | + | // A commit whose event does not come by stuckAt is abandoned with a |
| 67 | - | // stuckTimeout) is abandoned with a modeset. A commit refused with | 67 | + | // modeset once its fences signalled (stuckAfter, default |
| 68 | - | // EBUSY is retried after busyRetry; busySince is when EBUSY began. | 68 | + | // stuckTimeout; fenceWarned once it waits for a fence). A commit |
| 69 | - | stuckAfter time.Duration | 69 | + | // refused with EBUSY is retried after busyRetry; busySince is when |
| 70 | - | busySince time.Time | 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 | |||
| 71 | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | 79 | // Direct scanout: client framebuffers by DMABuf ID and the buffer on | ||
| 72 | // screen and queued (0: the composed image). | 80 | // screen and queued (0: the composed image). | ||
| 73 | scanout bool | 81 | scanout bool | ||
| @@ -103,6 +111,86 @@ type pendingFrame struct { | |||||
| 103 | zeroCopy ports.WindowID // window shown without composition | 111 | zeroCopy ports.WindowID // window shown without composition | ||
| 104 | async bool | 112 | async bool | ||
| 105 | shows map[ports.WindowID]uint64 | 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. | |||
| 122 | + | func (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. | |||
| 130 | + | func 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. | |||
| 142 | + | func 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. | |||
| 162 | + | func (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. | |||
| 168 | + | func (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. | |||
| 183 | + | func (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. | |||
| 191 | + | func (o *Output) endPending() { | |||
| 192 | + | o.pendingFrame.closeFences() | |||
| 193 | + | o.pending, o.pendingFrame = false, pendingFrame{} | |||
| 106 | } | 194 | } | ||
| 107 | 195 | ||||
| 108 | // Commit event userData: the commit serial above userKindBits, the kind | 196 | // Commit event userData: the commit serial above userKindBits, the kind | ||
| @@ -123,7 +211,9 @@ func (o *Output) userData(kind uint64) uint64 { | |||||
| 123 | // begin marks the commit made with the last userData pending. | 211 | // begin marks the commit made with the last userData pending. | ||
| 124 | func (o *Output) begin(f pendingFrame) { | 212 | func (o *Output) begin(f pendingFrame) { | ||
| 125 | o.pendingSerial = o.nextSerial | 213 | o.pendingSerial = o.nextSerial | ||
| 214 | + | o.pendingFrame.closeFences() | |||
| 126 | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | 215 | o.pending, o.pendingFrame, o.flipStart = true, f, time.Now() | ||
| 216 | + | o.stuckAt, o.fenceWarned = o.flipStart.Add(o.stuckLimit()), false | |||
| 127 | o.busySince = time.Time{} | 217 | o.busySince = time.Time{} | ||
| 128 | } | 218 | } | ||
| 129 | 219 | ||||
| @@ -141,29 +231,29 @@ func (o *Output) stuckLimit() time.Duration { | |||||
| 141 | return stuckTimeout | 231 | return stuckTimeout | ||
| 142 | } | 232 | } | ||
| 143 | 233 | ||||
| 144 | - | // pendingLimit is how long the pending commit may wait for its event. | 234 | + | // expire handles a pending commit whose event did not come by stuckAt. |
| 145 | - | // After EBUSY no commit of ours may be in flight: no event may come, so | 235 | + | // A commit still waiting for a fence (a long GPU job) keeps waiting. |
| 146 | - | // the commit is retried soon. | 236 | + | // Else the commit is abandoned so the next can go; it reports whether |
| 147 | - | func (o *Output) pendingLimit() time.Duration { | 237 | + | // the output needs a modeset: an event is missing (the kernel state is |
| 148 | - | if o.pendingSerial == 0 { | 238 | + | // unknown) or EBUSY lasted past the limit. |
| 149 | - | return busyRetry | |||
| 150 | - | } | |||
| 151 | - | return o.stuckLimit() | |||
| 152 | - | } | |||
| 153 | - | ||||
| 154 | - | // expire ends a pending commit whose event did not come in time, so the | |||
| 155 | - | // next commit can go. It reports whether the output needs a modeset: an | |||
| 156 | - | // event is missing (the kernel state is unknown) or EBUSY lasted past | |||
| 157 | - | // the limit. | |||
| 158 | func (o *Output) expire() bool { | 239 | func (o *Output) expire() bool { | ||
| 159 | - | age := time.Since(o.flipStart) | 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 | + | } | |||
| 160 | kind := "state" | 250 | kind := "state" | ||
| 161 | if o.pendingFrame.frame { | 251 | if o.pendingFrame.frame { | ||
| 162 | kind = "frame" | 252 | kind = "frame" | ||
| 163 | } | 253 | } | ||
| 164 | - | o.pending, o.pendingFrame = false, pendingFrame{} | 254 | + | o.endPending() |
| 165 | if o.pendingSerial == 0 { | 255 | if o.pendingSerial == 0 { | ||
| 166 | - | busy := time.Since(o.busySince) | 256 | + | busy := now.Sub(o.busySince) |
| 167 | if o.busySince.IsZero() || busy < o.stuckLimit() { | 257 | if o.busySince.IsZero() || busy < o.stuckLimit() { | ||
| 168 | return false | 258 | return false | ||
| 169 | } | 259 | } | ||
| @@ -316,9 +406,6 @@ func (o *Output) primaryProps(req *atomicReq, fb uint32) { | |||||
| 316 | // other plane on the CRTC; needed at start and after every VT resume. | 406 | // other plane on the CRTC; needed at start and after every VT resume. | ||
| 317 | // It blocks until the kernel applied it. | 407 | // It blocks until the kernel applied it. | ||
| 318 | func (o *Output) modeset() error { | 408 | func (o *Output) modeset() error { | ||
| 319 | - | o.pending, o.busySince = false, time.Time{} | |||
| 320 | - | o.pendingFrame = pendingFrame{} | |||
| 321 | - | o.shown, o.queued = 0, 0 | |||
| 322 | blob, err := o.modeBlobFor(o.mode) | 409 | blob, err := o.modeBlobFor(o.mode) | ||
| 323 | if err != nil { | 410 | if err != nil { | ||
| 324 | return err | 411 | return err | ||
| @@ -327,6 +414,11 @@ func (o *Output) modeset() error { | |||||
| 327 | _ = o.k.destroyBlob(blob) | 414 | _ = o.k.destroyBlob(blob) | ||
| 328 | return fmt.Errorf("modeset: %w", err) | 415 | return fmt.Errorf("modeset: %w", err) | ||
| 329 | } | 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 | |||
| 330 | if o.modeBlob != 0 { | 422 | if o.modeBlob != 0 { | ||
| 331 | _ = o.k.destroyBlob(o.modeBlob) | 423 | _ = o.k.destroyBlob(o.modeBlob) | ||
| 332 | } | 424 | } | ||
| @@ -531,6 +623,7 @@ func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f p | |||||
| 531 | o.overlayOn = ov.buf | 623 | o.overlayOn = ov.buf | ||
| 532 | } | 624 | } | ||
| 533 | f.frame, f.async = true, async | 625 | f.frame, f.async = true, async | ||
| 626 | + | f.fences = dupFences(fence, ov.acquire) | |||
| 534 | o.setAsync(async) | 627 | o.setAsync(async) | ||
| 535 | o.begin(f) | 628 | o.begin(f) | ||
| 536 | return nil | 629 | return nil | ||
| @@ -711,6 +804,8 @@ func (o *Output) Close() { | |||||
| 711 | o.cursor.free(o.k) | 804 | o.cursor.free(o.k) | ||
| 712 | } | 805 | } | ||
| 713 | o.freeImages() | 806 | o.freeImages() | ||
| 807 | + | o.endPending() | |||
| 808 | + | o.dropRead() | |||
| 714 | o.shown, o.queued = 0, 0 | 809 | o.shown, o.queued = 0, 0 | ||
| 715 | o.dropClientFBs(time.Now(), true) | 810 | o.dropClientFBs(time.Now(), true) | ||
| 716 | } | 811 | } | ||
| @@ -762,14 +857,18 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 762 | cursorWake = o.cursor.wake | 857 | cursorWake = o.cursor.wake | ||
| 763 | } | 858 | } | ||
| 764 | for { | 859 | for { | ||
| 860 | + | readWait := false | |||
| 765 | if reportDirty && !o.pending { | 861 | if reportDirty && !o.pending { | ||
| 766 | - | o.report(nil, seen) | 862 | + | if readWait = !o.readDone(); !readWait { |
| 767 | - | reportDirty = false | 863 | + | o.report(nil, seen) |
| 864 | + | reportDirty = false | |||
| 865 | + | } | |||
| 768 | } | 866 | } | ||
| 769 | o.flushReport(presented) | 867 | o.flushReport(presented) | ||
| 770 | - | // An unsent report is retried soon, not only on the next event. | 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. | |||
| 771 | var retry <-chan time.Time | 870 | var retry <-chan time.Time | ||
| 772 | - | if len(o.unsent) > 0 { | 871 | + | if len(o.unsent) > 0 || readWait { |
| 773 | retry = time.After(time.Millisecond) | 872 | retry = time.After(time.Millisecond) | ||
| 774 | } | 873 | } | ||
| 775 | // A composed screen that stopped changing still drops VRR on time. | 874 | // A composed screen that stopped changing still drops VRR on time. | ||
| @@ -780,13 +879,16 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 780 | // A commit whose event never comes must not stop the output. | 879 | // A commit whose event never comes must not stop the output. | ||
| 781 | var stuck <-chan time.Time | 880 | var stuck <-chan time.Time | ||
| 782 | if enabled && o.pending { | 881 | if enabled && o.pending { | ||
| 783 | - | stuck = time.After(max(0, o.pendingLimit()-time.Since(o.flipStart))) | 882 | + | stuck = time.After(max(0, time.Until(o.stuckAt))) |
| 784 | } | 883 | } | ||
| 785 | stateDirty := false | 884 | stateDirty := false | ||
| 786 | select { | 885 | select { | ||
| 787 | case <-retry: | 886 | case <-retry: | ||
| 788 | continue | 887 | continue | ||
| 789 | case <-stuck: | 888 | case <-stuck: | ||
| 889 | + | if o.pending && o.stuckAt.After(time.Now()) { | |||
| 890 | + | continue // waits for a fence | |||
| 891 | + | } | |||
| 790 | if o.expire() { | 892 | if o.expire() { | ||
| 791 | if err := o.modeset(); err != nil { | 893 | if err := o.modeset(); err != nil { | ||
| 792 | if !o.commitFailed(err, &enabled) { | 894 | if !o.commitFailed(err, &enabled) { | ||
| @@ -919,8 +1021,12 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 919 | } | 1021 | } | ||
| 920 | // The overlay buffer is on screen like a scanned-out one: it | 1022 | // The overlay buffer is on screen like a scanned-out one: it | ||
| 921 | // is reported shown, so it is not released under the plane. | 1023 | // is reported shown, so it is not released under the plane. | ||
| 922 | - | f.queued, f.zeroCopy = ov.buf, ov.id | 1024 | + | f.queued, f.zeroCopy, f.composed = ov.buf, ov.id, true |
| 923 | err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov) | 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 | + | } | |||
| 924 | if done != nil { | 1030 | if done != nil { | ||
| 925 | done.Close() | 1031 | done.Close() | ||
| 926 | } | 1032 | } | ||
| @@ -958,12 +1064,16 @@ func (o *Output) commitFailed(err error, enabled *bool) bool { | |||||
| 958 | // The kernel still works on an earlier commit (e.g. across a VT | 1064 | // The kernel still works on an earlier commit (e.g. across a VT | ||
| 959 | // switch, or one whose event already came): wait for any event | 1065 | // switch, or one whose event already came): wait for any event | ||
| 960 | // of this CRTC, or retry after busyRetry since none may come. | 1066 | // of this CRTC, or retry after busyRetry since none may come. | ||
| 961 | - | o.pending, o.pendingFrame, o.flipStart = true, pendingFrame{}, time.Now() | 1067 | + | o.endPending() |
| 1068 | + | now := time.Now() | |||
| 1069 | + | o.pending, o.flipStart, o.stuckAt = true, now, now.Add(busyRetry) | |||
| 962 | o.pendingSerial = 0 | 1070 | o.pendingSerial = 0 | ||
| 963 | - | if o.busySince.IsZero() { | 1071 | + | // A run of EBUSY: each refusal follows the previous retry. |
| 964 | - | o.busySince = o.flipStart | 1072 | + | if o.busySince.IsZero() || now.Sub(o.lastBusy) > 4*busyRetry { |
| 1073 | + | o.busySince = now | |||
| 965 | o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy") | 1074 | o.log.Info().Err(err).Str("connector", o.conn.name).Msg("commit busy") | ||
| 966 | } | 1075 | } | ||
| 1076 | + | o.lastBusy = now | |||
| 967 | return true | 1077 | return true | ||
| 968 | case lostMaster(err): | 1078 | case lostMaster(err): | ||
| 969 | o.log.Warn().Err(err).Msg("commit refused") | 1079 | o.log.Warn().Err(err).Msg("commit refused") | ||
| @@ -982,7 +1092,7 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool { | |||||
| 982 | return false | 1092 | return false | ||
| 983 | } | 1093 | } | ||
| 984 | f := o.pendingFrame | 1094 | f := o.pendingFrame | ||
| 985 | - | o.pending, o.pendingFrame = false, pendingFrame{} | 1095 | + | o.endPending() |
| 986 | o.busySince = time.Time{} | 1096 | o.busySince = time.Time{} | ||
| 987 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | 1097 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | ||
| 988 | o.log.Info().Dur("flip_ms", d).Str("connector", o.conn.name).Msg("slow flip") | 1098 | o.log.Info().Dur("flip_ms", d).Str("connector", o.conn.name).Msg("slow flip") | ||
| @@ -993,6 +1103,11 @@ func (o *Output) completed(ev flipEvent, seen map[ports.WindowID]uint64) bool { | |||||
| 993 | if !f.frame { | 1103 | if !f.frame { | ||
| 994 | return true | 1104 | return true | ||
| 995 | } | 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 | + | } | |||
| 996 | o.flips++ | 1111 | o.flips++ | ||
| 997 | o.shown = f.queued | 1112 | o.shown = f.queued | ||
| 998 | if o.queued == f.queued { | 1113 | if o.queued == f.queued { | ||
M internal/adapters/drm/stuck_test.go +155 -13
| @@ -3,6 +3,8 @@ package drm | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "os" | |
| 6 | + | "sync" | |
| 7 | + | "sync/atomic" | |
| 6 | 8 | "testing" | |
| 7 | 9 | "time" | |
| 8 | 10 | ||
| @@ -16,6 +18,28 @@ import ( | |||
| 16 | 18 | // modeset test and modeset) and whose CRTC never sends an event. It | |
| 17 | 19 | // returns the recorded commits once n were made, failing after 2s. | |
| 18 | 20 | func runStuck(t *testing.T, n int, errs ...error) []commitRec { | |
| 21 | + | t.Helper() | |
| 22 | + | h := startStuck(t, nil, errs...) | |
| 23 | + | h.scenes <- ports.Scene{Seq: 1} | |
| 24 | + | waitFor(t, func() bool { return h.count() >= n }) | |
| 25 | + | return h.stop() | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // stuckRun is an output run by startStuck. | |
| 29 | + | type stuckRun struct { | |
| 30 | + | t *testing.T | |
| 31 | + | scenes chan ports.Scene | |
| 32 | + | contents chan ports.SurfaceContent | |
| 33 | + | active chan bool | |
| 34 | + | presented chan ports.OutputPresented | |
| 35 | + | count func() int | |
| 36 | + | stop func() []commitRec | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // startStuck runs an output whose commits return errs (after the start | |
| 40 | + | // modeset test and modeset) and whose CRTC never sends an event. render | |
| 41 | + | // makes each frame's fence once the output started (nil: none). | |
| 42 | + | func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun { | |
| 19 | 43 | t.Helper() | |
| 20 | 44 | o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...) | |
| 21 | 45 | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | |
| @@ -29,33 +53,41 @@ func runStuck(t *testing.T, n int, errs ...error) []commitRec { | |||
| 29 | 53 | } | |
| 30 | 54 | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |
| 31 | 55 | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 32 | - | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |
| 56 | + | var started atomic.Bool | |
| 57 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |
| 58 | + | if render == nil || !started.Load() { | |
| 59 | + | return nil, nil | |
| 60 | + | } | |
| 61 | + | return render(), nil | |
| 62 | + | }) | |
| 33 | 63 | r.EXPECT().Close().Return().Once() | |
| 34 | 64 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | |
| 35 | 65 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 36 | 66 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 37 | 67 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 38 | - | scenes := make(chan ports.Scene, 1) | |
| 68 | + | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)} | |
| 39 | 69 | ctx, cancel := context.WithCancel(context.Background()) | |
| 40 | 70 | done := make(chan error, 1) | |
| 41 | 71 | go func() { | |
| 42 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 64)) | |
| 72 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented) | |
| 43 | 73 | }() | |
| 44 | - | count := func() int { | |
| 74 | + | h.count = func() int { | |
| 45 | 75 | commitMu.Lock() | |
| 46 | 76 | defer commitMu.Unlock() | |
| 47 | 77 | return len(*commits) | |
| 48 | 78 | } | |
| 49 | - | waitFor(t, func() bool { return count() == 2 }) | |
| 50 | - | scenes <- ports.Scene{Seq: 1} | |
| 51 | - | waitFor(t, func() bool { return count() >= n }) | |
| 52 | - | cancel() | |
| 53 | - | if err := <-done; err != nil { | |
| 54 | - | t.Fatal(err) | |
| 79 | + | h.stop = func() []commitRec { | |
| 80 | + | cancel() | |
| 81 | + | if err := <-done; err != nil { | |
| 82 | + | t.Fatal(err) | |
| 83 | + | } | |
| 84 | + | commitMu.Lock() | |
| 85 | + | defer commitMu.Unlock() | |
| 86 | + | return append([]commitRec(nil), (*commits)...) | |
| 55 | 87 | } | |
| 56 | - | commitMu.Lock() | |
| 57 | - | defer commitMu.Unlock() | |
| 58 | - | return append([]commitRec(nil), (*commits)...) | |
| 88 | + | waitFor(t, func() bool { return h.count() == 2 }) | |
| 89 | + | started.Store(true) | |
| 90 | + | return h | |
| 59 | 91 | } | |
| 60 | 92 | ||
| 61 | 93 | // A commit whose event never comes must not stop the output: it is | |
| @@ -100,3 +132,113 @@ func TestLastingBusyModesets(t *testing.T) { | |||
| 100 | 132 | t.Fatal("no modeset while EBUSY lasted") | |
| 101 | 133 | } | |
| 102 | 134 | } | |
| 135 | + | ||
| 136 | + | // fencePipe is a fence that signals when its write end is written. | |
| 137 | + | func fencePipe(t *testing.T) (fence, signal *os.File) { | |
| 138 | + | t.Helper() | |
| 139 | + | r, w, err := os.Pipe() | |
| 140 | + | if err != nil { | |
| 141 | + | t.Fatal(err) | |
| 142 | + | } | |
| 143 | + | t.Cleanup(func() { r.Close(); w.Close() }) | |
| 144 | + | return r, w | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // A frame whose GPU fence has not signalled is slow, not lost: it keeps | |
| 148 | + | // waiting past the limit; once the fence signalled and still no event | |
| 149 | + | // came, the output modesets. | |
| 150 | + | func TestUnsignalledFenceDelaysRecovery(t *testing.T) { | |
| 151 | + | var mu sync.Mutex | |
| 152 | + | var signals []*os.File | |
| 153 | + | h := startStuck(t, func() *os.File { | |
| 154 | + | f, w := fencePipe(t) | |
| 155 | + | mu.Lock() | |
| 156 | + | signals = append(signals, w) | |
| 157 | + | mu.Unlock() | |
| 158 | + | return f | |
| 159 | + | }) | |
| 160 | + | h.scenes <- ports.Scene{Seq: 1} | |
| 161 | + | waitFor(t, func() bool { return h.count() == 3 }) | |
| 162 | + | time.Sleep(3 * 200 * time.Millisecond) | |
| 163 | + | if n := h.count(); n != 3 { | |
| 164 | + | t.Fatalf("%d commits while the frame's fence is unsignalled", n) | |
| 165 | + | } | |
| 166 | + | mu.Lock() | |
| 167 | + | _, _ = signals[0].Write([]byte{1}) | |
| 168 | + | mu.Unlock() | |
| 169 | + | waitFor(t, func() bool { return h.count() >= 4 }) | |
| 170 | + | if c := h.stop(); c[3].flags&atomicAllowModes == 0 { | |
| 171 | + | t.Fatalf("no modeset once the fence signalled: %#x", c[3].flags) | |
| 172 | + | } | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | // A frame refused with EBUSY may still be read by the GPU: what was seen | |
| 176 | + | // is reported only once its fence signalled. | |
| 177 | + | func TestSeenWaitsForRefusedFrameFence(t *testing.T) { | |
| 178 | + | var mu sync.Mutex | |
| 179 | + | var signals []*os.File | |
| 180 | + | h := startStuck(t, func() *os.File { | |
| 181 | + | f, w := fencePipe(t) | |
| 182 | + | mu.Lock() | |
| 183 | + | signals = append(signals, w) | |
| 184 | + | mu.Unlock() | |
| 185 | + | return f | |
| 186 | + | }, unix.EBUSY, unix.EACCES) | |
| 187 | + | h.scenes <- ports.Scene{Seq: 1} | |
| 188 | + | // EBUSY, then the retry loses DRM master: nothing is pending. | |
| 189 | + | waitFor(t, func() bool { return h.count() == 4 }) | |
| 190 | + | h.contents <- ports.SurfaceContent{ID: 1, Seq: 7} | |
| 191 | + | select { | |
| 192 | + | case p := <-h.presented: | |
| 193 | + | t.Fatalf("seen reported while refused frames may be read: %+v", p) | |
| 194 | + | case <-time.After(50 * time.Millisecond): | |
| 195 | + | } | |
| 196 | + | mu.Lock() | |
| 197 | + | for _, w := range signals { | |
| 198 | + | _, _ = w.Write([]byte{1}) | |
| 199 | + | } | |
| 200 | + | mu.Unlock() | |
| 201 | + | select { | |
| 202 | + | case p := <-h.presented: | |
| 203 | + | if p.Seen[1] != 7 { | |
| 204 | + | t.Fatalf("report %+v", p) | |
| 205 | + | } | |
| 206 | + | case <-time.After(2 * time.Second): | |
| 207 | + | t.Fatal("seen not reported once the fences signalled") | |
| 208 | + | } | |
| 209 | + | h.stop() | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | // An EBUSY run ends when commits stop being refused: a later EBUSY is | |
| 213 | + | // retried, not taken as the same run lasting past the limit. | |
| 214 | + | func TestLaterBusyStartsNewRun(t *testing.T) { | |
| 215 | + | o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY) | |
| 216 | + | o.stuckAfter = 100 * time.Millisecond | |
| 217 | + | enabled := true | |
| 218 | + | o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) | |
| 219 | + | time.Sleep(busyRetry) | |
| 220 | + | if o.expire() { | |
| 221 | + | t.Fatal("first EBUSY retry modesets") | |
| 222 | + | } | |
| 223 | + | time.Sleep(2 * o.stuckAfter) | |
| 224 | + | o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) | |
| 225 | + | time.Sleep(busyRetry) | |
| 226 | + | if o.expire() { | |
| 227 | + | t.Fatal("a new EBUSY taken as the old run: modeset") | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | // A modeset that fails keeps what is on screen: buffers still shown are | |
| 232 | + | // not released under the plane. | |
| 233 | + | func TestFailedModesetKeepsShown(t *testing.T) { | |
| 234 | + | o, k, _ := testOutput(t, unix.EACCES) | |
| 235 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |
| 236 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |
| 237 | + | o.shown, o.queued = 5, 6 | |
| 238 | + | if err := o.modeset(); err == nil { | |
| 239 | + | t.Fatal("modeset succeeded") | |
| 240 | + | } | |
| 241 | + | if o.shown != 5 || o.queued != 6 { | |
| 242 | + | t.Fatalf("shown %d queued %d after a failed modeset", o.shown, o.queued) | |
| 243 | + | } | |
| 244 | + | } | |
| @@ -3,6 +3,8 @@ package drm | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | + | "sync" | |||
| 7 | + | "sync/atomic" | |||
| 6 | "testing" | 8 | "testing" | ||
| 7 | "time" | 9 | "time" | ||
| 8 | 10 | ||||
| @@ -16,6 +18,28 @@ import ( | |||||
| 16 | // modeset test and modeset) and whose CRTC never sends an event. It | 18 | // modeset test and modeset) and whose CRTC never sends an event. It | ||
| 17 | // returns the recorded commits once n were made, failing after 2s. | 19 | // returns the recorded commits once n were made, failing after 2s. | ||
| 18 | func runStuck(t *testing.T, n int, errs ...error) []commitRec { | 20 | func runStuck(t *testing.T, n int, errs ...error) []commitRec { | ||
| 21 | + | t.Helper() | |||
| 22 | + | h := startStuck(t, nil, errs...) | |||
| 23 | + | h.scenes <- ports.Scene{Seq: 1} | |||
| 24 | + | waitFor(t, func() bool { return h.count() >= n }) | |||
| 25 | + | return h.stop() | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // stuckRun is an output run by startStuck. | |||
| 29 | + | type stuckRun struct { | |||
| 30 | + | t *testing.T | |||
| 31 | + | scenes chan ports.Scene | |||
| 32 | + | contents chan ports.SurfaceContent | |||
| 33 | + | active chan bool | |||
| 34 | + | presented chan ports.OutputPresented | |||
| 35 | + | count func() int | |||
| 36 | + | stop func() []commitRec | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // startStuck runs an output whose commits return errs (after the start | |||
| 40 | + | // modeset test and modeset) and whose CRTC never sends an event. render | |||
| 41 | + | // makes each frame's fence once the output started (nil: none). | |||
| 42 | + | func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun { | |||
| 19 | t.Helper() | 43 | t.Helper() | ||
| 20 | o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...) | 44 | o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...) | ||
| 21 | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | 45 | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | ||
| @@ -29,33 +53,41 @@ func runStuck(t *testing.T, n int, errs ...error) []commitRec { | |||||
| 29 | } | 53 | } | ||
| 30 | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | 54 | r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | ||
| 31 | r.EXPECT().UseTarget(mock.Anything).Return() | 55 | r.EXPECT().UseTarget(mock.Anything).Return() | ||
| 32 | - | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | 56 | + | var started atomic.Bool |
| 57 | + | r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) { | |||
| 58 | + | if render == nil || !started.Load() { | |||
| 59 | + | return nil, nil | |||
| 60 | + | } | |||
| 61 | + | return render(), nil | |||
| 62 | + | }) | |||
| 33 | r.EXPECT().Close().Return().Once() | 63 | r.EXPECT().Close().Return().Once() | ||
| 34 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | 64 | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice() | ||
| 35 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | 65 | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | ||
| 36 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | 66 | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | ||
| 37 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | 67 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | ||
| 38 | - | scenes := make(chan ports.Scene, 1) | 68 | + | h := &stuckRun{t: t, scenes: make(chan ports.Scene, 1), contents: make(chan ports.SurfaceContent, 1), active: make(chan bool, 1), presented: make(chan ports.OutputPresented, 64)} |
| 39 | ctx, cancel := context.WithCancel(context.Background()) | 69 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 40 | done := make(chan error, 1) | 70 | done := make(chan error, 1) | ||
| 41 | go func() { | 71 | go func() { | ||
| 42 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 64)) | 72 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents, nil, h.presented) |
| 43 | }() | 73 | }() | ||
| 44 | - | count := func() int { | 74 | + | h.count = func() int { |
| 45 | commitMu.Lock() | 75 | commitMu.Lock() | ||
| 46 | defer commitMu.Unlock() | 76 | defer commitMu.Unlock() | ||
| 47 | return len(*commits) | 77 | return len(*commits) | ||
| 48 | } | 78 | } | ||
| 49 | - | waitFor(t, func() bool { return count() == 2 }) | 79 | + | h.stop = func() []commitRec { |
| 50 | - | scenes <- ports.Scene{Seq: 1} | 80 | + | cancel() |
| 51 | - | waitFor(t, func() bool { return count() >= n }) | 81 | + | if err := <-done; err != nil { |
| 52 | - | cancel() | 82 | + | t.Fatal(err) |
| 53 | - | if err := <-done; err != nil { | 83 | + | } |
| 54 | - | t.Fatal(err) | 84 | + | commitMu.Lock() |
| 85 | + | defer commitMu.Unlock() | |||
| 86 | + | return append([]commitRec(nil), (*commits)...) | |||
| 55 | } | 87 | } | ||
| 56 | - | commitMu.Lock() | 88 | + | waitFor(t, func() bool { return h.count() == 2 }) |
| 57 | - | defer commitMu.Unlock() | 89 | + | started.Store(true) |
| 58 | - | return append([]commitRec(nil), (*commits)...) | 90 | + | return h |
| 59 | } | 91 | } | ||
| 60 | 92 | ||||
| 61 | // A commit whose event never comes must not stop the output: it is | 93 | // A commit whose event never comes must not stop the output: it is | ||
| @@ -100,3 +132,113 @@ func TestLastingBusyModesets(t *testing.T) { | |||||
| 100 | t.Fatal("no modeset while EBUSY lasted") | 132 | t.Fatal("no modeset while EBUSY lasted") | ||
| 101 | } | 133 | } | ||
| 102 | } | 134 | } | ||
| 135 | + | ||||
| 136 | + | // fencePipe is a fence that signals when its write end is written. | |||
| 137 | + | func fencePipe(t *testing.T) (fence, signal *os.File) { | |||
| 138 | + | t.Helper() | |||
| 139 | + | r, w, err := os.Pipe() | |||
| 140 | + | if err != nil { | |||
| 141 | + | t.Fatal(err) | |||
| 142 | + | } | |||
| 143 | + | t.Cleanup(func() { r.Close(); w.Close() }) | |||
| 144 | + | return r, w | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // A frame whose GPU fence has not signalled is slow, not lost: it keeps | |||
| 148 | + | // waiting past the limit; once the fence signalled and still no event | |||
| 149 | + | // came, the output modesets. | |||
| 150 | + | func TestUnsignalledFenceDelaysRecovery(t *testing.T) { | |||
| 151 | + | var mu sync.Mutex | |||
| 152 | + | var signals []*os.File | |||
| 153 | + | h := startStuck(t, func() *os.File { | |||
| 154 | + | f, w := fencePipe(t) | |||
| 155 | + | mu.Lock() | |||
| 156 | + | signals = append(signals, w) | |||
| 157 | + | mu.Unlock() | |||
| 158 | + | return f | |||
| 159 | + | }) | |||
| 160 | + | h.scenes <- ports.Scene{Seq: 1} | |||
| 161 | + | waitFor(t, func() bool { return h.count() == 3 }) | |||
| 162 | + | time.Sleep(3 * 200 * time.Millisecond) | |||
| 163 | + | if n := h.count(); n != 3 { | |||
| 164 | + | t.Fatalf("%d commits while the frame's fence is unsignalled", n) | |||
| 165 | + | } | |||
| 166 | + | mu.Lock() | |||
| 167 | + | _, _ = signals[0].Write([]byte{1}) | |||
| 168 | + | mu.Unlock() | |||
| 169 | + | waitFor(t, func() bool { return h.count() >= 4 }) | |||
| 170 | + | if c := h.stop(); c[3].flags&atomicAllowModes == 0 { | |||
| 171 | + | t.Fatalf("no modeset once the fence signalled: %#x", c[3].flags) | |||
| 172 | + | } | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | // A frame refused with EBUSY may still be read by the GPU: what was seen | |||
| 176 | + | // is reported only once its fence signalled. | |||
| 177 | + | func TestSeenWaitsForRefusedFrameFence(t *testing.T) { | |||
| 178 | + | var mu sync.Mutex | |||
| 179 | + | var signals []*os.File | |||
| 180 | + | h := startStuck(t, func() *os.File { | |||
| 181 | + | f, w := fencePipe(t) | |||
| 182 | + | mu.Lock() | |||
| 183 | + | signals = append(signals, w) | |||
| 184 | + | mu.Unlock() | |||
| 185 | + | return f | |||
| 186 | + | }, unix.EBUSY, unix.EACCES) | |||
| 187 | + | h.scenes <- ports.Scene{Seq: 1} | |||
| 188 | + | // EBUSY, then the retry loses DRM master: nothing is pending. | |||
| 189 | + | waitFor(t, func() bool { return h.count() == 4 }) | |||
| 190 | + | h.contents <- ports.SurfaceContent{ID: 1, Seq: 7} | |||
| 191 | + | select { | |||
| 192 | + | case p := <-h.presented: | |||
| 193 | + | t.Fatalf("seen reported while refused frames may be read: %+v", p) | |||
| 194 | + | case <-time.After(50 * time.Millisecond): | |||
| 195 | + | } | |||
| 196 | + | mu.Lock() | |||
| 197 | + | for _, w := range signals { | |||
| 198 | + | _, _ = w.Write([]byte{1}) | |||
| 199 | + | } | |||
| 200 | + | mu.Unlock() | |||
| 201 | + | select { | |||
| 202 | + | case p := <-h.presented: | |||
| 203 | + | if p.Seen[1] != 7 { | |||
| 204 | + | t.Fatalf("report %+v", p) | |||
| 205 | + | } | |||
| 206 | + | case <-time.After(2 * time.Second): | |||
| 207 | + | t.Fatal("seen not reported once the fences signalled") | |||
| 208 | + | } | |||
| 209 | + | h.stop() | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | // An EBUSY run ends when commits stop being refused: a later EBUSY is | |||
| 213 | + | // retried, not taken as the same run lasting past the limit. | |||
| 214 | + | func TestLaterBusyStartsNewRun(t *testing.T) { | |||
| 215 | + | o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY) | |||
| 216 | + | o.stuckAfter = 100 * time.Millisecond | |||
| 217 | + | enabled := true | |||
| 218 | + | o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) | |||
| 219 | + | time.Sleep(busyRetry) | |||
| 220 | + | if o.expire() { | |||
| 221 | + | t.Fatal("first EBUSY retry modesets") | |||
| 222 | + | } | |||
| 223 | + | time.Sleep(2 * o.stuckAfter) | |||
| 224 | + | o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) | |||
| 225 | + | time.Sleep(busyRetry) | |||
| 226 | + | if o.expire() { | |||
| 227 | + | t.Fatal("a new EBUSY taken as the old run: modeset") | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // A modeset that fails keeps what is on screen: buffers still shown are | |||
| 232 | + | // not released under the plane. | |||
| 233 | + | func TestFailedModesetKeepsShown(t *testing.T) { | |||
| 234 | + | o, k, _ := testOutput(t, unix.EACCES) | |||
| 235 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil) | |||
| 236 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil) | |||
| 237 | + | o.shown, o.queued = 5, 6 | |||
| 238 | + | if err := o.modeset(); err == nil { | |||
| 239 | + | t.Fatal("modeset succeeded") | |||
| 240 | + | } | |||
| 241 | + | if o.shown != 5 || o.queued != 6 { | |||
| 242 | + | t.Fatalf("shown %d queued %d after a failed modeset", o.shown, o.queued) | |||
| 243 | + | } | |||
| 244 | + | } | |||