feat/hint-hybrid aed7ca4

renderer_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"image"
5	"image/color"
6	"os"
7	"testing"
8
9	"github.com/bnema/neferwl/internal/ports"
10	"golang.org/x/sys/unix"
11)
12
13func TestRendererClear(t *testing.T) {
14	r, err := New(64, 48)
15	if err != nil {
16		t.Skipf("Vulkan unavailable: %v", err)
17	}
18	defer r.Close()
19	if err := r.Clear([3]uint8{0x10, 0x20, 0x30}); err != nil {
20		t.Fatal(err)
21	}
22	pixels := readPixels(t, r)
23	want := color.RGBA{0x10, 0x20, 0x30, 255}
24	for _, p := range [][2]int{{0, 0}, {63, 47}} {
25		if got := pixels.At(p[0], p[1]); got != want {
26			t.Errorf("pixel %v = %v, want %v", p, got, want)
27		}
28	}
29}
30
31func TestParseColor(t *testing.T) {
32	for _, tc := range []struct {
33		s    string
34		want [3]uint8
35	}{{"#102030", [3]uint8{16, 32, 48}}, {"#FFFFFF", [3]uint8{255, 255, 255}}, {"bad", [3]uint8{}}, {"#12gg34", [3]uint8{}}, {"102030", [3]uint8{}}} {
36		if got := parseColor(tc.s); got != tc.want {
37			t.Errorf("parseColor(%q)=%v want %v", tc.s, got, tc.want)
38		}
39	}
40}
41
42func TestRendererRender(t *testing.T) {
43	r, err := New(64, 48)
44	if err != nil {
45		t.Skipf("Vulkan unavailable: %v", err)
46	}
47	defer r.Close()
48	bg := color.RGBA{16, 32, 48, 255}
49	wc, c2, fc := color.RGBA{200, 10, 10, 255}, color.RGBA{10, 200, 10, 255}, color.RGBA{10, 10, 200, 255}
50	contents := map[ports.WindowID]ports.SurfaceContent{
51		1: solidContent(t, 64, 48, wc), 2: solidContent(t, 64, 48, c2), 3: solidContent(t, 64, 48, fc),
52	}
53	check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
54		t.Helper()
55		if err := render(r, s, contents); err != nil {
56			t.Fatal(err)
57		}
58		for p, want := range expected {
59			if got := readPixels(t, r).At(p.X, p.Y); got != want {
60				t.Errorf("pixel %v=%v want %v", p, got, want)
61			}
62		}
63	}
64	// The client sits inside its inset sides; separators are drawn over
65	// the windows in order, with the border colors.
66	win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll}
67	gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255}
68	s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}
69	check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: bg, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 22}: bg, {24, 24}: bg})
70	s.Separators = []ports.Separator{
71		{Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}},
72		{Rect: ports.Rect{X: 8, Y: 8, W: 8, H: 2}, Active: true},
73	}
74	check(s, map[image.Point]color.RGBA{{9, 9}: lit, {15, 9}: lit, {16, 9}: gray, {23, 9}: gray, {10, 10}: wc})
75	s.Border.Inactive = ""
76	check(s, map[image.Point]color.RGBA{{9, 9}: lit, {16, 9}: bg})
77	// A float covers the tile lines; its own border is drawn with it.
78	s.Windows = []ports.SceneWindow{win, {ID: 3, Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 8}, Floating: true, Inset: ports.SideAll}}
79	s.Separators = []ports.Separator{
80		{Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}, Active: true},
81		{Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 1}, Active: true, Window: 3},
82	}
83	check(s, map[image.Point]color.RGBA{{9, 9}: lit, {14, 9}: fc, {14, 4}: lit})
84	s.Windows = []ports.SceneWindow{win}
85	s.Separators, s.Border.Inactive = nil, "#313244"
86	s.Windows[0].Fullscreen = true
87	check(s, map[image.Point]color.RGBA{{9, 9}: wc})
88	s.Windows[0].Fullscreen = false
89	s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}}
90	check(s, map[image.Point]color.RGBA{{5, 5}: c2, {15, 5}: bg})
91	s.Windows[0].Hidden = true
92	check(s, map[image.Point]color.RGBA{{5, 5}: bg})
93}
94
95// A faded window blends with the background (wp_alpha_modifier_v1).
96func TestRendererFade(t *testing.T) {
97	r, err := New(64, 48)
98	if err != nil {
99		t.Skipf("Vulkan unavailable: %v", err)
100	}
101	defer r.Close()
102	c := solidContent(t, 64, 48, color.RGBA{200, 0, 0, 255})
103	c.Fade = 0.5
104	c.Opaque = false
105	s := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}}
106	if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
107		t.Fatal(err)
108	}
109	if got := readPixels(t, r).At(10, 10).(color.RGBA); got.R < 99 || got.R > 101 || got.G != 0 || got.B != 0 {
110		t.Fatalf("faded pixel %v", got)
111	}
112}
113
114func TestRendererContents(t *testing.T) {
115	r, err := New(64, 48)
116	if err != nil {
117		t.Skipf("Vulkan unavailable: %v", err)
118	}
119	defer r.Close()
120	bg := color.RGBA{16, 32, 48, 255}
121	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 20, H: 10}}}}
122	pixels := make([]byte, 4*2*4)
123	for i := 0; i < len(pixels); i += 4 {
124		copy(pixels[i:i+4], []byte{0, 0, 255, 255})
125	}
126	render := func(c *ports.SurfaceContent) {
127		t.Helper()
128		var contents map[ports.WindowID]ports.SurfaceContent
129		if c != nil {
130			contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
131		}
132		if err := render(r, scene, contents); err != nil {
133			t.Fatal(err)
134		}
135	}
136	check := func(x, y int, want color.RGBA) {
137		t.Helper()
138		if got := readPixels(t, r).At(x, y); got != want {
139			t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)
140		}
141	}
142	render(shmContent(t, 4, 2, 16, pixels))
143	check(8, 8, color.RGBA{255, 0, 0, 255})
144	check(11, 9, color.RGBA{255, 0, 0, 255})
145	check(12, 8, bg)
146	check(0, 0, bg)
147	scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}
148	padded := make([]byte, 4*20)
149	copy(padded[8:12], []byte{7, 11, 23, 255})
150	render(shmContent(t, 4, 4, 20, padded))
151	check(0, 0, color.RGBA{23, 11, 7, 255})
152	// A window without a buffer yet shows the background, not a color.
153	render(nil)
154	check(0, 0, bg)
155}
156
157func TestRendererUploadOrderAndOpaque(t *testing.T) {
158	r, err := New(32, 32)
159	if err != nil {
160		t.Skipf("Vulkan unavailable: %v", err)
161	}
162	defer r.Close()
163	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{
164		{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
165		{ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
166	}}
167	top := color.RGBA{10, 200, 10, 255}
168	upper := map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255}), 2: solidContent(t, 16, 16, top)}
169	for id, c := range upper {
170		c.Version = 1 // the opaque buffer below is another commit
171		upper[id] = c
172	}
173	if err := render(r, scene, upper); err != nil {
174		t.Fatal(err)
175	}
176	if got, want := readPixels(t, r).RGBAAt(10, 10), top; got != want {
177		t.Errorf("overlap = %v, want %v", got, want)
178	}
179	scene.Border = ports.Border{Width: 4, Active: "#ffffff"}
180	scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true, Inset: ports.SideAll}}
181	scene.Separators = []ports.Separator{{Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 4}, Active: true}}
182	px := make([]byte, 16*16*4)
183	for i := 0; i < len(px); i += 4 {
184		copy(px[i:i+4], []byte{3, 5, 7, 0})
185	}
186	opaque := shmContent(t, 16, 16, 64, px)
187	opaque.Opaque = true
188	contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
189	if err := render(r, scene, contents); err != nil {
190		t.Fatal(err)
191	}
192	for _, tc := range []struct {
193		x, y int
194		want color.RGBA
195	}{{3, 3, color.RGBA{255, 255, 255, 255}}, {10, 10, color.RGBA{7, 5, 3, 255}}} {
196		if got := readPixels(t, r).RGBAAt(tc.x, tc.y); got != tc.want {
197			t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want)
198		}
199	}
200}
201
202func TestRendererLayers(t *testing.T) {
203	r, err := New(64, 48)
204	if err != nil {
205		t.Skipf("Vulkan unavailable: %v", err)
206	}
207	defer r.Close()
208	red := make([]byte, 64*8*4)
209	for i := 0; i < len(red); i += 4 {
210		copy(red[i:i+4], []byte{0, 0, 255, 0})
211	}
212	layer := shmContent(t, 64, 8, 64*4, red)
213	layer.ID, layer.Opaque = 2, true
214	contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}
215	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}}
216	window := color.RGBA{16, 32, 48, 255} // no buffer yet: background
217	check := func(want color.RGBA) {
218		t.Helper()
219		if err := render(r, scene, contents); err != nil {
220			t.Fatal(err)
221		}
222		if got := readPixels(t, r).RGBAAt(5, 2); got != want {
223			t.Errorf("pixel = %v, want %v", got, want)
224		}
225	}
226	check(color.RGBA{255, 0, 0, 255})
227	scene.Layers[0].Layer = ports.LayerBottom
228	check(window)
229	// Core leaves hidden layers out of the scene; the renderer draws what
230	// it gets, over a fullscreen window too.
231	scene.Windows[0].Fullscreen = true
232	scene.Layers[0].Layer = ports.LayerOverlay
233	check(color.RGBA{255, 0, 0, 255})
234	scene.Windows[0].Hidden = true
235	scene.Layers[0].Layer = ports.LayerBottom
236	check(color.RGBA{255, 0, 0, 255})
237	delete(contents, 2)
238	check(color.RGBA{16, 32, 48, 255})
239}
240
241// solid returns a w×h B8G8R8A8 buffer of one color.
242func solid(w, h int, c [3]uint8) []byte {
243	p := make([]byte, w*h*4)
244	for i := 0; i < len(p); i += 4 {
245		p[i], p[i+1], p[i+2], p[i+3] = c[2], c[1], c[0], 255
246	}
247	return p
248}
249
250func TestRendererScale(t *testing.T) {
251	r, err := New(64, 48)
252	if err != nil {
253		t.Skipf("Vulkan unavailable: %v", err)
254	}
255	defer r.Close()
256	red := [3]uint8{200, 0, 0}
257	// Scale 2: a 10x10 logical window at (4,4) covers physical (8,8)-(28,28);
258	// its 20x20 buffer (fractional client) is copied 1:1.
259	s := ports.Scene{Scale: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 10, H: 10}}}}
260	scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
261	scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
262	content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
263	if err := render(r, s, content); err != nil {
264		t.Fatal(err)
265	}
266	px := readPixels(t, r)
267	want := color.RGBA{200, 0, 0, 255}
268	for _, p := range []image.Point{{8, 8}, {27, 27}} {
269		if got := px.At(p.X, p.Y); got != want {
270			t.Errorf("%v = %v", p, got)
271		}
272	}
273	for _, p := range []image.Point{{7, 7}, {28, 28}} {
274		if got := px.At(p.X, p.Y); got != (color.RGBA{0, 0, 0, 255}) {
275			t.Errorf("outside %v = %v", p, got)
276		}
277	}
278	// Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20
279	// buffer is 10x10 logical and downscaled to 15x15 physical at (6,6).
280	s.Scale = 1.5
281	if err := render(r, s, content); err != nil {
282		t.Fatal(err)
283	}
284	px = readPixels(t, r)
285	for _, p := range []image.Point{{6, 6}, {20, 20}} {
286		if got := px.At(p.X, p.Y); got != want {
287			t.Errorf("1.5 %v = %v", p, got)
288		}
289	}
290	if got := px.At(21, 21); got != (color.RGBA{0, 0, 0, 255}) {
291		t.Errorf("1.5 outside = %v", got)
292	}
293	// A buffer 1px wider than its slot is clipped, not resampled.
294	odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
295	odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
296	content[1] = *odd
297	if err := render(r, s, content); err != nil {
298		t.Fatal(err)
299	}
300	if got := readPixels(t, r).At(21, 10); got != (color.RGBA{0, 0, 0, 255}) {
301		t.Errorf("clipped column = %v", got)
302	}
303}
304
305// An overview preview draws the whole buffer shrunk into its rect, not
306// the buffer's corner.
307func TestRendererPreview(t *testing.T) {
308	r, err := New(64, 48)
309	if err != nil {
310		t.Skipf("Vulkan unavailable: %v", err)
311	}
312	defer r.Close()
313	// 40x20 buffer: left half red, right half blue, drawn at half size.
314	px := make([]byte, 40*20*4)
315	for i := 0; i < len(px); i += 4 {
316		if (i/4)%40 < 20 {
317			px[i+2] = 200
318		} else {
319			px[i] = 200
320		}
321		px[i+3] = 255
322	}
323	c := shmContent(t, 40, 20, 160, px)
324	c.ID = 1
325	s := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 20, H: 10}, Preview: 0.5}}}
326	if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
327		t.Fatal(err)
328	}
329	out := readPixels(t, r)
330	for p, want := range map[image.Point]color.RGBA{
331		{5, 5}:   {200, 0, 0, 255},
332		{22, 12}: {0, 0, 200, 255},
333		{25, 5}:  {0, 0, 0, 255},
334		{5, 15}:  {0, 0, 0, 255},
335	} {
336		if got := out.At(p.X, p.Y); got != want {
337			t.Errorf("%v = %v, want %v", p, got, want)
338		}
339	}
340}
341
342// A buffer shrunk under half size averages its texels: one-pixel white
343// and black columns shrunk 4× come out an even gray, not a beat pattern.
344func TestRendererPreviewSmooth(t *testing.T) {
345	r, err := New(64, 48)
346	if err != nil {
347		t.Skipf("Vulkan unavailable: %v", err)
348	}
349	defer r.Close()
350	px := make([]byte, 80*40*4)
351	for i := 0; i < len(px); i += 4 {
352		if (i/4)%2 == 0 {
353			px[i], px[i+1], px[i+2] = 255, 255, 255
354		}
355		px[i+3] = 255
356	}
357	c := shmContent(t, 80, 40, 320, px)
358	c.ID = 1
359	s := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 10}, Preview: 0.25}}}
360	if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
361		t.Fatal(err)
362	}
363	out := readPixels(t, r)
364	for x := 1; x < 19; x++ {
365		if v := out.RGBAAt(x, 5).R; v < 100 || v > 155 {
366			t.Fatalf("x=%d: %d, want mid gray", x, v)
367		}
368	}
369}
370
371// solidContent is a w×h B8G8R8A8 buffer of one color.
372func solidContent(t testing.TB, w, h int, c color.RGBA) ports.SurfaceContent {
373	px := make([]byte, w*h*4)
374	for i := 0; i < len(px); i += 4 {
375		px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255
376	}
377	return *shmContent(t, w, h, w*4, px)
378}
379
380// shmContent puts pixels in a memfd pool, like a wl_shm client.
381func shmContent(t testing.TB, w, h, stride int, pixels []byte) *ports.SurfaceContent {
382	t.Helper()
383	fd, err := unix.MemfdCreate("shm-test", unix.MFD_CLOEXEC)
384	if err != nil {
385		t.Fatal(err)
386	}
387	f := os.NewFile(uintptr(fd), "shm-test")
388	t.Cleanup(func() { f.Close() })
389	if _, err := f.Write(pixels); err != nil {
390		t.Fatal(err)
391	}
392	shmPools++
393	return &ports.SurfaceContent{Width: w, Height: h, SHM: &ports.SHMBuffer{Pool: shmPools, File: f, Stride: stride}}
394}
395
396// shmPools numbers test pools: a renderer maps each pool ID once.
397var shmPools uint64
398
399// The window geometry lands on the window rect and the client shadow
400// around it is clipped; subsurfaces draw from the root origin, above or
401// below it.
402func TestRendererGeometryAndSubsurfaces(t *testing.T) {
403	r, err := New(64, 48)
404	if err != nil {
405		t.Skipf("Vulkan unavailable: %v", err)
406	}
407	defer r.Close()
408	red, green, blue := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 255, 0, 255}, color.RGBA{0, 0, 255, 255}
409	bg := color.RGBA{16, 32, 48, 255}
410	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}}}}
411	// A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
412	root := solidContent(t, 30, 20, red)
413	root.Surface, root.Version = 1, 1
414	root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
415	// Green covers the window's top-left 4×4 corner; blue sits below the
416	// root, hidden by it.
417	below := solidContent(t, 30, 20, blue)
418	below.Surface, below.Version = 2, 1
419	above := solidContent(t, 4, 4, green)
420	above.Surface, above.Version = 3, 1
421	root.Children = []ports.Subsurface{
422		{X: 3, Y: 3, Below: true, SurfaceContent: below},
423		{X: 5, Y: 5, SurfaceContent: above},
424	}
425	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
426		t.Fatal(err)
427	}
428	px := readPixels(t, r)
429	for _, c := range []struct {
430		x, y int
431		want color.RGBA
432	}{
433		{10, 10, green}, {13, 13, green}, {14, 14, red}, {29, 19, red},
434		// The shadow and the child reaching past it are clipped.
435		{9, 10, bg}, {30, 19, bg}, {29, 20, bg},
436	} {
437		if got := px.At(c.x, c.y); got != c.want {
438			t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
439		}
440	}
441	// A root with no buffer of its own still shows its children.
442	only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
443	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
444		t.Fatal(err)
445	}
446	if got := readPixels(t, r).At(20, 15); got != blue {
447		t.Errorf("child only: %v", got)
448	}
449}
450
451// A tall fill covers every row through the last one.
452func TestRendererTallFill(t *testing.T) {
453	h := 135
454	r, err := New(8, h)
455	if err != nil {
456		t.Skipf("Vulkan unavailable: %v", err)
457	}
458	defer r.Close()
459	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
460	wc := color.RGBA{200, 10, 10, 255}
461	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 6, h, wc)}); err != nil {
462		t.Fatal(err)
463	}
464	bg := color.RGBA{16, 32, 48, 255}
465	px := readPixels(t, r)
466	for y := 0; y < h; y++ {
467		if got := px.At(1, y); got != wc {
468			t.Fatalf("At(1,%d)=%v want %v", y, got, wc)
469		}
470		if got := px.At(6, y); got != wc {
471			t.Fatalf("At(6,%d)=%v want %v", y, got, wc)
472		}
473		if got := px.At(7, y); got != bg {
474			t.Fatalf("At(7,%d)=%v want background", y, got)
475		}
476	}
477}
478
479// fill returns a w×h premultiplied B8G8R8A8 buffer of one pixel.
480func fill(w, h int, bgra [4]byte) []byte {
481	p := make([]byte, w*h*4)
482	for i := 0; i < len(p); i += 4 {
483		copy(p[i:i+4], bgra[:])
484	}
485	return p
486}
487
488// Translucent buffers blend over what is below them, in paint order: a
489// bottom layer (one draw), an opaque window copied over it, then two
490// overlapping overlays sharing one draw. A transparent pixel changes nothing.
491func TestRendererAlphaBlend(t *testing.T) {
492	r, err := New(64, 48)
493	if err != nil {
494		t.Skipf("Vulkan unavailable: %v", err)
495	}
496	defer r.Close()
497	halfRed, halfBlue := [4]byte{0, 0, 128, 128}, [4]byte{128, 0, 0, 128}
498	partly := fill(16, 8, halfRed)
499	for y := range 8 {
500		copy(partly[y*64+48:y*64+64], make([]byte, 16)) // last 4 pixels: transparent
501	}
502	white := shmContent(t, 32, 48, 32*4, solid(32, 48, [3]uint8{255, 255, 255}))
503	white.Opaque = true
504	contents := map[ports.WindowID]ports.SurfaceContent{
505		1: *white,
506		2: *shmContent(t, 64, 48, 64*4, fill(64, 48, halfRed)),
507		3: *shmContent(t, 16, 8, 16*4, partly),
508		4: *shmContent(t, 16, 8, 16*4, fill(16, 8, halfBlue)),
509	}
510	scene := ports.Scene{Background: "#000000",
511		Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 32, H: 48}}},
512		Layers: []ports.SceneLayer{
513			{ID: 2, Layer: ports.LayerBottom, Rect: ports.Rect{W: 64, H: 48}},
514			{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}},
515			{ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},
516		}}
517	if err := render(r, scene, contents); err != nil {
518		t.Fatal(err)
519	}
520	px := readPixels(t, r)
521	near := func(a, b uint8) bool { d := int(a) - int(b); return d >= -1 && d <= 1 }
522	for _, tc := range []struct {
523		name    string
524		x, y    int
525		r, g, b uint8
526	}{
527		{"bottom layer over background", 40, 20, 128, 0, 0},
528		{"opaque window over bottom layer", 20, 20, 255, 255, 255},
529		{"red overlay over window", 4, 4, 255, 127, 127},
530		{"blue over red over window", 10, 4, 127, 63, 191},
531		{"blue over window", 20, 4, 127, 127, 255},
532		{"transparent red pixels: blue over window only", 14, 4, 127, 127, 255},
533	} {
534		got := px.RGBAAt(tc.x, tc.y)
535		if !near(got.R, tc.r) || !near(got.G, tc.g) || !near(got.B, tc.b) {
536			t.Errorf("%s: pixel (%d,%d) = %v, want ~{%d %d %d}", tc.name, tc.x, tc.y, got, tc.r, tc.g, tc.b)
537		}
538	}
539}
540
541// A window kept after its client closed (a closing window fading out) still
542// draws from the GPU copy of its content: the pool file is closed and its
543// mapping gone, and the copy is found without reading the pool.
544func TestRendererSHMKeptWindowDrawsFromCopy(t *testing.T) {
545	r, err := New(32, 32)
546	if err != nil {
547		t.Skipf("Vulkan unavailable: %v", err)
548	}
549	defer r.Close()
550	scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
551	c := solidContent(t, 16, 16, color.RGBA{R: 255, A: 255})
552	c.ID, c.Surface, c.Seq, c.Version = 1, 1, 1, 1
553	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
554		t.Fatal(err)
555	}
556	// Wayland closed the pool; the mapping was dropped meanwhile.
557	if err := c.SHM.File.Close(); err != nil {
558		t.Fatal(err)
559	}
560	r.dropPools()
561	for pool, m := range r.pools {
562		_ = unix.Munmap(m.data)
563		delete(r.pools, pool)
564	}
565	// The window moves (a fade-out), so it draws again.
566	scene.Seq = 2
567	scene.Windows[0].Rect.X = 4
568	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
569		t.Fatal(err)
570	}
571	got := readPixels(t, r)
572	if g, want := got.At(18, 8), (color.RGBA{R: 255, A: 255}); g != want {
573		t.Fatalf("kept window pixel %v, want %v", g, want)
574	}
575	if g, want := got.At(1, 8), (color.RGBA{A: 255}); g != want {
576		t.Fatalf("background pixel %v, want %v", g, want)
577	}
578}

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