v0.4.0 b6be3a8

dmabuf.go RawHistoryFolder

1package vulkan
2
3import (
4	"fmt"
5	"os"
6	"slices"
7	"unsafe"
8
9	"github.com/bnema/neferwl/internal/ports"
10	vk "github.com/bnema/purego-vulkan/vulkan"
11	"golang.org/x/sys/unix"
12)
13
14// dmabuf import (linux-dmabuf clients): each client buffer becomes a
15// VkImage bound to its imported memory, cached by buffer ID while the
16// client keeps attaching it, and sampled by the compose shader. Sync is
17// implicit: before sampling, the renderer waits on the fences the client's
18// driver attached to the buffer (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), imported
19// as a semaphore.
20
21// deviceExtensions are enabled when all present; without them the renderer
22// has no dmabuf support and clients fall back to wl_shm.
23var deviceExtensions = []string{
24	vk.KHRExternalMemoryFDExtensionName,
25	vk.EXTExternalMemoryDMABUFExtensionName,
26	vk.EXTImageDRMFormatModifierExtensionName,
27	vk.EXTQueueFamilyForeignExtensionName,
28	vk.KHRExternalSemaphoreFDExtensionName,
29	// The render node advertised to clients in dmabuf feedback.
30	vk.EXTPhysicalDeviceDRMExtensionName,
31}
32
33// fourcc → Vulkan format. DRM formats name the packed pixel from the high
34// byte, little-endian: ARGB8888 is B,G,R,A in memory, like B8G8R8A8.
35var dmabufFormats = []struct {
36	fourcc uint32
37	format vk.Format
38	opaque bool
39}{
40	{fourcc('A', 'R', '2', '4'), vk.FormatB8g8r8a8Unorm, false},
41	{fourcc('X', 'R', '2', '4'), vk.FormatB8g8r8a8Unorm, true},
42	{fourcc('A', 'B', '2', '4'), vk.FormatR8g8b8a8Unorm, false},
43	{fourcc('X', 'B', '2', '4'), vk.FormatR8g8b8a8Unorm, true},
44	// Vulkan's A2R10G10B10/A2B10G10R10 packed UNORM formats match
45	// DRM's little-endian 2101010 layout; do not use an sRGB view for PQ.
46	{fourcc('A', 'R', '3', '0'), vk.FormatA2r10g10b10UnormPack32, false},
47	{fourcc('X', 'R', '3', '0'), vk.FormatA2r10g10b10UnormPack32, true},
48	{fourcc('A', 'B', '3', '0'), vk.FormatA2b10g10r10UnormPack32, false},
49	{fourcc('X', 'B', '3', '0'), vk.FormatA2b10g10r10UnormPack32, true},
50	// Little-endian ABGR16161616F is RGBA16F; floating-point values
51	// are linear electrical signals, not sRGB-encoded.
52	{fourcc('A', 'B', '4', 'H'), vk.FormatR16g16b16a16Sfloat, false},
53	{fourcc('X', 'B', '4', 'H'), vk.FormatR16g16b16a16Sfloat, true},
54	{fourcc('N', 'V', '1', '2'), vk.Format(1000156003), true}, // VK_FORMAT_G8_B8R8_2PLANE_420_UNORM
55	{fourcc('P', '0', '1', '0'), vk.Format(1000156013), true}, // VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16
56}
57
58func isYUVFormat(f uint32) bool {
59	return f == fourcc('N', 'V', '1', '2') || f == fourcc('P', '0', '1', '0')
60}
61
62func fourcc(a, b, c, d byte) uint32 {
63	return uint32(a) | uint32(b)<<8 | uint32(c)<<16 | uint32(d)<<24
64}
65
66func vkFormat(f uint32) (vk.Format, bool, bool) {
67	for _, v := range dmabufFormats {
68		if v.fourcc == f {
69			return v.format, v.opaque, true
70		}
71	}
72	return 0, false, false
73}
74
75// extensions lists the device extensions supported by the selected GPU.
76func (r *Renderer) extensions(physical vk.PhysicalDevice) map[string]bool {
77	var n uint32
78	if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, nil) != vk.Success || n == 0 {
79		return nil
80	}
81	props := make([]vk.ExtensionProperties, n)
82	if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, &props[0]) != vk.Success {
83		return nil
84	}
85	have := map[string]bool{}
86	for _, p := range props[:n] {
87		have[cstring(p.ExtensionName[:])] = true
88	}
89	return have
90}
91
92func cstring(b []byte) string {
93	if i := slices.Index(b, 0); i >= 0 {
94		b = b[:i]
95	}
96	return string(b)
97}
98
99// probeDMABuf lists the sampled fourcc/modifier pairs supported by the GPU.
100// Multi-planar formats require mutable per-plane views; disjoint binding is
101// optional and only used when the device supports it and planes differ.
102func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
103	var sup ports.DMABufSupport
104	drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT}
105	props := vk.PhysicalDeviceProperties2{SType: vk.StructureTypePhysicalDeviceProperties2, Next: unsafe.Pointer(&drm)}
106	r.id.GetPhysicalDeviceProperties2(physical, &props)
107	if drm.HasRender != 0 {
108		sup.Device = unix.Mkdev(uint32(drm.RenderMajor), uint32(drm.RenderMinor))
109	}
110	for _, f := range dmabufFormats {
111		list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
112		fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
113		r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)
114		if list.DrmFormatModifierCount == 0 {
115			continue
116		}
117		mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
118		list.DrmFormatModifierProperties = &mods[0]
119		r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)
120		for _, m := range mods[:list.DrmFormatModifierCount] {
121			need := vk.FormatFeatureFlags(vk.FormatFeatureSampledImageBit | vk.FormatFeatureSampledImageFilterLinearBit)
122			planes := uint32(1)
123			if isYUVFormat(f.fourcc) {
124				planes = 2
125			}
126			if m.DrmFormatModifierPlaneCount != planes || m.DrmFormatModifierTilingFeatures&need != need {
127				continue
128			}
129			if r.importable(physical, f.format, m.DrmFormatModifier, isYUVFormat(f.fourcc), false) {
130				sup.Formats = append(sup.Formats, ports.DMABufFormat{Format: f.fourcc, Modifier: m.DrmFormatModifier})
131			}
132		}
133	}
134	return sup
135}
136
137// importable asks whether an image of that format and modifier can be
138// imported from a dmabuf.
139func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64, yuv, disjoint bool) bool {
140	mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
141	ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
142	flags := vk.ImageCreateFlags(0)
143	if yuv {
144		flags = vk.ImageCreateMutableFormatBit
145		if disjoint {
146			flags |= imageDisjoint
147			// Image-format queries alone do not imply the modifier supports
148			// disjoint plane bindings. Check its tiling features as well.
149			list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
150			properties := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
151			r.id.GetPhysicalDeviceFormatProperties2(physical, format, &properties)
152			if list.DrmFormatModifierCount == 0 {
153				return false
154			}
155			mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
156			list.DrmFormatModifierProperties = &mods[0]
157			r.id.GetPhysicalDeviceFormatProperties2(physical, format, &properties)
158			found := false
159			for _, m := range mods[:list.DrmFormatModifierCount] {
160				if m.DrmFormatModifier == modifier && m.DrmFormatModifierTilingFeatures&0x00400000 != 0 {
161					found = true
162					break
163				}
164			}
165			if !found {
166				return false
167			}
168		}
169		views := yuvViewFormats(format)
170		formats := imageFormatList{sType: structureImageFormatList, viewFormatCount: 3, viewFormats: &views[0]}
171		mod.Next = unsafe.Pointer(&formats)
172		info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Flags: flags, Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit}
173		extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
174		out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
175		return r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) == vk.Success && extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureImportableBit != 0
176	}
177	info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Flags: flags, Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit}
178	extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
179	out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
180	if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
181		return false
182	}
183	return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureImportableBit != 0
184}
185
186// DMABuf reports the formats this renderer imports.
187func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
188
189// imported is a client dmabuf bound to a VkImage, with the view and set
190// the compose shader samples it through.
191type imported struct {
192	image      vk.Image
193	memory     vk.DeviceMemory
194	view       vk.ImageView
195	chromaView vk.ImageView
196	memories   [2]vk.DeviceMemory
197	fds        [2]int
198	yuv        bool
199	// disjoint is available for this format/modifier; required only when
200	// the two planes refer to different underlying DMA-BUF objects.
201	disjoint bool
202	pool     vk.DescriptorPool
203	set      vk.DescriptorSet
204	fd       int // our duplicate of plane 0, for implicit sync
205	// last is the frame that drew it, for eviction.
206	last uint64
207}
208
209// importTTL is how many frames an import survives undrawn: clients cycle
210// through 2-4 buffers, and a destroyed buffer is freed shortly after.
211const importTTL = 120
212
213// importDMABuf returns the image of a client buffer, importing it once.
214func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
215	if im := r.imports[b.ID]; im != nil {
216		im.last = r.frame
217		return im, nil
218	}
219	if len(r.dmabuf.Formats) == 0 {
220		return nil, fmt.Errorf("dmabuf unsupported")
221	}
222	format, _, ok := vkFormat(b.Format)
223	if !ok || b.Width <= 0 || b.Height <= 0 {
224		return nil, fmt.Errorf("dmabuf format %#x with %d planes unsupported", b.Format, len(b.Planes))
225	}
226	if isYUVFormat(b.Format) {
227		if len(b.Planes) != 2 || b.Width%2 != 0 || b.Height%2 != 0 {
228			return nil, fmt.Errorf("invalid YUV buffer geometry or planes")
229		}
230		return r.importYUV(b, format)
231	}
232	if len(b.Planes) != 1 {
233		return nil, fmt.Errorf("RGB buffer needs one plane")
234	}
235	fd, err := dupFile(b.Planes[0].File)
236	if err != nil {
237		return nil, err
238	}
239	im := &imported{fd: fd, last: r.frame}
240	ok = false
241	defer func() {
242		if !ok {
243			r.release(im)
244		}
245	}()
246	d := r.dd
247	layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}
248	explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}
249	external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
250	ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, Next: unsafe.Pointer(&external), ImageType: vk.ImageType2d, Format: format, Extent: vk.Extent3D{Width: uint32(b.Width), Height: uint32(b.Height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
251	if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {
252		return nil, err
253	}
254	fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
255	if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
256		return nil, err
257	}
258	var req vk.MemoryRequirements
259	d.GetImageMemoryRequirements(r.device, im.image, &req)
260	kind, err := r.findMemoryType(req.MemoryTypeBits&fdProps.MemoryTypeBits, 0)
261	if err != nil {
262		return nil, err
263	}
264	// Vulkan owns the fd it imports on success: hand it its own duplicate.
265	memFD, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
266	if err != nil {
267		return nil, err
268	}
269	dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: im.image}
270	imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(memFD)}
271	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
272	if err := checked("vkAllocateMemory(dmabuf)", d.AllocateMemory(r.device, &alloc, nil, &im.memory)); err != nil {
273		unix.Close(memFD)
274		return nil, err
275	}
276	if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {
277		return nil, err
278	}
279	if im.view, err = r.imageView(im.image, format); err != nil {
280		return nil, err
281	}
282	if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil {
283		return nil, err
284	}
285	ok = true
286	r.imports[b.ID] = im
287	return im, nil
288}
289
290// dupFile duplicates the descriptor while the file is held open, so a close
291// on the wayland goroutine cannot swap it for a reused number.
292func dupFile(f *os.File) (int, error) {
293	if f == nil {
294		return -1, fmt.Errorf("dmabuf plane without file")
295	}
296	raw, err := f.SyscallConn()
297	if err != nil {
298		return -1, err
299	}
300	fd, dupErr := -1, error(nil)
301	if err := raw.Control(func(v uintptr) { fd, dupErr = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }); err != nil {
302		return -1, err
303	}
304	return fd, dupErr
305}
306
307func (r *Renderer) release(im *imported) {
308	if im.pool != 0 {
309		r.dd.DestroyDescriptorPool(r.device, im.pool, nil)
310	}
311	if im.view != 0 {
312		r.dd.DestroyImageView(r.device, im.view, nil)
313	}
314	if im.chromaView != 0 {
315		r.dd.DestroyImageView(r.device, im.chromaView, nil)
316	}
317	if im.image != 0 {
318		r.dd.DestroyImage(r.device, im.image, nil)
319	}
320	if im.memory != 0 {
321		r.dd.FreeMemory(r.device, im.memory, nil)
322	}
323	for i := range im.memories {
324		if im.memories[i] != 0 {
325			r.dd.FreeMemory(r.device, im.memories[i], nil)
326		}
327	}
328	if im.fd >= 0 {
329		unix.Close(im.fd)
330	}
331	if im.yuv {
332		for _, fd := range im.fds {
333			if fd >= 0 {
334				unix.Close(fd)
335			}
336		}
337	}
338}
339
340// dropUnused frees imports not drawn for importTTL frames (the client
341// moved on to other buffers or went away), once the last frame that drew
342// them completed.
343func (r *Renderer) dropUnused() {
344	for id, im := range r.imports {
345		if r.frame-im.last > importTTL {
346			r.retire(im.last, func() { r.release(im) })
347			delete(r.imports, id)
348		}
349	}
350}
351
352// dmaBufSync is struct dma_buf_export_sync_file.
353type dmaBufSync struct {
354	flags uint32
355	fd    int32
356}
357
358// DMA_BUF_IOCTL_EXPORT_SYNC_FILE = _IOWR('b', 2, struct dma_buf_export_sync_file)
359const ioctlExportSyncFile = 0xc0086202
360const dmaBufSyncRead = 1
361
362// appendReadFences waits on every distinct backing object's pending writes.
363// Shared YUV planes have one implicit fence; disjoint planes have two.
364func (r *Renderer) appendReadFences(waits []vk.Semaphore, im *imported) []vk.Semaphore {
365	fds := [2]int{im.fd}
366	count := 1
367	if im.yuv {
368		fds = im.fds
369		if im.disjoint {
370			count = 2
371		}
372	}
373	for _, fd := range fds[:count] {
374		arg := dmaBufSync{flags: dmaBufSyncRead, fd: -1}
375		if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {
376			continue // Older kernels cannot export implicit fences.
377		}
378		if sem := r.importSyncFD(int(arg.fd)); sem != 0 {
379			waits = append(waits, sem)
380		}
381	}
382	return waits
383}
384
385// importFence makes a wait semaphore of a sync file. The file stays the
386// caller's: Vulkan takes a duplicate, made while the file is held open.
387func (r *Renderer) importFence(f *os.File) vk.Semaphore {
388	fd, err := dupFile(f)
389	if err != nil {
390		return 0
391	}
392	return r.importSyncFD(fd)
393}
394
395// importSyncFD imports a sync file fd as a temporary semaphore payload;
396// the fd is Vulkan's on success and closed on failure.
397func (r *Renderer) importSyncFD(fd int) vk.Semaphore {
398	var sem vk.Semaphore
399	si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
400	if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {
401		unix.Close(fd)
402		return 0
403	}
404	imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: int32(fd)}
405	if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {
406		unix.Close(fd)
407		r.dd.DestroySemaphore(r.device, sem, nil)
408		return 0
409	}
410	return sem
411}
412
413// Probe reports the dmabuf formats of the device renderers use, or none
414// when Vulkan or the extensions are missing.
415func Probe() ports.DMABufSupport {
416	r, err := New(1, 1)
417	if err != nil {
418		return ports.DMABufSupport{}
419	}
420	defer r.Close()
421	return r.DMABuf()
422}

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 25

Tags 10

↑↓ moveenter openesc close