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