v0.7.3 bcc9a45

image_cache.go RawHistoryFolder

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	c.policy.maxDimension = d.imageDimension
91	defer func() {
92		if err != nil {
93			c.Close()
94		}
95	}()
96	binding := vulkan.DescriptorSetLayoutBinding{DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vulkan.ShaderStageFragmentBit}
97	li := vulkan.DescriptorSetLayoutCreateInfo{SType: vulkan.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &binding}
98	if err = vulkan.Check(d.Dispatch.CreateDescriptorSetLayout(d.Logical, &li, nil, &c.Layout)); err != nil {
99		return nil, err
100	}
101	// Allocate the full cache budget once. No FreeDescriptorSets binding is needed.
102	size := vulkan.DescriptorPoolSize{Type: vulkan.DescriptorTypeCombinedImageSampler, DescriptorCount: imageDescriptorSlots}
103	pi := vulkan.DescriptorPoolCreateInfo{SType: vulkan.StructureTypeDescriptorPoolCreateInfo, MaxSets: imageDescriptorSlots, PoolSizeCount: 1, PoolSizes: &size}
104	if err = vulkan.Check(d.Dispatch.CreateDescriptorPool(d.Logical, &pi, nil, &c.Pool)); err != nil {
105		return nil, err
106	}
107	for i := range c.sets {
108		ai := vulkan.DescriptorSetAllocateInfo{SType: vulkan.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: c.Pool, DescriptorSetCount: 1, SetLayouts: &c.Layout}
109		if err = vulkan.Check(d.Dispatch.AllocateDescriptorSets(d.Logical, &ai, &c.sets[i])); err != nil {
110			return nil, err
111		}
112	}
113	sampler := vulkan.SamplerCreateInfo{SType: vulkan.StructureTypeSamplerCreateInfo, MagFilter: vulkan.FilterLinear, MinFilter: vulkan.FilterLinear, MipmapMode: vulkan.SamplerMipmapModeNearest, AddressModeU: vulkan.SamplerAddressModeClampToEdge, AddressModeV: vulkan.SamplerAddressModeClampToEdge, AddressModeW: vulkan.SamplerAddressModeClampToEdge}
114	if err = vulkan.Check(d.Dispatch.CreateSampler(d.Logical, &sampler, nil, &c.Sampler)); err != nil {
115		return nil, err
116	}
117	c.fallback, err = c.create(image.NewNRGBA(image.Rect(0, 0, 1, 1)), 0)
118	if err != nil {
119		return nil, err
120	}
121	return c, nil
122}
123
124// complete releases resources only after their final sampling fence is signaled.
125func (c *imageCache) complete(serial uint64) {
126	if serial == 0 {
127		return
128	}
129	c.done[serial] = true
130	for c.done[c.policy.completed+1] {
131		delete(c.done, c.policy.completed+1)
132		c.policy.completed++
133	}
134	c.dispose(c.policy.collect())
135}
136
137// resolve is invoked after a reusable frame slot has been selected. Upload
138// bytes live in that slot's staging buffer until its fence signals.
139// On error the frame's serial and new entries are released by abort (the
140// caller invokes it for every failure before a successful Submit).
141func (c *imageCache) resolve(f *Frame, batches []Batch) ([]Batch, error) {
142	// Completion is otherwise observed only when a slot is recorded again; a
143	// slot the compositor keeps idle would block the contiguous completed
144	// serial and pin every later texture. Poll without waiting.
145	if err := c.pollInflight((*Frame).Ready); err != nil {
146		return nil, err
147	}
148	c.serial++
149	f.imageSerial = c.serial
150	f.imageCopies = f.imageCopies[:0]
151	c.dispose(c.policy.begin(c.serial, c.policy.completed))
152	c.batches = append(c.batches[:0], batches...)
153	result := c.batches
154	var pixels []byte
155	// Validate the entire frame before creating or changing any GPU resources.
156	// In particular, do not partially record an over-budget frame.
157	seen := make(map[image.Image]bool)
158	count, bytes := 0, 0
159	for _, batch := range result {
160		key := batch.Source
161		if key == nil {
162			continue
163		}
164		if !reflect.TypeOf(key).Comparable() {
165			return nil, fmt.Errorf("image cache: image identity must be comparable")
166		}
167		if seen[key] {
168			continue
169		}
170		seen[key] = true
171		count++
172		b := key.Bounds()
173		if b.Dx() <= 0 || b.Dy() <= 0 || b.Dx() > c.policy.maxDimension || b.Dy() > c.policy.maxDimension {
174			return nil, fmt.Errorf("image cache: invalid dimensions %dx%d (maximum %d)", b.Dx(), b.Dy(), c.policy.maxDimension)
175		}
176		if e := c.policy.entries[key]; e != nil {
177			bytes += e.bytes
178		} else {
179			bytes += b.Dx() * b.Dy() * 4
180		}
181	}
182	if count > maxImageTextures || bytes > maxImageBytes {
183		return nil, fmt.Errorf("image cache: frame exceeds 64 textures/64 MiB budget")
184	}
185	for i := range result {
186		if result[i].Source == nil {
187			continue
188		}
189		key := result[i].Source
190		e, retired, err := c.policy.reserve(key)
191		c.dispose(retired)
192		if err != nil {
193			return nil, err
194		}
195		t := c.textures[e]
196		if t == nil {
197			// Track the entry before creating GPU objects so abort can drop it.
198			f.imageCopies = append(f.imageCopies, imageCopy{entry: e})
199			t, err = c.create(key, e.slot)
200			if err != nil {
201				return nil, err
202			}
203			c.textures[e] = t
204			n := image.NewNRGBA(image.Rect(0, 0, key.Bounds().Dx(), key.Bounds().Dy()))
205			draw.Draw(n, n.Bounds(), key, key.Bounds().Min, draw.Src)
206			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())}
207			pixels = append(pixels, n.Pix...)
208		}
209		result[i].Texture = &t.Texture
210	}
211	if len(pixels) > 0 {
212		if err := f.stageImages(pixels); err != nil {
213			return nil, err
214		}
215	}
216	return result, nil
217}
218
219func (c *imageCache) pollInflight(readyFn func(*Frame) (bool, error)) error {
220	for g := range c.inflight {
221		ready, err := readyFn(g)
222		if err != nil {
223			return err
224		}
225		if ready {
226			g.settleImages()
227		}
228	}
229	return nil
230}
231
232// abort undoes resolve for a frame that will not be submitted: textures
233// created for it were never uploaded, so they are destroyed and their slots
234// freed at once (no GPU work references them), and its serial is completed so
235// later fences can still release retired textures.
236func (c *imageCache) abort(f *Frame) {
237	for _, cp := range f.imageCopies {
238		if t := c.textures[cp.entry]; t != nil {
239			c.destroy(t)
240			delete(c.textures, cp.entry)
241		}
242		c.policy.drop(cp.entry)
243	}
244	f.imageCopies = f.imageCopies[:0]
245	c.complete(f.imageSerial)
246	f.imageSerial = 0
247}
248
249type imageCopy struct {
250	entry         *imageEntry
251	texture       *gpuImage
252	offset        vulkan.DeviceSize
253	width, height uint32
254}
255
256func (c *imageCache) create(source image.Image, slot int) (t *gpuImage, err error) {
257	d := c.device
258	t = &gpuImage{Texture: ImageTexture{Set: c.sets[slot]}}
259	defer func() {
260		if err != nil {
261			c.destroy(t)
262		}
263	}()
264	b := source.Bounds()
265	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}
266	if err = vulkan.Check(d.Dispatch.CreateImage(d.Logical, &ci, nil, &t.Image)); err != nil {
267		return nil, err
268	}
269	var req vulkan.MemoryRequirements
270	d.Dispatch.GetImageMemoryRequirements(d.Logical, t.Image, &req)
271	var mem vulkan.PhysicalDeviceMemoryProperties
272	d.InstanceDispatch.GetPhysicalDeviceMemoryProperties(d.Physical, &mem)
273	index := uint32(math.MaxUint32)
274	for i := uint32(0); i < mem.MemoryTypeCount; i++ {
275		if req.MemoryTypeBits&(1<<i) != 0 && mem.MemoryTypes[i].PropertyFlags&vulkan.MemoryPropertyDeviceLocalBit != 0 {
276			index = i
277			break
278		}
279	}
280	if index == math.MaxUint32 {
281		return nil, fmt.Errorf("no device-local image memory")
282	}
283	alloc := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: index}
284	if err = vulkan.Check(d.Dispatch.AllocateMemory(d.Logical, &alloc, nil, &t.Memory)); err != nil {
285		return nil, err
286	}
287	if err = vulkan.Check(d.Dispatch.BindImageMemory(d.Logical, t.Image, t.Memory, 0)); err != nil {
288		return nil, err
289	}
290	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}}
291	if err = vulkan.Check(d.Dispatch.CreateImageView(d.Logical, &view, nil, &t.View)); err != nil {
292		return nil, err
293	}
294	info := vulkan.DescriptorImageInfo{Sampler: c.Sampler, ImageView: t.View, ImageLayout: vulkan.ImageLayoutShaderReadOnlyOptimal}
295	write := vulkan.WriteDescriptorSet{SType: vulkan.StructureTypeWriteDescriptorSet, DstSet: t.Texture.Set, DescriptorCount: 1, DescriptorType: vulkan.DescriptorTypeCombinedImageSampler, ImageInfo: &info}
296	d.Dispatch.UpdateDescriptorSets(d.Logical, 1, &write, 0, nil)
297	return t, nil
298}
299
300func (f *Frame) stageImages(pixels []byte) error {
301	if f.imageStaging == nil || len(pixels) > f.imageStaging.capacity {
302		if f.imageStaging != nil {
303			f.imageStaging.Close()
304		}
305		f.imageStaging = &instanceBuffer{device: f.device}
306		b := f.imageStaging
307		ci := vulkan.BufferCreateInfo{SType: vulkan.StructureTypeBufferCreateInfo, Size: vulkan.DeviceSize(len(pixels)), Usage: vulkan.BufferUsageTransferSrcBit, SharingMode: vulkan.SharingModeExclusive}
308		if err := vulkan.Check(f.device.Dispatch.CreateBuffer(f.device.Logical, &ci, nil, &b.Buffer)); err != nil {
309			return err
310		}
311		var req vulkan.MemoryRequirements
312		f.device.Dispatch.GetBufferMemoryRequirements(f.device.Logical, b.Buffer, &req)
313		var mem vulkan.PhysicalDeviceMemoryProperties
314		f.device.InstanceDispatch.GetPhysicalDeviceMemoryProperties(f.device.Physical, &mem)
315		want := vulkan.MemoryPropertyFlags(vulkan.MemoryPropertyHostVisibleBit | vulkan.MemoryPropertyHostCoherentBit)
316		index := uint32(math.MaxUint32)
317		for i := uint32(0); i < mem.MemoryTypeCount; i++ {
318			if req.MemoryTypeBits&(1<<i) != 0 && mem.MemoryTypes[i].PropertyFlags&want == want {
319				index = i
320				break
321			}
322		}
323		if index == math.MaxUint32 {
324			return fmt.Errorf("no host-coherent image staging memory")
325		}
326		ai := vulkan.MemoryAllocateInfo{SType: vulkan.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: index}
327		if err := vulkan.Check(f.device.Dispatch.AllocateMemory(f.device.Logical, &ai, nil, &b.Memory)); err != nil {
328			return err
329		}
330		if err := vulkan.Check(f.device.Dispatch.BindBufferMemory(f.device.Logical, b.Buffer, b.Memory, 0)); err != nil {
331			return err
332		}
333		b.capacity = len(pixels)
334	}
335	var ptr unsafe.Pointer
336	if err := vulkan.Check(f.device.Dispatch.MapMemory(f.device.Logical, f.imageStaging.Memory, 0, vulkan.DeviceSize(len(pixels)), 0, &ptr)); err != nil {
337		return err
338	}
339	copy(unsafe.Slice((*byte)(ptr), len(pixels)), pixels)
340	f.device.Dispatch.UnmapMemory(f.device.Logical, f.imageStaging.Memory)
341	return nil
342}
343
344// recordFallback transitions a dedicated, never sampled fallback until the
345// first list submission. The shader statically requires set 1 on every draw.
346func (f *Frame) recordFallback() {
347	c := f.device.images
348	f.fallbackRecorded = true
349	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}}
350	dep := vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: &barrier}
351	f.device.Dispatch.CmdPipelineBarrier2(f.Commands, &dep)
352}
353
354func (f *Frame) recordImages() {
355	d := f.device.Dispatch
356	for _, copyInfo := range f.imageCopies {
357		t := copyInfo.texture
358		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}}
359		dep := vulkan.DependencyInfo{SType: vulkan.StructureTypeDependencyInfo, ImageMemoryBarrierCount: 1, ImageMemoryBarriers: &barrier}
360		d.CmdPipelineBarrier2(f.Commands, &dep)
361		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}}
362		d.CmdCopyBufferToImage(f.Commands, f.imageStaging.Buffer, t.Image, vulkan.ImageLayoutTransferDstOptimal, 1, &region)
363		barrier.OldLayout = vulkan.ImageLayoutTransferDstOptimal
364		barrier.NewLayout = vulkan.ImageLayoutShaderReadOnlyOptimal
365		barrier.SrcStageMask = vulkan.PipelineStage2TransferBit
366		barrier.SrcAccessMask = vulkan.Access2TransferWriteBit
367		barrier.DstStageMask = vulkan.PipelineStage2FragmentShaderBit
368		barrier.DstAccessMask = vulkan.Access2ShaderSampledReadBit
369		d.CmdPipelineBarrier2(f.Commands, &dep)
370	}
371}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close