1package vkdevice
2
3import (
4 "fmt"
5 "image"
6 "reflect"
7)
8
9const maxImageTextures = 64
10const imageDescriptorSlots = maxImageTextures + 1 // one fallback plus the 64-texture budget
11const maxImageBytes = 64 << 20
12const maxImageDimension = 8192
13
14type imageEntry struct {
15 key image.Image
16 slot int
17 bytes int
18 used, lastSerial uint64
19 retiring bool
20}
21
22// imageCachePolicy tracks frame use separately from completed fence serials.
23// The 64 image descriptor sets plus one fallback set are allocated once,
24// without FREE_DESCRIPTOR_SET. A
25// retired texture keeps its slot until its last sampling fence completes;
26// only then may UpdateDescriptorSets overwrite the recycled set. The pool is
27// destroyed once at shutdown. This avoids requiring FreeDescriptorSets.
28type imageCachePolicy struct {
29 maxDimension int
30 entries map[image.Image]*imageEntry
31 free []int
32 retired []*imageEntry
33 bytes int
34 frame, completed uint64
35}
36
37func newImageCachePolicy() *imageCachePolicy {
38 p := &imageCachePolicy{entries: make(map[image.Image]*imageEntry), maxDimension: maxImageDimension}
39 // Slot 0 is permanently reserved for the valid set 1 fallback.
40 for i := imageDescriptorSlots - 1; i >= 1; i-- {
41 p.free = append(p.free, i)
42 }
43 return p
44}
45
46func (p *imageCachePolicy) begin(serial, completed uint64) []*imageEntry {
47 p.frame = serial
48 if completed > p.completed {
49 p.completed = completed
50 }
51 return p.collect()
52}
53
54func (p *imageCachePolicy) collect() []*imageEntry {
55 var destroyed []*imageEntry
56 keep := p.retired[:0]
57 for _, e := range p.retired {
58 if e.lastSerial > p.completed {
59 keep = append(keep, e)
60 continue
61 }
62 p.free = append(p.free, e.slot)
63 p.bytes -= e.bytes
64 destroyed = append(destroyed, e)
65 }
66 p.retired = keep
67 return destroyed
68}
69
70func (p *imageCachePolicy) reserve(key image.Image) (*imageEntry, []*imageEntry, error) {
71 if key == nil || !reflect.TypeOf(key).Comparable() {
72 return nil, nil, fmt.Errorf("image cache: image identity must be a comparable non-nil image.Image value")
73 }
74 bounds := key.Bounds()
75 w, h := bounds.Dx(), bounds.Dy()
76 if w <= 0 || h <= 0 || w > p.maxDimension || h > p.maxDimension {
77 return nil, nil, fmt.Errorf("image cache: invalid dimensions %dx%d (maximum %d)", w, h, p.maxDimension)
78 }
79 if e := p.entries[key]; e != nil {
80 e.used, e.lastSerial = p.frame, p.frame
81 return e, nil, nil
82 }
83 size := w * h * 4
84 if size > maxImageBytes {
85 return nil, nil, fmt.Errorf("image cache: %d bytes exceeds %d-byte budget", size, maxImageBytes)
86 }
87 var destroyed []*imageEntry
88 for len(p.free) == 0 || p.bytes+size > maxImageBytes {
89 var victim *imageEntry
90 for _, e := range p.entries {
91 if e.used == p.frame || e.lastSerial > p.completed || (victim != nil && e.used >= victim.used) {
92 continue
93 }
94 victim = e
95 }
96 if victim == nil {
97 return nil, destroyed, fmt.Errorf("image cache: 64 textures/64 MiB budget exhausted by images used this frame or awaiting fences")
98 }
99 delete(p.entries, victim.key)
100 victim.retiring = true
101 p.retired = append(p.retired, victim)
102 destroyed = append(destroyed, p.collect()...)
103 }
104 slot := p.free[len(p.free)-1]
105 p.free = p.free[:len(p.free)-1]
106 e := &imageEntry{key: key, slot: slot, bytes: size, used: p.frame, lastSerial: p.frame}
107 p.entries[key] = e
108 p.bytes += size
109 return e, destroyed, nil
110}
111
112// drop forgets an entry that no submitted work has used and frees its slot.
113func (p *imageCachePolicy) drop(e *imageEntry) {
114 if p.entries[e.key] != e {
115 return
116 }
117 delete(p.entries, e.key)
118 p.free = append(p.free, e.slot)
119 p.bytes -= e.bytes
120}