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