v0.1.0 d9218d6

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 := r.Pixels()
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	check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
50		t.Helper()
51		if err := render(r, s, nil); err != nil {
52			t.Fatal(err)
53		}
54		for p, want := range expected {
55			if got := r.Pixels().At(p.X, p.Y); got != want {
56				t.Errorf("pixel %v=%v want %v", p, got, want)
57			}
58		}
59	}
60	// The client sits inside its inset sides; separators are drawn over
61	// the windows in order, with the border colors.
62	win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll}
63	c := windowColor(1)
64	wc := color.RGBA{c[0], c[1], c[2], 255}
65	gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255}
66	s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}
67	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})
68	s.Separators = []ports.Separator{
69		{Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}},
70		{Rect: ports.Rect{X: 8, Y: 8, W: 8, H: 2}, Active: true},
71	}
72	check(s, map[image.Point]color.RGBA{{9, 9}: lit, {15, 9}: lit, {16, 9}: gray, {23, 9}: gray, {10, 10}: wc})
73	s.Border.Inactive = ""
74	check(s, map[image.Point]color.RGBA{{9, 9}: lit, {16, 9}: bg})
75	// A float covers the tile lines; its own border is drawn with it.
76	s.Windows = []ports.SceneWindow{win, {ID: 3, Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 8}, Floating: true, Inset: ports.SideAll}}
77	s.Separators = []ports.Separator{
78		{Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}, Active: true},
79		{Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 1}, Active: true, Window: 3},
80	}
81	fc := windowColor(3)
82	check(s, map[image.Point]color.RGBA{{9, 9}: lit, {14, 9}: {fc[0], fc[1], fc[2], 255}, {14, 4}: lit})
83	s.Windows = []ports.SceneWindow{win}
84	s.Separators, s.Border.Inactive = nil, "#313244"
85	s.Windows[0].Fullscreen = true
86	check(s, map[image.Point]color.RGBA{{9, 9}: wc})
87	s.Windows[0].Fullscreen = false
88	s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}}
89	c = windowColor(2)
90	check(s, map[image.Point]color.RGBA{{5, 5}: {c[0], c[1], c[2], 255}, {15, 5}: bg})
91	s.Windows[0].Hidden = true
92	check(s, map[image.Point]color.RGBA{{5, 5}: bg})
93}
94
95func TestRendererContents(t *testing.T) {
96	r, err := New(64, 48)
97	if err != nil {
98		t.Skipf("Vulkan unavailable: %v", err)
99	}
100	defer r.Close()
101	bg := color.RGBA{16, 32, 48, 255}
102	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 20, H: 10}}}}
103	pixels := make([]byte, 4*2*4)
104	for i := 0; i < len(pixels); i += 4 {
105		copy(pixels[i:i+4], []byte{0, 0, 255, 255})
106	}
107	render := func(c *ports.SurfaceContent) {
108		t.Helper()
109		var contents map[ports.WindowID]ports.SurfaceContent
110		if c != nil {
111			contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
112		}
113		if err := render(r, scene, contents); err != nil {
114			t.Fatal(err)
115		}
116	}
117	check := func(x, y int, want color.RGBA) {
118		t.Helper()
119		if got := r.Pixels().At(x, y); got != want {
120			t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)
121		}
122	}
123	render(shmContent(t, 4, 2, 16, pixels))
124	check(8, 8, color.RGBA{255, 0, 0, 255})
125	check(11, 9, color.RGBA{255, 0, 0, 255})
126	check(12, 8, bg)
127	check(0, 0, bg)
128	scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}
129	padded := make([]byte, 4*20)
130	copy(padded[8:12], []byte{7, 11, 23, 255})
131	render(shmContent(t, 4, 4, 20, padded))
132	check(0, 0, color.RGBA{23, 11, 7, 255})
133	render(nil)
134	c := windowColor(1)
135	check(0, 0, color.RGBA{c[0], c[1], c[2], 255})
136}
137
138func TestRendererUploadOrderAndOpaque(t *testing.T) {
139	r, err := New(32, 32)
140	if err != nil {
141		t.Skipf("Vulkan unavailable: %v", err)
142	}
143	defer r.Close()
144	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{
145		{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
146		{ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
147	}}
148	if err := render(r, scene, nil); err != nil {
149		t.Fatal(err)
150	}
151	c := windowColor(2)
152	if got, want := r.Pixels().RGBAAt(10, 10), (color.RGBA{c[0], c[1], c[2], 255}); got != want {
153		t.Errorf("overlap = %v, want %v", got, want)
154	}
155	scene.Border = ports.Border{Width: 4, Active: "#ffffff"}
156	scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true, Inset: ports.SideAll}}
157	scene.Separators = []ports.Separator{{Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 4}, Active: true}}
158	px := make([]byte, 16*16*4)
159	for i := 0; i < len(px); i += 4 {
160		copy(px[i:i+4], []byte{3, 5, 7, 0})
161	}
162	opaque := shmContent(t, 16, 16, 64, px)
163	opaque.Opaque = true
164	contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
165	if err := render(r, scene, contents); err != nil {
166		t.Fatal(err)
167	}
168	for _, tc := range []struct {
169		x, y int
170		want color.RGBA
171	}{{3, 3, color.RGBA{255, 255, 255, 255}}, {10, 10, color.RGBA{7, 5, 3, 255}}} {
172		if got := r.Pixels().RGBAAt(tc.x, tc.y); got != tc.want {
173			t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want)
174		}
175	}
176}
177
178func TestRendererLayers(t *testing.T) {
179	r, err := New(64, 48)
180	if err != nil {
181		t.Skipf("Vulkan unavailable: %v", err)
182	}
183	defer r.Close()
184	red := make([]byte, 64*8*4)
185	for i := 0; i < len(red); i += 4 {
186		copy(red[i:i+4], []byte{0, 0, 255, 0})
187	}
188	layer := shmContent(t, 64, 8, 64*4, red)
189	layer.ID, layer.Opaque = 2, true
190	contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}
191	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}}}}
192	wc := windowColor(1)
193	window := color.RGBA{wc[0], wc[1], wc[2], 255}
194	check := func(want color.RGBA) {
195		t.Helper()
196		if err := render(r, scene, contents); err != nil {
197			t.Fatal(err)
198		}
199		if got := r.Pixels().RGBAAt(5, 2); got != want {
200			t.Errorf("pixel = %v, want %v", got, want)
201		}
202	}
203	check(color.RGBA{255, 0, 0, 255})
204	scene.Layers[0].Layer = ports.LayerBottom
205	check(window)
206	scene.Windows[0].Fullscreen = true
207	scene.Layers[0].Layer = ports.LayerTop
208	check(window)
209	scene.Layers[0].Layer = ports.LayerOverlay
210	check(color.RGBA{255, 0, 0, 255})
211	scene.Windows[0].Hidden = true
212	scene.Layers[0].Layer = ports.LayerBottom
213	check(color.RGBA{255, 0, 0, 255})
214	delete(contents, 2)
215	check(color.RGBA{16, 32, 48, 255})
216}
217
218// solid returns a w×h B8G8R8A8 buffer of one color.
219func solid(w, h int, c [3]uint8) []byte {
220	p := make([]byte, w*h*4)
221	for i := 0; i < len(p); i += 4 {
222		p[i], p[i+1], p[i+2], p[i+3] = c[2], c[1], c[0], 255
223	}
224	return p
225}
226
227func TestRendererScale(t *testing.T) {
228	r, err := New(64, 48)
229	if err != nil {
230		t.Skipf("Vulkan unavailable: %v", err)
231	}
232	defer r.Close()
233	red := [3]uint8{200, 0, 0}
234	// Scale 2: a 10x10 logical window at (4,4) covers physical (8,8)-(28,28);
235	// its 20x20 buffer (fractional client) is copied 1:1.
236	s := ports.Scene{Scale: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 10, H: 10}}}}
237	scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
238	scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
239	content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
240	if err := render(r, s, content); err != nil {
241		t.Fatal(err)
242	}
243	px := r.Pixels()
244	want := color.RGBA{200, 0, 0, 255}
245	for _, p := range []image.Point{{8, 8}, {27, 27}} {
246		if got := px.At(p.X, p.Y); got != want {
247			t.Errorf("%v = %v", p, got)
248		}
249	}
250	for _, p := range []image.Point{{7, 7}, {28, 28}} {
251		if got := px.At(p.X, p.Y); got != (color.RGBA{0, 0, 0, 255}) {
252			t.Errorf("outside %v = %v", p, got)
253		}
254	}
255	// Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20
256	// buffer is 10x10 logical and downscaled to 15x15 physical at (6,6).
257	s.Scale = 1.5
258	if err := render(r, s, content); err != nil {
259		t.Fatal(err)
260	}
261	px = r.Pixels()
262	for _, p := range []image.Point{{6, 6}, {20, 20}} {
263		if got := px.At(p.X, p.Y); got != want {
264			t.Errorf("1.5 %v = %v", p, got)
265		}
266	}
267	if got := px.At(21, 21); got != (color.RGBA{0, 0, 0, 255}) {
268		t.Errorf("1.5 outside = %v", got)
269	}
270	// A buffer 1px wider than its slot is clipped, not resampled.
271	odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
272	odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
273	content[1] = *odd
274	if err := render(r, s, content); err != nil {
275		t.Fatal(err)
276	}
277	if got := r.Pixels().At(21, 10); got != (color.RGBA{0, 0, 0, 255}) {
278		t.Errorf("clipped column = %v", got)
279	}
280}
281
282// solidContent is a w×h B8G8R8A8 buffer of one color.
283func solidContent(t *testing.T, w, h int, c color.RGBA) ports.SurfaceContent {
284	px := make([]byte, w*h*4)
285	for i := 0; i < len(px); i += 4 {
286		px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255
287	}
288	return *shmContent(t, w, h, w*4, px)
289}
290
291// shmContent puts pixels in a memfd pool, like a wl_shm client.
292func shmContent(t *testing.T, w, h, stride int, pixels []byte) *ports.SurfaceContent {
293	t.Helper()
294	fd, err := unix.MemfdCreate("shm-test", unix.MFD_CLOEXEC)
295	if err != nil {
296		t.Fatal(err)
297	}
298	f := os.NewFile(uintptr(fd), "shm-test")
299	t.Cleanup(func() { f.Close() })
300	if _, err := f.Write(pixels); err != nil {
301		t.Fatal(err)
302	}
303	shmPools++
304	return &ports.SurfaceContent{Width: w, Height: h, SHM: &ports.SHMBuffer{Pool: shmPools, File: f, Stride: stride}}
305}
306
307// shmPools numbers test pools: a renderer maps each pool ID once.
308var shmPools uint64
309
310// The window geometry lands on the window rect and the client shadow
311// around it is clipped; subsurfaces draw from the root origin, above or
312// below it.
313func TestRendererGeometryAndSubsurfaces(t *testing.T) {
314	r, err := New(64, 48)
315	if err != nil {
316		t.Skipf("Vulkan unavailable: %v", err)
317	}
318	defer r.Close()
319	red, green, blue := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 255, 0, 255}, color.RGBA{0, 0, 255, 255}
320	bg := color.RGBA{16, 32, 48, 255}
321	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}}}}
322	// A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
323	root := solidContent(t, 30, 20, red)
324	root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
325	// Green covers the window's top-left 4×4 corner; blue sits below the
326	// root, hidden by it.
327	root.Children = []ports.Subsurface{
328		{X: 3, Y: 3, Below: true, SurfaceContent: solidContent(t, 30, 20, blue)},
329		{X: 5, Y: 5, SurfaceContent: solidContent(t, 4, 4, green)},
330	}
331	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
332		t.Fatal(err)
333	}
334	px := r.Pixels()
335	for _, c := range []struct {
336		x, y int
337		want color.RGBA
338	}{
339		{10, 10, green}, {13, 13, green}, {14, 14, red}, {29, 19, red},
340		// The shadow and the child reaching past it are clipped.
341		{9, 10, bg}, {30, 19, bg}, {29, 20, bg},
342	} {
343		if got := px.At(c.x, c.y); got != c.want {
344			t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
345		}
346	}
347	// A root with no buffer of its own still shows its children.
348	only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
349	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
350		t.Fatal(err)
351	}
352	if got := r.Pixels().At(20, 15); got != blue {
353		t.Errorf("child only: %v", got)
354	}
355}
356
357// A tall fill covers every row through the last one.
358func TestRendererTallFill(t *testing.T) {
359	h := 135
360	r, err := New(8, h)
361	if err != nil {
362		t.Skipf("Vulkan unavailable: %v", err)
363	}
364	defer r.Close()
365	scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
366	if err := render(r, scene, nil); err != nil {
367		t.Fatal(err)
368	}
369	c := windowColor(1)
370	wc := color.RGBA{c[0], c[1], c[2], 255}
371	bg := color.RGBA{16, 32, 48, 255}
372	px := r.Pixels()
373	for y := 0; y < h; y++ {
374		if got := px.At(1, y); got != wc {
375			t.Fatalf("At(1,%d)=%v want %v", y, got, wc)
376		}
377		if got := px.At(6, y); got != wc {
378			t.Fatalf("At(6,%d)=%v want %v", y, got, wc)
379		}
380		if got := px.At(7, y); got != bg {
381			t.Fatalf("At(7,%d)=%v want background", y, got)
382		}
383	}
384}
385
386// fill returns a w×h premultiplied B8G8R8A8 buffer of one pixel.
387func fill(w, h int, bgra [4]byte) []byte {
388	p := make([]byte, w*h*4)
389	for i := 0; i < len(p); i += 4 {
390		copy(p[i:i+4], bgra[:])
391	}
392	return p
393}
394
395// Translucent buffers blend over what is below them, in paint order: a
396// bottom layer (one draw), an opaque window copied over it, then two
397// overlapping overlays sharing one draw. A transparent pixel changes nothing.
398func TestRendererAlphaBlend(t *testing.T) {
399	r, err := New(64, 48)
400	if err != nil {
401		t.Skipf("Vulkan unavailable: %v", err)
402	}
403	defer r.Close()
404	halfRed, halfBlue := [4]byte{0, 0, 128, 128}, [4]byte{128, 0, 0, 128}
405	partly := fill(16, 8, halfRed)
406	for y := range 8 {
407		copy(partly[y*64+48:y*64+64], make([]byte, 16)) // last 4 pixels: transparent
408	}
409	white := shmContent(t, 32, 48, 32*4, solid(32, 48, [3]uint8{255, 255, 255}))
410	white.Opaque = true
411	contents := map[ports.WindowID]ports.SurfaceContent{
412		1: *white,
413		2: *shmContent(t, 64, 48, 64*4, fill(64, 48, halfRed)),
414		3: *shmContent(t, 16, 8, 16*4, partly),
415		4: *shmContent(t, 16, 8, 16*4, fill(16, 8, halfBlue)),
416	}
417	scene := ports.Scene{Background: "#000000",
418		Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 32, H: 48}}},
419		Layers: []ports.SceneLayer{
420			{ID: 2, Layer: ports.LayerBottom, Rect: ports.Rect{W: 64, H: 48}},
421			{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}},
422			{ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},
423		}}
424	if err := render(r, scene, contents); err != nil {
425		t.Fatal(err)
426	}
427	px := r.Pixels()
428	near := func(a, b uint8) bool { d := int(a) - int(b); return d >= -1 && d <= 1 }
429	for _, tc := range []struct {
430		name    string
431		x, y    int
432		r, g, b uint8
433	}{
434		{"bottom layer over background", 40, 20, 128, 0, 0},
435		{"opaque window over bottom layer", 20, 20, 255, 255, 255},
436		{"red overlay over window", 4, 4, 255, 127, 127},
437		{"blue over red over window", 10, 4, 127, 63, 191},
438		{"blue over window", 20, 4, 127, 127, 255},
439		{"transparent red pixels: blue over window only", 14, 4, 127, 127, 255},
440	} {
441		got := px.RGBAAt(tc.x, tc.y)
442		if !near(got.R, tc.r) || !near(got.G, tc.g) || !near(got.B, tc.b) {
443			t.Errorf("%s: pixel (%d,%d) = %v, want ~{%d %d %d}", tc.name, tc.x, tc.y, got, tc.r, tc.g, tc.b)
444		}
445	}
446}

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