transform_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close