1package wayland
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/viewporter"
8 "github.com/bnema/go-wayland-bindings/server/wayland"
9 "github.com/bnema/go-wayland-bindings/server/xdgshell"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/wlturbo"
12 "golang.org/x/sys/unix"
13)
14
15// transformClient maps a toplevel whose 30×20 argb8888 buffer is shown
16// through a viewport, and returns what it needs to commit more state.
17type transformClient struct {
18 c *wlturbo.Display
19 comp, surf, vp, bf uint32
20 contents chan ports.SurfaceContent
21}
22
23func newTransformClient(t *testing.T) transformClient {
24 t.Helper()
25 s, events, _, contents, dir := contentServer(t)
26 c := protocolClient(t, s, dir)
27 // set_buffer_transform needs wl_compositor v2.
28 g, _ := c.Registry().FindGlobal("wl_compositor")
29 comp, err := bindWireID(c, g.Name, g.Interface, 6)
30 if err != nil {
31 t.Fatal(err)
32 }
33 wm := bindProtocol(t, c, "xdg_wm_base")
34 shm := bindProtocol(t, c, "wl_shm")
35 vpm := bindProtocol(t, c, "wp_viewporter")
36 registerProtocol(t, c, shm)
37 surf := c.AllocateID()
38 registerProtocol(t, c, surf)
39 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
40 vp := c.AllocateID()
41 registerProtocol(t, c, vp)
42 requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
43 xdg := c.AllocateID()
44 serials := make(chan uint32, 4)
45 cp := &configureProxy{serial: serials}
46 cp.SetID(xdg)
47 registerWireProxy(c, cp)
48 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
49 top := c.AllocateID()
50 registerProtocol(t, c, top)
51 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
52 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
53 if err := c.Roundtrip(); err != nil {
54 t.Fatal(err)
55 }
56 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)
57 fd, err := unix.MemfdCreate("transform", 0)
58 if err != nil {
59 t.Fatal(err)
60 }
61 t.Cleanup(func() { unix.Close(fd) })
62 size := int32(30 * 20 * 4)
63 if err := unix.Ftruncate(fd, int64(size)); err != nil {
64 t.Fatal(err)
65 }
66 pool, bf := c.AllocateID(), c.AllocateID()
67 registerProtocol(t, c, pool)
68 registerProtocol(t, c, bf)
69 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
70 t.Fatal(err)
71 }
72 // argb8888: not opaque by format.
73 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, bf, int32(0), int32(30), int32(20), int32(120), uint32(wayland.ShmFormatArgb8888))
74 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bf, int32(0), int32(0))
75 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
76 if err := c.Roundtrip(); err != nil {
77 t.Fatal(err)
78 }
79 mapped(t, events, 2*time.Second)
80 tc := transformClient{c: c, comp: comp, surf: surf, vp: vp, bf: bf, contents: contents}
81 if got := tc.next(t); got.Transform != 0 || got.Opaque || got.LogicalW != 30 || got.LogicalH != 20 {
82 t.Fatalf("initial content: %+v", got)
83 }
84 return tc
85}
86
87// next returns the next published content with a buffer.
88func (tc transformClient) next(t *testing.T) ports.SurfaceContent {
89 t.Helper()
90 deadline := time.After(2 * time.Second)
91 for {
92 select {
93 case got := <-tc.contents:
94 if got.SHM != nil {
95 return got
96 }
97 case <-deadline:
98 t.Fatal("no content")
99 }
100 }
101}
102
103func (tc transformClient) commit(t *testing.T) {
104 t.Helper()
105 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestCommit)
106 if err := tc.c.Roundtrip(); err != nil {
107 t.Fatal(err)
108 }
109}
110
111// region creates a wl_region holding rects.
112func (tc transformClient) region(t *testing.T, rects ...ports.Rect) uint32 {
113 t.Helper()
114 id := tc.c.AllocateID()
115 registerProtocol(t, tc.c, id)
116 requestProtocol(t, tc.c, tc.comp, wayland.CompositorRequestCreateRegion, id)
117 for _, r := range rects {
118 requestProtocol(t, tc.c, id, wayland.RegionRequestAdd, int32(r.X), int32(r.Y), int32(r.W), int32(r.H))
119 }
120 return id
121}
122
123// A 90° buffer transform swaps the surface size and maps the viewport
124// source back to buffer pixels; it applies on commit, like the scale.
125func TestBufferTransformContent(t *testing.T) {
126 tc := newTransformClient(t)
127 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetBufferTransform, int32(1))
128 tc.commit(t)
129 if got := tc.next(t); got.Transform != 1 || got.Width != 30 || got.Height != 20 || got.LogicalW != 20 || got.LogicalH != 30 {
130 t.Fatalf("rotated content: %+v", got)
131 }
132 // Surface crop x 2..12, y 3..8 of the 20×30 rotated surface. Surface
133 // (x, y) is buffer (y, 20-x): buffer x 3..8, y 8..18.
134 requestProtocol(t, tc.c, tc.vp, viewporter.WpViewportRequestSetSource, int32(2*256), int32(3*256), int32(10*256), int32(5*256))
135 tc.commit(t)
136 if got := tc.next(t); got.LogicalW != 10 || got.LogicalH != 5 || got.Source != [4]float32{3, 8, 5, 10} {
137 t.Fatalf("rotated crop: %+v", got)
138 }
139 // A crop that fits the unrotated buffer but not the rotated surface
140 // (x up to 25 > 20) is out of buffer.
141 requestProtocol(t, tc.c, tc.vp, viewporter.WpViewportRequestSetSource, int32(15*256), int32(0), int32(10*256), int32(5*256))
142 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestCommit)
143 expectProtocolError(t, tc.c, tc.vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
144}
145
146func TestBufferTransformInvalid(t *testing.T) {
147 tc := newTransformClient(t)
148 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetBufferTransform, int32(8))
149 expectProtocolError(t, tc.c, tc.surf, uint32(wayland.SurfaceErrorInvalidTransform))
150}
151
152// An opaque region covering the surface marks an alpha buffer opaque; a
153// partial or NULL region does not. The region applies on commit.
154func TestOpaqueRegionContent(t *testing.T) {
155 tc := newTransformClient(t)
156 half := tc.region(t, ports.Rect{W: 15, H: 20})
157 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, half)
158 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestDamage, int32(0), int32(0), int32(1), int32(1))
159 tc.commit(t)
160 prev := tc.next(t)
161 if prev.Opaque {
162 t.Fatalf("partial region made the surface opaque: %+v", prev)
163 }
164 if want := (ports.Rect{W: 15, H: 20}); prev.OpaqueRect != want {
165 t.Fatalf("partial region: OpaqueRect %v, want %v", prev.OpaqueRect, want)
166 }
167 // Two halves cover the whole surface, even through a larger rect.
168 // Opacity changes blending everywhere: the commit damages it all.
169 whole := tc.region(t, ports.Rect{W: 15, H: 20}, ports.Rect{X: 15, W: 100, H: 20})
170 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, whole)
171 tc.commit(t)
172 got := tc.next(t)
173 if !got.Opaque || got.OpaqueRect != (ports.Rect{}) {
174 t.Fatalf("covering region: %+v", got)
175 }
176 if rects, ok := got.DamageSince(prev.Seq); ok {
177 t.Fatalf("opacity change damaged only %v", rects)
178 }
179 // The surface grows past the region through the viewport: not opaque.
180 requestProtocol(t, tc.c, tc.vp, viewporter.WpViewportRequestSetDestination, int32(200), int32(20))
181 tc.commit(t)
182 if got := tc.next(t); got.Opaque {
183 t.Fatalf("region no longer covers: %+v", got)
184 }
185 requestProtocol(t, tc.c, tc.vp, viewporter.WpViewportRequestSetDestination, int32(-1), int32(-1))
186 tc.commit(t)
187 if got := tc.next(t); !got.Opaque {
188 t.Fatalf("region covers again: %+v", got)
189 }
190 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, uint32(0))
191 tc.commit(t)
192 if got := tc.next(t); got.Opaque || got.OpaqueRect != (ports.Rect{}) {
193 t.Fatalf("NULL region: %+v", got)
194 }
195 // A region one pixel short of the surface (what Firefox sets) is not
196 // opaque, but its rect is reported.
197 short := tc.region(t, ports.Rect{W: 29, H: 19})
198 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, short)
199 tc.commit(t)
200 if got := tc.next(t); got.Opaque || got.OpaqueRect != (ports.Rect{W: 29, H: 19}) {
201 t.Fatalf("short region: %+v", got)
202 }
203}
204
205// opaqueRect is the largest rect of the region inside the surface, none for
206// an opaque or faded surface.
207func TestOpaqueRect(t *testing.T) {
208 for _, tc := range []struct {
209 name string
210 region []ports.Rect
211 opaque bool
212 w, h int
213 fade float32
214 want ports.Rect
215 }{
216 {"none", nil, false, 10, 10, 0, ports.Rect{}},
217 {"one pixel short", []ports.Rect{{W: 983, H: 1230}}, false, 984, 1231, 0, ports.Rect{W: 983, H: 1230}},
218 {"clipped to the surface", []ports.Rect{{X: -5, Y: 2, W: 100, H: 100}}, false, 10, 10, 0, ports.Rect{Y: 2, W: 10, H: 8}},
219 {"largest of several", []ports.Rect{{W: 2, H: 2}, {X: 3, Y: 3, W: 5, H: 4}}, false, 10, 10, 0, ports.Rect{X: 3, Y: 3, W: 5, H: 4}},
220 {"outside", []ports.Rect{{X: 10, W: 5, H: 5}}, false, 10, 10, 0, ports.Rect{}},
221 {"wholly opaque", []ports.Rect{{W: 9, H: 9}}, true, 10, 10, 0, ports.Rect{}},
222 {"faded", []ports.Rect{{W: 9, H: 9}}, false, 10, 10, 0.5, ports.Rect{}},
223 {"no size", []ports.Rect{{W: 9, H: 9}}, false, 0, 0, 0, ports.Rect{}},
224 } {
225 t.Run(tc.name, func(t *testing.T) {
226 s := &surface{opaque: tc.region}
227 if got := s.opaqueRect(tc.opaque, tc.w, tc.h, tc.fade); got != tc.want {
228 t.Fatalf("opaqueRect = %v, want %v", got, tc.want)
229 }
230 })
231 }
232}
233
234// Transform and opaque region requested after a queued commit belong to
235// the next commit.
236func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) {
237 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}}
238 surf := &surface{server: srv}
239 surf.next.transform = 1
240 surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true
241 surf.queueUpdate()
242 surf.next.transform = 2
243 surf.next.opaque, surf.next.opaqueSet = nil, true
244 surf.queue[0].applyGraph()
245 if surf.transform != 1 || len(surf.opaque) != 1 {
246 t.Fatalf("captured state: transform %d opaque %v", surf.transform, surf.opaque)
247 }
248 if surf.next.transform != 2 || !surf.next.opaqueSet || surf.next.opaque != nil {
249 t.Fatal("application consumed a later request")
250 }
251 surf.queueUpdate()
252 if u := surf.queue[0]; u.transform != 2 || !u.opaqueSet || u.opaque != nil {
253 t.Fatalf("next commit: transform %d opaque %v/%v", u.transform, u.opaqueSet, u.opaque)
254 }
255 surf.dropQueue()
256}
257
258func TestCovers(t *testing.T) {
259 r := ports.Rect{W: 10, H: 10}
260 for _, tc := range []struct {
261 name string
262 rects []ports.Rect
263 want bool
264 }{
265 {"empty", nil, false},
266 {"exact", []ports.Rect{{W: 10, H: 10}}, true},
267 {"larger", []ports.Rect{{X: -5, Y: -5, W: 30, H: 30}}, true},
268 {"two halves", []ports.Rect{{W: 5, H: 10}, {X: 5, W: 5, H: 10}}, true},
269 {"gap", []ports.Rect{{W: 4, H: 10}, {X: 5, W: 5, H: 10}}, false},
270 {"hole", []ports.Rect{{W: 10, H: 4}, {Y: 6, W: 10, H: 4}, {Y: 4, W: 4, H: 2}, {X: 5, Y: 4, W: 5, H: 2}}, false},
271 {"four quarters", []ports.Rect{{W: 5, H: 5}, {X: 5, W: 5, H: 5}, {Y: 5, W: 5, H: 5}, {X: 5, Y: 5, W: 5, H: 5}}, true},
272 {"too many", make([]ports.Rect, maxCoverRects+1), false},
273 } {
274 if got := covers(tc.rects, r); got != tc.want {
275 t.Errorf("%s: %v, want %v", tc.name, got, tc.want)
276 }
277 }
278}