1package vulkan
2
3import (
4 "image"
5 "image/color"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// TestRenderSteadyStateAllocations measures the cost of a small stable scene,
12// on a normal and on a rotated output (orient works in place), and that
13// building the draws of a scene with a moving window (a new Seq and window
14// slice every frame, as an animation publishes) allocates nothing.
15// A new scene Seq with one of two windows moved is a partial redraw seeded by
16// sceneDelta and allocates nothing either.
17func TestRenderSteadyStateAllocations(t *testing.T) {
18 for _, tc := range []struct {
19 name string
20 transform ports.BufferTransform
21 tw, th int
22 sceneW, sceneH int
23 }{
24 {"transform 0", 0, 32, 32, 32, 32},
25 {"transform 1", 1, 32, 24, 24, 32},
26 } {
27 t.Run(tc.name, func(t *testing.T) {
28 r, err := New(tc.tw, tc.th)
29 if err != nil {
30 t.Skipf("Vulkan unavailable: %v", err)
31 }
32 defer r.Close()
33 scene := ports.Scene{Seq: 1, Transform: tc.transform, Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: tc.sceneW, H: tc.sceneH}}}}
34 content := solidContent(t, 8, 8, color.RGBA{R: 255, A: 255})
35 content.ID, content.Surface, content.Seq, content.Version = 1, 1, 1, 1
36 contents := map[ports.WindowID]ports.SurfaceContent{1: content}
37 frame := func() {
38 if err := render(r, scene, contents); err != nil {
39 t.Fatal(err)
40 }
41 }
42 frame()
43 if allocs := testing.AllocsPerRun(20, frame); allocs > 13 {
44 t.Errorf("small steady-state Render: %.1f allocs/frame, want <=13", allocs)
45 } else {
46 t.Logf("small steady-state Render: %.1f allocs/frame", allocs)
47 }
48 // A moving window: scenes are prebuilt (core allocates them, not
49 // the renderer), each with a fresh Seq, so every frame draws
50 // everything again. The draw building must not allocate.
51 var moving [4]ports.Scene
52 for i := range moving {
53 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}}}}
54 }
55 bounds := image.Rect(0, 0, r.width, r.height)
56 n := 0
57 frameDraws := func() {
58 n++
59 s := moving[n%len(moving)]
60 _ = r.draws(s, contents, r.frameDamage(r.last, s, bounds))
61 }
62 frameDraws()
63 if allocs := testing.AllocsPerRun(50, frameDraws); allocs != 0 {
64 t.Errorf("moving window draws: %.1f allocs/frame, want 0", allocs)
65 }
66 // A content change under an unchanged scene Seq: a partial
67 // redraw clipped to the changed buffer region, replicating the
68 // partial sequence of Render. The stand-in target holds the
69 // previous content Seq of the window, and is never updated, so
70 // every run is damaged.
71 dc := content
72 dc.Seq, dc.Version = 2, 2
73 dc.DamageHistory = []ports.SeqDamage{{Seq: 2, Rects: []ports.Rect{{X: 1, Y: 1, W: 2, H: 2}}}}
74 pcontents := map[ports.WindowID]ports.SurfaceContent{1: dc}
75 held := &target{valid: true, sceneSeq: scene.Seq, windows: map[ports.WindowID]heldWindow{1: {seq: 1, rect: image.Rect(0, 0, r.width, r.height)}}}
76 var area image.Rectangle
77 var whole bool
78 partial := func() {
79 dmg := r.frameDamage(held, scene, bounds)
80 ds := r.draws(scene, pcontents, dmg)
81 dmg.finish()
82 ds = append(ds, draw{})
83 copy(ds[1:], ds[:len(ds)-1])
84 ds[0] = r.fillDraw(dmg.area, parseColor(scene.Background))
85 ds = dmg.clip(ds)
86 r.scratchDraws = ds
87 r.orient(ds, scene.Transform)
88 area, whole = dmg.area, dmg.all()
89 }
90 partial()
91 if whole || area.Empty() || area.Dx()*area.Dy() >= bounds.Dx()*bounds.Dy() {
92 t.Fatalf("not a partial redraw: all %v, area %v of %v", whole, area, bounds)
93 }
94 if allocs := testing.AllocsPerRun(50, partial); allocs != 0 {
95 t.Errorf("partial redraw draws: %.1f allocs/frame, want 0", allocs)
96 }
97 // A new scene Seq with one window moved: the delta against the
98 // scene the target holds seeds the partial redraw (A2). The
99 // stand-in target holds the old scene and is never updated.
100 half := tc.sceneW / 2
101 content2 := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
102 content2.ID, content2.Surface, content2.Seq, content2.Version = 2, 2, 1, 1
103 dcontents := map[ports.WindowID]ports.SurfaceContent{1: content, 2: content2}
104 before := ports.Scene{Seq: 200, Transform: tc.transform, Scale: 1, Windows: []ports.SceneWindow{
105 {ID: 1, Rect: ports.Rect{W: half, H: tc.sceneH}},
106 {ID: 2, Rect: ports.Rect{X: half, W: tc.sceneW - half, H: tc.sceneH}},
107 }}
108 moved := before
109 moved.Seq = 201
110 moved.Windows = []ports.SceneWindow{before.Windows[0], {ID: 2, Rect: ports.Rect{X: half + 1, W: tc.sceneW - half - 1, H: tc.sceneH}}}
111 full := image.Rect(0, 0, r.width, r.height)
112 held = &target{valid: true, sceneSeq: before.Seq, scene: before, windows: map[ports.WindowID]heldWindow{
113 1: {seq: 1, rect: image.Rect(0, 0, half, tc.sceneH)},
114 2: {seq: 1, rect: image.Rect(half, 0, tc.sceneW, tc.sceneH)},
115 }}
116 seeded := func() {
117 dmg := r.frameDamage(held, moved, bounds)
118 ds := r.draws(moved, dcontents, dmg)
119 dmg.finish()
120 ds = append(ds, draw{})
121 copy(ds[1:], ds[:len(ds)-1])
122 ds[0] = r.fillDraw(dmg.area, parseColor(moved.Background))
123 ds = dmg.clip(ds)
124 r.scratchDraws = ds
125 r.orient(ds, moved.Transform)
126 area, whole = dmg.area, dmg.all()
127 }
128 seeded()
129 if whole || area.Empty() || area.Dx()*area.Dy() >= full.Dx()*full.Dy() {
130 t.Fatalf("sceneDelta did not seed a partial redraw: all %v, area %v of %v", whole, area, bounds)
131 }
132 if allocs := testing.AllocsPerRun(50, seeded); allocs != 0 {
133 t.Errorf("sceneDelta-seeded partial redraw draws: %.1f allocs/frame, want 0", allocs)
134 }
135 })
136 }
137}
138
139// TestSceneWalkUnchangedTiledSHM guards the GPU copy budget of an unchanged
140// subsurface tree. Each child has its own real wl_shm pool.
141func TestSceneWalkUnchangedTiledSHM(t *testing.T) {
142 const tiles = 64
143 r, err := New(128, 128)
144 if err != nil {
145 t.Skipf("Vulkan unavailable: %v", err)
146 }
147 defer r.Close()
148 root := ports.SurfaceContent{ID: 1, Seq: 1, Surface: 1, Version: 1}
149 for i := range tiles {
150 child := solidContent(t, 8, 8, color.RGBA{R: uint8(i + 1), A: 255})
151 child.ID, child.Seq, child.Surface, child.Version = 1, 1, uint64(i+2), 1
152 root.Children = append(root.Children, ports.Subsurface{X: i % 8 * 8, Y: i / 8 * 8, SurfaceContent: child})
153 }
154 video := solidContent(t, 8, 8, color.RGBA{G: 255, A: 255})
155 video.ID, video.Seq, video.Surface, video.Version = 1, 1, 66, 1
156 root.Children = append(root.Children, ports.Subsurface{X: 80, Y: 0, SurfaceContent: video})
157 scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 128}}}}
158 contents := map[ports.WindowID]ports.SurfaceContent{1: root}
159 if err := render(r, scene, contents); err != nil {
160 t.Fatal(err)
161 }
162 for _, count := range []int{16, 64} {
163 contents[1] = ports.SurfaceContent{ID: 1, Seq: root.Seq, Surface: root.Surface, Version: root.Version, Children: root.Children[:count]}
164 walk := func() {
165 dmg := newDamage(&target{}, scene, image.Rect(0, 0, r.width, r.height))
166 _ = r.draws(scene, contents, dmg)
167 }
168 walk()
169 if allocs := testing.AllocsPerRun(20, walk); allocs > 8 {
170 t.Errorf("%d tiles: %.1f allocations/walk, want <=8", count, allocs)
171 } else {
172 t.Logf("%d tiles: %.1f allocations/walk", count, allocs)
173 }
174 }
175 contents[1] = root
176 // The same renderer and scene are reused; measure only steady-state frame work.
177 frame := func() {
178 if err := render(r, scene, contents); err != nil {
179 t.Fatal(err)
180 }
181 }
182 frame()
183 if allocs := testing.AllocsPerRun(20, frame); allocs > 16 {
184 t.Errorf("steady-state Render: %.1f allocs/frame, want <=16", allocs)
185 } else {
186 t.Logf("steady-state Render: %.1f allocs/frame", allocs)
187 }
188 before := r.copied
189 for i := range 100 {
190 root.Seq++
191 root.Children[tiles].Version++
192 contents[1] = root
193 if err := render(r, scene, contents); err != nil {
194 t.Fatal(err)
195 }
196 if delta := r.copied - before; delta != 8*8*4 {
197 t.Fatalf("publication %d copied %d bytes, want video only (%d)", i, delta, 8*8*4)
198 }
199 before = r.copied
200 }
201 // Reordering equal-sized children must not reuse another child's pixels.
202 root.Children[0], root.Children[1] = root.Children[1], root.Children[0]
203 root.Children[0].X, root.Children[1].X = 0, 8
204 root.Seq++
205 contents[1] = root
206 if err := render(r, scene, contents); err != nil {
207 t.Fatal(err)
208 }
209 if got := readPixels(t, r).RGBAAt(4, 4); got != (color.RGBA{R: 2, A: 255}) {
210 t.Fatalf("restacked tile has pixels %v, want second child's colour", got)
211 }
212}