fix(wayland): hold client buffers until every output moved past them
- commit
- 3ed810f75981e851cf910faea390458f23751ada
- author
- brice · · 2026-09-25 17:00 UTC
- parent
- 34cdd04
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +96 -45
M internal/adapters/drm/output.go +24 -3
| @@ -37,6 +37,7 @@ type Output struct { | |||
| 37 | 37 | clientFBs map[uint64]*clientFB | |
| 38 | 38 | shown, queued uint64 | |
| 39 | 39 | reason string // why the last frame was composed ("" = scanout) | |
| 40 | + | unsent ports.OutputPresented | |
| 40 | 41 | } | |
| 41 | 42 | ||
| 42 | 43 | // CursorLoader returns the image of a cursor at an output scale, at most | |
| @@ -204,8 +205,17 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 204 | 205 | if flipped || reportDirty { | |
| 205 | 206 | o.report(presented, flipped, seen) | |
| 206 | 207 | flipped, reportDirty = false, false | |
| 208 | + | } else { | |
| 209 | + | o.flushReport(presented) | |
| 210 | + | } | |
| 211 | + | // An unsent report is retried soon, not only on the next event. | |
| 212 | + | var retry <-chan time.Time | |
| 213 | + | if o.unsent.Output != "" { | |
| 214 | + | retry = time.After(time.Millisecond) | |
| 207 | 215 | } | |
| 208 | 216 | select { | |
| 217 | + | case <-retry: | |
| 218 | + | continue | |
| 209 | 219 | case <-ctx.Done(): | |
| 210 | 220 | return nil | |
| 211 | 221 | case on := <-active: | |
| @@ -320,11 +330,22 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||
| 320 | 330 | // report tells wayland what the output shows and has read, so replaced | |
| 321 | 331 | // client buffers can be released. It must follow the frame decision: a | |
| 322 | 332 | // buffer is safe once a later content is seen and it is neither shown nor | |
| 323 | - | // queued. A full channel drops the report; the next one carries the state. | |
| 333 | + | // queued. A report the channel cannot take waits in unsent, replaced by | |
| 334 | + | // newer ones (Flip kept), and is retried on the next loop. | |
| 324 | 335 | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | |
| 325 | - | r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |
| 336 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | |
| 337 | + | o.unsent = r | |
| 338 | + | o.flushReport(presented) | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | // flushReport sends the unsent report, if any, without blocking. | |
| 342 | + | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |
| 343 | + | if o.unsent.Output == "" { | |
| 344 | + | return | |
| 345 | + | } | |
| 326 | 346 | select { | |
| 327 | - | case presented <- r: | |
| 347 | + | case presented <- o.unsent: | |
| 348 | + | o.unsent = ports.OutputPresented{} | |
| 328 | 349 | default: | |
| 329 | 350 | } | |
| 330 | 351 | } | |
| @@ -37,6 +37,7 @@ type Output struct { | |||||
| 37 | clientFBs map[uint64]*clientFB | 37 | clientFBs map[uint64]*clientFB | ||
| 38 | shown, queued uint64 | 38 | shown, queued uint64 | ||
| 39 | reason string // why the last frame was composed ("" = scanout) | 39 | reason string // why the last frame was composed ("" = scanout) | ||
| 40 | + | unsent ports.OutputPresented | |||
| 40 | } | 41 | } | ||
| 41 | 42 | ||||
| 42 | // CursorLoader returns the image of a cursor at an output scale, at most | 43 | // CursorLoader returns the image of a cursor at an output scale, at most | ||
| @@ -204,8 +205,17 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 204 | if flipped || reportDirty { | 205 | if flipped || reportDirty { | ||
| 205 | o.report(presented, flipped, seen) | 206 | o.report(presented, flipped, seen) | ||
| 206 | flipped, reportDirty = false, false | 207 | flipped, reportDirty = false, false | ||
| 208 | + | } else { | |||
| 209 | + | o.flushReport(presented) | |||
| 210 | + | } | |||
| 211 | + | // An unsent report is retried soon, not only on the next event. | |||
| 212 | + | var retry <-chan time.Time | |||
| 213 | + | if o.unsent.Output != "" { | |||
| 214 | + | retry = time.After(time.Millisecond) | |||
| 207 | } | 215 | } | ||
| 208 | select { | 216 | select { | ||
| 217 | + | case <-retry: | |||
| 218 | + | continue | |||
| 209 | case <-ctx.Done(): | 219 | case <-ctx.Done(): | ||
| 210 | return nil | 220 | return nil | ||
| 211 | case on := <-active: | 221 | case on := <-active: | ||
| @@ -320,11 +330,22 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6 | |||||
| 320 | // report tells wayland what the output shows and has read, so replaced | 330 | // report tells wayland what the output shows and has read, so replaced | ||
| 321 | // client buffers can be released. It must follow the frame decision: a | 331 | // client buffers can be released. It must follow the frame decision: a | ||
| 322 | // buffer is safe once a later content is seen and it is neither shown nor | 332 | // buffer is safe once a later content is seen and it is neither shown nor | ||
| 323 | - | // queued. A full channel drops the report; the next one carries the state. | 333 | + | // queued. A report the channel cannot take waits in unsent, replaced by |
| 334 | + | // newer ones (Flip kept), and is retried on the next loop. | |||
| 324 | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | 335 | func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) { | ||
| 325 | - | r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} | 336 | + | r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)} |
| 337 | + | o.unsent = r | |||
| 338 | + | o.flushReport(presented) | |||
| 339 | + | } | |||
| 340 | + | ||||
| 341 | + | // flushReport sends the unsent report, if any, without blocking. | |||
| 342 | + | func (o *Output) flushReport(presented chan<- ports.OutputPresented) { | |||
| 343 | + | if o.unsent.Output == "" { | |||
| 344 | + | return | |||
| 345 | + | } | |||
| 326 | select { | 346 | select { | ||
| 327 | - | case presented <- r: | 347 | + | case presented <- o.unsent: |
| 348 | + | o.unsent = ports.OutputPresented{} | |||
| 328 | default: | 349 | default: | ||
| 329 | } | 350 | } | ||
| 330 | } | 351 | } | ||
M internal/adapters/headless/output.go +23 -7
| @@ -42,6 +42,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 42 | 42 | var want ports.CursorChange | |
| 43 | 43 | cursorScale := -1.0 // not loaded yet | |
| 44 | 44 | seen := map[ports.WindowID]uint64{} | |
| 45 | + | // pending is a report the channel could not take, retried soon. | |
| 46 | + | var pending *ports.OutputPresented | |
| 45 | 47 | update := func(c ports.SurfaceContent) { | |
| 46 | 48 | seen[c.ID] = max(seen[c.ID], c.Seq) | |
| 47 | 49 | dirty = dirty || scene.Shows(c.ID) | |
| @@ -55,9 +57,16 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 55 | 57 | if ctx.Err() != nil { | |
| 56 | 58 | return nil | |
| 57 | 59 | } | |
| 60 | + | var retry <-chan time.Time | |
| 61 | + | if pending != nil { | |
| 62 | + | retry = time.After(time.Millisecond) | |
| 63 | + | } | |
| 58 | 64 | select { | |
| 59 | 65 | case <-ctx.Done(): | |
| 60 | 66 | return nil | |
| 67 | + | case <-retry: | |
| 68 | + | pending = opts.send(pending) | |
| 69 | + | continue | |
| 61 | 70 | case s, ok := <-scenes: | |
| 62 | 71 | if !ok { | |
| 63 | 72 | scenes = nil | |
| @@ -96,7 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 96 | 105 | } | |
| 97 | 106 | if !haveScene || !dirty { | |
| 98 | 107 | // Contents not drawn are still read: report them. | |
| 99 | - | opts.report(false, seen) | |
| 108 | + | pending = opts.report(pending, seen) | |
| 100 | 109 | continue | |
| 101 | 110 | } | |
| 102 | 111 | dirty = false | |
| @@ -113,7 +122,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 113 | 122 | return fmt.Errorf("render frame: %w", err) | |
| 114 | 123 | } | |
| 115 | 124 | frame++ | |
| 116 | - | opts.report(false, seen) | |
| 125 | + | pending = opts.report(pending, seen) | |
| 117 | 126 | if opts.ScreenshotDir != "" { | |
| 118 | 127 | shot := r.Pixels() | |
| 119 | 128 | if opts.Cursor != nil { | |
| @@ -147,14 +156,21 @@ func writePNG(path string, img *image.RGBA) error { | |||
| 147 | 156 | } | |
| 148 | 157 | ||
| 149 | 158 | // report sends what the output read; a full channel drops it. | |
| 150 | - | // Headless frames are not paced by a screen: Flip stays false and frame | |
| 151 | - | // callbacks follow the refresh timer. | |
| 152 | - | func (opts Options) report(flip bool, seen map[ports.WindowID]uint64) { | |
| 159 | + | // report sends what the output read, or returns it to retry when the | |
| 160 | + | // channel is full. Headless frames are not paced by a screen: Flip stays | |
| 161 | + | // false and frame callbacks follow the refresh timer. | |
| 162 | + | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | |
| 153 | 163 | if opts.Presented == nil { | |
| 154 | - | return | |
| 164 | + | return nil | |
| 155 | 165 | } | |
| 166 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |
| 156 | 170 | select { | |
| 157 | - | case opts.Presented <- ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: maps.Clone(seen)}: | |
| 171 | + | case opts.Presented <- *r: | |
| 172 | + | return nil | |
| 158 | 173 | default: | |
| 174 | + | return r | |
| 159 | 175 | } | |
| 160 | 176 | } | |
| @@ -42,6 +42,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 42 | var want ports.CursorChange | 42 | var want ports.CursorChange | ||
| 43 | cursorScale := -1.0 // not loaded yet | 43 | cursorScale := -1.0 // not loaded yet | ||
| 44 | seen := map[ports.WindowID]uint64{} | 44 | seen := map[ports.WindowID]uint64{} | ||
| 45 | + | // pending is a report the channel could not take, retried soon. | |||
| 46 | + | var pending *ports.OutputPresented | |||
| 45 | update := func(c ports.SurfaceContent) { | 47 | update := func(c ports.SurfaceContent) { | ||
| 46 | seen[c.ID] = max(seen[c.ID], c.Seq) | 48 | seen[c.ID] = max(seen[c.ID], c.Seq) | ||
| 47 | dirty = dirty || scene.Shows(c.ID) | 49 | dirty = dirty || scene.Shows(c.ID) | ||
| @@ -55,9 +57,16 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 55 | if ctx.Err() != nil { | 57 | if ctx.Err() != nil { | ||
| 56 | return nil | 58 | return nil | ||
| 57 | } | 59 | } | ||
| 60 | + | var retry <-chan time.Time | |||
| 61 | + | if pending != nil { | |||
| 62 | + | retry = time.After(time.Millisecond) | |||
| 63 | + | } | |||
| 58 | select { | 64 | select { | ||
| 59 | case <-ctx.Done(): | 65 | case <-ctx.Done(): | ||
| 60 | return nil | 66 | return nil | ||
| 67 | + | case <-retry: | |||
| 68 | + | pending = opts.send(pending) | |||
| 69 | + | continue | |||
| 61 | case s, ok := <-scenes: | 70 | case s, ok := <-scenes: | ||
| 62 | if !ok { | 71 | if !ok { | ||
| 63 | scenes = nil | 72 | scenes = nil | ||
| @@ -96,7 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 96 | } | 105 | } | ||
| 97 | if !haveScene || !dirty { | 106 | if !haveScene || !dirty { | ||
| 98 | // Contents not drawn are still read: report them. | 107 | // Contents not drawn are still read: report them. | ||
| 99 | - | opts.report(false, seen) | 108 | + | pending = opts.report(pending, seen) |
| 100 | continue | 109 | continue | ||
| 101 | } | 110 | } | ||
| 102 | dirty = false | 111 | dirty = false | ||
| @@ -113,7 +122,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 113 | return fmt.Errorf("render frame: %w", err) | 122 | return fmt.Errorf("render frame: %w", err) | ||
| 114 | } | 123 | } | ||
| 115 | frame++ | 124 | frame++ | ||
| 116 | - | opts.report(false, seen) | 125 | + | pending = opts.report(pending, seen) |
| 117 | if opts.ScreenshotDir != "" { | 126 | if opts.ScreenshotDir != "" { | ||
| 118 | shot := r.Pixels() | 127 | shot := r.Pixels() | ||
| 119 | if opts.Cursor != nil { | 128 | if opts.Cursor != nil { | ||
| @@ -147,14 +156,21 @@ func writePNG(path string, img *image.RGBA) error { | |||||
| 147 | } | 156 | } | ||
| 148 | 157 | ||||
| 149 | // report sends what the output read; a full channel drops it. | 158 | // report sends what the output read; a full channel drops it. | ||
| 150 | - | // Headless frames are not paced by a screen: Flip stays false and frame | 159 | + | // report sends what the output read, or returns it to retry when the |
| 151 | - | // callbacks follow the refresh timer. | 160 | + | // channel is full. Headless frames are not paced by a screen: Flip stays |
| 152 | - | func (opts Options) report(flip bool, seen map[ports.WindowID]uint64) { | 161 | + | // false and frame callbacks follow the refresh timer. |
| 162 | + | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | |||
| 153 | if opts.Presented == nil { | 163 | if opts.Presented == nil { | ||
| 154 | - | return | 164 | + | return nil |
| 155 | } | 165 | } | ||
| 166 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |||
| 156 | select { | 170 | select { | ||
| 157 | - | case opts.Presented <- ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: maps.Clone(seen)}: | 171 | + | case opts.Presented <- *r: |
| 172 | + | return nil | |||
| 158 | default: | 173 | default: | ||
| 174 | + | return r | |||
| 159 | } | 175 | } | ||
| 160 | } | 176 | } | ||
M internal/adapters/wayland/release.go +26 -19
| @@ -23,7 +23,6 @@ const heldTimeout = 100 * time.Millisecond | |||
| 23 | 23 | type heldBuffer struct { | |
| 24 | 24 | res *wayland.Buffer | |
| 25 | 25 | window ports.WindowID | |
| 26 | - | output string | |
| 27 | 26 | id uint64 // DMABuf ID, 0 for wl_shm | |
| 28 | 27 | after uint64 // the window's content Seq when the buffer was replaced | |
| 29 | 28 | at time.Time | |
| @@ -41,7 +40,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |||
| 41 | 40 | b.SendRelease() | |
| 42 | 41 | return | |
| 43 | 42 | } | |
| 44 | - | h := heldBuffer{res: b, window: window, output: name, at: time.Now()} | |
| 43 | + | h := heldBuffer{res: b, window: window, at: time.Now()} | |
| 45 | 44 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | |
| 46 | 45 | h.id = d.buf.ID | |
| 47 | 46 | } | |
| @@ -57,24 +56,26 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |||
| 57 | 56 | } | |
| 58 | 57 | ||
| 59 | 58 | // releaseHeld records the output reports and releases the buffers no | |
| 60 | - | // output reads. It reports whether buffers are still held. | |
| 59 | + | // output reads. Every output reads every window's content, so a buffer | |
| 60 | + | // waits until no live output shows or queues it and all of them have seen | |
| 61 | + | // a later content of its window (or heldTimeout passed). It reports | |
| 62 | + | // whether buffers are still held. | |
| 61 | 63 | func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool { | |
| 62 | 64 | for _, r := range reports { | |
| 63 | 65 | s.reports[r.Output] = r | |
| 64 | 66 | } | |
| 65 | - | live := map[string]bool{} | |
| 66 | - | for _, o := range s.outputs { | |
| 67 | - | live[o.name()] = true | |
| 68 | - | } | |
| 67 | + | live := make([]ports.OutputPresented, 0, len(s.outputs)) | |
| 69 | 68 | for name := range s.reports { | |
| 70 | - | if !live[name] { | |
| 69 | + | if s.outputByNameExact(name) == nil { | |
| 71 | 70 | delete(s.reports, name) | |
| 72 | 71 | } | |
| 73 | 72 | } | |
| 73 | + | for _, r := range s.reports { | |
| 74 | + | live = append(live, r) | |
| 75 | + | } | |
| 74 | 76 | kept := s.held[:0] | |
| 75 | 77 | for _, h := range s.held { | |
| 76 | - | r, reported := s.reports[h.output] | |
| 77 | - | if live[h.output] && keepHeld(h, r, reported, now) { | |
| 78 | + | if keepHeld(h, live, now) { | |
| 78 | 79 | kept = append(kept, h) | |
| 79 | 80 | continue | |
| 80 | 81 | } | |
| @@ -87,15 +88,21 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo | |||
| 87 | 88 | return len(s.held) > 0 | |
| 88 | 89 | } | |
| 89 | 90 | ||
| 90 | - | // keepHeld reports whether the output (last report r) may still read a | |
| 91 | - | // held buffer: it scans it out or queued it, it has not got a later | |
| 92 | - | // content of the window yet, or it has not reported within heldTimeout. | |
| 93 | - | func keepHeld(h heldBuffer, r ports.OutputPresented, reported bool, now time.Time) bool { | |
| 94 | - | if reported && h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { | |
| 95 | - | return true | |
| 91 | + | // keepHeld reports whether an output may still read a held buffer: one | |
| 92 | + | // scans it out or queued it, or one has not got a later content of its | |
| 93 | + | // window yet and heldTimeout has not passed. | |
| 94 | + | func keepHeld(h heldBuffer, reports []ports.OutputPresented, now time.Time) bool { | |
| 95 | + | seen := true | |
| 96 | + | for _, r := range reports { | |
| 97 | + | if h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { | |
| 98 | + | return true | |
| 99 | + | } | |
| 100 | + | if r.Seen[h.window] <= h.after { | |
| 101 | + | seen = false | |
| 102 | + | } | |
| 96 | 103 | } | |
| 97 | - | if reported && r.Seen[h.window] > h.after { | |
| 98 | - | return false | |
| 104 | + | if len(reports) == 0 { | |
| 105 | + | seen = false | |
| 99 | 106 | } | |
| 100 | - | return now.Sub(h.at) < heldTimeout | |
| 107 | + | return !seen && now.Sub(h.at) < heldTimeout | |
| 101 | 108 | } | |
| @@ -23,7 +23,6 @@ const heldTimeout = 100 * time.Millisecond | |||||
| 23 | type heldBuffer struct { | 23 | type heldBuffer struct { | ||
| 24 | res *wayland.Buffer | 24 | res *wayland.Buffer | ||
| 25 | window ports.WindowID | 25 | window ports.WindowID | ||
| 26 | - | output string | |||
| 27 | id uint64 // DMABuf ID, 0 for wl_shm | 26 | id uint64 // DMABuf ID, 0 for wl_shm | ||
| 28 | after uint64 // the window's content Seq when the buffer was replaced | 27 | after uint64 // the window's content Seq when the buffer was replaced | ||
| 29 | at time.Time | 28 | at time.Time | ||
| @@ -41,7 +40,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |||||
| 41 | b.SendRelease() | 40 | b.SendRelease() | ||
| 42 | return | 41 | return | ||
| 43 | } | 42 | } | ||
| 44 | - | h := heldBuffer{res: b, window: window, output: name, at: time.Now()} | 43 | + | h := heldBuffer{res: b, window: window, at: time.Now()} |
| 45 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | 44 | if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok { | ||
| 46 | h.id = d.buf.ID | 45 | h.id = d.buf.ID | ||
| 47 | } | 46 | } | ||
| @@ -57,24 +56,26 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) { | |||||
| 57 | } | 56 | } | ||
| 58 | 57 | ||||
| 59 | // releaseHeld records the output reports and releases the buffers no | 58 | // releaseHeld records the output reports and releases the buffers no | ||
| 60 | - | // output reads. It reports whether buffers are still held. | 59 | + | // output reads. Every output reads every window's content, so a buffer |
| 60 | + | // waits until no live output shows or queues it and all of them have seen | |||
| 61 | + | // a later content of its window (or heldTimeout passed). It reports | |||
| 62 | + | // whether buffers are still held. | |||
| 61 | func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool { | 63 | func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool { | ||
| 62 | for _, r := range reports { | 64 | for _, r := range reports { | ||
| 63 | s.reports[r.Output] = r | 65 | s.reports[r.Output] = r | ||
| 64 | } | 66 | } | ||
| 65 | - | live := map[string]bool{} | 67 | + | live := make([]ports.OutputPresented, 0, len(s.outputs)) |
| 66 | - | for _, o := range s.outputs { | |||
| 67 | - | live[o.name()] = true | |||
| 68 | - | } | |||
| 69 | for name := range s.reports { | 68 | for name := range s.reports { | ||
| 70 | - | if !live[name] { | 69 | + | if s.outputByNameExact(name) == nil { |
| 71 | delete(s.reports, name) | 70 | delete(s.reports, name) | ||
| 72 | } | 71 | } | ||
| 73 | } | 72 | } | ||
| 73 | + | for _, r := range s.reports { | |||
| 74 | + | live = append(live, r) | |||
| 75 | + | } | |||
| 74 | kept := s.held[:0] | 76 | kept := s.held[:0] | ||
| 75 | for _, h := range s.held { | 77 | for _, h := range s.held { | ||
| 76 | - | r, reported := s.reports[h.output] | 78 | + | if keepHeld(h, live, now) { |
| 77 | - | if live[h.output] && keepHeld(h, r, reported, now) { | |||
| 78 | kept = append(kept, h) | 79 | kept = append(kept, h) | ||
| 79 | continue | 80 | continue | ||
| 80 | } | 81 | } | ||
| @@ -87,15 +88,21 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo | |||||
| 87 | return len(s.held) > 0 | 88 | return len(s.held) > 0 | ||
| 88 | } | 89 | } | ||
| 89 | 90 | ||||
| 90 | - | // keepHeld reports whether the output (last report r) may still read a | 91 | + | // keepHeld reports whether an output may still read a held buffer: one |
| 91 | - | // held buffer: it scans it out or queued it, it has not got a later | 92 | + | // scans it out or queued it, or one has not got a later content of its |
| 92 | - | // content of the window yet, or it has not reported within heldTimeout. | 93 | + | // window yet and heldTimeout has not passed. |
| 93 | - | func keepHeld(h heldBuffer, r ports.OutputPresented, reported bool, now time.Time) bool { | 94 | + | func keepHeld(h heldBuffer, reports []ports.OutputPresented, now time.Time) bool { |
| 94 | - | if reported && h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { | 95 | + | seen := true |
| 95 | - | return true | 96 | + | for _, r := range reports { |
| 97 | + | if h.id != 0 && (r.Shown == h.id || r.Queued == h.id) { | |||
| 98 | + | return true | |||
| 99 | + | } | |||
| 100 | + | if r.Seen[h.window] <= h.after { | |||
| 101 | + | seen = false | |||
| 102 | + | } | |||
| 96 | } | 103 | } | ||
| 97 | - | if reported && r.Seen[h.window] > h.after { | 104 | + | if len(reports) == 0 { |
| 98 | - | return false | 105 | + | seen = false |
| 99 | } | 106 | } | ||
| 100 | - | return now.Sub(h.at) < heldTimeout | 107 | + | return !seen && now.Sub(h.at) < heldTimeout |
| 101 | } | 108 | } | ||
M internal/adapters/wayland/release_test.go +23 -16
| @@ -11,25 +11,32 @@ func TestKeepHeld(t *testing.T) { | |||
| 11 | 11 | now := time.Unix(100, 0) | |
| 12 | 12 | dma := heldBuffer{window: 1, id: 7, after: 5, at: now} | |
| 13 | 13 | shm := heldBuffer{window: 1, after: 5, at: now} | |
| 14 | - | seen := func(seq uint64) map[ports.WindowID]uint64 { return map[ports.WindowID]uint64{1: seq} } | |
| 14 | + | out := func(shown, queued, seq uint64) ports.OutputPresented { | |
| 15 | + | return ports.OutputPresented{Shown: shown, Queued: queued, Seen: map[ports.WindowID]uint64{1: seq}} | |
| 16 | + | } | |
| 17 | + | type R = []ports.OutputPresented | |
| 15 | 18 | for _, tc := range []struct { | |
| 16 | - | name string | |
| 17 | - | h heldBuffer | |
| 18 | - | r ports.OutputPresented | |
| 19 | - | reported bool | |
| 20 | - | after time.Duration | |
| 21 | - | want bool | |
| 19 | + | name string | |
| 20 | + | h heldBuffer | |
| 21 | + | reports R | |
| 22 | + | after time.Duration | |
| 23 | + | want bool | |
| 22 | 24 | }{ | |
| 23 | - | {"no report yet", dma, ports.OutputPresented{}, false, 0, true}, | |
| 24 | - | {"output has only this content", dma, ports.OutputPresented{Seen: seen(5)}, true, 0, true}, | |
| 25 | - | {"later content seen", dma, ports.OutputPresented{Seen: seen(6)}, true, 0, false}, | |
| 26 | - | {"scanned out", dma, ports.OutputPresented{Shown: 7, Seen: seen(6)}, true, time.Second, true}, | |
| 27 | - | {"queued for scanout", dma, ports.OutputPresented{Queued: 7, Seen: seen(6)}, true, time.Second, true}, | |
| 28 | - | {"another buffer scanned out", dma, ports.OutputPresented{Shown: 8, Seen: seen(6)}, true, 0, false}, | |
| 29 | - | {"silent output times out", dma, ports.OutputPresented{Seen: seen(5)}, true, heldTimeout, false}, | |
| 30 | - | {"shm follows content", shm, ports.OutputPresented{Shown: 0, Seen: seen(6)}, true, 0, false}, | |
| 25 | + | {"no report yet", dma, nil, 0, true}, | |
| 26 | + | {"no report times out", dma, nil, heldTimeout, false}, | |
| 27 | + | {"output has only this content", dma, R{out(0, 0, 5)}, 0, true}, | |
| 28 | + | {"later content seen", dma, R{out(0, 0, 6)}, 0, false}, | |
| 29 | + | {"scanned out", dma, R{out(7, 0, 6)}, time.Second, true}, | |
| 30 | + | {"queued for scanout", dma, R{out(0, 7, 6)}, time.Second, true}, | |
| 31 | + | {"another buffer scanned out", dma, R{out(8, 0, 6)}, 0, false}, | |
| 32 | + | {"silent output times out", dma, R{out(0, 0, 5)}, heldTimeout, false}, | |
| 33 | + | {"shm follows content", shm, R{out(0, 0, 6)}, 0, false}, | |
| 34 | + | // A window moved off output A: A still scans the buffer out. | |
| 35 | + | {"other output scans it out", dma, R{out(0, 0, 6), out(7, 0, 6)}, time.Second, true}, | |
| 36 | + | {"other output lags", dma, R{out(0, 0, 6), out(0, 0, 5)}, 0, true}, | |
| 37 | + | {"all outputs moved on", dma, R{out(0, 0, 6), out(0, 0, 6)}, 0, false}, | |
| 31 | 38 | } { | |
| 32 | - | if got := keepHeld(tc.h, tc.r, tc.reported, now.Add(tc.after)); got != tc.want { | |
| 39 | + | if got := keepHeld(tc.h, tc.reports, now.Add(tc.after)); got != tc.want { | |
| 33 | 40 | t.Errorf("%s: %v", tc.name, got) | |
| 34 | 41 | } | |
| 35 | 42 | } | |
| @@ -11,25 +11,32 @@ func TestKeepHeld(t *testing.T) { | |||||
| 11 | now := time.Unix(100, 0) | 11 | now := time.Unix(100, 0) | ||
| 12 | dma := heldBuffer{window: 1, id: 7, after: 5, at: now} | 12 | dma := heldBuffer{window: 1, id: 7, after: 5, at: now} | ||
| 13 | shm := heldBuffer{window: 1, after: 5, at: now} | 13 | shm := heldBuffer{window: 1, after: 5, at: now} | ||
| 14 | - | seen := func(seq uint64) map[ports.WindowID]uint64 { return map[ports.WindowID]uint64{1: seq} } | 14 | + | out := func(shown, queued, seq uint64) ports.OutputPresented { |
| 15 | + | return ports.OutputPresented{Shown: shown, Queued: queued, Seen: map[ports.WindowID]uint64{1: seq}} | |||
| 16 | + | } | |||
| 17 | + | type R = []ports.OutputPresented | |||
| 15 | for _, tc := range []struct { | 18 | for _, tc := range []struct { | ||
| 16 | - | name string | 19 | + | name string |
| 17 | - | h heldBuffer | 20 | + | h heldBuffer |
| 18 | - | r ports.OutputPresented | 21 | + | reports R |
| 19 | - | reported bool | 22 | + | after time.Duration |
| 20 | - | after time.Duration | 23 | + | want bool |
| 21 | - | want bool | |||
| 22 | }{ | 24 | }{ | ||
| 23 | - | {"no report yet", dma, ports.OutputPresented{}, false, 0, true}, | 25 | + | {"no report yet", dma, nil, 0, true}, |
| 24 | - | {"output has only this content", dma, ports.OutputPresented{Seen: seen(5)}, true, 0, true}, | 26 | + | {"no report times out", dma, nil, heldTimeout, false}, |
| 25 | - | {"later content seen", dma, ports.OutputPresented{Seen: seen(6)}, true, 0, false}, | 27 | + | {"output has only this content", dma, R{out(0, 0, 5)}, 0, true}, |
| 26 | - | {"scanned out", dma, ports.OutputPresented{Shown: 7, Seen: seen(6)}, true, time.Second, true}, | 28 | + | {"later content seen", dma, R{out(0, 0, 6)}, 0, false}, |
| 27 | - | {"queued for scanout", dma, ports.OutputPresented{Queued: 7, Seen: seen(6)}, true, time.Second, true}, | 29 | + | {"scanned out", dma, R{out(7, 0, 6)}, time.Second, true}, |
| 28 | - | {"another buffer scanned out", dma, ports.OutputPresented{Shown: 8, Seen: seen(6)}, true, 0, false}, | 30 | + | {"queued for scanout", dma, R{out(0, 7, 6)}, time.Second, true}, |
| 29 | - | {"silent output times out", dma, ports.OutputPresented{Seen: seen(5)}, true, heldTimeout, false}, | 31 | + | {"another buffer scanned out", dma, R{out(8, 0, 6)}, 0, false}, |
| 30 | - | {"shm follows content", shm, ports.OutputPresented{Shown: 0, Seen: seen(6)}, true, 0, false}, | 32 | + | {"silent output times out", dma, R{out(0, 0, 5)}, heldTimeout, false}, |
| 33 | + | {"shm follows content", shm, R{out(0, 0, 6)}, 0, false}, | |||
| 34 | + | // A window moved off output A: A still scans the buffer out. | |||
| 35 | + | {"other output scans it out", dma, R{out(0, 0, 6), out(7, 0, 6)}, time.Second, true}, | |||
| 36 | + | {"other output lags", dma, R{out(0, 0, 6), out(0, 0, 5)}, 0, true}, | |||
| 37 | + | {"all outputs moved on", dma, R{out(0, 0, 6), out(0, 0, 6)}, 0, false}, | |||
| 31 | } { | 38 | } { | ||
| 32 | - | if got := keepHeld(tc.h, tc.r, tc.reported, now.Add(tc.after)); got != tc.want { | 39 | + | if got := keepHeld(tc.h, tc.reports, now.Add(tc.after)); got != tc.want { |
| 33 | t.Errorf("%s: %v", tc.name, got) | 40 | t.Errorf("%s: %v", tc.name, got) | ||
| 34 | } | 41 | } | ||
| 35 | } | 42 | } | ||