feat/data-dnd e05cb15

compose_batch_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"image"
5	"image/color"
6	"slices"
7	"testing"
8	"unsafe"
9
10	"github.com/bnema/neferwl/internal/ports"
11	vk "github.com/bnema/purego-vulkan/vulkan"
12)
13
14func TestInstanceDrawLayout(t *testing.T) {
15	if unsafe.Sizeof(drawParameters{}) != 112 || unsafe.Sizeof(instanceDraw{}) != 128 || unsafe.Offsetof(instanceDraw{}.resource) != 112 {
16		t.Fatal("draw layout differs from compose.vert std430 layout")
17	}
18}
19
20func TestComposeBatchPaintOrder(t *testing.T) {
21	a, b := &shmCopy{}, &shmCopy{}
22	ds := []draw{{}, {shm: a}, {}, {shm: b}, {shm: a}}
23	for i := range ds {
24		ds[i].pc.rect[0] = int32(i)
25	}
26	var batch composeBatch
27	for _, dr := range ds {
28		if !batch.add(dr, 2, 20) {
29			t.Fatal("unexpected split")
30		}
31	}
32	if batch.used != 2 {
33		t.Fatalf("resources = %d, want 2", batch.used)
34	}
35	for i, want := range []uint32{0, 0, 0, 1, 0} {
36		if batch.instances[i].resource[0] != want || batch.instances[i].pc.rect[0] != int32(i) {
37			t.Fatalf("draw %d reordered or assigned wrong resource: %+v", i, batch.instances[i])
38		}
39	}
40	if batch.add(draw{shm: &shmCopy{}}, 2, 20) {
41		t.Fatal("accepted resource beyond descriptor capacity")
42	}
43	if batch.add(draw{}, 2, len(ds)) {
44		t.Fatal("accepted draw beyond buffer capacity")
45	}
46	batch = composeBatch{}
47	for _, dr := range ds[:3] {
48		if !batch.add(dr, 1, 20) {
49			t.Fatal("solid split a single-resource run")
50		}
51	}
52	if batch.add(ds[3], 1, 20) {
53		t.Fatal("single-resource batch accepted another resource")
54	}
55}
56
57func TestComposeResourceLimit(t *testing.T) {
58	limits := vk.PhysicalDeviceLimits{
59		MaxPerStageDescriptorSamplers: 128, MaxDescriptorSetSamplers: 128,
60		MaxPerStageDescriptorSampledImages: 128, MaxDescriptorSetSampledImages: 128,
61		MaxPerStageDescriptorStorageBuffers: 64, MaxDescriptorSetStorageBuffers: 65,
62		MaxPerStageResources: 193,
63	}
64	if got := composeResourceLimit(limits); got != 64 {
65		t.Fatalf("limit = %d, want 64", got)
66	}
67	limits.MaxPerStageResources = 192
68	if got := composeResourceLimit(limits); got != 63 {
69		t.Fatalf("color attachment not counted: %d, want 63", got)
70	}
71	limits.MaxDescriptorSetStorageBuffers = 5
72	if got := composeResourceLimit(limits); got != 4 {
73		t.Fatalf("instance buffer not counted: %d, want 4", got)
74	}
75	limits.MaxPerStageDescriptorSamplers = 4
76	if got := composeResourceLimit(limits); got != 2 {
77		t.Fatalf("two image planes not counted: %d, want 2", got)
78	}
79	limits.MaxDescriptorSetStorageBuffers = 0
80	if got := composeResourceLimit(limits); got != 0 {
81		t.Fatalf("zero limit underflowed: %d", got)
82	}
83}
84
85// Both shader variants preserve ordered blending across batch boundaries,
86// resource changes, and more batches than the slot's initial slice capacity.
87func TestComposeInstancedBlendAndBatches(t *testing.T) {
88	for _, tc := range []struct {
89		name      string
90		single    bool
91		resources uint32
92	}{
93		{"indexed", false, 3},
94		{"single resource", true, 1},
95	} {
96		t.Run(tc.name, func(t *testing.T) {
97			r, err := New(16, 16)
98			if err != nil {
99				t.Skipf("Vulkan unavailable: %v", err)
100			}
101			defer r.Close()
102			if !tc.single && !r.compose.indexing {
103				t.Skip("nonuniform descriptor indexing unavailable")
104			}
105			// Rebuild only the composition pipeline/layout before rendering,
106			// to exercise bounded tables and the feature-free shader variant.
107			r.destroyComposer()
108			r.compose.resources, r.compose.indexing = tc.resources, !tc.single
109			if err := r.createComposer(); err != nil {
110				t.Fatal(err)
111			}
112			scene := ports.Scene{Background: "#000000"}
113			contents := make(map[ports.WindowID]ports.SurfaceContent)
114			var reference [3]float64
115			for i := range 7 {
116				id := ports.WindowID(i + 1)
117				pixel := [4]byte{byte(12 + i*15), byte(90 - i*9), byte(20 + i*12), 128}
118				for ch := range 3 {
119					reference[ch] = float64(pixel[2-ch]) + reference[ch]*(127.0/255)
120				}
121				c := *shmContent(t, 16, 16, 64, fill(16, 16, pixel))
122				c.ID, c.Seq, c.Version = id, 1, 1
123				contents[id] = c
124				scene.Layers = append(scene.Layers, ports.SceneLayer{ID: id, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 16}})
125			}
126			for range 4 {
127				if err := render(r, scene, contents); err != nil {
128					t.Fatal(err)
129				}
130				if got := readPixels(t, r).RGBAAt(8, 8); !near(got, color.RGBA{R: uint8(reference[0] + 0.5), G: uint8(reference[1] + 0.5), B: uint8(reference[2] + 0.5), A: 255}, 2) {
131					t.Fatalf("ordered translucent overlays = %v", got)
132				}
133			}
134			wantBatches := (7 + int(tc.resources) - 1) / int(tc.resources)
135			for i := range r.slots {
136				if len(r.slots[i].batches) != wantBatches {
137					t.Fatalf("slot %d batches = %d, want %d", i, len(r.slots[i].batches), wantBatches)
138				}
139			}
140			// Replace a resource: both slots must refresh their descriptors.
141			oldKeys := [frameSlots][maxComposeResources]resourceKey{}
142			for i := range r.slots {
143				oldKeys[i] = r.slots[i].batches[wantBatches-1].cached
144			}
145			replacement := solidContent(t, 8, 8, color.RGBA{B: 255, A: 255})
146			replacement.LogicalW, replacement.LogicalH = 16, 16
147			replacement.ID, replacement.Seq, replacement.Version = 99, 2, 2
148			contents[7] = replacement
149			for range 4 {
150				if err := render(r, scene, contents); err != nil {
151					t.Fatal(err)
152				}
153				if got := readPixels(t, r).RGBAAt(8, 8); got != (color.RGBA{B: 255, A: 255}) {
154					t.Fatalf("replaced resource = %v", got)
155				}
156			}
157			for i := range r.slots {
158				if r.slots[i].batches[wantBatches-1].cached == oldKeys[i] {
159					t.Fatalf("slot %d retained old resource descriptors", i)
160				}
161			}
162		})
163	}
164}
165
166func TestComposePreparedReuseAndGrowth(t *testing.T) {
167	r, err := New(16, 16)
168	if err != nil {
169		t.Skipf("Vulkan unavailable: %v", err)
170	}
171	defer r.Close()
172	slot := &r.slots[0]
173	ds := []draw{r.fillDraw(image.Rect(0, 0, 16, 16), [3]uint8{255})}
174	if _, err := r.prepareCompose(slot, ds); err != nil {
175		t.Fatal(err)
176	}
177	batch := slot.batches[0]
178	initialSize := batch.gpu.size
179	large := slices.Repeat(ds, 100)
180	if _, err := r.prepareCompose(slot, large); err != nil {
181		t.Fatal(err)
182	}
183	if batch.gpu.size <= initialSize || batch.drawBuffer.Buffer != batch.gpu.buffer || batch.count != 100 {
184		t.Fatal("grown draw buffer not rebound")
185	}
186	if allocs := testing.AllocsPerRun(50, func() {
187		if _, err := r.prepareCompose(slot, large); err != nil {
188			t.Fatal(err)
189		}
190	}); allocs != 0 {
191		t.Fatalf("steady compose upload: %.1f allocations, want zero", allocs)
192	}
193	if batches, err := r.prepareCompose(slot, nil); err != nil || len(batches) != 0 {
194		t.Fatal("empty pass returned batches")
195	}
196	// Lower the draw range to force a split independently of descriptors.
197	r.maxRange = 2 * int(unsafe.Sizeof(instanceDraw{}))
198	if batches, err := r.prepareCompose(slot, ds); err != nil || len(batches) != 1 {
199		t.Fatal("single draw did not fit")
200	}
201	if batches, err := r.prepareCompose(slot, slices.Repeat(ds, 5)); err != nil || len(batches) != 3 || batches[0].count != 2 || batches[2].count != 1 {
202		t.Fatal("draw-buffer range did not split into 2,2,1")
203	}
204	// Shrink the scene and age its unused batches beyond the frame TTL.
205	r.frame += importTTL + 1
206	if batches, err := r.prepareCompose(slot, ds); err != nil || len(batches) != 1 || len(slot.batches) != 1 {
207		t.Fatal("unused batches retained beyond frame TTL")
208	}
209}
210
211// Zero handles: release, freeShmCopy and trimCompose make no Vulkan call.
212func TestComposeReleaseInvalidatesCachedDescriptors(t *testing.T) {
213	im, shm := &imported{fd: -1}, &shmCopy{}
214	other := resourceKey{shm: &shmCopy{}}
215	for _, tc := range []struct {
216		name    string
217		key     resourceKey
218		release func(*Renderer)
219	}{
220		{"dmabuf", resourceKey{im: im}, func(r *Renderer) { r.release(im) }},
221		{"shm", resourceKey{shm: shm}, func(r *Renderer) { r.freeShmCopy(shm) }},
222	} {
223		t.Run(tc.name, func(t *testing.T) {
224			r := &Renderer{}
225			for i := range r.slots {
226				hit := &composeBatch{valid: true}
227				hit.keys[0], hit.cached[0], hit.cached[1] = tc.key, tc.key, other
228				miss := &composeBatch{valid: true}
229				miss.cached[0] = other
230				r.slots[i].batches = []*composeBatch{hit, miss}
231			}
232			tc.release(r)
233			for i := range r.slots {
234				hit, miss := r.slots[i].batches[0], r.slots[i].batches[1]
235				if hit.valid || hit.cached[0] != (resourceKey{}) || hit.keys[0] != (resourceKey{}) || hit.cached[1] != other {
236					t.Fatalf("slot %d: released resource kept its descriptor cache", i)
237				}
238				if !miss.valid || miss.cached[0] != other {
239					t.Fatalf("slot %d: unrelated batch invalidated", i)
240				}
241			}
242		})
243	}
244}
245
246func TestTrimComposeKeepsInFlightAndRecentBatches(t *testing.T) {
247	const frame = 100
248	for _, tc := range []struct {
249		name   string
250		busy   bool
251		cutoff uint64
252		last   []uint64 // per batch, in slot order
253		want   int
254	}{
255		{"idle stale", false, 50, []uint64{10, 20}, 0},
256		{"busy stale", true, 50, []uint64{10, 20}, 2},
257		{"newer than cutoff", false, 50, []uint64{10, 60}, 2},
258		{"current frame", false, frame, []uint64{10, frame}, 2},
259		{"only trailing stale", false, 50, []uint64{60, 10}, 1},
260	} {
261		t.Run(tc.name, func(t *testing.T) {
262			r := &Renderer{frame: frame}
263			s := &r.slots[0]
264			s.busy = tc.busy
265			for _, last := range tc.last {
266				s.batches = append(s.batches, &composeBatch{last: last})
267			}
268			r.trimCompose(tc.cutoff)
269			if len(s.batches) != tc.want {
270				t.Fatalf("kept %d batches, want %d", len(s.batches), tc.want)
271			}
272		})
273	}
274}

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