1package wayland
2
3import (
4 "reflect"
5 "testing"
6 "time"
7
8 "github.com/bnema/go-wayland-bindings/server/fifo"
9 "github.com/bnema/go-wayland-bindings/server/presentationtime"
10 "github.com/bnema/go-wayland-bindings/server/viewporter"
11 "github.com/bnema/go-wayland-bindings/server/wayland"
12 "github.com/bnema/go-wayland-bindings/server/wlrlayershell"
13 "github.com/bnema/go-wayland-bindings/server/xdgshell"
14 "github.com/bnema/neferwl/internal/ports"
15 "github.com/bnema/purego-libwayland/server"
16)
17
18// Applying an older captured commit must never transiently own newer requests.
19// The next commit captures those requests without losing its one-shot state.
20func TestCapturedCommitKeepsLaterRequests(t *testing.T) {
21 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}}
22 surf := &surface{server: srv, xdg: &xdgSurface{}}
23 cons := &constraint{surface: surf}
24 srv.constraints[surf] = cons
25 firstRegion := &ports.Rect{W: 2}
26 cons.pending = firstRegion
27 surf.next.scale, surf.next.async, surf.next.kind = 2, true, 1
28 surf.xdg.pendingGeometry = ports.Rect{W: 10}
29 surf.next.inputSet, surf.next.inputAll = true, true
30 surf.next.damage = []ports.Rect{{W: 2}}
31 surf.next.barrier = true
32 surf.queueUpdate()
33
34 laterRegion := &ports.Rect{W: 3}
35 cons.pending = laterRegion
36 surf.next.scale, surf.next.async, surf.next.kind = 3, false, 2
37 surf.sub.pendingLayout = []childLayout{{x: 7}}
38 surf.xdg.pendingGeometry = ports.Rect{W: 20}
39 surf.next.inputSet, surf.next.inputAll = true, false
40 surf.next.inputRects = []ports.Rect{{W: 3}}
41 surf.next.damage = []ports.Rect{{W: 3}}
42 surf.next.barrier = true
43 laterTime := time.Now().Add(time.Second)
44 surf.next.at = laterTime
45
46 surf.queue[0].applyGraph()
47 if surf.bufferScale != 2 || !surf.async || surf.contentKind != 1 || len(surf.sub.layout) != 0 || surf.xdg.geometry.W != 10 || cons.region.W != 2 || !surf.inputAll || !surf.barrier {
48 t.Fatalf("captured state changed: scale=%d async=%v kind=%d geometry=%v region=%v input=%v barrier=%v", surf.bufferScale, surf.async, surf.contentKind, surf.xdg.geometry, cons.region, surf.inputAll, surf.barrier)
49 }
50 if surf.next.scale != 3 || surf.next.async || surf.next.kind != 2 || len(surf.sub.pendingLayout) != 1 || surf.xdg.pendingGeometry.W != 20 || cons.pending != laterRegion || !surf.next.inputSet || surf.next.inputAll || len(surf.next.inputRects) != 1 || len(surf.next.damage) != 1 || !surf.next.barrier || !surf.next.at.Equal(laterTime) {
51 t.Fatal("application consumed a later request")
52 }
53 surf.queueUpdate()
54 second := surf.queue[0]
55 if second.scale != 3 || second.async || second.kind != 2 || len(second.layout) != 1 || second.geometry.W != 20 || second.region != laterRegion || !second.inputSet || second.inputAll || len(second.inputRects) != 1 || len(second.damage) != 1 || !second.barrier || !second.at.Equal(laterTime) {
56 t.Fatal("subsequent commit did not capture later requests")
57 }
58 surf.dropQueue()
59}
60
61// The viewport resource is live, but its requests after capture must not be
62// installed by an older queued commit.
63func TestCapturedViewportKeepsLaterRequests(t *testing.T) {
64 s, _, _, _, dir := contentServer(t)
65 c := protocolClient(t, s, dir)
66 comp := bindProtocol(t, c, "wl_compositor")
67 vpm := bindProtocol(t, c, "wp_viewporter")
68 surfID := c.AllocateID()
69 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
70 registerProtocol(t, c, surfID)
71 vpID := c.AllocateID()
72 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vpID, surfID)
73 registerProtocol(t, c, vpID)
74 if err := c.Roundtrip(); err != nil {
75 t.Fatal(err)
76 }
77 s.display.Do(func() {
78 var surf *surface
79 for _, candidate := range s.surfaces {
80 if candidate.wl != nil && candidate.wl.ID() == surfID {
81 surf = candidate
82 }
83 }
84 if surf == nil || surf.viewport == nil {
85 t.Error("missing viewport")
86 return
87 }
88 v := surf.viewport
89 v.SetSource(v.resource, 0, 0, server.Fixed(256), server.Fixed(256))
90 v.SetDestination(v.resource, 1, 1)
91 surf.queueUpdate()
92 v.SetSource(v.resource, 0, 0, server.Fixed(512), server.Fixed(512))
93 v.SetDestination(v.resource, 2, 2)
94 surf.queue[0].applyGraph()
95 if surf.committedViewport.destW != 1 || surf.committedViewport.src[2] != 256 || v.pendingW != 2 || v.pendingSrc[2] != 512 {
96 t.Errorf("captured=%+v pending=%+v", surf.committedViewport, v)
97 }
98 surf.queueUpdate()
99 surf.queue[0].applyGraph()
100 if surf.committedViewport.destW != 2 || surf.committedViewport.src[2] != 512 {
101 t.Errorf("next commit: %+v", surf.committedViewport)
102 }
103 })
104}
105
106// Real protocol requests captured behind a barrier keep the first buffer;
107// an attach made after capture is owned by the next commit.
108func TestQueuedAttachKeepsLaterBuffer(t *testing.T) {
109 s, _, _, _, dir := contentServer(t)
110 c := protocolClient(t, s, dir)
111 comp := bindProtocol(t, c, "wl_compositor")
112 fm := bindProtocol(t, c, "wp_fifo_manager_v1")
113 id := c.AllocateID()
114 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, id)
115 registerProtocol(t, c, id)
116 f := c.AllocateID()
117 requestProtocol(t, c, fm, fifo.WpFifoManagerV1RequestGetFifo, f, id)
118 registerProtocol(t, c, f)
119 first, second := shmBuffer(t, c), shmBuffer(t, c)
120 requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
121 requestProtocol(t, c, id, wayland.SurfaceRequestCommit)
122 requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
123 requestProtocol(t, c, id, wayland.SurfaceRequestAttach, first, int32(0), int32(0))
124 requestProtocol(t, c, id, wayland.SurfaceRequestCommit)
125 requestProtocol(t, c, id, wayland.SurfaceRequestAttach, second, int32(0), int32(0))
126 if err := c.Roundtrip(); err != nil {
127 t.Fatal(err)
128 }
129 s.display.Do(func() {
130 var surf *surface
131 for _, candidate := range s.surfaces {
132 if candidate.wl != nil && candidate.wl.ID() == id {
133 surf = candidate
134 }
135 }
136 if surf == nil || len(surf.queue) != 1 || surf.queue[0].buffer.ID() != first || surf.next.buffer.ID() != second {
137 t.Error("queued attach lost its captured or later buffer")
138 return
139 }
140 surf.barrier = false
141 s.tickFifo(time.Now(), nil)
142 if surf.current == nil || surf.current.ID() != first || surf.next.buffer == nil || surf.next.buffer.ID() != second {
143 t.Error("applying captured attach consumed later request")
144 }
145 })
146 requestProtocol(t, c, id, wayland.SurfaceRequestCommit)
147 if err := c.Roundtrip(); err != nil {
148 t.Fatal(err)
149 }
150 s.display.Do(func() {
151 for _, surf := range s.surfaces {
152 if surf.wl != nil && surf.wl.ID() == id && (surf.current == nil || surf.current.ID() != second) {
153 t.Error("later attach not applied by next commit")
154 }
155 }
156 })
157}
158
159// Layer requests are sticky, but must not be read from live pending state
160// when an older queued update is finally applied.
161func TestCapturedLayerKeepsLaterRequests(t *testing.T) {
162 s, _, _, _, dir := contentServer(t)
163 c := protocolClient(t, s, dir)
164 comp := bindProtocol(t, c, "wl_compositor")
165 shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
166 id := c.AllocateID()
167 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, id)
168 registerProtocol(t, c, id)
169 layerID := c.AllocateID()
170 requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, layerID, id, uint32(0), uint32(ports.LayerTop), "captured")
171 registerProtocol(t, c, layerID)
172 if err := c.Roundtrip(); err != nil {
173 t.Fatal(err)
174 }
175 s.display.Do(func() {
176 var surf *surface
177 for _, candidate := range s.surfaces {
178 if candidate.wl != nil && candidate.wl.ID() == id {
179 surf = candidate
180 }
181 }
182 if surf == nil || surf.layer == nil {
183 t.Error("missing layer")
184 return
185 }
186 l := surf.layer
187 l.SetSize(l.resource, 1, 1)
188 l.SetAnchor(l.resource, 1)
189 l.SetMargin(l.resource, 1, 2, 3, 4)
190 l.SetExclusiveZone(l.resource, 5)
191 surf.queueUpdate()
192 l.SetSize(l.resource, 2, 2)
193 l.SetAnchor(l.resource, 2)
194 l.SetMargin(l.resource, 5, 6, 7, 8)
195 l.SetExclusiveZone(l.resource, 9)
196 surf.queue[0].applyGraph()
197 if l.current.width != 1 || l.current.anchor != 1 || l.current.margin != [4]int32{1, 2, 3, 4} || l.current.zone != 5 || l.pending.width != 2 || l.pending.anchor != 2 || l.pending.margin != [4]int32{5, 6, 7, 8} || l.pending.zone != 9 {
198 t.Errorf("captured=%+v pending=%+v", l.current, l.pending)
199 }
200 surf.queueUpdate()
201 surf.queue[0].applyGraph()
202 if l.current.width != 2 || l.current.anchor != 2 || l.current.zone != 9 {
203 t.Errorf("next commit: %+v", l.current)
204 }
205 })
206}
207
208// Role and surface destruction drop their queues. Viewport and buffer
209// destruction leave the queued update to apply safely at the next tick.
210// Roundtrip orders destruction before examining the display owner.
211func TestQueuedDestructionDropsUpdates(t *testing.T) {
212 for _, kind := range []string{"viewport", "toplevel", "layer", "surface", "buffer"} {
213 t.Run(kind, func(t *testing.T) {
214 s, events, _, _, dir := contentServer(t)
215 c := protocolClient(t, s, dir)
216 var surfID, objectID, bufferID uint32
217 if kind == "toplevel" {
218 _, surfID, _ = surfaceMapper(t, c, events)()
219 s.display.Do(func() {
220 for _, surf := range s.surfaces {
221 if surf.wl != nil && surf.wl.ID() == surfID {
222 objectID = surf.xdg.window.toplevel.ID()
223 }
224 }
225 })
226 } else {
227 comp := bindProtocol(t, c, "wl_compositor")
228 surfID = c.AllocateID()
229 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
230 registerProtocol(t, c, surfID)
231 if kind == "viewport" {
232 vpm := bindProtocol(t, c, "wp_viewporter")
233 objectID = c.AllocateID()
234 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, objectID, surfID)
235 registerProtocol(t, c, objectID)
236 } else if kind == "layer" {
237 shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
238 objectID = c.AllocateID()
239 requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, objectID, surfID, uint32(0), uint32(ports.LayerTop), "drop")
240 registerProtocol(t, c, objectID)
241 requestProtocol(t, c, objectID, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1))
242 } else if kind == "buffer" {
243 bufferID = shmBuffer(t, c)
244 }
245 }
246 if err := c.Roundtrip(); err != nil {
247 t.Fatal(err)
248 }
249 s.display.Do(func() {
250 for _, surf := range s.surfaces {
251 if surf.wl != nil && surf.wl.ID() == surfID {
252 surf.next.scale = 3
253 surf.next.damage = []ports.Rect{{W: 3}}
254 if kind == "viewport" {
255 surf.viewport.SetDestination(surf.viewport.resource, 2, 2)
256 }
257 surf.next.at = time.Now().Add(time.Second)
258 if kind == "buffer" {
259 for resource := range s.buffers {
260 if resource.ID() == bufferID {
261 surf.next.buffer = wayland.WrapBuffer(resource)
262 surf.next.attached = true
263 }
264 }
265 }
266 surf.queueUpdate()
267 }
268 }
269 })
270 switch kind {
271 case "viewport":
272 requestProtocol(t, c, objectID, viewporter.WpViewportRequestDestroy)
273 case "toplevel":
274 requestProtocol(t, c, objectID, xdgshell.ToplevelRequestDestroy)
275 case "layer":
276 requestProtocol(t, c, objectID, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
277 case "surface":
278 requestProtocol(t, c, surfID, wayland.SurfaceRequestDestroy)
279 case "buffer":
280 requestProtocol(t, c, bufferID, wayland.BufferRequestDestroy)
281 }
282 if err := c.Roundtrip(); err != nil {
283 t.Fatal(err)
284 }
285 s.display.Do(func() {
286 for _, surf := range s.surfaces {
287 if surf.wl != nil && surf.wl.ID() == surfID {
288 if kind == "viewport" || kind == "buffer" {
289 if kind == "buffer" && (len(surf.queue) != 1 || surf.queue[0].buffer == nil || surf.queue[0].buffer.Resource.Alive()) {
290 t.Error("expected destroyed buffer in queued update")
291 }
292 if len(surf.queue) != 1 {
293 t.Errorf("%s should retain queued update until tick", kind)
294 }
295 s.tickFifo(time.Now().Add(2*time.Second), nil)
296 }
297 if len(surf.queue) != 0 {
298 t.Errorf("%s retained queued update", kind)
299 }
300 }
301 }
302 fresh := &surface{server: s}
303 fresh.queueUpdate()
304 u := fresh.queue[0]
305 if u.scale != 0 || u.buffer != nil || u.vp != nil || u.layer != nil || len(u.damage) != 0 || u.owner != fresh {
306 t.Errorf("%s recycled stale update: %+v", kind, u)
307 }
308 fresh.queue[0].applyGraph()
309 })
310 })
311 }
312}
313
314// A dropped update must be cleared before a different surface reuses it.
315func TestDroppedUpdateRecycledWithoutState(t *testing.T) {
316 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
317 old := &surface{server: srv, next: pendingCommit{scale: 2, barrier: true, damage: []ports.Rect{{W: 2}}}}
318 old.queueUpdate()
319 old.destroyed = true
320 old.dropQueue()
321 fresh := &surface{server: srv}
322 fresh.queueUpdate()
323 if u := fresh.queue[0]; u.scale != 0 || u.barrier || len(u.damage) != 0 || u.owner != fresh || u.buffer != nil || u.vp != nil || u.layer != nil || u.xdg != nil {
324 t.Fatalf("recycled update retained old state: %+v", u)
325 }
326 fresh.queue[0].applyGraph()
327}
328
329// BenchmarkEmptySurfaceQueueApply measures an empty, roleless surface's
330// in-process queue/apply loop. It excludes protocol dispatch and real buffers.
331func BenchmarkEmptySurfaceQueueApply(b *testing.B) {
332 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
333 surf := &surface{server: srv}
334 surf.queueUpdate()
335 surf.queue[0].applyGraph()
336 b.ReportAllocs()
337 b.ResetTimer()
338 for range b.N {
339 surf.queueUpdate()
340 surf.queue[0].applyGraph()
341 }
342}
343
344// TestCapturedViewportApplyAllocations exercises the real viewport resource
345// on the display owner: a viewport-only commit has no heap work.
346func TestCapturedViewportApplyAllocations(t *testing.T) {
347 s, _, _, _, dir := contentServer(t)
348 c := protocolClient(t, s, dir)
349 comp := bindProtocol(t, c, "wl_compositor")
350 vpm := bindProtocol(t, c, "wp_viewporter")
351 surfID := c.AllocateID()
352 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
353 registerProtocol(t, c, surfID)
354 vpID := c.AllocateID()
355 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vpID, surfID)
356 registerProtocol(t, c, vpID)
357 if err := c.Roundtrip(); err != nil {
358 t.Fatal(err)
359 }
360 s.display.Do(func() {
361 var surf *surface
362 for _, candidate := range s.surfaces {
363 if candidate.wl != nil && candidate.wl.ID() == surfID {
364 surf = candidate
365 break
366 }
367 }
368 if surf == nil || surf.viewport == nil || surf.viewport.resource == nil {
369 t.Error("missing viewport")
370 return
371 }
372 apply := func() {
373 surf.queueUpdate()
374 surf.queue[0].applyGraph()
375 }
376 apply()
377 if allocs := testing.AllocsPerRun(100, apply); allocs != 0 {
378 t.Errorf("viewport queue/apply: %.1f allocs/commit; want 0", allocs)
379 }
380 })
381}
382
383func TestCapturedCommitApplyAllocations(t *testing.T) {
384 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
385 surf := &surface{server: srv}
386 apply := func() {
387 surf.queueUpdate()
388 surf.queue[0].applyGraph()
389 }
390 apply() // warm pool and queue backing storage
391 if allocs := testing.AllocsPerRun(100, apply); allocs != 0 {
392 t.Errorf("steady-state queue/apply: %.1f allocs/commit; want 0", allocs)
393 }
394}
395
396// Capture takes every requested field; only sticky hints carry over.
397func TestTakePendingKeepsStickyState(t *testing.T) {
398 srv := &Server{constraints: map[*surface]*constraint{}}
399 surf := &surface{server: srv}
400 all := pendingCommit{
401 buffer: &wayland.Buffer{}, attached: true, scale: 2, transform: 1,
402 opaque: []ports.Rect{{W: 1}}, opaqueSet: true, inputAll: true, inputSet: true, inputRects: []ports.Rect{{W: 1}},
403 callbacks: []*wayland.Callback{{}}, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3,
404 barrier: true, wait: true, at: time.Unix(1, 0), damage: []ports.Rect{{W: 1}}, bufDamage: []ports.Rect{{W: 1}},
405 feedback: []*presentationtime.WpPresentationFeedback{{}}, sync: &commitSync{},
406 }
407 surf.next = all
408 u := surf.takePending()
409 if !reflect.DeepEqual(u.pendingCommit, all) {
410 t.Fatalf("captured = %+v", u.pendingCommit)
411 }
412 want := pendingCommit{scale: 2, transform: 1, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3}
413 if !reflect.DeepEqual(surf.next, want) {
414 t.Fatalf("left pending = %+v, want %+v", surf.next, want)
415 }
416}