1package vkdevice
2
3import (
4 "image"
5 "image/color"
6 "strings"
7 "testing"
8)
9
10type nonComparableImage struct{ data []byte }
11
12func (n nonComparableImage) ColorModel() color.Model { return color.NRGBAModel }
13func (n nonComparableImage) Bounds() image.Rectangle { return image.Rect(0, 0, 1, 1) }
14func (n nonComparableImage) At(x, y int) color.Color { return color.NRGBA{} }
15
16func TestImageCacheBudgetAndFenceReuse(t *testing.T) {
17 p := newImageCachePolicy()
18 p.begin(1, 0)
19 images := make([]*image.RGBA, 64)
20 for i := range images {
21 images[i] = image.NewRGBA(image.Rect(0, 0, 1, 1))
22 if _, _, err := p.reserve(images[i]); err != nil {
23 t.Fatal(err)
24 }
25 }
26 fresh := image.NewRGBA(image.Rect(0, 0, 1, 1))
27 if _, _, err := p.reserve(fresh); err == nil || !strings.Contains(err.Error(), "budget") {
28 t.Fatalf("expected pinned budget: %v", err)
29 }
30 p.begin(2, 0)
31 if _, _, err := p.reserve(fresh); err == nil || !strings.Contains(err.Error(), "budget") {
32 t.Fatalf("expected in-flight budget: %v", err)
33 }
34 if len(p.free) != 0 || len(p.retired) != 0 {
35 t.Fatalf("slot retired before fence: %+v", p)
36 }
37 p.begin(3, 1)
38 entry, destroyed, err := p.reserve(fresh)
39 if err != nil || entry == nil || len(destroyed) != 1 || entry.slot != destroyed[0].slot {
40 t.Fatalf("slot reuse after fence: %+v %v", entry, err)
41 }
42 if _, _, err := p.reserve(nonComparableImage{data: []byte{1}}); err == nil || !strings.Contains(err.Error(), "comparable") {
43 t.Fatalf("expected identity error: %v", err)
44 }
45 if _, _, err := p.reserve(image.NewRGBA(image.Rect(0, 0, maxImageDimension+1, 1))); err == nil || !strings.Contains(err.Error(), "maximum") {
46 t.Fatalf("expected size error: %v", err)
47 }
48}
49
50func TestImageCacheWideImageAndDeviceLimit(t *testing.T) {
51 p := newImageCachePolicy()
52 p.begin(1, 0)
53 // Bounds alone suffice: reserve validates before reading pixels.
54 wide := &image.RGBA{Rect: image.Rect(0, 0, 5120, 2160)}
55 if _, _, err := p.reserve(wide); err != nil {
56 t.Fatalf("ultrawide image: %v", err)
57 }
58 p.maxDimension = 4096
59 if _, _, err := p.reserve(wide); err == nil || !strings.Contains(err.Error(), "maximum 4096") {
60 t.Fatalf("expected device limit: %v", err)
61 }
62 p.maxDimension = maxImageDimension
63 tooLarge := &image.RGBA{Rect: image.Rect(0, 0, 7680, 4320)}
64 if _, _, err := p.reserve(tooLarge); err == nil || !strings.Contains(err.Error(), "budget") {
65 t.Fatalf("expected unchanged byte budget: %v", err)
66 }
67}
68
69func TestImageCacheResolveDeviceLimit(t *testing.T) {
70 p := newImageCachePolicy()
71 p.maxDimension = 4096
72 c := &imageCache{policy: p}
73 wide := &image.RGBA{Rect: image.Rect(0, 0, 5120, 2160)}
74 if _, err := c.resolve(&Frame{}, []Batch{{Source: wide}}); err == nil || !strings.Contains(err.Error(), "maximum 4096") {
75 t.Fatalf("expected frame validation against device limit: %v", err)
76 }
77}
78
79func TestImageCacheLRUAndBytes(t *testing.T) {
80 p := newImageCachePolicy()
81 old := image.NewRGBA(image.Rect(0, 0, 2048, 2048))
82 other := image.NewRGBA(image.Rect(0, 0, 2048, 2048))
83 p.begin(1, 0)
84 p.reserve(old)
85 p.begin(2, 1)
86 p.reserve(other)
87 p.begin(3, 2)
88 p.reserve(other)
89 newest := image.NewRGBA(image.Rect(0, 0, 4096, 3072))
90 entry, destroyed, err := p.reserve(newest)
91 if err != nil || entry == nil || len(destroyed) == 0 || destroyed[0].key != old || p.bytes > maxImageBytes {
92 t.Fatalf("LRU: entry=%v evicted=%v err=%v bytes=%d", entry, destroyed, err, p.bytes)
93 }
94}
95
96// A frame whose resolution fails must not stall later fence completion or
97// leak the slots it reserved.
98func TestImageCacheAbortCompletesSerialAndFreesSlots(t *testing.T) {
99 c := &imageCache{policy: newImageCachePolicy(), textures: make(map[*imageEntry]*gpuImage), done: make(map[uint64]bool), inflight: make(map[*Frame]struct{})}
100 free := len(c.policy.free)
101 failed := &Frame{}
102 c.serial++
103 failed.imageSerial = c.serial
104 c.policy.begin(c.serial, c.policy.completed)
105 e, _, err := c.policy.reserve(image.NewRGBA(image.Rect(0, 0, 1, 1)))
106 if err != nil {
107 t.Fatal(err)
108 }
109 failed.imageCopies = append(failed.imageCopies, imageCopy{entry: e})
110 c.abort(failed)
111 if len(c.policy.free) != free || c.policy.bytes != 0 || len(c.policy.entries) != 0 {
112 t.Fatalf("abort leaked reservation: free=%d bytes=%d entries=%d", len(c.policy.free), c.policy.bytes, len(c.policy.entries))
113 }
114 c.serial++
115 c.policy.begin(c.serial, c.policy.completed)
116 c.complete(c.serial)
117 if c.policy.completed != c.serial {
118 t.Fatalf("completion stalled at %d, want %d", c.policy.completed, c.serial)
119 }
120}
121
122// After a submitted list frame settles, a failure on the same slot (e.g. a
123// rect submission) must not destroy textures that submitted work used.
124func TestSettledFrameDoesNotAbortSubmittedTextures(t *testing.T) {
125 c := &imageCache{policy: newImageCachePolicy(), textures: make(map[*imageEntry]*gpuImage), done: make(map[uint64]bool), inflight: make(map[*Frame]struct{})}
126 f := &Frame{device: &Device{images: c}}
127 c.serial++
128 f.imageSerial = c.serial
129 c.policy.begin(c.serial, c.policy.completed)
130 img := image.NewRGBA(image.Rect(0, 0, 1, 1))
131 e, _, err := c.policy.reserve(img)
132 if err != nil {
133 t.Fatal(err)
134 }
135 f.imageCopies = append(f.imageCopies, imageCopy{entry: e})
136 f.submitted = true
137 f.settleImages()
138 if f.imageSerial != 0 || len(f.imageCopies) != 0 || c.policy.completed != 1 {
139 t.Fatalf("settle: serial=%d copies=%d completed=%d", f.imageSerial, len(f.imageCopies), c.policy.completed)
140 }
141 // Close before submit (settle on an unsubmitted frame) frees new entries.
142 c.serial++
143 unsent := &Frame{device: f.device, imageSerial: c.serial}
144 c.policy.begin(c.serial, c.policy.completed)
145 fresh := image.NewRGBA(image.Rect(0, 0, 1, 1))
146 fe, _, err := c.policy.reserve(fresh)
147 if err != nil {
148 t.Fatal(err)
149 }
150 unsent.imageCopies = append(unsent.imageCopies, imageCopy{entry: fe})
151 unsent.settleImages()
152 if c.policy.entries[fresh] != nil || c.policy.completed != 2 {
153 t.Fatalf("unsubmitted settle: entry kept=%v completed=%d", c.policy.entries[fresh] != nil, c.policy.completed)
154 }
155 // Settling again (as a later failed submission would) must keep the entry.
156 f.submitted = false
157 f.settleImages()
158 if c.policy.entries[img] != e {
159 t.Fatal("submitted texture entry dropped")
160 }
161}
162
163// A frame slot that stays idle (not re-recorded) after its fence signals must
164// not stall the contiguous completed serial: later frames would otherwise pin
165// retired textures until the 64-slot budget is exhausted.
166func TestIdleSlotDoesNotPinTextures(t *testing.T) {
167 c := &imageCache{policy: newImageCachePolicy(), textures: make(map[*imageEntry]*gpuImage), done: make(map[uint64]bool), inflight: make(map[*Frame]struct{})}
168 d := &Device{images: c}
169 idle := &Frame{device: d}
170 busy := &Frame{device: d}
171 signalled := map[*Frame]bool{}
172 ready := func(f *Frame) (bool, error) { return signalled[f], nil }
173 submit := func(f *Frame) {
174 if err := c.pollInflight(ready); err != nil {
175 t.Fatal(err)
176 }
177 f.settleImages()
178 c.serial++
179 f.imageSerial = c.serial
180 c.policy.begin(c.serial, c.policy.completed)
181 if _, _, err := c.policy.reserve(image.NewRGBA(image.Rect(0, 0, 1, 1))); err != nil {
182 t.Fatalf("frame %d: %v", c.serial, err)
183 }
184 f.submitted = true
185 c.inflight[f] = struct{}{}
186 }
187 submit(idle)
188 signalled[idle] = true // fence done, but the slot is never recorded again
189 for i := 0; i < 3*maxImageTextures; i++ {
190 submit(busy)
191 signalled[busy] = true
192 }
193 if c.policy.completed+1 < c.serial {
194 t.Fatalf("completion stalled at %d of %d", c.policy.completed, c.serial)
195 }
196}