fix(presentation): present only windows a flip draws and keep software flips out of frame pacing
DRM flips report the windows the scene draws (or the scanned-out one), so feedback of hidden windows is discarded. Frame done uses a flip time only on a flip round. Headless flips answer feedback without flags and do not pace callbacks; a report it could not send counts as merged. Only a commit with a new buffer replaces a surface's waiting feedback, and feedback follows the surface's current output.
- commit
- 2bcca8a89d12ea22b1709d9f34163f4e82494532
- author
- brice · · 2026-09-26 12:18 UTC
- parent
- 76522c2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +68 -22
M internal/adapters/drm/output.go +15 -2
| @@ -818,11 +818,13 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 818 | 818 | continue | |
| 819 | 819 | } | |
| 820 | 820 | start := time.Now() | |
| 821 | - | f := pendingFrame{shows: maps.Clone(seen)} | |
| 821 | + | // shows is what the frame puts on screen: presentation feedback | |
| 822 | + | // of windows it does not draw is discarded, not presented. | |
| 823 | + | f := pendingFrame{shows: shownBy(scene, seen)} | |
| 822 | 824 | fb, c := o.scanoutFrame(scene, surfaces) | |
| 823 | 825 | direct := false | |
| 824 | 826 | if fb != 0 { | |
| 825 | - | direct, err = o.commitScanout(fb, c, f) | |
| 827 | + | direct, err = o.commitScanout(fb, c, pendingFrame{shows: map[ports.WindowID]uint64{c.ID: seen[c.ID]}}) | |
| 826 | 828 | } | |
| 827 | 829 | if !direct { | |
| 828 | 830 | ov, composed := o.overlayFrame(scene, surfaces) | |
| @@ -1087,3 +1089,14 @@ func (o *Output) freeImages() { | |||
| 1087 | 1089 | o.fbs[i] = 0 | |
| 1088 | 1090 | } | |
| 1089 | 1091 | } | |
| 1092 | + | ||
| 1093 | + | // shownBy is the content Seq of each window the scene draws. | |
| 1094 | + | func shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 { | |
| 1095 | + | out := map[ports.WindowID]uint64{} | |
| 1096 | + | for id, seq := range seen { | |
| 1097 | + | if s.Shows(id) { | |
| 1098 | + | out[id] = seq | |
| 1099 | + | } | |
| 1100 | + | } | |
| 1101 | + | return out | |
| 1102 | + | } | |
| @@ -818,11 +818,13 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 818 | continue | 818 | continue | ||
| 819 | } | 819 | } | ||
| 820 | start := time.Now() | 820 | start := time.Now() | ||
| 821 | - | f := pendingFrame{shows: maps.Clone(seen)} | 821 | + | // shows is what the frame puts on screen: presentation feedback |
| 822 | + | // of windows it does not draw is discarded, not presented. | |||
| 823 | + | f := pendingFrame{shows: shownBy(scene, seen)} | |||
| 822 | fb, c := o.scanoutFrame(scene, surfaces) | 824 | fb, c := o.scanoutFrame(scene, surfaces) | ||
| 823 | direct := false | 825 | direct := false | ||
| 824 | if fb != 0 { | 826 | if fb != 0 { | ||
| 825 | - | direct, err = o.commitScanout(fb, c, f) | 827 | + | direct, err = o.commitScanout(fb, c, pendingFrame{shows: map[ports.WindowID]uint64{c.ID: seen[c.ID]}}) |
| 826 | } | 828 | } | ||
| 827 | if !direct { | 829 | if !direct { | ||
| 828 | ov, composed := o.overlayFrame(scene, surfaces) | 830 | ov, composed := o.overlayFrame(scene, surfaces) | ||
| @@ -1087,3 +1089,14 @@ func (o *Output) freeImages() { | |||||
| 1087 | o.fbs[i] = 0 | 1089 | o.fbs[i] = 0 | ||
| 1088 | } | 1090 | } | ||
| 1089 | } | 1091 | } | ||
| 1092 | + | ||||
| 1093 | + | // shownBy is the content Seq of each window the scene draws. | |||
| 1094 | + | func shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[ports.WindowID]uint64 { | |||
| 1095 | + | out := map[ports.WindowID]uint64{} | |||
| 1096 | + | for id, seq := range seen { | |||
| 1097 | + | if s.Shows(id) { | |||
| 1098 | + | out[id] = seq | |||
| 1099 | + | } | |||
| 1100 | + | } | |||
| 1101 | + | return out | |||
| 1102 | + | } | |||
M internal/adapters/drm/scanout_test.go +10 -0
| @@ -143,3 +143,13 @@ func TestPlaneWithoutInFormatsIsLinear(t *testing.T) { | |||
| 143 | 143 | t.Fatal("tiled accepted without IN_FORMATS") | |
| 144 | 144 | } | |
| 145 | 145 | } | |
| 146 | + | ||
| 147 | + | // A flip shows only the windows its scene draws: presentation feedback of | |
| 148 | + | // a window on another workspace or hidden is discarded, not presented. | |
| 149 | + | func TestShownByScene(t *testing.T) { | |
| 150 | + | s := ports.Scene{Windows: []ports.SceneWindow{{ID: 1}, {ID: 2, Hidden: true}}, Layers: []ports.SceneLayer{{ID: 5}}} | |
| 151 | + | got := shownBy(s, map[ports.WindowID]uint64{1: 3, 2: 4, 5: 6, 9: 7}) | |
| 152 | + | if len(got) != 2 || got[1] != 3 || got[5] != 6 { | |
| 153 | + | t.Fatalf("shows %v", got) | |
| 154 | + | } | |
| 155 | + | } | |
| @@ -143,3 +143,13 @@ func TestPlaneWithoutInFormatsIsLinear(t *testing.T) { | |||||
| 143 | t.Fatal("tiled accepted without IN_FORMATS") | 143 | t.Fatal("tiled accepted without IN_FORMATS") | ||
| 144 | } | 144 | } | ||
| 145 | } | 145 | } | ||
| 146 | + | ||||
| 147 | + | // A flip shows only the windows its scene draws: presentation feedback of | |||
| 148 | + | // a window on another workspace or hidden is discarded, not presented. | |||
| 149 | + | func TestShownByScene(t *testing.T) { | |||
| 150 | + | s := ports.Scene{Windows: []ports.SceneWindow{{ID: 1}, {ID: 2, Hidden: true}}, Layers: []ports.SceneLayer{{ID: 5}}} | |||
| 151 | + | got := shownBy(s, map[ports.WindowID]uint64{1: 3, 2: 4, 5: 6, 9: 7}) | |||
| 152 | + | if len(got) != 2 || got[1] != 3 || got[5] != 6 { | |||
| 153 | + | t.Fatalf("shows %v", got) | |||
| 154 | + | } | |||
| 155 | + | } | |||
M internal/adapters/headless/output.go +7 -3
| @@ -187,9 +187,13 @@ func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.Windo | |||
| 187 | 187 | if opts.Presented == nil { | |
| 188 | 188 | return nil | |
| 189 | 189 | } | |
| 190 | + | var merged int | |
| 190 | 191 | if pending != nil && opts.send(pending) != nil { | |
| 191 | - | // The reader is behind: this frame's report replaces the waiting one. | |
| 192 | - | pending = nil | |
| 192 | + | // The reader is behind: this frame's report replaces the waiting | |
| 193 | + | // one, and its flip counts as merged (its feedback is discarded). | |
| 194 | + | if pending.Flip != nil { | |
| 195 | + | merged = 1 + pending.Flip.Merged | |
| 196 | + | } | |
| 193 | 197 | } | |
| 194 | 198 | var ts unix.Timespec | |
| 195 | 199 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| @@ -199,7 +203,7 @@ func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.Windo | |||
| 199 | 203 | shows[id] = c.Seq | |
| 200 | 204 | } | |
| 201 | 205 | } | |
| 202 | - | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows}}) | |
| 206 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows, Merged: merged}}) | |
| 203 | 207 | } | |
| 204 | 208 | ||
| 205 | 209 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |
| @@ -187,9 +187,13 @@ func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.Windo | |||||
| 187 | if opts.Presented == nil { | 187 | if opts.Presented == nil { | ||
| 188 | return nil | 188 | return nil | ||
| 189 | } | 189 | } | ||
| 190 | + | var merged int | |||
| 190 | if pending != nil && opts.send(pending) != nil { | 191 | if pending != nil && opts.send(pending) != nil { | ||
| 191 | - | // The reader is behind: this frame's report replaces the waiting one. | 192 | + | // The reader is behind: this frame's report replaces the waiting |
| 192 | - | pending = nil | 193 | + | // one, and its flip counts as merged (its feedback is discarded). |
| 194 | + | if pending.Flip != nil { | |||
| 195 | + | merged = 1 + pending.Flip.Merged | |||
| 196 | + | } | |||
| 193 | } | 197 | } | ||
| 194 | var ts unix.Timespec | 198 | var ts unix.Timespec | ||
| 195 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | 199 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | ||
| @@ -199,7 +203,7 @@ func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.Windo | |||||
| 199 | shows[id] = c.Seq | 203 | shows[id] = c.Seq | ||
| 200 | } | 204 | } | ||
| 201 | } | 205 | } | ||
| 202 | - | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows}}) | 206 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows, Merged: merged}}) |
| 203 | } | 207 | } | ||
| 204 | 208 | ||||
| 205 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | 209 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | ||
M internal/adapters/wayland/frames.go +11 -5
| @@ -35,7 +35,7 @@ func (s *Server) pace(ctx context.Context) { | |||
| 35 | 35 | case <-s.display.Stopped(): | |
| 36 | 36 | return | |
| 37 | 37 | case p := <-presented: | |
| 38 | - | flipped[p.Output] = flipped[p.Output] || p.Flip != nil | |
| 38 | + | flipped[p.Output] = flipped[p.Output] || paces(p) | |
| 39 | 39 | reports = append(reports, p) | |
| 40 | 40 | case <-s.frameReady: | |
| 41 | 41 | case <-timer.C: | |
| @@ -46,7 +46,7 @@ func (s *Server) pace(ctx context.Context) { | |||
| 46 | 46 | for { | |
| 47 | 47 | select { | |
| 48 | 48 | case p := <-presented: | |
| 49 | - | flipped[p.Output] = flipped[p.Output] || p.Flip != nil | |
| 49 | + | flipped[p.Output] = flipped[p.Output] || paces(p) | |
| 50 | 50 | reports = append(reports, p) | |
| 51 | 51 | default: | |
| 52 | 52 | break drain | |
| @@ -76,7 +76,7 @@ func (s *Server) pace(ctx context.Context) { | |||
| 76 | 76 | } | |
| 77 | 77 | wait = dueWait | |
| 78 | 78 | for _, name := range due { | |
| 79 | - | s.sendFrames(name) | |
| 79 | + | s.sendFrames(name, flipped[name]) | |
| 80 | 80 | } | |
| 81 | 81 | if s.releaseHeld(time.Now(), reports) && idle { | |
| 82 | 82 | // Held buffers wait for a flip or heldTimeout. | |
| @@ -186,7 +186,13 @@ func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []strin | |||
| 186 | 186 | return fire, max(wait, time.Millisecond), !waiting | |
| 187 | 187 | } | |
| 188 | 188 | ||
| 189 | - | func (s *Server) sendFrames(name string) { | |
| 189 | + | // paces reports whether a report paces frame callbacks: a flip of a real | |
| 190 | + | // display. Software flips (headless) answer presentation feedback, but | |
| 191 | + | // callbacks keep the output's refresh period. | |
| 192 | + | func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock } | |
| 193 | + | ||
| 194 | + | // sendFrames fires an output's callbacks; flipped: a flip of it fired them. | |
| 195 | + | func (s *Server) sendFrames(name string, flipped bool) { | |
| 190 | 196 | callbacks := s.awaiting[name] | |
| 191 | 197 | delete(s.awaiting, name) | |
| 192 | 198 | if len(callbacks) == 0 { | |
| @@ -195,7 +201,7 @@ func (s *Server) sendFrames(name string) { | |||
| 195 | 201 | // done carries the flip's time when the output flips (the frame the | |
| 196 | 202 | // client drew for is on screen), else now. | |
| 197 | 203 | ms := uint32(time.Since(s.started).Milliseconds()) | |
| 198 | - | if f, ok := s.flips[name]; ok && f.When > 0 { | |
| 204 | + | if f, ok := s.flips[name]; ok && flipped && f.When > 0 { | |
| 199 | 205 | ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds()) | |
| 200 | 206 | } | |
| 201 | 207 | for _, cb := range callbacks { | |
| @@ -35,7 +35,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 35 | case <-s.display.Stopped(): | 35 | case <-s.display.Stopped(): | ||
| 36 | return | 36 | return | ||
| 37 | case p := <-presented: | 37 | case p := <-presented: | ||
| 38 | - | flipped[p.Output] = flipped[p.Output] || p.Flip != nil | 38 | + | flipped[p.Output] = flipped[p.Output] || paces(p) |
| 39 | reports = append(reports, p) | 39 | reports = append(reports, p) | ||
| 40 | case <-s.frameReady: | 40 | case <-s.frameReady: | ||
| 41 | case <-timer.C: | 41 | case <-timer.C: | ||
| @@ -46,7 +46,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 46 | for { | 46 | for { | ||
| 47 | select { | 47 | select { | ||
| 48 | case p := <-presented: | 48 | case p := <-presented: | ||
| 49 | - | flipped[p.Output] = flipped[p.Output] || p.Flip != nil | 49 | + | flipped[p.Output] = flipped[p.Output] || paces(p) |
| 50 | reports = append(reports, p) | 50 | reports = append(reports, p) | ||
| 51 | default: | 51 | default: | ||
| 52 | break drain | 52 | break drain | ||
| @@ -76,7 +76,7 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 76 | } | 76 | } | ||
| 77 | wait = dueWait | 77 | wait = dueWait | ||
| 78 | for _, name := range due { | 78 | for _, name := range due { | ||
| 79 | - | s.sendFrames(name) | 79 | + | s.sendFrames(name, flipped[name]) |
| 80 | } | 80 | } | ||
| 81 | if s.releaseHeld(time.Now(), reports) && idle { | 81 | if s.releaseHeld(time.Now(), reports) && idle { | ||
| 82 | // Held buffers wait for a flip or heldTimeout. | 82 | // Held buffers wait for a flip or heldTimeout. | ||
| @@ -186,7 +186,13 @@ func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []strin | |||||
| 186 | return fire, max(wait, time.Millisecond), !waiting | 186 | return fire, max(wait, time.Millisecond), !waiting | ||
| 187 | } | 187 | } | ||
| 188 | 188 | ||||
| 189 | - | func (s *Server) sendFrames(name string) { | 189 | + | // paces reports whether a report paces frame callbacks: a flip of a real |
| 190 | + | // display. Software flips (headless) answer presentation feedback, but | |||
| 191 | + | // callbacks keep the output's refresh period. | |||
| 192 | + | func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock } | |||
| 193 | + | ||||
| 194 | + | // sendFrames fires an output's callbacks; flipped: a flip of it fired them. | |||
| 195 | + | func (s *Server) sendFrames(name string, flipped bool) { | |||
| 190 | callbacks := s.awaiting[name] | 196 | callbacks := s.awaiting[name] | ||
| 191 | delete(s.awaiting, name) | 197 | delete(s.awaiting, name) | ||
| 192 | if len(callbacks) == 0 { | 198 | if len(callbacks) == 0 { | ||
| @@ -195,7 +201,7 @@ func (s *Server) sendFrames(name string) { | |||||
| 195 | // done carries the flip's time when the output flips (the frame the | 201 | // done carries the flip's time when the output flips (the frame the | ||
| 196 | // client drew for is on screen), else now. | 202 | // client drew for is on screen), else now. | ||
| 197 | ms := uint32(time.Since(s.started).Milliseconds()) | 203 | ms := uint32(time.Since(s.started).Milliseconds()) | ||
| 198 | - | if f, ok := s.flips[name]; ok && f.When > 0 { | 204 | + | if f, ok := s.flips[name]; ok && flipped && f.When > 0 { |
| 199 | ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds()) | 205 | ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds()) | ||
| 200 | } | 206 | } | ||
| 201 | for _, cb := range callbacks { | 207 | for _, cb := range callbacks { | ||
M internal/adapters/wayland/presentation.go +11 -10
| @@ -19,14 +19,13 @@ import ( | |||
| 19 | 19 | ||
| 20 | 20 | const presentationVersion = 2 | |
| 21 | 21 | ||
| 22 | - | // feedbackWait is a committed feedback: presented once a flip on output | |
| 23 | - | // shows content seq of window win. | |
| 22 | + | // feedbackWait is a committed feedback: presented once a flip on the | |
| 23 | + | // surface's output shows content seq of window win. | |
| 24 | 24 | type feedbackWait struct { | |
| 25 | 25 | fb *presentationtime.WpPresentationFeedback | |
| 26 | 26 | surf *surface | |
| 27 | 27 | win ports.WindowID | |
| 28 | 28 | seq uint64 | |
| 29 | - | output string | |
| 30 | 29 | at time.Time | |
| 31 | 30 | // replaced is the window content Seq of the surface's next commit (0: | |
| 32 | 31 | // none yet): a flip showing it or later shows the replacement. | |
| @@ -69,12 +68,13 @@ func (feedbackHandler) Destroy(*presentationtime.WpPresentationFeedback) {} | |||
| 69 | 68 | ||
| 70 | 69 | // commitFeedback moves a surface's pending feedbacks to its window's | |
| 71 | 70 | // content just emitted, or discards them when the surface shows nowhere. | |
| 72 | - | // The commit replaces the surface's earlier waiting feedbacks. | |
| 73 | - | func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedback) { | |
| 71 | + | // A commit with a new buffer (fresh) replaces the surface's earlier | |
| 72 | + | // waiting feedbacks. | |
| 73 | + | func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedback, fresh bool) { | |
| 74 | 74 | srv := s.server | |
| 75 | 75 | root := s.root() | |
| 76 | 76 | win := root.windowID() | |
| 77 | - | if win != 0 { | |
| 77 | + | if win != 0 && fresh { | |
| 78 | 78 | srv.contentMu.Lock() | |
| 79 | 79 | seq := srv.contentSeq[win] | |
| 80 | 80 | srv.contentMu.Unlock() | |
| @@ -98,7 +98,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||
| 98 | 98 | seq := srv.contentSeq[win] | |
| 99 | 99 | srv.contentMu.Unlock() | |
| 100 | 100 | for _, fb := range pending { | |
| 101 | - | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, output: output, at: time.Now()}) | |
| 101 | + | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) | |
| 102 | 102 | } | |
| 103 | 103 | } | |
| 104 | 104 | ||
| @@ -124,7 +124,7 @@ func (s *Server) presentFlip(output string, f *ports.FlipInfo) { | |||
| 124 | 124 | case !w.fb.Resource.Alive(): | |
| 125 | 125 | case w.surf.destroyed: | |
| 126 | 126 | discard(w.fb) | |
| 127 | - | case w.output != output || !ok || shown < w.seq: | |
| 127 | + | case s.frameOutput(w.surf) != output || !ok || shown < w.seq: | |
| 128 | 128 | kept = append(kept, w) | |
| 129 | 129 | case f.Merged > 0: | |
| 130 | 130 | // The reader fell behind: which flip showed it is unknown. | |
| @@ -154,7 +154,8 @@ func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exa | |||
| 154 | 154 | } | |
| 155 | 155 | } | |
| 156 | 156 | flags := uint32(0) | |
| 157 | - | if !f.Async { | |
| 157 | + | // A software clock (headless) claims nothing about the display. | |
| 158 | + | if !f.Async && f.HardwareClock { | |
| 158 | 159 | flags |= uint32(presentationtime.WpPresentationFeedbackKindVsync) | |
| 159 | 160 | } | |
| 160 | 161 | if f.HardwareClock { | |
| @@ -181,7 +182,7 @@ func (s *Server) dropFeedbacks(now time.Time) { | |||
| 181 | 182 | if !w.fb.Resource.Alive() { | |
| 182 | 183 | continue | |
| 183 | 184 | } | |
| 184 | - | if w.surf.destroyed || s.outputByNameExact(w.output) == nil || now.Sub(w.at) > feedbackTimeout { | |
| 185 | + | if w.surf.destroyed || s.frameOutput(w.surf) == "" || now.Sub(w.at) > feedbackTimeout { | |
| 185 | 186 | discard(w.fb) | |
| 186 | 187 | continue | |
| 187 | 188 | } | |
| @@ -19,14 +19,13 @@ import ( | |||||
| 19 | 19 | ||||
| 20 | const presentationVersion = 2 | 20 | const presentationVersion = 2 | ||
| 21 | 21 | ||||
| 22 | - | // feedbackWait is a committed feedback: presented once a flip on output | 22 | + | // feedbackWait is a committed feedback: presented once a flip on the |
| 23 | - | // shows content seq of window win. | 23 | + | // surface's output shows content seq of window win. |
| 24 | type feedbackWait struct { | 24 | type feedbackWait struct { | ||
| 25 | fb *presentationtime.WpPresentationFeedback | 25 | fb *presentationtime.WpPresentationFeedback | ||
| 26 | surf *surface | 26 | surf *surface | ||
| 27 | win ports.WindowID | 27 | win ports.WindowID | ||
| 28 | seq uint64 | 28 | seq uint64 | ||
| 29 | - | output string | |||
| 30 | at time.Time | 29 | at time.Time | ||
| 31 | // replaced is the window content Seq of the surface's next commit (0: | 30 | // replaced is the window content Seq of the surface's next commit (0: | ||
| 32 | // none yet): a flip showing it or later shows the replacement. | 31 | // none yet): a flip showing it or later shows the replacement. | ||
| @@ -69,12 +68,13 @@ func (feedbackHandler) Destroy(*presentationtime.WpPresentationFeedback) {} | |||||
| 69 | 68 | ||||
| 70 | // commitFeedback moves a surface's pending feedbacks to its window's | 69 | // commitFeedback moves a surface's pending feedbacks to its window's | ||
| 71 | // content just emitted, or discards them when the surface shows nowhere. | 70 | // content just emitted, or discards them when the surface shows nowhere. | ||
| 72 | - | // The commit replaces the surface's earlier waiting feedbacks. | 71 | + | // A commit with a new buffer (fresh) replaces the surface's earlier |
| 73 | - | func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedback) { | 72 | + | // waiting feedbacks. |
| 73 | + | func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedback, fresh bool) { | |||
| 74 | srv := s.server | 74 | srv := s.server | ||
| 75 | root := s.root() | 75 | root := s.root() | ||
| 76 | win := root.windowID() | 76 | win := root.windowID() | ||
| 77 | - | if win != 0 { | 77 | + | if win != 0 && fresh { |
| 78 | srv.contentMu.Lock() | 78 | srv.contentMu.Lock() | ||
| 79 | seq := srv.contentSeq[win] | 79 | seq := srv.contentSeq[win] | ||
| 80 | srv.contentMu.Unlock() | 80 | srv.contentMu.Unlock() | ||
| @@ -98,7 +98,7 @@ func (s *surface) commitFeedback(pending []*presentationtime.WpPresentationFeedb | |||||
| 98 | seq := srv.contentSeq[win] | 98 | seq := srv.contentSeq[win] | ||
| 99 | srv.contentMu.Unlock() | 99 | srv.contentMu.Unlock() | ||
| 100 | for _, fb := range pending { | 100 | for _, fb := range pending { | ||
| 101 | - | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, output: output, at: time.Now()}) | 101 | + | srv.feedbacks = append(srv.feedbacks, feedbackWait{fb: fb, surf: s, win: win, seq: seq, at: time.Now()}) |
| 102 | } | 102 | } | ||
| 103 | } | 103 | } | ||
| 104 | 104 | ||||
| @@ -124,7 +124,7 @@ func (s *Server) presentFlip(output string, f *ports.FlipInfo) { | |||||
| 124 | case !w.fb.Resource.Alive(): | 124 | case !w.fb.Resource.Alive(): | ||
| 125 | case w.surf.destroyed: | 125 | case w.surf.destroyed: | ||
| 126 | discard(w.fb) | 126 | discard(w.fb) | ||
| 127 | - | case w.output != output || !ok || shown < w.seq: | 127 | + | case s.frameOutput(w.surf) != output || !ok || shown < w.seq: |
| 128 | kept = append(kept, w) | 128 | kept = append(kept, w) | ||
| 129 | case f.Merged > 0: | 129 | case f.Merged > 0: | ||
| 130 | // The reader fell behind: which flip showed it is unknown. | 130 | // The reader fell behind: which flip showed it is unknown. | ||
| @@ -154,7 +154,8 @@ func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exa | |||||
| 154 | } | 154 | } | ||
| 155 | } | 155 | } | ||
| 156 | flags := uint32(0) | 156 | flags := uint32(0) | ||
| 157 | - | if !f.Async { | 157 | + | // A software clock (headless) claims nothing about the display. |
| 158 | + | if !f.Async && f.HardwareClock { | |||
| 158 | flags |= uint32(presentationtime.WpPresentationFeedbackKindVsync) | 159 | flags |= uint32(presentationtime.WpPresentationFeedbackKindVsync) | ||
| 159 | } | 160 | } | ||
| 160 | if f.HardwareClock { | 161 | if f.HardwareClock { | ||
| @@ -181,7 +182,7 @@ func (s *Server) dropFeedbacks(now time.Time) { | |||||
| 181 | if !w.fb.Resource.Alive() { | 182 | if !w.fb.Resource.Alive() { | ||
| 182 | continue | 183 | continue | ||
| 183 | } | 184 | } | ||
| 184 | - | if w.surf.destroyed || s.outputByNameExact(w.output) == nil || now.Sub(w.at) > feedbackTimeout { | 185 | + | if w.surf.destroyed || s.frameOutput(w.surf) == "" || now.Sub(w.at) > feedbackTimeout { |
| 185 | discard(w.fb) | 186 | discard(w.fb) | ||
| 186 | continue | 187 | continue | ||
| 187 | } | 188 | } | ||
M internal/adapters/wayland/presentation_test.go +11 -0
| @@ -271,3 +271,14 @@ func TestNextRefreshFollowsFlips(t *testing.T) { | |||
| 271 | 271 | t.Fatalf("next refresh in %v, want %v", d, period*3/4) | |
| 272 | 272 | } | |
| 273 | 273 | } | |
| 274 | + | ||
| 275 | + | // Software flips (headless) answer feedback without flags and do not pace | |
| 276 | + | // frame callbacks. | |
| 277 | + | func TestSoftwareFlipsDoNotPace(t *testing.T) { | |
| 278 | + | if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) { | |
| 279 | + | t.Fatal("software flip paces callbacks") | |
| 280 | + | } | |
| 281 | + | if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) { | |
| 282 | + | t.Fatal("display flip does not pace") | |
| 283 | + | } | |
| 284 | + | } | |
| @@ -271,3 +271,14 @@ func TestNextRefreshFollowsFlips(t *testing.T) { | |||||
| 271 | t.Fatalf("next refresh in %v, want %v", d, period*3/4) | 271 | t.Fatalf("next refresh in %v, want %v", d, period*3/4) | ||
| 272 | } | 272 | } | ||
| 273 | } | 273 | } | ||
| 274 | + | ||||
| 275 | + | // Software flips (headless) answer feedback without flags and do not pace | |||
| 276 | + | // frame callbacks. | |||
| 277 | + | func TestSoftwareFlipsDoNotPace(t *testing.T) { | |||
| 278 | + | if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) { | |||
| 279 | + | t.Fatal("software flip paces callbacks") | |||
| 280 | + | } | |||
| 281 | + | if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) { | |||
| 282 | + | t.Fatal("display flip does not pace") | |||
| 283 | + | } | |||
| 284 | + | } | |||
M internal/adapters/wayland/surface.go +3 -2
| @@ -340,7 +340,8 @@ func (s *surface) applyCommit() { | |||
| 340 | 340 | if s.xdg != nil && s.xdg.window != nil { | |
| 341 | 341 | s.xdg.window.afterCommit() | |
| 342 | 342 | } | |
| 343 | - | if fresh || moved || geometry || hinted || s.sub.parent != nil { | |
| 343 | + | drawn := fresh || moved || geometry || hinted || s.sub.parent != nil | |
| 344 | + | if drawn { | |
| 344 | 345 | if moved || geometry { | |
| 345 | 346 | s.committed = damage{full: true} | |
| 346 | 347 | } | |
| @@ -348,7 +349,7 @@ func (s *surface) applyCommit() { | |||
| 348 | 349 | } | |
| 349 | 350 | fb := s.pendingFeedback | |
| 350 | 351 | s.pendingFeedback = nil | |
| 351 | - | s.commitFeedback(fb) | |
| 352 | + | s.commitFeedback(fb, fresh) | |
| 352 | 353 | } | |
| 353 | 354 | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | |
| 354 | 355 | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | |
| @@ -340,7 +340,8 @@ func (s *surface) applyCommit() { | |||||
| 340 | if s.xdg != nil && s.xdg.window != nil { | 340 | if s.xdg != nil && s.xdg.window != nil { | ||
| 341 | s.xdg.window.afterCommit() | 341 | s.xdg.window.afterCommit() | ||
| 342 | } | 342 | } | ||
| 343 | - | if fresh || moved || geometry || hinted || s.sub.parent != nil { | 343 | + | drawn := fresh || moved || geometry || hinted || s.sub.parent != nil |
| 344 | + | if drawn { | |||
| 344 | if moved || geometry { | 345 | if moved || geometry { | ||
| 345 | s.committed = damage{full: true} | 346 | s.committed = damage{full: true} | ||
| 346 | } | 347 | } | ||
| @@ -348,7 +349,7 @@ func (s *surface) applyCommit() { | |||||
| 348 | } | 349 | } | ||
| 349 | fb := s.pendingFeedback | 350 | fb := s.pendingFeedback | ||
| 350 | s.pendingFeedback = nil | 351 | s.pendingFeedback = nil | ||
| 351 | - | s.commitFeedback(fb) | 352 | + | s.commitFeedback(fb, fresh) |
| 352 | } | 353 | } | ||
| 353 | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | 354 | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | ||
| 354 | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | 355 | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | ||