detached d5fa86c

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
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/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 }

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 26

Tags 10

↑↓ moveenter openesc close