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