1package vulkan
2
3import (
4 "fmt"
5 "runtime"
6 "unsafe"
7
8 vk "github.com/bnema/purego-vulkan/vulkan"
9)
10
11// Bound descriptor-table work and memory even on devices with huge limits.
12const maxComposeResources = 64
13
14// instanceDraw matches Draw in compose.vert (std430, 128-byte stride).
15type instanceDraw struct {
16 pc drawParameters
17 resource [4]uint32
18}
19
20// resourceKey retains the Go identity as well as the Vulkan handles: a
21// destroyed object's recycled handle must not look like a cached descriptor.
22type resourceKey struct {
23 im *imported
24 shm *shmCopy
25}
26
27// composeBatch belongs to a frame slot. Neither its mapped memory nor its
28// descriptors are changed until the slot's previous submission completed.
29type composeBatch struct {
30 gpu gpuBuffer
31 pool vk.DescriptorPool
32 set vk.DescriptorSet
33 count uint32
34 last uint64 // frame that last prepared this batch
35 instances []instanceDraw
36 keys [maxComposeResources]resourceKey
37 cached [maxComposeResources]resourceKey
38 used int
39 valid bool
40 images [maxComposeResources]vk.DescriptorImageInfo
41 chroma [maxComposeResources]vk.DescriptorImageInfo
42 buffers [maxComposeResources]vk.DescriptorBufferInfo
43 drawBuffer vk.DescriptorBufferInfo
44 writes [4]vk.WriteDescriptorSet
45}
46
47// add preserves paint order. Solids need no resource and can share either
48// neighbouring resource's batch, including on devices without indexing.
49func (b *composeBatch) add(dr draw, resources, maxDraws int) bool {
50 if len(b.instances) == maxDraws {
51 return false
52 }
53 key := resourceKey{im: dr.im, shm: dr.shm}
54 index := 0
55 if key != (resourceKey{}) {
56 for index < b.used && b.keys[index] != key {
57 index++
58 }
59 if index == b.used {
60 if b.used == resources {
61 return false
62 }
63 b.keys[index] = key
64 b.used++
65 }
66 }
67 b.instances = append(b.instances, instanceDraw{pc: dr.pc, resource: [4]uint32{uint32(index)}})
68 return true
69}
70
71func (r *Renderer) prepareCompose(slot *frameSlot, ds []draw) ([]*composeBatch, error) {
72 resources := int(r.compose.resources)
73 maxDraws := r.maxRange / int(unsafe.Sizeof(instanceDraw{}))
74 if maxDraws < 1 {
75 return nil, fmt.Errorf("storage buffer range too small for composition")
76 }
77 used := 0
78 for len(ds) > 0 {
79 if used == len(slot.batches) {
80 slot.batches = append(slot.batches, &composeBatch{})
81 }
82 b := slot.batches[used]
83 clear(b.keys[:])
84 b.used = 0
85 b.instances = b.instances[:0]
86 for len(ds) > 0 && b.add(ds[0], resources, maxDraws) {
87 ds = ds[1:]
88 }
89 if err := r.uploadCompose(b); err != nil {
90 return nil, err
91 }
92 b.last = r.frame
93 used++
94 }
95 // The slot is idle. Drop trailing batches after the same frame TTL
96 // as client resources, rather than retaining a peak scene forever.
97 for len(slot.batches) > used {
98 last := len(slot.batches) - 1
99 b := slot.batches[last]
100 if r.frame-b.last <= importTTL {
101 break
102 }
103 r.freeComposeBatch(b)
104 slot.batches[last] = nil
105 slot.batches = slot.batches[:last]
106 }
107 return slot.batches[:used], nil
108}
109
110func (r *Renderer) uploadCompose(b *composeBatch) error {
111 count := len(b.instances)
112 size := count * int(unsafe.Sizeof(instanceDraw{}))
113 if b.gpu.size < size {
114 // This slot is idle, so its previous buffer can be replaced now.
115 capacity := min(r.maxRange, max(size, b.gpu.size*2, 64*int(unsafe.Sizeof(instanceDraw{}))))
116 b.valid = false
117 r.freeGPUBuffer(&b.gpu)
118 if err := r.newGPUBuffer(&b.gpu, capacity); err != nil {
119 return err
120 }
121 }
122 copy(unsafe.Slice((*instanceDraw)(b.gpu.mapped), count), b.instances)
123 b.count = uint32(count)
124 if b.set == 0 {
125 if err := r.createComposeSet(b); err != nil {
126 return err
127 }
128 }
129 if b.valid && b.keys == b.cached {
130 return nil
131 }
132 n := int(r.compose.resources)
133 for i, key := range b.keys[:n] {
134 view, chroma, buffer := r.compose.dummyView, r.compose.dummyView, r.compose.dummyBuffer.buffer
135 if key.im != nil {
136 view = key.im.view
137 chroma = view
138 if key.im.chromaView != 0 {
139 chroma = key.im.chromaView
140 }
141 }
142 if key.shm != nil {
143 buffer = key.shm.gpu.buffer
144 }
145 b.images[i] = vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
146 b.chroma[i] = vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: chroma, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
147 b.buffers[i] = vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize}
148 }
149 b.drawBuffer = vk.DescriptorBufferInfo{Buffer: b.gpu.buffer, Range: vk.DeviceSize(b.gpu.size)}
150 b.writes = [4]vk.WriteDescriptorSet{
151 {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 0, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &b.images[0]},
152 {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 1, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &b.buffers[0]},
153 {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 2, DescriptorCount: uint32(n), DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &b.chroma[0]},
154 {SType: vk.StructureTypeWriteDescriptorSet, DstSet: b.set, DstBinding: 3, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &b.drawBuffer},
155 }
156 r.dd.UpdateDescriptorSets(r.device, uint32(len(b.writes)), &b.writes[0], 0, nil)
157 runtime.KeepAlive(b)
158 b.cached, b.valid = b.keys, true
159 return nil
160}
161
162func (r *Renderer) createComposeSet(b *composeBatch) error {
163 n := r.compose.resources
164 sizes := [2]vk.DescriptorPoolSize{
165 {Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 2 * n},
166 {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: n + 1},
167 }
168 info := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: uint32(len(sizes)), PoolSizes: &sizes[0]}
169 if err := checked("vkCreateDescriptorPool(compose)", r.dd.CreateDescriptorPool(r.device, &info, nil, &b.pool)); err != nil {
170 return err
171 }
172 alloc := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: b.pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout}
173 if err := checked("vkAllocateDescriptorSets(compose)", r.dd.AllocateDescriptorSets(r.device, &alloc, &b.set)); err != nil {
174 r.dd.DestroyDescriptorPool(r.device, b.pool, nil)
175 b.pool = 0
176 return err
177 }
178 return nil
179}
180
181// Released resources may leave unbound descriptors naming dead handles.
182// Invalidate their cached identities before destruction: the next bind
183// rewrites the entire table after its slot fence. No GPU-visible mutation
184// happens here, even if another batch in the slot is still in flight.
185func (r *Renderer) invalidateCompose(key resourceKey) {
186 for i := range r.slots {
187 for _, b := range r.slots[i].batches {
188 for j := range b.cached {
189 if b.cached[j] == key {
190 b.cached[j], b.keys[j] = resourceKey{}, resourceKey{}
191 b.valid = false
192 }
193 }
194 }
195 }
196}
197
198// trimCompose runs only after poll, and never frees an in-flight slot.
199func (r *Renderer) trimCompose(cutoff uint64) {
200 for i := range r.slots {
201 s := &r.slots[i]
202 if s.busy {
203 continue
204 }
205 for len(s.batches) > 0 {
206 last := len(s.batches) - 1
207 b := s.batches[last]
208 if b.last > cutoff || b.last >= r.frame {
209 break
210 }
211 r.freeComposeBatch(b)
212 s.batches[last] = nil
213 s.batches = s.batches[:last]
214 }
215 }
216}
217
218func (r *Renderer) freeComposeBatch(b *composeBatch) {
219 if b.pool != 0 {
220 r.dd.DestroyDescriptorPool(r.device, b.pool, nil)
221 }
222 r.freeGPUBuffer(&b.gpu)
223 *b = composeBatch{}
224}