main f2d166b

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

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 26

Tags 10

↑↓ moveenter openesc close