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 // Two halves cover the whole surface, even through a larger rect.
165 // Opacity changes blending everywhere: the commit damages it all.
166 whole := tc.region(t, ports.Rect{W: 15, H: 20}, ports.Rect{X: 15, W: 100, H: 20})
167 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, whole)
168 tc.commit(t)
169 got := tc.next(t)
170 if !got.Opaque {
171 t.Fatalf("covering region: %+v", got)
172 }
173 if rects, ok := got.DamageSince(prev.Seq); ok {
174 t.Fatalf("opacity change damaged only %v", rects)
175 }
176 // The surface grows past the region through the viewport: not opaque.
177 requestProtocol(t, tc.c, tc.vp, viewporter.WpViewportRequestSetDestination, int32(200), int32(20))
178 tc.commit(t)
179 if got := tc.next(t); got.Opaque {
180 t.Fatalf("region no longer covers: %+v", got)
181 }
182 requestProtocol(t, tc.c, tc.vp, viewporter.WpViewportRequestSetDestination, int32(-1), int32(-1))
183 tc.commit(t)
184 if got := tc.next(t); !got.Opaque {
185 t.Fatalf("region covers again: %+v", got)
186 }
187 requestProtocol(t, tc.c, tc.surf, wayland.SurfaceRequestSetOpaqueRegion, uint32(0))
188 tc.commit(t)
189 if got := tc.next(t); got.Opaque {
190 t.Fatalf("NULL region: %+v", got)
191 }
192}
193
194// Transform and opaque region requested after a queued commit belong to
195// the next commit.
196func TestCapturedTransformAndOpaqueKeepLaterRequests(t *testing.T) {
197 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1), constraints: map[*surface]*constraint{}}
198 surf := &surface{server: srv}
199 surf.next.transform = 1
200 surf.next.opaque, surf.next.opaqueSet = []ports.Rect{{W: 4, H: 4}}, true
201 surf.queueUpdate()
202 surf.next.transform = 2
203 surf.next.opaque, surf.next.opaqueSet = nil, true
204 surf.queue[0].applyGraph()
205 if surf.transform != 1 || len(surf.opaque) != 1 {
206 t.Fatalf("captured state: transform %d opaque %v", surf.transform, surf.opaque)
207 }
208 if surf.next.transform != 2 || !surf.next.opaqueSet || surf.next.opaque != nil {
209 t.Fatal("application consumed a later request")
210 }
211 surf.queueUpdate()
212 if u := surf.queue[0]; u.transform != 2 || !u.opaqueSet || u.opaque != nil {
213 t.Fatalf("next commit: transform %d opaque %v/%v", u.transform, u.opaqueSet, u.opaque)
214 }
215 surf.dropQueue()
216}
217
218func TestCovers(t *testing.T) {
219 r := ports.Rect{W: 10, H: 10}
220 for _, tc := range []struct {
221 name string
222 rects []ports.Rect
223 want bool
224 }{
225 {"empty", nil, false},
226 {"exact", []ports.Rect{{W: 10, H: 10}}, true},
227 {"larger", []ports.Rect{{X: -5, Y: -5, W: 30, H: 30}}, true},
228 {"two halves", []ports.Rect{{W: 5, H: 10}, {X: 5, W: 5, H: 10}}, true},
229 {"gap", []ports.Rect{{W: 4, H: 10}, {X: 5, W: 5, H: 10}}, false},
230 {"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},
231 {"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},
232 {"too many", make([]ports.Rect, maxCoverRects+1), false},
233 } {
234 if got := covers(tc.rects, r); got != tc.want {
235 t.Errorf("%s: %v, want %v", tc.name, got, tc.want)
236 }
237 }
238}