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 and its retire closure (about 4 per frame). Skipped without a
144// GPU or /dev/udmabuf.
145func TestRenderSteadyStateDMABufAllocations(t *testing.T) {
146 r, err := New(80, 32)
147 if err != nil {
148 t.Skipf("Vulkan unavailable: %v", err)
149 }
150 defer r.Close()
151 linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
152 if !slices.Contains(r.DMABuf().Formats, linear) {
153 t.Skipf("linear ARGB8888 not importable")
154 }
155 f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
156 buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
157 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}}}}
158 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Surface: 1, Seq: 1, Version: 1, Width: 64, Height: 16, DMABuf: buf}}
159 frame := func() {
160 if err := render(r, scene, contents); err != nil {
161 t.Fatal(err)
162 }
163 }
164 frame()
165 if len(r.imports) != 1 {
166 t.Fatalf("imports %d: the frame does not take the dmabuf path", len(r.imports))
167 }
168 // Each new scene Seq draws the dmabuf again, with its barriers.
169 var moving [4]ports.Scene
170 for i := range moving {
171 moving[i] = scene
172 moving[i].Seq = uint64(10 + i)
173 moving[i].Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4 + i, Y: 4, W: 64, H: 16}}}
174 }
175 n := 0
176 moved := func() {
177 n++
178 if err := render(r, moving[n%len(moving)], contents); err != nil {
179 t.Fatal(err)
180 }
181 }
182 moved()
183 allocs := testing.AllocsPerRun(20, moved)
184 t.Logf("dmabuf Render: %.1f allocs/frame", allocs)
185 if allocs > 7 {
186 t.Errorf("dmabuf steady-state Render: %.1f allocs/frame, want <=7 (syncFD path: %v; a driver without sync-file export takes another path)", allocs, r.syncFD)
187 }
188}
189
190// TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
191// subsurface tree. Each child has its own real wl_shm pool.
192func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
193 const tiles = 64
194 r, err := New(128, 128)
195 if err != nil {
196 t.Skipf("Vulkan unavailable: %v", err)
197 }
198 defer r.Close()
199 root := ports.SurfaceContent{ID: 1, Seq: 1, Surface: 1, Version: 1}
200 for i := range tiles {
201 child := solidContent(t, 8, 8, color.RGBA{R: uint8(i + 1), A: 255})
202 child.ID, child.Seq, child.Surface, child.Version = 1, 1, uint64(i+2), 1
203 root.Children = append(root.Children, ports.Subsurface{X: i % 8 * 8, Y: i / 8 * 8, SurfaceContent: child})
204 }
205 video := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
206 video.ID, video.Seq, video.Surface, video.Version = 1, 1, 66, 1
207 root.Children = append(root.Children, ports.Subsurface{X: 80, Y: 0, SurfaceContent: video})
208 scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 128}}}}
209 contents := map[ports.WindowID]ports.SurfaceContent{1: root}
210 if err := render(r, scene, contents); err != nil {
211 t.Fatal(err)
212 }
213 for _, count := range []int{16, 64} {
214 contents[1] = ports.SurfaceContent{ID: 1, Seq: root.Seq, Surface: root.Surface, Version: root.Version, Children: root.Children[:count]}
215 walk := func() {
216 dmg := newDamage(&target{}, scene, image.Rect(0, 0, r.width, r.height))
217 _ = r.draws(scene, contents, dmg)
218 }
219 walk()
220 if allocs := testing.AllocsPerRun(20, walk); allocs > 8 {
221 t.Errorf("%d tiles: %.1f allocations/walk, want <=8", count, allocs)
222 } else {
223 t.Logf("%d tiles: %.1f allocations/walk", count, allocs)
224 }
225 }
226 contents[1] = root
227 // The same renderer and scene are reused; measure only steady-state frame work.
228 frame := func() {
229 if err := render(r, scene, contents); err != nil {
230 t.Fatal(err)
231 }
232 }
233 frame()
234 if allocs := testing.AllocsPerRun(20, frame); allocs > 16 {
235 t.Errorf("steady-state Render: %.1f allocs/frame, want <=16", allocs)
236 } else {
237 t.Logf("steady-state Render: %.1f allocs/frame", allocs)
238 }
239 before := r.copied
240 for i := range 100 {
241 root.Seq++
242 root.Children[tiles].Version++
243 contents[1] = root
244 if err := render(r, scene, contents); err != nil {
245 t.Fatal(err)
246 }
247 if delta := r.copied - before; delta != 8*8*4 {
248 t.Fatalf("publication %d copied %d bytes, want video only (%d)", i, delta, 8*8*4)
249 }
250 before = r.copied
251 }
252 // Reordering equal-sized children must not reuse another child's pixels.
253 root.Children[0], root.Children[1] = root.Children[1], root.Children[0]
254 root.Children[0].X, root.Children[1].X = 0, 8
255 root.Seq++
256 contents[1] = root
257 if err := render(r, scene, contents); err != nil {
258 t.Fatal(err)
259 }
260 if got := readPixels(t, r).RGBAAt(4, 4); got != (color.RGBA{R: 2, A: 255}) {
261 t.Fatalf("restacked tile has pixels %v, want second child's colour", got)
262 }
263}