feat(wayland): fifo, commit-timing and content-type with a per-surface commit queue
- commit
- d5fa86c623740bd71eb20572df72f105828e6deb
- author
- brice · · 2026-09-25 18:51 UTC
- parent
- 07a1604
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +562 -9
A internal/adapters/wayland/fifo.go +379 -0
| @@ -0,0 +1,379 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/committiming" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/contenttype" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | + | "github.com/bnema/purego-libwayland/server" | |
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // Presentation constraints on content updates (games, Vulkan FIFO): | |
| 16 | + | // | |
| 17 | + | // - wp_fifo_v1: a commit with wait_barrier waits while the barrier set | |
| 18 | + | // by an earlier commit stands; the barrier clears at the output's | |
| 19 | + | // next refresh (its flip, or a deadline when it does not flip). | |
| 20 | + | // - wp_commit_timing_v1: a commit waits until the refresh that shows it | |
| 21 | + | // is not before its timestamp (CLOCK_MONOTONIC). | |
| 22 | + | // | |
| 23 | + | // A commit that must wait is taken out of the surface's pending state and | |
| 24 | + | // 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 | + | ||
| 27 | + | func registerPresentation(d *server.Display, s *Server) error { | |
| 28 | + | if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 29 | + | _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s}) | |
| 30 | + | }); err != nil { | |
| 31 | + | return err | |
| 32 | + | } | |
| 33 | + | if err := committiming.NewWpCommitTimingManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 34 | + | _, _ = committiming.NewWpCommitTimingManagerV1(c, int32(v), id, timingManager{s}) | |
| 35 | + | }); err != nil { | |
| 36 | + | return err | |
| 37 | + | } | |
| 38 | + | return contenttype.NewWpContentTypeManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 39 | + | _, _ = contenttype.NewWpContentTypeManagerV1(c, int32(v), id, contentTypeManager{s}) | |
| 40 | + | }) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // fifo_v1 | |
| 44 | + | ||
| 45 | + | type fifoManager struct{ server *Server } | |
| 46 | + | ||
| 47 | + | func (fifoManager) Destroy(*fifo.WpFifoManagerV1) {} | |
| 48 | + | ||
| 49 | + | func (m fifoManager) GetFifo(r *fifo.WpFifoManagerV1, id uint32, surf *wayland.Surface) { | |
| 50 | + | state := m.server.surfaceOf(surf) | |
| 51 | + | if state != nil && state.fifo != nil { | |
| 52 | + | r.PostError(uint32(fifo.WpFifoManagerV1ErrorAlreadyExists), "surface already has a fifo") | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | h := &fifoHandler{surface: state} | |
| 56 | + | res, err := fifo.NewWpFifoV1(r.Client(), r.Version(), id, h) | |
| 57 | + | if err != nil || state == nil { | |
| 58 | + | return | |
| 59 | + | } | |
| 60 | + | state.fifo = h | |
| 61 | + | res.OnDestroy = func() { | |
| 62 | + | if state.fifo == h { | |
| 63 | + | state.fifo = nil | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | type fifoHandler struct{ surface *surface } | |
| 69 | + | ||
| 70 | + | func (h *fifoHandler) live(r *fifo.WpFifoV1) bool { | |
| 71 | + | if h.surface == nil || h.surface.destroyed { | |
| 72 | + | r.PostError(uint32(fifo.WpFifoV1ErrorSurfaceDestroyed), "surface destroyed") | |
| 73 | + | return false | |
| 74 | + | } | |
| 75 | + | return true | |
| 76 | + | } | |
| 77 | + | func (h *fifoHandler) SetBarrier(r *fifo.WpFifoV1) { | |
| 78 | + | if h.live(r) { | |
| 79 | + | h.surface.pendingBarrier = true | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | func (h *fifoHandler) WaitBarrier(r *fifo.WpFifoV1) { | |
| 83 | + | if h.live(r) { | |
| 84 | + | h.surface.pendingWait = true | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | func (*fifoHandler) Destroy(*fifo.WpFifoV1) {} | |
| 88 | + | ||
| 89 | + | // commit_timing_v1 | |
| 90 | + | ||
| 91 | + | type timingManager struct{ server *Server } | |
| 92 | + | ||
| 93 | + | func (timingManager) Destroy(*committiming.WpCommitTimingManagerV1) {} | |
| 94 | + | ||
| 95 | + | func (m timingManager) GetTimer(r *committiming.WpCommitTimingManagerV1, id uint32, surf *wayland.Surface) { | |
| 96 | + | state := m.server.surfaceOf(surf) | |
| 97 | + | if state != nil && state.timer != nil { | |
| 98 | + | r.PostError(uint32(committiming.WpCommitTimingManagerV1ErrorCommitTimerExists), "surface already has a commit timer") | |
| 99 | + | return | |
| 100 | + | } | |
| 101 | + | h := &timerHandler{surface: state} | |
| 102 | + | res, err := committiming.NewWpCommitTimerV1(r.Client(), r.Version(), id, h) | |
| 103 | + | if err != nil || state == nil { | |
| 104 | + | return | |
| 105 | + | } | |
| 106 | + | state.timer = h | |
| 107 | + | res.OnDestroy = func() { | |
| 108 | + | if state.timer == h { | |
| 109 | + | state.timer = nil | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | type timerHandler struct{ surface *surface } | |
| 115 | + | ||
| 116 | + | func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secLo, nsec uint32) { | |
| 117 | + | switch { | |
| 118 | + | case h.surface == nil || h.surface.destroyed: | |
| 119 | + | r.PostError(uint32(committiming.WpCommitTimerV1ErrorSurfaceDestroyed), "surface destroyed") | |
| 120 | + | case nsec >= 1e9: | |
| 121 | + | r.PostError(uint32(committiming.WpCommitTimerV1ErrorInvalidTimestamp), "tv_nsec out of range") | |
| 122 | + | case !h.surface.pendingTime.IsZero(): | |
| 123 | + | r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set") | |
| 124 | + | default: | |
| 125 | + | h.surface.pendingTime = monotonicTime(int64(secHi)<<32|int64(secLo), int64(nsec)) | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {} | |
| 129 | + | ||
| 130 | + | // monotonicTime converts a CLOCK_MONOTONIC time to wall time. | |
| 131 | + | func monotonicTime(sec, nsec int64) time.Time { | |
| 132 | + | var now unix.Timespec | |
| 133 | + | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &now) | |
| 134 | + | return time.Now().Add(time.Duration((sec-now.Sec)*1e9 + nsec - now.Nsec)) | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // content_type_v1 | |
| 138 | + | ||
| 139 | + | type contentTypeManager struct{ server *Server } | |
| 140 | + | ||
| 141 | + | func (contentTypeManager) Destroy(*contenttype.WpContentTypeManagerV1) {} | |
| 142 | + | ||
| 143 | + | func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeManagerV1, id uint32, surf *wayland.Surface) { | |
| 144 | + | state := m.server.surfaceOf(surf) | |
| 145 | + | if state != nil && state.contentType != nil { | |
| 146 | + | r.PostError(uint32(contenttype.WpContentTypeManagerV1ErrorAlreadyConstructed), "surface already has a content type") | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | h := &contentTypeHandler{surface: state} | |
| 150 | + | res, err := contenttype.NewWpContentTypeV1(r.Client(), r.Version(), id, h) | |
| 151 | + | if err != nil || state == nil { | |
| 152 | + | return | |
| 153 | + | } | |
| 154 | + | state.contentType = h | |
| 155 | + | res.OnDestroy = func() { | |
| 156 | + | // Destroying it sets the type back to none, double-buffered. | |
| 157 | + | if state.contentType == h { | |
| 158 | + | state.contentType = nil | |
| 159 | + | state.pendingKind = 0 | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | type contentTypeHandler struct{ surface *surface } | |
| 165 | + | ||
| 166 | + | func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) { | |
| 167 | + | if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h { | |
| 168 | + | h.surface.pendingKind = kind | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {} | |
| 172 | + | ||
| 173 | + | func (s *Server) surfaceOf(w *wayland.Surface) *surface { | |
| 174 | + | if w == nil { | |
| 175 | + | return nil | |
| 176 | + | } | |
| 177 | + | return s.surfaces[w.Resource] | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // Content update queue | |
| 181 | + | ||
| 182 | + | // update is a surface's pending state taken out by a commit that waits. | |
| 183 | + | type update struct { | |
| 184 | + | attached bool | |
| 185 | + | buffer *wayland.Buffer | |
| 186 | + | scale int | |
| 187 | + | async bool | |
| 188 | + | kind uint32 | |
| 189 | + | callbacks []*wayland.Callback | |
| 190 | + | vp *viewport | |
| 191 | + | vpW, vpH int32 | |
| 192 | + | vpSet bool | |
| 193 | + | moves []childMove | |
| 194 | + | xdg *xdgSurface | |
| 195 | + | geometry ports.Rect | |
| 196 | + | cons *constraint | |
| 197 | + | region *ports.Rect | |
| 198 | + | layer *layerSurface | |
| 199 | + | layerNext layerState | |
| 200 | + | barrier bool | |
| 201 | + | wait bool | |
| 202 | + | at time.Time | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | type childMove struct { | |
| 206 | + | child *surface | |
| 207 | + | x, y int | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // takePending moves the pending state into an update: one-shot state is | |
| 211 | + | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |
| 212 | + | func (s *surface) takePending() update { | |
| 213 | + | 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} | |
| 214 | + | s.attached, s.pending, s.callbacks = false, nil, nil | |
| 215 | + | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | |
| 216 | + | if v := s.viewport; v != nil { | |
| 217 | + | u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet | |
| 218 | + | } | |
| 219 | + | for _, ch := range s.sub.children { | |
| 220 | + | if ch.sub.moved { | |
| 221 | + | u.moves = append(u.moves, childMove{ch, ch.sub.pendX, ch.sub.pendY}) | |
| 222 | + | ch.sub.moved = false | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | if s.xdg != nil { | |
| 226 | + | u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry | |
| 227 | + | } | |
| 228 | + | if c := s.server.constraints[s]; c != nil && c.pending != nil { | |
| 229 | + | u.cons, u.region, c.pending = c, c.pending, nil | |
| 230 | + | } | |
| 231 | + | if s.layer != nil { | |
| 232 | + | u.layer, u.layerNext = s.layer, s.layer.pending | |
| 233 | + | } | |
| 234 | + | return u | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // putPending makes an update the surface's pending state again. Parts | |
| 238 | + | // whose object is gone (viewport, child, role, constraint) are dropped. | |
| 239 | + | func (s *surface) putPending(u update) { | |
| 240 | + | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | |
| 241 | + | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | |
| 242 | + | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | |
| 243 | + | if u.vp != nil && s.viewport == u.vp { | |
| 244 | + | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | |
| 245 | + | } | |
| 246 | + | for _, m := range u.moves { | |
| 247 | + | if m.child.sub.parent == s { | |
| 248 | + | m.child.sub.pendX, m.child.sub.pendY, m.child.sub.moved = m.x, m.y, true | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | if u.xdg != nil && s.xdg == u.xdg { | |
| 252 | + | u.xdg.pendingGeometry = u.geometry | |
| 253 | + | } | |
| 254 | + | if u.cons != nil && s.server.constraints[s] == u.cons { | |
| 255 | + | u.cons.pending = u.region | |
| 256 | + | } | |
| 257 | + | if u.layer != nil && s.layer == u.layer { | |
| 258 | + | u.layer.pending = u.layerNext | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // mustWait reports whether the pending commit cannot apply now. | |
| 263 | + | func (s *surface) mustWait(now time.Time) bool { | |
| 264 | + | return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | // tooEarly reports whether content applied now would show before at: | |
| 268 | + | // it reaches the screen one refresh later. | |
| 269 | + | func (s *surface) tooEarly(at, now time.Time) bool { | |
| 270 | + | return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s)))) | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | // queueUpdate queues the pending commit until it can apply. | |
| 274 | + | func (s *surface) queueUpdate() { | |
| 275 | + | s.queue = append(s.queue, s.takePending()) | |
| 276 | + | s.server.fifoSurfaces[s] = struct{}{} | |
| 277 | + | s.server.wakePacer() | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | // applyUpdate applies a queued update, keeping the requests made since. | |
| 281 | + | func (s *surface) applyUpdate(u update) { | |
| 282 | + | later := s.takePending() | |
| 283 | + | s.putPending(u) | |
| 284 | + | s.applyCommit() | |
| 285 | + | s.putPending(later) | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | // dropQueue discards the queued updates of a destroyed surface. | |
| 289 | + | func (s *surface) dropQueue() { | |
| 290 | + | for _, u := range s.queue { | |
| 291 | + | for _, cb := range u.callbacks { | |
| 292 | + | cb.Destroy() | |
| 293 | + | } | |
| 294 | + | if u.buffer != nil && u.buffer != s.current && u.buffer.Resource.Alive() { | |
| 295 | + | u.buffer.SendRelease() | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | s.queue = nil | |
| 299 | + | delete(s.server.fifoSurfaces, s) | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | // setBarrier applies a committed set_barrier. | |
| 303 | + | func (s *surface) setBarrier(now time.Time) { | |
| 304 | + | s.barrier, s.barrierAt = true, now | |
| 305 | + | s.server.fifoSurfaces[s] = struct{}{} | |
| 306 | + | s.server.wakePacer() | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | // fifoOutput is the output pacing a surface, "" when none does. | |
| 310 | + | func (s *Server) fifoOutput(surf *surface) string { | |
| 311 | + | name := s.frameOutput(surf) | |
| 312 | + | if name != "" && s.outputByNameExact(name) == nil { | |
| 313 | + | return "" | |
| 314 | + | } | |
| 315 | + | return name | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | // outputPeriod is one refresh of an output ("": 60 Hz). | |
| 319 | + | func (s *Server) outputPeriod(name string) time.Duration { | |
| 320 | + | if o := s.outputByNameExact(name); o != nil && name != "" { | |
| 321 | + | return framePeriod(o.place.Info.RefreshMilli) | |
| 322 | + | } | |
| 323 | + | return defaultFramePeriod | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | // tickFifo clears the barriers whose latching deadline passed and applies | |
| 327 | + | // the queued updates now ready. A flip of the surface's output is its | |
| 328 | + | // deadline; outputs that do not flip (headless, switched away, no output) | |
| 329 | + | // clear it after one refresh, or 1.5 when they usually flip. It returns | |
| 330 | + | // the time to the next deadline and whether any surface still waits. | |
| 331 | + | func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) { | |
| 332 | + | for name := range flipped { | |
| 333 | + | if flipped[name] { | |
| 334 | + | s.lastFlip[name] = now | |
| 335 | + | } | |
| 336 | + | } | |
| 337 | + | wait := defaultFramePeriod | |
| 338 | + | for surf := range s.fifoSurfaces { | |
| 339 | + | name := s.fifoOutput(surf) | |
| 340 | + | p := s.outputPeriod(name) | |
| 341 | + | if surf.barrier { | |
| 342 | + | deadline := surf.barrierAt.Add(p) | |
| 343 | + | if name != "" && now.Sub(s.lastFlip[name]) < time.Second { | |
| 344 | + | deadline = deadline.Add(p / 2) | |
| 345 | + | } | |
| 346 | + | if (flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline) { | |
| 347 | + | surf.barrier = false | |
| 348 | + | } | |
| 349 | + | } | |
| 350 | + | for len(surf.queue) > 0 && !surf.destroyed { | |
| 351 | + | u := surf.queue[0] | |
| 352 | + | if u.wait && surf.barrier { | |
| 353 | + | break | |
| 354 | + | } | |
| 355 | + | if surf.tooEarly(u.at, now) { | |
| 356 | + | wait = min(wait, u.at.Add(-p).Sub(now)) | |
| 357 | + | break | |
| 358 | + | } | |
| 359 | + | surf.queue = surf.queue[1:] | |
| 360 | + | surf.applyUpdate(u) | |
| 361 | + | } | |
| 362 | + | if surf.barrier { | |
| 363 | + | deadline := surf.barrierAt.Add(p) | |
| 364 | + | wait = min(wait, deadline.Sub(now)) | |
| 365 | + | } | |
| 366 | + | if !surf.barrier && len(surf.queue) == 0 { | |
| 367 | + | delete(s.fifoSurfaces, surf) | |
| 368 | + | } | |
| 369 | + | } | |
| 370 | + | return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0 | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | // wakePacer makes the pacer look at its queues now. | |
| 374 | + | func (s *Server) wakePacer() { | |
| 375 | + | select { | |
| 376 | + | case s.frameReady <- struct{}{}: | |
| 377 | + | default: | |
| 378 | + | } | |
| 379 | + | } | |
| @@ -0,0 +1,379 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "time" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/committiming" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/contenttype" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 11 | + | "github.com/bnema/purego-libwayland/server" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // Presentation constraints on content updates (games, Vulkan FIFO): | |||
| 16 | + | // | |||
| 17 | + | // - wp_fifo_v1: a commit with wait_barrier waits while the barrier set | |||
| 18 | + | // by an earlier commit stands; the barrier clears at the output's | |||
| 19 | + | // next refresh (its flip, or a deadline when it does not flip). | |||
| 20 | + | // - wp_commit_timing_v1: a commit waits until the refresh that shows it | |||
| 21 | + | // is not before its timestamp (CLOCK_MONOTONIC). | |||
| 22 | + | // | |||
| 23 | + | // A commit that must wait is taken out of the surface's pending state and | |||
| 24 | + | // 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 | + | ||||
| 27 | + | func registerPresentation(d *server.Display, s *Server) error { | |||
| 28 | + | if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 29 | + | _, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s}) | |||
| 30 | + | }); err != nil { | |||
| 31 | + | return err | |||
| 32 | + | } | |||
| 33 | + | if err := committiming.NewWpCommitTimingManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 34 | + | _, _ = committiming.NewWpCommitTimingManagerV1(c, int32(v), id, timingManager{s}) | |||
| 35 | + | }); err != nil { | |||
| 36 | + | return err | |||
| 37 | + | } | |||
| 38 | + | return contenttype.NewWpContentTypeManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 39 | + | _, _ = contenttype.NewWpContentTypeManagerV1(c, int32(v), id, contentTypeManager{s}) | |||
| 40 | + | }) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // fifo_v1 | |||
| 44 | + | ||||
| 45 | + | type fifoManager struct{ server *Server } | |||
| 46 | + | ||||
| 47 | + | func (fifoManager) Destroy(*fifo.WpFifoManagerV1) {} | |||
| 48 | + | ||||
| 49 | + | func (m fifoManager) GetFifo(r *fifo.WpFifoManagerV1, id uint32, surf *wayland.Surface) { | |||
| 50 | + | state := m.server.surfaceOf(surf) | |||
| 51 | + | if state != nil && state.fifo != nil { | |||
| 52 | + | r.PostError(uint32(fifo.WpFifoManagerV1ErrorAlreadyExists), "surface already has a fifo") | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | h := &fifoHandler{surface: state} | |||
| 56 | + | res, err := fifo.NewWpFifoV1(r.Client(), r.Version(), id, h) | |||
| 57 | + | if err != nil || state == nil { | |||
| 58 | + | return | |||
| 59 | + | } | |||
| 60 | + | state.fifo = h | |||
| 61 | + | res.OnDestroy = func() { | |||
| 62 | + | if state.fifo == h { | |||
| 63 | + | state.fifo = nil | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | type fifoHandler struct{ surface *surface } | |||
| 69 | + | ||||
| 70 | + | func (h *fifoHandler) live(r *fifo.WpFifoV1) bool { | |||
| 71 | + | if h.surface == nil || h.surface.destroyed { | |||
| 72 | + | r.PostError(uint32(fifo.WpFifoV1ErrorSurfaceDestroyed), "surface destroyed") | |||
| 73 | + | return false | |||
| 74 | + | } | |||
| 75 | + | return true | |||
| 76 | + | } | |||
| 77 | + | func (h *fifoHandler) SetBarrier(r *fifo.WpFifoV1) { | |||
| 78 | + | if h.live(r) { | |||
| 79 | + | h.surface.pendingBarrier = true | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | func (h *fifoHandler) WaitBarrier(r *fifo.WpFifoV1) { | |||
| 83 | + | if h.live(r) { | |||
| 84 | + | h.surface.pendingWait = true | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | func (*fifoHandler) Destroy(*fifo.WpFifoV1) {} | |||
| 88 | + | ||||
| 89 | + | // commit_timing_v1 | |||
| 90 | + | ||||
| 91 | + | type timingManager struct{ server *Server } | |||
| 92 | + | ||||
| 93 | + | func (timingManager) Destroy(*committiming.WpCommitTimingManagerV1) {} | |||
| 94 | + | ||||
| 95 | + | func (m timingManager) GetTimer(r *committiming.WpCommitTimingManagerV1, id uint32, surf *wayland.Surface) { | |||
| 96 | + | state := m.server.surfaceOf(surf) | |||
| 97 | + | if state != nil && state.timer != nil { | |||
| 98 | + | r.PostError(uint32(committiming.WpCommitTimingManagerV1ErrorCommitTimerExists), "surface already has a commit timer") | |||
| 99 | + | return | |||
| 100 | + | } | |||
| 101 | + | h := &timerHandler{surface: state} | |||
| 102 | + | res, err := committiming.NewWpCommitTimerV1(r.Client(), r.Version(), id, h) | |||
| 103 | + | if err != nil || state == nil { | |||
| 104 | + | return | |||
| 105 | + | } | |||
| 106 | + | state.timer = h | |||
| 107 | + | res.OnDestroy = func() { | |||
| 108 | + | if state.timer == h { | |||
| 109 | + | state.timer = nil | |||
| 110 | + | } | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | type timerHandler struct{ surface *surface } | |||
| 115 | + | ||||
| 116 | + | func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secLo, nsec uint32) { | |||
| 117 | + | switch { | |||
| 118 | + | case h.surface == nil || h.surface.destroyed: | |||
| 119 | + | r.PostError(uint32(committiming.WpCommitTimerV1ErrorSurfaceDestroyed), "surface destroyed") | |||
| 120 | + | case nsec >= 1e9: | |||
| 121 | + | r.PostError(uint32(committiming.WpCommitTimerV1ErrorInvalidTimestamp), "tv_nsec out of range") | |||
| 122 | + | case !h.surface.pendingTime.IsZero(): | |||
| 123 | + | r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set") | |||
| 124 | + | default: | |||
| 125 | + | h.surface.pendingTime = monotonicTime(int64(secHi)<<32|int64(secLo), int64(nsec)) | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {} | |||
| 129 | + | ||||
| 130 | + | // monotonicTime converts a CLOCK_MONOTONIC time to wall time. | |||
| 131 | + | func monotonicTime(sec, nsec int64) time.Time { | |||
| 132 | + | var now unix.Timespec | |||
| 133 | + | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &now) | |||
| 134 | + | return time.Now().Add(time.Duration((sec-now.Sec)*1e9 + nsec - now.Nsec)) | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | // content_type_v1 | |||
| 138 | + | ||||
| 139 | + | type contentTypeManager struct{ server *Server } | |||
| 140 | + | ||||
| 141 | + | func (contentTypeManager) Destroy(*contenttype.WpContentTypeManagerV1) {} | |||
| 142 | + | ||||
| 143 | + | func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeManagerV1, id uint32, surf *wayland.Surface) { | |||
| 144 | + | state := m.server.surfaceOf(surf) | |||
| 145 | + | if state != nil && state.contentType != nil { | |||
| 146 | + | r.PostError(uint32(contenttype.WpContentTypeManagerV1ErrorAlreadyConstructed), "surface already has a content type") | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | h := &contentTypeHandler{surface: state} | |||
| 150 | + | res, err := contenttype.NewWpContentTypeV1(r.Client(), r.Version(), id, h) | |||
| 151 | + | if err != nil || state == nil { | |||
| 152 | + | return | |||
| 153 | + | } | |||
| 154 | + | state.contentType = h | |||
| 155 | + | res.OnDestroy = func() { | |||
| 156 | + | // Destroying it sets the type back to none, double-buffered. | |||
| 157 | + | if state.contentType == h { | |||
| 158 | + | state.contentType = nil | |||
| 159 | + | state.pendingKind = 0 | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | type contentTypeHandler struct{ surface *surface } | |||
| 165 | + | ||||
| 166 | + | func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) { | |||
| 167 | + | if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h { | |||
| 168 | + | h.surface.pendingKind = kind | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {} | |||
| 172 | + | ||||
| 173 | + | func (s *Server) surfaceOf(w *wayland.Surface) *surface { | |||
| 174 | + | if w == nil { | |||
| 175 | + | return nil | |||
| 176 | + | } | |||
| 177 | + | return s.surfaces[w.Resource] | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // Content update queue | |||
| 181 | + | ||||
| 182 | + | // update is a surface's pending state taken out by a commit that waits. | |||
| 183 | + | type update struct { | |||
| 184 | + | attached bool | |||
| 185 | + | buffer *wayland.Buffer | |||
| 186 | + | scale int | |||
| 187 | + | async bool | |||
| 188 | + | kind uint32 | |||
| 189 | + | callbacks []*wayland.Callback | |||
| 190 | + | vp *viewport | |||
| 191 | + | vpW, vpH int32 | |||
| 192 | + | vpSet bool | |||
| 193 | + | moves []childMove | |||
| 194 | + | xdg *xdgSurface | |||
| 195 | + | geometry ports.Rect | |||
| 196 | + | cons *constraint | |||
| 197 | + | region *ports.Rect | |||
| 198 | + | layer *layerSurface | |||
| 199 | + | layerNext layerState | |||
| 200 | + | barrier bool | |||
| 201 | + | wait bool | |||
| 202 | + | at time.Time | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | type childMove struct { | |||
| 206 | + | child *surface | |||
| 207 | + | x, y int | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // takePending moves the pending state into an update: one-shot state is | |||
| 211 | + | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |||
| 212 | + | func (s *surface) takePending() update { | |||
| 213 | + | 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} | |||
| 214 | + | s.attached, s.pending, s.callbacks = false, nil, nil | |||
| 215 | + | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | |||
| 216 | + | if v := s.viewport; v != nil { | |||
| 217 | + | u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet | |||
| 218 | + | } | |||
| 219 | + | for _, ch := range s.sub.children { | |||
| 220 | + | if ch.sub.moved { | |||
| 221 | + | u.moves = append(u.moves, childMove{ch, ch.sub.pendX, ch.sub.pendY}) | |||
| 222 | + | ch.sub.moved = false | |||
| 223 | + | } | |||
| 224 | + | } | |||
| 225 | + | if s.xdg != nil { | |||
| 226 | + | u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry | |||
| 227 | + | } | |||
| 228 | + | if c := s.server.constraints[s]; c != nil && c.pending != nil { | |||
| 229 | + | u.cons, u.region, c.pending = c, c.pending, nil | |||
| 230 | + | } | |||
| 231 | + | if s.layer != nil { | |||
| 232 | + | u.layer, u.layerNext = s.layer, s.layer.pending | |||
| 233 | + | } | |||
| 234 | + | return u | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // putPending makes an update the surface's pending state again. Parts | |||
| 238 | + | // whose object is gone (viewport, child, role, constraint) are dropped. | |||
| 239 | + | func (s *surface) putPending(u update) { | |||
| 240 | + | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | |||
| 241 | + | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | |||
| 242 | + | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | |||
| 243 | + | if u.vp != nil && s.viewport == u.vp { | |||
| 244 | + | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | |||
| 245 | + | } | |||
| 246 | + | for _, m := range u.moves { | |||
| 247 | + | if m.child.sub.parent == s { | |||
| 248 | + | m.child.sub.pendX, m.child.sub.pendY, m.child.sub.moved = m.x, m.y, true | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | if u.xdg != nil && s.xdg == u.xdg { | |||
| 252 | + | u.xdg.pendingGeometry = u.geometry | |||
| 253 | + | } | |||
| 254 | + | if u.cons != nil && s.server.constraints[s] == u.cons { | |||
| 255 | + | u.cons.pending = u.region | |||
| 256 | + | } | |||
| 257 | + | if u.layer != nil && s.layer == u.layer { | |||
| 258 | + | u.layer.pending = u.layerNext | |||
| 259 | + | } | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | // mustWait reports whether the pending commit cannot apply now. | |||
| 263 | + | func (s *surface) mustWait(now time.Time) bool { | |||
| 264 | + | return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now) | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | // tooEarly reports whether content applied now would show before at: | |||
| 268 | + | // it reaches the screen one refresh later. | |||
| 269 | + | func (s *surface) tooEarly(at, now time.Time) bool { | |||
| 270 | + | return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s)))) | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | // queueUpdate queues the pending commit until it can apply. | |||
| 274 | + | func (s *surface) queueUpdate() { | |||
| 275 | + | s.queue = append(s.queue, s.takePending()) | |||
| 276 | + | s.server.fifoSurfaces[s] = struct{}{} | |||
| 277 | + | s.server.wakePacer() | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | // applyUpdate applies a queued update, keeping the requests made since. | |||
| 281 | + | func (s *surface) applyUpdate(u update) { | |||
| 282 | + | later := s.takePending() | |||
| 283 | + | s.putPending(u) | |||
| 284 | + | s.applyCommit() | |||
| 285 | + | s.putPending(later) | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | // dropQueue discards the queued updates of a destroyed surface. | |||
| 289 | + | func (s *surface) dropQueue() { | |||
| 290 | + | for _, u := range s.queue { | |||
| 291 | + | for _, cb := range u.callbacks { | |||
| 292 | + | cb.Destroy() | |||
| 293 | + | } | |||
| 294 | + | if u.buffer != nil && u.buffer != s.current && u.buffer.Resource.Alive() { | |||
| 295 | + | u.buffer.SendRelease() | |||
| 296 | + | } | |||
| 297 | + | } | |||
| 298 | + | s.queue = nil | |||
| 299 | + | delete(s.server.fifoSurfaces, s) | |||
| 300 | + | } | |||
| 301 | + | ||||
| 302 | + | // setBarrier applies a committed set_barrier. | |||
| 303 | + | func (s *surface) setBarrier(now time.Time) { | |||
| 304 | + | s.barrier, s.barrierAt = true, now | |||
| 305 | + | s.server.fifoSurfaces[s] = struct{}{} | |||
| 306 | + | s.server.wakePacer() | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | // fifoOutput is the output pacing a surface, "" when none does. | |||
| 310 | + | func (s *Server) fifoOutput(surf *surface) string { | |||
| 311 | + | name := s.frameOutput(surf) | |||
| 312 | + | if name != "" && s.outputByNameExact(name) == nil { | |||
| 313 | + | return "" | |||
| 314 | + | } | |||
| 315 | + | return name | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | // outputPeriod is one refresh of an output ("": 60 Hz). | |||
| 319 | + | func (s *Server) outputPeriod(name string) time.Duration { | |||
| 320 | + | if o := s.outputByNameExact(name); o != nil && name != "" { | |||
| 321 | + | return framePeriod(o.place.Info.RefreshMilli) | |||
| 322 | + | } | |||
| 323 | + | return defaultFramePeriod | |||
| 324 | + | } | |||
| 325 | + | ||||
| 326 | + | // tickFifo clears the barriers whose latching deadline passed and applies | |||
| 327 | + | // the queued updates now ready. A flip of the surface's output is its | |||
| 328 | + | // deadline; outputs that do not flip (headless, switched away, no output) | |||
| 329 | + | // clear it after one refresh, or 1.5 when they usually flip. It returns | |||
| 330 | + | // the time to the next deadline and whether any surface still waits. | |||
| 331 | + | func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) { | |||
| 332 | + | for name := range flipped { | |||
| 333 | + | if flipped[name] { | |||
| 334 | + | s.lastFlip[name] = now | |||
| 335 | + | } | |||
| 336 | + | } | |||
| 337 | + | wait := defaultFramePeriod | |||
| 338 | + | for surf := range s.fifoSurfaces { | |||
| 339 | + | name := s.fifoOutput(surf) | |||
| 340 | + | p := s.outputPeriod(name) | |||
| 341 | + | if surf.barrier { | |||
| 342 | + | deadline := surf.barrierAt.Add(p) | |||
| 343 | + | if name != "" && now.Sub(s.lastFlip[name]) < time.Second { | |||
| 344 | + | deadline = deadline.Add(p / 2) | |||
| 345 | + | } | |||
| 346 | + | if (flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline) { | |||
| 347 | + | surf.barrier = false | |||
| 348 | + | } | |||
| 349 | + | } | |||
| 350 | + | for len(surf.queue) > 0 && !surf.destroyed { | |||
| 351 | + | u := surf.queue[0] | |||
| 352 | + | if u.wait && surf.barrier { | |||
| 353 | + | break | |||
| 354 | + | } | |||
| 355 | + | if surf.tooEarly(u.at, now) { | |||
| 356 | + | wait = min(wait, u.at.Add(-p).Sub(now)) | |||
| 357 | + | break | |||
| 358 | + | } | |||
| 359 | + | surf.queue = surf.queue[1:] | |||
| 360 | + | surf.applyUpdate(u) | |||
| 361 | + | } | |||
| 362 | + | if surf.barrier { | |||
| 363 | + | deadline := surf.barrierAt.Add(p) | |||
| 364 | + | wait = min(wait, deadline.Sub(now)) | |||
| 365 | + | } | |||
| 366 | + | if !surf.barrier && len(surf.queue) == 0 { | |||
| 367 | + | delete(s.fifoSurfaces, surf) | |||
| 368 | + | } | |||
| 369 | + | } | |||
| 370 | + | return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0 | |||
| 371 | + | } | |||
| 372 | + | ||||
| 373 | + | // wakePacer makes the pacer look at its queues now. | |||
| 374 | + | func (s *Server) wakePacer() { | |||
| 375 | + | select { | |||
| 376 | + | case s.frameReady <- struct{}{}: | |||
| 377 | + | default: | |||
| 378 | + | } | |||
| 379 | + | } | |||
A internal/adapters/wayland/fifo_test.go +130 -0
| @@ -0,0 +1,130 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/committiming" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | + | "github.com/bnema/wlturbo" | |
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // nextContent waits for a content of window id. | |
| 16 | + | func nextContent(t *testing.T, contents <-chan ports.SurfaceContent, id ports.WindowID) time.Time { | |
| 17 | + | t.Helper() | |
| 18 | + | deadline := time.After(2 * time.Second) | |
| 19 | + | for { | |
| 20 | + | select { | |
| 21 | + | case c := <-contents: | |
| 22 | + | if c.ID == id && !c.Empty() { | |
| 23 | + | return time.Now() | |
| 24 | + | } | |
| 25 | + | case <-deadline: | |
| 26 | + | t.Fatal("no content") | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // shmBuffer creates a 1x1 wl_shm buffer. | |
| 32 | + | func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 { | |
| 33 | + | t.Helper() | |
| 34 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 35 | + | registerProtocol(t, c, shm) | |
| 36 | + | fd, err := unix.MemfdCreate("fifo-buffer", 0) | |
| 37 | + | if err != nil { | |
| 38 | + | t.Fatal(err) | |
| 39 | + | } | |
| 40 | + | defer unix.Close(fd) | |
| 41 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |
| 42 | + | t.Fatal(err) | |
| 43 | + | } | |
| 44 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |
| 45 | + | registerProtocol(t, c, pool) | |
| 46 | + | registerProtocol(t, c, buffer) | |
| 47 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |
| 48 | + | t.Fatal(err) | |
| 49 | + | } | |
| 50 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 51 | + | return buffer | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | func drainContents(contents <-chan ports.SurfaceContent) { | |
| 55 | + | for { | |
| 56 | + | select { | |
| 57 | + | case <-contents: | |
| 58 | + | case <-time.After(20 * time.Millisecond): | |
| 59 | + | return | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func TestFifoBarrierHoldsCommitOneRefresh(t *testing.T) { | |
| 65 | + | s, events, _, contents, dir := contentServer(t) | |
| 66 | + | c := protocolClient(t, s, dir) | |
| 67 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 68 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |
| 69 | + | f := c.AllocateID() | |
| 70 | + | registerProtocol(t, c, f) | |
| 71 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |
| 72 | + | buf := shmBuffer(t, c) | |
| 73 | + | drainContents(contents) | |
| 74 | + | ||
| 75 | + | // Vulkan FIFO: every frame sets a barrier and waits on the last one. | |
| 76 | + | // Headless outputs never flip: the barrier clears after one 60 Hz | |
| 77 | + | // refresh, so three frames take at least two refreshes. | |
| 78 | + | start := time.Now() | |
| 79 | + | for range 3 { | |
| 80 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |
| 81 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |
| 82 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 83 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 84 | + | } | |
| 85 | + | if err := c.Roundtrip(); err != nil { | |
| 86 | + | t.Fatal(err) | |
| 87 | + | } | |
| 88 | + | for range 3 { | |
| 89 | + | nextContent(t, contents, w.ID) | |
| 90 | + | } | |
| 91 | + | if d := time.Since(start); d < 2*defaultFramePeriod-2*time.Millisecond { | |
| 92 | + | t.Fatalf("three fifo frames in %v", d) | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func TestCommitTimingDelaysCommit(t *testing.T) { | |
| 97 | + | s, events, _, contents, dir := contentServer(t) | |
| 98 | + | c := protocolClient(t, s, dir) | |
| 99 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 100 | + | manager := bindProtocol(t, c, "wp_commit_timing_manager_v1") | |
| 101 | + | timer := c.AllocateID() | |
| 102 | + | registerProtocol(t, c, timer) | |
| 103 | + | requestProtocol(t, c, manager, committiming.WpCommitTimingManagerV1RequestGetTimer, timer, surf) | |
| 104 | + | buf := shmBuffer(t, c) | |
| 105 | + | drainContents(contents) | |
| 106 | + | ||
| 107 | + | var now unix.Timespec | |
| 108 | + | if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil { | |
| 109 | + | t.Fatal(err) | |
| 110 | + | } | |
| 111 | + | target := time.Duration(now.Nano()) + 100*time.Millisecond | |
| 112 | + | start := time.Now() | |
| 113 | + | sec := uint64(target / time.Second) | |
| 114 | + | requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(sec>>32), uint32(sec), uint32(target%time.Second)) | |
| 115 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 116 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 117 | + | if err := c.Roundtrip(); err != nil { | |
| 118 | + | t.Fatal(err) | |
| 119 | + | } | |
| 120 | + | // Applied one refresh before the target, so it shows at the target. | |
| 121 | + | at := nextContent(t, contents, w.ID).Sub(start) | |
| 122 | + | if at < 100*time.Millisecond-defaultFramePeriod-2*time.Millisecond || at > time.Second { | |
| 123 | + | t.Fatalf("timed commit applied after %v", at) | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // A second timestamp before the commit is an error. | |
| 127 | + | requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0)) | |
| 128 | + | requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0)) | |
| 129 | + | expectProtocolError(t, c, timer, uint32(committiming.WpCommitTimerV1ErrorTimestampExists)) | |
| 130 | + | } | |
| @@ -0,0 +1,130 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/committiming" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 11 | + | "github.com/bnema/wlturbo" | |||
| 12 | + | "golang.org/x/sys/unix" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // nextContent waits for a content of window id. | |||
| 16 | + | func nextContent(t *testing.T, contents <-chan ports.SurfaceContent, id ports.WindowID) time.Time { | |||
| 17 | + | t.Helper() | |||
| 18 | + | deadline := time.After(2 * time.Second) | |||
| 19 | + | for { | |||
| 20 | + | select { | |||
| 21 | + | case c := <-contents: | |||
| 22 | + | if c.ID == id && !c.Empty() { | |||
| 23 | + | return time.Now() | |||
| 24 | + | } | |||
| 25 | + | case <-deadline: | |||
| 26 | + | t.Fatal("no content") | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // shmBuffer creates a 1x1 wl_shm buffer. | |||
| 32 | + | func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 { | |||
| 33 | + | t.Helper() | |||
| 34 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 35 | + | registerProtocol(t, c, shm) | |||
| 36 | + | fd, err := unix.MemfdCreate("fifo-buffer", 0) | |||
| 37 | + | if err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | defer unix.Close(fd) | |||
| 41 | + | if err := unix.Ftruncate(fd, 4); err != nil { | |||
| 42 | + | t.Fatal(err) | |||
| 43 | + | } | |||
| 44 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |||
| 45 | + | registerProtocol(t, c, pool) | |||
| 46 | + | registerProtocol(t, c, buffer) | |||
| 47 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |||
| 48 | + | t.Fatal(err) | |||
| 49 | + | } | |||
| 50 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 51 | + | return buffer | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | func drainContents(contents <-chan ports.SurfaceContent) { | |||
| 55 | + | for { | |||
| 56 | + | select { | |||
| 57 | + | case <-contents: | |||
| 58 | + | case <-time.After(20 * time.Millisecond): | |||
| 59 | + | return | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func TestFifoBarrierHoldsCommitOneRefresh(t *testing.T) { | |||
| 65 | + | s, events, _, contents, dir := contentServer(t) | |||
| 66 | + | c := protocolClient(t, s, dir) | |||
| 67 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 68 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |||
| 69 | + | f := c.AllocateID() | |||
| 70 | + | registerProtocol(t, c, f) | |||
| 71 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |||
| 72 | + | buf := shmBuffer(t, c) | |||
| 73 | + | drainContents(contents) | |||
| 74 | + | ||||
| 75 | + | // Vulkan FIFO: every frame sets a barrier and waits on the last one. | |||
| 76 | + | // Headless outputs never flip: the barrier clears after one 60 Hz | |||
| 77 | + | // refresh, so three frames take at least two refreshes. | |||
| 78 | + | start := time.Now() | |||
| 79 | + | for range 3 { | |||
| 80 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |||
| 81 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |||
| 82 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 83 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 84 | + | } | |||
| 85 | + | if err := c.Roundtrip(); err != nil { | |||
| 86 | + | t.Fatal(err) | |||
| 87 | + | } | |||
| 88 | + | for range 3 { | |||
| 89 | + | nextContent(t, contents, w.ID) | |||
| 90 | + | } | |||
| 91 | + | if d := time.Since(start); d < 2*defaultFramePeriod-2*time.Millisecond { | |||
| 92 | + | t.Fatalf("three fifo frames in %v", d) | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func TestCommitTimingDelaysCommit(t *testing.T) { | |||
| 97 | + | s, events, _, contents, dir := contentServer(t) | |||
| 98 | + | c := protocolClient(t, s, dir) | |||
| 99 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 100 | + | manager := bindProtocol(t, c, "wp_commit_timing_manager_v1") | |||
| 101 | + | timer := c.AllocateID() | |||
| 102 | + | registerProtocol(t, c, timer) | |||
| 103 | + | requestProtocol(t, c, manager, committiming.WpCommitTimingManagerV1RequestGetTimer, timer, surf) | |||
| 104 | + | buf := shmBuffer(t, c) | |||
| 105 | + | drainContents(contents) | |||
| 106 | + | ||||
| 107 | + | var now unix.Timespec | |||
| 108 | + | if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil { | |||
| 109 | + | t.Fatal(err) | |||
| 110 | + | } | |||
| 111 | + | target := time.Duration(now.Nano()) + 100*time.Millisecond | |||
| 112 | + | start := time.Now() | |||
| 113 | + | sec := uint64(target / time.Second) | |||
| 114 | + | requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(sec>>32), uint32(sec), uint32(target%time.Second)) | |||
| 115 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 116 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 117 | + | if err := c.Roundtrip(); err != nil { | |||
| 118 | + | t.Fatal(err) | |||
| 119 | + | } | |||
| 120 | + | // Applied one refresh before the target, so it shows at the target. | |||
| 121 | + | at := nextContent(t, contents, w.ID).Sub(start) | |||
| 122 | + | if at < 100*time.Millisecond-defaultFramePeriod-2*time.Millisecond || at > time.Second { | |||
| 123 | + | t.Fatalf("timed commit applied after %v", at) | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | // A second timestamp before the commit is an error. | |||
| 127 | + | requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0)) | |||
| 128 | + | requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0)) | |||
| 129 | + | expectProtocolError(t, c, timer, uint32(committiming.WpCommitTimerV1ErrorTimestampExists)) | |||
| 130 | + | } | |||
M internal/adapters/wayland/frames.go +4 -0
| @@ -63,6 +63,10 @@ func (s *Server) pace(ctx context.Context) { | |||
| 63 | 63 | // Held buffers wait for a flip or heldTimeout. | |
| 64 | 64 | wait, idle = heldTimeout, false | |
| 65 | 65 | } | |
| 66 | + | // Fifo barriers and queued commits wait for a refresh. | |
| 67 | + | if fw, waiting := s.tickFifo(time.Now(), flipped); waiting { | |
| 68 | + | wait, idle = min(wait, fw), false | |
| 69 | + | } | |
| 66 | 70 | } | |
| 67 | 71 | }) { | |
| 68 | 72 | return | |
| @@ -63,6 +63,10 @@ func (s *Server) pace(ctx context.Context) { | |||||
| 63 | // Held buffers wait for a flip or heldTimeout. | 63 | // Held buffers wait for a flip or heldTimeout. | ||
| 64 | wait, idle = heldTimeout, false | 64 | wait, idle = heldTimeout, false | ||
| 65 | } | 65 | } | ||
| 66 | + | // Fifo barriers and queued commits wait for a refresh. | |||
| 67 | + | if fw, waiting := s.tickFifo(time.Now(), flipped); waiting { | |||
| 68 | + | wait, idle = min(wait, fw), false | |||
| 69 | + | } | |||
| 66 | } | 70 | } | ||
| 67 | }) { | 71 | }) { | ||
| 68 | return | 72 | return | ||
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 registerPresentation(d, s) }, | |
| 21 | 22 | func() error { | |
| 22 | 23 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | |
| 23 | 24 | }, | |
| @@ -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 registerPresentation(d, s) }, | |||
| 21 | func() error { | 22 | func() error { | ||
| 22 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | 23 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | ||
| 23 | }, | 24 | }, | ||
M internal/adapters/wayland/server.go +13 -8
| @@ -54,14 +54,18 @@ type Server struct { | |||
| 54 | 54 | frameReady chan struct{} | |
| 55 | 55 | // held buffers wait until no output reads them (release.go); reports | |
| 56 | 56 | // are the outputs' latest ports.OutputPresented. | |
| 57 | - | held []heldBuffer | |
| 58 | - | reports map[string]ports.OutputPresented | |
| 59 | - | frames uint64 | |
| 60 | - | started time.Time | |
| 61 | - | surfaces map[*server.Resource]*surface | |
| 62 | - | buffers map[*server.Resource]clientBuffer | |
| 63 | - | dmabuf *dmabufGlobal | |
| 64 | - | serial uint32 | |
| 57 | + | held []heldBuffer | |
| 58 | + | reports map[string]ports.OutputPresented | |
| 59 | + | frames uint64 | |
| 60 | + | started time.Time | |
| 61 | + | // fifoSurfaces have a fifo barrier or queued commits (fifo.go); | |
| 62 | + | // lastFlip is each output's latest page flip. | |
| 63 | + | fifoSurfaces map[*surface]struct{} | |
| 64 | + | lastFlip map[string]time.Time | |
| 65 | + | surfaces map[*server.Resource]*surface | |
| 66 | + | buffers map[*server.Resource]clientBuffer | |
| 67 | + | dmabuf *dmabufGlobal | |
| 68 | + | serial uint32 | |
| 65 | 69 | // press is the serial of the last button or key press, sent to | |
| 66 | 70 | // pressClient: popup grabs must come from it. | |
| 67 | 71 | press uint32 | |
| @@ -149,6 +153,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 149 | 153 | } | |
| 150 | 154 | 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), 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} | |
| 151 | 155 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 156 | + | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | |
| 152 | 157 | if opts.Keymap != "" { | |
| 153 | 158 | fd, size, e := keymapFile(opts.Keymap) | |
| 154 | 159 | if e != nil { | |
| @@ -54,14 +54,18 @@ type Server struct { | |||||
| 54 | frameReady chan struct{} | 54 | frameReady chan struct{} | ||
| 55 | // held buffers wait until no output reads them (release.go); reports | 55 | // held buffers wait until no output reads them (release.go); reports | ||
| 56 | // are the outputs' latest ports.OutputPresented. | 56 | // are the outputs' latest ports.OutputPresented. | ||
| 57 | - | held []heldBuffer | 57 | + | held []heldBuffer |
| 58 | - | reports map[string]ports.OutputPresented | 58 | + | reports map[string]ports.OutputPresented |
| 59 | - | frames uint64 | 59 | + | frames uint64 |
| 60 | - | started time.Time | 60 | + | started time.Time |
| 61 | - | surfaces map[*server.Resource]*surface | 61 | + | // fifoSurfaces have a fifo barrier or queued commits (fifo.go); |
| 62 | - | buffers map[*server.Resource]clientBuffer | 62 | + | // lastFlip is each output's latest page flip. |
| 63 | - | dmabuf *dmabufGlobal | 63 | + | fifoSurfaces map[*surface]struct{} |
| 64 | - | serial uint32 | 64 | + | lastFlip map[string]time.Time |
| 65 | + | surfaces map[*server.Resource]*surface | |||
| 66 | + | buffers map[*server.Resource]clientBuffer | |||
| 67 | + | dmabuf *dmabufGlobal | |||
| 68 | + | serial uint32 | |||
| 65 | // press is the serial of the last button or key press, sent to | 69 | // press is the serial of the last button or key press, sent to | ||
| 66 | // pressClient: popup grabs must come from it. | 70 | // pressClient: popup grabs must come from it. | ||
| 67 | press uint32 | 71 | press uint32 | ||
| @@ -149,6 +153,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 149 | } | 153 | } | ||
| 150 | 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), 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} | 154 | 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), 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} | ||
| 151 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 155 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 156 | + | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | |||
| 152 | if opts.Keymap != "" { | 157 | if opts.Keymap != "" { | ||
| 153 | fd, size, e := keymapFile(opts.Keymap) | 158 | fd, size, e := keymapFile(opts.Keymap) | ||
| 154 | if e != nil { | 159 | if e != nil { | ||
M internal/adapters/wayland/surface.go +35 -1
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 4 | 6 | "github.com/bnema/nefertty/internal/ports" | |
| 5 | 7 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | 8 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| @@ -46,6 +48,18 @@ type surface struct { | |||
| 46 | 48 | // committed hint, pendingAsync the requested one. | |
| 47 | 49 | tearing *tearingHandler | |
| 48 | 50 | async, pendingAsync bool | |
| 51 | + | // Presentation constraints (fifo.go): pending fifo requests and commit | |
| 52 | + | // timestamp, the barrier, and the commits waiting to apply. | |
| 53 | + | fifo *fifoHandler | |
| 54 | + | timer *timerHandler | |
| 55 | + | pendingBarrier, pendingWait bool | |
| 56 | + | pendingTime time.Time | |
| 57 | + | barrier bool | |
| 58 | + | barrierAt time.Time | |
| 59 | + | queue []update | |
| 60 | + | // contentType is the wp_content_type_v1; contentKind the committed type. | |
| 61 | + | contentType *contentTypeHandler | |
| 62 | + | contentKind, pendingKind uint32 | |
| 49 | 63 | } | |
| 50 | 64 | ||
| 51 | 65 | // subState is the subsurface tree: parent is set on subsurfaces, children | |
| @@ -141,6 +155,7 @@ func (s *surface) detach() { | |||
| 141 | 155 | func (s *surface) Destroy(*wayland.Surface) { | |
| 142 | 156 | s.destroyed = true | |
| 143 | 157 | s.tearing = nil // the control becomes inert | |
| 158 | + | s.dropQueue() | |
| 144 | 159 | if s.server.cursorSurface == s { | |
| 145 | 160 | // The pointer keeps no cursor until the client sets another. | |
| 146 | 161 | s.server.cursorSurface = nil | |
| @@ -178,7 +193,6 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) { | |||
| 178 | 193 | } | |
| 179 | 194 | } | |
| 180 | 195 | func (s *surface) Commit(*wayland.Surface) { | |
| 181 | - | fresh := s.attached && s.pending != nil | |
| 182 | 196 | if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked { | |
| 183 | 197 | s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack") | |
| 184 | 198 | return | |
| @@ -187,6 +201,26 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 187 | 201 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | |
| 188 | 202 | return | |
| 189 | 203 | } | |
| 204 | + | // A commit behind a fifo barrier or a future timestamp waits (fifo.go). | |
| 205 | + | if s.mustWait(time.Now()) { | |
| 206 | + | s.queueUpdate() | |
| 207 | + | return | |
| 208 | + | } | |
| 209 | + | s.applyCommit() | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | // applyCommit makes the pending state current. | |
| 213 | + | func (s *surface) applyCommit() { | |
| 214 | + | fresh := s.attached && s.pending != nil | |
| 215 | + | if s.pendingBarrier { | |
| 216 | + | s.pendingBarrier = false | |
| 217 | + | s.setBarrier(time.Now()) | |
| 218 | + | } | |
| 219 | + | s.pendingWait, s.pendingTime = false, time.Time{} | |
| 220 | + | if s.contentKind != s.pendingKind { | |
| 221 | + | s.contentKind = s.pendingKind | |
| 222 | + | s.server.log.Info().Uint64("id", uint64(s.root().windowID())).Uint32("content_type", s.contentKind).Msg("content type") | |
| 223 | + | } | |
| 190 | 224 | if s.pendingScale > 0 { | |
| 191 | 225 | s.bufferScale = s.pendingScale | |
| 192 | 226 | } | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "time" | |||
| 5 | + | ||||
| 4 | "github.com/bnema/nefertty/internal/ports" | 6 | "github.com/bnema/nefertty/internal/ports" | ||
| 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | 7 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 6 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | 8 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | ||
| @@ -46,6 +48,18 @@ type surface struct { | |||||
| 46 | // committed hint, pendingAsync the requested one. | 48 | // committed hint, pendingAsync the requested one. | ||
| 47 | tearing *tearingHandler | 49 | tearing *tearingHandler | ||
| 48 | async, pendingAsync bool | 50 | async, pendingAsync bool | ||
| 51 | + | // Presentation constraints (fifo.go): pending fifo requests and commit | |||
| 52 | + | // timestamp, the barrier, and the commits waiting to apply. | |||
| 53 | + | fifo *fifoHandler | |||
| 54 | + | timer *timerHandler | |||
| 55 | + | pendingBarrier, pendingWait bool | |||
| 56 | + | pendingTime time.Time | |||
| 57 | + | barrier bool | |||
| 58 | + | barrierAt time.Time | |||
| 59 | + | queue []update | |||
| 60 | + | // contentType is the wp_content_type_v1; contentKind the committed type. | |||
| 61 | + | contentType *contentTypeHandler | |||
| 62 | + | contentKind, pendingKind uint32 | |||
| 49 | } | 63 | } | ||
| 50 | 64 | ||||
| 51 | // subState is the subsurface tree: parent is set on subsurfaces, children | 65 | // subState is the subsurface tree: parent is set on subsurfaces, children | ||
| @@ -141,6 +155,7 @@ func (s *surface) detach() { | |||||
| 141 | func (s *surface) Destroy(*wayland.Surface) { | 155 | func (s *surface) Destroy(*wayland.Surface) { | ||
| 142 | s.destroyed = true | 156 | s.destroyed = true | ||
| 143 | s.tearing = nil // the control becomes inert | 157 | s.tearing = nil // the control becomes inert | ||
| 158 | + | s.dropQueue() | |||
| 144 | if s.server.cursorSurface == s { | 159 | if s.server.cursorSurface == s { | ||
| 145 | // The pointer keeps no cursor until the client sets another. | 160 | // The pointer keeps no cursor until the client sets another. | ||
| 146 | s.server.cursorSurface = nil | 161 | s.server.cursorSurface = nil | ||
| @@ -178,7 +193,6 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) { | |||||
| 178 | } | 193 | } | ||
| 179 | } | 194 | } | ||
| 180 | func (s *surface) Commit(*wayland.Surface) { | 195 | func (s *surface) Commit(*wayland.Surface) { | ||
| 181 | - | fresh := s.attached && s.pending != nil | |||
| 182 | if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked { | 196 | if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked { | ||
| 183 | s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack") | 197 | s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack") | ||
| 184 | return | 198 | return | ||
| @@ -187,6 +201,26 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 187 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | 201 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | ||
| 188 | return | 202 | return | ||
| 189 | } | 203 | } | ||
| 204 | + | // A commit behind a fifo barrier or a future timestamp waits (fifo.go). | |||
| 205 | + | if s.mustWait(time.Now()) { | |||
| 206 | + | s.queueUpdate() | |||
| 207 | + | return | |||
| 208 | + | } | |||
| 209 | + | s.applyCommit() | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | // applyCommit makes the pending state current. | |||
| 213 | + | func (s *surface) applyCommit() { | |||
| 214 | + | fresh := s.attached && s.pending != nil | |||
| 215 | + | if s.pendingBarrier { | |||
| 216 | + | s.pendingBarrier = false | |||
| 217 | + | s.setBarrier(time.Now()) | |||
| 218 | + | } | |||
| 219 | + | s.pendingWait, s.pendingTime = false, time.Time{} | |||
| 220 | + | if s.contentKind != s.pendingKind { | |||
| 221 | + | s.contentKind = s.pendingKind | |||
| 222 | + | s.server.log.Info().Uint64("id", uint64(s.root().windowID())).Uint32("content_type", s.contentKind).Msg("content type") | |||
| 223 | + | } | |||
| 190 | if s.pendingScale > 0 { | 224 | if s.pendingScale > 0 { | ||
| 191 | s.bufferScale = s.pendingScale | 225 | s.bufferScale = s.pendingScale | ||
| 192 | } | 226 | } | ||