feat/data-dnd e05cb15

scene_perf_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"image"
5	"image/color"
6	"slices"
7	"testing"
8
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// TestRenderSteadyStateAllocations measures the cost of a small stable scene,
13// on a normal and on a rotated output (orient works in place), and that
14// building the draws of a scene with a moving window (a new Seq and window
15// slice every frame, as an animation publishes) allocates nothing.
16// A new scene Seq with one of two windows moved is a partial redraw seeded by
17// sceneDelta and allocates nothing either.
18func TestRenderSteadyStateAllocations(t *testing.T) {
19	for _, tc := range []struct {
20		name           string
21		transform      ports.BufferTransform
22		tw, th         int
23		sceneW, sceneH int
24	}{
25		{"transform 0", 0, 32, 32, 32, 32},
26		{"transform 1", 1, 32, 24, 24, 32},
27	} {
28		t.Run(tc.name, func(t *testing.T) {
29			r, err := New(tc.tw, tc.th)
30			if err != nil {
31				t.Skipf("Vulkan unavailable: %v", err)
32			}
33			defer r.Close()
34			scene := ports.Scene{Seq: 1, Transform: tc.transform, Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: tc.sceneW, H: tc.sceneH}}}}
35			content := solidContent(t, 8, 8, color.RGBA{R: 255, A: 255})
36			content.ID, content.Surface, content.Seq, content.Version = 1, 1, 1, 1
37			contents := map[ports.WindowID]ports.SurfaceContent{1: content}
38			frame := func() {
39				if err := render(r, scene, contents); err != nil {
40					t.Fatal(err)
41				}
42			}
43			frame()
44			if allocs := testing.AllocsPerRun(20, frame); allocs > 3 {
45				t.Errorf("small steady-state Render: %.1f allocs/frame, want <=3 (syncFD path: %v)", allocs, r.syncFD)
46			} else {
47				t.Logf("small steady-state Render: %.1f allocs/frame", allocs)
48			}
49			// A moving window: scenes are prebuilt (core allocates them, not
50			// the renderer), each with a fresh Seq, so every frame draws
51			// everything again. The draw building must not allocate.
52			var moving [4]ports.Scene
53			for i := range moving {
54				moving[i] = ports.Scene{Seq: uint64(100 + i), Transform: tc.transform, Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: i, W: tc.sceneW - 4, H: tc.sceneH - 4}}}}
55			}
56			bounds := image.Rect(0, 0, r.width, r.height)
57			n := 0
58			frameDraws := func() {
59				n++
60				s := moving[n%len(moving)]
61				_ = r.draws(s, contents, r.frameDamage(r.last, s, bounds))
62			}
63			frameDraws()
64			if allocs := testing.AllocsPerRun(50, frameDraws); allocs != 0 {
65				t.Errorf("moving window draws: %.1f allocs/frame, want 0", allocs)
66			}
67			// A content change under an unchanged scene Seq: a partial
68			// redraw clipped to the changed buffer region, replicating the
69			// partial sequence of Render. The stand-in target holds the
70			// previous content Seq of the window, and is never updated, so
71			// every run is damaged.
72			dc := content
73			dc.Seq, dc.Version = 2, 2
74			dc.DamageHistory = []ports.SeqDamage{{Seq: 2, Rects: []ports.Rect{{X: 1, Y: 1, W: 2, H: 2}}}}
75			pcontents := map[ports.WindowID]ports.SurfaceContent{1: dc}
76			held := &target{valid: true, sceneSeq: scene.Seq, windows: map[ports.WindowID]heldWindow{1: {seq: 1, rect: image.Rect(0, 0, r.width, r.height)}}}
77			var area image.Rectangle
78			var whole bool
79			partial := func() {
80				dmg := r.frameDamage(held, scene, bounds)
81				ds := r.draws(scene, pcontents, dmg)
82				dmg.finish()
83				ds = append(ds, draw{})
84				copy(ds[1:], ds[:len(ds)-1])
85				ds[0] = r.fillDraw(dmg.area, parseColor(scene.Background))
86				ds = dmg.clip(ds)
87				r.scratchDraws = ds
88				r.orient(ds, scene.Transform)
89				area, whole = dmg.area, dmg.all()
90			}
91			partial()
92			if whole || area.Empty() || area.Dx()*area.Dy() >= bounds.Dx()*bounds.Dy() {
93				t.Fatalf("not a partial redraw: all %v, area %v of %v", whole, area, bounds)
94			}
95			if allocs := testing.AllocsPerRun(50, partial); allocs != 0 {
96				t.Errorf("partial redraw draws: %.1f allocs/frame, want 0", allocs)
97			}
98			// A new scene Seq with one window moved: the delta against the
99			// scene the target holds seeds the partial redraw (A2). The
100			// stand-in target holds the old scene and is never updated.
101			half := tc.sceneW / 2
102			content2 := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
103			content2.ID, content2.Surface, content2.Seq, content2.Version = 2, 2, 1, 1
104			dcontents := map[ports.WindowID]ports.SurfaceContent{1: content, 2: content2}
105			before := ports.Scene{Seq: 200, Transform: tc.transform, Scale: 1, Windows: []ports.SceneWindow{
106				{ID: 1, Rect: ports.Rect{W: half, H: tc.sceneH}},
107				{ID: 2, Rect: ports.Rect{X: half, W: tc.sceneW - half, H: tc.sceneH}},
108			}}
109			moved := before
110			moved.Seq = 201
111			moved.Windows = []ports.SceneWindow{before.Windows[0], {ID: 2, Rect: ports.Rect{X: half + 1, W: tc.sceneW - half - 1, H: tc.sceneH}}}
112			full := image.Rect(0, 0, r.width, r.height)
113			held = &target{valid: true, sceneSeq: before.Seq, scene: before, windows: map[ports.WindowID]heldWindow{
114				1: {seq: 1, rect: image.Rect(0, 0, half, tc.sceneH)},
115				2: {seq: 1, rect: image.Rect(half, 0, tc.sceneW, tc.sceneH)},
116			}}
117			seeded := func() {
118				dmg := r.frameDamage(held, moved, bounds)
119				ds := r.draws(moved, dcontents, dmg)
120				dmg.finish()
121				ds = append(ds, draw{})
122				copy(ds[1:], ds[:len(ds)-1])
123				ds[0] = r.fillDraw(dmg.area, parseColor(moved.Background))
124				ds = dmg.clip(ds)
125				r.scratchDraws = ds
126				r.orient(ds, moved.Transform)
127				area, whole = dmg.area, dmg.all()
128			}
129			seeded()
130			if whole || area.Empty() || area.Dx()*area.Dy() >= full.Dx()*full.Dy() {
131				t.Fatalf("sceneDelta did not seed a partial redraw: all %v, area %v of %v", whole, area, bounds)
132			}
133			if allocs := testing.AllocsPerRun(50, seeded); allocs != 0 {
134				t.Errorf("sceneDelta-seeded partial redraw draws: %.1f allocs/frame, want 0", allocs)
135			}
136		})
137	}
138}
139
140// A steady frame that draws a client dmabuf also records the ownership
141// barriers (acquire and release) of its image, which allocate nothing. The
142// frame still allocates more than the SHM scene: importing the client's
143// read fence (measured 5.0 on radv; the semaphore retire is typed and costs
144// no closure, locked by TestRetireSemaphoreAllocatesNothing). The budget
145// keeps one alloc of headroom for drivers whose import path differs.
146// Skipped without a GPU or /dev/udmabuf.
147func TestRenderSteadyStateDMABufAllocations(t *testing.T) {
148	r, err := New(80, 32)
149	if err != nil {
150		t.Skipf("Vulkan unavailable: %v", err)
151	}
152	defer r.Close()
153	linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
154	if !slices.Contains(r.DMABuf().Formats, linear) {
155		t.Skipf("linear ARGB8888 not importable")
156	}
157	f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
158	buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
159	scene := ports.Scene{Seq: 1, Background: "#000000", Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
160	contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Surface: 1, Seq: 1, Version: 1, Width: 64, Height: 16, DMABuf: buf}}
161	frame := func() {
162		if err := render(r, scene, contents); err != nil {
163			t.Fatal(err)
164		}
165	}
166	frame()
167	if len(r.imports) != 1 {
168		t.Fatalf("imports %d: the frame does not take the dmabuf path", len(r.imports))
169	}
170	// Each new scene Seq draws the dmabuf again, with its barriers.
171	var moving [4]ports.Scene
172	for i := range moving {
173		moving[i] = scene
174		moving[i].Seq = uint64(10 + i)
175		moving[i].Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4 + i, Y: 4, W: 64, H: 16}}}
176	}
177	n := 0
178	moved := func() {
179		n++
180		if err := render(r, moving[n%len(moving)], contents); err != nil {
181			t.Fatal(err)
182		}
183	}
184	moved()
185	allocs := testing.AllocsPerRun(20, moved)
186	t.Logf("dmabuf Render: %.1f allocs/frame", allocs)
187	if allocs > 6 {
188		t.Errorf("dmabuf steady-state Render: %.1f allocs/frame, want <=6 (syncFD path: %v; a driver without sync-file export takes another path)", allocs, r.syncFD)
189	}
190}
191
192// Retiring a frame's wait semaphore appends a typed entry to the reused
193// deferred list: no closure, no allocation once the list has its capacity.
194func TestRetireSemaphoreAllocatesNothing(t *testing.T) {
195	r, err := New(32, 32)
196	if err != nil {
197		t.Skipf("Vulkan unavailable: %v", err)
198	}
199	defer r.Close()
200	r.completed = 0
201	retire := func() {
202		// A null handle is a valid vkDestroySemaphore argument.
203		r.retireSemaphore(r.completed+1, 0)
204		r.completed++
205		r.runDeferred()
206	}
207	retire()
208	if allocs := testing.AllocsPerRun(50, retire); allocs != 0 {
209		t.Errorf("retireSemaphore: %.1f allocs, want 0", allocs)
210	}
211	if len(r.deferred) != 0 {
212		t.Errorf("%d deferred entries left after their frame completed", len(r.deferred))
213	}
214}
215
216// Semaphore and closure entries share the deferred list: each is freed once
217// its frame completes, in order, and a semaphore whose frame already
218// completed is destroyed at once instead of queued.
219func TestRetireMixedDeferredEntries(t *testing.T) {
220	r, err := New(32, 32)
221	if err != nil {
222		t.Skipf("Vulkan unavailable: %v", err)
223	}
224	defer r.Close()
225	r.runDeferred()
226	r.completed = 5
227	// A null handle is a valid vkDestroySemaphore argument.
228	r.retireSemaphore(5, 0)
229	if len(r.deferred) != 0 {
230		t.Fatalf("completed frame's semaphore queued: %d entries", len(r.deferred))
231	}
232	var freed []string
233	r.retire(7, func() { freed = append(freed, "late") })
234	r.retireSemaphore(6, 0)
235	r.retire(6, func() { freed = append(freed, "early") })
236	r.completed = 6
237	r.runDeferred()
238	if !slices.Equal(freed, []string{"early"}) || len(r.deferred) != 1 || r.deferred[0].frame != 7 {
239		t.Fatalf("frame 6: freed %v, %d entries left", freed, len(r.deferred))
240	}
241	r.completed = 7
242	r.runDeferred()
243	if !slices.Equal(freed, []string{"early", "late"}) || len(r.deferred) != 0 {
244		t.Fatalf("frame 7: freed %v, %d entries left", freed, len(r.deferred))
245	}
246}
247
248// TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
249// subsurface tree. Each child has its own real wl_shm pool.
250func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
251	const tiles = 64
252	r, err := New(128, 128)
253	if err != nil {
254		t.Skipf("Vulkan unavailable: %v", err)
255	}
256	defer r.Close()
257	root := ports.SurfaceContent{ID: 1, Seq: 1, Surface: 1, Version: 1}
258	for i := range tiles {
259		child := solidContent(t, 8, 8, color.RGBA{R: uint8(i + 1), A: 255})
260		child.ID, child.Seq, child.Surface, child.Version = 1, 1, uint64(i+2), 1
261		root.Children = append(root.Children, ports.Subsurface{X: i % 8 * 8, Y: i / 8 * 8, SurfaceContent: child})
262	}
263	video := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
264	video.ID, video.Seq, video.Surface, video.Version = 1, 1, 66, 1
265	root.Children = append(root.Children, ports.Subsurface{X: 80, Y: 0, SurfaceContent: video})
266	scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 128}}}}
267	contents := map[ports.WindowID]ports.SurfaceContent{1: root}
268	if err := render(r, scene, contents); err != nil {
269		t.Fatal(err)
270	}
271	for _, count := range []int{16, 64} {
272		contents[1] = ports.SurfaceContent{ID: 1, Seq: root.Seq, Surface: root.Surface, Version: root.Version, Children: root.Children[:count]}
273		walk := func() {
274			dmg := newDamage(&target{}, scene, image.Rect(0, 0, r.width, r.height))
275			_ = r.draws(scene, contents, dmg)
276		}
277		walk()
278		if allocs := testing.AllocsPerRun(20, walk); allocs > 8 {
279			t.Errorf("%d tiles: %.1f allocations/walk, want <=8", count, allocs)
280		} else {
281			t.Logf("%d tiles: %.1f allocations/walk", count, allocs)
282		}
283	}
284	contents[1] = root
285	// The same renderer and scene are reused; measure only steady-state frame work.
286	frame := func() {
287		if err := render(r, scene, contents); err != nil {
288			t.Fatal(err)
289		}
290	}
291	frame()
292	if allocs := testing.AllocsPerRun(20, frame); allocs > 16 {
293		t.Errorf("steady-state Render: %.1f allocs/frame, want <=16", allocs)
294	} else {
295		t.Logf("steady-state Render: %.1f allocs/frame", allocs)
296	}
297	before := r.copied
298	for i := range 100 {
299		root.Seq++
300		root.Children[tiles].Version++
301		contents[1] = root
302		if err := render(r, scene, contents); err != nil {
303			t.Fatal(err)
304		}
305		if delta := r.copied - before; delta != 8*8*4 {
306			t.Fatalf("publication %d copied %d bytes, want video only (%d)", i, delta, 8*8*4)
307		}
308		before = r.copied
309	}
310	// Reordering equal-sized children must not reuse another child's pixels.
311	root.Children[0], root.Children[1] = root.Children[1], root.Children[0]
312	root.Children[0].X, root.Children[1].X = 0, 8
313	root.Seq++
314	contents[1] = root
315	if err := render(r, scene, contents); err != nil {
316		t.Fatal(err)
317	}
318	if got := readPixels(t, r).RGBAAt(4, 4); got != (color.RGBA{R: 2, A: 255}) {
319		t.Fatalf("restacked tile has pixels %v, want second child's colour", got)
320	}
321}

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