perf(wayland): tick the pacer inline on client commits
Every pacer wake cost a display.Do round trip (channel send, eventfd write, wait) even when the trigger already ran on the display goroutine: a commit called wakePacer from queueUpdate, the pacer goroutine woke and immediately Do'd back to the goroutine that woke it. pace now only collects flips and sleeps; the work moved to paceTick, which runs on the display goroutine and arms the pacer timer itself. A commit (queueAndTick) runs the tick inline when something needs the pacer, and wakePacer calls made while a commit or tick runs are recorded (paceDirty) and coalesced into one extra pass instead of one channel wake each. queueUpdate no longer wakes the pacer: a commit that applies at once needs no tick beyond what its callbacks and held buffers request, and one that stays queued (barrier, fence, too early) requests it from queueAndTick. The Do closure is a method value built once. Wakes from other goroutines (sync waiter) use signalPacer, unchanged. Tradeoff: the commit handler now does the frame/FIFO/release pass itself, so a burst of commits costs a few microseconds of extra display-goroutine work each, in exchange for no goroutine switch and no eventfd write per commit. Wakes outside a commit (configure, desync, flips, fences) still go through the pacer goroutine; flips need its channel anyway. Pacing semantics (per-output flips, 60 Hz default, 1 Hz suspended, feedback timeouts, held buffers, FIFO barriers) are unchanged; the timer is only armed from ticks, all on the display goroutine.
- commit
- 5db49007718ddc83d6fc3508a2f56f0aea1974c4
- author
- brice · · 2026-10-10 14:16 UTC
- committer
- brice · · 2026-10-10 15:39 UTC
- parent
- 0f9d086
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +239 -54
M internal/adapters/wayland/fifo.go +13 -2
| @@ -377,7 +377,6 @@ func (s *surface) queueUpdate() { | |||
| 377 | 377 | } | |
| 378 | 378 | s.queue = append(s.queue, u) | |
| 379 | 379 | s.server.fifoSurfaces[s] = struct{}{} | |
| 380 | - | s.server.wakePacer() | |
| 381 | 380 | } | |
| 382 | 381 | ||
| 383 | 382 | // applyUpdate applies only the captured commit; later requests remain pending. | |
| @@ -745,8 +744,20 @@ func sameBuffer(a, b *wayland.Buffer) bool { | |||
| 745 | 744 | return a != nil && b != nil && a.Resource == b.Resource | |
| 746 | 745 | } | |
| 747 | 746 | ||
| 748 | - | // wakePacer makes the pacer look at its queues now. | |
| 747 | + | // wakePacer makes the pacer look at its queues. Display goroutine only. | |
| 748 | + | // While a commit or a tick runs (paceDepth) it only records the request: the | |
| 749 | + | // outermost caller then ticks inline (paceNow), saving the round trip of | |
| 750 | + | // waking the pacer goroutine just so that it calls Do back. | |
| 749 | 751 | func (s *Server) wakePacer() { | |
| 752 | + | if s.paceDepth > 0 { | |
| 753 | + | s.paceDirty = true | |
| 754 | + | return | |
| 755 | + | } | |
| 756 | + | s.signalPacer() | |
| 757 | + | } | |
| 758 | + | ||
| 759 | + | // signalPacer wakes the pacer goroutine; safe from any goroutine. | |
| 760 | + | func (s *Server) signalPacer() { | |
| 750 | 761 | select { | |
| 751 | 762 | case s.frameReady <- struct{}{}: | |
| 752 | 763 | default: | |
| @@ -377,7 +377,6 @@ func (s *surface) queueUpdate() { | |||||
| 377 | } | 377 | } | ||
| 378 | s.queue = append(s.queue, u) | 378 | s.queue = append(s.queue, u) | ||
| 379 | s.server.fifoSurfaces[s] = struct{}{} | 379 | s.server.fifoSurfaces[s] = struct{}{} | ||
| 380 | - | s.server.wakePacer() | |||
| 381 | } | 380 | } | ||
| 382 | 381 | ||||
| 383 | // applyUpdate applies only the captured commit; later requests remain pending. | 382 | // applyUpdate applies only the captured commit; later requests remain pending. | ||
| @@ -745,8 +744,20 @@ func sameBuffer(a, b *wayland.Buffer) bool { | |||||
| 745 | return a != nil && b != nil && a.Resource == b.Resource | 744 | return a != nil && b != nil && a.Resource == b.Resource | ||
| 746 | } | 745 | } | ||
| 747 | 746 | ||||
| 748 | - | // wakePacer makes the pacer look at its queues now. | 747 | + | // wakePacer makes the pacer look at its queues. Display goroutine only. |
| 748 | + | // While a commit or a tick runs (paceDepth) it only records the request: the | |||
| 749 | + | // outermost caller then ticks inline (paceNow), saving the round trip of | |||
| 750 | + | // waking the pacer goroutine just so that it calls Do back. | |||
| 749 | func (s *Server) wakePacer() { | 751 | func (s *Server) wakePacer() { | ||
| 752 | + | if s.paceDepth > 0 { | |||
| 753 | + | s.paceDirty = true | |||
| 754 | + | return | |||
| 755 | + | } | |||
| 756 | + | s.signalPacer() | |||
| 757 | + | } | |||
| 758 | + | ||||
| 759 | + | // signalPacer wakes the pacer goroutine; safe from any goroutine. | |||
| 760 | + | func (s *Server) signalPacer() { | |||
| 750 | select { | 761 | select { | ||
| 751 | case s.frameReady <- struct{}{}: | 762 | case s.frameReady <- struct{}{}: | ||
| 752 | default: | 763 | default: | ||
M internal/adapters/wayland/frames.go +99 -44
| @@ -24,14 +24,26 @@ const defaultFramePeriod = time.Second / 60 | |||
| 24 | 24 | const suspendedFramePeriod = time.Second | |
| 25 | 25 | const suspendedFrameQueue = "\x00suspended" // never a wl_output name | |
| 26 | 26 | ||
| 27 | - | // pace fires the due frame callbacks on page flips and deadlines. | |
| 27 | + | // maxPacePasses bounds the passes one tick repeats while a pass queues | |
| 28 | + | // more work; the rest goes back to the pacer goroutine. | |
| 29 | + | const maxPacePasses = 3 | |
| 30 | + | ||
| 31 | + | // pace sleeps until a flip, a wake or the deadline armed by the last tick, | |
| 32 | + | // then runs a tick on the display goroutine. The timer is only armed from | |
| 33 | + | // ticks, which all run there, so a commit that ticks inline (paceNow) never | |
| 34 | + | // races the pacer for it. | |
| 28 | 35 | func (s *Server) pace(ctx context.Context) { | |
| 29 | - | timer := time.NewTimer(defaultFramePeriod) | |
| 36 | + | if s.paceTimer == nil { | |
| 37 | + | s.paceTimer = time.NewTimer(defaultFramePeriod) | |
| 38 | + | } | |
| 39 | + | timer := s.paceTimer | |
| 30 | 40 | defer timer.Stop() | |
| 31 | 41 | presented := s.channels.Presented | |
| 32 | 42 | if s.flipped == nil { | |
| 33 | 43 | s.flipped = make(map[string]bool) | |
| 34 | 44 | } | |
| 45 | + | // One method value for the whole loop: no closure per wake. | |
| 46 | + | tick := s.paceFromPacer | |
| 35 | 47 | for { | |
| 36 | 48 | clear(s.flipped) | |
| 37 | 49 | flipped := s.flipped | |
| @@ -61,52 +73,95 @@ func (s *Server) pace(ctx context.Context) { | |||
| 61 | 73 | } | |
| 62 | 74 | } | |
| 63 | 75 | s.frameReports = reports | |
| 64 | - | wait, idle := defaultFramePeriod, false | |
| 65 | - | if !s.display.Do(func() { | |
| 66 | - | if ctx.Err() == nil { | |
| 67 | - | // Each flip answers its own presentation feedbacks, in | |
| 68 | - | // order: flips are never merged here. | |
| 69 | - | for _, p := range reports { | |
| 70 | - | if p.Flip != nil { | |
| 71 | - | s.flips[p.Output] = *p.Flip | |
| 72 | - | s.presentFlip(p.Output, p.Flip) | |
| 73 | - | } | |
| 74 | - | } | |
| 75 | - | s.dropFeedbacks(time.Now()) | |
| 76 | - | if len(s.feedbacks) > 0 { | |
| 77 | - | wait = feedbackTimeout | |
| 78 | - | } | |
| 79 | - | var due []string | |
| 80 | - | var dueWait time.Duration | |
| 81 | - | due, dueWait, idle = s.dueFrames(time.Now(), flipped) | |
| 82 | - | if len(s.feedbacks) > 0 { | |
| 83 | - | // Unanswered feedbacks time out: keep ticking. | |
| 84 | - | dueWait, idle = min(dueWait, feedbackTimeout), false | |
| 85 | - | } | |
| 86 | - | wait = dueWait | |
| 87 | - | for _, name := range due { | |
| 88 | - | s.sendFrames(name, flipped[name]) | |
| 89 | - | } | |
| 90 | - | if s.releaseHeld(time.Now(), reports) && idle { | |
| 91 | - | // Held buffers wait for a flip or heldTimeout. | |
| 92 | - | wait, idle = heldTimeout, false | |
| 93 | - | } | |
| 94 | - | // Fifo barriers and queued commits wait for a refresh. | |
| 95 | - | if fw, waiting := s.tickFifo(time.Now(), flipped); waiting { | |
| 96 | - | wait, idle = min(wait, fw), false | |
| 97 | - | } | |
| 98 | - | } | |
| 99 | - | }) { | |
| 76 | + | if !s.display.Do(tick) { | |
| 100 | 77 | return | |
| 101 | 78 | } | |
| 102 | - | if idle { | |
| 103 | - | // Nothing waits: sleep until a commit queues a callback | |
| 104 | - | // (frameReady) or a flip. | |
| 105 | - | timer.Stop() | |
| 106 | - | continue | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // paceFromPacer runs the pacer's collected flips (flipped, frameReports) | |
| 83 | + | // on the display goroutine. | |
| 84 | + | func (s *Server) paceFromPacer() { s.paceTick(s.flipped, s.frameReports) } | |
| 85 | + | ||
| 86 | + | // paceNow runs a tick from code already on the display goroutine (a client | |
| 87 | + | // commit): the pacer goroutine would only call Do to get back here. Without | |
| 88 | + | // a running pacer (no timer) it falls back to waking it. | |
| 89 | + | func (s *Server) paceNow() { | |
| 90 | + | if s.paceTimer == nil { | |
| 91 | + | s.signalPacer() | |
| 92 | + | return | |
| 93 | + | } | |
| 94 | + | s.paceTick(nil, nil) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // paceTick answers the flips in reports, fires the due frame callbacks, | |
| 98 | + | // releases buffers and applies the ready FIFO commits, then arms the timer | |
| 99 | + | // for the next deadline (stopped when nothing waits). Display goroutine only. | |
| 100 | + | func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresented) { | |
| 101 | + | if s.ctx != nil && s.ctx.Err() != nil { | |
| 102 | + | return | |
| 103 | + | } | |
| 104 | + | wait, idle := defaultFramePeriod, false | |
| 105 | + | for pass := 0; ; pass++ { | |
| 106 | + | s.paceDirty = false | |
| 107 | + | s.paceDepth++ | |
| 108 | + | wait, idle = s.pacePass(flipped, reports) | |
| 109 | + | s.paceDepth-- | |
| 110 | + | if !s.paceDirty { | |
| 111 | + | break | |
| 107 | 112 | } | |
| 108 | - | timer.Reset(wait) | |
| 113 | + | // The pass queued work behind its own checks (applied commits | |
| 114 | + | // queue callbacks, released buffers): look again, as a flip-less | |
| 115 | + | // tick, instead of waking the pacer for it. | |
| 116 | + | flipped, reports = nil, nil | |
| 117 | + | if pass+1 == maxPacePasses { | |
| 118 | + | s.paceDirty = false | |
| 119 | + | s.signalPacer() | |
| 120 | + | break | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | if s.paceTimer == nil { | |
| 124 | + | return | |
| 125 | + | } | |
| 126 | + | if idle { | |
| 127 | + | // Nothing waits: sleep until a commit queues a callback | |
| 128 | + | // (wakePacer) or a flip. | |
| 129 | + | s.paceTimer.Stop() | |
| 130 | + | return | |
| 131 | + | } | |
| 132 | + | s.paceTimer.Reset(wait) | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // pacePass is one pass of paceTick; it returns the time to the next | |
| 136 | + | // deadline and whether nothing waits. | |
| 137 | + | func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresented) (wait time.Duration, idle bool) { | |
| 138 | + | // Each flip answers its own presentation feedbacks, in order: flips | |
| 139 | + | // are never merged here. | |
| 140 | + | for _, p := range reports { | |
| 141 | + | if p.Flip != nil { | |
| 142 | + | s.flips[p.Output] = *p.Flip | |
| 143 | + | s.presentFlip(p.Output, p.Flip) | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | s.dropFeedbacks(time.Now()) | |
| 147 | + | var due []string | |
| 148 | + | due, wait, idle = s.dueFrames(time.Now(), flipped) | |
| 149 | + | if len(s.feedbacks) > 0 { | |
| 150 | + | // Unanswered feedbacks time out: keep ticking. | |
| 151 | + | wait, idle = min(wait, feedbackTimeout), false | |
| 152 | + | } | |
| 153 | + | for _, name := range due { | |
| 154 | + | s.sendFrames(name, flipped[name]) | |
| 155 | + | } | |
| 156 | + | if s.releaseHeld(time.Now(), reports) && idle { | |
| 157 | + | // Held buffers wait for a flip or heldTimeout. | |
| 158 | + | wait, idle = heldTimeout, false | |
| 159 | + | } | |
| 160 | + | // Fifo barriers and queued commits wait for a refresh. | |
| 161 | + | if fw, waiting := s.tickFifo(time.Now(), flipped); waiting { | |
| 162 | + | wait, idle = min(wait, fw), false | |
| 109 | 163 | } | |
| 164 | + | return wait, idle | |
| 110 | 165 | } | |
| 111 | 166 | ||
| 112 | 167 | // toplevelRoot resolves subsurfaces and popups to their parent toplevel. | |
| @@ -24,14 +24,26 @@ const defaultFramePeriod = time.Second / 60 | |||||
| 24 | const suspendedFramePeriod = time.Second | 24 | const suspendedFramePeriod = time.Second | ||
| 25 | const suspendedFrameQueue = "\x00suspended" // never a wl_output name | 25 | const suspendedFrameQueue = "\x00suspended" // never a wl_output name | ||
| 26 | 26 | ||||
| 27 | - | // pace fires the due frame callbacks on page flips and deadlines. | 27 | + | // maxPacePasses bounds the passes one tick repeats while a pass queues |
| 28 | + | // more work; the rest goes back to the pacer goroutine. | |||
| 29 | + | const maxPacePasses = 3 | |||
| 30 | + | ||||
| 31 | + | // pace sleeps until a flip, a wake or the deadline armed by the last tick, | |||
| 32 | + | // then runs a tick on the display goroutine. The timer is only armed from | |||
| 33 | + | // ticks, which all run there, so a commit that ticks inline (paceNow) never | |||
| 34 | + | // races the pacer for it. | |||
| 28 | func (s *Server) pace(ctx context.Context) { | 35 | func (s *Server) pace(ctx context.Context) { | ||
| 29 | - | timer := time.NewTimer(defaultFramePeriod) | 36 | + | if s.paceTimer == nil { |
| 37 | + | s.paceTimer = time.NewTimer(defaultFramePeriod) | |||
| 38 | + | } | |||
| 39 | + | timer := s.paceTimer | |||
| 30 | defer timer.Stop() | 40 | defer timer.Stop() | ||
| 31 | presented := s.channels.Presented | 41 | presented := s.channels.Presented | ||
| 32 | if s.flipped == nil { | 42 | if s.flipped == nil { | ||
| 33 | s.flipped = make(map[string]bool) | 43 | s.flipped = make(map[string]bool) | ||
| 34 | } | 44 | } | ||
| 45 | + | // One method value for the whole loop: no closure per wake. | |||
| 46 | + | tick := s.paceFromPacer | |||
| 35 | for { | 47 | for { | ||
| 36 | clear(s.flipped) | 48 | clear(s.flipped) | ||
| 37 | flipped := s.flipped | 49 | flipped := s.flipped | ||
| @@ -61,52 +73,95 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 61 | } | 73 | } | ||
| 62 | } | 74 | } | ||
| 63 | s.frameReports = reports | 75 | s.frameReports = reports | ||
| 64 | - | wait, idle := defaultFramePeriod, false | 76 | + | if !s.display.Do(tick) { |
| 65 | - | if !s.display.Do(func() { | |||
| 66 | - | if ctx.Err() == nil { | |||
| 67 | - | // Each flip answers its own presentation feedbacks, in | |||
| 68 | - | // order: flips are never merged here. | |||
| 69 | - | for _, p := range reports { | |||
| 70 | - | if p.Flip != nil { | |||
| 71 | - | s.flips[p.Output] = *p.Flip | |||
| 72 | - | s.presentFlip(p.Output, p.Flip) | |||
| 73 | - | } | |||
| 74 | - | } | |||
| 75 | - | s.dropFeedbacks(time.Now()) | |||
| 76 | - | if len(s.feedbacks) > 0 { | |||
| 77 | - | wait = feedbackTimeout | |||
| 78 | - | } | |||
| 79 | - | var due []string | |||
| 80 | - | var dueWait time.Duration | |||
| 81 | - | due, dueWait, idle = s.dueFrames(time.Now(), flipped) | |||
| 82 | - | if len(s.feedbacks) > 0 { | |||
| 83 | - | // Unanswered feedbacks time out: keep ticking. | |||
| 84 | - | dueWait, idle = min(dueWait, feedbackTimeout), false | |||
| 85 | - | } | |||
| 86 | - | wait = dueWait | |||
| 87 | - | for _, name := range due { | |||
| 88 | - | s.sendFrames(name, flipped[name]) | |||
| 89 | - | } | |||
| 90 | - | if s.releaseHeld(time.Now(), reports) && idle { | |||
| 91 | - | // Held buffers wait for a flip or heldTimeout. | |||
| 92 | - | wait, idle = heldTimeout, false | |||
| 93 | - | } | |||
| 94 | - | // Fifo barriers and queued commits wait for a refresh. | |||
| 95 | - | if fw, waiting := s.tickFifo(time.Now(), flipped); waiting { | |||
| 96 | - | wait, idle = min(wait, fw), false | |||
| 97 | - | } | |||
| 98 | - | } | |||
| 99 | - | }) { | |||
| 100 | return | 77 | return | ||
| 101 | } | 78 | } | ||
| 102 | - | if idle { | 79 | + | } |
| 103 | - | // Nothing waits: sleep until a commit queues a callback | 80 | + | } |
| 104 | - | // (frameReady) or a flip. | 81 | + | |
| 105 | - | timer.Stop() | 82 | + | // paceFromPacer runs the pacer's collected flips (flipped, frameReports) |
| 106 | - | continue | 83 | + | // on the display goroutine. |
| 84 | + | func (s *Server) paceFromPacer() { s.paceTick(s.flipped, s.frameReports) } | |||
| 85 | + | ||||
| 86 | + | // paceNow runs a tick from code already on the display goroutine (a client | |||
| 87 | + | // commit): the pacer goroutine would only call Do to get back here. Without | |||
| 88 | + | // a running pacer (no timer) it falls back to waking it. | |||
| 89 | + | func (s *Server) paceNow() { | |||
| 90 | + | if s.paceTimer == nil { | |||
| 91 | + | s.signalPacer() | |||
| 92 | + | return | |||
| 93 | + | } | |||
| 94 | + | s.paceTick(nil, nil) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // paceTick answers the flips in reports, fires the due frame callbacks, | |||
| 98 | + | // releases buffers and applies the ready FIFO commits, then arms the timer | |||
| 99 | + | // for the next deadline (stopped when nothing waits). Display goroutine only. | |||
| 100 | + | func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresented) { | |||
| 101 | + | if s.ctx != nil && s.ctx.Err() != nil { | |||
| 102 | + | return | |||
| 103 | + | } | |||
| 104 | + | wait, idle := defaultFramePeriod, false | |||
| 105 | + | for pass := 0; ; pass++ { | |||
| 106 | + | s.paceDirty = false | |||
| 107 | + | s.paceDepth++ | |||
| 108 | + | wait, idle = s.pacePass(flipped, reports) | |||
| 109 | + | s.paceDepth-- | |||
| 110 | + | if !s.paceDirty { | |||
| 111 | + | break | |||
| 107 | } | 112 | } | ||
| 108 | - | timer.Reset(wait) | 113 | + | // The pass queued work behind its own checks (applied commits |
| 114 | + | // queue callbacks, released buffers): look again, as a flip-less | |||
| 115 | + | // tick, instead of waking the pacer for it. | |||
| 116 | + | flipped, reports = nil, nil | |||
| 117 | + | if pass+1 == maxPacePasses { | |||
| 118 | + | s.paceDirty = false | |||
| 119 | + | s.signalPacer() | |||
| 120 | + | break | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | if s.paceTimer == nil { | |||
| 124 | + | return | |||
| 125 | + | } | |||
| 126 | + | if idle { | |||
| 127 | + | // Nothing waits: sleep until a commit queues a callback | |||
| 128 | + | // (wakePacer) or a flip. | |||
| 129 | + | s.paceTimer.Stop() | |||
| 130 | + | return | |||
| 131 | + | } | |||
| 132 | + | s.paceTimer.Reset(wait) | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // pacePass is one pass of paceTick; it returns the time to the next | |||
| 136 | + | // deadline and whether nothing waits. | |||
| 137 | + | func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresented) (wait time.Duration, idle bool) { | |||
| 138 | + | // Each flip answers its own presentation feedbacks, in order: flips | |||
| 139 | + | // are never merged here. | |||
| 140 | + | for _, p := range reports { | |||
| 141 | + | if p.Flip != nil { | |||
| 142 | + | s.flips[p.Output] = *p.Flip | |||
| 143 | + | s.presentFlip(p.Output, p.Flip) | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | s.dropFeedbacks(time.Now()) | |||
| 147 | + | var due []string | |||
| 148 | + | due, wait, idle = s.dueFrames(time.Now(), flipped) | |||
| 149 | + | if len(s.feedbacks) > 0 { | |||
| 150 | + | // Unanswered feedbacks time out: keep ticking. | |||
| 151 | + | wait, idle = min(wait, feedbackTimeout), false | |||
| 152 | + | } | |||
| 153 | + | for _, name := range due { | |||
| 154 | + | s.sendFrames(name, flipped[name]) | |||
| 155 | + | } | |||
| 156 | + | if s.releaseHeld(time.Now(), reports) && idle { | |||
| 157 | + | // Held buffers wait for a flip or heldTimeout. | |||
| 158 | + | wait, idle = heldTimeout, false | |||
| 159 | + | } | |||
| 160 | + | // Fifo barriers and queued commits wait for a refresh. | |||
| 161 | + | if fw, waiting := s.tickFifo(time.Now(), flipped); waiting { | |||
| 162 | + | wait, idle = min(wait, fw), false | |||
| 109 | } | 163 | } | ||
| 164 | + | return wait, idle | |||
| 110 | } | 165 | } | ||
| 111 | 166 | ||||
| 112 | // toplevelRoot resolves subsurfaces and popups to their parent toplevel. | 167 | // toplevelRoot resolves subsurfaces and popups to their parent toplevel. | ||
A internal/adapters/wayland/pace_inline_test.go +93 -0
| @@ -0,0 +1,93 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // paceFixture is a server whose surface waits on an output whose next | |
| 12 | + | // frame is an hour away: a commit queues its callback and nothing fires. | |
| 13 | + | func paceFixture(timer bool) (*Server, *surface) { | |
| 14 | + | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | |
| 15 | + | s := &Server{ | |
| 16 | + | outputs: []*output{out}, | |
| 17 | + | awaiting: map[string][]*wayland.Callback{}, | |
| 18 | + | frameDue: map[string]time.Time{"DP-2": time.Now().Add(time.Hour)}, | |
| 19 | + | fifoSurfaces: map[*surface]struct{}{}, | |
| 20 | + | lastFlip: map[string]time.Time{}, | |
| 21 | + | flips: map[string]ports.FlipInfo{}, | |
| 22 | + | reports: map[string]ports.OutputPresented{}, | |
| 23 | + | frameReady: make(chan struct{}, 1), | |
| 24 | + | } | |
| 25 | + | if timer { | |
| 26 | + | s.paceTimer = time.NewTimer(time.Hour) | |
| 27 | + | s.paceTimer.Stop() | |
| 28 | + | } | |
| 29 | + | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | |
| 30 | + | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | |
| 31 | + | surf.next.callbacks = []*wayland.Callback{new(wayland.Callback)} | |
| 32 | + | return s, surf | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // A commit on the display goroutine ticks the pacer inline: it arms the | |
| 36 | + | // pacer's timer itself and does not wake the pacer goroutine, which would | |
| 37 | + | // only call display.Do to get back to the display goroutine. | |
| 38 | + | func TestCommitTicksPacerInline(t *testing.T) { | |
| 39 | + | s, surf := paceFixture(true) | |
| 40 | + | surf.queueAndTick() | |
| 41 | + | if len(s.awaiting["DP-2"]) != 1 { | |
| 42 | + | t.Fatalf("callback not queued: %+v", s.awaiting) | |
| 43 | + | } | |
| 44 | + | if len(s.frameReady) != 0 { | |
| 45 | + | t.Fatal("commit woke the pacer goroutine") | |
| 46 | + | } | |
| 47 | + | if !s.paceTimer.Stop() { | |
| 48 | + | t.Fatal("inline tick did not arm the pacer timer for the next deadline") | |
| 49 | + | } | |
| 50 | + | if s.paceDepth != 0 || s.paceDirty { | |
| 51 | + | t.Fatalf("pace state leaked: depth %d dirty %v", s.paceDepth, s.paceDirty) | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // Without a running pacer (no timer) the wake falls back to the channel. | |
| 56 | + | func TestCommitWakesPacerWithoutTimer(t *testing.T) { | |
| 57 | + | s, surf := paceFixture(false) | |
| 58 | + | surf.queueAndTick() | |
| 59 | + | if len(s.frameReady) != 1 { | |
| 60 | + | t.Fatal("pacer not woken") | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // Wakes inside a tick coalesce into one more pass, not one channel wake each. | |
| 65 | + | func TestWakePacerDefersInsideTick(t *testing.T) { | |
| 66 | + | s, _ := paceFixture(true) | |
| 67 | + | s.paceDepth = 1 | |
| 68 | + | s.wakePacer() | |
| 69 | + | s.wakePacer() | |
| 70 | + | if len(s.frameReady) != 0 || !s.paceDirty { | |
| 71 | + | t.Fatalf("wake inside a tick: queued %d dirty %v", len(s.frameReady), s.paceDirty) | |
| 72 | + | } | |
| 73 | + | s.paceDepth = 0 | |
| 74 | + | s.wakePacer() | |
| 75 | + | if len(s.frameReady) != 1 { | |
| 76 | + | t.Fatal("wake outside a tick lost") | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // A commit that waits (a fifo barrier) is timed by the inline tick. | |
| 81 | + | func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) { | |
| 82 | + | s, surf := paceFixture(true) | |
| 83 | + | surf.next.callbacks = nil | |
| 84 | + | surf.barrier, surf.barrierAt = true, time.Now() | |
| 85 | + | surf.next.wait = true | |
| 86 | + | surf.queueAndTick() | |
| 87 | + | if len(surf.queue) != 1 || len(s.frameReady) != 0 { | |
| 88 | + | t.Fatalf("queued %d, pacer wakes %d", len(surf.queue), len(s.frameReady)) | |
| 89 | + | } | |
| 90 | + | if !s.paceTimer.Stop() { | |
| 91 | + | t.Fatal("timer not armed for the barrier deadline") | |
| 92 | + | } | |
| 93 | + | } | |
| @@ -0,0 +1,93 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // paceFixture is a server whose surface waits on an output whose next | |||
| 12 | + | // frame is an hour away: a commit queues its callback and nothing fires. | |||
| 13 | + | func paceFixture(timer bool) (*Server, *surface) { | |||
| 14 | + | out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}} | |||
| 15 | + | s := &Server{ | |||
| 16 | + | outputs: []*output{out}, | |||
| 17 | + | awaiting: map[string][]*wayland.Callback{}, | |||
| 18 | + | frameDue: map[string]time.Time{"DP-2": time.Now().Add(time.Hour)}, | |||
| 19 | + | fifoSurfaces: map[*surface]struct{}{}, | |||
| 20 | + | lastFlip: map[string]time.Time{}, | |||
| 21 | + | flips: map[string]ports.FlipInfo{}, | |||
| 22 | + | reports: map[string]ports.OutputPresented{}, | |||
| 23 | + | frameReady: make(chan struct{}, 1), | |||
| 24 | + | } | |||
| 25 | + | if timer { | |||
| 26 | + | s.paceTimer = time.NewTimer(time.Hour) | |||
| 27 | + | s.paceTimer.Stop() | |||
| 28 | + | } | |||
| 29 | + | w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}} | |||
| 30 | + | surf := &surface{server: s, xdg: &xdgSurface{window: w}} | |||
| 31 | + | surf.next.callbacks = []*wayland.Callback{new(wayland.Callback)} | |||
| 32 | + | return s, surf | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // A commit on the display goroutine ticks the pacer inline: it arms the | |||
| 36 | + | // pacer's timer itself and does not wake the pacer goroutine, which would | |||
| 37 | + | // only call display.Do to get back to the display goroutine. | |||
| 38 | + | func TestCommitTicksPacerInline(t *testing.T) { | |||
| 39 | + | s, surf := paceFixture(true) | |||
| 40 | + | surf.queueAndTick() | |||
| 41 | + | if len(s.awaiting["DP-2"]) != 1 { | |||
| 42 | + | t.Fatalf("callback not queued: %+v", s.awaiting) | |||
| 43 | + | } | |||
| 44 | + | if len(s.frameReady) != 0 { | |||
| 45 | + | t.Fatal("commit woke the pacer goroutine") | |||
| 46 | + | } | |||
| 47 | + | if !s.paceTimer.Stop() { | |||
| 48 | + | t.Fatal("inline tick did not arm the pacer timer for the next deadline") | |||
| 49 | + | } | |||
| 50 | + | if s.paceDepth != 0 || s.paceDirty { | |||
| 51 | + | t.Fatalf("pace state leaked: depth %d dirty %v", s.paceDepth, s.paceDirty) | |||
| 52 | + | } | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // Without a running pacer (no timer) the wake falls back to the channel. | |||
| 56 | + | func TestCommitWakesPacerWithoutTimer(t *testing.T) { | |||
| 57 | + | s, surf := paceFixture(false) | |||
| 58 | + | surf.queueAndTick() | |||
| 59 | + | if len(s.frameReady) != 1 { | |||
| 60 | + | t.Fatal("pacer not woken") | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // Wakes inside a tick coalesce into one more pass, not one channel wake each. | |||
| 65 | + | func TestWakePacerDefersInsideTick(t *testing.T) { | |||
| 66 | + | s, _ := paceFixture(true) | |||
| 67 | + | s.paceDepth = 1 | |||
| 68 | + | s.wakePacer() | |||
| 69 | + | s.wakePacer() | |||
| 70 | + | if len(s.frameReady) != 0 || !s.paceDirty { | |||
| 71 | + | t.Fatalf("wake inside a tick: queued %d dirty %v", len(s.frameReady), s.paceDirty) | |||
| 72 | + | } | |||
| 73 | + | s.paceDepth = 0 | |||
| 74 | + | s.wakePacer() | |||
| 75 | + | if len(s.frameReady) != 1 { | |||
| 76 | + | t.Fatal("wake outside a tick lost") | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // A commit that waits (a fifo barrier) is timed by the inline tick. | |||
| 81 | + | func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) { | |||
| 82 | + | s, surf := paceFixture(true) | |||
| 83 | + | surf.next.callbacks = nil | |||
| 84 | + | surf.barrier, surf.barrierAt = true, time.Now() | |||
| 85 | + | surf.next.wait = true | |||
| 86 | + | surf.queueAndTick() | |||
| 87 | + | if len(surf.queue) != 1 || len(s.frameReady) != 0 { | |||
| 88 | + | t.Fatalf("queued %d, pacer wakes %d", len(surf.queue), len(s.frameReady)) | |||
| 89 | + | } | |||
| 90 | + | if !s.paceTimer.Stop() { | |||
| 91 | + | t.Fatal("timer not armed for the barrier deadline") | |||
| 92 | + | } | |||
| 93 | + | } | |||
M internal/adapters/wayland/presentation.go +1 -0
| @@ -108,6 +108,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||
| 108 | 108 | for _, fb := range pending { | |
| 109 | 109 | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | |
| 110 | 110 | } | |
| 111 | + | srv.wakePacer() // an unanswered feedback times out: the pacer must keep ticking | |
| 111 | 112 | } | |
| 112 | 113 | ||
| 113 | 114 | func discard(fb *presentationtime.WpPresentationFeedback) { | |
| @@ -108,6 +108,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||||
| 108 | for _, fb := range pending { | 108 | for _, fb := range pending { | ||
| 109 | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | 109 | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | ||
| 110 | } | 110 | } | ||
| 111 | + | srv.wakePacer() // an unanswered feedback times out: the pacer must keep ticking | |||
| 111 | } | 112 | } | ||
| 112 | 113 | ||||
| 113 | func discard(fb *presentationtime.WpPresentationFeedback) { | 114 | func discard(fb *presentationtime.WpPresentationFeedback) { | ||
M internal/adapters/wayland/release.go +1 -4
| @@ -52,10 +52,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) | |||
| 52 | 52 | s.contentMu.Unlock() | |
| 53 | 53 | s.held = append(s.held, h) | |
| 54 | 54 | // Wake the pacer: it releases the buffer on the next output report. | |
| 55 | - | select { | |
| 56 | - | case s.frameReady <- struct{}{}: | |
| 57 | - | default: | |
| 58 | - | } | |
| 55 | + | s.wakePacer() | |
| 59 | 56 | } | |
| 60 | 57 | ||
| 61 | 58 | // release gives a buffer back: its release point is signalled when it | |
| @@ -52,10 +52,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer, sync syncHold) | |||||
| 52 | s.contentMu.Unlock() | 52 | s.contentMu.Unlock() | ||
| 53 | s.held = append(s.held, h) | 53 | s.held = append(s.held, h) | ||
| 54 | // Wake the pacer: it releases the buffer on the next output report. | 54 | // Wake the pacer: it releases the buffer on the next output report. | ||
| 55 | - | select { | 55 | + | s.wakePacer() |
| 56 | - | case s.frameReady <- struct{}{}: | |||
| 57 | - | default: | |||
| 58 | - | } | |||
| 59 | } | 56 | } | ||
| 60 | 57 | ||||
| 61 | // release gives a buffer back: its release point is signalled when it | 58 | // release gives a buffer back: its release point is signalled when it | ||
M internal/adapters/wayland/server.go +10 -3
| @@ -151,6 +151,12 @@ type Server struct { | |||
| 151 | 151 | frameOwners map[*wayland.Callback]*surface | |
| 152 | 152 | frameDue map[string]time.Time | |
| 153 | 153 | frameReady chan struct{} | |
| 154 | + | // paceTimer is the pacer's deadline, armed by ticks on the display | |
| 155 | + | // goroutine; paceDepth/paceDirty defer wakePacer inside a commit or | |
| 156 | + | // tick (display goroutine only). | |
| 157 | + | paceTimer *time.Timer | |
| 158 | + | paceDepth int | |
| 159 | + | paceDirty bool | |
| 154 | 160 | // Pacer scratch: periods is used on the display goroutine, while | |
| 155 | 161 | // flipped and frameReports belong to pace and its synchronous Do call. | |
| 156 | 162 | framePeriods map[string]time.Duration | |
| @@ -369,7 +375,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 369 | 375 | var node *renderNode | |
| 370 | 376 | if opts.syncDev != nil { | |
| 371 | 377 | s.syncDev = opts.syncDev | |
| 372 | - | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |
| 378 | + | if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil { | |
| 373 | 379 | cleanup() | |
| 374 | 380 | d.Close() | |
| 375 | 381 | return nil, err | |
| @@ -378,7 +384,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 378 | 384 | if node, err = openSyncobj(opts.SyncobjNode); err != nil { | |
| 379 | 385 | log.Info().Err(err).Msg("explicit sync off") | |
| 380 | 386 | node = nil | |
| 381 | - | } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |
| 387 | + | } else if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil { | |
| 382 | 388 | node.close() | |
| 383 | 389 | node = nil | |
| 384 | 390 | } else { | |
| @@ -387,7 +393,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 387 | 393 | } | |
| 388 | 394 | // Implicit dma-buf fences also need this waiter when explicit sync is off. | |
| 389 | 395 | if s.syncWait == nil { | |
| 390 | - | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | |
| 396 | + | if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil { | |
| 391 | 397 | cleanup() | |
| 392 | 398 | d.Close() | |
| 393 | 399 | return nil, err | |
| @@ -453,6 +459,7 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 453 | 459 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | |
| 454 | 460 | s.started = time.Now() | |
| 455 | 461 | s.ctx = ctx | |
| 462 | + | s.paceTimer = time.NewTimer(defaultFramePeriod) | |
| 456 | 463 | var wg sync.WaitGroup | |
| 457 | 464 | wg.Add(15) | |
| 458 | 465 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | |
| @@ -151,6 +151,12 @@ type Server struct { | |||||
| 151 | frameOwners map[*wayland.Callback]*surface | 151 | frameOwners map[*wayland.Callback]*surface | ||
| 152 | frameDue map[string]time.Time | 152 | frameDue map[string]time.Time | ||
| 153 | frameReady chan struct{} | 153 | frameReady chan struct{} | ||
| 154 | + | // paceTimer is the pacer's deadline, armed by ticks on the display | |||
| 155 | + | // goroutine; paceDepth/paceDirty defer wakePacer inside a commit or | |||
| 156 | + | // tick (display goroutine only). | |||
| 157 | + | paceTimer *time.Timer | |||
| 158 | + | paceDepth int | |||
| 159 | + | paceDirty bool | |||
| 154 | // Pacer scratch: periods is used on the display goroutine, while | 160 | // Pacer scratch: periods is used on the display goroutine, while | ||
| 155 | // flipped and frameReports belong to pace and its synchronous Do call. | 161 | // flipped and frameReports belong to pace and its synchronous Do call. | ||
| 156 | framePeriods map[string]time.Duration | 162 | framePeriods map[string]time.Duration | ||
| @@ -369,7 +375,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 369 | var node *renderNode | 375 | var node *renderNode | ||
| 370 | if opts.syncDev != nil { | 376 | if opts.syncDev != nil { | ||
| 371 | s.syncDev = opts.syncDev | 377 | s.syncDev = opts.syncDev | ||
| 372 | - | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | 378 | + | if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil { |
| 373 | cleanup() | 379 | cleanup() | ||
| 374 | d.Close() | 380 | d.Close() | ||
| 375 | return nil, err | 381 | return nil, err | ||
| @@ -378,7 +384,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 378 | if node, err = openSyncobj(opts.SyncobjNode); err != nil { | 384 | if node, err = openSyncobj(opts.SyncobjNode); err != nil { | ||
| 379 | log.Info().Err(err).Msg("explicit sync off") | 385 | log.Info().Err(err).Msg("explicit sync off") | ||
| 380 | node = nil | 386 | node = nil | ||
| 381 | - | } else if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | 387 | + | } else if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil { |
| 382 | node.close() | 388 | node.close() | ||
| 383 | node = nil | 389 | node = nil | ||
| 384 | } else { | 390 | } else { | ||
| @@ -387,7 +393,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 387 | } | 393 | } | ||
| 388 | // Implicit dma-buf fences also need this waiter when explicit sync is off. | 394 | // Implicit dma-buf fences also need this waiter when explicit sync is off. | ||
| 389 | if s.syncWait == nil { | 395 | if s.syncWait == nil { | ||
| 390 | - | if s.syncWait, err = newSyncWaiter(s.wakePacer); err != nil { | 396 | + | if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil { |
| 391 | cleanup() | 397 | cleanup() | ||
| 392 | d.Close() | 398 | d.Close() | ||
| 393 | return nil, err | 399 | return nil, err | ||
| @@ -453,6 +459,7 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 453 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | 459 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | ||
| 454 | s.started = time.Now() | 460 | s.started = time.Now() | ||
| 455 | s.ctx = ctx | 461 | s.ctx = ctx | ||
| 462 | + | s.paceTimer = time.NewTimer(defaultFramePeriod) | |||
| 456 | var wg sync.WaitGroup | 463 | var wg sync.WaitGroup | ||
| 457 | wg.Add(15) | 464 | wg.Add(15) | ||
| 458 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | 465 | go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }() | ||
M internal/adapters/wayland/surface.go +22 -1
| @@ -371,8 +371,29 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 371 | 371 | s.takeSyncPoints() | |
| 372 | 372 | // Each request creates an ordered content update. A synchronized update | |
| 373 | 373 | // can only be reached through a parent's committed dependency graph. | |
| 374 | + | s.queueAndTick() | |
| 375 | + | } | |
| 376 | + | ||
| 377 | + | // queueAndTick queues the commit and applies what is ready. The pacer is | |
| 378 | + | // woken at most once, after the work, and by a tick on this goroutine | |
| 379 | + | // rather than by a wake that makes the pacer call Do back. | |
| 380 | + | func (s *surface) queueAndTick() { | |
| 381 | + | srv := s.server | |
| 382 | + | srv.paceDepth++ | |
| 374 | 383 | s.queueUpdate() | |
| 375 | - | s.server.tickFifo(time.Now(), nil) | |
| 384 | + | srv.tickFifo(time.Now(), nil) | |
| 385 | + | if len(s.queue) > 0 { | |
| 386 | + | // The commit waits for a barrier, a fence or a refresh: the pacer | |
| 387 | + | // must time it. A commit applied at once needs no tick of its own; | |
| 388 | + | // its callbacks, barrier and held buffers wake the pacer where | |
| 389 | + | // they are queued. | |
| 390 | + | srv.wakePacer() | |
| 391 | + | } | |
| 392 | + | srv.paceDepth-- | |
| 393 | + | if srv.paceDepth == 0 && srv.paceDirty { | |
| 394 | + | srv.paceDirty = false | |
| 395 | + | srv.paceNow() | |
| 396 | + | } | |
| 376 | 397 | } | |
| 377 | 398 | ||
| 378 | 399 | // applyCommit makes only the captured update current. Live pending requests | |
| @@ -371,8 +371,29 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 371 | s.takeSyncPoints() | 371 | s.takeSyncPoints() | ||
| 372 | // Each request creates an ordered content update. A synchronized update | 372 | // Each request creates an ordered content update. A synchronized update | ||
| 373 | // can only be reached through a parent's committed dependency graph. | 373 | // can only be reached through a parent's committed dependency graph. | ||
| 374 | + | s.queueAndTick() | |||
| 375 | + | } | |||
| 376 | + | ||||
| 377 | + | // queueAndTick queues the commit and applies what is ready. The pacer is | |||
| 378 | + | // woken at most once, after the work, and by a tick on this goroutine | |||
| 379 | + | // rather than by a wake that makes the pacer call Do back. | |||
| 380 | + | func (s *surface) queueAndTick() { | |||
| 381 | + | srv := s.server | |||
| 382 | + | srv.paceDepth++ | |||
| 374 | s.queueUpdate() | 383 | s.queueUpdate() | ||
| 375 | - | s.server.tickFifo(time.Now(), nil) | 384 | + | srv.tickFifo(time.Now(), nil) |
| 385 | + | if len(s.queue) > 0 { | |||
| 386 | + | // The commit waits for a barrier, a fence or a refresh: the pacer | |||
| 387 | + | // must time it. A commit applied at once needs no tick of its own; | |||
| 388 | + | // its callbacks, barrier and held buffers wake the pacer where | |||
| 389 | + | // they are queued. | |||
| 390 | + | srv.wakePacer() | |||
| 391 | + | } | |||
| 392 | + | srv.paceDepth-- | |||
| 393 | + | if srv.paceDepth == 0 && srv.paceDirty { | |||
| 394 | + | srv.paceDirty = false | |||
| 395 | + | srv.paceNow() | |||
| 396 | + | } | |||
| 376 | } | 397 | } | ||
| 377 | 398 | ||||
| 378 | // applyCommit makes only the captured update current. Live pending requests | 399 | // applyCommit makes only the captured update current. Live pending requests | ||