1package wayland
2
3import (
4 "context"
5 "slices"
6 "testing"
7 "time"
8
9 "github.com/bnema/go-wayland-bindings/server/fractionalscale"
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/xdgshell"
13 "github.com/bnema/neferwl/internal/logging"
14 "github.com/bnema/neferwl/internal/ports"
15 "github.com/bnema/wlturbo"
16 "golang.org/x/sys/unix"
17)
18
19type scaleProxy struct {
20 wlturbo.BaseProxy
21 values chan uint32
22}
23
24func (p *scaleProxy) Dispatch(e *wlturbo.Event) {
25 if e.Opcode == 0 {
26 p.values <- e.Uint32()
27 }
28}
29
30// surfaceScaleProxy records wl_surface.preferred_buffer_scale (opcode 2).
31type surfaceScaleProxy struct {
32 wlturbo.BaseProxy
33 scales chan int32
34}
35
36func (p *surfaceScaleProxy) Dispatch(e *wlturbo.Event) {
37 if e.Opcode == 2 {
38 p.scales <- e.Int32()
39 }
40}
41
42// surfaceTransformProxy records wl_surface.preferred_buffer_transform (opcode 3).
43type surfaceTransformProxy struct {
44 wlturbo.BaseProxy
45 transforms chan uint32
46}
47
48func (p *surfaceTransformProxy) Dispatch(e *wlturbo.Event) {
49 if e.Opcode == 3 {
50 p.transforms <- e.Uint32()
51 }
52}
53
54func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, string) {
55 t.Helper()
56 dir := t.TempDir()
57 events := make(chan ports.ClientEvent, 16)
58 commands := make(chan ports.ClientCommand, 16)
59 contents := make(chan ports.SurfaceContent, 16)
60 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents}, logging.For(context.Background(), "wayland"))
61 if err != nil {
62 t.Fatal(err)
63 }
64 ctx, cancel := context.WithCancel(context.Background())
65 done := make(chan error, 1)
66 go func() { done <- s.Run(ctx) }()
67 t.Cleanup(func() { cancel(); <-done })
68 return s, events, commands, contents, dir
69}
70
71// A v6 surface learns the transform of its output when it differs from
72// normal (the protocol default), and again only when it changes. A cursor
73// surface never does: cursor buffers must stay untransformed.
74func TestPreferredBufferTransform(t *testing.T) {
75 s, _, _, _, dir := contentServer(t)
76 info := testOutputs[0].Info
77 setOutputs := func(tr ports.BufferTransform, scale float64) {
78 if !s.display.Do(func() {
79 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: info, Width: info.Height, Height: info.Width, Scale: scale, Transform: tr}}})
80 }) {
81 t.Fatal("display stopped")
82 }
83 }
84 newSurface := func(c *wlturbo.Display, comp uint32) (uint32, func() []uint32) {
85 surf := c.AllocateID()
86 p := &surfaceTransformProxy{transforms: make(chan uint32, 8)}
87 p.SetID(surf)
88 registerWireProxy(c, p)
89 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
90 return surf, func() []uint32 {
91 if err := c.Roundtrip(); err != nil {
92 t.Fatal(err)
93 }
94 var got []uint32
95 for len(p.transforms) > 0 {
96 got = append(got, <-p.transforms)
97 }
98 return got
99 }
100 }
101 setOutputs(0, 1)
102 c := protocolClient(t, s, dir)
103 g, _ := c.Registry().FindGlobal("wl_compositor")
104 comp, err := bindWireID(c, g.Name, g.Interface, 6)
105 if err != nil {
106 t.Fatal(err)
107 }
108 seat := bindProtocol(t, c, "wl_seat")
109 registerProtocol(t, c, seat)
110 pointer := c.AllocateID()
111 registerProtocol(t, c, pointer)
112 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
113
114 _, normal := newSurface(c, comp)
115 cursor, cursorSeen := newSurface(c, comp)
116 if got := normal(); len(got) != 0 {
117 t.Fatalf("normal output sent transforms %v", got)
118 }
119 cursorSeen()
120 requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), cursor, int32(0), int32(0))
121
122 setOutputs(1, 1)
123 if got := normal(); !slices.Equal(got, []uint32{1}) {
124 t.Fatalf("rotated transforms %v, want [1]", got)
125 }
126 if got := cursorSeen(); len(got) != 0 {
127 t.Fatalf("cursor surface got transforms %v", got)
128 }
129 // A scale change alone does not repeat the transform.
130 setOutputs(1, 2)
131 if got := normal(); len(got) != 0 {
132 t.Fatalf("unchanged transform resent: %v", got)
133 }
134 setOutputs(3, 2)
135 if got := normal(); !slices.Equal(got, []uint32{3}) {
136 t.Fatalf("changed transforms %v, want [3]", got)
137 }
138 setOutputs(0, 2)
139 if got := normal(); !slices.Equal(got, []uint32{0}) {
140 t.Fatalf("back to normal: %v, want [0]", got)
141 }
142 if got := cursorSeen(); len(got) != 0 {
143 t.Fatalf("cursor surface got transforms %v", got)
144 }
145}
146
147func TestFractionalScaleAndViewport(t *testing.T) {
148 s, events, commands, contents, dir := contentServer(t)
149 c := protocolClient(t, s, dir)
150 // wl_surface.preferred_buffer_scale needs wl_compositor v6.
151 g, _ := c.Registry().FindGlobal("wl_compositor")
152 comp, err := bindWireID(c, g.Name, g.Interface, 6)
153 if err != nil {
154 t.Fatal(err)
155 }
156 wm := bindProtocol(t, c, "xdg_wm_base")
157 shm := bindProtocol(t, c, "wl_shm")
158 fm := bindProtocol(t, c, "wp_fractional_scale_manager_v1")
159 vpm := bindProtocol(t, c, "wp_viewporter")
160 registerProtocol(t, c, shm) // wl_shm sends its formats right away
161 // New IDs must be used in allocation order.
162 surf := c.AllocateID()
163 bufferScales := make(chan int32, 4)
164 sp := &surfaceScaleProxy{scales: bufferScales}
165 sp.SetID(surf)
166 registerWireProxy(c, sp)
167 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
168 frac := c.AllocateID()
169 values := make(chan uint32, 4)
170 proxy := &scaleProxy{values: values}
171 proxy.SetID(frac)
172 registerWireProxy(c, proxy)
173 requestProtocol(t, c, fm, fractionalscale.WpFractionalScaleManagerV1RequestGetFractionalScale, frac, surf)
174 vp := c.AllocateID()
175 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
176 registerProtocol(t, c, vp)
177 if err := c.Roundtrip(); err != nil {
178 t.Fatal(err)
179 }
180 if v := <-values; v != 120 {
181 t.Fatalf("initial scale %d", v)
182 }
183 // Core switches to 1.5: the surface gets 180/120.
184 commands <- ports.SetOutputs{Outputs: ports.Layout{{Info: testOutputs[0].Info, Width: 1280, Height: 720, Scale: 1.5}}}
185 // Events are read by Roundtrip; the command is applied asynchronously.
186 var got uint32
187 for deadline := time.Now().Add(2 * time.Second); got == 0 && time.Now().Before(deadline); {
188 if err := c.Roundtrip(); err != nil {
189 t.Fatal(err)
190 }
191 select {
192 case got = <-values:
193 case <-time.After(10 * time.Millisecond):
194 }
195 }
196 if got != 180 {
197 t.Fatalf("scale %d", got)
198 }
199 // Clients without fractional scale get the integer scale rounded up.
200 var last int32
201 for len(bufferScales) > 0 {
202 last = <-bufferScales
203 }
204 if last != 2 {
205 t.Fatalf("preferred_buffer_scale %d", last)
206 }
207 // A 30x15 buffer shown at 20x10 logical through the viewport.
208 xdg := c.AllocateID()
209 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
210 serials := make(chan uint32, 4)
211 cp := &configureProxy{serial: serials}
212 cp.SetID(xdg)
213 registerWireProxy(c, cp)
214 top := c.AllocateID()
215 registerProtocol(t, c, top)
216 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
217 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
218 if err := c.Roundtrip(); err != nil {
219 t.Fatal(err)
220 }
221 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)
222 fd, err := unix.MemfdCreate("scaled", 0)
223 if err != nil {
224 t.Fatal(err)
225 }
226 defer unix.Close(fd)
227 size := int32(30 * 15 * 4)
228 if err := unix.Ftruncate(fd, int64(size)); err != nil {
229 t.Fatal(err)
230 }
231 pool, buffer := c.AllocateID(), c.AllocateID()
232 registerProtocol(t, c, pool)
233 registerProtocol(t, c, buffer)
234 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
235 t.Fatal(err)
236 }
237 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(30), int32(15), int32(120), uint32(0))
238 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(20), int32(10))
239 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
240 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
241 if err := c.Roundtrip(); err != nil {
242 t.Fatal(err)
243 }
244 mapped(t, events, 2*time.Second)
245 deadline := time.After(2 * time.Second)
246 for {
247 select {
248 case got := <-contents:
249 if got.SHM == nil {
250 continue
251 }
252 if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {
253 t.Fatalf("%+v", got)
254 }
255 goto viewportOnly
256 case <-deadline:
257 t.Fatal("no content")
258 }
259 }
260viewportOnly:
261 // Changing only the destination must update content with the current buffer.
262 for _, size := range [][2]int32{{1, 1}, {7, 3}} {
263 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, size[0], size[1])
264 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
265 if err := c.Roundtrip(); err != nil {
266 t.Fatal(err)
267 }
268 select {
269 case got := <-contents:
270 if got.LogicalW != int(size[0]) || got.LogicalH != int(size[1]) || got.Width != 30 || got.Height != 15 {
271 t.Fatalf("viewport-only content: %+v", got)
272 }
273 case <-time.After(2 * time.Second):
274 t.Fatal("no viewport-only content")
275 }
276 }
277 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(256), int32(256), int32(5*256), int32(4*256))
278 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(-1), int32(-1))
279 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
280 if err := c.Roundtrip(); err != nil {
281 t.Fatal(err)
282 }
283 select {
284 case got := <-contents:
285 if got.LogicalW != 5 || got.LogicalH != 4 || got.Source != [4]float32{1, 1, 5, 4} {
286 t.Fatalf("source-only crop: %+v", got)
287 }
288 case <-time.After(2 * time.Second):
289 t.Fatal("no crop content")
290 }
291 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(2), int32(3))
292 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
293 if err := c.Roundtrip(); err != nil {
294 t.Fatal(err)
295 }
296 select {
297 case got := <-contents:
298 if got.LogicalW != 2 || got.LogicalH != 3 || got.Source != [4]float32{1, 1, 5, 4} {
299 t.Fatalf("source and destination: %+v", got)
300 }
301 case <-time.After(2 * time.Second):
302 t.Fatal("no source and destination content")
303 }
304}
305
306func TestBufferScaleLogicalSize(t *testing.T) {
307 st := &surface{bufferScale: 2}
308 if w, h := st.logicalSize(200, 100); w != 100 || h != 50 {
309 t.Fatal(w, h)
310 }
311 st.committedViewport = viewportState{destW: 7, destH: 3, dest: true}
312 if w, h := st.logicalSize(200, 100); w != 7 || h != 3 {
313 t.Fatal(w, h)
314 }
315}
316
317func TestViewportBadSource(t *testing.T) {
318 s, _, _, _, dir := contentServer(t)
319 c := protocolClient(t, s, dir)
320 comp := bindProtocol(t, c, "wl_compositor")
321 vpm := bindProtocol(t, c, "wp_viewporter")
322 surf, vp := c.AllocateID(), c.AllocateID()
323 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
324 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
325 registerProtocol(t, c, vp)
326 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(-256), int32(0), int32(256), int32(256))
327 expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorBadValue))
328}
329
330func TestViewportApplyErrors(t *testing.T) {
331 for _, tc := range []struct {
332 name string
333 width int32
334 destination bool
335 code viewporter.WpViewportError
336 }{
337 {"bad_size", 128, false, viewporter.WpViewportErrorBadSize},
338 {"out_of_buffer", 512, true, viewporter.WpViewportErrorOutOfBuffer},
339 } {
340 t.Run(tc.name, func(t *testing.T) {
341 s, _, _, _, dir := contentServer(t)
342 c := protocolClient(t, s, dir)
343 comp := bindProtocol(t, c, "wl_compositor")
344 vpm := bindProtocol(t, c, "wp_viewporter")
345 shm := bindProtocol(t, c, "wl_shm")
346 registerProtocol(t, c, shm)
347 surf, vp := c.AllocateID(), c.AllocateID()
348 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
349 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
350 registerProtocol(t, c, vp)
351 fd, err := unix.MemfdCreate("viewport", 0)
352 if err != nil {
353 t.Fatal(err)
354 }
355 defer unix.Close(fd)
356 if err := unix.Ftruncate(fd, 4); err != nil {
357 t.Fatal(err)
358 }
359 pool, buf := c.AllocateID(), c.AllocateID()
360 registerProtocol(t, c, pool)
361 registerProtocol(t, c, buf)
362 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
363 t.Fatal(err)
364 }
365 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
366 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), tc.width, int32(256))
367 if tc.destination {
368 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(1), int32(1))
369 }
370 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
371 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
372 expectProtocolError(t, c, vp, uint32(tc.code))
373 })
374 }
375}
376
377// A client rounds the source position and size to 1/256 independently, so
378// their sum can pass the buffer edge by a unit: that is accepted. The
379// values are a real rejected source (y 213.86, height 1946.14 on 2160 rows).
380// One pixel past the edge is still out_of_buffer.
381func TestViewportSourceEdgeRounding(t *testing.T) {
382 for _, tc := range []struct {
383 name string
384 y, h int32
385 error bool
386 }{
387 {"rounded edge", 54749, 498212, false},
388 {"one pixel past", 54749, 498212 + 256, true},
389 } {
390 t.Run(tc.name, func(t *testing.T) {
391 s, events, _, contents, dir := contentServer(t)
392 c := protocolClient(t, s, dir)
393 win, surf, _ := surfaceMapper(t, c, events)()
394 drainContents(contents)
395 vpm := bindProtocol(t, c, "wp_viewporter")
396 shm := bindProtocol(t, c, "wl_shm")
397 registerProtocol(t, c, shm)
398 vp := c.AllocateID()
399 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
400 registerProtocol(t, c, vp)
401 const w, h = 4, 2160
402 fd, err := unix.MemfdCreate("viewport-edge", 0)
403 if err != nil {
404 t.Fatal(err)
405 }
406 defer unix.Close(fd)
407 if err := unix.Ftruncate(fd, w*h*4); err != nil {
408 t.Fatal(err)
409 }
410 pool, buf := c.AllocateID(), c.AllocateID()
411 registerProtocol(t, c, pool)
412 registerProtocol(t, c, buf)
413 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
414 t.Fatal(err)
415 }
416 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
417 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(w), int32(100))
418 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), tc.y, int32(w*256), tc.h)
419 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
420 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
421 if tc.error {
422 expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
423 return
424 }
425 roundtrip(t, c)
426 select {
427 case got := <-contents:
428 if got.ID != win.ID || got.Source[1]+got.Source[3] > h || got.Source[1] == 0 {
429 t.Fatalf("published source outside buffer: %+v", got)
430 }
431 case <-time.After(2 * time.Second):
432 t.Fatal("missing viewport publication")
433 }
434 })
435 }
436}
437
438// A NULL attach unmaps the surface: viewporter.xml exempts it from
439// out_of_buffer, so a source larger than the previous buffer is not an error.
440// A later commit that keeps the retained buffer is still validated.
441func TestViewportNullAttachSkipsBufferBounds(t *testing.T) {
442 s, _, _, _, dir := contentServer(t)
443 c := protocolClient(t, s, dir)
444 comp := bindProtocol(t, c, "wl_compositor")
445 vpm := bindProtocol(t, c, "wp_viewporter")
446 surf, vp := c.AllocateID(), c.AllocateID()
447 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
448 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
449 registerProtocol(t, c, vp)
450 buf := shmBuffer(t, c)
451 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
452 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
453 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
454 roundtrip(t, c)
455
456 // Oversized source with a NULL attach: no error.
457 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(512), int32(256))
458 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
459 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
460 roundtrip(t, c)
461
462 // Reattach the 1x1 buffer: the oversized source now applies to it.
463 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
464 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
465 expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
466}
467
468func TestViewportBadDestination(t *testing.T) {
469 s, _, _, _, dir := contentServer(t)
470 c := protocolClient(t, s, dir)
471 comp := bindProtocol(t, c, "wl_compositor")
472 vpm := bindProtocol(t, c, "wp_viewporter")
473 surf, vp := c.AllocateID(), c.AllocateID()
474 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
475 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
476 registerProtocol(t, c, vp)
477 requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(0), int32(10))
478 expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorBadValue))
479}