scale_test.go RawHistoryFolder

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}

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