v0.8.0 446d1e3

scale_test.go RawHistoryFolder

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

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 23

Tags 10

↑↓ moveenter openesc close