v0.1.0 d9218d6

compose_test.go RawHistoryFolder

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

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 26

Tags 10

↑↓ moveenter openesc close