1package wayland
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/wayland"
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// paceFixture is a server whose surface waits on an output whose next
12// frame is an hour away: a commit queues its callback and nothing fires.
13func paceFixture(timer bool) (*Server, *surface) {
14 out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
15 s := &Server{
16 outputs: []*output{out},
17 awaiting: map[string][]*wayland.Callback{},
18 frameDue: map[string]time.Time{"DP-2": time.Now().Add(time.Hour)},
19 fifoSurfaces: map[*surface]struct{}{},
20 lastFlip: map[string]time.Time{},
21 flips: map[string]ports.FlipInfo{},
22 reports: map[string]ports.OutputPresented{},
23 frameReady: make(chan struct{}, 1),
24 }
25 if timer {
26 s.paceTimer = time.NewTimer(time.Hour)
27 s.paceTimer.Stop()
28 }
29 w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
30 surf := &surface{server: s, xdg: &xdgSurface{window: w}}
31 surf.next.callbacks = []*wayland.Callback{new(wayland.Callback)}
32 return s, surf
33}
34
35// A commit on the display goroutine ticks the pacer inline: it arms the
36// pacer's timer itself and does not wake the pacer goroutine, which would
37// only call display.Do to get back to the display goroutine.
38func TestCommitTicksPacerInline(t *testing.T) {
39 s, surf := paceFixture(true)
40 surf.queueAndTick()
41 if len(s.awaiting["DP-2"]) != 1 {
42 t.Fatalf("callback not queued: %+v", s.awaiting)
43 }
44 if len(s.frameReady) != 0 {
45 t.Fatal("commit woke the pacer goroutine")
46 }
47 waitArmed(t, s, "inline tick did not arm the pacer timer for the next deadline")
48 if s.paceDepth != 0 || s.paceDirty {
49 t.Fatalf("pace state leaked: depth %d dirty %v", s.paceDepth, s.paceDirty)
50 }
51}
52
53// Without a running pacer (no timer) the wake falls back to the channel.
54func TestCommitWakesPacerWithoutTimer(t *testing.T) {
55 s, surf := paceFixture(false)
56 surf.queueAndTick()
57 if len(s.frameReady) != 1 {
58 t.Fatal("pacer not woken")
59 }
60}
61
62// Wakes inside a tick coalesce into one more pass, not one channel wake each.
63func TestWakePacerDefersInsideTick(t *testing.T) {
64 s, _ := paceFixture(true)
65 s.paceDepth = 1
66 s.wakePacer()
67 s.wakePacer()
68 if len(s.frameReady) != 0 || !s.paceDirty {
69 t.Fatalf("wake inside a tick: queued %d dirty %v", len(s.frameReady), s.paceDirty)
70 }
71 s.paceDepth = 0
72 s.wakePacer()
73 if len(s.frameReady) != 1 {
74 t.Fatal("wake outside a tick lost")
75 }
76}
77
78// A commit that waits (a fifo barrier) is timed by the inline tick.
79func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) {
80 s, surf := paceFixture(true)
81 surf.next.callbacks = nil
82 surf.barrier, surf.barrierAt = true, time.Now()
83 surf.next.wait = true
84 surf.queueAndTick()
85 if len(surf.queue) != 1 || len(s.frameReady) != 0 {
86 t.Fatalf("queued %d, pacer wakes %d", len(surf.queue), len(s.frameReady))
87 }
88 waitArmed(t, s, "timer not armed for the barrier deadline")
89}
90
91// waitArmed asserts the timer was armed: it is pending, or it already
92// fired and holds its value.
93func waitArmed(t *testing.T, s *Server, msg string) {
94 t.Helper()
95 if s.paceTimer.Stop() {
96 return
97 }
98 select {
99 case <-s.paceTimer.C:
100 default:
101 t.Fatal(msg)
102 }
103}
104
105// A synchronized subsurface commit waits for its parent's commit: it does
106// not tick the pacer, so a burst of child commits costs no ticks.
107func TestSyncedCommitDoesNotTick(t *testing.T) {
108 s, surf := paceFixture(true)
109 surf.sub.parent = &surface{server: s}
110 surf.sub.synced = true
111 surf.queueAndTick()
112 if len(surf.queue) != 1 || !surf.queue[0].synced {
113 t.Fatalf("synced commit not queued: %d", len(surf.queue))
114 }
115 if len(s.frameReady) != 0 || s.paceTimer.Stop() {
116 t.Fatal("synced commit ticked the pacer")
117 }
118}