v0.4.0 b6be3a8

scene_opaque_region_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"image"
5	"image/color"
6	"slices"
7	"testing"
8	"time"
9
10	"github.com/bnema/neferwl/internal/ports"
11)
12
13// opaqueRegionContent is a 2x-scale shm root of w×h logical pixels under one
14// child of the same size whose opaque region stops a pixel short of its right
15// and bottom edges (Firefox's page over its shm root).
16func opaqueRegionContent(t testing.TB, w, h int) ports.SurfaceContent {
17	root := solidContent(t, 2*w, 2*h, color.RGBA{R: 255, A: 255})
18	root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
19	root.LogicalW, root.LogicalH = w, h
20	child := solidContent(t, w, h, color.RGBA{G: 255, A: 255})
21	child.Surface, child.Version = 2, 1
22	child.LogicalW, child.LogicalH = w, h
23	child.OpaqueRect = ports.Rect{W: w - 1, H: h - 1}
24	root.Children = []ports.Subsurface{{SurfaceContent: child}}
25	return root
26}
27
28// drawRects are the rects of the content draws (not the window's fills).
29func drawRects(ds []draw) []image.Rectangle {
30	var out []image.Rectangle
31	for _, d := range ds {
32		if d.pc.misc[0] == modeSolid {
33			continue
34		}
35		out = append(out, image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3])))
36	}
37	return out
38}
39
40// A child whose opaque region leaves a thin edge out hides the root under the
41// region only: the root draws the two strips around it, and the child draws
42// over its own whole rect.
43func TestSceneOpaqueRegionClipsRoot(t *testing.T) {
44	r, err := New(64, 48)
45	if err != nil {
46		t.Skipf("Vulkan unavailable: %v", err)
47	}
48	defer r.Close()
49	scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
50	contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
51	ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
52	want := []image.Rectangle{
53		// The root: the strip under the last row, then the one right of the
54		// region; then the child over its whole rect.
55		image.Rect(0, 11, 20, 12), image.Rect(19, 0, 20, 11),
56		image.Rect(0, 0, 20, 12),
57	}
58	if got := drawRects(ds); !sameRects(got, want) {
59		t.Fatalf("draws %v, want %v", got, want)
60	}
61}
62
63func sameRects(a, b []image.Rectangle) bool {
64	if len(a) != len(b) {
65		return false
66	}
67	for i := range a {
68		if a[i] != b[i] {
69			return false
70		}
71	}
72	return true
73}
74
75// A wholly opaque child still skips the root.
76func TestSceneOpaqueChildSkipsRoot(t *testing.T) {
77	r, err := New(64, 48)
78	if err != nil {
79		t.Skipf("Vulkan unavailable: %v", err)
80	}
81	defer r.Close()
82	root := opaqueRegionContent(t, 20, 12)
83	root.Children[0].Opaque, root.Children[0].OpaqueRect = true, ports.Rect{}
84	scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
85	ds := r.draws(scene, map[ports.WindowID]ports.SurfaceContent{1: root}, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
86	if got, want := drawRects(ds), []image.Rectangle{image.Rect(0, 0, 20, 12)}; !sameRects(got, want) {
87		t.Fatalf("draws %v, want %v", got, want)
88	}
89}
90
91// The strips show the root exactly as it draws alone: the child is
92// transparent on its last row and column, outside its opaque region, and the
93// root's pixels show through there, mapped as without the clipping. Inside
94// the region the child shows.
95func TestRendererOpaqueRegionStripsKeepRoot(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	const w, h = 20, 12
102	scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
103	root := opaqueRegionContent(t, w, h)
104	// A 2x root with a gradient, to see a wrong strip mapping; a child
105	// that is green but for a transparent last row and column.
106	px := make([]byte, 2*w*2*h*4)
107	for y := range 2 * h {
108		for x := range 2 * w {
109			copy(px[(y*2*w+x)*4:], []byte{0, uint8(y * 10), uint8(x * 6), 255})
110		}
111	}
112	root.SHM = shmContent(t, 2*w, 2*h, 2*w*4, px).SHM
113	cp := make([]byte, w*h*4)
114	for y := range h {
115		for x := range w {
116			if x < w-1 && y < h-1 {
117				copy(cp[(y*w+x)*4:], []byte{0, 255, 0, 255})
118			}
119		}
120	}
121	root.Children[0].SHM = shmContent(t, w, h, w*4, cp).SHM
122	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
123		t.Fatal(err)
124	}
125	with := readPixels(t, r)
126	alone := root
127	alone.Children = nil
128	alone.Seq, alone.Version = 2, 2
129	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: alone}); err != nil {
130		t.Fatal(err)
131	}
132	without := readPixels(t, r)
133	green := color.RGBA{G: 255, A: 255}
134	for y := range h {
135		for x := range w {
136			want := without.At(x, y)
137			if x < w-1 && y < h-1 {
138				want = green
139			}
140			if got := with.At(x, y); got != want {
141				t.Fatalf("At(%d,%d)=%v want %v", x, y, got, want)
142			}
143		}
144	}
145}
146
147// opaqueCover maps the opaque rect onto the physical rect of the surface,
148// rounding inward, clipped to what shows.
149func TestOpaqueCover(t *testing.T) {
150	c := ports.SurfaceContent{LogicalW: 984, LogicalH: 1231, OpaqueRect: ports.Rect{W: 983, H: 1230}}
151	for _, tc := range []struct {
152		name      string
153		full, dst image.Rectangle
154		rect      ports.Rect
155		want      image.Rectangle
156	}{
157		{"scale 1.3", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 1279, 1600), c.OpaqueRect, image.Rect(0, 0, 1277, 1598)},
158		{"offset", image.Rect(10, 20, 1289, 1620), image.Rect(10, 20, 1289, 1620), c.OpaqueRect, image.Rect(10, 20, 1287, 1618)},
159		{"clipped", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 100, 100), c.OpaqueRect, image.Rect(0, 0, 100, 100)},
160		{"inner rect", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 984, 1231), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rect(4, 8, 104, 208)},
161		{"outside", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 3, 3), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rectangle{}},
162	} {
163		t.Run(tc.name, func(t *testing.T) {
164			c.OpaqueRect = tc.rect
165			got := opaqueCover(&c, tc.full, tc.dst)
166			if got != tc.want {
167				t.Fatalf("opaqueCover = %v, want %v", got, tc.want)
168			}
169		})
170	}
171}
172
173// Building the draws of a window with a partly opaque child allocates
174// nothing.
175func TestSceneOpaqueRegionAllocations(t *testing.T) {
176	r, err := New(64, 48)
177	if err != nil {
178		t.Skipf("Vulkan unavailable: %v", err)
179	}
180	defer r.Close()
181	scene := ports.Scene{Seq: 1, Scale: 1.5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
182	contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
183	bounds := image.Rect(0, 0, 64, 48)
184	frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
185	frame()
186	if allocs := testing.AllocsPerRun(50, frame); allocs != 0 {
187		t.Errorf("partly opaque child draws: %.1f allocs/frame, want 0", allocs)
188	}
189}
190
191// BenchmarkComposeOpaqueRegionChild composes a Firefox-like window moved every
192// frame as a workspace swipe does, at 2560x1600 and scale 1.3: a 2x shm root
193// of 984x1231 logical pixels under a dmabuf page of 1279x1600 pixels shown
194// at 984x1231 (the viewport destination). "none" is a page with no opaque
195// region (the root draws in full), "short" one with a region a logical pixel
196// short of its right and bottom edges, "whole" one that covers it. The GPU is
197// waited for after every frame. Select it with NEFERWL_VK_DEVICE.
198func BenchmarkComposeOpaqueRegionChild(b *testing.B) {
199	const w, h, lw, lh = 2560, 1600, 984, 1231
200	for _, tc := range []struct {
201		name   string
202		opaque bool
203		rect   ports.Rect
204	}{
205		{"none", false, ports.Rect{}},
206		{"short", false, ports.Rect{W: lw - 1, H: lh - 1}},
207		{"whole", true, ports.Rect{}},
208	} {
209		b.Run(tc.name, func(b *testing.B) {
210			r, err := New(w, h)
211			if err != nil {
212				b.Skipf("Vulkan unavailable: %v", err)
213			}
214			defer r.Close()
215			linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
216			if !slices.Contains(r.DMABuf().Formats, linear) {
217				b.Skipf("linear ARGB8888 not importable")
218			}
219			const pw, ph = 1279, 1600
220			f := udmabuf(b, 1280, ph, func(x, y int) [4]byte { return [4]byte{byte(x), byte(y), 200, 255} })
221			page := ports.SurfaceContent{Width: pw, Height: ph, LogicalW: lw, LogicalH: lh, Surface: 2, Version: 1,
222				Opaque: tc.opaque, OpaqueRect: tc.rect,
223				DMABuf: &ports.DMABuf{ID: 9, Width: pw, Height: ph, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 5120}}}}
224			root := solidContent(b, 2*lw, 2*lh, color.RGBA{R: 240, G: 240, B: 240, A: 255})
225			root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
226			root.LogicalW, root.LogicalH = lw, lh
227			root.Opaque = true
228			root.Children = []ports.Subsurface{{SurfaceContent: page}}
229			contents := map[ports.WindowID]ports.SurfaceContent{1: root}
230			frame := func(i int) {
231				x := i % 64
232				scene := ports.Scene{Seq: uint64(i + 1), Scale: 1.3, Background: "#101010", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: x, W: lw, H: lh}}}}
233				if err := render(r, scene, contents); err != nil {
234					b.Fatal(err)
235				}
236				if err := r.waitFrame(r.submitted); err != nil {
237					b.Fatal(err)
238				}
239			}
240			for i := range 8 {
241				frame(i)
242			}
243			b.ResetTimer()
244			start := time.Now()
245			for i := range b.N {
246				frame(i + 8)
247			}
248			b.ReportMetric(float64(time.Since(start).Microseconds())/1000/float64(b.N), "ms/frame")
249		})
250	}
251}
252
253// crossingCovers is a root of w×h pixels under n opaque 10×10 children
254// staggered over it, so that each cover crosses the root and the others.
255func crossingCovers(t testing.TB, w, h, n int) ports.SurfaceContent {
256	root := solidContent(t, w, h, color.RGBA{R: 255, A: 255})
257	root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
258	for i := range n {
259		child := solidContent(t, 10, 10, color.RGBA{G: 255, A: 255})
260		child.Surface, child.Version, child.Opaque = uint64(2+i), 1, true
261		root.Children = append(root.Children, ports.Subsurface{SurfaceContent: child, X: (i * 7) % (w - 10), Y: (i * 5) % (h - 10)})
262	}
263	return root
264}
265
266// A client chooses how many opaque subsurfaces cross its root, and each one
267// cuts the root quad into strips: the split stops at maxSplitCovers covers
268// or maxSplitStrips draws, and the root then draws whole (overdraw only, the
269// covers above it still hide the rest).
270func TestSceneSplitIsBounded(t *testing.T) {
271	r, err := New(64, 48)
272	if err != nil {
273		t.Skipf("Vulkan unavailable: %v", err)
274	}
275	defer r.Close()
276	const w, h = 60, 40
277	scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
278	full := image.Rect(0, 0, w, h)
279	for _, n := range []int{1, maxSplitCovers, maxSplitCovers + 1, 40} {
280		contents := map[ports.WindowID]ports.SurfaceContent{1: crossingCovers(t, w, h, n)}
281		ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
282		rects := drawRects(ds)
283		root := len(rects) - n // the children draw once each, after the root
284		if root < 1 || root > maxSplitStrips {
285			t.Errorf("%d covers: %d root draws, want 1..%d", n, root, maxSplitStrips)
286		}
287		if n > maxSplitCovers && (root != 1 || rects[0] != full) {
288			t.Errorf("%d covers: root draws %v, want the whole quad %v", n, rects[:root], full)
289		}
290		if n == 1 && root < 2 {
291			t.Errorf("one cover: %d root draws, want the quad split around it", root)
292		}
293		bounds := image.Rect(0, 0, 64, 48)
294		frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
295		frame()
296		if allocs := testing.AllocsPerRun(20, frame); allocs != 0 {
297			t.Errorf("%d covers: %.1f allocs/frame, want 0", n, allocs)
298		}
299	}
300}
301
302// With the split given up on, the covers above the whole root quad still
303// show: the window's pixels are the children's inside them and the root's
304// elsewhere.
305func TestRendererSplitBoundKeepsRoot(t *testing.T) {
306	r, err := New(64, 48)
307	if err != nil {
308		t.Skipf("Vulkan unavailable: %v", err)
309	}
310	defer r.Close()
311	const w, h, n = 60, 40, 40
312	scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
313	root := crossingCovers(t, w, h, n)
314	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
315		t.Fatal(err)
316	}
317	got := readPixels(t, r)
318	red, green := color.RGBA{R: 255, A: 255}, color.RGBA{G: 255, A: 255}
319	for y := range h {
320		for x := range w {
321			want := red
322			for _, c := range root.Children {
323				if image.Pt(x, y).In(image.Rect(c.X, c.Y, c.X+10, c.Y+10)) {
324					want = green
325				}
326			}
327			if g := got.At(x, y); g != want {
328				t.Fatalf("At(%d,%d)=%v want %v", x, y, g, want)
329			}
330		}
331	}
332}

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