detached 856d644

merge: fifo (fifo-v1, commit-timing-v1, content-type-v1 with a commit queue)

commit
856d644a373a14be422672995e8f31f590f15106
author
brice · · 2026-09-25 19:18 UTC
merge of
07a1604 + c3cdeb4 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +679 -9
A internal/adapters/wayland/fifo.go +415 -0
@@ -0,0 +1,415 @@
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(uint64(secHi)<<32|uint64(secLo), int64(nsec))
126+ }
127+}
128+func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
129+
130+// maxTimestampAhead bounds how far a commit timestamp can hold a commit;
131+// a later one applies early (a deviation from "not before").
132+const maxTimestampAhead = time.Second
133+
134+// monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most
135+// maxTimestampAhead from now: a far timestamp must not stall the surface.
136+func monotonicTime(sec uint64, nsec int64) time.Time {
137+ var now unix.Timespec
138+ if unix.ClockGettime(unix.CLOCK_MONOTONIC, &now) != nil {
139+ return time.Now()
140+ }
141+ ahead := time.Duration(nsec - now.Nsec)
142+ switch n := uint64(now.Sec); {
143+ case sec >= n+2:
144+ ahead = maxTimestampAhead
145+ case sec+2 <= n:
146+ ahead = 0 // in the past: apply now
147+ default:
148+ ahead += time.Duration(int64(sec)-now.Sec) * time.Second
149+ }
150+ return time.Now().Add(min(max(ahead, 0), maxTimestampAhead))
151+}
152+
153+// content_type_v1
154+
155+type contentTypeManager struct{ server *Server }
156+
157+func (contentTypeManager) Destroy(*contenttype.WpContentTypeManagerV1) {}
158+
159+func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeManagerV1, id uint32, surf *wayland.Surface) {
160+ state := m.server.surfaceOf(surf)
161+ if state != nil && state.contentType != nil {
162+ r.PostError(uint32(contenttype.WpContentTypeManagerV1ErrorAlreadyConstructed), "surface already has a content type")
163+ return
164+ }
165+ h := &contentTypeHandler{surface: state}
166+ res, err := contenttype.NewWpContentTypeV1(r.Client(), r.Version(), id, h)
167+ if err != nil || state == nil {
168+ return
169+ }
170+ state.contentType = h
171+ res.OnDestroy = func() {
172+ // Destroying it sets the type back to none, double-buffered.
173+ if state.contentType == h {
174+ state.contentType = nil
175+ state.pendingKind = 0
176+ }
177+ }
178+}
179+
180+type contentTypeHandler struct{ surface *surface }
181+
182+func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) {
183+ if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h {
184+ h.surface.pendingKind = kind
185+ }
186+}
187+func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {}
188+
189+func (s *Server) surfaceOf(w *wayland.Surface) *surface {
190+ if w == nil {
191+ return nil
192+ }
193+ return s.surfaces[w.Resource]
194+}
195+
196+// Content update queue
197+
198+// update is a surface's pending state taken out by a commit that waits.
199+type update struct {
200+ attached bool
201+ buffer *wayland.Buffer
202+ scale int
203+ async bool
204+ kind uint32
205+ callbacks []*wayland.Callback
206+ vp *viewport
207+ vpW, vpH int32
208+ vpSet bool
209+ moves []childMove
210+ xdg *xdgSurface
211+ geometry ports.Rect
212+ cons *constraint
213+ region *ports.Rect
214+ layer *layerSurface
215+ layerNext layerState
216+ barrier bool
217+ wait bool
218+ at time.Time
219+}
220+
221+type childMove struct {
222+ child *surface
223+ x, y int
224+}
225+
226+// takePending moves the pending state into an update: one-shot state is
227+// cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
228+func (s *surface) takePending() update {
229+ 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}
230+ s.attached, s.pending, s.callbacks = false, nil, nil
231+ s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
232+ if v := s.viewport; v != nil {
233+ u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet
234+ }
235+ for _, ch := range s.sub.children {
236+ if ch.sub.moved {
237+ u.moves = append(u.moves, childMove{ch, ch.sub.pendX, ch.sub.pendY})
238+ ch.sub.moved = false
239+ }
240+ }
241+ if s.xdg != nil {
242+ u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry
243+ }
244+ if c := s.server.constraints[s]; c != nil && c.pending != nil {
245+ u.cons, u.region, c.pending = c, c.pending, nil
246+ }
247+ if s.layer != nil {
248+ u.layer, u.layerNext = s.layer, s.layer.pending
249+ }
250+ return u
251+}
252+
253+// putPending makes an update the surface's pending state again. Parts
254+// whose object is gone (viewport, child, role, constraint) are dropped.
255+func (s *surface) putPending(u update) {
256+ s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
257+ s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
258+ s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
259+ if u.vp != nil && s.viewport == u.vp {
260+ u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
261+ }
262+ for _, m := range u.moves {
263+ if m.child.sub.parent == s {
264+ m.child.sub.pendX, m.child.sub.pendY, m.child.sub.moved = m.x, m.y, true
265+ }
266+ }
267+ if u.xdg != nil && s.xdg == u.xdg {
268+ u.xdg.pendingGeometry = u.geometry
269+ }
270+ if u.cons != nil && s.server.constraints[s] == u.cons {
271+ u.cons.pending = u.region
272+ }
273+ if u.layer != nil && s.layer == u.layer {
274+ u.layer.pending = u.layerNext
275+ }
276+}
277+
278+// mustWait reports whether the pending commit cannot apply now.
279+func (s *surface) mustWait(now time.Time) bool {
280+ return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now)
281+}
282+
283+// tooEarly reports whether content applied now would show before at:
284+// it reaches the screen one refresh later.
285+func (s *surface) tooEarly(at, now time.Time) bool {
286+ return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s))))
287+}
288+
289+// queueUpdate queues the pending commit until it can apply.
290+func (s *surface) queueUpdate() {
291+ s.queue = append(s.queue, s.takePending())
292+ s.server.fifoSurfaces[s] = struct{}{}
293+ s.server.wakePacer()
294+}
295+
296+// applyUpdate applies a queued update, keeping the requests made since.
297+func (s *surface) applyUpdate(u update) {
298+ // A buffer destroyed while queued (a swapchain resize) is skipped: the
299+ // surface keeps its content, and is not unmapped behind the client.
300+ if u.buffer != nil && !u.buffer.Resource.Alive() {
301+ u.buffer, u.attached = nil, false
302+ }
303+ later := s.takePending()
304+ s.putPending(u)
305+ s.applyCommit()
306+ s.putPending(later)
307+}
308+
309+// dropQueue discards the queued updates of a destroyed surface or role.
310+// A live surface still gets its queued frame callbacks.
311+func (s *surface) dropQueue() {
312+ released := map[*server.Resource]bool{}
313+ for _, u := range s.queue {
314+ if s.destroyed {
315+ for _, cb := range u.callbacks {
316+ cb.Destroy()
317+ }
318+ } else if len(u.callbacks) > 0 {
319+ s.server.queueFrames(s.server.frameOutput(s), u.callbacks)
320+ }
321+ if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
322+ released[u.buffer.Resource] = true
323+ u.buffer.SendRelease()
324+ }
325+ }
326+ s.queue = nil
327+ delete(s.server.fifoSurfaces, s)
328+}
329+
330+// setBarrier applies a committed set_barrier.
331+func (s *surface) setBarrier(now time.Time) {
332+ s.barrier, s.barrierAt = true, now
333+ s.server.fifoSurfaces[s] = struct{}{}
334+ s.server.wakePacer()
335+}
336+
337+// fifoOutput is the output pacing a surface, "" when none does.
338+func (s *Server) fifoOutput(surf *surface) string {
339+ name := s.frameOutput(surf)
340+ if name != "" && s.outputByNameExact(name) == nil {
341+ return ""
342+ }
343+ return name
344+}
345+
346+// outputPeriod is one refresh of an output ("": 60 Hz).
347+func (s *Server) outputPeriod(name string) time.Duration {
348+ if o := s.outputByNameExact(name); o != nil && name != "" {
349+ return framePeriod(o.place.Info.RefreshMilli)
350+ }
351+ return defaultFramePeriod
352+}
353+
354+// tickFifo clears the barriers whose latching deadline passed and applies
355+// the queued updates now ready. A flip of the surface's output is its
356+// deadline; outputs that do not flip (headless, switched away, no output)
357+// clear it after one refresh, or 1.5 when they usually flip. It returns
358+// the time to the next deadline and whether any surface still waits.
359+func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) {
360+ for name, f := range flipped {
361+ if f {
362+ s.lastFlip[name] = now
363+ }
364+ }
365+ for name := range s.lastFlip {
366+ if s.outputByNameExact(name) == nil {
367+ delete(s.lastFlip, name)
368+ }
369+ }
370+ wait := defaultFramePeriod
371+ for surf := range s.fifoSurfaces {
372+ name := s.fifoOutput(surf)
373+ p := s.outputPeriod(name)
374+ // The deadline of outputs that usually flip only catches a missed flip.
375+ deadline := surf.barrierAt.Add(p)
376+ if name != "" && now.Sub(s.lastFlip[name]) < time.Second {
377+ deadline = deadline.Add(p / 2)
378+ }
379+ if surf.barrier && ((flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline)) {
380+ surf.barrier = false
381+ }
382+ for len(surf.queue) > 0 && !surf.destroyed {
383+ u := surf.queue[0]
384+ if u.wait && surf.barrier {
385+ break
386+ }
387+ if surf.tooEarly(u.at, now) {
388+ wait = min(wait, u.at.Add(-p).Sub(now))
389+ break
390+ }
391+ surf.queue = surf.queue[1:]
392+ surf.applyUpdate(u)
393+ }
394+ if surf.barrier {
395+ wait = min(wait, deadline.Sub(now))
396+ }
397+ if !surf.barrier && len(surf.queue) == 0 {
398+ delete(s.fifoSurfaces, surf)
399+ }
400+ }
401+ return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0
402+}
403+
404+// sameBuffer reports whether two wrappers are the same wl_buffer.
405+func sameBuffer(a, b *wayland.Buffer) bool {
406+ return a != nil && b != nil && a.Resource == b.Resource
407+}
408+
409+// wakePacer makes the pacer look at its queues now.
410+func (s *Server) wakePacer() {
411+ select {
412+ case s.frameReady <- struct{}{}:
413+ default:
414+ }
415+}
@@ -0,0 +1,415 @@
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(uint64(secHi)<<32|uint64(secLo), int64(nsec))
126+ }
127+}
128+func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
129+
130+// maxTimestampAhead bounds how far a commit timestamp can hold a commit;
131+// a later one applies early (a deviation from "not before").
132+const maxTimestampAhead = time.Second
133+
134+// monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most
135+// maxTimestampAhead from now: a far timestamp must not stall the surface.
136+func monotonicTime(sec uint64, nsec int64) time.Time {
137+ var now unix.Timespec
138+ if unix.ClockGettime(unix.CLOCK_MONOTONIC, &now) != nil {
139+ return time.Now()
140+ }
141+ ahead := time.Duration(nsec - now.Nsec)
142+ switch n := uint64(now.Sec); {
143+ case sec >= n+2:
144+ ahead = maxTimestampAhead
145+ case sec+2 <= n:
146+ ahead = 0 // in the past: apply now
147+ default:
148+ ahead += time.Duration(int64(sec)-now.Sec) * time.Second
149+ }
150+ return time.Now().Add(min(max(ahead, 0), maxTimestampAhead))
151+}
152+
153+// content_type_v1
154+
155+type contentTypeManager struct{ server *Server }
156+
157+func (contentTypeManager) Destroy(*contenttype.WpContentTypeManagerV1) {}
158+
159+func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeManagerV1, id uint32, surf *wayland.Surface) {
160+ state := m.server.surfaceOf(surf)
161+ if state != nil && state.contentType != nil {
162+ r.PostError(uint32(contenttype.WpContentTypeManagerV1ErrorAlreadyConstructed), "surface already has a content type")
163+ return
164+ }
165+ h := &contentTypeHandler{surface: state}
166+ res, err := contenttype.NewWpContentTypeV1(r.Client(), r.Version(), id, h)
167+ if err != nil || state == nil {
168+ return
169+ }
170+ state.contentType = h
171+ res.OnDestroy = func() {
172+ // Destroying it sets the type back to none, double-buffered.
173+ if state.contentType == h {
174+ state.contentType = nil
175+ state.pendingKind = 0
176+ }
177+ }
178+}
179+
180+type contentTypeHandler struct{ surface *surface }
181+
182+func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) {
183+ if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h {
184+ h.surface.pendingKind = kind
185+ }
186+}
187+func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {}
188+
189+func (s *Server) surfaceOf(w *wayland.Surface) *surface {
190+ if w == nil {
191+ return nil
192+ }
193+ return s.surfaces[w.Resource]
194+}
195+
196+// Content update queue
197+
198+// update is a surface's pending state taken out by a commit that waits.
199+type update struct {
200+ attached bool
201+ buffer *wayland.Buffer
202+ scale int
203+ async bool
204+ kind uint32
205+ callbacks []*wayland.Callback
206+ vp *viewport
207+ vpW, vpH int32
208+ vpSet bool
209+ moves []childMove
210+ xdg *xdgSurface
211+ geometry ports.Rect
212+ cons *constraint
213+ region *ports.Rect
214+ layer *layerSurface
215+ layerNext layerState
216+ barrier bool
217+ wait bool
218+ at time.Time
219+}
220+
221+type childMove struct {
222+ child *surface
223+ x, y int
224+}
225+
226+// takePending moves the pending state into an update: one-shot state is
227+// cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
228+func (s *surface) takePending() update {
229+ 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}
230+ s.attached, s.pending, s.callbacks = false, nil, nil
231+ s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
232+ if v := s.viewport; v != nil {
233+ u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet
234+ }
235+ for _, ch := range s.sub.children {
236+ if ch.sub.moved {
237+ u.moves = append(u.moves, childMove{ch, ch.sub.pendX, ch.sub.pendY})
238+ ch.sub.moved = false
239+ }
240+ }
241+ if s.xdg != nil {
242+ u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry
243+ }
244+ if c := s.server.constraints[s]; c != nil && c.pending != nil {
245+ u.cons, u.region, c.pending = c, c.pending, nil
246+ }
247+ if s.layer != nil {
248+ u.layer, u.layerNext = s.layer, s.layer.pending
249+ }
250+ return u
251+}
252+
253+// putPending makes an update the surface's pending state again. Parts
254+// whose object is gone (viewport, child, role, constraint) are dropped.
255+func (s *surface) putPending(u update) {
256+ s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
257+ s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
258+ s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
259+ if u.vp != nil && s.viewport == u.vp {
260+ u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
261+ }
262+ for _, m := range u.moves {
263+ if m.child.sub.parent == s {
264+ m.child.sub.pendX, m.child.sub.pendY, m.child.sub.moved = m.x, m.y, true
265+ }
266+ }
267+ if u.xdg != nil && s.xdg == u.xdg {
268+ u.xdg.pendingGeometry = u.geometry
269+ }
270+ if u.cons != nil && s.server.constraints[s] == u.cons {
271+ u.cons.pending = u.region
272+ }
273+ if u.layer != nil && s.layer == u.layer {
274+ u.layer.pending = u.layerNext
275+ }
276+}
277+
278+// mustWait reports whether the pending commit cannot apply now.
279+func (s *surface) mustWait(now time.Time) bool {
280+ return len(s.queue) > 0 || (s.pendingWait && s.barrier) || s.tooEarly(s.pendingTime, now)
281+}
282+
283+// tooEarly reports whether content applied now would show before at:
284+// it reaches the screen one refresh later.
285+func (s *surface) tooEarly(at, now time.Time) bool {
286+ return !at.IsZero() && at.After(now.Add(s.server.outputPeriod(s.server.fifoOutput(s))))
287+}
288+
289+// queueUpdate queues the pending commit until it can apply.
290+func (s *surface) queueUpdate() {
291+ s.queue = append(s.queue, s.takePending())
292+ s.server.fifoSurfaces[s] = struct{}{}
293+ s.server.wakePacer()
294+}
295+
296+// applyUpdate applies a queued update, keeping the requests made since.
297+func (s *surface) applyUpdate(u update) {
298+ // A buffer destroyed while queued (a swapchain resize) is skipped: the
299+ // surface keeps its content, and is not unmapped behind the client.
300+ if u.buffer != nil && !u.buffer.Resource.Alive() {
301+ u.buffer, u.attached = nil, false
302+ }
303+ later := s.takePending()
304+ s.putPending(u)
305+ s.applyCommit()
306+ s.putPending(later)
307+}
308+
309+// dropQueue discards the queued updates of a destroyed surface or role.
310+// A live surface still gets its queued frame callbacks.
311+func (s *surface) dropQueue() {
312+ released := map[*server.Resource]bool{}
313+ for _, u := range s.queue {
314+ if s.destroyed {
315+ for _, cb := range u.callbacks {
316+ cb.Destroy()
317+ }
318+ } else if len(u.callbacks) > 0 {
319+ s.server.queueFrames(s.server.frameOutput(s), u.callbacks)
320+ }
321+ if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() {
322+ released[u.buffer.Resource] = true
323+ u.buffer.SendRelease()
324+ }
325+ }
326+ s.queue = nil
327+ delete(s.server.fifoSurfaces, s)
328+}
329+
330+// setBarrier applies a committed set_barrier.
331+func (s *surface) setBarrier(now time.Time) {
332+ s.barrier, s.barrierAt = true, now
333+ s.server.fifoSurfaces[s] = struct{}{}
334+ s.server.wakePacer()
335+}
336+
337+// fifoOutput is the output pacing a surface, "" when none does.
338+func (s *Server) fifoOutput(surf *surface) string {
339+ name := s.frameOutput(surf)
340+ if name != "" && s.outputByNameExact(name) == nil {
341+ return ""
342+ }
343+ return name
344+}
345+
346+// outputPeriod is one refresh of an output ("": 60 Hz).
347+func (s *Server) outputPeriod(name string) time.Duration {
348+ if o := s.outputByNameExact(name); o != nil && name != "" {
349+ return framePeriod(o.place.Info.RefreshMilli)
350+ }
351+ return defaultFramePeriod
352+}
353+
354+// tickFifo clears the barriers whose latching deadline passed and applies
355+// the queued updates now ready. A flip of the surface's output is its
356+// deadline; outputs that do not flip (headless, switched away, no output)
357+// clear it after one refresh, or 1.5 when they usually flip. It returns
358+// the time to the next deadline and whether any surface still waits.
359+func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) {
360+ for name, f := range flipped {
361+ if f {
362+ s.lastFlip[name] = now
363+ }
364+ }
365+ for name := range s.lastFlip {
366+ if s.outputByNameExact(name) == nil {
367+ delete(s.lastFlip, name)
368+ }
369+ }
370+ wait := defaultFramePeriod
371+ for surf := range s.fifoSurfaces {
372+ name := s.fifoOutput(surf)
373+ p := s.outputPeriod(name)
374+ // The deadline of outputs that usually flip only catches a missed flip.
375+ deadline := surf.barrierAt.Add(p)
376+ if name != "" && now.Sub(s.lastFlip[name]) < time.Second {
377+ deadline = deadline.Add(p / 2)
378+ }
379+ if surf.barrier && ((flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline)) {
380+ surf.barrier = false
381+ }
382+ for len(surf.queue) > 0 && !surf.destroyed {
383+ u := surf.queue[0]
384+ if u.wait && surf.barrier {
385+ break
386+ }
387+ if surf.tooEarly(u.at, now) {
388+ wait = min(wait, u.at.Add(-p).Sub(now))
389+ break
390+ }
391+ surf.queue = surf.queue[1:]
392+ surf.applyUpdate(u)
393+ }
394+ if surf.barrier {
395+ wait = min(wait, deadline.Sub(now))
396+ }
397+ if !surf.barrier && len(surf.queue) == 0 {
398+ delete(s.fifoSurfaces, surf)
399+ }
400+ }
401+ return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0
402+}
403+
404+// sameBuffer reports whether two wrappers are the same wl_buffer.
405+func sameBuffer(a, b *wayland.Buffer) bool {
406+ return a != nil && b != nil && a.Resource == b.Resource
407+}
408+
409+// wakePacer makes the pacer look at its queues now.
410+func (s *Server) wakePacer() {
411+ select {
412+ case s.frameReady <- struct{}{}:
413+ default:
414+ }
415+}
A internal/adapters/wayland/fifo_test.go +208 -0
@@ -0,0 +1,208 @@
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+}
131+
132+// Far or past timestamps never hold a surface longer than a second.
133+func TestMonotonicTimeClamps(t *testing.T) {
134+ var now unix.Timespec
135+ if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
136+ t.Fatal(err)
137+ }
138+ for _, tc := range []struct {
139+ sec uint64
140+ nsec int64
141+ lo, hi time.Duration
142+ }{
143+ {^uint64(0), 0, maxTimestampAhead - 10*time.Millisecond, maxTimestampAhead},
144+ {0, 0, -10 * time.Millisecond, 10 * time.Millisecond},
145+ {uint64(now.Sec), now.Nsec, -10 * time.Millisecond, 10 * time.Millisecond},
146+ } {
147+ got := time.Until(monotonicTime(tc.sec, tc.nsec))
148+ if got < tc.lo || got > tc.hi {
149+ t.Errorf("sec %d: %v not in [%v, %v]", tc.sec, got, tc.lo, tc.hi)
150+ }
151+ }
152+}
153+
154+// A barrier on an output that flips clears on its next flip.
155+func TestTickFifoClearsBarrierOnFlip(t *testing.T) {
156+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
157+ s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)}
158+ surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}}
159+ now := time.Unix(100, 0)
160+ s.lastFlip["DP-2"] = now.Add(-time.Millisecond)
161+ surf.setBarrier(now)
162+ if _, waiting := s.tickFifo(now.Add(time.Millisecond), nil); !waiting || !surf.barrier {
163+ t.Fatal("barrier cleared before the flip")
164+ }
165+ // A flipping output waits up to 1.5 refreshes for a missed flip.
166+ if w, _ := s.tickFifo(now.Add(20*time.Millisecond), nil); !surf.barrier || w < 4*time.Millisecond {
167+ t.Fatalf("barrier %v wait %v", surf.barrier, w)
168+ }
169+ if _, waiting := s.tickFifo(now.Add(21*time.Millisecond), map[string]bool{"DP-2": true}); waiting || surf.barrier {
170+ t.Fatal("barrier kept after the flip")
171+ }
172+}
173+
174+// A queued commit whose buffer is destroyed (a swapchain resize) is skipped
175+// without unmapping the window.
176+func TestFifoQueuedBufferDestroyed(t *testing.T) {
177+ s, events, _, contents, dir := contentServer(t)
178+ c := protocolClient(t, s, dir)
179+ _, surf, _ := surfaceMapper(t, c, events)()
180+ manager := bindProtocol(t, c, "wp_fifo_manager_v1")
181+ f := c.AllocateID()
182+ registerProtocol(t, c, f)
183+ requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
184+ buf := shmBuffer(t, c)
185+ requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
186+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
187+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
188+ // Queued behind the barrier, then its buffer goes away.
189+ requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
191+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
192+ requestProtocol(t, c, buf, wayland.BufferRequestDestroy)
193+ if err := c.Roundtrip(); err != nil {
194+ t.Fatal(err)
195+ }
196+ drainContents(contents)
197+ time.Sleep(3 * defaultFramePeriod)
198+ for {
199+ select {
200+ case ev := <-events:
201+ if _, ok := ev.(ports.WindowUnmapped); ok {
202+ t.Fatal("window unmapped by a dead queued buffer")
203+ }
204+ default:
205+ return
206+ }
207+ }
208+}
@@ -0,0 +1,208 @@
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+}
131+
132+// Far or past timestamps never hold a surface longer than a second.
133+func TestMonotonicTimeClamps(t *testing.T) {
134+ var now unix.Timespec
135+ if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
136+ t.Fatal(err)
137+ }
138+ for _, tc := range []struct {
139+ sec uint64
140+ nsec int64
141+ lo, hi time.Duration
142+ }{
143+ {^uint64(0), 0, maxTimestampAhead - 10*time.Millisecond, maxTimestampAhead},
144+ {0, 0, -10 * time.Millisecond, 10 * time.Millisecond},
145+ {uint64(now.Sec), now.Nsec, -10 * time.Millisecond, 10 * time.Millisecond},
146+ } {
147+ got := time.Until(monotonicTime(tc.sec, tc.nsec))
148+ if got < tc.lo || got > tc.hi {
149+ t.Errorf("sec %d: %v not in [%v, %v]", tc.sec, got, tc.lo, tc.hi)
150+ }
151+ }
152+}
153+
154+// A barrier on an output that flips clears on its next flip.
155+func TestTickFifoClearsBarrierOnFlip(t *testing.T) {
156+ out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
157+ s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}, frameReady: make(chan struct{}, 1)}
158+ surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}}
159+ now := time.Unix(100, 0)
160+ s.lastFlip["DP-2"] = now.Add(-time.Millisecond)
161+ surf.setBarrier(now)
162+ if _, waiting := s.tickFifo(now.Add(time.Millisecond), nil); !waiting || !surf.barrier {
163+ t.Fatal("barrier cleared before the flip")
164+ }
165+ // A flipping output waits up to 1.5 refreshes for a missed flip.
166+ if w, _ := s.tickFifo(now.Add(20*time.Millisecond), nil); !surf.barrier || w < 4*time.Millisecond {
167+ t.Fatalf("barrier %v wait %v", surf.barrier, w)
168+ }
169+ if _, waiting := s.tickFifo(now.Add(21*time.Millisecond), map[string]bool{"DP-2": true}); waiting || surf.barrier {
170+ t.Fatal("barrier kept after the flip")
171+ }
172+}
173+
174+// A queued commit whose buffer is destroyed (a swapchain resize) is skipped
175+// without unmapping the window.
176+func TestFifoQueuedBufferDestroyed(t *testing.T) {
177+ s, events, _, contents, dir := contentServer(t)
178+ c := protocolClient(t, s, dir)
179+ _, surf, _ := surfaceMapper(t, c, events)()
180+ manager := bindProtocol(t, c, "wp_fifo_manager_v1")
181+ f := c.AllocateID()
182+ registerProtocol(t, c, f)
183+ requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
184+ buf := shmBuffer(t, c)
185+ requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
186+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
187+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
188+ // Queued behind the barrier, then its buffer goes away.
189+ requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
190+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
191+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
192+ requestProtocol(t, c, buf, wayland.BufferRequestDestroy)
193+ if err := c.Roundtrip(); err != nil {
194+ t.Fatal(err)
195+ }
196+ drainContents(contents)
197+ time.Sleep(3 * defaultFramePeriod)
198+ for {
199+ select {
200+ case ev := <-events:
201+ if _, ok := ev.(ports.WindowUnmapped); ok {
202+ t.Fatal("window unmapped by a dead queued buffer")
203+ }
204+ default:
205+ return
206+ }
207+ }
208+}
M internal/adapters/wayland/frames.go +4 -0
@@ -63,6 +63,10 @@ func (s *Server) pace(ctx context.Context) {
6363 // Held buffers wait for a flip or heldTimeout.
6464 wait, idle = heldTimeout, false
6565 }
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+ }
6670 }
6771 }) {
6872 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, false64 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 return72 return
M internal/adapters/wayland/globals.go +1 -0
@@ -18,6 +18,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
1818 func() error { return registerDMABuf(d, s, o.DMABuf) },
1919 func() error { return registerPointerConstraints(d, s) },
2020 func() error { return registerTearing(d, s) },
21+ func() error { return registerPresentation(d, s) },
2122 func() error {
2223 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
2324 },
@@ -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/layer.go +1 -0
@@ -76,6 +76,7 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
7676 delete(h.server.layers, l.id)
7777 state.layer = nil
7878 state.role = nil
79+ state.dropQueue()
7980 state.current, state.pending = nil, nil
8081 state.attached = false
8182 }
@@ -76,6 +76,7 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
76 delete(h.server.layers, l.id)76 delete(h.server.layers, l.id)
77 state.layer = nil77 state.layer = nil
78 state.role = nil78 state.role = nil
79+ state.dropQueue()
79 state.current, state.pending = nil, nil80 state.current, state.pending = nil, nil
80 state.attached = false81 state.attached = false
81 }82 }
M internal/adapters/wayland/popup.go +1 -0
@@ -75,6 +75,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
7575 }
7676 w.unmap()
7777 pp.done = true
78+ x.surface.dropQueue()
7879 x.surface.current, x.surface.pending = nil, nil
7980 x.surface.attached = false
8081 delete(s.windows, w.id)
@@ -75,6 +75,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
75 }75 }
76 w.unmap()76 w.unmap()
77 pp.done = true77 pp.done = true
78+ x.surface.dropQueue()
78 x.surface.current, x.surface.pending = nil, nil79 x.surface.current, x.surface.pending = nil, nil
79 x.surface.attached = false80 x.surface.attached = false
80 delete(s.windows, w.id)81 delete(s.windows, w.id)
M internal/adapters/wayland/server.go +13 -8
@@ -54,14 +54,18 @@ type Server struct {
5454 frameReady chan struct{}
5555 // held buffers wait until no output reads them (release.go); reports
5656 // 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
6569 // press is the serial of the last button or key press, sent to
6670 // pressClient: popup grabs must come from it.
6771 press uint32
@@ -149,6 +153,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
149153 }
150154 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}
151155 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
156+ s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
152157 if opts.Keymap != "" {
153158 fd, size, e := keymapFile(opts.Keymap)
154159 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); reports55 // 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 []heldBuffer57+ held []heldBuffer
58- reports map[string]ports.OutputPresented58+ reports map[string]ports.OutputPresented
59- frames uint6459+ frames uint64
60- started time.Time60+ started time.Time
61- surfaces map[*server.Resource]*surface61+ // fifoSurfaces have a fifo barrier or queued commits (fifo.go);
62- buffers map[*server.Resource]clientBuffer62+ // lastFlip is each output's latest page flip.
63- dmabuf *dmabufGlobal63+ fifoSurfaces map[*surface]struct{}
64- serial uint3264+ 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 to69 // 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 uint3271 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 @@
11 package wayland
22
33 import (
4+ "time"
5+
46 "github.com/bnema/nefertty/internal/ports"
57 "github.com/bnema/purego-libwayland/protocol/wayland"
68 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
@@ -46,6 +48,18 @@ type surface struct {
4648 // committed hint, pendingAsync the requested one.
4749 tearing *tearingHandler
4850 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
4963 }
5064
5165 // subState is the subsurface tree: parent is set on subsurfaces, children
@@ -141,6 +155,7 @@ func (s *surface) detach() {
141155 func (s *surface) Destroy(*wayland.Surface) {
142156 s.destroyed = true
143157 s.tearing = nil // the control becomes inert
158+ s.dropQueue()
144159 if s.server.cursorSurface == s {
145160 // The pointer keeps no cursor until the client sets another.
146161 s.server.cursorSurface = nil
@@ -178,7 +193,6 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) {
178193 }
179194 }
180195 func (s *surface) Commit(*wayland.Surface) {
181- fresh := s.attached && s.pending != nil
182196 if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked {
183197 s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")
184198 return
@@ -187,6 +201,26 @@ func (s *surface) Commit(*wayland.Surface) {
187201 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
188202 return
189203 }
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+ }
190224 if s.pendingScale > 0 {
191225 s.bufferScale = s.pendingScale
192226 }
@@ -1,6 +1,8 @@
1 package wayland1 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 *tearingHandler49 tearing *tearingHandler
48 async, pendingAsync bool50 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, children65 // 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 = true156 s.destroyed = true
143 s.tearing = nil // the control becomes inert157 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 = nil161 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 return198 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 return202 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.pendingScale225 s.bufferScale = s.pendingScale
192 }226 }
M internal/adapters/wayland/xdg.go +1 -0
@@ -222,6 +222,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
222222 }
223223 t.OnDestroy = func() {
224224 w.unmap()
225+ x.surface.dropQueue()
225226 x.surface.current, x.surface.pending = nil, nil
226227 x.surface.attached = false
227228 delete(x.server.windows, w.id)
@@ -222,6 +222,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
222 }222 }
223 t.OnDestroy = func() {223 t.OnDestroy = func() {
224 w.unmap()224 w.unmap()
225+ x.surface.dropQueue()
225 x.surface.current, x.surface.pending = nil, nil226 x.surface.current, x.surface.pending = nil, nil
226 x.surface.attached = false227 x.surface.attached = false
227 delete(x.server.windows, w.id)228 delete(x.server.windows, w.id)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close