feat(wayland): wp_presentation feedback from kernel flip timestamps, flip-based frame times and commit timing
Feedbacks attach to a surface's commit and wait for the flip that shows their window's content: presented with that flip's CLOCK_MONOTONIC time, vblank counter, refresh (0 under VRR) and vsync/hw_clock/hw_completion/zero_copy flags, sync_output first; discarded when the surface committed again before, was destroyed, shows nowhere, its output left, or after a second without a flip. Each flip is handled on its own. Frame callback done carries the flip time, and commit timing targets the next vblank counted from the last flip instead of now plus a period. Headless frames report flips at the software clock.
- commit
- f0db5a64ce7f31a0a10a0cec5ef3d23f94b8e257
- author
- brice · · 2026-09-26 12:05 UTC
- parent
- 979e3cd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +571 -10
M internal/adapters/headless/output.go +26 -4
| @@ -3,6 +3,7 @@ package headless | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "golang.org/x/sys/unix" | |
| 6 | 7 | "image" | |
| 7 | 8 | "image/png" | |
| 8 | 9 | "maps" | |
| @@ -136,7 +137,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 136 | 137 | } | |
| 137 | 138 | } | |
| 138 | 139 | frame++ | |
| 139 | - | pending = opts.report(pending, seen) | |
| 140 | + | pending = opts.flipped(pending, seen, scene, surfaces) | |
| 140 | 141 | if opts.ScreenshotDir != "" { | |
| 141 | 142 | shot := r.Pixels() | |
| 142 | 143 | if opts.Cursor != nil { | |
| @@ -169,10 +170,8 @@ func writePNG(path string, img *image.RGBA) error { | |||
| 169 | 170 | return os.Rename(f.Name(), path) | |
| 170 | 171 | } | |
| 171 | 172 | ||
| 172 | - | // report sends what the output read; a full channel drops it. | |
| 173 | 173 | // report sends what the output read, or returns it to retry when the | |
| 174 | - | // channel is full. Headless frames are not paced by a screen: Flip stays | |
| 175 | - | // false and frame callbacks follow the refresh timer. | |
| 174 | + | // channel is full. Frames that were not drawn carry no flip. | |
| 176 | 175 | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | |
| 177 | 176 | if opts.Presented == nil { | |
| 178 | 177 | return nil | |
| @@ -180,6 +179,29 @@ func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uin | |||
| 180 | 179 | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | |
| 181 | 180 | } | |
| 182 | 181 | ||
| 182 | + | // flipped reports a drawn frame as a flip at the current CLOCK_MONOTONIC | |
| 183 | + | // time (software clock, refresh unknown), so presentation feedback and | |
| 184 | + | // frame callbacks follow headless frames. A report still waiting is | |
| 185 | + | // sent first. | |
| 186 | + | func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.WindowID]uint64, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) *ports.OutputPresented { | |
| 187 | + | if opts.Presented == nil { | |
| 188 | + | return nil | |
| 189 | + | } | |
| 190 | + | if pending != nil && opts.send(pending) != nil { | |
| 191 | + | // The reader is behind: this frame's report replaces the waiting one. | |
| 192 | + | pending = nil | |
| 193 | + | } | |
| 194 | + | var ts unix.Timespec | |
| 195 | + | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| 196 | + | shows := map[ports.WindowID]uint64{} | |
| 197 | + | for id, c := range surfaces { | |
| 198 | + | if scene.Shows(id) { | |
| 199 | + | shows[id] = c.Seq | |
| 200 | + | } | |
| 201 | + | } | |
| 202 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows}}) | |
| 203 | + | } | |
| 204 | + | ||
| 183 | 205 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | |
| 184 | 206 | select { | |
| 185 | 207 | case opts.Presented <- *r: | |
| @@ -3,6 +3,7 @@ package headless | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "golang.org/x/sys/unix" | |||
| 6 | "image" | 7 | "image" | ||
| 7 | "image/png" | 8 | "image/png" | ||
| 8 | "maps" | 9 | "maps" | ||
| @@ -136,7 +137,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 136 | } | 137 | } | ||
| 137 | } | 138 | } | ||
| 138 | frame++ | 139 | frame++ | ||
| 139 | - | pending = opts.report(pending, seen) | 140 | + | pending = opts.flipped(pending, seen, scene, surfaces) |
| 140 | if opts.ScreenshotDir != "" { | 141 | if opts.ScreenshotDir != "" { | ||
| 141 | shot := r.Pixels() | 142 | shot := r.Pixels() | ||
| 142 | if opts.Cursor != nil { | 143 | if opts.Cursor != nil { | ||
| @@ -169,10 +170,8 @@ func writePNG(path string, img *image.RGBA) error { | |||||
| 169 | return os.Rename(f.Name(), path) | 170 | return os.Rename(f.Name(), path) | ||
| 170 | } | 171 | } | ||
| 171 | 172 | ||||
| 172 | - | // report sends what the output read; a full channel drops it. | |||
| 173 | // report sends what the output read, or returns it to retry when the | 173 | // report sends what the output read, or returns it to retry when the | ||
| 174 | - | // channel is full. Headless frames are not paced by a screen: Flip stays | 174 | + | // channel is full. Frames that were not drawn carry no flip. |
| 175 | - | // false and frame callbacks follow the refresh timer. | |||
| 176 | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | 175 | func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented { | ||
| 177 | if opts.Presented == nil { | 176 | if opts.Presented == nil { | ||
| 178 | return nil | 177 | return nil | ||
| @@ -180,6 +179,29 @@ func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uin | |||||
| 180 | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | 179 | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)}) | ||
| 181 | } | 180 | } | ||
| 182 | 181 | ||||
| 182 | + | // flipped reports a drawn frame as a flip at the current CLOCK_MONOTONIC | |||
| 183 | + | // time (software clock, refresh unknown), so presentation feedback and | |||
| 184 | + | // frame callbacks follow headless frames. A report still waiting is | |||
| 185 | + | // sent first. | |||
| 186 | + | func (opts Options) flipped(pending *ports.OutputPresented, seen map[ports.WindowID]uint64, scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) *ports.OutputPresented { | |||
| 187 | + | if opts.Presented == nil { | |||
| 188 | + | return nil | |||
| 189 | + | } | |||
| 190 | + | if pending != nil && opts.send(pending) != nil { | |||
| 191 | + | // The reader is behind: this frame's report replaces the waiting one. | |||
| 192 | + | pending = nil | |||
| 193 | + | } | |||
| 194 | + | var ts unix.Timespec | |||
| 195 | + | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |||
| 196 | + | shows := map[ports.WindowID]uint64{} | |||
| 197 | + | for id, c := range surfaces { | |||
| 198 | + | if scene.Shows(id) { | |||
| 199 | + | shows[id] = c.Seq | |||
| 200 | + | } | |||
| 201 | + | } | |||
| 202 | + | return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen), Flip: &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows}}) | |||
| 203 | + | } | |||
| 204 | + | ||||
| 183 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | 205 | func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented { | ||
| 184 | select { | 206 | select { | ||
| 185 | case opts.Presented <- *r: | 207 | case opts.Presented <- *r: | ||
M internal/adapters/wayland/fifo.go +29 -4
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "github.com/bnema/purego-libwayland/protocol/committiming" | |
| 8 | 8 | "github.com/bnema/purego-libwayland/protocol/contenttype" | |
| 9 | 9 | "github.com/bnema/purego-libwayland/protocol/fifo" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 10 | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | 12 | "github.com/bnema/purego-libwayland/server" | |
| 12 | 13 | "golang.org/x/sys/unix" | |
| @@ -24,7 +25,7 @@ import ( | |||
| 24 | 25 | // queued; the pacer (frames.go) applies queued updates in order when they | |
| 25 | 26 | // are ready. wp_content_type_v1 is recorded and logged only. | |
| 26 | 27 | ||
| 27 | - | func registerPresentation(d *server.Display, s *Server) error { | |
| 28 | + | func registerPresentationConstraints(d *server.Display, s *Server) error { | |
| 28 | 29 | if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 29 | 30 | _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s}) | |
| 30 | 31 | }); err != nil { | |
| @@ -216,6 +217,7 @@ type update struct { | |||
| 216 | 217 | barrier bool | |
| 217 | 218 | wait bool | |
| 218 | 219 | at time.Time | |
| 220 | + | feedback []*presentationtime.WpPresentationFeedback | |
| 219 | 221 | damage []ports.Rect | |
| 220 | 222 | bufDamage []ports.Rect | |
| 221 | 223 | } | |
| @@ -228,8 +230,9 @@ type childMove struct { | |||
| 228 | 230 | // takePending moves the pending state into an update: one-shot state is | |
| 229 | 231 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |
| 230 | 232 | func (s *surface) takePending() update { | |
| 231 | - | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage} | |
| 233 | + | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback} | |
| 232 | 234 | s.attached, s.pending, s.callbacks = false, nil, nil | |
| 235 | + | s.pendingFeedback = nil | |
| 233 | 236 | s.pendingDamage, s.pendingBufDamage = nil, nil | |
| 234 | 237 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | |
| 235 | 238 | if v := s.viewport; v != nil { | |
| @@ -260,6 +263,7 @@ func (s *surface) putPending(u update) { | |||
| 260 | 263 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | |
| 261 | 264 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | |
| 262 | 265 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | |
| 266 | + | s.pendingFeedback = u.feedback | |
| 263 | 267 | if u.vp != nil && s.viewport == u.vp { | |
| 264 | 268 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | |
| 265 | 269 | } | |
| @@ -287,7 +291,25 @@ func (s *surface) mustWait(now time.Time) bool { | |||
| 287 | 291 | // tooEarly reports whether content applied now would show before at: | |
| 288 | 292 | // it reaches the screen one refresh later. | |
| 289 | 293 | func (s *surface) tooEarly(at, now time.Time) bool { | |
| 290 | - | return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s)))) | |
| 294 | + | return !at.IsZero() && at.After(s.server.nextRefresh(s.server.fifoOutput(s), now)) | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // nextRefresh is when the output's next frame reaches the screen: the | |
| 298 | + | // vblank after now counted from its last flip (kernel time, refresh | |
| 299 | + | // period), or one period from now without flips or while VRR is on. | |
| 300 | + | func (s *Server) nextRefresh(name string, now time.Time) time.Time { | |
| 301 | + | p := s.outputPeriod(name) | |
| 302 | + | f, ok := s.flips[name] | |
| 303 | + | if !ok || f.When <= 0 || f.Refresh <= 0 { | |
| 304 | + | return now.Add(p) | |
| 305 | + | } | |
| 306 | + | mono := monotonic(now) | |
| 307 | + | since := mono - f.When | |
| 308 | + | if since < 0 || since > time.Second { | |
| 309 | + | return now.Add(p) | |
| 310 | + | } | |
| 311 | + | n := since/f.Refresh + 1 | |
| 312 | + | return now.Add(f.When + n*f.Refresh - mono) | |
| 291 | 313 | } | |
| 292 | 314 | ||
| 293 | 315 | // queueUpdate queues the pending commit until it can apply. | |
| @@ -322,6 +344,9 @@ func (s *surface) dropQueue() { | |||
| 322 | 344 | } else if len(u.callbacks) > 0 { | |
| 323 | 345 | s.server.queueFrames(s.server.frameOutput(s), u.callbacks) | |
| 324 | 346 | } | |
| 347 | + | for _, fb := range u.feedback { | |
| 348 | + | discard(fb) | |
| 349 | + | } | |
| 325 | 350 | if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() { | |
| 326 | 351 | released[u.buffer.Resource] = true | |
| 327 | 352 | u.buffer.SendRelease() | |
| @@ -389,7 +414,7 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration | |||
| 389 | 414 | break | |
| 390 | 415 | } | |
| 391 | 416 | if surf.tooEarly(u.at, now) { | |
| 392 | - | wait = min(wait, u.at.Add(-p).Sub(now)) | |
| 417 | + | wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond) | |
| 393 | 418 | break | |
| 394 | 419 | } | |
| 395 | 420 | surf.queue = surf.queue[1:] | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "github.com/bnema/purego-libwayland/protocol/committiming" | 7 | "github.com/bnema/purego-libwayland/protocol/committiming" | ||
| 8 | "github.com/bnema/purego-libwayland/protocol/contenttype" | 8 | "github.com/bnema/purego-libwayland/protocol/contenttype" | ||
| 9 | "github.com/bnema/purego-libwayland/protocol/fifo" | 9 | "github.com/bnema/purego-libwayland/protocol/fifo" | ||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 10 | "github.com/bnema/purego-libwayland/protocol/wayland" | 11 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 11 | "github.com/bnema/purego-libwayland/server" | 12 | "github.com/bnema/purego-libwayland/server" | ||
| 12 | "golang.org/x/sys/unix" | 13 | "golang.org/x/sys/unix" | ||
| @@ -24,7 +25,7 @@ import ( | |||||
| 24 | // queued; the pacer (frames.go) applies queued updates in order when they | 25 | // queued; the pacer (frames.go) applies queued updates in order when they | ||
| 25 | // are ready. wp_content_type_v1 is recorded and logged only. | 26 | // are ready. wp_content_type_v1 is recorded and logged only. | ||
| 26 | 27 | ||||
| 27 | - | func registerPresentation(d *server.Display, s *Server) error { | 28 | + | func registerPresentationConstraints(d *server.Display, s *Server) error { |
| 28 | if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | 29 | if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | ||
| 29 | _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s}) | 30 | _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s}) | ||
| 30 | }); err != nil { | 31 | }); err != nil { | ||
| @@ -216,6 +217,7 @@ type update struct { | |||||
| 216 | barrier bool | 217 | barrier bool | ||
| 217 | wait bool | 218 | wait bool | ||
| 218 | at time.Time | 219 | at time.Time | ||
| 220 | + | feedback []*presentationtime.WpPresentationFeedback | |||
| 219 | damage []ports.Rect | 221 | damage []ports.Rect | ||
| 220 | bufDamage []ports.Rect | 222 | bufDamage []ports.Rect | ||
| 221 | } | 223 | } | ||
| @@ -228,8 +230,9 @@ type childMove struct { | |||||
| 228 | // takePending moves the pending state into an update: one-shot state is | 230 | // takePending moves the pending state into an update: one-shot state is | ||
| 229 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | 231 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | ||
| 230 | func (s *surface) takePending() update { | 232 | func (s *surface) takePending() update { | ||
| 231 | - | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage} | 233 | + | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback} |
| 232 | s.attached, s.pending, s.callbacks = false, nil, nil | 234 | s.attached, s.pending, s.callbacks = false, nil, nil | ||
| 235 | + | s.pendingFeedback = nil | |||
| 233 | s.pendingDamage, s.pendingBufDamage = nil, nil | 236 | s.pendingDamage, s.pendingBufDamage = nil, nil | ||
| 234 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | 237 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | ||
| 235 | if v := s.viewport; v != nil { | 238 | if v := s.viewport; v != nil { | ||
| @@ -260,6 +263,7 @@ func (s *surface) putPending(u update) { | |||||
| 260 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | 263 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | ||
| 261 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | 264 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | ||
| 262 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | 265 | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | ||
| 266 | + | s.pendingFeedback = u.feedback | |||
| 263 | if u.vp != nil && s.viewport == u.vp { | 267 | if u.vp != nil && s.viewport == u.vp { | ||
| 264 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | 268 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | ||
| 265 | } | 269 | } | ||
| @@ -287,7 +291,25 @@ func (s *surface) mustWait(now time.Time) bool { | |||||
| 287 | // tooEarly reports whether content applied now would show before at: | 291 | // tooEarly reports whether content applied now would show before at: | ||
| 288 | // it reaches the screen one refresh later. | 292 | // it reaches the screen one refresh later. | ||
| 289 | func (s *surface) tooEarly(at, now time.Time) bool { | 293 | func (s *surface) tooEarly(at, now time.Time) bool { | ||
| 290 | - | return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s)))) | 294 | + | return !at.IsZero() && at.After(s.server.nextRefresh(s.server.fifoOutput(s), now)) |
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | // nextRefresh is when the output's next frame reaches the screen: the | |||
| 298 | + | // vblank after now counted from its last flip (kernel time, refresh | |||
| 299 | + | // period), or one period from now without flips or while VRR is on. | |||
| 300 | + | func (s *Server) nextRefresh(name string, now time.Time) time.Time { | |||
| 301 | + | p := s.outputPeriod(name) | |||
| 302 | + | f, ok := s.flips[name] | |||
| 303 | + | if !ok || f.When <= 0 || f.Refresh <= 0 { | |||
| 304 | + | return now.Add(p) | |||
| 305 | + | } | |||
| 306 | + | mono := monotonic(now) | |||
| 307 | + | since := mono - f.When | |||
| 308 | + | if since < 0 || since > time.Second { | |||
| 309 | + | return now.Add(p) | |||
| 310 | + | } | |||
| 311 | + | n := since/f.Refresh + 1 | |||
| 312 | + | return now.Add(f.When + n*f.Refresh - mono) | |||
| 291 | } | 313 | } | ||
| 292 | 314 | ||||
| 293 | // queueUpdate queues the pending commit until it can apply. | 315 | // queueUpdate queues the pending commit until it can apply. | ||
| @@ -322,6 +344,9 @@ func (s *surface) dropQueue() { | |||||
| 322 | } else if len(u.callbacks) > 0 { | 344 | } else if len(u.callbacks) > 0 { | ||
| 323 | s.server.queueFrames(s.server.frameOutput(s), u.callbacks) | 345 | s.server.queueFrames(s.server.frameOutput(s), u.callbacks) | ||
| 324 | } | 346 | } | ||
| 347 | + | for _, fb := range u.feedback { | |||
| 348 | + | discard(fb) | |||
| 349 | + | } | |||
| 325 | if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() { | 350 | if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() { | ||
| 326 | released[u.buffer.Resource] = true | 351 | released[u.buffer.Resource] = true | ||
| 327 | u.buffer.SendRelease() | 352 | u.buffer.SendRelease() | ||
| @@ -389,7 +414,7 @@ func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration | |||||
| 389 | break | 414 | break | ||
| 390 | } | 415 | } | ||
| 391 | if surf.tooEarly(u.at, now) { | 416 | if surf.tooEarly(u.at, now) { | ||
| 392 | - | wait = min(wait, u.at.Add(-p).Sub(now)) | 417 | + | wait = min(wait, u.at.Sub(s.nextRefresh(name, now))+time.Millisecond) |
| 393 | break | 418 | break | ||
| 394 | } | 419 | } | ||
| 395 | surf.queue = surf.queue[1:] | 420 | surf.queue = surf.queue[1:] | ||
M internal/adapters/wayland/frames.go +34 -1
| @@ -2,6 +2,7 @@ package wayland | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "golang.org/x/sys/unix" | |
| 5 | 6 | "slices" | |
| 6 | 7 | "time" | |
| 7 | 8 | ||
| @@ -54,8 +55,26 @@ func (s *Server) pace(ctx context.Context) { | |||
| 54 | 55 | wait, idle := defaultFramePeriod, false | |
| 55 | 56 | if !s.display.Do(func() { | |
| 56 | 57 | if ctx.Err() == nil { | |
| 58 | + | // Each flip answers its own presentation feedbacks, in | |
| 59 | + | // order: flips are never merged here. | |
| 60 | + | for _, p := range reports { | |
| 61 | + | if p.Flip != nil { | |
| 62 | + | s.flips[p.Output] = *p.Flip | |
| 63 | + | s.presentFlip(p.Output, p.Flip) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | s.dropFeedbacks(time.Now()) | |
| 67 | + | if len(s.feedbacks) > 0 { | |
| 68 | + | wait = feedbackTimeout | |
| 69 | + | } | |
| 57 | 70 | var due []string | |
| 58 | - | due, wait, idle = s.dueFrames(time.Now(), flipped) | |
| 71 | + | var dueWait time.Duration | |
| 72 | + | due, dueWait, idle = s.dueFrames(time.Now(), flipped) | |
| 73 | + | if len(s.feedbacks) > 0 { | |
| 74 | + | // Unanswered feedbacks time out: keep ticking. | |
| 75 | + | dueWait, idle = min(dueWait, feedbackTimeout), false | |
| 76 | + | } | |
| 77 | + | wait = dueWait | |
| 59 | 78 | for _, name := range due { | |
| 60 | 79 | s.sendFrames(name) | |
| 61 | 80 | } | |
| @@ -173,7 +192,12 @@ func (s *Server) sendFrames(name string) { | |||
| 173 | 192 | if len(callbacks) == 0 { | |
| 174 | 193 | return | |
| 175 | 194 | } | |
| 195 | + | // done carries the flip's time when the output flips (the frame the | |
| 196 | + | // client drew for is on screen), else now. | |
| 176 | 197 | ms := uint32(time.Since(s.started).Milliseconds()) | |
| 198 | + | if f, ok := s.flips[name]; ok && f.When > 0 { | |
| 199 | + | ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds()) | |
| 200 | + | } | |
| 177 | 201 | for _, cb := range callbacks { | |
| 178 | 202 | if !cb.Resource.Alive() { | |
| 179 | 203 | continue | |
| @@ -191,3 +215,12 @@ func framePeriod(refreshMilli int) time.Duration { | |||
| 191 | 215 | } | |
| 192 | 216 | return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli)) | |
| 193 | 217 | } | |
| 218 | + | ||
| 219 | + | // monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps. | |
| 220 | + | func monotonic(t time.Time) time.Duration { | |
| 221 | + | var ts unix.Timespec | |
| 222 | + | if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil { | |
| 223 | + | return 0 | |
| 224 | + | } | |
| 225 | + | return time.Duration(ts.Nano()) - time.Since(t) | |
| 226 | + | } | |
| @@ -2,6 +2,7 @@ package wayland | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "golang.org/x/sys/unix" | |||
| 5 | "slices" | 6 | "slices" | ||
| 6 | "time" | 7 | "time" | ||
| 7 | 8 | ||||
| @@ -54,8 +55,26 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 54 | wait, idle := defaultFramePeriod, false | 55 | wait, idle := defaultFramePeriod, false | ||
| 55 | if !s.display.Do(func() { | 56 | if !s.display.Do(func() { | ||
| 56 | if ctx.Err() == nil { | 57 | if ctx.Err() == nil { | ||
| 58 | + | // Each flip answers its own presentation feedbacks, in | |||
| 59 | + | // order: flips are never merged here. | |||
| 60 | + | for _, p := range reports { | |||
| 61 | + | if p.Flip != nil { | |||
| 62 | + | s.flips[p.Output] = *p.Flip | |||
| 63 | + | s.presentFlip(p.Output, p.Flip) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | s.dropFeedbacks(time.Now()) | |||
| 67 | + | if len(s.feedbacks) > 0 { | |||
| 68 | + | wait = feedbackTimeout | |||
| 69 | + | } | |||
| 57 | var due []string | 70 | var due []string | ||
| 58 | - | due, wait, idle = s.dueFrames(time.Now(), flipped) | 71 | + | var dueWait time.Duration |
| 72 | + | due, dueWait, idle = s.dueFrames(time.Now(), flipped) | |||
| 73 | + | if len(s.feedbacks) > 0 { | |||
| 74 | + | // Unanswered feedbacks time out: keep ticking. | |||
| 75 | + | dueWait, idle = min(dueWait, feedbackTimeout), false | |||
| 76 | + | } | |||
| 77 | + | wait = dueWait | |||
| 59 | for _, name := range due { | 78 | for _, name := range due { | ||
| 60 | s.sendFrames(name) | 79 | s.sendFrames(name) | ||
| 61 | } | 80 | } | ||
| @@ -173,7 +192,12 @@ func (s *Server) sendFrames(name string) { | |||||
| 173 | if len(callbacks) == 0 { | 192 | if len(callbacks) == 0 { | ||
| 174 | return | 193 | return | ||
| 175 | } | 194 | } | ||
| 195 | + | // done carries the flip's time when the output flips (the frame the | |||
| 196 | + | // client drew for is on screen), else now. | |||
| 176 | ms := uint32(time.Since(s.started).Milliseconds()) | 197 | ms := uint32(time.Since(s.started).Milliseconds()) | ||
| 198 | + | if f, ok := s.flips[name]; ok && f.When > 0 { | |||
| 199 | + | ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds()) | |||
| 200 | + | } | |||
| 177 | for _, cb := range callbacks { | 201 | for _, cb := range callbacks { | ||
| 178 | if !cb.Resource.Alive() { | 202 | if !cb.Resource.Alive() { | ||
| 179 | continue | 203 | continue | ||
| @@ -191,3 +215,12 @@ func framePeriod(refreshMilli int) time.Duration { | |||||
| 191 | } | 215 | } | ||
| 192 | return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli)) | 216 | return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli)) | ||
| 193 | } | 217 | } | ||
| 218 | + | ||||
| 219 | + | // monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps. | |||
| 220 | + | func monotonic(t time.Time) time.Duration { | |||
| 221 | + | var ts unix.Timespec | |||
| 222 | + | if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil { | |||
| 223 | + | return 0 | |||
| 224 | + | } | |||
| 225 | + | return time.Duration(ts.Nano()) - time.Since(t) | |||
| 226 | + | } | |||
M internal/adapters/wayland/globals.go +1 -0
| @@ -18,6 +18,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 18 | 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | |
| 19 | 19 | func() error { return registerPointerConstraints(d, s) }, | |
| 20 | 20 | func() error { return registerTearing(d, s) }, | |
| 21 | + | func() error { return registerPresentationConstraints(d, s) }, | |
| 21 | 22 | func() error { return registerPresentation(d, s) }, | |
| 22 | 23 | func() error { return registerActivation(d, s) }, | |
| 23 | 24 | func() error { return registerVirtualKeyboard(d, s) }, | |
| @@ -18,6 +18,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | ||
| 19 | func() error { return registerPointerConstraints(d, s) }, | 19 | func() error { return registerPointerConstraints(d, s) }, | ||
| 20 | func() error { return registerTearing(d, s) }, | 20 | func() error { return registerTearing(d, s) }, | ||
| 21 | + | func() error { return registerPresentationConstraints(d, s) }, | |||
| 21 | func() error { return registerPresentation(d, s) }, | 22 | func() error { return registerPresentation(d, s) }, | ||
| 22 | func() error { return registerActivation(d, s) }, | 23 | func() error { return registerActivation(d, s) }, | ||
| 23 | func() error { return registerVirtualKeyboard(d, s) }, | 24 | func() error { return registerVirtualKeyboard(d, s) }, | ||
A internal/adapters/wayland/presentation.go +192 -0
| @@ -0,0 +1,192 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 9 | + | "github.com/bnema/purego-libwayland/server" | |
| 10 | + | "golang.org/x/sys/unix" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // wp_presentation: a client learns when each commit reached the screen. | |
| 14 | + | // A feedback belongs to the commit it was requested before; on commit it | |
| 15 | + | // waits for the flip that shows its window's content (FlipInfo.Shows) | |
| 16 | + | // and gets that flip's kernel timestamp, vblank counter and refresh. | |
| 17 | + | // A content replaced before any flip showed it, a destroyed surface or a | |
| 18 | + | // surface on no output is discarded. | |
| 19 | + | ||
| 20 | + | const presentationVersion = 2 | |
| 21 | + | ||
| 22 | + | // feedbackWait is a committed feedback: presented once a flip on output | |
| 23 | + | // shows content seq of window win. | |
| 24 | + | type feedbackWait struct { | |
| 25 | + | fb *presentationtime.WpPresentationFeedback | |
| 26 | + | surf *surface | |
| 27 | + | win ports.WindowID | |
| 28 | + | seq uint64 | |
| 29 | + | output string | |
| 30 | + | at time.Time | |
| 31 | + | // 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. | |
| 33 | + | replaced uint64 | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func registerPresentation(d *server.Display, s *Server) error { | |
| 37 | + | return presentationtime.NewWpPresentationGlobal(d, presentationVersion, func(c server.Client, v, id uint32) { | |
| 38 | + | r, err := presentationtime.NewWpPresentation(c, int32(v), id, presentation{s}) | |
| 39 | + | if err == nil { | |
| 40 | + | r.SendClockId(unix.CLOCK_MONOTONIC) | |
| 41 | + | } | |
| 42 | + | }) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | type presentation struct{ server *Server } | |
| 46 | + | ||
| 47 | + | func (presentation) Destroy(*presentationtime.WpPresentation) {} | |
| 48 | + | ||
| 49 | + | // Feedback attaches a feedback to the surface's next commit. | |
| 50 | + | func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland.Surface, id uint32) { | |
| 51 | + | fb, err := presentationtime.NewWpPresentationFeedback(r.Client(), r.Version(), id, feedbackHandler{}) | |
| 52 | + | if err != nil { | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | state := p.server.surfaceOf(surf) | |
| 56 | + | if state == nil || state.destroyed { | |
| 57 | + | fb.SendDiscarded() | |
| 58 | + | fb.Destroy() | |
| 59 | + | return | |
| 60 | + | } | |
| 61 | + | state.pendingFeedback = append(state.pendingFeedback, fb) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | type feedbackHandler struct{} | |
| 65 | + | ||
| 66 | + | // Destroy is not a request of wp_presentation_feedback; the handler | |
| 67 | + | // satisfies the generated interface. | |
| 68 | + | func (feedbackHandler) Destroy(*presentationtime.WpPresentationFeedback) {} | |
| 69 | + | ||
| 70 | + | // commitFeedback moves a surface's pending feedbacks to its window's | |
| 71 | + | // 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) { | |
| 74 | + | srv := s.server | |
| 75 | + | root := s.root() | |
| 76 | + | win := root.windowID() | |
| 77 | + | if win != 0 { | |
| 78 | + | srv.contentMu.Lock() | |
| 79 | + | seq := srv.contentSeq[win] | |
| 80 | + | srv.contentMu.Unlock() | |
| 81 | + | for i := range srv.feedbacks { | |
| 82 | + | if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq { | |
| 83 | + | w.replaced = seq | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | if len(pending) == 0 { | |
| 88 | + | return | |
| 89 | + | } | |
| 90 | + | output := srv.frameOutput(s) | |
| 91 | + | if win == 0 || output == "" { | |
| 92 | + | for _, fb := range pending { | |
| 93 | + | discard(fb) | |
| 94 | + | } | |
| 95 | + | return | |
| 96 | + | } | |
| 97 | + | srv.contentMu.Lock() | |
| 98 | + | seq := srv.contentSeq[win] | |
| 99 | + | srv.contentMu.Unlock() | |
| 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()}) | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func discard(fb *presentationtime.WpPresentationFeedback) { | |
| 106 | + | if fb.Resource.Alive() { | |
| 107 | + | fb.SendDiscarded() | |
| 108 | + | fb.Destroy() | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // presentFlip answers the feedbacks a flip of output answers: a window | |
| 113 | + | // content at or after a feedback's is shown; the feedback is presented, | |
| 114 | + | // or discarded when its surface committed again by then. | |
| 115 | + | func (s *Server) presentFlip(output string, f *ports.FlipInfo) { | |
| 116 | + | if f == nil || len(s.feedbacks) == 0 { | |
| 117 | + | return | |
| 118 | + | } | |
| 119 | + | o := s.outputByNameExact(output) | |
| 120 | + | kept := s.feedbacks[:0] | |
| 121 | + | for _, w := range s.feedbacks { | |
| 122 | + | shown, ok := f.Shows[w.win] | |
| 123 | + | switch { | |
| 124 | + | case !w.fb.Resource.Alive(): | |
| 125 | + | case w.surf.destroyed: | |
| 126 | + | discard(w.fb) | |
| 127 | + | case w.output != output || !ok || shown < w.seq: | |
| 128 | + | kept = append(kept, w) | |
| 129 | + | case f.Merged > 0: | |
| 130 | + | // The reader fell behind: which flip showed it is unknown. | |
| 131 | + | discard(w.fb) | |
| 132 | + | default: | |
| 133 | + | // Shown, unless the surface's next commit is shown too. | |
| 134 | + | s.sendPresented(w, o, f, w.replaced == 0 || shown < w.replaced) | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | clear(s.feedbacks[len(kept):]) | |
| 138 | + | s.feedbacks = kept | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | // sendPresented sends presented for the flip; exact is false when a | |
| 142 | + | // later commit of the surface replaced it before the flip (then it is | |
| 143 | + | // discarded). | |
| 144 | + | func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exact bool) { | |
| 145 | + | if !exact { | |
| 146 | + | discard(w.fb) | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | if o != nil { | |
| 150 | + | for _, r := range o.resources { | |
| 151 | + | if r.Resource.Alive() && r.Client() == w.fb.Client() { | |
| 152 | + | w.fb.SendSyncOutput(r) | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | flags := uint32(0) | |
| 157 | + | if !f.Async { | |
| 158 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindVsync) | |
| 159 | + | } | |
| 160 | + | if f.HardwareClock { | |
| 161 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion) | |
| 162 | + | } | |
| 163 | + | if f.ZeroCopy { | |
| 164 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) | |
| 165 | + | } | |
| 166 | + | sec, nsec := uint64(f.When/time.Second), uint32(f.When%time.Second) | |
| 167 | + | w.fb.SendPresented(uint32(sec>>32), uint32(sec), nsec, uint32(f.Refresh), uint32(f.Seq>>32), uint32(f.Seq), flags) | |
| 168 | + | w.fb.Destroy() | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // feedbackTimeout discards a feedback no flip answered: its window is | |
| 172 | + | // hidden, on an output that does not flip (headless, switched away), or | |
| 173 | + | // was never drawn. | |
| 174 | + | const feedbackTimeout = time.Second | |
| 175 | + | ||
| 176 | + | // dropFeedbacks discards the feedbacks of destroyed surfaces, of outputs | |
| 177 | + | // that are gone, and those older than feedbackTimeout. | |
| 178 | + | func (s *Server) dropFeedbacks(now time.Time) { | |
| 179 | + | kept := s.feedbacks[:0] | |
| 180 | + | for _, w := range s.feedbacks { | |
| 181 | + | if !w.fb.Resource.Alive() { | |
| 182 | + | continue | |
| 183 | + | } | |
| 184 | + | if w.surf.destroyed || s.outputByNameExact(w.output) == nil || now.Sub(w.at) > feedbackTimeout { | |
| 185 | + | discard(w.fb) | |
| 186 | + | continue | |
| 187 | + | } | |
| 188 | + | kept = append(kept, w) | |
| 189 | + | } | |
| 190 | + | clear(s.feedbacks[len(kept):]) | |
| 191 | + | s.feedbacks = kept | |
| 192 | + | } | |
| @@ -0,0 +1,192 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "time" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 9 | + | "github.com/bnema/purego-libwayland/server" | |||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // wp_presentation: a client learns when each commit reached the screen. | |||
| 14 | + | // A feedback belongs to the commit it was requested before; on commit it | |||
| 15 | + | // waits for the flip that shows its window's content (FlipInfo.Shows) | |||
| 16 | + | // and gets that flip's kernel timestamp, vblank counter and refresh. | |||
| 17 | + | // A content replaced before any flip showed it, a destroyed surface or a | |||
| 18 | + | // surface on no output is discarded. | |||
| 19 | + | ||||
| 20 | + | const presentationVersion = 2 | |||
| 21 | + | ||||
| 22 | + | // feedbackWait is a committed feedback: presented once a flip on output | |||
| 23 | + | // shows content seq of window win. | |||
| 24 | + | type feedbackWait struct { | |||
| 25 | + | fb *presentationtime.WpPresentationFeedback | |||
| 26 | + | surf *surface | |||
| 27 | + | win ports.WindowID | |||
| 28 | + | seq uint64 | |||
| 29 | + | output string | |||
| 30 | + | at time.Time | |||
| 31 | + | // 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. | |||
| 33 | + | replaced uint64 | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func registerPresentation(d *server.Display, s *Server) error { | |||
| 37 | + | return presentationtime.NewWpPresentationGlobal(d, presentationVersion, func(c server.Client, v, id uint32) { | |||
| 38 | + | r, err := presentationtime.NewWpPresentation(c, int32(v), id, presentation{s}) | |||
| 39 | + | if err == nil { | |||
| 40 | + | r.SendClockId(unix.CLOCK_MONOTONIC) | |||
| 41 | + | } | |||
| 42 | + | }) | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | type presentation struct{ server *Server } | |||
| 46 | + | ||||
| 47 | + | func (presentation) Destroy(*presentationtime.WpPresentation) {} | |||
| 48 | + | ||||
| 49 | + | // Feedback attaches a feedback to the surface's next commit. | |||
| 50 | + | func (p presentation) Feedback(r *presentationtime.WpPresentation, surf *wayland.Surface, id uint32) { | |||
| 51 | + | fb, err := presentationtime.NewWpPresentationFeedback(r.Client(), r.Version(), id, feedbackHandler{}) | |||
| 52 | + | if err != nil { | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | state := p.server.surfaceOf(surf) | |||
| 56 | + | if state == nil || state.destroyed { | |||
| 57 | + | fb.SendDiscarded() | |||
| 58 | + | fb.Destroy() | |||
| 59 | + | return | |||
| 60 | + | } | |||
| 61 | + | state.pendingFeedback = append(state.pendingFeedback, fb) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | type feedbackHandler struct{} | |||
| 65 | + | ||||
| 66 | + | // Destroy is not a request of wp_presentation_feedback; the handler | |||
| 67 | + | // satisfies the generated interface. | |||
| 68 | + | func (feedbackHandler) Destroy(*presentationtime.WpPresentationFeedback) {} | |||
| 69 | + | ||||
| 70 | + | // commitFeedback moves a surface's pending feedbacks to its window's | |||
| 71 | + | // 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) { | |||
| 74 | + | srv := s.server | |||
| 75 | + | root := s.root() | |||
| 76 | + | win := root.windowID() | |||
| 77 | + | if win != 0 { | |||
| 78 | + | srv.contentMu.Lock() | |||
| 79 | + | seq := srv.contentSeq[win] | |||
| 80 | + | srv.contentMu.Unlock() | |||
| 81 | + | for i := range srv.feedbacks { | |||
| 82 | + | if w := &srv.feedbacks[i]; w.surf == s && w.replaced == 0 && seq > w.seq { | |||
| 83 | + | w.replaced = seq | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | if len(pending) == 0 { | |||
| 88 | + | return | |||
| 89 | + | } | |||
| 90 | + | output := srv.frameOutput(s) | |||
| 91 | + | if win == 0 || output == "" { | |||
| 92 | + | for _, fb := range pending { | |||
| 93 | + | discard(fb) | |||
| 94 | + | } | |||
| 95 | + | return | |||
| 96 | + | } | |||
| 97 | + | srv.contentMu.Lock() | |||
| 98 | + | seq := srv.contentSeq[win] | |||
| 99 | + | srv.contentMu.Unlock() | |||
| 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()}) | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func discard(fb *presentationtime.WpPresentationFeedback) { | |||
| 106 | + | if fb.Resource.Alive() { | |||
| 107 | + | fb.SendDiscarded() | |||
| 108 | + | fb.Destroy() | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // presentFlip answers the feedbacks a flip of output answers: a window | |||
| 113 | + | // content at or after a feedback's is shown; the feedback is presented, | |||
| 114 | + | // or discarded when its surface committed again by then. | |||
| 115 | + | func (s *Server) presentFlip(output string, f *ports.FlipInfo) { | |||
| 116 | + | if f == nil || len(s.feedbacks) == 0 { | |||
| 117 | + | return | |||
| 118 | + | } | |||
| 119 | + | o := s.outputByNameExact(output) | |||
| 120 | + | kept := s.feedbacks[:0] | |||
| 121 | + | for _, w := range s.feedbacks { | |||
| 122 | + | shown, ok := f.Shows[w.win] | |||
| 123 | + | switch { | |||
| 124 | + | case !w.fb.Resource.Alive(): | |||
| 125 | + | case w.surf.destroyed: | |||
| 126 | + | discard(w.fb) | |||
| 127 | + | case w.output != output || !ok || shown < w.seq: | |||
| 128 | + | kept = append(kept, w) | |||
| 129 | + | case f.Merged > 0: | |||
| 130 | + | // The reader fell behind: which flip showed it is unknown. | |||
| 131 | + | discard(w.fb) | |||
| 132 | + | default: | |||
| 133 | + | // Shown, unless the surface's next commit is shown too. | |||
| 134 | + | s.sendPresented(w, o, f, w.replaced == 0 || shown < w.replaced) | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | clear(s.feedbacks[len(kept):]) | |||
| 138 | + | s.feedbacks = kept | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | // sendPresented sends presented for the flip; exact is false when a | |||
| 142 | + | // later commit of the surface replaced it before the flip (then it is | |||
| 143 | + | // discarded). | |||
| 144 | + | func (s *Server) sendPresented(w feedbackWait, o *output, f *ports.FlipInfo, exact bool) { | |||
| 145 | + | if !exact { | |||
| 146 | + | discard(w.fb) | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | if o != nil { | |||
| 150 | + | for _, r := range o.resources { | |||
| 151 | + | if r.Resource.Alive() && r.Client() == w.fb.Client() { | |||
| 152 | + | w.fb.SendSyncOutput(r) | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | } | |||
| 156 | + | flags := uint32(0) | |||
| 157 | + | if !f.Async { | |||
| 158 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindVsync) | |||
| 159 | + | } | |||
| 160 | + | if f.HardwareClock { | |||
| 161 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion) | |||
| 162 | + | } | |||
| 163 | + | if f.ZeroCopy { | |||
| 164 | + | flags |= uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) | |||
| 165 | + | } | |||
| 166 | + | sec, nsec := uint64(f.When/time.Second), uint32(f.When%time.Second) | |||
| 167 | + | w.fb.SendPresented(uint32(sec>>32), uint32(sec), nsec, uint32(f.Refresh), uint32(f.Seq>>32), uint32(f.Seq), flags) | |||
| 168 | + | w.fb.Destroy() | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | // feedbackTimeout discards a feedback no flip answered: its window is | |||
| 172 | + | // hidden, on an output that does not flip (headless, switched away), or | |||
| 173 | + | // was never drawn. | |||
| 174 | + | const feedbackTimeout = time.Second | |||
| 175 | + | ||||
| 176 | + | // dropFeedbacks discards the feedbacks of destroyed surfaces, of outputs | |||
| 177 | + | // that are gone, and those older than feedbackTimeout. | |||
| 178 | + | func (s *Server) dropFeedbacks(now time.Time) { | |||
| 179 | + | kept := s.feedbacks[:0] | |||
| 180 | + | for _, w := range s.feedbacks { | |||
| 181 | + | if !w.fb.Resource.Alive() { | |||
| 182 | + | continue | |||
| 183 | + | } | |||
| 184 | + | if w.surf.destroyed || s.outputByNameExact(w.output) == nil || now.Sub(w.at) > feedbackTimeout { | |||
| 185 | + | discard(w.fb) | |||
| 186 | + | continue | |||
| 187 | + | } | |||
| 188 | + | kept = append(kept, w) | |||
| 189 | + | } | |||
| 190 | + | clear(s.feedbacks[len(kept):]) | |||
| 191 | + | s.feedbacks = kept | |||
| 192 | + | } | |||
A internal/adapters/wayland/presentation_test.go +273 -0
| @@ -0,0 +1,273 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/nefertty/internal/logging" | |
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |
| 11 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 12 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 13 | + | "github.com/bnema/wlturbo" | |
| 14 | + | "golang.org/x/sys/unix" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // presentedEvent is one feedback answer: presented (with its fields) or | |
| 18 | + | // discarded. | |
| 19 | + | type presentedEvent struct { | |
| 20 | + | discarded bool | |
| 21 | + | when time.Duration | |
| 22 | + | refresh uint32 | |
| 23 | + | seq uint64 | |
| 24 | + | flags uint32 | |
| 25 | + | syncOutputs int | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | type feedbackEvents struct { | |
| 29 | + | wlturbo.BaseProxy | |
| 30 | + | out chan presentedEvent | |
| 31 | + | sync int | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func (p *feedbackEvents) Dispatch(e *wlturbo.Event) { | |
| 35 | + | switch uint32(e.Opcode) { | |
| 36 | + | case presentationtime.WpPresentationFeedbackEventSyncOutput: | |
| 37 | + | p.sync++ | |
| 38 | + | case presentationtime.WpPresentationFeedbackEventPresented: | |
| 39 | + | d := e.Data() | |
| 40 | + | u := func(i int) uint32 { return binary.NativeEndian.Uint32(d[i*4:]) } | |
| 41 | + | sec := uint64(u(0))<<32 | uint64(u(1)) | |
| 42 | + | p.out <- presentedEvent{when: time.Duration(sec)*time.Second + time.Duration(u(2)), refresh: u(3), seq: uint64(u(4))<<32 | uint64(u(5)), flags: u(6), syncOutputs: p.sync} | |
| 43 | + | case presentationtime.WpPresentationFeedbackEventDiscarded: | |
| 44 | + | p.out <- presentedEvent{discarded: true} | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // presentServer is a server whose flips the test sends. | |
| 49 | + | func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, chan ports.OutputPresented, string) { | |
| 50 | + | t.Helper() | |
| 51 | + | dir := t.TempDir() | |
| 52 | + | events := make(chan ports.ClientEvent, 16) | |
| 53 | + | commands := make(chan ports.ClientCommand, 16) | |
| 54 | + | contents := make(chan ports.SurfaceContent, 64) | |
| 55 | + | presented := make(chan ports.OutputPresented, 16) | |
| 56 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents, Presented: presented}, logging.For(context.Background(), "wayland")) | |
| 57 | + | if err != nil { | |
| 58 | + | t.Fatal(err) | |
| 59 | + | } | |
| 60 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 61 | + | done := make(chan error, 1) | |
| 62 | + | go func() { done <- s.Run(ctx) }() | |
| 63 | + | t.Cleanup(func() { cancel(); <-done }) | |
| 64 | + | return s, events, commands, contents, presented, dir | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func TestPresentationFeedback(t *testing.T) { | |
| 68 | + | s, events, commands, contents, presented, dir := presentServer(t) | |
| 69 | + | c := protocolClient(t, s, dir) | |
| 70 | + | w, surf, xdg := surfaceMapper(t, c, events)() | |
| 71 | + | registerProtocol(t, c, xdg) | |
| 72 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1"} | |
| 73 | + | for deadline := time.Now().Add(2 * time.Second); ; { | |
| 74 | + | var out string | |
| 75 | + | s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) }) | |
| 76 | + | if out != "" { | |
| 77 | + | break | |
| 78 | + | } | |
| 79 | + | if time.Now().After(deadline) { | |
| 80 | + | t.Fatal("configure not applied") | |
| 81 | + | } | |
| 82 | + | time.Sleep(2 * time.Millisecond) | |
| 83 | + | } | |
| 84 | + | registerProtocol(t, c, bindProtocol(t, c, "wl_output")) | |
| 85 | + | pres := bindProtocol(t, c, "wp_presentation") | |
| 86 | + | registerProtocol(t, c, pres) | |
| 87 | + | // Drain contents so seqs are known. | |
| 88 | + | lastSeq := func() uint64 { | |
| 89 | + | t.Helper() | |
| 90 | + | var seq uint64 | |
| 91 | + | deadline := time.After(2 * time.Second) | |
| 92 | + | for { | |
| 93 | + | select { | |
| 94 | + | case got := <-contents: | |
| 95 | + | if got.ID == w.ID { | |
| 96 | + | seq = got.Seq | |
| 97 | + | } | |
| 98 | + | case <-time.After(50 * time.Millisecond): | |
| 99 | + | if seq != 0 { | |
| 100 | + | return seq | |
| 101 | + | } | |
| 102 | + | case <-deadline: | |
| 103 | + | t.Fatal("no content") | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | feedback := func() *feedbackEvents { | |
| 108 | + | t.Helper() | |
| 109 | + | fb := c.AllocateID() | |
| 110 | + | p := &feedbackEvents{out: make(chan presentedEvent, 4)} | |
| 111 | + | p.SetID(fb) | |
| 112 | + | c.Context().Register(p) | |
| 113 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb) | |
| 114 | + | return p | |
| 115 | + | } | |
| 116 | + | answer := func(p *feedbackEvents, what string) presentedEvent { | |
| 117 | + | t.Helper() | |
| 118 | + | deadline := time.Now().Add(2 * time.Second) | |
| 119 | + | for time.Now().Before(deadline) { | |
| 120 | + | if err := c.Roundtrip(); err != nil { | |
| 121 | + | t.Fatal(err) | |
| 122 | + | } | |
| 123 | + | select { | |
| 124 | + | case ev := <-p.out: | |
| 125 | + | return ev | |
| 126 | + | case <-time.After(5 * time.Millisecond): | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | t.Fatalf("no answer: %s", what) | |
| 130 | + | return presentedEvent{} | |
| 131 | + | } | |
| 132 | + | // A fresh 1×1 buffer per commit: every commit is a new content. | |
| 133 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 134 | + | registerProtocol(t, c, shm) | |
| 135 | + | fd, err := unix.MemfdCreate("present", 0) | |
| 136 | + | if err != nil { | |
| 137 | + | t.Fatal(err) | |
| 138 | + | } | |
| 139 | + | defer unix.Close(fd) | |
| 140 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |
| 141 | + | t.Fatal(err) | |
| 142 | + | } | |
| 143 | + | pool := c.AllocateID() | |
| 144 | + | registerProtocol(t, c, pool) | |
| 145 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |
| 146 | + | t.Fatal(err) | |
| 147 | + | } | |
| 148 | + | commit := func() { | |
| 149 | + | buf := c.AllocateID() | |
| 150 | + | registerProtocol(t, c, buf) | |
| 151 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 152 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 153 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 154 | + | if err := c.Roundtrip(); err != nil { | |
| 155 | + | t.Fatal(err) | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | flip := func(when time.Duration, seq uint64, shows uint64, zero bool) { | |
| 159 | + | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | |
| 160 | + | } | |
| 161 | + | // Presented with the flip's time, counter and flags. | |
| 162 | + | a := feedback() | |
| 163 | + | commit() | |
| 164 | + | seqA := lastSeq() | |
| 165 | + | flip(5*time.Second+7, 42, seqA, true) | |
| 166 | + | ev := answer(a, "presented") | |
| 167 | + | if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 { | |
| 168 | + | t.Fatalf("presented %+v", ev) | |
| 169 | + | } | |
| 170 | + | want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy) | |
| 171 | + | if ev.flags != want { | |
| 172 | + | t.Fatalf("flags %#x want %#x", ev.flags, want) | |
| 173 | + | } | |
| 174 | + | // Two flips in one drain: two presented events, each with its time. | |
| 175 | + | b := feedback() | |
| 176 | + | commit() | |
| 177 | + | seqB := lastSeq() | |
| 178 | + | d := feedback() | |
| 179 | + | commit() | |
| 180 | + | seqD := lastSeq() | |
| 181 | + | flip(6*time.Second, 43, seqB, false) | |
| 182 | + | flip(7*time.Second, 44, seqD, false) | |
| 183 | + | if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 { | |
| 184 | + | t.Fatalf("first %+v", ev) | |
| 185 | + | } | |
| 186 | + | if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 { | |
| 187 | + | t.Fatalf("second %+v", ev) | |
| 188 | + | } | |
| 189 | + | // Replaced before shown: discarded when the flip shows the newer one. | |
| 190 | + | old := feedback() | |
| 191 | + | commit() | |
| 192 | + | lastSeq() | |
| 193 | + | commit() | |
| 194 | + | seqNew := lastSeq() | |
| 195 | + | flip(8*time.Second, 45, seqNew, false) | |
| 196 | + | if ev := answer(old, "replaced"); !ev.discarded { | |
| 197 | + | t.Fatalf("replaced: %+v", ev) | |
| 198 | + | } | |
| 199 | + | // A subsurface's feedback follows its root: presented with the root's | |
| 200 | + | // content that the flip shows. | |
| 201 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 202 | + | sub := bindProtocol(t, c, "wl_subcompositor") | |
| 203 | + | child, subsurf := c.AllocateID(), c.AllocateID() | |
| 204 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |
| 205 | + | registerProtocol(t, c, child) | |
| 206 | + | requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf) | |
| 207 | + | registerProtocol(t, c, subsurf) | |
| 208 | + | cfb := c.AllocateID() | |
| 209 | + | cp := &feedbackEvents{out: make(chan presentedEvent, 1)} | |
| 210 | + | cp.SetID(cfb) | |
| 211 | + | c.Context().Register(cp) | |
| 212 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb) | |
| 213 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 214 | + | commit() // the parent applies the subsurface | |
| 215 | + | seqC := lastSeq() | |
| 216 | + | flip(8500*time.Millisecond, 50, seqC, false) | |
| 217 | + | if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 { | |
| 218 | + | t.Fatalf("subsurface %+v", ev) | |
| 219 | + | } | |
| 220 | + | // Surface destroyed: discarded. | |
| 221 | + | gone := feedback() | |
| 222 | + | commit() | |
| 223 | + | lastSeq() | |
| 224 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | |
| 225 | + | flip(9*time.Second, 46, 0, false) | |
| 226 | + | if ev := answer(gone, "destroyed"); !ev.discarded { | |
| 227 | + | t.Fatalf("destroyed: %+v", ev) | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | // A hidden window (no output) gets discarded at once. | |
| 232 | + | func TestPresentationHiddenDiscarded(t *testing.T) { | |
| 233 | + | s, events, _, _, _, dir := presentServer(t) | |
| 234 | + | c := protocolClient(t, s, dir) | |
| 235 | + | _, surf, _ := surfaceMapper(t, c, events)() | |
| 236 | + | pres := bindProtocol(t, c, "wp_presentation") | |
| 237 | + | registerProtocol(t, c, pres) | |
| 238 | + | fb := c.AllocateID() | |
| 239 | + | p := &feedbackEvents{out: make(chan presentedEvent, 1)} | |
| 240 | + | p.SetID(fb) | |
| 241 | + | c.Context().Register(p) | |
| 242 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb) | |
| 243 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 244 | + | if err := c.Roundtrip(); err != nil { | |
| 245 | + | t.Fatal(err) | |
| 246 | + | } | |
| 247 | + | if err := c.Roundtrip(); err != nil { | |
| 248 | + | t.Fatal(err) | |
| 249 | + | } | |
| 250 | + | select { | |
| 251 | + | case ev := <-p.out: | |
| 252 | + | if !ev.discarded { | |
| 253 | + | t.Fatalf("%+v", ev) | |
| 254 | + | } | |
| 255 | + | default: | |
| 256 | + | t.Fatal("not discarded") | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | // Commit timing: a commit targeted two refreshes ahead of the last flip | |
| 261 | + | // is not applied on the next one. | |
| 262 | + | func TestNextRefreshFollowsFlips(t *testing.T) { | |
| 263 | + | s := &Server{flips: map[string]ports.FlipInfo{}} | |
| 264 | + | s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}} | |
| 265 | + | now := time.Now() | |
| 266 | + | mono := monotonic(now) | |
| 267 | + | period := time.Second / 60 | |
| 268 | + | s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period} | |
| 269 | + | next := s.nextRefresh("A", now) | |
| 270 | + | if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond { | |
| 271 | + | t.Fatalf("next refresh in %v, want %v", d, period*3/4) | |
| 272 | + | } | |
| 273 | + | } | |
| @@ -0,0 +1,273 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/nefertty/internal/logging" | |||
| 10 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 11 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 12 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 13 | + | "github.com/bnema/wlturbo" | |||
| 14 | + | "golang.org/x/sys/unix" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // presentedEvent is one feedback answer: presented (with its fields) or | |||
| 18 | + | // discarded. | |||
| 19 | + | type presentedEvent struct { | |||
| 20 | + | discarded bool | |||
| 21 | + | when time.Duration | |||
| 22 | + | refresh uint32 | |||
| 23 | + | seq uint64 | |||
| 24 | + | flags uint32 | |||
| 25 | + | syncOutputs int | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | type feedbackEvents struct { | |||
| 29 | + | wlturbo.BaseProxy | |||
| 30 | + | out chan presentedEvent | |||
| 31 | + | sync int | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func (p *feedbackEvents) Dispatch(e *wlturbo.Event) { | |||
| 35 | + | switch uint32(e.Opcode) { | |||
| 36 | + | case presentationtime.WpPresentationFeedbackEventSyncOutput: | |||
| 37 | + | p.sync++ | |||
| 38 | + | case presentationtime.WpPresentationFeedbackEventPresented: | |||
| 39 | + | d := e.Data() | |||
| 40 | + | u := func(i int) uint32 { return binary.NativeEndian.Uint32(d[i*4:]) } | |||
| 41 | + | sec := uint64(u(0))<<32 | uint64(u(1)) | |||
| 42 | + | p.out <- presentedEvent{when: time.Duration(sec)*time.Second + time.Duration(u(2)), refresh: u(3), seq: uint64(u(4))<<32 | uint64(u(5)), flags: u(6), syncOutputs: p.sync} | |||
| 43 | + | case presentationtime.WpPresentationFeedbackEventDiscarded: | |||
| 44 | + | p.out <- presentedEvent{discarded: true} | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // presentServer is a server whose flips the test sends. | |||
| 49 | + | func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, chan ports.OutputPresented, string) { | |||
| 50 | + | t.Helper() | |||
| 51 | + | dir := t.TempDir() | |||
| 52 | + | events := make(chan ports.ClientEvent, 16) | |||
| 53 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 54 | + | contents := make(chan ports.SurfaceContent, 64) | |||
| 55 | + | presented := make(chan ports.OutputPresented, 16) | |||
| 56 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents, Presented: presented}, logging.For(context.Background(), "wayland")) | |||
| 57 | + | if err != nil { | |||
| 58 | + | t.Fatal(err) | |||
| 59 | + | } | |||
| 60 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 61 | + | done := make(chan error, 1) | |||
| 62 | + | go func() { done <- s.Run(ctx) }() | |||
| 63 | + | t.Cleanup(func() { cancel(); <-done }) | |||
| 64 | + | return s, events, commands, contents, presented, dir | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func TestPresentationFeedback(t *testing.T) { | |||
| 68 | + | s, events, commands, contents, presented, dir := presentServer(t) | |||
| 69 | + | c := protocolClient(t, s, dir) | |||
| 70 | + | w, surf, xdg := surfaceMapper(t, c, events)() | |||
| 71 | + | registerProtocol(t, c, xdg) | |||
| 72 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1"} | |||
| 73 | + | for deadline := time.Now().Add(2 * time.Second); ; { | |||
| 74 | + | var out string | |||
| 75 | + | s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) }) | |||
| 76 | + | if out != "" { | |||
| 77 | + | break | |||
| 78 | + | } | |||
| 79 | + | if time.Now().After(deadline) { | |||
| 80 | + | t.Fatal("configure not applied") | |||
| 81 | + | } | |||
| 82 | + | time.Sleep(2 * time.Millisecond) | |||
| 83 | + | } | |||
| 84 | + | registerProtocol(t, c, bindProtocol(t, c, "wl_output")) | |||
| 85 | + | pres := bindProtocol(t, c, "wp_presentation") | |||
| 86 | + | registerProtocol(t, c, pres) | |||
| 87 | + | // Drain contents so seqs are known. | |||
| 88 | + | lastSeq := func() uint64 { | |||
| 89 | + | t.Helper() | |||
| 90 | + | var seq uint64 | |||
| 91 | + | deadline := time.After(2 * time.Second) | |||
| 92 | + | for { | |||
| 93 | + | select { | |||
| 94 | + | case got := <-contents: | |||
| 95 | + | if got.ID == w.ID { | |||
| 96 | + | seq = got.Seq | |||
| 97 | + | } | |||
| 98 | + | case <-time.After(50 * time.Millisecond): | |||
| 99 | + | if seq != 0 { | |||
| 100 | + | return seq | |||
| 101 | + | } | |||
| 102 | + | case <-deadline: | |||
| 103 | + | t.Fatal("no content") | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | feedback := func() *feedbackEvents { | |||
| 108 | + | t.Helper() | |||
| 109 | + | fb := c.AllocateID() | |||
| 110 | + | p := &feedbackEvents{out: make(chan presentedEvent, 4)} | |||
| 111 | + | p.SetID(fb) | |||
| 112 | + | c.Context().Register(p) | |||
| 113 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb) | |||
| 114 | + | return p | |||
| 115 | + | } | |||
| 116 | + | answer := func(p *feedbackEvents, what string) presentedEvent { | |||
| 117 | + | t.Helper() | |||
| 118 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 119 | + | for time.Now().Before(deadline) { | |||
| 120 | + | if err := c.Roundtrip(); err != nil { | |||
| 121 | + | t.Fatal(err) | |||
| 122 | + | } | |||
| 123 | + | select { | |||
| 124 | + | case ev := <-p.out: | |||
| 125 | + | return ev | |||
| 126 | + | case <-time.After(5 * time.Millisecond): | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 129 | + | t.Fatalf("no answer: %s", what) | |||
| 130 | + | return presentedEvent{} | |||
| 131 | + | } | |||
| 132 | + | // A fresh 1×1 buffer per commit: every commit is a new content. | |||
| 133 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 134 | + | registerProtocol(t, c, shm) | |||
| 135 | + | fd, err := unix.MemfdCreate("present", 0) | |||
| 136 | + | if err != nil { | |||
| 137 | + | t.Fatal(err) | |||
| 138 | + | } | |||
| 139 | + | defer unix.Close(fd) | |||
| 140 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |||
| 141 | + | t.Fatal(err) | |||
| 142 | + | } | |||
| 143 | + | pool := c.AllocateID() | |||
| 144 | + | registerProtocol(t, c, pool) | |||
| 145 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |||
| 146 | + | t.Fatal(err) | |||
| 147 | + | } | |||
| 148 | + | commit := func() { | |||
| 149 | + | buf := c.AllocateID() | |||
| 150 | + | registerProtocol(t, c, buf) | |||
| 151 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 152 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 153 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 154 | + | if err := c.Roundtrip(); err != nil { | |||
| 155 | + | t.Fatal(err) | |||
| 156 | + | } | |||
| 157 | + | } | |||
| 158 | + | flip := func(when time.Duration, seq uint64, shows uint64, zero bool) { | |||
| 159 | + | presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}} | |||
| 160 | + | } | |||
| 161 | + | // Presented with the flip's time, counter and flags. | |||
| 162 | + | a := feedback() | |||
| 163 | + | commit() | |||
| 164 | + | seqA := lastSeq() | |||
| 165 | + | flip(5*time.Second+7, 42, seqA, true) | |||
| 166 | + | ev := answer(a, "presented") | |||
| 167 | + | if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 { | |||
| 168 | + | t.Fatalf("presented %+v", ev) | |||
| 169 | + | } | |||
| 170 | + | want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy) | |||
| 171 | + | if ev.flags != want { | |||
| 172 | + | t.Fatalf("flags %#x want %#x", ev.flags, want) | |||
| 173 | + | } | |||
| 174 | + | // Two flips in one drain: two presented events, each with its time. | |||
| 175 | + | b := feedback() | |||
| 176 | + | commit() | |||
| 177 | + | seqB := lastSeq() | |||
| 178 | + | d := feedback() | |||
| 179 | + | commit() | |||
| 180 | + | seqD := lastSeq() | |||
| 181 | + | flip(6*time.Second, 43, seqB, false) | |||
| 182 | + | flip(7*time.Second, 44, seqD, false) | |||
| 183 | + | if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 { | |||
| 184 | + | t.Fatalf("first %+v", ev) | |||
| 185 | + | } | |||
| 186 | + | if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 { | |||
| 187 | + | t.Fatalf("second %+v", ev) | |||
| 188 | + | } | |||
| 189 | + | // Replaced before shown: discarded when the flip shows the newer one. | |||
| 190 | + | old := feedback() | |||
| 191 | + | commit() | |||
| 192 | + | lastSeq() | |||
| 193 | + | commit() | |||
| 194 | + | seqNew := lastSeq() | |||
| 195 | + | flip(8*time.Second, 45, seqNew, false) | |||
| 196 | + | if ev := answer(old, "replaced"); !ev.discarded { | |||
| 197 | + | t.Fatalf("replaced: %+v", ev) | |||
| 198 | + | } | |||
| 199 | + | // A subsurface's feedback follows its root: presented with the root's | |||
| 200 | + | // content that the flip shows. | |||
| 201 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 202 | + | sub := bindProtocol(t, c, "wl_subcompositor") | |||
| 203 | + | child, subsurf := c.AllocateID(), c.AllocateID() | |||
| 204 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |||
| 205 | + | registerProtocol(t, c, child) | |||
| 206 | + | requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf) | |||
| 207 | + | registerProtocol(t, c, subsurf) | |||
| 208 | + | cfb := c.AllocateID() | |||
| 209 | + | cp := &feedbackEvents{out: make(chan presentedEvent, 1)} | |||
| 210 | + | cp.SetID(cfb) | |||
| 211 | + | c.Context().Register(cp) | |||
| 212 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb) | |||
| 213 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 214 | + | commit() // the parent applies the subsurface | |||
| 215 | + | seqC := lastSeq() | |||
| 216 | + | flip(8500*time.Millisecond, 50, seqC, false) | |||
| 217 | + | if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 { | |||
| 218 | + | t.Fatalf("subsurface %+v", ev) | |||
| 219 | + | } | |||
| 220 | + | // Surface destroyed: discarded. | |||
| 221 | + | gone := feedback() | |||
| 222 | + | commit() | |||
| 223 | + | lastSeq() | |||
| 224 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | |||
| 225 | + | flip(9*time.Second, 46, 0, false) | |||
| 226 | + | if ev := answer(gone, "destroyed"); !ev.discarded { | |||
| 227 | + | t.Fatalf("destroyed: %+v", ev) | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // A hidden window (no output) gets discarded at once. | |||
| 232 | + | func TestPresentationHiddenDiscarded(t *testing.T) { | |||
| 233 | + | s, events, _, _, _, dir := presentServer(t) | |||
| 234 | + | c := protocolClient(t, s, dir) | |||
| 235 | + | _, surf, _ := surfaceMapper(t, c, events)() | |||
| 236 | + | pres := bindProtocol(t, c, "wp_presentation") | |||
| 237 | + | registerProtocol(t, c, pres) | |||
| 238 | + | fb := c.AllocateID() | |||
| 239 | + | p := &feedbackEvents{out: make(chan presentedEvent, 1)} | |||
| 240 | + | p.SetID(fb) | |||
| 241 | + | c.Context().Register(p) | |||
| 242 | + | requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb) | |||
| 243 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 244 | + | if err := c.Roundtrip(); err != nil { | |||
| 245 | + | t.Fatal(err) | |||
| 246 | + | } | |||
| 247 | + | if err := c.Roundtrip(); err != nil { | |||
| 248 | + | t.Fatal(err) | |||
| 249 | + | } | |||
| 250 | + | select { | |||
| 251 | + | case ev := <-p.out: | |||
| 252 | + | if !ev.discarded { | |||
| 253 | + | t.Fatalf("%+v", ev) | |||
| 254 | + | } | |||
| 255 | + | default: | |||
| 256 | + | t.Fatal("not discarded") | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | // Commit timing: a commit targeted two refreshes ahead of the last flip | |||
| 261 | + | // is not applied on the next one. | |||
| 262 | + | func TestNextRefreshFollowsFlips(t *testing.T) { | |||
| 263 | + | s := &Server{flips: map[string]ports.FlipInfo{}} | |||
| 264 | + | s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}} | |||
| 265 | + | now := time.Now() | |||
| 266 | + | mono := monotonic(now) | |||
| 267 | + | period := time.Second / 60 | |||
| 268 | + | s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period} | |||
| 269 | + | next := s.nextRefresh("A", now) | |||
| 270 | + | if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond { | |||
| 271 | + | t.Fatalf("next refresh in %v, want %v", d, period*3/4) | |||
| 272 | + | } | |||
| 273 | + | } | |||
M internal/adapters/wayland/server.go +6 -1
| @@ -109,7 +109,11 @@ type Server struct { | |||
| 109 | 109 | contents map[ports.WindowID]ports.SurfaceContent | |
| 110 | 110 | contentSeq map[ports.WindowID]uint64 | |
| 111 | 111 | // damage is each window's recent damage (contentMu). | |
| 112 | - | damage map[ports.WindowID][]ports.SeqDamage | |
| 112 | + | damage map[ports.WindowID][]ports.SeqDamage | |
| 113 | + | // feedbacks wait for the flip that shows their content (presentation.go). | |
| 114 | + | feedbacks []feedbackWait | |
| 115 | + | // flips is each output's latest flip (pacer, under Do). | |
| 116 | + | flips map[string]ports.FlipInfo | |
| 113 | 117 | contentNotify chan struct{} | |
| 114 | 118 | contentReady chan struct{} | |
| 115 | 119 | outputs []*output | |
| @@ -168,6 +172,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 168 | 172 | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 169 | 173 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 170 | 174 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | |
| 175 | + | s.flips = map[string]ports.FlipInfo{} | |
| 171 | 176 | s.tokens = map[string]activationToken{} | |
| 172 | 177 | if opts.Keymap != "" { | |
| 173 | 178 | fd, size, e := keymapFile(opts.Keymap) | |
| @@ -109,7 +109,11 @@ type Server struct { | |||||
| 109 | contents map[ports.WindowID]ports.SurfaceContent | 109 | contents map[ports.WindowID]ports.SurfaceContent | ||
| 110 | contentSeq map[ports.WindowID]uint64 | 110 | contentSeq map[ports.WindowID]uint64 | ||
| 111 | // damage is each window's recent damage (contentMu). | 111 | // damage is each window's recent damage (contentMu). | ||
| 112 | - | damage map[ports.WindowID][]ports.SeqDamage | 112 | + | damage map[ports.WindowID][]ports.SeqDamage |
| 113 | + | // feedbacks wait for the flip that shows their content (presentation.go). | |||
| 114 | + | feedbacks []feedbackWait | |||
| 115 | + | // flips is each output's latest flip (pacer, under Do). | |||
| 116 | + | flips map[string]ports.FlipInfo | |||
| 113 | contentNotify chan struct{} | 117 | contentNotify chan struct{} | ||
| 114 | contentReady chan struct{} | 118 | contentReady chan struct{} | ||
| 115 | outputs []*output | 119 | outputs []*output | ||
| @@ -168,6 +172,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 168 | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | 172 | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | ||
| 169 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 173 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 170 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | 174 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | ||
| 175 | + | s.flips = map[string]ports.FlipInfo{} | |||
| 171 | s.tokens = map[string]activationToken{} | 176 | s.tokens = map[string]activationToken{} | ||
| 172 | if opts.Keymap != "" { | 177 | if opts.Keymap != "" { | ||
| 173 | fd, size, e := keymapFile(opts.Keymap) | 178 | fd, size, e := keymapFile(opts.Keymap) | ||
M internal/adapters/wayland/surface.go +10 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |
| 4 | 5 | "image" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| @@ -66,6 +67,8 @@ type surface struct { | |||
| 66 | 67 | // pixels (full: everything), read by the window's damage history. | |
| 67 | 68 | pendingDamage, pendingBufDamage []ports.Rect | |
| 68 | 69 | committed damage | |
| 70 | + | // pendingFeedback are wp_presentation feedbacks for the next commit. | |
| 71 | + | pendingFeedback []*presentationtime.WpPresentationFeedback | |
| 69 | 72 | } | |
| 70 | 73 | ||
| 71 | 74 | // damage is what one commit changed in a surface's buffer. | |
| @@ -174,6 +177,10 @@ func (s *surface) detach() { | |||
| 174 | 177 | ||
| 175 | 178 | func (s *surface) Destroy(*wayland.Surface) { | |
| 176 | 179 | s.destroyed = true | |
| 180 | + | for _, fb := range s.pendingFeedback { | |
| 181 | + | discard(fb) | |
| 182 | + | } | |
| 183 | + | s.pendingFeedback = nil | |
| 177 | 184 | s.tearing = nil // the control becomes inert | |
| 178 | 185 | s.dropQueue() | |
| 179 | 186 | if s.server.cursorSurface == s { | |
| @@ -309,6 +316,9 @@ func (s *surface) applyCommit() { | |||
| 309 | 316 | } | |
| 310 | 317 | s.redraw() | |
| 311 | 318 | } | |
| 319 | + | fb := s.pendingFeedback | |
| 320 | + | s.pendingFeedback = nil | |
| 321 | + | s.commitFeedback(fb) | |
| 312 | 322 | } | |
| 313 | 323 | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | |
| 314 | 324 | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "github.com/bnema/purego-libwayland/protocol/presentationtime" | |||
| 4 | "image" | 5 | "image" | ||
| 5 | "time" | 6 | "time" | ||
| 6 | 7 | ||||
| @@ -66,6 +67,8 @@ type surface struct { | |||||
| 66 | // pixels (full: everything), read by the window's damage history. | 67 | // pixels (full: everything), read by the window's damage history. | ||
| 67 | pendingDamage, pendingBufDamage []ports.Rect | 68 | pendingDamage, pendingBufDamage []ports.Rect | ||
| 68 | committed damage | 69 | committed damage | ||
| 70 | + | // pendingFeedback are wp_presentation feedbacks for the next commit. | |||
| 71 | + | pendingFeedback []*presentationtime.WpPresentationFeedback | |||
| 69 | } | 72 | } | ||
| 70 | 73 | ||||
| 71 | // damage is what one commit changed in a surface's buffer. | 74 | // damage is what one commit changed in a surface's buffer. | ||
| @@ -174,6 +177,10 @@ func (s *surface) detach() { | |||||
| 174 | 177 | ||||
| 175 | func (s *surface) Destroy(*wayland.Surface) { | 178 | func (s *surface) Destroy(*wayland.Surface) { | ||
| 176 | s.destroyed = true | 179 | s.destroyed = true | ||
| 180 | + | for _, fb := range s.pendingFeedback { | |||
| 181 | + | discard(fb) | |||
| 182 | + | } | |||
| 183 | + | s.pendingFeedback = nil | |||
| 177 | s.tearing = nil // the control becomes inert | 184 | s.tearing = nil // the control becomes inert | ||
| 178 | s.dropQueue() | 185 | s.dropQueue() | ||
| 179 | if s.server.cursorSurface == s { | 186 | if s.server.cursorSurface == s { | ||
| @@ -309,6 +316,9 @@ func (s *surface) applyCommit() { | |||||
| 309 | } | 316 | } | ||
| 310 | s.redraw() | 317 | s.redraw() | ||
| 311 | } | 318 | } | ||
| 319 | + | fb := s.pendingFeedback | |||
| 320 | + | s.pendingFeedback = nil | |||
| 321 | + | s.commitFeedback(fb) | |||
| 312 | } | 322 | } | ||
| 313 | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | 323 | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | ||
| 314 | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | 324 | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | ||