v0.7.3 bcc9a45

image_cache_policy_test.go RawHistoryFolder

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}

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 1

Tags 12

↑↓ moveenter openesc close