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/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
122	setOutputs(1, 1)
123	if got := normal(); !slices.Equal(got, []uint32{1}) {
124		t.Fatalf("rotated transforms %v, want [1]", got)
125	}
126	if got := cursorSeen(); len(got) != 0 {
127		t.Fatalf("cursor surface got transforms %v", got)
128	}
129	// A scale change alone does not repeat the transform.
130	setOutputs(1, 2)
131	if got := normal(); len(got) != 0 {
132		t.Fatalf("unchanged transform resent: %v", got)
133	}
134	setOutputs(3, 2)
135	if got := normal(); !slices.Equal(got, []uint32{3}) {
136		t.Fatalf("changed transforms %v, want [3]", got)
137	}
138	setOutputs(0, 2)
139	if got := normal(); !slices.Equal(got, []uint32{0}) {
140		t.Fatalf("back to normal: %v, want [0]", got)
141	}
142	if got := cursorSeen(); len(got) != 0 {
143		t.Fatalf("cursor surface got transforms %v", got)
144	}
145}
146
147func TestFractionalScaleAndViewport(t *testing.T) {
148	s, events, commands, contents, dir := contentServer(t)
149	c := protocolClient(t, s, dir)
150	// wl_surface.preferred_buffer_scale needs wl_compositor v6.
151	g, _ := c.Registry().FindGlobal("wl_compositor")
152	comp, err := bindWireID(c, g.Name, g.Interface, 6)
153	if err != nil {
154		t.Fatal(err)
155	}
156	wm := bindProtocol(t, c, "xdg_wm_base")
157	shm := bindProtocol(t, c, "wl_shm")
158	fm := bindProtocol(t, c, "wp_fractional_scale_manager_v1")
159	vpm := bindProtocol(t, c, "wp_viewporter")
160	registerProtocol(t, c, shm) // wl_shm sends its formats right away
161	// New IDs must be used in allocation order.
162	surf := c.AllocateID()
163	bufferScales := make(chan int32, 4)
164	sp := &surfaceScaleProxy{scales: bufferScales}
165	sp.SetID(surf)
166	registerWireProxy(c, sp)
167	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
168	frac := c.AllocateID()
169	values := make(chan uint32, 4)
170	proxy := &scaleProxy{values: values}
171	proxy.SetID(frac)
172	registerWireProxy(c, proxy)
173	requestProtocol(t, c, fm, fractionalscale.WpFractionalScaleManagerV1RequestGetFractionalScale, frac, surf)
174	vp := c.AllocateID()
175	requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
176	registerProtocol(t, c, vp)
177	if err := c.Roundtrip(); err != nil {
178		t.Fatal(err)
179	}
180	if v := <-values; v != 120 {
181		t.Fatalf("initial scale %d", v)
182	}
183	// Core switches to 1.5: the surface gets 180/120.
184	commands <- ports.SetOutputs{Outputs: ports.Layout{{Info: testOutputs[0].Info, Width: 1280, Height: 720, Scale: 1.5}}}
185	// Events are read by Roundtrip; the command is applied asynchronously.
186	var got uint32
187	for deadline := time.Now().Add(2 * time.Second); got == 0 && time.Now().Before(deadline); {
188		if err := c.Roundtrip(); err != nil {
189			t.Fatal(err)
190		}
191		select {
192		case got = <-values:
193		case <-time.After(10 * time.Millisecond):
194		}
195	}
196	if got != 180 {
197		t.Fatalf("scale %d", got)
198	}
199	// Clients without fractional scale get the integer scale rounded up.
200	var last int32
201	for len(bufferScales) > 0 {
202		last = <-bufferScales
203	}
204	if last != 2 {
205		t.Fatalf("preferred_buffer_scale %d", last)
206	}
207	// A 30x15 buffer shown at 20x10 logical through the viewport.
208	xdg := c.AllocateID()
209	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
210	serials := make(chan uint32, 4)
211	cp := &configureProxy{serial: serials}
212	cp.SetID(xdg)
213	registerWireProxy(c, cp)
214	top := c.AllocateID()
215	registerProtocol(t, c, top)
216	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
217	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
218	if err := c.Roundtrip(); err != nil {
219		t.Fatal(err)
220	}
221	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)
222	fd, err := unix.MemfdCreate("scaled", 0)
223	if err != nil {
224		t.Fatal(err)
225	}
226	defer unix.Close(fd)
227	size := int32(30 * 15 * 4)
228	if err := unix.Ftruncate(fd, int64(size)); err != nil {
229		t.Fatal(err)
230	}
231	pool, buffer := c.AllocateID(), c.AllocateID()
232	registerProtocol(t, c, pool)
233	registerProtocol(t, c, buffer)
234	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
235		t.Fatal(err)
236	}
237	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(30), int32(15), int32(120), uint32(0))
238	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(20), int32(10))
239	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
240	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
241	if err := c.Roundtrip(); err != nil {
242		t.Fatal(err)
243	}
244	mapped(t, events, 2*time.Second)
245	deadline := time.After(2 * time.Second)
246	for {
247		select {
248		case got := <-contents:
249			if got.SHM == nil {
250				continue
251			}
252			if got.Width != 30 || got.Height != 15 || got.LogicalW != 20 || got.LogicalH != 10 {
253				t.Fatalf("%+v", got)
254			}
255			goto viewportOnly
256		case <-deadline:
257			t.Fatal("no content")
258		}
259	}
260viewportOnly:
261	// Changing only the destination must update content with the current buffer.
262	for _, size := range [][2]int32{{1, 1}, {7, 3}} {
263		requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, size[0], size[1])
264		requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
265		if err := c.Roundtrip(); err != nil {
266			t.Fatal(err)
267		}
268		select {
269		case got := <-contents:
270			if got.LogicalW != int(size[0]) || got.LogicalH != int(size[1]) || got.Width != 30 || got.Height != 15 {
271				t.Fatalf("viewport-only content: %+v", got)
272			}
273		case <-time.After(2 * time.Second):
274			t.Fatal("no viewport-only content")
275		}
276	}
277	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(256), int32(256), int32(5*256), int32(4*256))
278	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(-1), int32(-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 != 5 || got.LogicalH != 4 || got.Source != [4]float32{1, 1, 5, 4} {
286			t.Fatalf("source-only crop: %+v", got)
287		}
288	case <-time.After(2 * time.Second):
289		t.Fatal("no crop content")
290	}
291	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(2), int32(3))
292	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
293	if err := c.Roundtrip(); err != nil {
294		t.Fatal(err)
295	}
296	select {
297	case got := <-contents:
298		if got.LogicalW != 2 || got.LogicalH != 3 || got.Source != [4]float32{1, 1, 5, 4} {
299			t.Fatalf("source and destination: %+v", got)
300		}
301	case <-time.After(2 * time.Second):
302		t.Fatal("no source and destination content")
303	}
304}
305
306func TestBufferScaleLogicalSize(t *testing.T) {
307	st := &surface{bufferScale: 2}
308	if w, h := st.logicalSize(200, 100); w != 100 || h != 50 {
309		t.Fatal(w, h)
310	}
311	st.committedViewport = viewportState{destW: 7, destH: 3, dest: true}
312	if w, h := st.logicalSize(200, 100); w != 7 || h != 3 {
313		t.Fatal(w, h)
314	}
315}
316
317func TestViewportBadSource(t *testing.T) {
318	s, _, _, _, dir := contentServer(t)
319	c := protocolClient(t, s, dir)
320	comp := bindProtocol(t, c, "wl_compositor")
321	vpm := bindProtocol(t, c, "wp_viewporter")
322	surf, vp := c.AllocateID(), c.AllocateID()
323	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
324	requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
325	registerProtocol(t, c, vp)
326	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(-256), int32(0), int32(256), int32(256))
327	expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorBadValue))
328}
329
330func TestViewportApplyErrors(t *testing.T) {
331	for _, tc := range []struct {
332		name        string
333		width       int32
334		destination bool
335		code        viewporter.WpViewportError
336	}{
337		{"bad_size", 128, false, viewporter.WpViewportErrorBadSize},
338		{"out_of_buffer", 512, true, viewporter.WpViewportErrorOutOfBuffer},
339	} {
340		t.Run(tc.name, func(t *testing.T) {
341			s, _, _, _, dir := contentServer(t)
342			c := protocolClient(t, s, dir)
343			comp := bindProtocol(t, c, "wl_compositor")
344			vpm := bindProtocol(t, c, "wp_viewporter")
345			shm := bindProtocol(t, c, "wl_shm")
346			registerProtocol(t, c, shm)
347			surf, vp := c.AllocateID(), c.AllocateID()
348			requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
349			requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
350			registerProtocol(t, c, vp)
351			fd, err := unix.MemfdCreate("viewport", 0)
352			if err != nil {
353				t.Fatal(err)
354			}
355			defer unix.Close(fd)
356			if err := unix.Ftruncate(fd, 4); err != nil {
357				t.Fatal(err)
358			}
359			pool, buf := c.AllocateID(), c.AllocateID()
360			registerProtocol(t, c, pool)
361			registerProtocol(t, c, buf)
362			if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
363				t.Fatal(err)
364			}
365			requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
366			requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), tc.width, int32(256))
367			if tc.destination {
368				requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(1), int32(1))
369			}
370			requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
371			requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
372			expectProtocolError(t, c, vp, uint32(tc.code))
373		})
374	}
375}
376
377// A client rounds the source position and size to 1/256 independently, so
378// their sum can pass the buffer edge by a unit: that is accepted. The
379// values are a real rejected source (y 213.86, height 1946.14 on 2160 rows).
380// One pixel past the edge is still out_of_buffer.
381func TestViewportSourceEdgeRounding(t *testing.T) {
382	for _, tc := range []struct {
383		name  string
384		y, h  int32
385		error bool
386	}{
387		{"rounded edge", 54749, 498212, false},
388		{"one pixel past", 54749, 498212 + 256, true},
389	} {
390		t.Run(tc.name, func(t *testing.T) {
391			s, events, _, contents, dir := contentServer(t)
392			c := protocolClient(t, s, dir)
393			win, surf, _ := surfaceMapper(t, c, events)()
394			drainContents(contents)
395			vpm := bindProtocol(t, c, "wp_viewporter")
396			shm := bindProtocol(t, c, "wl_shm")
397			registerProtocol(t, c, shm)
398			vp := c.AllocateID()
399			requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
400			registerProtocol(t, c, vp)
401			const w, h = 4, 2160
402			fd, err := unix.MemfdCreate("viewport-edge", 0)
403			if err != nil {
404				t.Fatal(err)
405			}
406			defer unix.Close(fd)
407			if err := unix.Ftruncate(fd, w*h*4); err != nil {
408				t.Fatal(err)
409			}
410			pool, buf := c.AllocateID(), c.AllocateID()
411			registerProtocol(t, c, pool)
412			registerProtocol(t, c, buf)
413			if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
414				t.Fatal(err)
415			}
416			requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
417			requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(w), int32(100))
418			requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), tc.y, int32(w*256), tc.h)
419			requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
420			requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
421			if tc.error {
422				expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
423				return
424			}
425			roundtrip(t, c)
426			select {
427			case got := <-contents:
428				if got.ID != win.ID || got.Source[1]+got.Source[3] > h || got.Source[1] == 0 {
429					t.Fatalf("published source outside buffer: %+v", got)
430				}
431			case <-time.After(2 * time.Second):
432				t.Fatal("missing viewport publication")
433			}
434		})
435	}
436}
437
438// A NULL attach unmaps the surface: viewporter.xml exempts it from
439// out_of_buffer, so a source larger than the previous buffer is not an error.
440// A later commit that keeps the retained buffer is still validated.
441func TestViewportNullAttachSkipsBufferBounds(t *testing.T) {
442	s, _, _, _, dir := contentServer(t)
443	c := protocolClient(t, s, dir)
444	comp := bindProtocol(t, c, "wl_compositor")
445	vpm := bindProtocol(t, c, "wp_viewporter")
446	surf, vp := c.AllocateID(), c.AllocateID()
447	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
448	requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
449	registerProtocol(t, c, vp)
450	buf := shmBuffer(t, c)
451	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(256), int32(256))
452	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
453	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
454	roundtrip(t, c)
455
456	// Oversized source with a NULL attach: no error.
457	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetSource, int32(0), int32(0), int32(512), int32(256))
458	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
459	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
460	roundtrip(t, c)
461
462	// Reattach the 1x1 buffer: the oversized source now applies to it.
463	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
464	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
465	expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorOutOfBuffer))
466}
467
468func TestViewportBadDestination(t *testing.T) {
469	s, _, _, _, dir := contentServer(t)
470	c := protocolClient(t, s, dir)
471	comp := bindProtocol(t, c, "wl_compositor")
472	vpm := bindProtocol(t, c, "wp_viewporter")
473	surf, vp := c.AllocateID(), c.AllocateID()
474	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
475	requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vp, surf)
476	registerProtocol(t, c, vp)
477	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(0), int32(10))
478	expectProtocolError(t, c, vp, uint32(viewporter.WpViewportErrorBadValue))
479}

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