1package vulkan
2
3import (
4 "image/color"
5 "slices"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// trimSettled calls Trim at now until the frames the GPU was running
13// finished and their deferred frees ran: Trim never waits for them.
14func trimSettled(t *testing.T, r *Renderer, now time.Time) {
15 t.Helper()
16 deadline := time.Now().Add(5 * time.Second)
17 for {
18 if err := r.Trim(now); err != nil {
19 t.Fatal(err)
20 }
21 if len(r.deferred) == 0 {
22 return
23 }
24 if time.Now().After(deadline) {
25 t.Fatalf("%d frees never ran", len(r.deferred))
26 }
27 time.Sleep(time.Millisecond)
28 }
29}
30
31// An idle output frees the pool mapping and GPU copies of a window it no
32// longer draws once idleTTL passed, without rendering; the window drawn
33// by the last frame stays, and a window coming back is copied again.
34func TestTrimFreesUndrawnSHMWithoutRender(t *testing.T) {
35 r, err := New(32, 32)
36 if err != nil {
37 t.Skipf("Vulkan unavailable: %v", err)
38 }
39 defer r.Close()
40 a := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
41 a.ID, a.Seq, a.Version = 1, 1, 1
42 b := solidContent(t, 16, 16, color.RGBA{0, 0, 255, 255})
43 b.ID, b.Seq, b.Version = 2, 1, 1
44 contents := map[ports.WindowID]ports.SurfaceContent{1: a, 2: b}
45 both := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}, {ID: 2, Rect: ports.Rect{X: 16, W: 16, H: 16}}}}
46 onlyB := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 2, Rect: ports.Rect{X: 16, W: 16, H: 16}}}}
47 if err := render(r, both, contents); err != nil {
48 t.Fatal(err)
49 }
50 if err := render(r, onlyB, contents); err != nil {
51 t.Fatal(err)
52 }
53 start := time.Unix(1000, 0)
54 for _, dt := range []time.Duration{0, 10 * time.Second, idleTTL - time.Second} {
55 if err := r.Trim(start.Add(dt)); err != nil {
56 t.Fatal(err)
57 }
58 }
59 if len(r.pools) != 2 || len(r.shm) != 2 {
60 t.Fatalf("freed before idleTTL: pools %d, copies %d", len(r.pools), len(r.shm))
61 }
62 trimSettled(t, r, start.Add(idleTTL))
63 if _, ok := r.pools[a.SHM.Pool]; ok || len(r.pools) != 1 {
64 t.Fatalf("undrawn pool kept: %d pools", len(r.pools))
65 }
66 if _, ok := r.shm[shmKey{win: 1}]; ok || len(r.shm) != 1 {
67 t.Fatalf("undrawn copies kept: %d surfaces", len(r.shm))
68 }
69 // Still idle: the window on screen stays.
70 if err := r.Trim(start.Add(10 * idleTTL)); err != nil {
71 t.Fatal(err)
72 }
73 if len(r.pools) != 1 || len(r.shm) != 1 {
74 t.Fatalf("shown window freed: pools %d, copies %d", len(r.pools), len(r.shm))
75 }
76 copied := r.copied
77 if err := render(r, both, contents); err != nil {
78 t.Fatal(err)
79 }
80 if r.copied != copied+16*16*4 {
81 t.Fatalf("returning window copied %d bytes", r.copied-copied)
82 }
83 if got := readPixels(t, r).RGBAAt(4, 4); got != (color.RGBA{255, 0, 0, 255}) {
84 t.Fatalf("returning window shows %v", got)
85 }
86}
87
88// A window drawn again within idleTTL keeps its caches: frames date them.
89func TestTrimKeepsRecentlyDrawn(t *testing.T) {
90 r, err := New(32, 32)
91 if err != nil {
92 t.Skipf("Vulkan unavailable: %v", err)
93 }
94 defer r.Close()
95 a := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
96 a.ID, a.Seq, a.Version = 1, 1, 1
97 contents := map[ports.WindowID]ports.SurfaceContent{1: a}
98 shown := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
99 empty := ports.Scene{Background: "#000000"}
100 start := time.Unix(1000, 0)
101 if err := render(r, shown, contents); err != nil {
102 t.Fatal(err)
103 }
104 if err := render(r, empty, contents); err != nil {
105 t.Fatal(err)
106 }
107 if err := r.Trim(start); err != nil {
108 t.Fatal(err)
109 }
110 // Back on screen, then away again just before idleTTL.
111 if err := render(r, shown, contents); err != nil {
112 t.Fatal(err)
113 }
114 if err := render(r, empty, contents); err != nil {
115 t.Fatal(err)
116 }
117 if err := r.Trim(start.Add(idleTTL - time.Second)); err != nil {
118 t.Fatal(err)
119 }
120 if err := r.Trim(start.Add(idleTTL)); err != nil {
121 t.Fatal(err)
122 }
123 if len(r.pools) != 1 || len(r.shm) != 1 {
124 t.Fatalf("drawn %v ago but freed", time.Second)
125 }
126 if err := r.Trim(start.Add(2*idleTTL - time.Second)); err != nil {
127 t.Fatal(err)
128 }
129 if len(r.pools) != 0 || len(r.shm) != 0 {
130 t.Fatalf("kept after idleTTL: pools %d, copies %d", len(r.pools), len(r.shm))
131 }
132}
133
134// A dmabuf import left undrawn is released by Trim without a frame.
135func TestTrimReleasesUndrawnDMABuf(t *testing.T) {
136 r, err := New(80, 32)
137 if err != nil {
138 t.Skipf("Vulkan unavailable: %v", err)
139 }
140 defer r.Close()
141 linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
142 if !slices.Contains(r.DMABuf().Formats, linear) {
143 t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
144 }
145 f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
146 buf := &ports.DMABuf{ID: 9, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
147 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
148 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}
149 if err := render(r, scene, contents); err != nil {
150 t.Fatal(err)
151 }
152 if err := render(r, ports.Scene{Background: "#000000"}, nil); err != nil {
153 t.Fatal(err)
154 }
155 start := time.Unix(1000, 0)
156 if err := r.Trim(start); err != nil {
157 t.Fatal(err)
158 }
159 trimSettled(t, r, start.Add(idleTTL))
160 if len(r.imports) != 0 {
161 t.Fatalf("imports %d", len(r.imports))
162 }
163}
164
165// poll frees what a finished frame held without waiting for it.
166func TestTrimPollsFinishedFrames(t *testing.T) {
167 r, err := New(32, 32)
168 if err != nil {
169 t.Skipf("Vulkan unavailable: %v", err)
170 }
171 defer r.Close()
172 if !r.syncFD {
173 t.Skip("frames are finished on return without sync files")
174 }
175 if err := render(r, ports.Scene{Background: "#000000"}, nil); err != nil {
176 t.Fatal(err)
177 }
178 freed := false
179 r.retire(r.frame, func() { freed = true })
180 deadline := time.Now().Add(5 * time.Second)
181 for !freed {
182 if time.Now().After(deadline) {
183 t.Fatal("finished frame never polled")
184 }
185 if err := r.Trim(time.Now()); err != nil {
186 t.Fatal(err)
187 }
188 }
189 if r.completed != r.frame {
190 t.Fatalf("completed %d, frame %d", r.completed, r.frame)
191 }
192}