compose_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"context"
5	"image"
6	"image/color"
7	"os"
8	"slices"
9	"testing"
10	"time"
11
12	"github.com/bnema/neferwl/internal/adapters/syncfile"
13	"github.com/bnema/neferwl/internal/ports"
14)
15
16// near reports whether two colors are within tol per channel.
17func near(a, b color.RGBA, tol int) bool {
18	d := func(x, y uint8) bool { v := int(x) - int(y); return v >= -tol && v <= tol }
19	return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B)
20}
21
22// Scaled wl_shm pixels filter linearly: halfway between a black and a
23// white texel is grey, not either one (nearest neighbour).
24func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
25	r, err := New(64, 16)
26	if err != nil {
27		t.Skipf("Vulkan unavailable: %v", err)
28	}
29	defer r.Close()
30	// 2×1 buffer: black, white; drawn 64×16, texel centres at x=16, x=48.
31	px := []byte{0, 0, 0, 255, 255, 255, 255, 255}
32	c := shmContent(t, 2, 1, 8, px)
33	c.LogicalW, c.LogicalH = 64, 16
34	scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
35	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
36		t.Fatal(err)
37	}
38	got := readPixels(t, r).RGBAAt(32, 8)
39	if !near(got, color.RGBA{128, 128, 128, 255}, 8) {
40		t.Fatalf("midpoint %v, want grey", got)
41	}
42	if got := readPixels(t, r).RGBAAt(2, 8); !near(got, color.RGBA{0, 0, 0, 255}, 2) {
43		t.Fatalf("left edge %v, want black (clamped)", got)
44	}
45}
46
47func TestRendererSourceCrop(t *testing.T) {
48	r, err := New(16, 8)
49	if err != nil {
50		t.Skipf("Vulkan unavailable: %v", err)
51	}
52	defer r.Close()
53	px := []byte{0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255}
54	c := shmContent(t, 4, 1, 16, px)
55	c.LogicalW, c.LogicalH = 16, 8
56	c.Source = [4]float32{1, 0, 2, 1}
57	scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 8}}}}
58	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
59		t.Fatal(err)
60	}
61	for _, tc := range []struct {
62		x    int
63		want color.RGBA
64	}{{0, color.RGBA{0, 255, 0, 255}}, {15, color.RGBA{0, 0, 255, 255}}} {
65		if got := readPixels(t, r).RGBAAt(tc.x, 4); !near(got, tc.want, 5) {
66			t.Errorf("crop x=%d: %v want %v", tc.x, got, tc.want)
67		}
68	}
69}
70
71// Scaled dmabufs are sampled with the linear sampler.
72func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
73	r, err := New(128, 4)
74	if err != nil {
75		t.Skipf("Vulkan unavailable: %v", err)
76	}
77	defer r.Close()
78	linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
79	if !slices.Contains(r.DMABuf().Formats, linear) {
80		t.Skipf("linear ARGB8888 not importable")
81	}
82	// 64×1: left half black, right half white, drawn twice as wide.
83	f := udmabuf(t, 64, 1, func(x, _ int) [4]byte {
84		if x < 32 {
85			return [4]byte{0, 0, 0, 255}
86		}
87		return [4]byte{255, 255, 255, 255}
88	})
89	buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
90	scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}}}}
91	c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}
92	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
93		t.Fatal(err)
94	}
95	// Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31
96	// (black, centre 31.5) and 32 (white, centre 32.5), 75% white.
97	if got := readPixels(t, r).RGBAAt(64, 2); !near(got, color.RGBA{191, 191, 191, 255}, 10) {
98		t.Fatalf("boundary %v, want a linear mix", got)
99	}
100}
101
102// Render returns a sync file that becomes readable when the GPU is done.
103func TestRendererReturnsDoneFence(t *testing.T) {
104	r, err := New(32, 32)
105	if err != nil {
106		t.Skipf("Vulkan unavailable: %v", err)
107	}
108	defer r.Close()
109	if !r.syncFD {
110		t.Skip("device cannot export sync files")
111	}
112	done, err := r.Render(ports.Scene{Background: "#102030"}, nil)
113	if err != nil {
114		t.Fatal(err)
115	}
116	if done == nil {
117		t.Fatal("no done fence")
118	}
119	defer done.Close()
120	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
121	defer cancel()
122	if err := syncfile.Wait(ctx, done); err != nil {
123		t.Fatal(err)
124	}
125}
126
127// An unchanged wl_shm window is not copied again; a new content is.
128func TestRendererSHMCopiedOncePerContent(t *testing.T) {
129	r, err := New(32, 32)
130	if err != nil {
131		t.Skipf("Vulkan unavailable: %v", err)
132	}
133	defer r.Close()
134	c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})
135	c.ID, c.Seq, c.Version = 1, 1, 1
136	scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
137	contents := map[ports.WindowID]ports.SurfaceContent{1: c}
138	render := func() {
139		t.Helper()
140		done, err := r.Render(scene, contents)
141		if done != nil {
142			done.Close()
143		}
144		if err != nil {
145			t.Fatal(err)
146		}
147	}
148	render()
149	if r.copied != 16*16*4 {
150		t.Fatalf("first frame copied %d", r.copied)
151	}
152	for range 3 {
153		render()
154	}
155	if r.copied != 16*16*4 {
156		t.Fatalf("unchanged content copied again: %d", r.copied)
157	}
158	c.Seq, c.Version = 2, 2
159	contents[1] = c
160	render()
161	if r.copied != 2*16*16*4 {
162		t.Fatalf("new content copied %d", r.copied)
163	}
164}
165
166// A surface fully hidden by an opaque subsurface above it is not copied:
167// only the subsurface's bytes are, and the frame shows it.
168func TestRendererSkipsOccludedSurface(t *testing.T) {
169	r, err := New(32, 32)
170	if err != nil {
171		t.Skipf("Vulkan unavailable: %v", err)
172	}
173	defer r.Close()
174	green := color.RGBA{0, 255, 0, 255}
175	root := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
176	root.ID, root.Seq, root.Surface, root.Version = 1, 1, 1, 1
177	game := solidContent(t, 16, 16, green)
178	game.Surface, game.Version = 2, 1
179	game.Opaque = true
180	root.Children = []ports.Subsurface{{SurfaceContent: game}}
181	scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
182	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
183		t.Fatal(err)
184	}
185	if r.copied != 16*16*4 {
186		t.Fatalf("copied %d bytes, want only the subsurface's %d", r.copied, 16*16*4)
187	}
188	if got := readPixels(t, r).At(8, 8); got != green {
189		t.Fatalf("At(8,8)=%v want %v", got, green)
190	}
191	// A smaller opaque child leaves part of the root visible: it is copied.
192	small := solidContent(t, 8, 8, green)
193	small.Surface, small.Version = 3, 1
194	small.Opaque = true
195	root.Seq = 2
196	root.Children = []ports.Subsurface{{SurfaceContent: small}}
197	r.copied = 0
198	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
199		t.Fatal(err)
200	}
201	if r.copied != 16*16*4+8*8*4 {
202		t.Fatalf("partly hidden root: copied %d", r.copied)
203	}
204}
205
206// A covering child whose buffer will not draw does not hide the root:
207// an unimportable dmabuf still shows the surface below it, not a hole.
208func TestRendererKeepsSurfaceBelowFailedCover(t *testing.T) {
209	r, err := New(32, 32)
210	if err != nil {
211		t.Skipf("Vulkan unavailable: %v", err)
212	}
213	defer r.Close()
214	red := color.RGBA{255, 0, 0, 255}
215	root := solidContent(t, 16, 16, red)
216	root.ID, root.Seq = 1, 1
217	bad := &ports.DMABuf{ID: 9, Width: 16, Height: 16, Format: 0xdeadbeef}
218	root.Children = []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Width: 16, Height: 16, Opaque: true, DMABuf: bad}}}
219	scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
220	if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
221		t.Fatal(err)
222	}
223	if r.copied != 16*16*4 {
224		t.Fatalf("copied %d bytes, want the root's %d", r.copied, 16*16*4)
225	}
226	if got := readPixels(t, r).At(8, 8); got != red {
227		t.Fatalf("At(8,8)=%v want %v", got, red)
228	}
229}
230
231// render draws a frame and closes its fence.
232func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
233	done, err := r.Render(s, contents)
234	if done != nil {
235		done.Close()
236	}
237	return err
238}
239
240// A wl_shm window's GPU buffer is rewritten only after every frame that
241// read it completed: contents 1, 2, 3 back to back alternate two buffers,
242// and the third copy waits for the frame that drew the first (the slot
243// ring and shmCopyFor's waitFrame both guarantee it).
244func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
245	r, err := New(32, 32)
246	if err != nil {
247		t.Skipf("Vulkan unavailable: %v", err)
248	}
249	defer r.Close()
250	if !r.syncFD {
251		t.Skip("frames are finished on return without sync files")
252	}
253	scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
254	var readers []uint64
255	for seq := uint64(1); seq <= 3; seq++ {
256		c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})
257		c.ID, c.Seq, c.Version = 1, seq, seq
258		if seq == 3 {
259			// The buffer about to be reused was last read by frame 1.
260			if r.completed >= readers[0] {
261				t.Fatalf("frame %d already known done before the reuse", readers[0])
262			}
263		}
264		if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
265			t.Fatal(err)
266		}
267		readers = append(readers, r.frame)
268		if seq == 3 && r.completed < readers[0] {
269			t.Fatalf("buffer of frame %d rewritten with completed=%d", readers[0], r.completed)
270		}
271	}
272	if got := readPixels(t, r).RGBAAt(4, 4); got != (color.RGBA{180, 0, 0, 255}) {
273		t.Fatalf("pixel %v", got)
274	}
275}
276
277// A content's explicit acquire fence becomes a wait semaphore; a file
278// that is not a sync file is refused (no semaphore) without failing the
279// frame, and the caller's file stays open.
280func TestRendererImportsAcquireFence(t *testing.T) {
281	r, err := New(8, 8)
282	if err != nil {
283		t.Skipf("Vulkan unavailable: %v", err)
284	}
285	defer r.Close()
286	done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
287	if err != nil || done == nil {
288		t.Skipf("no sync file: %v", err)
289	}
290	defer done.Close()
291	sem := r.importFence(done)
292	if sem == 0 {
293		t.Fatal("sync file not imported")
294	}
295	r.dd.DestroySemaphore(r.device, sem, nil)
296	if _, err := done.Stat(); err != nil {
297		t.Fatal("caller's fence closed")
298	}
299	f, w, _ := os.Pipe()
300	defer f.Close()
301	defer w.Close()
302	if sem := r.importFence(f); sem != 0 {
303		t.Fatal("pipe imported as a fence")
304	}
305}
306
307// readPixels reads the frame back, failing the test when Pixels cannot.
308func readPixels(t *testing.T, r *Renderer) *image.RGBA {
309	t.Helper()
310	img := r.Pixels()
311	if img == nil {
312		t.Fatal("Pixels: no free capture slot or copy failed")
313	}
314	return img
315}

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