1//go:build linux
2
3package vkdevice
4
5import (
6 "fmt"
7 "image"
8 "image/draw"
9 "math"
10 "reflect"
11 "unsafe"
12
13 "github.com/bnema/purego-vulkan/vulkan"
14)
15
16type gpuImage struct {
17 Image vulkan.Image
18 Memory vulkan.DeviceMemory
19 View vulkan.ImageView
20 Texture ImageTexture
21}
22
23type imageCache struct {
24 device *Device
25 policy *imageCachePolicy
26 Layout vulkan.DescriptorSetLayout
27 Pool vulkan.DescriptorPool
28 Sampler vulkan.Sampler
29 fallback *gpuImage
30 fallbackReady bool
31 sets [imageDescriptorSlots]vulkan.DescriptorSet
32 textures map[*imageEntry]*gpuImage
33 serial uint64
34 done map[uint64]bool
35 // inflight holds submitted frames whose image serial is not settled yet.
36 // resolve polls them so an idle frame slot cannot stall completion.
37 inflight map[*Frame]struct{}
38 // batches is the backing store of resolve's result. The caller records it
39 // before the next resolve and never retains it.
40 batches []Batch
41}
42
43func (c *imageCache) Close() {
44 if c == nil || c.device == nil {
45 return
46 }
47 for _, t := range c.textures {
48 c.destroy(t)
49 }
50 if c.fallback != nil {
51 c.destroy(c.fallback)
52 c.fallback = nil
53 }
54 d := c.device.Dispatch
55 if c.Pool != 0 {
56 d.DestroyDescriptorPool(c.device.Logical, c.Pool, nil)
57 c.Pool = 0
58 }
59 if c.Layout != 0 {
60 d.DestroyDescriptorSetLayout(c.device.Logical, c.Layout, nil)
61 c.Layout = 0
62 }
63 if c.Sampler != 0 {
64 d.DestroySampler(c.device.Logical, c.Sampler, nil)
65 c.Sampler = 0
66 }
67}
68func (c *imageCache) destroy(t *gpuImage) {
69 d := c.device.Dispatch
70 if t.View != 0 {
71 d.DestroyImageView(c.device.Logical, t.View, nil)
72 }
73 if t.Image != 0 {
74 d.DestroyImage(c.device.Logical, t.Image, nil)
75 }
76 if t.Memory != 0 {
77 d.FreeMemory(c.device.Logical, t.Memory, nil)
78 }
79}
80func (c *imageCache) dispose(entries []*imageEntry) {
81 for _, e := range entries {
82 if t := c.textures[e]; t != nil {
83 c.destroy(t)
84 delete(c.textures, e)
85 }
86 }
87}
88func (d *Device) newImageCache() (c *imageCache, err error) {
89 c = &imageCache{device: d, policy: newImageCachePolicy(), textures: make(map[*imageEntry]*gpuImage), done: make(map[uint64]bool), inflight: make(map[*Frame]struct{})}
90 defer func() {
91 if err != nil {
92 c.Close()
93 }
94 }()
95 binding := vulkan.DescriptorSetLayoutBinding{DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vulkan.ShaderStageFragmentBit}
96 li := vulkan.DescriptorSetLayoutCreateInfo{SType: vulkan.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &binding}
97 if err = vulkan.Check(d.Dispatch.CreateDescriptorSetLayout(d.Logical, &li, nil, &c.Layout)); err != nil {
98 return nil, err
99 }
100 // Allocate the full cache budget once. No FreeDescriptorSets binding is needed.
101 size := vulkan.DescriptorPoolSize{Type: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: imageDescriptorSlots}
102 pi := vulkan.DescriptorPoolCreateInfo{SType: vulkan.StructureTypeDescriptorPoolCreateInfo, MaxSets: imageDescriptorSlots, PoolSizeCount: 1, PoolSizes: &size}
103 if err = vulkan.Check(d.Dispatch.CreateDescriptorPool(d.Logical, &pi, nil, &c.Pool)); err != nil {
104 return nil, err
105 }
106 for i := range c.sets {
107 ai := vulkan.DescriptorSetAllocateInfo{SType: vulkan.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: c.Pool, DescriptorSetCount: 1, SetLayouts: &c.Layout}
108 if err = vulkan.Check(d.Dispatch.AllocateDescriptorSets(d.Logical, &ai, &c.sets[i])); err != nil {
109 return nil, err
110 }
111 }
112 sampler := vulkan.SamplerCreateInfo{SType: vulkan.StructureTypeSamplerCreateInfo, MagFilter: vulkan.FilterLinear, MinFilter: vulkan.FilterLinear, MipmapMode: vulkan.SamplerMipmapModeNearest, AddressModeU: vulkan.SamplerAddressModeClampToEdge, AddressModeV: vulkan.SamplerAddressModeClampToEdge, AddressModeW: vulkan.SamplerAddressModeClampToEdge}
113 if err = vulkan.Check(d.Dispatch.CreateSampler(d.Logical, &sampler, nil, &c.Sampler)); err != nil {
114 return nil, err
115 }
116 c.fallback, err = c.create(image.NewNRGBA(image.Rect(0, 0, 1, 1)), 0)
117 if err != nil {
118 return nil, err
119 }
120 return c, nil
121}
122
123// complete releases resources only after their final sampling fence is signaled.
124func (c *imageCache) complete(serial uint64) {
125 if serial == 0 {
126 return
127 }
128 c.done[serial] = true
129 for c.done[c.policy.completed+1] {
130 delete(c.done, c.policy.completed+1)
131 c.policy.completed++
132 }
133 c.dispose(c.policy.collect())
134}
135
136// resolve is invoked after a reusable frame slot has been selected. Upload
137// bytes live in that slot's staging buffer until its fence signals.
138// On error the frame's serial and new entries are released by abort (the
139// caller invokes it for every failure before a successful Submit).
140func (c *imageCache) resolve(f *Frame, batches []Batch) ([]Batch, error) {
141 // Completion is otherwise observed only when a slot is recorded again; a
142 // slot the compositor keeps idle would block the contiguous completed
143 // serial and pin every later texture. Poll without waiting.
144 if err := c.pollInflight((*Frame).Ready); err != nil {
145 return nil, err
146 }
147 c.serial++
148 f.imageSerial = c.serial
149 f.imageCopies = f.imageCopies[:0]
150 c.dispose(c.policy.begin(c.serial, c.policy.completed))
151 c.batches = append(c.batches[:0], batches...)
152 result := c.batches
153 var pixels []byte
154 // Validate the entire frame before creating or changing any GPU resources.
155 // In particular, do not partially record an over-budget frame.
156 seen := make(map[image.Image]bool)
157 count, bytes := 0, 0
158 for _, batch := range result {
159 key := batch.Source
160 if key == nil {
161 continue
162 }
163 if !reflect.TypeOf(key).Comparable() {
164 return nil, fmt.Errorf("image cache: image identity must be comparable")
165 }
166 if seen[key] {
167 continue
168 }
169 seen[key] = true
170 count++
171 b := key.Bounds()
172 if b.Dx() <= 0 || b.Dy() <= 0 || b.Dx() > maxImageDimension || b.Dy() > maxImageDimension {
173 return nil, fmt.Errorf("image cache: invalid dimensions %dx%d (maximum %d)", b.Dx(), b.Dy(), maxImageDimension)
174 }
175 if e := c.policy.entries[key]; e != nil {
176 bytes += e.bytes
177 } else {
178 bytes += b.Dx() * b.Dy() * 4
179 }
180 }
181 if count > maxImageTextures || bytes > maxImageBytes {
182 return nil, fmt.Errorf("image cache: frame exceeds 64 textures/64 MiB budget")
183 }
184 for i := range result {
185 if result[i].Source == nil {
186 continue
187 }
188 key := result[i].Source
189 e, retired, err := c.policy.reserve(key)
190 c.dispose(retired)
191 if err != nil {
192 return nil, err
193 }
194 t := c.textures[e]
195 if t == nil {
196 // Track the entry before creating GPU objects so abort can drop it.
197 f.imageCopies = append(f.imageCopies, imageCopy{entry: e})
198 t, err = c.create(key, e.slot)
199 if err != nil {
200 return nil, err
201 }
202 c.textures[e] = t
203 n := image.NewNRGBA(image.Rect(0, 0, key.Bounds().Dx(), key.Bounds().Dy()))
204 draw.Draw(n, n.Bounds(), key, key.Bounds().Min, draw.Src)
205 f.imageCopies[len(f.imageCopies)-1] = imageCopy{entry: e, texture: t, offset: vulkan.DeviceSize(len(pixels)), width: uint32(n.Rect.Dx()), height: uint32(n.Rect.Dy())}
206 pixels = append(pixels, n.Pix...)
207 }
208 result[i].Texture = &t.Texture
209 }
210 if len(pixels) > 0 {
211 if err := f.stageImages(pixels); err != nil {
212 return nil, err
213 }
214 }
215 return result, nil
216}
217
218func (c *imageCache) pollInflight(readyFn func(*Frame) (bool, error)) error {
219 for g := range c.inflight {
220 ready, err := readyFn(g)
221 if err != nil {
222 return err
223 }
224 if ready {
225 g.settleImages()
226 }
227 }
228 return nil
229}
230
231// abort undoes resolve for a frame that will not be submitted: textures
232// created for it were never uploaded, so they are destroyed and their slots
233// freed at once (no GPU work references them), and its serial is completed so
234// later fences can still release retired textures.
235func (c *imageCache) abort(f *Frame) {
236 for _, cp := range f.imageCopies {
237 if t := c.textures[cp.entry]; t != nil {
238 c.destroy(t)
239 delete(c.textures, cp.entry)
240 }
241 c.policy.drop(cp.entry)
242 }
243 f.imageCopies = f.imageCopies[:0]
244 c.complete(f.imageSerial)
245 f.imageSerial = 0
246}
247
248type imageCopy struct {
249 entry *imageEntry
250 texture *gpuImage
251 offset vulkan.DeviceSize
252 width, height uint32
253}
254
255func (c *imageCache) create(source image.Image, slot int) (t *gpuImage, err error) {
256 d := c.device
257 t = &gpuImage{Texture: ImageTexture{Set: c.sets[slot]}}
258 defer func() {
259 if err != nil {
260 c.destroy(t)
261 }
262 }()
263 b := source.Bounds()
264 ci := vulkan.ImageCreateInfo{SType: vulkan.StructureTypeImageCreateInfo, ImageType: vulkan.ImageType2d, Format: vulkan.FormatR8g8b8a8Unorm, Extent: vulkan.Extent3D{Width: uint32(b.Dx()), Height: uint32(b.Dy()), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vulkan.SampleCount1Bit, Tiling: vulkan.ImageTilingOptimal, Usage: vulkan.ImageUsageSampledBit | vulkan.ImageUsageTransferDstBit, SharingMode: vulkan.SharingModeExclusive, InitialLayout: vulkan.ImageLayoutUndefined}
265 if err = vulkan.Check(d.Dispatch.CreateImage(d.Logical, &ci, nil, &t.Image)); err != nil {
266 return nil, err
267 }
268 var req vulkan.MemoryRequirements
269 d.Dispatch.GetImageMemoryRequirements(d.Logical, t.Image, &req)
270 var mem vulkan.PhysicalDeviceMemoryProperties
271 d.InstanceDispatch.GetPhysicalDeviceMemoryProperties(d.Physical, &mem)
272 index := uint32(math.MaxUint32)
273 for i := uint32(0); i < mem.MemoryTypeCount; i++ {
274 if req.MemoryTypeBits&(1<<i) != 0 && mem.MemoryTypes[i].PropertyFlags&vulkan.MemoryPropertyDeviceLocalBit != 0 {
275 index = i
276 break
277 }
278 }
279 if index == math.MaxUint32 {
280 return nil, fmt.Errorf("no device-local image memory")
281 }
282 alloc := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: index}
283 if err = vulkan.Check(d.Dispatch.AllocateMemory(d.Logical, &alloc, nil, &t.Memory)); err != nil {
284 return nil, err
285 }
286 if err = vulkan.Check(d.Dispatch.BindImageMemory(d.Logical, t.Image, t.Memory, 0)); err != nil {
287 return nil, err
288 }
289 view := vulkan.ImageViewCreateInfo{SType: vulkan.StructureTypeImageViewCreateInfo, Image: t.Image, ViewType: vulkan.ImageViewType2d, Format: vulkan.FormatR8g8b8a8Unorm, SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}}
290 if err = vulkan.Check(d.Dispatch.CreateImageView(d.Logical, &view, nil, &t.View)); err != nil {
291 return nil, err
292 }
293 info := vulkan.DescriptorImageInfo{Sampler: c.Sampler, ImageView: t.View, ImageLayout: vulkan.ImageLayoutShaderReadOnlyOptimal}
294 write := vulkan.WriteDescriptorSet{SType: vulkan.StructureTypeWriteDescriptorSet, DstSet: t.Texture.Set, DescriptorCount: 1, DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, ImageInfo: &info}
295 d.Dispatch.UpdateDescriptorSets(d.Logical, 1, &write, 0, nil)
296 return t, nil
297}
298
299func (f *Frame) stageImages(pixels []byte) error {
300 if f.imageStaging == nil || len(pixels) > f.imageStaging.capacity {
301 if f.imageStaging != nil {
302 f.imageStaging.Close()
303 }
304 f.imageStaging = &instanceBuffer{device: f.device}
305 b := f.imageStaging
306 ci := vulkan.BufferCreateInfo{SType: vulkan.StructureTypeBufferCreateInfo, Size: vulkan.DeviceSize(len(pixels)), Usage: vulkan.BufferUsageTransferSrcBit, SharingMode: vulkan.SharingModeExclusive}
307 if err := vulkan.Check(f.device.Dispatch.CreateBuffer(f.device.Logical, &ci, nil, &b.Buffer)); err != nil {
308 return err
309 }
310 var req vulkan.MemoryRequirements
311 f.device.Dispatch.GetBufferMemoryRequirements(f.device.Logical, b.Buffer, &req)
312 var mem vulkan.PhysicalDeviceMemoryProperties
313 f.device.InstanceDispatch.GetPhysicalDeviceMemoryProperties(f.device.Physical, &mem)
314 want := vulkan.MemoryPropertyFlags(vulkan.MemoryPropertyHostVisibleBit | vulkan.MemoryPropertyHostCoherentBit)
315 index := uint32(math.MaxUint32)
316 for i := uint32(0); i < mem.MemoryTypeCount; i++ {
317 if req.MemoryTypeBits&(1<<i) != 0 && mem.MemoryTypes[i].PropertyFlags&want == want {
318 index = i
319 break
320 }
321 }
322 if index == math.MaxUint32 {
323 return fmt.Errorf("no host-coherent image staging memory")
324 }
325 ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: index}
326 if err := vulkan.Check(f.device.Dispatch.AllocateMemory(f.device.Logical, &ai, nil, &b.Memory)); err != nil {
327 return err
328 }
329 if err := vulkan.Check(f.device.Dispatch.BindBufferMemory(f.device.Logical, b.Buffer, b.Memory, 0)); err != nil {
330 return err
331 }
332 b.capacity = len(pixels)
333 }
334 var ptr unsafe.Pointer
335 if err := vulkan.Check(f.device.Dispatch.MapMemory(f.device.Logical, f.imageStaging.Memory, 0, vulkan.DeviceSize(len(pixels)), 0, &ptr)); err != nil {
336 return err
337 }
338 copy(unsafe.Slice((*byte)(ptr), len(pixels)), pixels)
339 f.device.Dispatch.UnmapMemory(f.device.Logical, f.imageStaging.Memory)
340 return nil
341}
342
343// recordFallback transitions a dedicated, never sampled fallback until the
344// first list submission. The shader statically requires set 1 on every draw.
345func (f *Frame) recordFallback() {
346 c := f.device.images
347 f.fallbackRecorded = true
348 barrier := vulkan.ImageMemoryBarrier2{SType: vulkan.StructureTypeImageMemoryBarrier2, SrcStageMask: vulkan.PipelineStage2None, DstStageMask: vulkan.PipelineStage2FragmentShaderBit, DstAccessMask: vulkan.Access2ShaderSampledReadBit, OldLayout: vulkan.ImageLayoutUndefined, NewLayout: vulkan.ImageLayoutShaderReadOnlyOptimal, SrcQueueFamilyIndex: ^uint32(0), DstQueueFamilyIndex: ^uint32(0), Image: c.fallback.Image, SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}}
349 dep := vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: &barrier}
350 f.device.Dispatch.CmdPipelineBarrier2(f.Commands, &dep)
351}
352
353func (f *Frame) recordImages() {
354 d := f.device.Dispatch
355 for _, copyInfo := range f.imageCopies {
356 t := copyInfo.texture
357 barrier := vulkan.ImageMemoryBarrier2{SType: vulkan.StructureTypeImageMemoryBarrier2, SrcStageMask: vulkan.PipelineStage2None, DstStageMask: vulkan.PipelineStage2TransferBit, DstAccessMask: vulkan.Access2TransferWriteBit, OldLayout: vulkan.ImageLayoutUndefined, NewLayout: vulkan.ImageLayoutTransferDstOptimal, SrcQueueFamilyIndex: ^uint32(0), DstQueueFamilyIndex: ^uint32(0), Image: t.Image, SubresourceRange: vulkan.ImageSubresourceRange{AspectMask: vulkan.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}}
358 dep := vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: &barrier}
359 d.CmdPipelineBarrier2(f.Commands, &dep)
360 region := vulkan.BufferImageCopy{BufferOffset: copyInfo.offset, ImageSubresource: vulkan.ImageSubresourceLayers{AspectMask: vulkan.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vulkan.Extent3D{Width: copyInfo.width, Height: copyInfo.height, Depth: 1}}
361 d.CmdCopyBufferToImage(f.Commands, f.imageStaging.Buffer, t.Image, vulkan.ImageLayoutTransferDstOptimal, 1, ®ion)
362 barrier.OldLayout = vulkan.ImageLayoutTransferDstOptimal
363 barrier.NewLayout = vulkan.ImageLayoutShaderReadOnlyOptimal
364 barrier.SrcStageMask = vulkan.PipelineStage2TransferBit
365 barrier.SrcAccessMask = vulkan.Access2TransferWriteBit
366 barrier.DstStageMask = vulkan.PipelineStage2FragmentShaderBit
367 barrier.DstAccessMask = vulkan.Access2ShaderSampledReadBit
368 d.CmdPipelineBarrier2(f.Commands, &dep)
369 }
370}