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 = 4096
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 entries map[image.Image]*imageEntry
30 free []int
31 retired []*imageEntry
32 bytes int
33 frame, completed uint64
34}
35
36func newImageCachePolicy() *imageCachePolicy {
37 p := &imageCachePolicy{entries: make(map[image.Image]*imageEntry)}
38 // Slot 0 is permanently reserved for the valid set 1 fallback.
39 for i := imageDescriptorSlots - 1; i >= 1; i-- {
40 p.free = append(p.free, i)
41 }
42 return p
43}
44
45func (p *imageCachePolicy) begin(serial, completed uint64) []*imageEntry {
46 p.frame = serial
47 if completed > p.completed {
48 p.completed = completed
49 }
50 return p.collect()
51}
52
53func (p *imageCachePolicy) collect() []*imageEntry {
54 var destroyed []*imageEntry
55 keep := p.retired[:0]
56 for _, e := range p.retired {
57 if e.lastSerial > p.completed {
58 keep = append(keep, e)
59 continue
60 }
61 p.free = append(p.free, e.slot)
62 p.bytes -= e.bytes
63 destroyed = append(destroyed, e)
64 }
65 p.retired = keep
66 return destroyed
67}
68
69func (p *imageCachePolicy) reserve(key image.Image) (*imageEntry, []*imageEntry, error) {
70 if key == nil || !reflect.TypeOf(key).Comparable() {
71 return nil, nil, fmt.Errorf("image cache: image identity must be a comparable non-nil image.Image value")
72 }
73 bounds := key.Bounds()
74 w, h := bounds.Dx(), bounds.Dy()
75 if w <= 0 || h <= 0 || w > maxImageDimension || h > maxImageDimension {
76 return nil, nil, fmt.Errorf("image cache: invalid dimensions %dx%d (maximum %d)", w, h, maxImageDimension)
77 }
78 if e := p.entries[key]; e != nil {
79 e.used, e.lastSerial = p.frame, p.frame
80 return e, nil, nil
81 }
82 size := w * h * 4
83 if size > maxImageBytes {
84 return nil, nil, fmt.Errorf("image cache: %d bytes exceeds %d-byte budget", size, maxImageBytes)
85 }
86 var destroyed []*imageEntry
87 for len(p.free) == 0 || p.bytes+size > maxImageBytes {
88 var victim *imageEntry
89 for _, e := range p.entries {
90 if e.used == p.frame || e.lastSerial > p.completed || (victim != nil && e.used >= victim.used) {
91 continue
92 }
93 victim = e
94 }
95 if victim == nil {
96 return nil, destroyed, fmt.Errorf("image cache: 64 textures/64 MiB budget exhausted by images used this frame or awaiting fences")
97 }
98 delete(p.entries, victim.key)
99 victim.retiring = true
100 p.retired = append(p.retired, victim)
101 destroyed = append(destroyed, p.collect()...)
102 }
103 slot := p.free[len(p.free)-1]
104 p.free = p.free[:len(p.free)-1]
105 e := &imageEntry{key: key, slot: slot, bytes: size, used: p.frame, lastSerial: p.frame}
106 p.entries[key] = e
107 p.bytes += size
108 return e, destroyed, nil
109}
110
111// drop forgets an entry that no submitted work has used and frees its slot.
112func (p *imageCachePolicy) drop(e *imageEntry) {
113 if p.entries[e.key] != e {
114 return
115 }
116 delete(p.entries, e.key)
117 p.free = append(p.free, e.slot)
118 p.bytes -= e.bytes
119}